mirror of
https://github.com/YTVanced/VancedMicroG
synced 2024-12-01 07:33:02 +00:00
Add initial Exposure Notification API implementation
This commit is contained in:
parent
af28a78bba
commit
5f70d943cb
92 changed files with 4921 additions and 19 deletions
|
@ -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'
|
||||
|
|
|
@ -19,3 +19,7 @@ wire {
|
|||
compileKotlin {
|
||||
kotlinOptions.jvmTarget = 1.8
|
||||
}
|
||||
|
||||
compileTestKotlin {
|
||||
kotlinOptions.jvmTarget = 1.8
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
18
play-services-core/src/main/res/drawable/ic_check_list.xml
Normal file
18
play-services-core/src/main/res/drawable/ic_check_list.xml
Normal 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>
|
17
play-services-core/src/main/res/drawable/ic_open.xml
Normal file
17
play-services-core/src/main/res/drawable/ic_open.xml
Normal 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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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've been near about potential exposure.
|
||||
|
||||
Your identity or test result won'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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
28
play-services-nearby-api/build.gradle
Normal file
28
play-services-nearby-api/build.gradle
Normal 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')
|
||||
}
|
6
play-services-nearby-api/src/main/AndroidManifest.xml
Normal file
6
play-services-nearby-api/src/main/AndroidManifest.xml
Normal 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"/>
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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";
|
||||
}
|
23
play-services-nearby-core-proto/build.gradle
Normal file
23
play-services-nearby-core-proto/build.gradle
Normal 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
|
||||
}
|
|
@ -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;
|
||||
}
|
50
play-services-nearby-core/build.gradle
Normal file
50
play-services-nearby-core/build.gradle
Normal 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
|
||||
}
|
||||
}
|
42
play-services-nearby-core/src/main/AndroidManifest.xml
Normal file
42
play-services-nearby-core/src/main/AndroidManifest.xml
Normal 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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
27
play-services-nearby/build.gradle
Normal file
27
play-services-nearby/build.gradle
Normal 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')
|
||||
}
|
6
play-services-nearby/src/main/AndroidManifest.xml
Normal file
6
play-services-nearby/src/main/AndroidManifest.xml
Normal 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"/>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -19,3 +19,7 @@ wire {
|
|||
compileKotlin {
|
||||
kotlinOptions.jvmTarget = 1.8
|
||||
}
|
||||
|
||||
compileTestKotlin {
|
||||
kotlinOptions.jvmTarget = 1.8
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue