Add initial Exposure Notification API implementation

This commit is contained in:
Marvin W 2020-08-03 18:07:06 +02:00
parent af28a78bba
commit 5f70d943cb
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
92 changed files with 4921 additions and 19 deletions

View File

@ -14,6 +14,7 @@ buildscript {
ext.annotationVersion = '1.1.0'
ext.appcompatVersion = '1.1.0'
ext.coreVersion = '1.3.0'
ext.fragmentVersion = '1.2.5'
ext.lifecycleVersion = '2.2.0'
ext.mediarouterVersion = '1.1.0'

View File

@ -19,3 +19,7 @@ wire {
compileKotlin {
kotlinOptions.jvmTarget = 1.8
}
compileTestKotlin {
kotlinOptions.jvmTarget = 1.8
}

View File

@ -26,6 +26,7 @@ configurations {
dependencies {
implementation "com.squareup.wire:wire-runtime:$wireVersion"
implementation "de.hdodenhof:circleimageview:1.3.0"
implementation "com.diogobernardino:williamchart:3.7.1"
implementation "org.conscrypt:conscrypt-android:2.1.0"
// TODO: Switch to upstream once raw requests are merged
// https://github.com/vitalidze/chromecast-java-api-v2/pull/99
@ -40,6 +41,7 @@ dependencies {
implementation project(':firebase-dynamic-links-api')
implementation project(':play-services-base-core')
implementation project(':play-services-location-core')
implementation project(':play-services-nearby-core')
implementation project(':play-services-core-proto')
implementation project(':play-services-core:microg-ui-tools') // deprecated
implementation project(':play-services-api')

View File

@ -107,6 +107,8 @@
android:name="android.permission.UPDATE_APP_OPS_STATS"
tools:ignore="ProtectedPermissions" />
<uses-sdk tools:overrideLibrary="com.db.williamchart" />
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
@ -419,6 +421,15 @@
<!-- microG custom UI -->
<activity
android:name="org.microg.gms.ui.ExposureNotificationsConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar">
<intent-filter>
<action android:name="org.microg.gms.nearby.exposurenotification.CONFIRM" />
</intent-filter>
</activity>
<!-- microG Settings shown in Launcher -->
<activity
android:name="org.microg.gms.ui.SettingsActivity"
@ -427,11 +438,11 @@
android:roundIcon="@mipmap/ic_microg_settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<action android:name="com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

View File

@ -1,5 +1,7 @@
package org.microg.gms.ui;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
@ -11,6 +13,8 @@ import androidx.navigation.ui.NavigationUI;
import com.google.android.gms.R;
import org.microg.gms.nearby.exposurenotification.Constants;
public class SettingsActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
@ -21,6 +25,12 @@ public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (Constants.ACTION_EXPOSURE_NOTIFICATION_SETTINGS.equals(intent.getAction()) && intent.getData() == null) {
intent.setData(Uri.parse("x-gms-settings://exposure-notifications"));
}
setContentView(R.layout.settings_root_activity);
appBarConfiguration = new AppBarConfiguration.Builder(getNavController().getGraph()).build();

View File

@ -1,5 +1,6 @@
package org.microg.gms.ui;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
@ -10,6 +11,7 @@ import com.google.android.gms.R;
import org.microg.gms.checkin.CheckinPrefs;
import org.microg.gms.gcm.GcmDatabase;
import org.microg.gms.gcm.GcmPrefs;
import org.microg.gms.nearby.exposurenotification.ExposurePreferences;
import org.microg.gms.snet.SafetyNetPrefs;
import org.microg.tools.ui.ResourceSettingsFragment;
@ -20,6 +22,7 @@ public class SettingsFragment extends ResourceSettingsFragment {
public static final String PREF_SNET = "pref_snet";
public static final String PREF_UNIFIEDNLP = "pref_unifiednlp";
public static final String PREF_CHECKIN = "pref_checkin";
public static final String PREF_EXPOSURE = "pref_exposure";
public SettingsFragment() {
preferencesResource = R.xml.preferences_start;
@ -86,6 +89,20 @@ public class SettingsFragment extends ResourceSettingsFragment {
NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.openUnifiedNlpSettings);
return true;
});
if (Build.VERSION.SDK_INT >= 21) {
findPreference(PREF_EXPOSURE).setVisible(true);
if (new ExposurePreferences(getContext()).getScannerEnabled()) {
findPreference(PREF_EXPOSURE).setSummary(getString(R.string.service_status_enabled_short));
} else {
findPreference(PREF_EXPOSURE).setSummary(R.string.service_status_disabled_short);
}
findPreference(PREF_EXPOSURE).setOnPreferenceClickListener(preference -> {
NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.openExposureNotificationSettings);
return true;
});
} else {
findPreference(PREF_EXPOSURE).setVisible(false);
}
boolean checkinEnabled = CheckinPrefs.get(getContext()).isEnabled();
findPreference(PREF_CHECKIN).setSummary(checkinEnabled ? R.string.service_status_enabled_short : R.string.service_status_disabled_short);

View File

@ -6,15 +6,21 @@
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.widget.ImageView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
class AppIconPreference(context: Context) : Preference(context) {
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
class AppIconPreference : Preference {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val icon = holder?.findViewById(android.R.id.icon)
val icon = holder.findViewById(android.R.id.icon)
if (icon is ImageView) {
icon.adjustViewBounds = true
icon.scaleType = ImageView.ScaleType.CENTER_INSIDE

View File

@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.db.williamchart.data.Scale
import com.db.williamchart.view.BarChartView
import com.google.android.gms.R
class BarChartPreference : Preference {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?) : super(context)
init {
layoutResource = R.layout.preference_bar_chart
}
private lateinit var chart: BarChartView
var labelsFormatter: (Float) -> String = { it.toString() }
set(value) {
field = value
if (this::chart.isInitialized) {
chart.labelsFormatter = value
}
}
var scale: Scale? = null
set(value) {
field = value
if (value != null && this::chart.isInitialized) {
chart.scale = value
}
}
var data: LinkedHashMap<String, Float> = linkedMapOf()
set(value) {
field = value
if (this::chart.isInitialized) {
chart.animate(data)
}
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
chart = holder.itemView as? BarChartView ?: holder.findViewById(R.id.bar_chart) as BarChartView
chart.labelsFormatter = labelsFormatter
scale?.let { chart.scale = it }
chart.animate(data)
}
}

View File

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.R
import com.google.android.gms.databinding.ExposureNotificationsAppFragmentBinding
import com.google.android.gms.databinding.ExposureNotificationsFragmentBinding
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsAppFragment : Fragment(R.layout.exposure_notifications_app_fragment) {
private lateinit var binding: ExposureNotificationsAppFragmentBinding
val packageName: String?
get() = arguments?.getString("package")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = ExposureNotificationsAppFragmentBinding.inflate(inflater, container, false)
binding.callbacks = object : ExposureNotificationsAppFragmentCallbacks {
override fun onAppClicked() {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri: Uri = Uri.fromParts("package", packageName, null)
intent.data = uri
context!!.startActivity(intent)
}
}
childFragmentManager.findFragmentById(R.id.sub_preferences)?.arguments = arguments
return binding.root
}
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
val pm = requireContext().packageManager
val applicationInfo = pm.getApplicationInfoIfExists(packageName)
binding.appName = applicationInfo?.loadLabel(pm)?.toString() ?: packageName
binding.appIcon = applicationInfo?.loadIcon(pm)
?: AppCompatResources.getDrawable(requireContext(), android.R.mipmap.sym_def_app_icon)
}
}
}
interface ExposureNotificationsAppFragmentCallbacks {
fun onAppClicked()
}

View File

@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var open: Preference
private lateinit var checks: Preference
private val packageName: String?
get() = arguments?.getString("package")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications_app)
}
override fun onBindPreferences() {
open = preferenceScreen.findPreference("pref_exposure_app_open") ?: open
checks = preferenceScreen.findPreference("pref_exposure_app_checks") ?: checks
open.onPreferenceClickListener = Preference.OnPreferenceClickListener {
try {
packageName?.let {
context?.packageManager?.getLaunchIntentForPackage(it)?.let { intent ->
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context?.startActivity(intent)
}
}
} catch (ignored: Exception) {
}
true
}
}
override fun onResume() {
super.onResume()
updateContent()
}
fun updateContent() {
packageName?.let { packageName ->
val database = ExposureDatabase(requireContext())
var str = getString(R.string.pref_exposure_app_checks_summary, database.countMethodCalls(packageName, "provideDiagnosisKeys"))
val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys")
if (lastCheckTime != null && lastCheckTime != 0L) {
str += "\n" + getString(R.string.pref_exposure_app_last_check_summary, DateUtils.getRelativeDateTimeString(context, lastCheckTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))
}
checks.summary = str
database.close()
}
}
}

View File

@ -0,0 +1,68 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.os.ResultReceiver
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.R
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import org.microg.gms.nearby.exposurenotification.*
class ExposureNotificationsConfirmActivity : AppCompatActivity() {
private var resultCode: Int = FAILED
private val resultData: Bundle = Bundle()
private val receiver: ResultReceiver?
get() = intent.getParcelableExtra(KEY_CONFIRM_RECEIVER)
private val action: String?
get() = intent.getStringExtra(KEY_CONFIRM_ACTION)
private val targetPackageName: String?
get() = intent.getStringExtra(KEY_CONFIRM_PACKAGE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.exposure_notifications_confirm_activity)
val applicationInfo = packageManager.getApplicationInfoIfExists(targetPackageName)
when (action) {
CONFIRM_ACTION_START -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_start_title)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_start_summary, applicationInfo?.loadLabel(packageManager)
?: targetPackageName)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_start_button)
}
CONFIRM_ACTION_STOP -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_stop_title)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_stop_summary)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_stop_button)
}
CONFIRM_ACTION_KEYS -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_keys_title, applicationInfo?.loadLabel(packageManager)
?: targetPackageName)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_keys_summary)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_keys_button)
}
else -> {
resultCode = INTERNAL_ERROR
finish()
}
}
findViewById<Button>(android.R.id.button1).setOnClickListener {
resultCode = SUCCESS
finish()
}
findViewById<Button>(android.R.id.button2).setOnClickListener {
resultCode = FAILED_REJECTED_OPT_IN
finish()
}
}
override fun onStop() {
super.onStop()
receiver?.send(resultCode, resultData)
}
}

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.R
import com.google.android.gms.databinding.ExposureNotificationsFragmentBinding
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsFragment : Fragment(R.layout.exposure_notifications_fragment) {
private lateinit var binding: ExposureNotificationsFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = ExposureNotificationsFragmentBinding.inflate(inflater, container, false)
binding.switchBarCallback = object : PreferenceSwitchBarCallback {
override fun onChecked(newStatus: Boolean) {
ExposurePreferences(requireContext()).scannerEnabled = newStatus
binding.scannerEnabled = newStatus
}
}
return binding.root
}
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
binding.scannerEnabled = ExposurePreferences(requireContext()).scannerEnabled
}
}
}

View File

@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.os.Handler
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureEnableInfo: Preference
private lateinit var exposureApps: PreferenceCategory
private lateinit var exposureAppsNone: Preference
private lateinit var collectedRpis: Preference
private lateinit var advertisingId: Preference
private lateinit var database: ExposureDatabase
private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database = ExposureDatabase(requireContext())
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications)
}
override fun onBindPreferences() {
exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo
exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps
exposureAppsNone = preferenceScreen.findPreference("pref_exposure_apps_none") ?: exposureAppsNone
collectedRpis = preferenceScreen.findPreference("pref_exposure_collected_rpis") ?: collectedRpis
advertisingId = preferenceScreen.findPreference("pref_exposure_advertising_id") ?: advertisingId
collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(R.id.openExposureRpis)
true
}
}
override fun onResume() {
super.onResume()
updateStatus()
updateContent()
}
override fun onPause() {
super.onPause()
database.close()
handler.removeCallbacks(updateStatusRunnable)
handler.removeCallbacks(updateContentRunnable)
}
private fun updateStatus() {
lifecycleScope.launchWhenResumed {
handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL)
val preferences = ExposurePreferences(requireContext())
exposureEnableInfo.isVisible = !preferences.scannerEnabled
advertisingId.isVisible = preferences.advertiserEnabled
}
}
private fun updateContent() {
lifecycleScope.launchWhenResumed {
handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL)
val context = requireContext()
val (apps, lastHourKeys, currentId) = withContext(Dispatchers.IO) {
val apps = database.appList.map { packageName ->
context.packageManager.getApplicationInfoIfExists(packageName)
}.filterNotNull().mapIndexed { idx, applicationInfo ->
val pref = AppIconPreference(context)
pref.order = idx
pref.title = applicationInfo.loadLabel(context.packageManager)
pref.icon = applicationInfo.loadIcon(context.packageManager)
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(R.id.openExposureAppDetails, bundleOf(
"package" to applicationInfo.packageName
))
true
}
pref.key = "pref_exposure_app_" + applicationInfo.packageName
pref
}
val lastHourKeys = database.hourRpiCount
val currentId = database.currentRpiId
database.close()
Triple(apps, lastHourKeys, currentId)
}
collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys)
advertisingId.summary = currentId.toString()
exposureApps.removeAll()
if (apps.isEmpty()) {
exposureApps.addPreference(exposureAppsNone)
} else {
for (app in apps) {
exposureApps.addPreference(app)
}
}
}
}
companion object {
private const val UPDATE_STATUS_INTERVAL = 1000L
private const val UPDATE_CONTENT_INTERVAL = 60000L
}
}

View File

@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.annotation.TargetApi
import android.os.Bundle
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.db.williamchart.data.Scale
import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
import java.util.*
import kotlin.math.roundToInt
@TargetApi(21)
class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
private lateinit var histogramCategory: PreferenceCategory
private lateinit var histogram: BarChartPreference
private lateinit var database: ExposureDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database = ExposureDatabase(requireContext())
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications_rpis)
}
override fun onBindPreferences() {
histogramCategory = preferenceScreen.findPreference("prefcat_exposure_rpi_histogram") ?: histogramCategory
histogram = preferenceScreen.findPreference("pref_exposure_rpi_histogram") ?: histogram
}
override fun onResume() {
super.onResume()
updateChart()
}
override fun onPause() {
super.onPause()
database.close()
}
fun updateChart() {
lifecycleScope.launchWhenResumed {
val (totalRpiCount, rpiHistogram) = withContext(Dispatchers.IO) {
val map = linkedMapOf<String, Float>()
val lowestDate = Math.round((Date().time / 24 / 60 / 60 / 1000 - 13).toDouble()) * 24 * 60 * 60 * 1000
for (i in 0..13) {
val date = Calendar.getInstance().apply { this.time = Date(lowestDate + i * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
map[date.toString()] = 0f
}
for (entry in database.rpiHistogram) {
val time = Date(entry.key * 24 * 60 * 60 * 1000)
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
map[date.toString()] = entry.value.toFloat()
}
val totalRpiCount = database.totalRpiCount
database.close()
totalRpiCount to map
}
histogramCategory.title = getString(R.string.prefcat_exposure_rpis_histogram_title, totalRpiCount)
histogram.labelsFormatter = { it.roundToInt().toString() }
histogram.scale = Scale(0f, rpiHistogram.values.max() ?: 0f)
histogram.data = rpiHistogram
}
}
}

View File

@ -34,7 +34,7 @@ class PushNotificationAppFragment : Fragment(R.layout.push_notification_fragment
val uri: Uri = Uri.fromParts("package", packageName, null)
intent.data = uri
try {
context!!.startActivity(intent)
requireContext().startActivity(intent)
} catch (e: Exception) {
Log.w(TAG, "Failed to launch app", e)
}

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.LinearLayout
import android.widget.TextView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
class TextPreference : Preference {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
super.onBindViewHolder(holder)
val iconFrame = holder?.findViewById(androidx.preference.R.id.icon_frame)
iconFrame?.layoutParams?.height = MATCH_PARENT
(iconFrame as? LinearLayout)?.gravity = Gravity.TOP or Gravity.START
val pad = (context.resources.displayMetrics.densityDpi/160f * 20).toInt()
iconFrame?.setPadding(0, pad, 0, pad)
val textView = holder?.findViewById(android.R.id.summary) as? TextView
textView?.maxLines = Int.MAX_VALUE
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, The Android Open Source Project
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M14,10H2V12H14V10M14,6H2V8H14V6M2,16H10V14H2V16M21.5,11.5L23,13L16,20L11.5,15.5L13,14L16,17L21.5,11.5Z" />
</vector>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2019, The Android Open Source Project
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
</vector>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorAccent"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12 0.5C11.03 0.5 10.25 1.28 10.25 2.25C10.25 2.84 10.55 3.37 11 3.68V5.08C10.1 5.21 9.26 5.5 8.5 5.94L7.39 4.35C7.58 3.83 7.53 3.23 7.19 2.75C6.84 2.26 6.3 2 5.75 2C5.4 2 5.05 2.1 4.75 2.32C3.96 2.87 3.76 3.96 4.32 4.75C4.66 5.24 5.2 5.5 5.75 5.5L6.93 7.18C6.5 7.61 6.16 8.09 5.87 8.62C5.67 8.54 5.46 8.5 5.25 8.5C4.8 8.5 4.35 8.67 4 9C3.33 9.7 3.33 10.8 4 11.5C4.29 11.77 4.64 11.92 5 12L5 12C5 12.54 5.07 13.06 5.18 13.56L3.87 13.91C3.56 13.65 3.16 13.5 2.75 13.5C2.6 13.5 2.44 13.5 2.29 13.56C1.36 13.81 0.809 14.77 1.06 15.71C1.27 16.5 2 17 2.75 17C2.9 17 3.05 17 3.21 16.94C3.78 16.78 4.21 16.36 4.39 15.84L5.9 15.43C6.35 16.22 6.95 16.92 7.65 17.5L6.55 19.5C6 19.58 5.5 19.89 5.21 20.42C4.75 21.27 5.07 22.33 5.92 22.79C6.18 22.93 6.47 23 6.75 23C7.37 23 7.97 22.67 8.29 22.08C8.57 21.56 8.56 20.96 8.31 20.47L9.38 18.5C10.19 18.82 11.07 19 12 19C12.06 19 12.12 19 12.18 19C12.05 19.26 12 19.56 12 19.88C12.08 20.8 12.84 21.5 13.75 21.5C13.79 21.5 13.84 21.5 13.88 21.5C14.85 21.42 15.57 20.58 15.5 19.62C15.46 19.12 15.21 18.68 14.85 18.39C15.32 18.18 15.77 17.91 16.19 17.6L18.53 19.94C18.43 20.5 18.59 21.07 19 21.5C19.35 21.83 19.8 22 20.25 22S21.15 21.83 21.5 21.5C22.17 20.8 22.17 19.7 21.5 19C21.15 18.67 20.7 18.5 20.25 18.5C20.15 18.5 20.05 18.5 19.94 18.53L17.6 16.19C18.09 15.54 18.47 14.8 18.71 14H19.82C20.13 14.45 20.66 14.75 21.25 14.75C22.22 14.75 23 13.97 23 13S22.22 11.25 21.25 11.25C20.66 11.25 20.13 11.55 19.82 12H19C19 10.43 18.5 9 17.6 7.81L18.94 6.47C19.05 6.5 19.15 6.5 19.25 6.5C19.7 6.5 20.15 6.33 20.5 6C21.17 5.31 21.17 4.2 20.5 3.5C20.15 3.17 19.7 3 19.25 3S18.35 3.17 18 3.5C17.59 3.93 17.43 4.5 17.53 5.06L16.19 6.4C15.27 5.71 14.19 5.25 13 5.08V3.68C13.45 3.37 13.75 2.84 13.75 2.25C13.75 1.28 12.97 0.5 12 0.5M12 17C9.24 17 7 14.76 7 12S9.24 7 12 7 17 9.24 17 12 14.76 17 12 17M10.5 9C9.67 9 9 9.67 9 10.5S9.67 12 10.5 12 12 11.33 12 10.5 11.33 9 10.5 9M14 13C13.45 13 13 13.45 13 14C13 14.55 13.45 15 14 15C14.55 15 15 14.55 15 14C15 13.45 14.55 13 14 13Z" />
</vector>

View File

@ -28,7 +28,7 @@
layout="@layout/preference_switch_bar"
app:callback="@{switchBarCallback}"
app:checked="@{checkinEnabled}"
app:description='@{"Register device"}'
app:description='@{@string/checkin_enable_switch}'
app:enabled="@{true}" />
<androidx.fragment.app.FragmentContainerView

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="appName"
type="String" />
<variable
name="appIcon"
type="android.graphics.drawable.Drawable" />
<variable
name="callbacks"
type="org.microg.gms.ui.ExposureNotificationsAppFragmentCallbacks" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:paddingStart="?attr/listPreferredItemPaddingStart"
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
android:paddingTop="24dp"
android:paddingEnd="?attr/listPreferredItemPaddingEnd"
android:paddingRight="?attr/listPreferredItemPaddingRight"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:gravity="center_horizontal"
android:onClick='@{() -> callbacks.onAppClicked()}'
android:orientation="vertical">
<ImageView
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
android:antialias="true"
android:scaleType="fitCenter"
android:src="@{appIcon}"
tools:src="@android:mipmap/sym_def_app_icon" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:ellipsize="marquee"
android:gravity="center"
android:singleLine="false"
android:text='@{appName}'
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
tools:text="@tools:sample/lorem" />
</LinearLayout>
</RelativeLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/sub_preferences"
android:name="org.microg.gms.ui.ExposureNotificationsAppPreferencesFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
</layout>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@android:id/title"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingTop="24dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:textColor="?android:attr/textColorPrimary"
tools:text="@string/exposure_confirm_start_title" />
<TextView
android:id="@android:id/summary"
style="@style/TextAppearance.AppCompat.Small"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
tools:text="Your phone needs to use Bluetooth to securely collect and share IDs with other phones that are nearby.\n\nCorona Warn can notify you if you were exposed to someone who reported to be diagnosed positive.\n\nThe date, duration, and signal strength associated with an exposure will be shared with the app." />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:paddingLeft="8dp"
android:paddingTop="0dp"
android:paddingRight="8dp"
android:paddingBottom="8dp">
<Button
android:id="@android:id/button2"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<Button
android:id="@android:id/button1"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="scannerEnabled"
type="boolean" />
<variable
name="switchBarCallback"
type="org.microg.gms.ui.PreferenceSwitchBarCallback" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include
layout="@layout/preference_switch_bar"
app:callback="@{switchBarCallback}"
app:checked="@{scannerEnabled}"
app:description='@{@string/exposure_enable_switch}'
app:enabled="@{scannerEnabled}" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/sub_preferences"
android:name="org.microg.gms.ui.ExposureNotificationsPreferencesFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
</layout>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<com.db.williamchart.view.BarChartView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/bar_chart"
android:layout_width="match_parent"
android:layout_height="180dp"
android:padding="16dp"
app:chart_barsColor="?attr/colorAccent"
app:chart_barsRadius="5dp"
app:chart_labelsColor="?android:attr/textColorSecondary"
app:chart_labelsSize="14sp"
app:chart_spacing="10dp" />

View File

@ -32,7 +32,7 @@
layout="@layout/preference_switch_bar"
app:callback="@{switchBarCallback}"
app:checked="@{gcmEnabled}"
app:description='@{"Receive push notifications"}'
app:description='@{@string/gcm_enable_switch}'
app:enabled="@{checkinEnabled}" />
<androidx.fragment.app.FragmentContainerView

View File

@ -32,7 +32,7 @@
layout="@layout/preference_switch_bar"
app:callback="@{switchBarCallback}"
app:checked="@{safetynetEnabled}"
app:description='@{"Allow device attestation"}'
app:description='@{@string/snet_enable_switch}'
app:enabled="@{checkinEnabled}" />
<androidx.fragment.app.FragmentContainerView

View File

@ -51,6 +51,13 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/openExposureNotificationSettings"
app:destination="@id/exposureNotificationsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/openAbout"
app:destination="@id/aboutFragment"
@ -124,7 +131,8 @@
<fragment
android:id="@+id/safetyNetFragment"
android:name="org.microg.gms.ui.SafetyNetFragment"
android:label="@string/service_name_snet">
android:label="@string/service_name_snet"
tools:layout="@layout/safety_net_fragment">
<action
android:id="@+id/openSafetyNetAdvancedSettings"
app:destination="@id/safetyNetAdvancedFragment"
@ -145,8 +153,49 @@
<include app:graph="@navigation/nav_unlp" />
<fragment
android:id="@+id/exposureNotificationsFragment"
android:name="org.microg.gms.ui.ExposureNotificationsFragment"
android:label="@string/service_name_exposure"
tools:layout="@layout/exposure_notifications_fragment">
<deepLink
app:action="com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS"
app:uri="x-gms-settings://exposure-notifications" />
<action
android:id="@+id/openExposureRpis"
app:destination="@id/exposureNotificationsRpisFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/openExposureAppDetails"
app:destination="@id/exposureNotificationsAppFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/exposureNotificationsRpisFragment"
android:name="org.microg.gms.ui.ExposureNotificationsRpisFragment"
android:label="@string/pref_exposure_collected_rpis_title" />
<fragment
android:id="@+id/exposureNotificationsAppFragment"
android:name="org.microg.gms.ui.ExposureNotificationsAppFragment"
android:label="@string/service_name_exposure"
tools:layout="@layout/exposure_notifications_app_fragment">
<argument
android:name="package"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/aboutFragment"
android:name="org.microg.gms.ui.AboutFragment"
android:label="@string/prefcat_about" />
android:label="@string/prefcat_about"
tools:layout="@layout/about_root" />
</navigation>

View File

@ -58,6 +58,7 @@ This can take a couple of minutes."</string>
<string name="service_name_checkin">Google device registration</string>
<string name="service_name_mcs">Google Cloud Messaging</string>
<string name="service_name_snet">Google SafetyNet</string>
<string name="service_name_exposure">Exposure Notifications</string>
<string name="service_status_disabled">Disabled</string>
<string name="service_status_enabled">Enabled</string>
@ -70,6 +71,8 @@ This can take a couple of minutes."</string>
<string name="list_no_item_none">None</string>
<string name="list_item_see_all">See all</string>
<string name="open_app">Open</string>
<string name="games_title">Google Play Games</string>
<string name="games_info_title"><xliff:g example="F-Droid">%1$s</xliff:g> would like to use Play Games</string>
<string name="games_info_content">To use Play Games it is required to install the Google Play Games app. The application might continue without Play Games, but it is possible that it will behave unexpectedly.</string>
@ -140,6 +143,40 @@ This can take a couple of minutes."</string>
<string name="checkin_not_registered">Not registered</string>
<string name="checkin_last_registration">Last registration: <xliff:g example="Yesterday, 02:20 PM">%1$s</xliff:g></string>
<string name="checkin_enable_switch">Register device</string>
<string name="pref_exposure_enable_info_summary">To enable Exposure Notifications, open any app supporting it.</string>
<string name="prefcat_exposure_apps_title">Apps using Exposure Notifications</string>
<string name="pref_exposure_collected_rpis_title">Collected IDs</string>
<string name="pref_exposure_collected_rpis_summary"><xliff:g example="63">%1$d</xliff:g> IDs in last hour</string>
<string name="pref_exposure_advertising_id_title">Currently broadcasted ID</string>
<string name="pref_exposure_app_checks_summary"><xliff:g example="5">%1$d</xliff:g> checks in past 14 days</string>
<string name="pref_exposure_app_last_check_summary">Last check: <xliff:g example="3 hours ago">%1$s</xliff:g></string>
<string name="prefcat_exposure_rpis_histogram_title"><xliff:g example="230">%1$d</xliff:g> IDs collected</string>
<string name="pref_exposure_rpi_delete_all_title">Delete all collected IDs</string>
<string name="pref_exposure_info_summary">"Exposure Notifications API allows apps to notify you if you were exposed to someone who reported to be diagnosed positive.
The date, duration, and signal strength associated with an exposure will be shared with the corresponding app."</string>
<string name="pref_exposure_rpis_details_summary">"While Exposure Notification API is enabled, your device passively collects IDs (called Rolling Proximity Identifiers, or RPIs) from nearby devices.
When device owners report to be diagnosed positive, their IDs can be shared. Your device checks if any of the known diagnosed IDs matches any of the collected IDs and calculates your infection risk."</string>
<string name="exposure_enable_switch">Use Exposure Notifications</string>
<string name="exposure_confirm_start_title">Turn on Exposure Notifications?</string>
<string name="exposure_confirm_start_summary">"Your phone needs to use Bluetooth to securely collect and share IDs with other phones that are nearby.
<xliff:g example="Corona-Warn">%1$s</xliff:g> can notify you if you were exposed to someone who reported to be diagnosed positive.
The date, duration, and signal strength associated with an exposure will be shared with the app."</string>
<string name="exposure_confirm_start_button">Turn on</string>
<string name="exposure_confirm_stop_title">Turn off Exposure Notifications?</string>
<string name="exposure_confirm_stop_summary">After disabling Exposure Notifications, you will no longer be notified when you were exposed to someone who reported to be diagnosed positive.</string>
<string name="exposure_confirm_stop_button">Turn off</string>
<string name="exposure_confirm_keys_title">Share your IDs with <xliff:g example="Corona-Warn">%1$s</xliff:g>?</string>
<string name="exposure_confirm_keys_summary">"Your IDs from the last 14 days will be used to help notify others that you&apos;ve been near about potential exposure.
Your identity or test result won&apos;t be shared with other people."</string>
<string name="exposure_confirm_keys_button">Share</string>
<string name="pref_info_status">Status</string>
<string name="pref_more_settings">More</string>
@ -172,6 +209,7 @@ This can take a couple of minutes."</string>
<string name="gcm_messages_counter">Messages: <xliff:g example="123">%1$d</xliff:g> (<xliff:g example="12345">%2$d</xliff:g> bytes)</string>
<string name="gcm_network_state_disconnected">Disconnected</string>
<string name="gcm_network_state_connected">Connected since <xliff:g example="2 hours ago">%1$s</xliff:g></string>
<string name="gcm_enable_switch">Receive push notifications</string>
<string name="pref_push_app_allow_register_title">Allow registration</string>
<string name="pref_push_app_allow_register_summary">Allow the app to register for push notifications.</string>
@ -183,6 +221,7 @@ This can take a couple of minutes."</string>
<string name="prefcat_push_networks_title">Networks to use for push notifications</string>
<string name="snet_intro">Google SafetyNet is a device certification system, ensuring that the device is properly secured and compatible with Android CTS. Some applications use SafetyNet for security reasons or as a prerequisite for tamper-protection.\n\nmicroG GmsCore contains a free implementation of SafetyNet, but the official server requires SafetyNet requests to be signed using the proprietary DroidGuard system. A sandboxed version of DroidGuard is available as a separate “DroidGuard Helper” app.</string>
<string name="snet_enable_switch">Allow device attestation</string>
<string name="pref_snet_testdrive_title">Try SafetyNet attestation</string>

View File

@ -16,6 +16,11 @@
<resources>
<style name="Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="LoginBlueTheme" parent="LoginBlueTheme.Base" />
<style name="LoginBlueTheme.Base" parent="Theme.AppCompat.Light">

View File

@ -16,7 +16,7 @@
tools:summary="Last registration: 13 hours ago" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
<org.microg.gms.ui.TextPreference
android:icon="@drawable/ic_info_outline"
android:selectable="false"
android:summary="@string/pref_checkin_enable_summary" />

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<Preference
android:key="pref_exposure_enable_info"
android:selectable="false"
android:summary="@string/pref_exposure_enable_info_summary"
app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" />
<PreferenceCategory
android:key="prefcat_exposure_apps"
android:title="@string/prefcat_exposure_apps_title">
<Preference
android:enabled="false"
android:key="pref_exposure_apps_none"
android:title="@string/list_no_item_none" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:key="pref_exposure_collected_rpis"
android:title="@string/pref_exposure_collected_rpis_title"
tools:summary="@string/pref_exposure_collected_rpis_summary" />
<Preference
android:key="pref_exposure_advertising_id"
android:selectable="false"
android:title="@string/pref_exposure_advertising_id_title"
tools:summary="9a799d68-925f-4c0c-a73c-b418f22a1250" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<org.microg.gms.ui.TextPreference
android:icon="@drawable/ic_info_outline"
android:selectable="false"
android:summary="@string/pref_exposure_info_summary" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:icon="@drawable/ic_open"
android:key="pref_exposure_app_open"
android:title="@string/open_app" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:key="pref_exposure_app_checks"
android:selectable="false"
tools:summary="7 checks in past 14 days\nLast check: 3 hours ago" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory
android:key="prefcat_exposure_rpi_histogram"
tools:title="@string/prefcat_exposure_rpis_histogram_title">
<org.microg.gms.ui.BarChartPreference
android:key="pref_exposure_rpi_histogram"
tools:layout="@layout/preference_bar_chart" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:enabled="false"
android:key="pref_exposure_rpi_delete_all"
android:title="@string/pref_exposure_rpi_delete_all_title" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<org.microg.gms.ui.TextPreference
android:icon="@drawable/ic_info_outline"
android:selectable="false"
android:summary="@string/pref_exposure_rpis_details_summary" />
</PreferenceCategory>
</PreferenceScreen>

View File

@ -14,8 +14,7 @@
<Preference
android:enabled="false"
android:key="pref_push_apps_none"
android:title="@string/list_no_item_none"
tools:isPreferenceVisible="true" />
android:title="@string/list_no_item_none" />
<Preference
android:icon="@drawable/ic_expand_apps"
android:key="pref_push_apps_all"
@ -32,7 +31,7 @@
tools:summary="Connected since 15 minutes ago" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
<org.microg.gms.ui.TextPreference
android:icon="@drawable/ic_info_outline"
android:selectable="false"
android:summary="@string/pref_gcm_enable_mcs_summary" />

View File

@ -18,7 +18,7 @@
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<org.microg.tools.ui.LongTextPreference
<org.microg.gms.ui.TextPreference
android:icon="@drawable/ic_info_outline"
android:key="pref_snet_summary"
android:selectable="false"

View File

@ -62,6 +62,10 @@
android:icon="@drawable/ic_map_marker"
android:key="pref_unifiednlp"
android:title="@string/nlp_backends_title" />
<Preference
android:icon="@drawable/ic_virus_outline"
android:key="pref_exposure"
android:title="@string/service_name_exposure" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference

View File

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
dependencies {
api project(':play-services-basement')
api project(':play-services-base-api')
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest package="org.microg.gms.nearby.api"/>

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable ExposureInformation;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable ExposureSummary;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable TemporaryExposureKey;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetExposureInformationParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetExposureSummaryParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetTemporaryExposureKeyHistoryParams;

View File

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
interface IBooleanCallback {
void onResult(in Status status, boolean result);
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.ExposureInformation;
interface IExposureInformationListCallback {
void onResult(in Status status, in List<ExposureInformation> result);
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.ExposureSummary;
interface IExposureSummaryCallback {
void onResult(in Status status, in ExposureSummary result);
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.nearby.exposurenotification.internal.StartParams;
import com.google.android.gms.nearby.exposurenotification.internal.StopParams;
import com.google.android.gms.nearby.exposurenotification.internal.IsEnabledParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetTemporaryExposureKeyHistoryParams;
import com.google.android.gms.nearby.exposurenotification.internal.ProvideDiagnosisKeysParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureSummaryParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureInformationParams;
interface INearbyExposureNotificationService{
void start(in StartParams params) = 0;
void stop(in StopParams params) = 1;
void isEnabled(in IsEnabledParams params) = 2;
void getTemporaryExposureKeyHistory(in GetTemporaryExposureKeyHistoryParams params) = 3;
void provideDiagnosisKeys(in ProvideDiagnosisKeysParams params) = 4;
void getExposureSummary(in GetExposureSummaryParams params) = 6;
void getExposureInformation(in GetExposureInformationParams params) = 7;
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey;
interface ITemporaryExposureKeyListCallback {
void onResult(in Status status, in List<TemporaryExposureKey> result);
}

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable IsEnabledParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable ProvideDiagnosisKeysParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable StartParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable StopParams;

View File

@ -0,0 +1,208 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Arrays;
public class ExposureConfiguration extends AutoSafeParcelable {
@Field(1)
private int minimumRiskScore;
@Field(2)
private int[] attenuationScores;
@Field(3)
private int attenuationWeight;
@Field(4)
private int[] daysSinceLastExposureScores;
@Field(5)
private int daysSinceLastExposureWeight;
@Field(6)
private int[] durationScores;
@Field(7)
private int durationWeight;
@Field(8)
private int[] transmissionRiskScores;
@Field(9)
private int transmissionRiskWeight;
@Field(10)
private int[] durationAtAttenuationThresholds;
private ExposureConfiguration() {
}
ExposureConfiguration(int minimumRiskScore, int[] attenuationScores, int attenuationWeight, int[] daysSinceLastExposureScores, int daysSinceLastExposureWeight, int[] durationScores, int durationWeight, int[] transmissionRiskScores, int transmissionRiskWeight, int[] durationAtAttenuationThresholds) {
this.minimumRiskScore = minimumRiskScore;
this.attenuationScores = attenuationScores;
this.attenuationWeight = attenuationWeight;
this.daysSinceLastExposureScores = daysSinceLastExposureScores;
this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
this.durationScores = durationScores;
this.durationWeight = durationWeight;
this.transmissionRiskScores = transmissionRiskScores;
this.transmissionRiskWeight = transmissionRiskWeight;
this.durationAtAttenuationThresholds = durationAtAttenuationThresholds;
}
public int getMinimumRiskScore() {
return minimumRiskScore;
}
public int[] getAttenuationScores() {
return Arrays.copyOf(attenuationScores, attenuationScores.length);
}
public int getAttenuationWeight() {
return attenuationWeight;
}
public int[] getDaysSinceLastExposureScores() {
return Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
}
public int getDaysSinceLastExposureWeight() {
return daysSinceLastExposureWeight;
}
public int[] getDurationScores() {
return Arrays.copyOf(durationScores, durationScores.length);
}
public int getDurationWeight() {
return durationWeight;
}
public int[] getTransmissionRiskScores() {
return Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
}
public int getTransmissionRiskWeight() {
return transmissionRiskWeight;
}
public int[] getDurationAtAttenuationThresholds() {
return Arrays.copyOf(durationAtAttenuationThresholds, durationAtAttenuationThresholds.length);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExposureConfiguration that = (ExposureConfiguration) o;
if (minimumRiskScore != that.minimumRiskScore) return false;
if (attenuationWeight != that.attenuationWeight) return false;
if (daysSinceLastExposureWeight != that.daysSinceLastExposureWeight) return false;
if (durationWeight != that.durationWeight) return false;
if (transmissionRiskWeight != that.transmissionRiskWeight) return false;
if (!Arrays.equals(attenuationScores, that.attenuationScores)) return false;
if (!Arrays.equals(daysSinceLastExposureScores, that.daysSinceLastExposureScores)) return false;
if (!Arrays.equals(durationScores, that.durationScores)) return false;
if (!Arrays.equals(transmissionRiskScores, that.transmissionRiskScores)) return false;
return Arrays.equals(durationAtAttenuationThresholds, that.durationAtAttenuationThresholds);
}
@Override
public int hashCode() {
int result = minimumRiskScore;
result = 31 * result + Arrays.hashCode(attenuationScores);
result = 31 * result + attenuationWeight;
result = 31 * result + Arrays.hashCode(daysSinceLastExposureScores);
result = 31 * result + daysSinceLastExposureWeight;
result = 31 * result + Arrays.hashCode(durationScores);
result = 31 * result + durationWeight;
result = 31 * result + Arrays.hashCode(transmissionRiskScores);
result = 31 * result + transmissionRiskWeight;
result = 31 * result + Arrays.hashCode(durationAtAttenuationThresholds);
return result;
}
@Override
public String toString() {
return "ExposureConfiguration{" +
"minimumRiskScore=" + minimumRiskScore +
", attenuationScores=" + Arrays.toString(attenuationScores) +
", attenuationWeight=" + attenuationWeight +
", daysSinceLastExposureScores=" + Arrays.toString(daysSinceLastExposureScores) +
", daysSinceLastExposureWeight=" + daysSinceLastExposureWeight +
", durationScores=" + Arrays.toString(durationScores) +
", durationWeight=" + durationWeight +
", transmissionRiskScores=" + Arrays.toString(transmissionRiskScores) +
", transmissionRiskWeight=" + transmissionRiskWeight +
", durationAtAttenuationThresholds=" + Arrays.toString(durationAtAttenuationThresholds) +
'}';
}
public static class ExposureConfigurationBuilder {
private int minimumRiskScore = 4;
private int[] attenuationScores = new int[]{4, 4, 4, 4, 4, 4, 4, 4};
private int attenuationWeight = 50;
private int[] daysSinceLastExposureScores = new int[]{4, 4, 4, 4, 4, 4, 4, 4};
private int daysSinceLastExposureWeight = 50;
private int[] durationScores = new int[]{4, 4, 4, 4, 4, 4, 4, 4};
private int durationWeight = 50;
private int[] transmissionRiskScores = new int[]{4, 4, 4, 4, 4, 4, 4, 4};
private int transmissionRiskWeight = 50;
private int[] durationAtAttenuationThresholds = new int[]{50, 74};
public ExposureConfigurationBuilder setMinimumRiskScore(int minimumRiskScore) {
this.minimumRiskScore = minimumRiskScore;
return this;
}
public ExposureConfigurationBuilder setAttenuationScores(int... attenuationScores) {
this.attenuationScores = Arrays.copyOf(attenuationScores, attenuationScores.length);
return this;
}
public ExposureConfigurationBuilder setAttenuationWeight(int attenuationWeight) {
this.attenuationWeight = attenuationWeight;
return this;
}
public ExposureConfigurationBuilder setDaysSinceLastExposureScores(int... daysSinceLastExposureScores) {
this.daysSinceLastExposureScores = Arrays.copyOf(daysSinceLastExposureScores, daysSinceLastExposureScores.length);
return this;
}
public ExposureConfigurationBuilder setDaysSinceLastExposureWeight(int daysSinceLastExposureWeight) {
this.daysSinceLastExposureWeight = daysSinceLastExposureWeight;
return this;
}
public ExposureConfigurationBuilder setDurationScores(int... durationScores) {
this.durationScores = Arrays.copyOf(durationScores, durationScores.length);
return this;
}
public ExposureConfigurationBuilder setDurationWeight(int durationWeight) {
this.durationWeight = durationWeight;
return this;
}
public ExposureConfigurationBuilder setTransmissionRiskScores(int... transmissionRiskScores) {
this.transmissionRiskScores = Arrays.copyOf(transmissionRiskScores, transmissionRiskScores.length);
return this;
}
public ExposureConfigurationBuilder setTransmissionRiskWeight(int transmissionRiskWeight) {
this.transmissionRiskWeight = transmissionRiskWeight;
return this;
}
public ExposureConfigurationBuilder setDurationAtAttenuationThresholds(int... durationAtAttenuationThresholds) {
this.durationAtAttenuationThresholds = Arrays.copyOf(durationAtAttenuationThresholds, durationAtAttenuationThresholds.length);
return this;
}
public ExposureConfiguration build() {
return new ExposureConfiguration(minimumRiskScore, attenuationScores, attenuationWeight, daysSinceLastExposureScores, daysSinceLastExposureWeight, durationScores, durationWeight, transmissionRiskScores, transmissionRiskWeight, durationAtAttenuationThresholds);
}
}
public static final Creator<ExposureConfiguration> CREATOR = new AutoCreator<>(ExposureConfiguration.class);
}

View File

@ -0,0 +1,152 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Arrays;
import java.util.Date;
public class ExposureInformation extends AutoSafeParcelable {
@Field(1)
private long dateMillisSinceEpoch;
@Field(2)
private int durationMinutes;
@Field(3)
private int attenuationValue;
@Field(4)
@RiskLevel
private int transmissionRiskLevel;
@Field(5)
private int totalRiskScore;
@Field(6)
private int[] attenuationDurationsInMinutes;
private ExposureInformation() {
}
ExposureInformation(long dateMillisSinceEpoch, int durationMinutes, int attenuationValue, @RiskLevel int transmissionRiskLevel, int totalRiskScore, int[] attenuationDurationsInMinutes) {
this.dateMillisSinceEpoch = dateMillisSinceEpoch;
this.durationMinutes = durationMinutes;
this.attenuationValue = attenuationValue;
this.transmissionRiskLevel = transmissionRiskLevel;
this.totalRiskScore = totalRiskScore;
this.attenuationDurationsInMinutes = attenuationDurationsInMinutes;
}
public long getDateMillisSinceEpoch() {
return dateMillisSinceEpoch;
}
public Date getDate() {
return new Date(dateMillisSinceEpoch);
}
public int getDurationMinutes() {
return durationMinutes;
}
public int getAttenuationValue() {
return attenuationValue;
}
@RiskLevel
public int getTransmissionRiskLevel() {
return transmissionRiskLevel;
}
public int getTotalRiskScore() {
return totalRiskScore;
}
public int[] getAttenuationDurationsInMinutes() {
return Arrays.copyOf(attenuationDurationsInMinutes, attenuationDurationsInMinutes.length);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExposureInformation that = (ExposureInformation) o;
if (dateMillisSinceEpoch != that.dateMillisSinceEpoch) return false;
if (durationMinutes != that.durationMinutes) return false;
if (attenuationValue != that.attenuationValue) return false;
if (transmissionRiskLevel != that.transmissionRiskLevel) return false;
if (totalRiskScore != that.totalRiskScore) return false;
return Arrays.equals(attenuationDurationsInMinutes, that.attenuationDurationsInMinutes);
}
@Override
public int hashCode() {
int result = (int) (dateMillisSinceEpoch ^ (dateMillisSinceEpoch >>> 32));
result = 31 * result + durationMinutes;
result = 31 * result + attenuationValue;
result = 31 * result + transmissionRiskLevel;
result = 31 * result + totalRiskScore;
result = 31 * result + Arrays.hashCode(attenuationDurationsInMinutes);
return result;
}
@Override
public String toString() {
return "ExposureInformation{" +
"date=" + getDate() +
", durationMinutes=" + durationMinutes +
", attenuationValue=" + attenuationValue +
", transmissionRiskLevel=" + transmissionRiskLevel +
", totalRiskScore=" + totalRiskScore +
", attenuationDurationsInMinutes=" + Arrays.toString(attenuationDurationsInMinutes) +
'}';
}
public static class ExposureInformationBuilder {
private long dateMillisSinceEpoch;
private int durationMinutes;
private int attenuationValue;
@RiskLevel
private int transmissionRiskLevel;
private int totalRiskScore;
private int[] attenuationDurations = new int[]{0, 0};
public ExposureInformationBuilder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
this.dateMillisSinceEpoch = dateMillisSinceEpoch;
return this;
}
public ExposureInformationBuilder setDurationMinutes(int durationMinutes) {
this.durationMinutes = durationMinutes;
return this;
}
public ExposureInformationBuilder setAttenuationValue(int attenuationValue) {
this.attenuationValue = attenuationValue;
return this;
}
public ExposureInformationBuilder setTransmissionRiskLevel(@RiskLevel int transmissionRiskLevel) {
this.transmissionRiskLevel = transmissionRiskLevel;
return this;
}
public ExposureInformationBuilder setTotalRiskScore(int totalRiskScore) {
this.totalRiskScore = totalRiskScore;
return this;
}
public ExposureInformationBuilder setAttenuationDurations(int[] attenuationDurations) {
this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
return this;
}
public ExposureInformation build() {
return new ExposureInformation(dateMillisSinceEpoch, durationMinutes, attenuationValue, transmissionRiskLevel, totalRiskScore, attenuationDurations);
}
}
public static final Creator<ExposureInformation> CREATOR = new AutoCreator<>(ExposureInformation.class);
}

View File

@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import com.google.android.gms.common.api.CommonStatusCodes;
public class ExposureNotificationStatusCodes extends CommonStatusCodes {
public static final int FAILED = 13;
public static final int FAILED_ALREADY_STARTED = 39500;
public static final int FAILED_NOT_SUPPORTED = 39501;
public static final int FAILED_REJECTED_OPT_IN = 39502;
public static final int FAILED_SERVICE_DISABLED = 39503;
public static final int FAILED_BLUETOOTH_DISABLED = 39504;
public static final int FAILED_TEMPORARILY_DISABLED = 39505;
public static final int FAILED_DISK_IO = 39506;
public static final int FAILED_UNAUTHORIZED = 39507;
public static String getStatusCodeString(final int statusCode) {
switch (statusCode) {
case FAILED_ALREADY_STARTED:
return "FAILED_ALREADY_STARTED";
case FAILED_NOT_SUPPORTED:
return "FAILED_NOT_SUPPORTED";
case FAILED_REJECTED_OPT_IN:
return "FAILED_REJECTED_OPT_IN";
case FAILED_SERVICE_DISABLED:
return "FAILED_SERVICE_DISABLED";
case FAILED_BLUETOOTH_DISABLED:
return "FAILED_BLUETOOTH_DISABLED";
case FAILED_TEMPORARILY_DISABLED:
return "FAILED_TEMPORARILY_DISABLED";
case FAILED_DISK_IO:
return "FAILED_DISK_IO";
case FAILED_UNAUTHORIZED:
return "FAILED_UNAUTHORIZED";
default:
return CommonStatusCodes.getStatusCodeString(statusCode);
}
}
}

View File

@ -0,0 +1,128 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Arrays;
public class ExposureSummary extends AutoSafeParcelable {
@Field(1)
private int daysSinceLastExposure;
@Field(2)
private int matchedKeyCount;
@Field(3)
private int maximumRiskScore;
@Field(4)
private int[] attenuationDurationsInMinutes;
@Field(5)
private int summationRiskScore;
private ExposureSummary() {
}
ExposureSummary(int daysSinceLastExposure, int matchedKeyCount, int maximumRiskScore, int[] attenuationDurationsInMinutes, int summationRiskScore) {
this.daysSinceLastExposure = daysSinceLastExposure;
this.matchedKeyCount = matchedKeyCount;
this.maximumRiskScore = maximumRiskScore;
this.attenuationDurationsInMinutes = attenuationDurationsInMinutes;
this.summationRiskScore = summationRiskScore;
}
public int getDaysSinceLastExposure() {
return daysSinceLastExposure;
}
public int getMatchedKeyCount() {
return matchedKeyCount;
}
public int getMaximumRiskScore() {
return maximumRiskScore;
}
public int[] getAttenuationDurationsInMinutes() {
return Arrays.copyOf(attenuationDurationsInMinutes, attenuationDurationsInMinutes.length);
}
public int getSummationRiskScore() {
return summationRiskScore;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ExposureSummary that = (ExposureSummary) o;
if (daysSinceLastExposure != that.daysSinceLastExposure) return false;
if (matchedKeyCount != that.matchedKeyCount) return false;
if (maximumRiskScore != that.maximumRiskScore) return false;
if (summationRiskScore != that.summationRiskScore) return false;
return Arrays.equals(attenuationDurationsInMinutes, that.attenuationDurationsInMinutes);
}
@Override
public int hashCode() {
int result = daysSinceLastExposure;
result = 31 * result + matchedKeyCount;
result = 31 * result + maximumRiskScore;
result = 31 * result + Arrays.hashCode(attenuationDurationsInMinutes);
result = 31 * result + summationRiskScore;
return result;
}
@Override
public String toString() {
return "ExposureSummary{" +
"daysSinceLastExposure=" + daysSinceLastExposure +
", matchedKeyCount=" + matchedKeyCount +
", maximumRiskScore=" + maximumRiskScore +
", attenuationDurationsInMinutes=" + Arrays.toString(attenuationDurationsInMinutes) +
", summationRiskScore=" + summationRiskScore +
'}';
}
public static class ExposureSummaryBuilder {
private int daysSinceLastExposure;
private int matchedKeyCount;
private int maximumRiskScore;
private int[] attenuationDurations = new int[]{0, 0, 0};
private int summationRiskScore;
public ExposureSummaryBuilder setDaysSinceLastExposure(int daysSinceLastExposure) {
this.daysSinceLastExposure = daysSinceLastExposure;
return this;
}
public ExposureSummaryBuilder setMatchedKeyCount(int matchedKeyCount) {
this.matchedKeyCount = matchedKeyCount;
return this;
}
public ExposureSummaryBuilder setMaximumRiskScore(int maximumRiskScore) {
this.maximumRiskScore = maximumRiskScore;
return this;
}
public ExposureSummaryBuilder setAttenuationDurations(int[] attenuationDurations) {
this.attenuationDurations = Arrays.copyOf(attenuationDurations, attenuationDurations.length);
return this;
}
public ExposureSummaryBuilder setSummationRiskScore(int summationRiskScore) {
this.summationRiskScore = summationRiskScore;
return this;
}
public ExposureSummary build() {
return new ExposureSummary(daysSinceLastExposure, matchedKeyCount, maximumRiskScore, attenuationDurations, summationRiskScore);
}
}
public static final Creator<ExposureSummary> CREATOR = new AutoCreator<>(ExposureSummary.class);
}

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
public @interface RiskLevel {
int RISK_LEVEL_INVALID = 0;
int RISK_LEVEL_LOWEST = 1;
int RISK_LEVEL_LOW = 2;
int RISK_LEVEL_LOW_MEDIUM = 3;
int RISK_LEVEL_MEDIUM = 4;
int RISK_LEVEL_MEDIUM_HIGH = 5;
int RISK_LEVEL_HIGH = 6;
int RISK_LEVEL_VERY_HIGH = 7;
int RISK_LEVEL_HIGHEST = 8;
}

View File

@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Arrays;
public class TemporaryExposureKey extends AutoSafeParcelable {
@Field(1)
private byte[] keyData;
@Field(2)
private int rollingStartIntervalNumber;
@Field(3)
@RiskLevel
private int transmissionRiskLevel;
@Field(4)
private int rollingPeriod;
private TemporaryExposureKey() {
}
TemporaryExposureKey(byte[] keyData, int rollingStartIntervalNumber, @RiskLevel int transmissionRiskLevel, int rollingPeriod) {
this.keyData = keyData;
this.rollingStartIntervalNumber = rollingStartIntervalNumber;
this.transmissionRiskLevel = transmissionRiskLevel;
this.rollingPeriod = rollingPeriod;
}
public byte[] getKeyData() {
return Arrays.copyOf(keyData, keyData.length);
}
public int getRollingStartIntervalNumber() {
return rollingStartIntervalNumber;
}
@RiskLevel
public int getTransmissionRiskLevel() {
return transmissionRiskLevel;
}
public int getRollingPeriod() {
return rollingPeriod;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TemporaryExposureKey that = (TemporaryExposureKey) o;
if (rollingStartIntervalNumber != that.rollingStartIntervalNumber) return false;
if (transmissionRiskLevel != that.transmissionRiskLevel) return false;
if (rollingPeriod != that.rollingPeriod) return false;
return Arrays.equals(keyData, that.keyData);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(keyData);
result = 31 * result + rollingStartIntervalNumber;
result = 31 * result + transmissionRiskLevel;
result = 31 * result + rollingPeriod;
return result;
}
@Override
public String toString() {
return "TemporaryExposureKey{" +
"keyData=" + Arrays.toString(keyData) +
", rollingStartIntervalNumber=" + rollingStartIntervalNumber +
", transmissionRiskLevel=" + transmissionRiskLevel +
", rollingPeriod=" + rollingPeriod +
'}';
}
public static class TemporaryExposureKeyBuilder {
private byte[] keyData;
private int rollingStartIntervalNumber;
@RiskLevel
private int transmissionRiskLevel;
private int rollingPeriod;
public TemporaryExposureKeyBuilder setKeyData(byte[] keyData) {
this.keyData = Arrays.copyOf(keyData, keyData.length);
return this;
}
public TemporaryExposureKeyBuilder setRollingStartIntervalNumber(int rollingStartIntervalNumber) {
this.rollingStartIntervalNumber = rollingStartIntervalNumber;
return this;
}
public TemporaryExposureKeyBuilder setTransmissionRiskLevel(@RiskLevel int transmissionRiskLevel) {
this.transmissionRiskLevel = transmissionRiskLevel;
return this;
}
public TemporaryExposureKeyBuilder setRollingPeriod(int rollingPeriod) {
this.rollingPeriod = rollingPeriod;
return this;
}
public TemporaryExposureKey build() {
return new TemporaryExposureKey(keyData, rollingStartIntervalNumber, transmissionRiskLevel, rollingPeriod);
}
}
public static final Creator<TemporaryExposureKey> CREATOR = new AutoCreator<>(TemporaryExposureKey.class);
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetExposureInformationParams extends AutoSafeParcelable {
@Field(2)
public IExposureInformationListCallback callback;
@Field(3)
public String token;
private GetExposureInformationParams() {}
public GetExposureInformationParams(IExposureInformationListCallback callback, String token) {
this.callback = callback;
this.token = token;
}
public static final Creator<GetExposureInformationParams> CREATOR = new AutoCreator<>(GetExposureInformationParams.class);
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetExposureSummaryParams extends AutoSafeParcelable {
@Field(2)
public IExposureSummaryCallback callback;
@Field(3)
public String token;
private GetExposureSummaryParams() {}
public GetExposureSummaryParams(IExposureSummaryCallback callback, String token) {
this.callback = callback;
this.token = token;
}
public static final Creator<GetExposureSummaryParams> CREATOR = new AutoCreator<>(GetExposureSummaryParams.class);
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetTemporaryExposureKeyHistoryParams extends AutoSafeParcelable {
@Field(2)
public ITemporaryExposureKeyListCallback callback;
private GetTemporaryExposureKeyHistoryParams() {}
public GetTemporaryExposureKeyHistoryParams(ITemporaryExposureKeyListCallback callback) {
this.callback = callback;
}
public static final Creator<GetTemporaryExposureKeyHistoryParams> CREATOR = new AutoCreator<>(GetTemporaryExposureKeyHistoryParams.class);
}

View File

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class IsEnabledParams extends AutoSafeParcelable {
@Field(2)
public IBooleanCallback callback;
private IsEnabledParams() {
}
public IsEnabledParams(IBooleanCallback callback) {
this.callback = callback;
}
public static final Creator<IsEnabledParams> CREATOR = new AutoCreator<>(IsEnabledParams.class);
}

View File

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import android.os.ParcelFileDescriptor;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration;
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.List;
public class ProvideDiagnosisKeysParams extends AutoSafeParcelable {
@Field(1)
public List<TemporaryExposureKey> keys;
@Field(2)
public IStatusCallback callback;
@Field(3)
public List<ParcelFileDescriptor> keyFiles;
@Field(4)
public ExposureConfiguration configuration;
@Field(5)
public String token;
public static final Creator<ProvideDiagnosisKeysParams> CREATOR = new AutoCreator<>(ProvideDiagnosisKeysParams.class);
}

View File

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration;
import org.microg.safeparcel.AutoSafeParcelable;
public class StartParams extends AutoSafeParcelable {
@Field(3)
public IStatusCallback callback;
@Field(4)
public ExposureConfiguration configuration;
private StartParams() {
}
public StartParams(IStatusCallback callback) {
this.callback = callback;
}
public static final Creator<StartParams> CREATOR = new AutoCreator<>(StartParams.class);
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.internal.IStatusCallback;
import org.microg.safeparcel.AutoSafeParcelable;
public class StopParams extends AutoSafeParcelable {
@Field(1)
public IStatusCallback callback;
private StopParams() {
}
public StopParams(IStatusCallback callback) {
this.callback = callback;
}
public static final Creator<StopParams> CREATOR = new AutoCreator<>(StopParams.class);
}

View File

@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification;
public class Constants {
public static final String ACTION_EXPOSURE_NOTIFICATION_SETTINGS = "com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS";
public static final String ACTION_EXPOSURE_NOT_FOUND = "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_NOT_FOUND";
public static final String ACTION_EXPOSURE_STATE_UPDATED = "com.google.android.gms.exposurenotification.ACTION_EXPOSURE_STATE_UPDATED";
public static final String EXTRA_EXPOSURE_SUMMARY = "com.google.android.gms.exposurenotification.EXTRA_EXPOSURE_SUMMARY";
public static final String EXTRA_TOKEN = "com.google.android.gms.exposurenotification.EXTRA_TOKEN";
}

View File

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.squareup.wire'
apply plugin: 'kotlin'
dependencies {
implementation "com.squareup.wire:wire-runtime:$wireVersion"
}
wire {
kotlin {}
}
compileKotlin {
kotlinOptions.jvmTarget = 1.8
}
compileTestKotlin {
kotlinOptions.jvmTarget = 1.8
}

View File

@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2020, Google
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are derived from work created and shared by Google and used
* according to the terms described in the Apache License, Version 2.0.
* See https://developers.google.com/android/exposure-notifications/exposure-key-file-format
*/
syntax = "proto2";
package org.microg.gms.nearby.exposurenotification.proto;
message TemporaryExposureKeyExport {
// Time window of keys in this batch based on arrival to server, in UTC seconds.
optional fixed64 start_timestamp = 1;
optional fixed64 end_timestamp = 2;
// Region for which these keys came from, such as country.
optional string region = 3;
// For example, file 2 in batch size of 10. Ordinal, 1-based numbering.
// Note: Not yet supported on iOS.
optional int32 batch_num = 4;
optional int32 batch_size = 5;
// Information about associated signatures
repeated SignatureInfo signature_infos = 6;
// The TemporaryExposureKeys for initial release of keys.
// Keys should be included in this list for initial release,
// whereas revised or revoked keys should go in revised_keys.
repeated TemporaryExposureKeyProto keys = 7;
// TemporaryExposureKeys that have changed status.
// Keys should be included in this list if they have changed status
// or have been revoked.
repeated TemporaryExposureKeyProto revised_keys = 8;
}
message SignatureInfo {
// The first two fields have been deprecated
reserved 1, 2;
reserved "app_bundle_id", "android_package";
// Key version for rollovers
// Must be in character class [a-zA-Z0-9_]. For example, 'v1'
optional string verification_key_version = 3;
// Alias with which to identify public key to be used for verification
// Must be in character class [a-zA-Z0-9_.]
// For cross-compatibility with Apple, you can use your region's three-digit
// mobile country code (MCC). If your region has more than one MCC, choose the
// one that Apple has configured.
optional string verification_key_id = 4;
// ASN.1 OID for Algorithm Identifier. For example, `1.2.840.10045.4.3.2'
optional string signature_algorithm = 5;
}
message TemporaryExposureKeyProto {
// Key of infected user
optional bytes key_data = 1;
// Varying risk associated with a key depending on diagnosis method
optional int32 transmission_risk_level = 2 [deprecated = true];
// The interval number since epoch for which a key starts
optional int32 rolling_start_interval_number = 3;
// Increments of 10 minutes describing how long a key is valid
optional int32 rolling_period = 4
[default = 144]; // defaults to 24 hours
// Data type representing why this key was published.
enum ReportType {
UNKNOWN = 0; // Never returned by the client API.
CONFIRMED_TEST = 1;
CONFIRMED_CLINICAL_DIAGNOSIS = 2;
SELF_REPORT = 3;
RECURSIVE = 4; // Reserved for future use.
REVOKED = 5; // Used to revoke a key, never returned by client API.
}
// Type of diagnosis associated with a key.
optional ReportType report_type = 5;
// Number of days elapsed between symptom onset and the TEK being used.
// E.g. 2 means TEK is 2 days after onset of symptoms.
optional sint32 days_since_onset_of_symptoms = 6;
}
message TEKSignatureList {
repeated TEKSignature signatures = 1;
}
message TEKSignature {
// Info about the signing key, version, algorithm, etc. Only the
// verification_key_id, verification_key_version, and
// signature_algorithm fields within signature_info are read.
optional SignatureInfo signature_info = 1;
// E.g., Batch 2 of 10 - these fields are ignored on android in favor of the
// batch fields within TemporaryExposureKeyExport
optional int32 batch_num = 2;
optional int32 batch_size = 3;
// Signature in X9.62 format (ASN.1 SEQUENCE of two INTEGER fields)
optional bytes signature = 4;
}

View File

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
dependencies {
api project(':play-services-nearby-api')
implementation project(':play-services-base-core')
implementation project(':play-services-nearby-core-proto')
implementation "androidx.annotation:annotation:$annotationVersion"
implementation "androidx.lifecycle:lifecycle-service:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion"
implementation "androidx.preference:preference:$preferenceVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
implementation "com.squareup.wire:wire-runtime:$wireVersion"
testImplementation 'junit:junit:4.12'
}
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
sourceSets {
main {
java.srcDirs = ['src/main/kotlin']
}
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.microg.gms.nearby.core">
<permission
android:name="com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK"
android:protectionLevel="normal" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application>
<service
android:name="org.microg.gms.nearby.exposurenotification.ScannerService"
android:exported="true" />
<service
android:name="org.microg.gms.nearby.exposurenotification.AdvertiserService"
android:exported="true" />
<service android:name="org.microg.gms.nearby.exposurenotification.ExposureNotificationService">
<intent-filter>
<action android:name="com.google.android.gms.nearby.exposurenotification.START" />
</intent-filter>
</service>
<receiver android:name="org.microg.gms.nearby.exposurenotification.ServiceTrigger">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.AIRPLANE_MODE" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_RESTARTED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@ -0,0 +1,101 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseSettings.*
import android.content.Intent
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import java.io.FileDescriptor
import java.io.PrintWriter
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@TargetApi(21)
class AdvertiserService : LifecycleService() {
private var callback: AdvertiseCallback? = null
private val advertiser: BluetoothLeAdvertiser
get() = BluetoothAdapter.getDefaultAdapter().bluetoothLeAdvertiser
private lateinit var database: ExposureDatabase
private suspend fun BluetoothLeAdvertiser.startAdvertising(settings: AdvertiseSettings, advertiseData: AdvertiseData): AdvertiseCallback = suspendCoroutine {
startAdvertising(settings, advertiseData, object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
it.resume(this)
}
override fun onStartFailure(errorCode: Int) {
it.resumeWithException(RuntimeException("Error code: $errorCode"))
}
})
}
override fun onCreate() {
super.onCreate()
database = ExposureDatabase(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (ExposurePreferences(this).advertiserEnabled) {
startAdvertising()
} else {
stopSelf()
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
stopAdvertising()
database.close()
}
fun startAdvertising() {
lifecycleScope.launchWhenStarted {
do {
val payload = database.generateCurrentPayload(byteArrayOf(
0x40, // Version 1.0
currentDeviceInfo.txPowerCorrection.toByte(), // TX Power (TODO)
0x00, // Reserved
0x00 // Reserved
))
var nextSend = nextKeyMillis.coerceAtMost(180000)
startAdvertising(payload, nextSend.toInt())
delay(nextSend)
} while (callback != null)
}
}
suspend fun startAdvertising(bytes: ByteArray, nextSend: Int) {
stopAdvertising()
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, bytes).build()
val settings = AdvertiseSettings.Builder()
.setTimeout(nextSend)
.setAdvertiseMode(ADVERTISE_MODE_LOW_POWER)
.setTxPowerLevel(ADVERTISE_TX_POWER_MEDIUM)
.setConnectable(false)
.build()
callback = advertiser.startAdvertising(settings, data)
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
writer?.println("Active: ${callback != null}")
writer?.println("Currently advertising: ${database.currentRpiId}")
writer?.println("Next key change in ${nextKeyMillis}ms")
}
@Synchronized
fun stopAdvertising() {
callback?.let { advertiser.stopAdvertising(it) }
callback = null
}
}

View File

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.os.ParcelUuid
import java.util.*
const val TAG = "ExposureNotification"
val SERVICE_UUID = ParcelUuid(UUID.fromString("0000FD6F-0000-1000-8000-00805F9B34FB"))
const val ROLLING_WINDOW_LENGTH = 10 * 60
const val ROLLING_WINDOW_LENGTH_MS = ROLLING_WINDOW_LENGTH * 1000
const val ROLLING_PERIOD = 144
const val ALLOWED_KEY_OFFSET_MS = 60 * 60 * 1000
const val MINIMUM_EXPOSURE_DURATION_MS = 0
const val KEEP_DAYS = 14
const val ACTION_CONFIRM = "org.microg.gms.nearby.exposurenotification.CONFIRM"
const val KEY_CONFIRM_ACTION = "action"
const val KEY_CONFIRM_RECEIVER = "receiver"
const val KEY_CONFIRM_PACKAGE = "package"
const val CONFIRM_ACTION_START = "start"
const val CONFIRM_ACTION_STOP = "stop"
const val CONFIRM_ACTION_KEYS = "keys"
const val PERMISSION_EXPOSURE_CALLBACK = "com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK"

View File

@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
import java.util.*
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.floor
private const val RPIK_HKDF_INFO = "EN-RPIK"
private const val RPIK_ALGORITHM = "AES"
private const val AEMK_HKDF_INFO = "EN-AEMK"
private const val AEMK_ALGORITHM = "AES"
private const val HKDF_ALGORITHM = "HmacSHA256"
private const val HKDF_LENGTH = 16
private const val HASH_LENGTH = 32
private const val RPID_ALGORITHM = "AES/ECB/NoPadding"
private const val RPID_PREFIX = "EN-RPI"
private const val AES_BLOCK_SIZE = 16
private const val AEM_ALGORITHM = "AES/CTR/NoPadding"
val currentIntervalNumber: Long
get() = floor(System.currentTimeMillis() / 1000.0 / ROLLING_WINDOW_LENGTH).toLong()
val currentRollingStartNumber: Long
get() = floor(currentIntervalNumber.toDouble() / ROLLING_PERIOD).toLong() * ROLLING_PERIOD
val nextKeyMillis: Long
get() {
val currentWindowStart = currentIntervalNumber * ROLLING_WINDOW_LENGTH * 1000
val currentWindowEnd = currentWindowStart + ROLLING_WINDOW_LENGTH * 1000
return (currentWindowEnd - System.currentTimeMillis()).coerceAtLeast(0)
}
fun TemporaryExposureKey.TemporaryExposureKeyBuilder.setCurrentRollingStartNumber(): TemporaryExposureKey.TemporaryExposureKeyBuilder =
setRollingStartIntervalNumber(currentRollingStartNumber.toInt())
fun TemporaryExposureKey.TemporaryExposureKeyBuilder.generate(): TemporaryExposureKey.TemporaryExposureKeyBuilder {
var keyData = ByteArray(16)
SecureRandom().nextBytes(keyData)
setKeyData(keyData)
setRollingPeriod(ROLLING_PERIOD)
return this
}
fun generateCurrentTemporaryExposureKey(): TemporaryExposureKey = TemporaryExposureKey.TemporaryExposureKeyBuilder().generate().setCurrentRollingStartNumber().build()
@TargetApi(21)
fun TemporaryExposureKey.generateRpiKey(): SecretKeySpec {
return SecretKeySpec(hkdf(keyData, null, RPIK_HKDF_INFO.toByteArray(StandardCharsets.UTF_8)), RPIK_ALGORITHM)
}
@TargetApi(21)
fun TemporaryExposureKey.generateAemKey(): SecretKeySpec {
return SecretKeySpec(hkdf(keyData, null, AEMK_HKDF_INFO.toByteArray(StandardCharsets.UTF_8)), AEMK_ALGORITHM)
}
@TargetApi(21)
fun TemporaryExposureKey.generateRpiId(intervalNumber: Int): ByteArray {
val cipher = Cipher.getInstance(RPID_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, generateRpiKey())
val data = ByteBuffer.allocate(AES_BLOCK_SIZE).order(ByteOrder.LITTLE_ENDIAN).apply {
put(RPID_PREFIX.toByteArray(StandardCharsets.UTF_8))
position(12)
putInt(intervalNumber)
}.array()
return cipher.doFinal(data)
}
@TargetApi(21)
fun TemporaryExposureKey.generateAllRpiIds(): ByteArray {
val cipher = Cipher.getInstance(RPID_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, generateRpiKey())
val data = ByteBuffer.allocate(AES_BLOCK_SIZE * rollingPeriod).order(ByteOrder.LITTLE_ENDIAN).apply {
val prefix = RPID_PREFIX.toByteArray(StandardCharsets.UTF_8)
for (i in 0 until rollingPeriod) {
put(prefix)
position(i * 16 + 12)
putInt(rollingStartIntervalNumber + i)
}
}.array()
return cipher.doFinal(data)
}
fun TemporaryExposureKey.cryptAem(rpi: ByteArray, metadata: ByteArray): ByteArray {
val cipher = Cipher.getInstance(AEM_ALGORITHM)
cipher.init(Cipher.ENCRYPT_MODE, generateAemKey(), IvParameterSpec(rpi))
return cipher.doFinal(metadata)
}
fun TemporaryExposureKey.generatePayload(intervalNumber: Int, metadata: ByteArray): ByteArray {
val rpi = generateRpiId(intervalNumber)
val aem = cryptAem(rpi, metadata)
return rpi + aem
}
private fun hkdf(inputKeyingMaterial: ByteArray, inputSalt: ByteArray?, info: ByteArray): ByteArray {
val mac = Mac.getInstance(HKDF_ALGORITHM)
val salt = if (inputSalt == null || inputSalt.isEmpty()) ByteArray(HASH_LENGTH) else inputSalt
mac.init(SecretKeySpec(salt, HKDF_ALGORITHM))
val pseudoRandomKey = mac.doFinal(inputKeyingMaterial)
mac.init(SecretKeySpec(pseudoRandomKey, HKDF_ALGORITHM))
mac.update(info)
return Arrays.copyOf(mac.doFinal(byteArrayOf(0x01)), HKDF_LENGTH)
}

View File

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
data class DeviceInfo(val txPowerCorrection: Int, val rssiCorrection: Int)
// TODO
val currentDeviceInfo: DeviceInfo
get() = DeviceInfo(-17, -5)

View File

@ -0,0 +1,447 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DB_VERSION)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 1) {
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_ADVERTISEMENTS(rpi BLOB NOT NULL, aem BLOB NOT NULL, timestamp INTEGER NOT NULL, rssi INTEGER NOT NULL, duration INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(rpi, timestamp));")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_ADVERTISEMENTS}_rpi ON $TABLE_ADVERTISEMENTS(rpi);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_ADVERTISEMENTS}_timestamp ON $TABLE_ADVERTISEMENTS(timestamp);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_APP_LOG(package TEXT NOT NULL, timestamp INTEGER NOT NULL, method TEXT NOT NULL, args TEXT);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_APP_LOG}_package_timestamp ON $TABLE_APP_LOG(package, timestamp);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(keyData, rollingStartNumber, rollingPeriod));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, transmissionRiskLevel INTEGER NOT NULL, PRIMARY KEY(package, token, keyData));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CONFIGURATIONS(package TEXT NOT NULL, token TEXT NOT NULL, configuration BLOB, PRIMARY KEY(package, token))")
}
}
fun dailyCleanup() = writableDatabase.run {
val rollingStartTime = currentRollingStartNumber * ROLLING_WINDOW_LENGTH * 1000 - TimeUnit.DAYS.toMillis(KEEP_DAYS.toLong())
delete(TABLE_ADVERTISEMENTS, "timestamp < ?", arrayOf(rollingStartTime.toString()))
delete(TABLE_APP_LOG, "timestamp < ?", arrayOf(rollingStartTime.toString()))
delete(TABLE_TEK, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
delete(TABLE_TEK_CHECK, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
delete(TABLE_DIAGNOSIS, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
}
fun noteAdvertisement(rpi: ByteArray, aem: ByteArray, rssi: Int, timestamp: Long = Date().time) = writableDatabase.run {
val update = compileStatement("UPDATE $TABLE_ADVERTISEMENTS SET rssi = ((rssi * duration) + (? * (? - timestamp - duration)) / (? - timestamp)), duration = (? - timestamp) WHERE rpi = ? AND timestamp > ? AND timestamp < ?").run {
bindLong(1, rssi.toLong())
bindLong(2, timestamp)
bindLong(3, timestamp)
bindLong(4, timestamp)
bindBlob(5, rpi)
bindLong(6, timestamp - ALLOWED_KEY_OFFSET_MS)
bindLong(7, timestamp + ALLOWED_KEY_OFFSET_MS)
executeUpdateDelete()
}
if (update <= 0) {
insert(TABLE_ADVERTISEMENTS, "NULL", ContentValues().apply {
put("rpi", rpi)
put("aem", aem)
put("timestamp", timestamp)
put("rssi", rssi)
put("duration", MINIMUM_EXPOSURE_DURATION_MS)
})
}
}
fun deleteAllCollectedAdvertisements() = writableDatabase.run {
delete(TABLE_ADVERTISEMENTS, null, null)
update(TABLE_DIAGNOSIS, ContentValues().apply {
put("matched", 0)
}, null, null)
}
fun noteAppAction(packageName: String, method: String, args: String? = null, timestamp: Long = Date().time) = writableDatabase.run {
insert(TABLE_APP_LOG, "NULL", ContentValues().apply {
put("package", packageName)
put("timestamp", timestamp)
put("method", method)
put("args", args)
})
}
fun storeOwnKey(key: TemporaryExposureKey): TemporaryExposureKey = writableDatabase.run {
insert(TABLE_TEK, "NULL", ContentValues().apply {
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
})
key
}
fun storeDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey) = writableDatabase.run {
insert(TABLE_DIAGNOSIS, "NULL", ContentValues().apply {
put("package", packageName)
put("token", token)
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
put("transmissionRiskLevel", key.transmissionRiskLevel)
})
}
fun updateDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey) = writableDatabase.run {
compileStatement("UPDATE $TABLE_DIAGNOSIS SET rollingStartNumber = ?, rollingPeriod = ?, transmissionRiskLevel = ? WHERE package = ? AND token = ? AND keyData = ?;").use {
it.bindLong(1, key.rollingStartIntervalNumber.toLong())
it.bindLong(2, key.rollingPeriod.toLong())
it.bindLong(3, key.transmissionRiskLevel.toLong())
it.bindString(4, packageName)
it.bindString(5, token)
it.bindBlob(6, key.keyData)
it.executeUpdateDelete()
}
}
fun listDiagnosisKeysPendingSearch(packageName: String, token: String) = readableDatabase.run {
rawQuery("""
SELECT $TABLE_DIAGNOSIS.keyData, $TABLE_DIAGNOSIS.rollingStartNumber, $TABLE_DIAGNOSIS.rollingPeriod
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON
$TABLE_DIAGNOSIS.keyData = $TABLE_TEK_CHECK.keyData AND
$TABLE_DIAGNOSIS.rollingStartNumber = $TABLE_TEK_CHECK.rollingStartNumber AND
$TABLE_DIAGNOSIS.rollingPeriod = $TABLE_TEK_CHECK.rollingPeriod
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
$TABLE_TEK_CHECK.matched IS NULL
""", arrayOf(packageName, token)).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(cursor.getBlob(0))
.setRollingStartIntervalNumber(cursor.getLong(1).toInt())
.setRollingPeriod(cursor.getLong(2).toInt())
.build())
}
list
}
}
fun applyDiagnosisKeySearchResult(packageName: String, token: String, key: TemporaryExposureKey, matched: Boolean) = writableDatabase.run {
insert(TABLE_TEK_CHECK, "NULL", ContentValues().apply {
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
put("matched", if (matched) 1 else 0)
})
}
fun listMatchedDiagnosisKeys(packageName: String, token: String) = readableDatabase.run {
rawQuery("""
SELECT $TABLE_DIAGNOSIS.keyData, $TABLE_DIAGNOSIS.rollingStartNumber, $TABLE_DIAGNOSIS.rollingPeriod, $TABLE_DIAGNOSIS.transmissionRiskLevel
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON
$TABLE_DIAGNOSIS.keyData = $TABLE_TEK_CHECK.keyData AND
$TABLE_DIAGNOSIS.rollingStartNumber = $TABLE_TEK_CHECK.rollingStartNumber AND
$TABLE_DIAGNOSIS.rollingPeriod = $TABLE_TEK_CHECK.rollingPeriod
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
$TABLE_TEK_CHECK.matched = 1
""", arrayOf(packageName, token)).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(cursor.getBlob(0))
.setRollingStartIntervalNumber(cursor.getLong(1).toInt())
.setRollingPeriod(cursor.getLong(2).toInt())
.setTransmissionRiskLevel(cursor.getLong(3).toInt())
.build())
}
list
}
}
fun finishMatching(packageName: String, token: String) {
val start = System.currentTimeMillis()
val workQueue = LinkedBlockingQueue<Runnable>()
val poolSize = Runtime.getRuntime().availableProcessors()
val executor = ThreadPoolExecutor(poolSize, poolSize, 1, TimeUnit.SECONDS, workQueue)
val futures = arrayListOf<Future<*>>()
val keys = listDiagnosisKeysPendingSearch(packageName, token)
val oldestRpi = oldestRpi
for (key in keys) {
if (oldestRpi == null || key.rollingStartIntervalNumber * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS < oldestRpi) {
// Early ignore because key is older than since we started scanning.
applyDiagnosisKeySearchResult(packageName, token, key, false)
} else {
futures.add(executor.submit {
applyDiagnosisKeySearchResult(packageName, token, key, findMeasuredExposures(key).isNotEmpty())
})
}
}
for (future in futures) {
future.get()
}
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
executor.shutdown()
Log.d(TAG, "Processed ${keys.size} keys in ${System.currentTimeMillis() - start}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
}
fun findAllMeasuredExposures(packageName: String, token: String): List<MeasuredExposure> {
val list = arrayListOf<MeasuredExposure>()
for (key in listMatchedDiagnosisKeys(packageName, token)) {
list.addAll(findMeasuredExposures(key))
}
return list
}
fun findMeasuredExposures(key: TemporaryExposureKey): List<MeasuredExposure> {
val list = arrayListOf<MeasuredExposure>()
val allRpis = key.generateAllRpiIds()
val rpis = (0 until key.rollingPeriod).map { i ->
val pos = i * 16
allRpis.sliceArray(pos until (pos + 16))
}
val start = System.currentTimeMillis()
val measures = findMeasuredExposures(rpis, key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS, (key.rollingStartIntervalNumber.toLong() + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS)
measures.filter {
val index = rpis.indexOf(it.rpi)
val targetTimestamp = (key.rollingStartIntervalNumber + index).toLong() * ROLLING_WINDOW_LENGTH_MS
it.timestamp > targetTimestamp - ALLOWED_KEY_OFFSET_MS && it.timestamp < targetTimestamp + ALLOWED_KEY_OFFSET_MS
}.mapNotNull {
val decrypted = key.cryptAem(it.rpi, it.aem)
if (decrypted[0] == 0x40.toByte() || decrypted[0] == 0x50.toByte()) {
val txPower = decrypted[1]
it.copy(key = key, notCorrectedAttenuation = txPower - it.rssi)
} else {
Log.w(TAG, "Unknown AEM version ${decrypted[0]}, ignoring")
null
}
}
return list
}
fun findMeasuredExposures(rpis: List<ByteArray>, minTime: Long, maxTime: Long): List<MeasuredExposure> = readableDatabase.run {
if (rpis.isEmpty()) return emptyList()
val qs = rpis.map { "?" }.joinToString(",")
queryWithFactory({ _, cursorDriver, editTable, query ->
query.bindLong(1, minTime)
query.bindLong(2, maxTime)
for (i in (3..(rpis.size + 2))) {
query.bindBlob(i, rpis[i - 3])
}
SQLiteCursor(cursorDriver, editTable, query)
}, false, TABLE_ADVERTISEMENTS, arrayOf("rpi", "aem", "timestamp", "duration", "rssi"), "timestamp > ? AND timestamp < ? AND rpi IN ($qs)", null, null, null, null, null).use { cursor ->
val list = arrayListOf<MeasuredExposure>()
while (cursor.moveToNext()) {
list.add(MeasuredExposure(cursor.getBlob(1), cursor.getBlob(2), cursor.getLong(3), cursor.getLong(4), cursor.getInt(5)))
}
list
}
}
fun findMeasuredExposure(rpi: ByteArray, minTime: Long, maxTime: Long): MeasuredExposure? = readableDatabase.run {
queryWithFactory({ _, cursorDriver, editTable, query ->
query.bindBlob(1, rpi)
query.bindLong(2, minTime)
query.bindLong(3, maxTime)
SQLiteCursor(cursorDriver, editTable, query)
}, false, TABLE_ADVERTISEMENTS, arrayOf("aem", "timestamp", "duration", "rssi"), "rpi = ? AND timestamp > ? AND timestamp < ?", null, null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
MeasuredExposure(rpi, cursor.getBlob(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3))
} else {
null
}
}
}
fun findOwnKeyAt(rollingStartNumber: Int): TemporaryExposureKey? = readableDatabase.run {
query(TABLE_TEK, arrayOf("keyData", "rollingStartNumber", "rollingPeriod"), "rollingStartNumber = ?", arrayOf(rollingStartNumber.toString()), null, null, null).use { cursor ->
if (cursor.moveToNext()) {
TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(cursor.getBlob(0))
.setRollingStartIntervalNumber(cursor.getLong(1).toInt())
.setRollingPeriod(cursor.getLong(2).toInt())
.build()
} else {
null
}
}
}
fun Parcelable.marshall(): ByteArray {
val parcel = Parcel.obtain()
writeToParcel(parcel, 0)
val bytes = parcel.marshall()
parcel.recycle()
return bytes
}
fun <T> Parcelable.Creator<T>.unmarshall(data: ByteArray): T {
val parcel = Parcel.obtain()
parcel.unmarshall(data, 0, data.size)
parcel.setDataPosition(0)
val res = createFromParcel(parcel)
parcel.recycle()
return res
}
fun storeConfiguration(packageName: String, token: String, configuration: ExposureConfiguration) = writableDatabase.run {
val update = update(TABLE_CONFIGURATIONS, ContentValues().apply { put("configuration", configuration.marshall()) }, "package = ? AND token = ?", arrayOf(packageName, token))
if (update <= 0) {
insert(TABLE_CONFIGURATIONS, "NULL", ContentValues().apply {
put("package", packageName)
put("token", token)
put("configuration", configuration.marshall())
})
}
}
fun loadConfiguration(packageName: String, token: String): ExposureConfiguration? = readableDatabase.run {
query(TABLE_CONFIGURATIONS, arrayOf("configuration"), "package = ? AND token = ?", arrayOf(packageName, token), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
ExposureConfiguration.CREATOR.unmarshall(cursor.getBlob(0))
} else {
null
}
}
}
val allKeys: List<TemporaryExposureKey> = readableDatabase.run {
val startRollingNumber = (currentRollingStartNumber - 14 * ROLLING_PERIOD)
query(TABLE_TEK, arrayOf("keyData", "rollingStartNumber", "rollingPeriod"), "rollingStartNumber >= ? AND rollingStartNumber < ?", arrayOf(startRollingNumber.toString(), currentIntervalNumber.toString()), null, null, null).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(cursor.getBlob(0))
.setRollingStartIntervalNumber(cursor.getLong(1).toInt())
.setRollingPeriod(cursor.getLong(2).toInt())
.build())
}
list
}
}
val rpiHistogram: Map<Long, Long>
get() = readableDatabase.run {
rawQuery("SELECT round(timestamp/(24*60*60*1000)), COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ? GROUP BY round(timestamp/(24*60*60*1000)) ORDER BY timestamp ASC;", arrayOf((Date().time - (14 * 24 * 60 * 60 * 1000)).toString())).use { cursor ->
val map = linkedMapOf<Long, Long>()
while (cursor.moveToNext()) {
map[cursor.getLong(0)] = cursor.getLong(1)
}
map
}
}
val totalRpiCount: Long
get() = readableDatabase.run {
rawQuery("SELECT COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ?;", arrayOf((Date().time - (14 * 24 * 60 * 60 * 1000)).toString())).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
0L
}
}
}
val hourRpiCount: Long
get() = readableDatabase.run {
rawQuery("SELECT COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ?;", arrayOf((Date().time - (60 * 60 * 1000)).toString())).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
0L
}
}
}
val oldestRpi: Long?
get() = readableDatabase.run {
query(TABLE_ADVERTISEMENTS, arrayOf("MIN(timestamp)"), null, null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
null
}
}
}
val appList: List<String>
get() = readableDatabase.run {
query(true, TABLE_APP_LOG, arrayOf("package"), null, null, null, null, "timestamp DESC", null).use { cursor ->
val list = arrayListOf<String>()
while (cursor.moveToNext()) {
list.add(cursor.getString(0))
}
list
}
}
fun countMethodCalls(packageName: String, method: String): Int = readableDatabase.run {
query(TABLE_APP_LOG, arrayOf("COUNT(*)"), "package = ? AND method = ? AND timestamp > ?", arrayOf(packageName, method, (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(KEEP_DAYS.toLong())).toString()), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getInt(0)
} else {
0
}
}
}
fun lastMethodCall(packageName: String, method: String): Long? = readableDatabase.run {
query(TABLE_APP_LOG, arrayOf("MAX(timestamp)"), "package = ? AND method = ?", arrayOf(packageName, method), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
null
}
}
}
private val currentTemporaryExposureKey: TemporaryExposureKey
get() = findOwnKeyAt(currentRollingStartNumber.toInt())
?: storeOwnKey(generateCurrentTemporaryExposureKey())
val currentRpiId: UUID
get() {
val buffer = ByteBuffer.wrap(currentTemporaryExposureKey.generateRpiId(currentIntervalNumber.toInt()))
return UUID(buffer.long, buffer.long)
}
fun generateCurrentPayload(metadata: ByteArray) = currentTemporaryExposureKey.generatePayload(currentIntervalNumber.toInt(), metadata)
companion object {
private const val DB_NAME = "exposure.db"
private const val DB_VERSION = 1
private const val TABLE_ADVERTISEMENTS = "advertisements"
private const val TABLE_APP_LOG = "app_log"
private const val TABLE_TEK = "tek"
private const val TABLE_TEK_CHECK = "tek_check"
private const val TABLE_DIAGNOSIS = "diagnosis"
private const val TABLE_CONFIGURATIONS = "configurations"
}
}

View File

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import com.google.android.gms.common.Feature
import com.google.android.gms.common.internal.ConnectionInfo
import com.google.android.gms.common.internal.GetServiceRequest
import com.google.android.gms.common.internal.IGmsCallbacks
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import org.microg.gms.BaseService
import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils
class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE) {
lateinit var database: ExposureDatabase
override fun onDestroy() {
super.onDestroy()
database.close()
}
override fun onCreate() {
super.onCreate()
database = ExposureDatabase(this)
}
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {
PackageUtils.getAndCheckCallingPackage(this, request.packageName)
fun checkPermission(permission: String): String? {
if (checkCallingPermission(permission) != PackageManager.PERMISSION_GRANTED) {
callback.onPostInitComplete(FAILED_UNAUTHORIZED, null, null)
return null
}
return permission
}
checkPermission("android.permission.BLUETOOTH") ?: return
checkPermission("android.permission.INTERNET") ?: return
if (Build.VERSION.SDK_INT < 21) {
callback.onPostInitComplete(FAILED_NOT_SUPPORTED, null, null)
return
}
Log.d(TAG, "handleServiceRequest: " + request.packageName)
callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, request.packageName, database), ConnectionInfo().apply {
features = arrayOf(Feature("nearby_exposure_notification", 2))
})
}
}

View File

@ -0,0 +1,278 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.*
import android.os.*
import android.util.Log
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import com.google.android.gms.nearby.exposurenotification.internal.*
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.nearby.exposurenotification.Constants.ACTION_EXPOSURE_NOT_FOUND
import org.microg.gms.nearby.exposurenotification.Constants.ACTION_EXPOSURE_STATE_UPDATED
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
import java.util.*
import java.util.zip.ZipInputStream
class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String, private val database: ExposureDatabase) : INearbyExposureNotificationService.Stub() {
private fun confirm(action: String, callback: (resultCode: Int, resultData: Bundle?) -> Unit) {
val intent = Intent(ACTION_CONFIRM)
intent.`package` = context.packageName
intent.putExtra(KEY_CONFIRM_PACKAGE, packageName)
intent.putExtra(KEY_CONFIRM_ACTION, action)
intent.putExtra(KEY_CONFIRM_RECEIVER, object : ResultReceiver(Handler(Looper.getMainLooper())) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
Log.d(TAG, "Result from action $action: ${getStatusCodeString(resultCode)}")
callback(resultCode, resultData)
}
})
intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
intent.addFlags(FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
try {
intent.component = ComponentName(context, context.packageManager.resolveActivity(intent, 0)?.activityInfo?.name!!)
context.startActivity(intent)
} catch (e: Exception) {
Log.w(TAG, e)
callback(CommonStatusCodes.INTERNAL_ERROR, null)
}
}
override fun start(params: StartParams) {
if (ExposurePreferences(context).scannerEnabled) {
params.callback.onResult(Status(FAILED_ALREADY_STARTED))
return
}
confirm(CONFIRM_ACTION_START) { resultCode, resultData ->
if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = true
}
database.use {
it.noteAppAction(packageName, "start", JSONObject().apply {
put("result", resultCode)
}.toString())
}
try {
params.callback.onResult(Status(if (resultCode == SUCCESS) SUCCESS else FAILED_REJECTED_OPT_IN, resultData?.getString("message")))
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
}
override fun stop(params: StopParams) {
confirm(CONFIRM_ACTION_STOP) { resultCode, _ ->
if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = false
}
database.use {
it.noteAppAction(packageName, "stop", JSONObject().apply {
put("result", resultCode)
}.toString())
}
try {
params.callback.onResult(Status.SUCCESS)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
}
override fun isEnabled(params: IsEnabledParams) {
try {
params.callback.onResult(Status.SUCCESS, ExposurePreferences(context).scannerEnabled)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams) {
confirm(CONFIRM_ACTION_START) { resultCode, resultData ->
val (status, response) = if (resultCode == SUCCESS) {
SUCCESS to database.allKeys
} else {
FAILED_REJECTED_OPT_IN to emptyList()
}
database.use {
it.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply {
put("result", resultCode)
put("response_keys_size", response.size)
}.toString())
}
try {
params.callback.onResult(Status(status, resultData?.getString("message")), response)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
}
private fun TemporaryExposureKeyProto.toKey(): TemporaryExposureKey = TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(key_data?.toByteArray() ?: throw IllegalArgumentException("key data missing"))
.setRollingStartIntervalNumber(rolling_start_interval_number
?: throw IllegalArgumentException("rolling start interval number missing"))
.setRollingPeriod(rolling_period ?: throw IllegalArgumentException("rolling period missing"))
.setTransmissionRiskLevel(transmission_risk_level ?: 0)
.build()
private fun storeDiagnosisKeyExport(token: String, export: TemporaryExposureKeyExport): Int {
Log.d(TAG, "Importing keys from file ${export.start_timestamp?.let { Date(it * 1000) }} to ${export.end_timestamp?.let { Date(it * 1000) }}")
for (key in export.keys) {
database.storeDiagnosisKey(packageName, token, key.toKey())
}
for (key in export.revised_keys) {
database.updateDiagnosisKey(packageName, token, key.toKey())
}
return export.keys.size + export.revised_keys.size
}
override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
Thread(Runnable {
if (params.configuration != null) {
database.storeConfiguration(packageName, params.token, params.configuration)
}
// keys
for (key in params.keys.orEmpty()) {
database.storeDiagnosisKey(packageName, params.token, key)
}
// Key files
var keys = params.keys?.size ?: 0
for (file in params.keyFiles.orEmpty()) {
try {
ZipInputStream(ParcelFileDescriptor.AutoCloseInputStream(file)).use { stream ->
do {
val entry = stream.nextEntry ?: break
if (entry.name == "export.bin") {
val prefix = ByteArray(16)
if (stream.read(prefix) == prefix.size && String(prefix).trim() == "EK Export v1") {
val fileKeys = storeDiagnosisKeyExport(params.token, TemporaryExposureKeyExport.ADAPTER.decode(stream))
keys + fileKeys
} else {
Log.d(TAG, "export.bin had invalid prefix")
}
}
stream.closeEntry()
} while (true);
}
} catch (e: Exception) {
Log.w(TAG, "Failed parsing file", e)
}
}
Log.d(TAG, "$packageName/${params.token} provided $keys keys")
Handler(Looper.getMainLooper()).post {
database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply {
put("request_token", params.token)
put("request_keys_size", params.keys?.size)
put("request_keyFiles_size", params.keyFiles?.size)
put("request_keys_count", keys)
}.toString())
try {
params.callback.onResult(Status.SUCCESS)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
val match = database.use {
it.finishMatching(packageName, params.token)
it.findAllMeasuredExposures(packageName, params.token).isNotEmpty()
}
try {
val intent = Intent(if (match) ACTION_EXPOSURE_STATE_UPDATED else ACTION_EXPOSURE_NOT_FOUND)
intent.`package` = packageName
Log.d(TAG, "Sending $intent")
context.sendOrderedBroadcast(intent, PERMISSION_EXPOSURE_CALLBACK)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}).start()
}
override fun getExposureSummary(params: GetExposureSummaryParams) {
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
try {
params.callback.onResult(Status.INTERNAL_ERROR, null)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return
}
val exposures = database.findAllMeasuredExposures(packageName, params.token)
val response = ExposureSummary.ExposureSummaryBuilder()
.setDaysSinceLastExposure(exposures.map { it.daysSinceExposure }.min()?.toInt() ?: 0)
.setMatchedKeyCount(exposures.map { it.key }.distinct().size)
.setMaximumRiskScore(exposures.map { it.getRiskScore(configuration) }.max()?.toInt() ?: 0)
.setAttenuationDurations(intArrayOf(
exposures.map { it.getAttenuationDurations(configuration)[0] }.sum(),
exposures.map { it.getAttenuationDurations(configuration)[1] }.sum(),
exposures.map { it.getAttenuationDurations(configuration)[2] }.sum()
))
.setSummationRiskScore(exposures.map { it.getRiskScore(configuration) }.sum())
.build()
database.use {
it.noteAppAction(packageName, "getExposureSummary", JSONObject().apply {
put("request_token", params.token)
put("response_days_since", response.daysSinceLastExposure)
put("response_matched_keys", response.matchedKeyCount)
put("response_max_risk", response.maximumRiskScore)
put("response_attenuation_durations", JSONArray().apply {
response.attenuationDurationsInMinutes.forEach { put(it) }
})
put("response_summation_risk", response.summationRiskScore)
}.toString())
}
try {
params.callback.onResult(Status.SUCCESS, response)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
override fun getExposureInformation(params: GetExposureInformationParams) {
// TODO: Notify user?
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
try {
params.callback.onResult(Status.INTERNAL_ERROR, null)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return
}
val response = database.findAllMeasuredExposures(packageName, params.token).map {
it.toExposureInformation(configuration)
}
database.use {
database.noteAppAction(packageName, "getExposureInformation", JSONObject().apply {
put("request_token", params.token)
put("response_size", response.size)
}.toString())
}
try {
params.callback.onResult(Status.SUCCESS, response)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
}

View File

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
class ExposurePreferences(private val context: Context) {
private var preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
var scannerEnabled
get() = preferences.getBoolean(PREF_SCANNER_ENABLED, false)
set(newStatus) {
preferences.edit().putBoolean(PREF_SCANNER_ENABLED, newStatus).commit()
if (newStatus) {
context.sendOrderedBroadcast(Intent(context, ServiceTrigger::class.java), null)
} else {
context.stopService(Intent(context, ScannerService::class.java))
context.stopService(Intent(context, AdvertiserService::class.java))
}
}
val advertiserEnabled
get() = scannerEnabled
companion object {
private const val PREF_SCANNER_ENABLED = "exposure_scanner_enabled"
}
}

View File

@ -0,0 +1,102 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.ExposureInformation
import com.google.android.gms.nearby.exposurenotification.RiskLevel
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import java.util.concurrent.TimeUnit
data class MeasuredExposure(val rpi: ByteArray, val aem: ByteArray, val timestamp: Long, val duration: Long, val rssi: Int, val notCorrectedAttenuation: Int = 0, val key: TemporaryExposureKey? = null) {
@RiskLevel
val transmissionRiskLevel: Int
get() = key?.transmissionRiskLevel ?: RiskLevel.RISK_LEVEL_INVALID
val durationInMinutes
get() = TimeUnit.MILLISECONDS.toMinutes(duration)
val daysSinceExposure
get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestamp)
val attenuation
get() = notCorrectedAttenuation - currentDeviceInfo.rssiCorrection
fun getAttenuationRiskScore(configuration: ExposureConfiguration): Int {
return when {
attenuation > 73 -> configuration.attenuationScores[0]
attenuation > 63 -> configuration.attenuationScores[1]
attenuation > 51 -> configuration.attenuationScores[2]
attenuation > 33 -> configuration.attenuationScores[3]
attenuation > 27 -> configuration.attenuationScores[4]
attenuation > 15 -> configuration.attenuationScores[5]
attenuation > 10 -> configuration.attenuationScores[6]
else -> configuration.attenuationScores[7]
}
}
fun getDaysSinceLastExposureRiskScore(configuration: ExposureConfiguration): Int {
return when {
daysSinceExposure >= 14 -> configuration.daysSinceLastExposureScores[0]
daysSinceExposure >= 12 -> configuration.daysSinceLastExposureScores[1]
daysSinceExposure >= 10 -> configuration.daysSinceLastExposureScores[2]
daysSinceExposure >= 8 -> configuration.daysSinceLastExposureScores[3]
daysSinceExposure >= 6 -> configuration.daysSinceLastExposureScores[4]
daysSinceExposure >= 4 -> configuration.daysSinceLastExposureScores[5]
daysSinceExposure >= 2 -> configuration.daysSinceLastExposureScores[6]
else -> configuration.daysSinceLastExposureScores[7]
}
}
fun getDurationRiskScore(configuration: ExposureConfiguration): Int {
return when {
durationInMinutes == 0L -> configuration.durationScores[0]
durationInMinutes <= 5 -> configuration.durationScores[1]
durationInMinutes <= 10 -> configuration.durationScores[2]
durationInMinutes <= 15 -> configuration.durationScores[3]
durationInMinutes <= 20 -> configuration.durationScores[4]
durationInMinutes <= 25 -> configuration.durationScores[5]
durationInMinutes <= 30 -> configuration.durationScores[6]
else -> configuration.durationScores[7]
}
}
fun getTransmissionRiskScore(configuration: ExposureConfiguration): Int {
return when (transmissionRiskLevel) {
RiskLevel.RISK_LEVEL_LOWEST -> configuration.transmissionRiskScores[0]
RiskLevel.RISK_LEVEL_LOW -> configuration.transmissionRiskScores[1]
RiskLevel.RISK_LEVEL_LOW_MEDIUM -> configuration.transmissionRiskScores[2]
RiskLevel.RISK_LEVEL_MEDIUM -> configuration.transmissionRiskScores[3]
RiskLevel.RISK_LEVEL_MEDIUM_HIGH -> configuration.transmissionRiskScores[4]
RiskLevel.RISK_LEVEL_HIGH -> configuration.transmissionRiskScores[5]
RiskLevel.RISK_LEVEL_VERY_HIGH -> configuration.transmissionRiskScores[6]
RiskLevel.RISK_LEVEL_HIGHEST -> configuration.transmissionRiskScores[7]
else -> 1
}
}
fun getRiskScore(configuration: ExposureConfiguration): Int {
return getAttenuationRiskScore(configuration) * getDaysSinceLastExposureRiskScore(configuration) * getDurationRiskScore(configuration) * getTransmissionRiskScore(configuration)
}
fun getAttenuationDurations(configuration: ExposureConfiguration): IntArray {
return when {
attenuation < configuration.durationAtAttenuationThresholds[0] -> intArrayOf(durationInMinutes.toInt(), 0, 0)
attenuation < configuration.durationAtAttenuationThresholds[1] -> intArrayOf(0, durationInMinutes.toInt(), 0)
else -> intArrayOf(0, 0, durationInMinutes.toInt())
}
}
fun toExposureInformation(configuration: ExposureConfiguration): ExposureInformation =
ExposureInformation.ExposureInformationBuilder()
.setDateMillisSinceEpoch(timestamp)
.setDurationMinutes(durationInMinutes.toInt())
.setAttenuationValue(attenuation)
.setTransmissionRiskLevel(transmissionRiskLevel)
.setTotalRiskScore(getRiskScore(configuration))
.setAttenuationDurations(getAttenuationDurations(configuration))
.build()
}

View File

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
import android.content.Intent
import android.os.IBinder
@TargetApi(21)
class ScannerService : Service() {
private var started = false
private lateinit var db: ExposureDatabase
private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
val data = result?.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
if (data.size < 16) return // Ignore invalid advertisements
db.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
}
}
private val scanner: BluetoothLeScanner
get() = BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (ExposurePreferences(this).scannerEnabled) {
startScan()
} else {
stopSelf()
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
stopScan()
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
@Synchronized
private fun startScan() {
if (started) return
db = ExposureDatabase(this)
scanner.startScan(
listOf(ScanFilter.Builder().setServiceUuid(SERVICE_UUID).setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)).build()),
ScanSettings.Builder().build(),
callback
)
started = true
}
@Synchronized
private fun stopScan() {
if (!started) return
scanner.stopScan(callback)
db.close()
started = false
}
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.microg.gms.common.ForegroundServiceContext
class ServiceTrigger : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context, intent: Intent?) {
if (ExposurePreferences(context).scannerEnabled) {
ForegroundServiceContext(context).startService(Intent(context, ScannerService::class.java))
}
if (ExposurePreferences(context).advertiserEnabled) {
ForegroundServiceContext(context).startService(Intent(context, AdvertiserService::class.java))
}
}
}

View File

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification;
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey;
import junit.framework.Test;
import junit.framework.TestCase;
import org.junit.Assert;
import java.util.Arrays;
public class CryptoTest extends TestCase {
private TemporaryExposureKey key;
@Override
protected void setUp() {
key = new TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(TestVectors.get_TEMPORARY_TRACING_KEY())
.setRollingStartIntervalNumber(TestVectors.CTINTERVAL_NUMBER_OF_GENERATED_KEY)
.setRollingPeriod(TestVectors.KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD)
.build();
}
@Override
protected void tearDown() {
key = null;
}
public void testGenerateRpiKey() {
Assert.assertArrayEquals(CryptoKt.generateRpiKey(key).getEncoded(), TestVectors.get_RPIK());
}
public void testGenerateAemKey() {
Assert.assertArrayEquals(CryptoKt.generateAemKey(key).getEncoded(), TestVectors.get_AEMK());
}
public void testGenerateRpiId() {
for (int i = 0; i < TestVectors.KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD; i++) {
byte[] gen = CryptoKt.generateRpiId(key, key.getRollingStartIntervalNumber() + i);
Assert.assertArrayEquals(gen, TestVectors.ADVERTISED_DATA.get(i).get_RPI());
}
}
public void testGeneratePayload() {
for (int i = 0; i < TestVectors.KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD; i++) {
byte[] gen = CryptoKt.generatePayload(key, key.getRollingStartIntervalNumber() + i, TestVectors.get_BLE_METADATA());
Assert.assertArrayEquals(gen, TestVectors.ADVERTISED_DATA.get(i).get_merged());
}
}
public void testGenerateAllRpiIds() {
byte[] all = CryptoKt.generateAllRpiIds(key);
for (int i = 0; i < TestVectors.KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD; i++) {
byte[] ref = CryptoKt.generateRpiId(key, key.getRollingStartIntervalNumber() + i);
byte[] gen = Arrays.copyOfRange(all, i * 16, (i + 1) * 16);
Assert.assertArrayEquals(gen, ref);
}
}
}

View File

@ -0,0 +1,976 @@
/*
* SPDX-FileCopyrightText: 2020, Google LLC
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are derived from work created and shared by Google and used
* according to the terms described in the Apache License, Version 2.0.
* See https://github.com/google/exposure-notifications-internals
*/
package org.microg.gms.nearby.exposurenotification;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class TestVectors {
public static byte[] asBytes(int... ints) {
byte[] bytes = new byte[ints.length];
for (int i = 0; i < ints.length; i++) {
int value = ints[i];
bytes[i] = (byte) value;
}
return bytes;
}
/**
* Class holding a matched pair of RPI and AEM values.
*/
public static class AdvertisedData {
private final byte[] rollingProximityIndicator;
private final byte[] associatedEncryptedMetadata;
AdvertisedData(byte[] rollingProximityIndicator, byte[] associatedEncryptedMetadata) {
this.rollingProximityIndicator = rollingProximityIndicator;
this.associatedEncryptedMetadata = associatedEncryptedMetadata;
}
public byte[] get_RPI() {
return rollingProximityIndicator.clone();
}
public byte[] get_AEM() {
return associatedEncryptedMetadata.clone();
}
public byte[] get_merged() {
byte[] bytes = new byte[rollingProximityIndicator.length + associatedEncryptedMetadata.length];
System.arraycopy(rollingProximityIndicator, 0, bytes, 0, rollingProximityIndicator.length);
System.arraycopy(associatedEncryptedMetadata, 0, bytes, rollingProximityIndicator.length, associatedEncryptedMetadata.length);
return bytes;
}
}
public static byte[] get_TEMPORARY_TRACING_KEY() {
return TEMPORARY_TRACING_KEY.clone();
}
public static byte[] get_RPIK() {
return RPIK.clone();
}
public static byte[] get_AEMK() {
return AEMK.clone();
}
public static byte[] get_BLE_METADATA() {
return BLE_METADATA.clone();
}
public static String get_RPIK_HKDF_INFO_STRING() {
return "EN-RPIK";
}
public static String get_RPI_AES_PADDED_STRING() {
return "EN-RPI";
}
public static String get_AEMK_HKDF_INFO_STRING() {
return "EN-AEMK";
}
// From TestVectors.h.txt
// ------------------------------------------------------------------------------
public static final int KEY_GENERATION_NSECONDS = 1585785600;
public static final int CTINTERVAL_NUMBER_OF_GENERATED_KEY = 2642976;
public static final int ID_ROLLING_PERIOD_MINUTES = 10;
public static final int KEY_ROLLING_PERIOD_MULTIPLE_OF_ID_PERIOD = 144;
private static final byte[] TEMPORARY_TRACING_KEY =
asBytes(
0x75, 0xc7, 0x34, 0xc6, 0xdd, 0x1a, 0x78, 0x2d, 0xe7, 0xa9, 0x65, 0xda, 0x5e, 0xb9, 0x31,
0x25);
private static final byte[] RPIK =
asBytes(
0x18, 0x5a, 0xd9, 0x1d, 0xb6, 0x9e, 0xc7, 0xdd, 0x04, 0x89, 0x60, 0xf1, 0xf3, 0xba, 0x61,
0x75);
private static final byte[] AEMK =
asBytes(
0xd5, 0x7c, 0x46, 0xaf, 0x7a, 0x1d, 0x83, 0x96, 0x5b, 0x9b, 0xed, 0x8b, 0xd1, 0x52, 0x93,
0x6a);
private static final byte[] BLE_METADATA = asBytes(0x40, 0x08, 0x00, 0x00);
private static final byte[] RPI0 =
asBytes(
0x8b, 0xe6, 0xcd, 0x37, 0x1c, 0x5c, 0x89, 0x16, 0x04, 0xbf, 0xbe, 0x49, 0xdf, 0x84, 0x50,
0x96);
private static final byte[] AEM0 = asBytes(0x72, 0x03, 0x38, 0x74);
private static final byte[] RPI1 =
asBytes(
0x3c, 0x9a, 0x1d, 0xe5, 0xdd, 0x6b, 0x02, 0xaf, 0xa7, 0xfd, 0xed, 0x7b, 0x57, 0x0b, 0x3e,
0x56);
private static final byte[] AEM1 = asBytes(0xc2, 0x92, 0x11, 0xb1);
private static final byte[] RPI2 =
asBytes(
0x24, 0x3f, 0xfe, 0x9a, 0x3b, 0x08, 0xbd, 0xed, 0x30, 0x94, 0xba, 0xc8, 0x63, 0x0b, 0xb8,
0xad);
private static final byte[] AEM2 = asBytes(0x6a, 0xdf, 0xad, 0x03);
private static final byte[] RPI3 =
asBytes(
0xdf, 0xc3, 0xed, 0x26, 0x5e, 0x97, 0xd0, 0xea, 0xbb, 0x63, 0x0e, 0x16, 0x8b, 0x42, 0x14,
0xed);
private static final byte[] AEM3 = asBytes(0xf1, 0xe2, 0xf8, 0x0b);
private static final byte[] RPI4 =
asBytes(
0xb3, 0xb8, 0x5a, 0x69, 0xeb, 0xae, 0xc7, 0x8d, 0xb7, 0x39, 0x85, 0x2d, 0x1f, 0x34, 0xe0,
0xfa);
private static final byte[] AEM4 = asBytes(0x56, 0x68, 0xc4, 0x74);
private static final byte[] RPI5 =
asBytes(
0x29, 0x8a, 0xbd, 0x6f, 0xda, 0xd2, 0x9e, 0xfb, 0xf0, 0xf8, 0x5a, 0x63, 0x95, 0x6c, 0xf1,
0x88);
private static final byte[] AEM5 = asBytes(0xf7, 0xb9, 0x7e, 0x84);
private static final byte[] RPI6 =
asBytes(
0x10, 0x5e, 0x82, 0x21, 0xdd, 0x60, 0x3d, 0x25, 0xb9, 0x4a, 0xba, 0x0c, 0x3c, 0xc8, 0xde,
0xe1);
private static final byte[] AEM6 = asBytes(0xee, 0xab, 0xfd, 0xc7);
private static final byte[] RPI7 =
asBytes(
0x98, 0x88, 0xca, 0x7e, 0x67, 0x38, 0xec, 0x4b, 0xc6, 0xe4, 0x20, 0xb2, 0x0f, 0x87, 0x8b,
0x3a);
private static final byte[] AEM7 = asBytes(0x9f, 0x84, 0xa9, 0xa6);
private static final byte[] RPI8 =
asBytes(
0x86, 0x94, 0xa7, 0x9a, 0xe9, 0x96, 0x71, 0xbe, 0x3f, 0x18, 0xaa, 0xf6, 0xb0, 0x90, 0x65,
0x7a);
private static final byte[] AEM8 = asBytes(0xc1, 0x1a, 0xcb, 0x4d);
private static final byte[] RPI9 =
asBytes(
0xb8, 0xcd, 0xe2, 0x8a, 0xd2, 0x0d, 0x1c, 0xd2, 0xfd, 0xd7, 0x36, 0x9d, 0xc0, 0xc6, 0xf7,
0xc8);
private static final byte[] AEM9 = asBytes(0x1f, 0x8d, 0x53, 0xde);
private static final byte[] RPI10 =
asBytes(
0xf6, 0x01, 0x23, 0xc6, 0x7b, 0xd2, 0xfc, 0x4b, 0x62, 0x00, 0x2b, 0x4b, 0x7d, 0x59, 0x5b,
0xa6);
private static final byte[] AEM10 = asBytes(0xe0, 0xa7, 0xf9, 0xf2);
private static final byte[] RPI11 =
asBytes(
0x8a, 0x84, 0xc8, 0x6e, 0x05, 0xf7, 0xa8, 0x77, 0x2a, 0xae, 0x7a, 0x80, 0x68, 0x6e, 0x1a,
0x1c);
private static final byte[] AEM11 = asBytes(0xf4, 0xb7, 0xb0, 0x9d);
private static final byte[] RPI12 =
asBytes(
0xac, 0x46, 0x1b, 0xf2, 0xb9, 0x3b, 0x0d, 0x90, 0x84, 0x17, 0x46, 0xf5, 0x1a, 0xde, 0xd6,
0xc0);
private static final byte[] AEM12 = asBytes(0x73, 0x91, 0xdd, 0x64);
private static final byte[] RPI13 =
asBytes(
0xf2, 0x56, 0x7e, 0xed, 0x0a, 0xa1, 0xcd, 0xf2, 0xcd, 0x3d, 0x0b, 0xd2, 0x82, 0x52, 0xf1,
0x96);
private static final byte[] AEM13 = asBytes(0x12, 0x7d, 0xb0, 0x9e);
private static final byte[] RPI14 =
asBytes(
0xa2, 0x55, 0x22, 0x5b, 0xaa, 0x9e, 0x37, 0xb7, 0x30, 0xa9, 0x5f, 0x99, 0x7a, 0x69, 0x72,
0xf5);
private static final byte[] AEM14 = asBytes(0x43, 0x5e, 0xa5, 0x56);
private static final byte[] RPI15 =
asBytes(
0x04, 0xcf, 0xae, 0xe4, 0x10, 0x21, 0xcb, 0x7d, 0x4d, 0x02, 0x0b, 0x30, 0x6b, 0x24, 0xbe,
0xa8);
private static final byte[] AEM15 = asBytes(0xdc, 0x1f, 0x19, 0x06);
private static final byte[] RPI16 =
asBytes(
0xf5, 0x37, 0x2b, 0xbc, 0x92, 0xf7, 0x80, 0x59, 0x64, 0x70, 0x1e, 0x87, 0x9b, 0x48, 0xd4,
0x31);
private static final byte[] AEM16 = asBytes(0xb3, 0xea, 0xaf, 0xdd);
private static final byte[] RPI17 =
asBytes(
0xf2, 0xc8, 0x11, 0x54, 0x63, 0x82, 0xb1, 0x0d, 0xf1, 0xac, 0x06, 0xc3, 0x2c, 0x61, 0x7b,
0xa7);
private static final byte[] AEM17 = asBytes(0xcd, 0x51, 0x86, 0xf9);
private static final byte[] RPI18 =
asBytes(
0xf8, 0xf3, 0x44, 0x1f, 0x76, 0x22, 0x3d, 0xac, 0x15, 0xec, 0x6b, 0x35, 0xfd, 0xb2, 0x51,
0x40);
private static final byte[] AEM18 = asBytes(0x7f, 0x38, 0x7e, 0x7f);
private static final byte[] RPI19 =
asBytes(
0xd0, 0x71, 0x83, 0xdb, 0x3c, 0x80, 0x45, 0x08, 0x7d, 0x61, 0xee, 0x9e, 0x73, 0x0c, 0x93,
0x06);
private static final byte[] AEM19 = asBytes(0x1a, 0x42, 0xa1, 0x5b);
private static final byte[] RPI20 =
asBytes(
0xc7, 0x42, 0xdd, 0x9c, 0x96, 0xa3, 0xe6, 0xfa, 0x7c, 0x4f, 0x22, 0x62, 0x1d, 0xac, 0xc2,
0x4d);
private static final byte[] AEM20 = asBytes(0x06, 0x39, 0x0d, 0xed);
private static final byte[] RPI21 =
asBytes(
0x08, 0x23, 0x33, 0xfa, 0xd9, 0xfa, 0x29, 0x2a, 0xb8, 0x99, 0xd6, 0x00, 0x0c, 0x65, 0x97,
0x97);
private static final byte[] AEM21 = asBytes(0x0f, 0x01, 0xbc, 0xd7);
private static final byte[] RPI22 =
asBytes(
0xbe, 0x43, 0x00, 0xda, 0xfb, 0x8d, 0x07, 0xc8, 0x8c, 0xb2, 0xb5, 0x07, 0x7a, 0x06, 0x11,
0x66);
private static final byte[] AEM22 = asBytes(0xc7, 0xbf, 0xbb, 0x92);
private static final byte[] RPI23 =
asBytes(
0x5f, 0xed, 0x1b, 0x4d, 0x3b, 0x3a, 0x13, 0x33, 0x2f, 0x05, 0x44, 0x75, 0x60, 0x35, 0x26,
0x32);
private static final byte[] AEM23 = asBytes(0x21, 0x7c, 0x8e, 0x4a);
private static final byte[] RPI24 =
asBytes(
0xfd, 0x1e, 0xe2, 0xcc, 0x5c, 0x60, 0xe6, 0xee, 0xe6, 0x1f, 0x04, 0x91, 0x9f, 0x67, 0x59,
0xa7);
private static final byte[] AEM24 = asBytes(0xee, 0x63, 0x9b, 0xd6);
private static final byte[] RPI25 =
asBytes(
0x96, 0xad, 0xf5, 0xb8, 0xdc, 0x7e, 0xe7, 0x5d, 0xf4, 0x6f, 0xbd, 0x8a, 0x1f, 0xc4, 0xad,
0x0d);
private static final byte[] AEM25 = asBytes(0x60, 0x8c, 0x13, 0x0f);
private static final byte[] RPI26 =
asBytes(
0xfa, 0x4b, 0xa2, 0x20, 0x6d, 0x42, 0xa1, 0xc8, 0x0d, 0x52, 0x48, 0xae, 0x68, 0x83, 0x09,
0xa4);
private static final byte[] AEM26 = asBytes(0x74, 0xb2, 0xc8, 0x73);
private static final byte[] RPI27 =
asBytes(
0xa5, 0x90, 0xf9, 0x5d, 0xbf, 0x24, 0x02, 0x61, 0xda, 0x10, 0x1a, 0x7c, 0xdb, 0x24, 0xdb,
0xba);
private static final byte[] AEM27 = asBytes(0x95, 0x6a, 0x95, 0xeb);
private static final byte[] RPI28 =
asBytes(
0x67, 0xe8, 0x1b, 0x91, 0xd1, 0xcf, 0x9e, 0x09, 0x58, 0x13, 0x54, 0x29, 0xda, 0xd0, 0x1e,
0x82);
private static final byte[] AEM28 = asBytes(0xc6, 0xfa, 0x3c, 0x7a);
private static final byte[] RPI29 =
asBytes(
0x18, 0x3c, 0xac, 0x22, 0x36, 0xc3, 0xe0, 0x53, 0x3b, 0xe4, 0x70, 0x4d, 0x83, 0x6e, 0x47,
0x55);
private static final byte[] AEM29 = asBytes(0xa2, 0xab, 0xc7, 0x03);
private static final byte[] RPI30 =
asBytes(
0x34, 0x11, 0x62, 0x55, 0x1c, 0x29, 0x31, 0x9b, 0xc5, 0x35, 0x38, 0xed, 0xfc, 0xf2, 0x30,
0x40);
private static final byte[] AEM30 = asBytes(0x80, 0x09, 0xb2, 0xaa);
private static final byte[] RPI31 =
asBytes(
0x22, 0x51, 0x68, 0x70, 0x18, 0x21, 0x4b, 0x65, 0xdd, 0x8e, 0xe8, 0x3e, 0xae, 0xd3, 0x30,
0xab);
private static final byte[] AEM31 = asBytes(0xcf, 0xe8, 0x04, 0xcb);
private static final byte[] RPI32 =
asBytes(
0xa4, 0xcf, 0x6e, 0x50, 0x21, 0x5f, 0xe2, 0x78, 0xcc, 0x5c, 0xff, 0x1b, 0x05, 0x34, 0xa3,
0xe0);
private static final byte[] AEM32 = asBytes(0xcb, 0x2a, 0x7e, 0x22);
private static final byte[] RPI33 =
asBytes(
0xdf, 0x8f, 0x2a, 0xc3, 0x03, 0x23, 0x2b, 0x2e, 0x5b, 0x3e, 0xfd, 0x86, 0x81, 0xaa, 0xa8,
0xdd);
private static final byte[] AEM33 = asBytes(0x65, 0x03, 0xa7, 0x27);
private static final byte[] RPI34 =
asBytes(
0xba, 0x2e, 0x75, 0xd7, 0xf4, 0x8c, 0xf5, 0x5c, 0x0c, 0x86, 0x8f, 0xd4, 0x5c, 0xf1, 0x6b,
0x5c);
private static final byte[] AEM34 = asBytes(0x8f, 0x29, 0x78, 0x7e);
private static final byte[] RPI35 =
asBytes(
0xec, 0x6a, 0x40, 0x05, 0x8d, 0xeb, 0xff, 0xff, 0x3c, 0x51, 0x97, 0x7f, 0x24, 0x56, 0x2e,
0x21);
private static final byte[] AEM35 = asBytes(0xb2, 0xad, 0xd3, 0xb7);
private static final byte[] RPI36 =
asBytes(
0x6a, 0x68, 0xe3, 0x0b, 0x2f, 0xb9, 0x3b, 0x5d, 0xf7, 0x8e, 0xe3, 0xa9, 0xa3, 0x50, 0xa6,
0xce);
private static final byte[] AEM36 = asBytes(0x1b, 0xfb, 0x46, 0x0a);
private static final byte[] RPI37 =
asBytes(
0x8d, 0x33, 0xa7, 0x05, 0x62, 0x62, 0x99, 0x94, 0xf8, 0xdf, 0x99, 0x05, 0x2b, 0x0e, 0xb6,
0x9a);
private static final byte[] AEM37 = asBytes(0x8e, 0xcf, 0x07, 0x7b);
private static final byte[] RPI38 =
asBytes(
0x21, 0x05, 0x3c, 0xcb, 0x8f, 0x92, 0x51, 0x11, 0xe2, 0x54, 0xbd, 0x69, 0x4e, 0x97, 0x94,
0x6b);
private static final byte[] AEM38 = asBytes(0x2b, 0xa7, 0x7b, 0x38);
private static final byte[] RPI39 =
asBytes(
0xe1, 0xc9, 0xcd, 0xf2, 0x0f, 0x90, 0x0a, 0xe6, 0xd2, 0x4b, 0xf7, 0xbc, 0xb4, 0xe6, 0x61,
0x35);
private static final byte[] AEM39 = asBytes(0x8c, 0x12, 0x40, 0xde);
private static final byte[] RPI40 =
asBytes(
0xba, 0xf7, 0x89, 0xf5, 0x50, 0x94, 0x6b, 0x43, 0x10, 0x64, 0x45, 0x07, 0x71, 0xb2, 0xa1,
0x43);
private static final byte[] AEM40 = asBytes(0x43, 0x89, 0x99, 0xb6);
private static final byte[] RPI41 =
asBytes(
0xf7, 0xf9, 0x1e, 0xc2, 0x50, 0x85, 0xd0, 0x35, 0x3e, 0x02, 0x78, 0xe5, 0x98, 0xcc, 0x62,
0x01);
private static final byte[] AEM41 = asBytes(0x1d, 0xcb, 0xdc, 0x6e);
private static final byte[] RPI42 =
asBytes(
0xc8, 0x93, 0x3d, 0x70, 0x22, 0x0b, 0xa9, 0xc8, 0xc1, 0x48, 0x39, 0x3a, 0x39, 0x59, 0xd2,
0x56);
private static final byte[] AEM42 = asBytes(0x63, 0x85, 0xff, 0xda);
private static final byte[] RPI43 =
asBytes(
0xf2, 0x93, 0x2e, 0x6e, 0x6e, 0xf6, 0x0f, 0x0f, 0x5b, 0xbc, 0xe4, 0x39, 0x10, 0x0a, 0x90,
0xe4);
private static final byte[] AEM43 = asBytes(0x69, 0x6c, 0x0c, 0xdf);
private static final byte[] RPI44 =
asBytes(
0x18, 0xf5, 0xd8, 0x10, 0x2a, 0x59, 0x30, 0xd8, 0x02, 0x30, 0xf2, 0xc3, 0x9a, 0x42, 0x66,
0xd6);
private static final byte[] AEM44 = asBytes(0x63, 0x84, 0x2b, 0xba);
private static final byte[] RPI45 =
asBytes(
0xe6, 0x07, 0x5c, 0x28, 0x93, 0x9f, 0xb0, 0xc6, 0x72, 0x46, 0xce, 0x38, 0xc5, 0xff, 0x93,
0x8a);
private static final byte[] AEM45 = asBytes(0xb7, 0xe2, 0x2c, 0x60);
private static final byte[] RPI46 =
asBytes(
0x66, 0x16, 0x88, 0x62, 0xbc, 0x44, 0x5f, 0x48, 0xe5, 0xb0, 0xed, 0x07, 0xe1, 0xdf, 0x3f,
0x5a);
private static final byte[] AEM46 = asBytes(0xd2, 0xe7, 0xd6, 0xd8);
private static final byte[] RPI47 =
asBytes(
0x1d, 0x0a, 0x01, 0xc3, 0x8d, 0xa4, 0xac, 0x41, 0xec, 0x7a, 0x63, 0x8f, 0x5d, 0xf7, 0x05,
0xa9);
private static final byte[] AEM47 = asBytes(0x91, 0xbe, 0x92, 0x2c);
private static final byte[] RPI48 =
asBytes(
0xa7, 0x37, 0x00, 0x1a, 0x2d, 0x2f, 0x80, 0x2c, 0x64, 0x78, 0x9a, 0x99, 0x52, 0xe6, 0xd1,
0xa7);
private static final byte[] AEM48 = asBytes(0x7e, 0x04, 0x21, 0xbb);
private static final byte[] RPI49 =
asBytes(
0x7c, 0x37, 0x25, 0xb6, 0x08, 0x4e, 0x68, 0x1f, 0xb3, 0x4d, 0x26, 0xc3, 0xa3, 0x94, 0xa6,
0x43);
private static final byte[] AEM49 = asBytes(0x7e, 0xa5, 0x20, 0x9f);
private static final byte[] RPI50 =
asBytes(
0x26, 0xd1, 0xf8, 0x36, 0x55, 0x7a, 0x25, 0x9a, 0x81, 0xb5, 0xdb, 0x54, 0x19, 0xc6, 0xa7,
0x29);
private static final byte[] AEM50 = asBytes(0x1c, 0x92, 0x06, 0x28);
private static final byte[] RPI51 =
asBytes(
0xeb, 0xc2, 0xa6, 0x06, 0x28, 0x54, 0xd1, 0xec, 0x62, 0x7b, 0x1f, 0x6e, 0x84, 0x32, 0xe1,
0x66);
private static final byte[] AEM51 = asBytes(0x4a, 0x76, 0x46, 0x32);
private static final byte[] RPI52 =
asBytes(
0x11, 0x32, 0x74, 0xe8, 0x0c, 0x31, 0xcf, 0xcd, 0x81, 0xc2, 0xad, 0x08, 0x64, 0x44, 0x51,
0x78);
private static final byte[] AEM52 = asBytes(0x69, 0xa7, 0x49, 0xb6);
private static final byte[] RPI53 =
asBytes(
0x45, 0x67, 0x97, 0x6c, 0x48, 0xbe, 0x72, 0x59, 0x06, 0x24, 0x7d, 0x0b, 0xd8, 0x1b, 0xb8,
0x11);
private static final byte[] AEM53 = asBytes(0x6f, 0x94, 0x85, 0xdb);
private static final byte[] RPI54 =
asBytes(
0x74, 0x81, 0x54, 0xba, 0x52, 0x3a, 0x1a, 0xa8, 0x10, 0xb7, 0x06, 0x2a, 0x13, 0xe5, 0xaa,
0x68);
private static final byte[] AEM54 = asBytes(0x74, 0x78, 0x07, 0x23);
private static final byte[] RPI55 =
asBytes(
0x30, 0xbc, 0xeb, 0x33, 0x45, 0x74, 0x51, 0x53, 0x35, 0x23, 0x65, 0x99, 0x85, 0x87, 0xcd,
0x10);
private static final byte[] AEM55 = asBytes(0x89, 0x17, 0xda, 0x61);
private static final byte[] RPI56 =
asBytes(
0x8f, 0x2d, 0x7b, 0x87, 0x00, 0xa8, 0x2f, 0xd4, 0x51, 0x4d, 0xfa, 0x42, 0x02, 0xee, 0x29,
0x8f);
private static final byte[] AEM56 = asBytes(0xfe, 0xc4, 0xf8, 0xb1);
private static final byte[] RPI57 =
asBytes(
0x0e, 0x66, 0x49, 0x53, 0x70, 0x0c, 0xdf, 0xc0, 0xd2, 0x79, 0x2f, 0xad, 0xf0, 0x73, 0x29,
0xeb);
private static final byte[] AEM57 = asBytes(0x78, 0x1a, 0x3e, 0xaf);
private static final byte[] RPI58 =
asBytes(
0xf4, 0x49, 0x58, 0xc4, 0xdd, 0x70, 0xd9, 0x96, 0x8a, 0x26, 0xfd, 0x60, 0xba, 0x92, 0x72,
0x90);
private static final byte[] AEM58 = asBytes(0xb9, 0x75, 0x9b, 0x61);
private static final byte[] RPI59 =
asBytes(
0x55, 0xfd, 0x2f, 0x6c, 0xbd, 0xe0, 0xe1, 0x3f, 0xd2, 0x2c, 0x0b, 0x3d, 0xb1, 0x62, 0x28,
0xe5);
private static final byte[] AEM59 = asBytes(0x48, 0x3c, 0x94, 0x10);
private static final byte[] RPI60 =
asBytes(
0x49, 0xf3, 0xf9, 0xd1, 0x24, 0x69, 0xdc, 0xc9, 0xed, 0x35, 0x63, 0x64, 0xc3, 0x00, 0x66,
0xe4);
private static final byte[] AEM60 = asBytes(0x2d, 0xa6, 0xeb, 0xac);
private static final byte[] RPI61 =
asBytes(
0xc5, 0x7d, 0x2d, 0x6e, 0x0d, 0x25, 0xa0, 0x65, 0x1c, 0xd7, 0x27, 0x86, 0xf8, 0xc9, 0x51,
0xce);
private static final byte[] AEM61 = asBytes(0x43, 0xea, 0xcf, 0x34);
private static final byte[] RPI62 =
asBytes(
0x88, 0xef, 0x25, 0x63, 0x51, 0xac, 0x49, 0xdf, 0xd1, 0x5a, 0xb5, 0xa2, 0xde, 0x97, 0xc0,
0x13);
private static final byte[] AEM62 = asBytes(0xb9, 0xa1, 0x36, 0x53);
private static final byte[] RPI63 =
asBytes(
0xd0, 0xfb, 0x6f, 0xd6, 0xdb, 0x89, 0xda, 0x52, 0x36, 0x1f, 0x1a, 0x30, 0xfb, 0x43, 0x6c,
0xe7);
private static final byte[] AEM63 = asBytes(0x35, 0x6d, 0xea, 0x55);
private static final byte[] RPI64 =
asBytes(
0x8a, 0x42, 0xa3, 0x30, 0xf0, 0x19, 0x28, 0xe5, 0x16, 0x31, 0x23, 0x19, 0x81, 0x60, 0x3f,
0xd5);
private static final byte[] AEM64 = asBytes(0x35, 0x05, 0xb7, 0xf3);
private static final byte[] RPI65 =
asBytes(
0x6e, 0x51, 0xb2, 0xa2, 0xae, 0xcb, 0xab, 0x1d, 0xf8, 0x08, 0x26, 0xef, 0x6d, 0x1e, 0x19,
0x58);
private static final byte[] AEM65 = asBytes(0x5c, 0x05, 0xcd, 0x94);
private static final byte[] RPI66 =
asBytes(
0x3c, 0xe6, 0x81, 0xa2, 0x8b, 0x1b, 0xe1, 0x9c, 0x9e, 0x36, 0xb9, 0xc5, 0x80, 0xb1, 0x23,
0xab);
private static final byte[] AEM66 = asBytes(0x98, 0x47, 0x45, 0xd6);
private static final byte[] RPI67 =
asBytes(
0x1e, 0x95, 0x8e, 0xd8, 0x9b, 0x86, 0xb9, 0x89, 0x77, 0xf7, 0x9e, 0x1b, 0x83, 0xf3, 0xd0,
0x5f);
private static final byte[] AEM67 = asBytes(0x93, 0xfd, 0xf7, 0x08);
private static final byte[] RPI68 =
asBytes(
0x1d, 0x66, 0x7b, 0x01, 0xd6, 0x63, 0x4b, 0x5f, 0x3b, 0x6f, 0x33, 0xac, 0x4b, 0x15, 0x0d,
0x23);
private static final byte[] AEM68 = asBytes(0xd7, 0x0f, 0x74, 0x4c);
private static final byte[] RPI69 =
asBytes(
0x67, 0xf2, 0x22, 0x15, 0x6c, 0x51, 0x7d, 0xeb, 0xc0, 0x70, 0x68, 0xcb, 0xc5, 0xee, 0xc1,
0xdd);
private static final byte[] AEM69 = asBytes(0x60, 0x57, 0x8c, 0x31);
private static final byte[] RPI70 =
asBytes(
0xa8, 0x45, 0x4b, 0x9c, 0x94, 0x7d, 0x16, 0x25, 0xee, 0x3f, 0xba, 0x26, 0x07, 0xc2, 0x3a,
0xff);
private static final byte[] AEM70 = asBytes(0x79, 0x00, 0xee, 0xad);
private static final byte[] RPI71 =
asBytes(
0x41, 0x6f, 0xfc, 0x7a, 0x32, 0xfc, 0xfd, 0xa9, 0xa3, 0x16, 0xd0, 0x17, 0x90, 0xe3, 0x19,
0x45);
private static final byte[] AEM71 = asBytes(0xc1, 0x22, 0xad, 0x68);
private static final byte[] RPI72 =
asBytes(
0xc1, 0x9a, 0x30, 0xc3, 0x9c, 0x9c, 0x3a, 0x08, 0x9b, 0xca, 0xdd, 0xe1, 0xc6, 0x69, 0x94,
0x47);
private static final byte[] AEM72 = asBytes(0x34, 0x0a, 0xa3, 0x82);
private static final byte[] RPI73 =
asBytes(
0x78, 0x6d, 0xdf, 0xae, 0x6f, 0xc7, 0x7c, 0x4c, 0x41, 0x0c, 0x4e, 0xc3, 0x2d, 0x34, 0x24,
0x7d);
private static final byte[] AEM73 = asBytes(0x67, 0xc6, 0x0d, 0xfb);
private static final byte[] RPI74 =
asBytes(
0xef, 0x0f, 0xd3, 0xa9, 0x5b, 0x96, 0x61, 0xe1, 0xfc, 0xcb, 0x4e, 0x30, 0xcd, 0xe3, 0x2c,
0x51);
private static final byte[] AEM74 = asBytes(0x45, 0x56, 0xb6, 0x73);
private static final byte[] RPI75 =
asBytes(
0xfc, 0x1f, 0x8a, 0x66, 0xf4, 0x05, 0xcc, 0xb6, 0x3d, 0xc3, 0xe4, 0x82, 0x07, 0xda, 0x77,
0x88);
private static final byte[] AEM75 = asBytes(0x9e, 0x4f, 0x1d, 0xb4);
private static final byte[] RPI76 =
asBytes(
0x0e, 0xac, 0xc2, 0x86, 0x31, 0xb1, 0x0f, 0x44, 0x98, 0x36, 0x86, 0x66, 0x13, 0x0f, 0xf0,
0xc9);
private static final byte[] AEM76 = asBytes(0x5b, 0xe0, 0x4e, 0x9d);
private static final byte[] RPI77 =
asBytes(
0xe8, 0xdb, 0x4a, 0x46, 0x26, 0x38, 0x5a, 0xe6, 0xe3, 0xb2, 0x45, 0x1d, 0x0a, 0x66, 0xed,
0xbf);
private static final byte[] AEM77 = asBytes(0x80, 0x6a, 0xf2, 0xf9);
private static final byte[] RPI78 =
asBytes(
0xc6, 0x00, 0x67, 0x8d, 0x4f, 0xbe, 0x92, 0x45, 0xad, 0x49, 0x73, 0xb1, 0xc8, 0x97, 0x1b,
0xc5);
private static final byte[] AEM78 = asBytes(0x7a, 0xf8, 0xd4, 0xfd);
private static final byte[] RPI79 =
asBytes(
0x08, 0x19, 0x26, 0xc3, 0x61, 0x83, 0x4c, 0x5c, 0x1d, 0x43, 0x19, 0xb8, 0x40, 0xf3, 0x15,
0xae);
private static final byte[] AEM79 = asBytes(0xfc, 0x8d, 0xdd, 0xd0);
private static final byte[] RPI80 =
asBytes(
0x1d, 0x82, 0x9e, 0xaf, 0xa0, 0x42, 0x32, 0xa6, 0xbb, 0x4d, 0x3c, 0x20, 0x22, 0xac, 0x3d,
0x0f);
private static final byte[] AEM80 = asBytes(0xda, 0x88, 0x15, 0x68);
private static final byte[] RPI81 =
asBytes(
0x79, 0x8a, 0xbc, 0xe8, 0xc8, 0xf6, 0x25, 0x10, 0xde, 0x59, 0x5a, 0x99, 0x73, 0xfb, 0x21,
0x8e);
private static final byte[] AEM81 = asBytes(0x13, 0x95, 0xcf, 0x7b);
private static final byte[] RPI82 =
asBytes(
0x61, 0xfc, 0x0d, 0xeb, 0x47, 0x08, 0x4f, 0xda, 0xf1, 0x48, 0x3e, 0x34, 0x2d, 0x73, 0xb1,
0x48);
private static final byte[] AEM82 = asBytes(0xd9, 0xe2, 0x5d, 0xb6);
private static final byte[] RPI83 =
asBytes(
0x49, 0x69, 0xed, 0xd4, 0x0f, 0x3e, 0xab, 0x46, 0x8b, 0x8a, 0x8d, 0x49, 0x68, 0x5a, 0x4e,
0xdd);
private static final byte[] AEM83 = asBytes(0xc1, 0xb2, 0x05, 0x97);
private static final byte[] RPI84 =
asBytes(
0x1d, 0x69, 0xd6, 0xd4, 0x15, 0x8a, 0x31, 0x8f, 0x1d, 0x9c, 0xaf, 0xba, 0x13, 0x58, 0x70,
0x17);
private static final byte[] AEM84 = asBytes(0xeb, 0x53, 0x91, 0x99);
private static final byte[] RPI85 =
asBytes(
0xab, 0x7c, 0x61, 0xf1, 0xcc, 0xa8, 0x13, 0xfd, 0x36, 0xe8, 0xf1, 0xb1, 0xe5, 0xdf, 0x6a,
0x0f);
private static final byte[] AEM85 = asBytes(0xc8, 0x1a, 0xb8, 0x54);
private static final byte[] RPI86 =
asBytes(
0x94, 0xf2, 0x4f, 0xde, 0xc1, 0x7a, 0xa3, 0x1f, 0x74, 0xf2, 0x02, 0xae, 0x4a, 0x68, 0x74,
0xbe);
private static final byte[] AEM86 = asBytes(0x46, 0xee, 0x82, 0xdc);
private static final byte[] RPI87 =
asBytes(
0x6e, 0x5d, 0xe0, 0x25, 0x79, 0xd3, 0xdf, 0xb1, 0x94, 0xcc, 0x7b, 0xd4, 0x92, 0x70, 0x25,
0x3d);
private static final byte[] AEM87 = asBytes(0x8a, 0x05, 0x28, 0xbd);
private static final byte[] RPI88 =
asBytes(
0xfe, 0x3e, 0x1e, 0x36, 0x21, 0x77, 0x3f, 0x18, 0x80, 0x40, 0xaa, 0x5d, 0xb3, 0xff, 0x1d,
0x4e);
private static final byte[] AEM88 = asBytes(0x28, 0x03, 0x3a, 0xae);
private static final byte[] RPI89 =
asBytes(
0xd7, 0x37, 0x6e, 0x0d, 0x77, 0x25, 0x5d, 0xe2, 0x3d, 0x54, 0x0f, 0x02, 0x71, 0x83, 0xf1,
0xba);
private static final byte[] AEM89 = asBytes(0xa0, 0x1c, 0xe7, 0x12);
private static final byte[] RPI90 =
asBytes(
0x64, 0xa7, 0x1e, 0x48, 0xa5, 0x0e, 0x7b, 0x5a, 0x37, 0xac, 0x91, 0x81, 0x6e, 0x2b, 0x0f,
0x53);
private static final byte[] AEM90 = asBytes(0xc9, 0xcb, 0x8c, 0x70);
private static final byte[] RPI91 =
asBytes(
0x0f, 0x22, 0xa2, 0xc0, 0xb6, 0x99, 0xe1, 0x89, 0xd5, 0x9e, 0x30, 0xd1, 0x74, 0x5d, 0x67,
0xd3);
private static final byte[] AEM91 = asBytes(0xdb, 0xe6, 0x8f, 0x47);
private static final byte[] RPI92 =
asBytes(
0xde, 0x87, 0x6b, 0xaf, 0x31, 0x22, 0x8e, 0x3b, 0x7f, 0xe0, 0xf0, 0x8e, 0x1f, 0x38, 0xea,
0x7b);
private static final byte[] AEM92 = asBytes(0x4c, 0xb1, 0xd9, 0x4f);
private static final byte[] RPI93 =
asBytes(
0x8c, 0x69, 0x27, 0xbc, 0xf5, 0xf7, 0xae, 0xe1, 0xee, 0xd8, 0xab, 0xbe, 0x43, 0xe2, 0xe2,
0xd1);
private static final byte[] AEM93 = asBytes(0x27, 0x74, 0x06, 0x32);
private static final byte[] RPI94 =
asBytes(
0xbc, 0x96, 0x83, 0x0f, 0x18, 0x2a, 0x72, 0xc8, 0x9e, 0x65, 0xce, 0xa9, 0xc4, 0x7d, 0x88,
0xc0);
private static final byte[] AEM94 = asBytes(0xe8, 0xad, 0xeb, 0x6d);
private static final byte[] RPI95 =
asBytes(
0x7b, 0x2f, 0xbe, 0x74, 0x6d, 0xd2, 0xda, 0x86, 0xe9, 0x86, 0x6a, 0x0e, 0x6a, 0xad, 0xbc,
0x4d);
private static final byte[] AEM95 = asBytes(0xe5, 0x3a, 0x5a, 0xc7);
private static final byte[] RPI96 =
asBytes(
0x93, 0xe2, 0x0f, 0x14, 0xa1, 0x3d, 0x56, 0x56, 0x75, 0x94, 0xaa, 0x96, 0x23, 0x50, 0xf1,
0x70);
private static final byte[] AEM96 = asBytes(0x94, 0x32, 0x42, 0xa4);
private static final byte[] RPI97 =
asBytes(
0x9e, 0x38, 0x14, 0xf9, 0x51, 0xfa, 0x03, 0x79, 0x6b, 0x9a, 0x66, 0xf8, 0x9a, 0x9f, 0x40,
0x0d);
private static final byte[] AEM97 = asBytes(0x06, 0x48, 0xdc, 0x89);
private static final byte[] RPI98 =
asBytes(
0x95, 0xfa, 0x09, 0x84, 0x5a, 0xa8, 0xd4, 0xb6, 0x00, 0x47, 0xfa, 0xf9, 0x9a, 0xeb, 0xca,
0x0c);
private static final byte[] AEM98 = asBytes(0x85, 0x5e, 0x31, 0xb3);
private static final byte[] RPI99 =
asBytes(
0xef, 0x0a, 0x75, 0x79, 0x33, 0x18, 0x53, 0xb9, 0xeb, 0xc2, 0x50, 0xb4, 0xd6, 0xf3, 0xeb,
0xcc);
private static final byte[] AEM99 = asBytes(0x9c, 0x1f, 0x07, 0xc2);
private static final byte[] RPI100 =
asBytes(
0x7f, 0xe9, 0xa9, 0x0d, 0xe0, 0x0c, 0x9f, 0x07, 0x37, 0xd3, 0xb4, 0x5f, 0xda, 0x65, 0x11,
0x15);
private static final byte[] AEM100 = asBytes(0x98, 0x4e, 0x1f, 0xf3);
private static final byte[] RPI101 =
asBytes(
0x44, 0x3b, 0x7b, 0x5a, 0xb9, 0xa8, 0x6a, 0x1f, 0xee, 0x67, 0xe1, 0x8c, 0xb8, 0xc4, 0x07,
0x64);
private static final byte[] AEM101 = asBytes(0xb3, 0xfb, 0xa7, 0xe1);
private static final byte[] RPI102 =
asBytes(
0xe8, 0xa6, 0xfa, 0x9a, 0x5c, 0xa9, 0xfb, 0x06, 0x1c, 0x4c, 0xdb, 0xe2, 0x17, 0xc6, 0x1d,
0x59);
private static final byte[] AEM102 = asBytes(0x3b, 0x6d, 0x9d, 0xe8);
private static final byte[] RPI103 =
asBytes(
0x08, 0x7a, 0x08, 0x04, 0x06, 0x86, 0xfd, 0x63, 0x5e, 0xf3, 0x89, 0x75, 0x27, 0x41, 0xcc,
0x1f);
private static final byte[] AEM103 = asBytes(0xf0, 0x85, 0x55, 0x1c);
private static final byte[] RPI104 =
asBytes(
0x58, 0x7c, 0x04, 0x86, 0x64, 0x4c, 0xeb, 0x2d, 0x0b, 0x7e, 0xbd, 0xd3, 0x9d, 0xd3, 0xa8,
0x60);
private static final byte[] AEM104 = asBytes(0x0a, 0x6b, 0x32, 0x6e);
private static final byte[] RPI105 =
asBytes(
0xdd, 0x82, 0x7b, 0xa6, 0x0f, 0x8a, 0x35, 0xb1, 0xdd, 0x4e, 0x4c, 0xdf, 0xe4, 0x9c, 0x42,
0x63);
private static final byte[] AEM105 = asBytes(0x81, 0xeb, 0x20, 0xe2);
private static final byte[] RPI106 =
asBytes(
0xcf, 0x23, 0x40, 0x00, 0x08, 0x0a, 0x4e, 0x8d, 0xa8, 0xfe, 0xb5, 0x33, 0xaa, 0x59, 0x04,
0xd3);
private static final byte[] AEM106 = asBytes(0x34, 0xd8, 0x6e, 0xe5);
private static final byte[] RPI107 =
asBytes(
0xc5, 0x0f, 0xb1, 0xec, 0x3e, 0xf5, 0x4e, 0x91, 0x61, 0x78, 0xca, 0x9d, 0x56, 0xee, 0x4f,
0x5c);
private static final byte[] AEM107 = asBytes(0xea, 0xea, 0xc3, 0xd2);
private static final byte[] RPI108 =
asBytes(
0xd4, 0x5f, 0xde, 0x46, 0xf4, 0x67, 0xd7, 0x6e, 0xd2, 0x8d, 0xd4, 0xd2, 0x49, 0x6d, 0xcb,
0x7f);
private static final byte[] AEM108 = asBytes(0xea, 0xa6, 0x4a, 0x8e);
private static final byte[] RPI109 =
asBytes(
0xe9, 0xba, 0xf8, 0x1c, 0x96, 0x1f, 0x51, 0x0d, 0x08, 0xa6, 0x63, 0xc7, 0x52, 0x4f, 0x36,
0xdf);
private static final byte[] AEM109 = asBytes(0x1e, 0x2c, 0xa3, 0x7b);
private static final byte[] RPI110 =
asBytes(
0xe3, 0xb0, 0x2c, 0xc0, 0x31, 0xe1, 0x44, 0xfa, 0xe6, 0x1a, 0x38, 0x99, 0x50, 0x1b, 0x49,
0x21);
private static final byte[] AEM110 = asBytes(0x44, 0xc0, 0xd7, 0x06);
private static final byte[] RPI111 =
asBytes(
0xa7, 0x5e, 0xea, 0x58, 0x23, 0x2e, 0x73, 0x66, 0xce, 0xa1, 0xa0, 0xe7, 0x2d, 0xa0, 0xce,
0xc5);
private static final byte[] AEM111 = asBytes(0x5a, 0x8c, 0x79, 0xb7);
private static final byte[] RPI112 =
asBytes(
0x6a, 0xd7, 0x62, 0x1f, 0xe1, 0xda, 0x01, 0x39, 0xff, 0x8b, 0xad, 0x7f, 0x37, 0x9c, 0xab,
0xf6);
private static final byte[] AEM112 = asBytes(0x27, 0x9f, 0x16, 0xb1);
private static final byte[] RPI113 =
asBytes(
0x6f, 0xe2, 0xac, 0x45, 0xf6, 0x5c, 0x8a, 0xc6, 0x9f, 0xdc, 0x5e, 0xf7, 0xfa, 0x9f, 0xf7,
0xf0);
private static final byte[] AEM113 = asBytes(0x4b, 0xd9, 0x07, 0xaa);
private static final byte[] RPI114 =
asBytes(
0x2f, 0xbe, 0xc6, 0x8f, 0xd2, 0x7d, 0xdd, 0xdb, 0x42, 0x23, 0x04, 0x4e, 0xfc, 0x77, 0x98,
0x51);
private static final byte[] AEM114 = asBytes(0x2d, 0xf6, 0xc5, 0xeb);
private static final byte[] RPI115 =
asBytes(
0x32, 0xbf, 0x68, 0x8a, 0x7c, 0x83, 0x4b, 0xe1, 0xbf, 0xab, 0x7c, 0x8e, 0x0e, 0x58, 0x0a,
0xdb);
private static final byte[] AEM115 = asBytes(0x66, 0x85, 0x2d, 0x43);
private static final byte[] RPI116 =
asBytes(
0xda, 0xe3, 0xa7, 0xd8, 0xc6, 0x24, 0x27, 0xb0, 0x9c, 0x0e, 0x7b, 0xbf, 0x48, 0x9d, 0x34,
0xbd);
private static final byte[] AEM116 = asBytes(0x83, 0x6d, 0x3b, 0x95);
private static final byte[] RPI117 =
asBytes(
0x3c, 0x4b, 0x02, 0xbd, 0x5e, 0xd2, 0x8c, 0x67, 0x82, 0x9c, 0x97, 0x79, 0x10, 0x79, 0xaf,
0xd2);
private static final byte[] AEM117 = asBytes(0x27, 0x35, 0xb9, 0x97);
private static final byte[] RPI118 =
asBytes(
0xe2, 0xfa, 0xea, 0xc3, 0xdb, 0xd1, 0x50, 0xec, 0x8e, 0xa8, 0xe7, 0xb3, 0xe5, 0xbb, 0x84,
0x54);
private static final byte[] AEM118 = asBytes(0xd7, 0x1f, 0x97, 0xc2);
private static final byte[] RPI119 =
asBytes(
0x69, 0x94, 0x2a, 0x72, 0x13, 0xea, 0xf3, 0xc1, 0x4a, 0x69, 0x99, 0x6b, 0xa6, 0xc6, 0xbf,
0xeb);
private static final byte[] AEM119 = asBytes(0x53, 0xbc, 0x4d, 0xb5);
private static final byte[] RPI120 =
asBytes(
0x1c, 0x5c, 0x4d, 0xd2, 0x54, 0x52, 0xe9, 0x7d, 0xd1, 0x87, 0xdd, 0x7c, 0xe1, 0xd1, 0xee,
0x81);
private static final byte[] AEM120 = asBytes(0x48, 0xa4, 0xd3, 0x79);
private static final byte[] RPI121 =
asBytes(
0xfb, 0xf5, 0x60, 0x7a, 0x7c, 0x61, 0x2a, 0xce, 0xd1, 0x60, 0xe7, 0x55, 0xa9, 0x87, 0x26,
0x2d);
private static final byte[] AEM121 = asBytes(0xb7, 0x8d, 0xc1, 0xf5);
private static final byte[] RPI122 =
asBytes(
0x3e, 0x2d, 0xe1, 0x30, 0x70, 0xf2, 0x74, 0x43, 0xd9, 0xba, 0x3e, 0xb4, 0x3f, 0x9a, 0x71,
0xea);
private static final byte[] AEM122 = asBytes(0x58, 0x21, 0x70, 0xca);
private static final byte[] RPI123 =
asBytes(
0x8a, 0x12, 0xd2, 0x5f, 0x00, 0x6f, 0xab, 0x5a, 0x27, 0x07, 0xda, 0x9e, 0x6c, 0x4e, 0x96,
0xbe);
private static final byte[] AEM123 = asBytes(0x93, 0x95, 0x94, 0xcc);
private static final byte[] RPI124 =
asBytes(
0x6f, 0xd9, 0x8c, 0x22, 0xe2, 0x27, 0x83, 0x8e, 0x6f, 0x67, 0x36, 0x97, 0x64, 0x43, 0x77,
0x25);
private static final byte[] AEM124 = asBytes(0xc1, 0x4f, 0x5b, 0x11);
private static final byte[] RPI125 =
asBytes(
0x3d, 0xa2, 0x12, 0xae, 0xbd, 0xb7, 0x8b, 0xa8, 0x19, 0x80, 0x9d, 0x03, 0xc6, 0xcf, 0x56,
0xe2);
private static final byte[] AEM125 = asBytes(0x30, 0x09, 0x12, 0xda);
private static final byte[] RPI126 =
asBytes(
0x8c, 0x48, 0xda, 0x73, 0xe2, 0x9e, 0xff, 0xc9, 0xb7, 0x4b, 0xb0, 0x97, 0x09, 0x6e, 0x0a,
0x0a);
private static final byte[] AEM126 = asBytes(0xce, 0x79, 0xc5, 0x0a);
private static final byte[] RPI127 =
asBytes(
0xe5, 0x3c, 0x68, 0xb4, 0xb0, 0x1c, 0x68, 0xf3, 0x7e, 0x65, 0xa0, 0xdc, 0x8e, 0x67, 0xf4,
0x5d);
private static final byte[] AEM127 = asBytes(0xe0, 0x4f, 0x38, 0x67);
private static final byte[] RPI128 =
asBytes(
0x83, 0x31, 0xdd, 0xe6, 0x36, 0x4b, 0x11, 0x95, 0x27, 0xaf, 0x76, 0xfe, 0xe1, 0x7a, 0xab,
0xcf);
private static final byte[] AEM128 = asBytes(0x64, 0x18, 0x7b, 0xdf);
private static final byte[] RPI129 =
asBytes(
0x8c, 0x14, 0x47, 0x1f, 0x55, 0x71, 0x92, 0x63, 0x96, 0xdd, 0xe6, 0xf7, 0xb7, 0xb3, 0x5b,
0x56);
private static final byte[] AEM129 = asBytes(0xe8, 0x7c, 0x05, 0xfd);
private static final byte[] RPI130 =
asBytes(
0x4e, 0xb6, 0xb2, 0xde, 0xb4, 0x0e, 0x5e, 0xc9, 0xbc, 0x39, 0x83, 0x81, 0x02, 0xa4, 0xf4,
0xf9);
private static final byte[] AEM130 = asBytes(0x4e, 0x70, 0x83, 0x25);
private static final byte[] RPI131 =
asBytes(
0x77, 0xf2, 0x14, 0x1c, 0xef, 0xfd, 0x0a, 0xa3, 0xbe, 0xe4, 0xb6, 0x7c, 0x45, 0x0d, 0x9a,
0xa6);
private static final byte[] AEM131 = asBytes(0xa1, 0x57, 0xeb, 0x59);
private static final byte[] RPI132 =
asBytes(
0x04, 0x3d, 0x78, 0xe2, 0x0c, 0xb5, 0x9c, 0x0b, 0xcb, 0x15, 0x78, 0xff, 0x93, 0xea, 0x54,
0x4a);
private static final byte[] AEM132 = asBytes(0x8d, 0x30, 0x43, 0x2a);
private static final byte[] RPI133 =
asBytes(
0x65, 0xb8, 0xec, 0xc4, 0x56, 0x1c, 0x1c, 0xca, 0x05, 0x3d, 0x81, 0x4f, 0xfd, 0x89, 0x61,
0xd4);
private static final byte[] AEM133 = asBytes(0x5b, 0x24, 0x38, 0x90);
private static final byte[] RPI134 =
asBytes(
0x32, 0xf2, 0x5a, 0x17, 0x24, 0xf2, 0xbd, 0xca, 0xd0, 0x5a, 0xbc, 0x14, 0x82, 0xe1, 0x32,
0x9e);
private static final byte[] AEM134 = asBytes(0x74, 0x90, 0xd2, 0x11);
private static final byte[] RPI135 =
asBytes(
0x20, 0x3b, 0xa3, 0xf3, 0xf7, 0x23, 0x02, 0x66, 0xb9, 0x93, 0xb3, 0xee, 0x7b, 0x2d, 0x86,
0x08);
private static final byte[] AEM135 = asBytes(0x1f, 0x01, 0x9d, 0xf2);
private static final byte[] RPI136 =
asBytes(
0xe5, 0xe7, 0xa4, 0x70, 0x69, 0x21, 0x6e, 0x1a, 0x88, 0x7b, 0x90, 0xef, 0x03, 0x94, 0xa3,
0x5c);
private static final byte[] AEM136 = asBytes(0x99, 0xe6, 0x12, 0xb7);
private static final byte[] RPI137 =
asBytes(
0x3f, 0xfc, 0x8b, 0xb9, 0x1d, 0xbc, 0xd8, 0xee, 0x92, 0x49, 0x48, 0xf5, 0x08, 0x0b, 0x19,
0x0d);
private static final byte[] AEM137 = asBytes(0x26, 0x4a, 0x57, 0x7e);
private static final byte[] RPI138 =
asBytes(
0x32, 0x37, 0x53, 0x91, 0x07, 0x7f, 0xbf, 0x76, 0x86, 0xba, 0xfa, 0x7d, 0xc1, 0x56, 0xbe,
0x1c);
private static final byte[] AEM138 = asBytes(0xa8, 0x11, 0xba, 0x62);
private static final byte[] RPI139 =
asBytes(
0xa8, 0x90, 0x49, 0x65, 0xae, 0xc5, 0xdd, 0xb6, 0x55, 0xe9, 0x70, 0x07, 0xd2, 0x23, 0xdb,
0x48);
private static final byte[] AEM139 = asBytes(0x2b, 0x48, 0x33, 0x21);
private static final byte[] RPI140 =
asBytes(
0x41, 0x5c, 0xf7, 0xaf, 0x1d, 0xc9, 0xfe, 0xac, 0xb3, 0x97, 0x84, 0x88, 0xf5, 0x04, 0x68,
0x93);
private static final byte[] AEM140 = asBytes(0xe2, 0xef, 0x53, 0x7e);
private static final byte[] RPI141 =
asBytes(
0x86, 0xf8, 0xdd, 0xc0, 0xc9, 0x3e, 0x53, 0xe3, 0xa5, 0x82, 0xe6, 0x1f, 0x01, 0xf7, 0xdf,
0x2f);
private static final byte[] AEM141 = asBytes(0x71, 0x1d, 0xbf, 0x6d);
private static final byte[] RPI142 =
asBytes(
0xa6, 0x5e, 0xf7, 0xba, 0x97, 0x52, 0xc4, 0x17, 0x3b, 0xa4, 0x8a, 0x33, 0x84, 0x9c, 0x5e,
0x52);
private static final byte[] AEM142 = asBytes(0x59, 0x2d, 0xbc, 0xa8);
private static final byte[] RPI143 =
asBytes(
0xf4, 0x31, 0xb6, 0x2e, 0xcf, 0x44, 0x31, 0x02, 0xce, 0x4e, 0xd0, 0x40, 0x7d, 0xe5, 0x4b,
0xd4);
private static final byte[] AEM143 = asBytes(0x12, 0x15, 0xe5, 0x7e);
// ------------------------------------------------------------------------------
public static final List<AdvertisedData> ADVERTISED_DATA =
Collections.unmodifiableList(Arrays.asList(
new AdvertisedData(RPI0, AEM0),
new AdvertisedData(RPI1, AEM1),
new AdvertisedData(RPI2, AEM2),
new AdvertisedData(RPI3, AEM3),
new AdvertisedData(RPI4, AEM4),
new AdvertisedData(RPI5, AEM5),
new AdvertisedData(RPI6, AEM6),
new AdvertisedData(RPI7, AEM7),
new AdvertisedData(RPI8, AEM8),
new AdvertisedData(RPI9, AEM9),
new AdvertisedData(RPI10, AEM10),
new AdvertisedData(RPI11, AEM11),
new AdvertisedData(RPI12, AEM12),
new AdvertisedData(RPI13, AEM13),
new AdvertisedData(RPI14, AEM14),
new AdvertisedData(RPI15, AEM15),
new AdvertisedData(RPI16, AEM16),
new AdvertisedData(RPI17, AEM17),
new AdvertisedData(RPI18, AEM18),
new AdvertisedData(RPI19, AEM19),
new AdvertisedData(RPI20, AEM20),
new AdvertisedData(RPI21, AEM21),
new AdvertisedData(RPI22, AEM22),
new AdvertisedData(RPI23, AEM23),
new AdvertisedData(RPI24, AEM24),
new AdvertisedData(RPI25, AEM25),
new AdvertisedData(RPI26, AEM26),
new AdvertisedData(RPI27, AEM27),
new AdvertisedData(RPI28, AEM28),
new AdvertisedData(RPI29, AEM29),
new AdvertisedData(RPI30, AEM30),
new AdvertisedData(RPI31, AEM31),
new AdvertisedData(RPI32, AEM32),
new AdvertisedData(RPI33, AEM33),
new AdvertisedData(RPI34, AEM34),
new AdvertisedData(RPI35, AEM35),
new AdvertisedData(RPI36, AEM36),
new AdvertisedData(RPI37, AEM37),
new AdvertisedData(RPI38, AEM38),
new AdvertisedData(RPI39, AEM39),
new AdvertisedData(RPI40, AEM40),
new AdvertisedData(RPI41, AEM41),
new AdvertisedData(RPI42, AEM42),
new AdvertisedData(RPI43, AEM43),
new AdvertisedData(RPI44, AEM44),
new AdvertisedData(RPI45, AEM45),
new AdvertisedData(RPI46, AEM46),
new AdvertisedData(RPI47, AEM47),
new AdvertisedData(RPI48, AEM48),
new AdvertisedData(RPI49, AEM49),
new AdvertisedData(RPI50, AEM50),
new AdvertisedData(RPI51, AEM51),
new AdvertisedData(RPI52, AEM52),
new AdvertisedData(RPI53, AEM53),
new AdvertisedData(RPI54, AEM54),
new AdvertisedData(RPI55, AEM55),
new AdvertisedData(RPI56, AEM56),
new AdvertisedData(RPI57, AEM57),
new AdvertisedData(RPI58, AEM58),
new AdvertisedData(RPI59, AEM59),
new AdvertisedData(RPI60, AEM60),
new AdvertisedData(RPI61, AEM61),
new AdvertisedData(RPI62, AEM62),
new AdvertisedData(RPI63, AEM63),
new AdvertisedData(RPI64, AEM64),
new AdvertisedData(RPI65, AEM65),
new AdvertisedData(RPI66, AEM66),
new AdvertisedData(RPI67, AEM67),
new AdvertisedData(RPI68, AEM68),
new AdvertisedData(RPI69, AEM69),
new AdvertisedData(RPI70, AEM70),
new AdvertisedData(RPI71, AEM71),
new AdvertisedData(RPI72, AEM72),
new AdvertisedData(RPI73, AEM73),
new AdvertisedData(RPI74, AEM74),
new AdvertisedData(RPI75, AEM75),
new AdvertisedData(RPI76, AEM76),
new AdvertisedData(RPI77, AEM77),
new AdvertisedData(RPI78, AEM78),
new AdvertisedData(RPI79, AEM79),
new AdvertisedData(RPI80, AEM80),
new AdvertisedData(RPI81, AEM81),
new AdvertisedData(RPI82, AEM82),
new AdvertisedData(RPI83, AEM83),
new AdvertisedData(RPI84, AEM84),
new AdvertisedData(RPI85, AEM85),
new AdvertisedData(RPI86, AEM86),
new AdvertisedData(RPI87, AEM87),
new AdvertisedData(RPI88, AEM88),
new AdvertisedData(RPI89, AEM89),
new AdvertisedData(RPI90, AEM90),
new AdvertisedData(RPI91, AEM91),
new AdvertisedData(RPI92, AEM92),
new AdvertisedData(RPI93, AEM93),
new AdvertisedData(RPI94, AEM94),
new AdvertisedData(RPI95, AEM95),
new AdvertisedData(RPI96, AEM96),
new AdvertisedData(RPI97, AEM97),
new AdvertisedData(RPI98, AEM98),
new AdvertisedData(RPI99, AEM99),
new AdvertisedData(RPI100, AEM100),
new AdvertisedData(RPI101, AEM101),
new AdvertisedData(RPI102, AEM102),
new AdvertisedData(RPI103, AEM103),
new AdvertisedData(RPI104, AEM104),
new AdvertisedData(RPI105, AEM105),
new AdvertisedData(RPI106, AEM106),
new AdvertisedData(RPI107, AEM107),
new AdvertisedData(RPI108, AEM108),
new AdvertisedData(RPI109, AEM109),
new AdvertisedData(RPI110, AEM110),
new AdvertisedData(RPI111, AEM111),
new AdvertisedData(RPI112, AEM112),
new AdvertisedData(RPI113, AEM113),
new AdvertisedData(RPI114, AEM114),
new AdvertisedData(RPI115, AEM115),
new AdvertisedData(RPI116, AEM116),
new AdvertisedData(RPI117, AEM117),
new AdvertisedData(RPI118, AEM118),
new AdvertisedData(RPI119, AEM119),
new AdvertisedData(RPI120, AEM120),
new AdvertisedData(RPI121, AEM121),
new AdvertisedData(RPI122, AEM122),
new AdvertisedData(RPI123, AEM123),
new AdvertisedData(RPI124, AEM124),
new AdvertisedData(RPI125, AEM125),
new AdvertisedData(RPI126, AEM126),
new AdvertisedData(RPI127, AEM127),
new AdvertisedData(RPI128, AEM128),
new AdvertisedData(RPI129, AEM129),
new AdvertisedData(RPI130, AEM130),
new AdvertisedData(RPI131, AEM131),
new AdvertisedData(RPI132, AEM132),
new AdvertisedData(RPI133, AEM133),
new AdvertisedData(RPI134, AEM134),
new AdvertisedData(RPI135, AEM135),
new AdvertisedData(RPI136, AEM136),
new AdvertisedData(RPI137, AEM137),
new AdvertisedData(RPI138, AEM138),
new AdvertisedData(RPI139, AEM139),
new AdvertisedData(RPI140, AEM140),
new AdvertisedData(RPI141, AEM141),
new AdvertisedData(RPI142, AEM142),
new AdvertisedData(RPI143, AEM143)));
private TestVectors() {
}
}

View File

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
android {
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
}
dependencies {
api project(':play-services-base')
api project(':play-services-nearby-api')
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest package="org.microg.gms.nearby"/>

View File

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby;
import android.content.Context;
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient;
import org.microg.gms.common.PublicApi;
import org.microg.gms.nearby.ExposureNotificationClientImpl;
@PublicApi
public class Nearby {
public static ExposureNotificationClient getExposureNotificationClient(Context context) {
return new ExposureNotificationClientImpl(context);
}
}

View File

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
import com.google.android.gms.common.api.Api;
import com.google.android.gms.common.api.HasApiKey;
import com.google.android.gms.tasks.Task;
import org.microg.gms.common.PublicApi;
import org.microg.gms.nearby.exposurenotification.Constants;
import java.io.File;
import java.util.List;
@PublicApi
public interface ExposureNotificationClient extends HasApiKey<Api.ApiOptions.NoOptions> {
String ACTION_EXPOSURE_NOTIFICATION_SETTINGS = Constants.ACTION_EXPOSURE_NOTIFICATION_SETTINGS;
String ACTION_EXPOSURE_NOT_FOUND = Constants.ACTION_EXPOSURE_NOT_FOUND;
String ACTION_EXPOSURE_STATE_UPDATED = Constants.ACTION_EXPOSURE_STATE_UPDATED;
String EXTRA_EXPOSURE_SUMMARY = Constants.EXTRA_EXPOSURE_SUMMARY;
String EXTRA_TOKEN = Constants.EXTRA_TOKEN;
Task<Void> start();
Task<Void> stop();
Task<Boolean> isEnabled();
Task<List<TemporaryExposureKey>> getTemporaryExposureKeyHistory();
Task<Void> provideDiagnosisKeys(List<File> keys, ExposureConfiguration configuration, String token);
Task<ExposureSummary> getExposureSummary(String token);
Task<List<ExposureInformation>> getExposureInformation(String token);
}

View File

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureInformationParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureSummaryParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetTemporaryExposureKeyHistoryParams;
import com.google.android.gms.nearby.exposurenotification.internal.INearbyExposureNotificationService;
import com.google.android.gms.nearby.exposurenotification.internal.IsEnabledParams;
import com.google.android.gms.nearby.exposurenotification.internal.ProvideDiagnosisKeysParams;
import com.google.android.gms.nearby.exposurenotification.internal.StartParams;
import com.google.android.gms.nearby.exposurenotification.internal.StopParams;
import org.microg.gms.common.GmsClient;
import org.microg.gms.common.GmsService;
import org.microg.gms.common.api.ConnectionCallbacks;
import org.microg.gms.common.api.OnConnectionFailedListener;
public class ExposureNotificationApiClient extends GmsClient<INearbyExposureNotificationService> {
public ExposureNotificationApiClient(Context context, ConnectionCallbacks callbacks, OnConnectionFailedListener connectionFailedListener) {
super(context, callbacks, connectionFailedListener, GmsService.NEARBY_EXPOSURE.ACTION);
serviceId = GmsService.NEARBY_EXPOSURE.SERVICE_ID;
}
@Override
protected INearbyExposureNotificationService interfaceFromBinder(IBinder binder) {
return INearbyExposureNotificationService.Stub.asInterface(binder);
}
public void start(StartParams params) throws RemoteException {
getServiceInterface().start(params);
}
public void stop(StopParams params) throws RemoteException {
getServiceInterface().stop(params);
}
public void isEnabled(IsEnabledParams params) throws RemoteException {
getServiceInterface().isEnabled(params);
}
public void getTemporaryExposureKeyHistory(GetTemporaryExposureKeyHistoryParams params) throws RemoteException {
getServiceInterface().getTemporaryExposureKeyHistory(params);
}
public void provideDiagnosisKeys(ProvideDiagnosisKeysParams params) throws RemoteException {
getServiceInterface().provideDiagnosisKeys(params);
}
public void getExposureSummary(GetExposureSummaryParams params) throws RemoteException {
getServiceInterface().getExposureSummary(params);
}
public void getExposureInformation(GetExposureInformationParams params) throws RemoteException {
getServiceInterface().getExposureInformation(params);
}
}

View File

@ -0,0 +1,179 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby;
import android.content.Context;
import com.google.android.gms.common.api.Api;
import com.google.android.gms.common.api.GoogleApi;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.api.internal.ApiKey;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration;
import com.google.android.gms.nearby.exposurenotification.ExposureInformation;
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient;
import com.google.android.gms.nearby.exposurenotification.ExposureSummary;
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureInformationParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureSummaryParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetTemporaryExposureKeyHistoryParams;
import com.google.android.gms.nearby.exposurenotification.internal.IBooleanCallback;
import com.google.android.gms.nearby.exposurenotification.internal.IExposureInformationListCallback;
import com.google.android.gms.nearby.exposurenotification.internal.IExposureSummaryCallback;
import com.google.android.gms.nearby.exposurenotification.internal.ITemporaryExposureKeyListCallback;
import com.google.android.gms.nearby.exposurenotification.internal.IsEnabledParams;
import com.google.android.gms.nearby.exposurenotification.internal.StartParams;
import com.google.android.gms.nearby.exposurenotification.internal.StopParams;
import com.google.android.gms.tasks.Task;
import org.microg.gms.common.api.PendingGoogleApiCall;
import java.io.File;
import java.util.List;
public class ExposureNotificationClientImpl extends GoogleApi<Api.ApiOptions.NoOptions> implements ExposureNotificationClient {
private static final Api<Api.ApiOptions.NoOptions> API = new Api<>((options, context, looper, clientSettings, callbacks, connectionFailedListener) -> new ExposureNotificationApiClient(context, callbacks, connectionFailedListener));
public ExposureNotificationClientImpl(Context context) {
super(context, API);
}
@Override
public Task<Void> start() {
return scheduleTask((PendingGoogleApiCall<Void, ExposureNotificationApiClient>) (client, completionSource) -> {
StartParams params = new StartParams(new IStatusCallback.Stub() {
@Override
public void onResult(Status status) {
if (status == Status.SUCCESS) {
completionSource.setResult(null);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
});
try {
client.start(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public Task<Void> stop() {
return scheduleTask((PendingGoogleApiCall<Void, ExposureNotificationApiClient>) (client, completionSource) -> {
StopParams params = new StopParams(new IStatusCallback.Stub() {
@Override
public void onResult(Status status) {
if (status == Status.SUCCESS) {
completionSource.setResult(null);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
});
try {
client.stop(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public Task<Boolean> isEnabled() {
return scheduleTask((PendingGoogleApiCall<Boolean, ExposureNotificationApiClient>) (client, completionSource) -> {
IsEnabledParams params = new IsEnabledParams(new IBooleanCallback.Stub() {
@Override
public void onResult(Status status, boolean result) {
if (status == Status.SUCCESS) {
completionSource.setResult(result);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
});
try {
client.isEnabled(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public Task<List<TemporaryExposureKey>> getTemporaryExposureKeyHistory() {
return scheduleTask((PendingGoogleApiCall<List<TemporaryExposureKey>, ExposureNotificationApiClient>) (client, completionSource) -> {
GetTemporaryExposureKeyHistoryParams params = new GetTemporaryExposureKeyHistoryParams(new ITemporaryExposureKeyListCallback.Stub() {
@Override
public void onResult(Status status, List<TemporaryExposureKey> result) {
if (status == Status.SUCCESS) {
completionSource.setResult(result);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
});
try {
client.getTemporaryExposureKeyHistory(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public Task<Void> provideDiagnosisKeys(List<File> keys, ExposureConfiguration configuration, String token) {
return null;
}
@Override
public Task<ExposureSummary> getExposureSummary(String token) {
return scheduleTask((PendingGoogleApiCall<ExposureSummary, ExposureNotificationApiClient>) (client, completionSource) -> {
GetExposureSummaryParams params = new GetExposureSummaryParams(new IExposureSummaryCallback.Stub() {
@Override
public void onResult(Status status, ExposureSummary result) {
if (status == Status.SUCCESS) {
completionSource.setResult(result);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
}, token);
try {
client.getExposureSummary(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public Task<List<ExposureInformation>> getExposureInformation(String token) {
return scheduleTask((PendingGoogleApiCall<List<ExposureInformation>, ExposureNotificationApiClient>) (client, completionSource) -> {
GetExposureInformationParams params = new GetExposureInformationParams(new IExposureInformationListCallback.Stub() {
@Override
public void onResult(Status status, List<ExposureInformation> result) {
if (status == Status.SUCCESS) {
completionSource.setResult(result);
} else {
completionSource.setException(new RuntimeException("Status: " + status));
}
}
}, token);
try {
client.getExposureInformation(params);
} catch (Exception e) {
completionSource.setException(e);
}
});
}
@Override
public ApiKey<Api.ApiOptions.NoOptions> getApiKey() {
return null;
}
}

View File

@ -19,3 +19,7 @@ wire {
compileKotlin {
kotlinOptions.jvmTarget = 1.8
}
compileTestKotlin {
kotlinOptions.jvmTarget = 1.8
}

View File

@ -8,7 +8,7 @@ include ':play-services-cast-api'
include ':play-services-cast-framework-api'
include ':play-services-iid-api'
include ':play-services-location-api'
//include ':play-services-nearby-api'
include ':play-services-nearby-api'
include ':play-services-wearable-api'
include ':play-services-api'
@ -16,7 +16,7 @@ include ':play-services-api'
include ':firebase-dynamic-links-api'
include ':play-services-core-proto'
//include ':play-services-nearby-core-proto'
include ':play-services-nearby-core-proto'
include ':play-services-wearable-proto'
include ':play-services-base-core'
@ -24,7 +24,7 @@ include ':play-services-location-core'
include ':play-services-maps-core-mapbox'
include ':play-services-maps-core-vtm'
include ':play-services-maps-core-vtm:vtm-microg-theme'
//include ':play-services-nearby-core'
include ':play-services-nearby-core'
include ':play-services-core:microg-ui-tools' // Legacy
include ':play-services-core'
@ -34,7 +34,7 @@ include ':play-services-cast'
include ':play-services-gcm'
include ':play-services-iid'
include ':play-services-location'
//include ':play-services-nearby'
include ':play-services-nearby'
include ':play-services-wearable'
include ':play-services'