EN: Request users to enable Bluetoooth/Location access

This commit is contained in:
Marvin W 2020-12-13 15:37:07 +01:00
parent 3dad397dd1
commit a814c7de7e
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
21 changed files with 489 additions and 72 deletions

View File

@ -54,6 +54,7 @@ public class ForegroundServiceContext extends ContextWrapper {
context.getSystemService(NotificationManager.class).createNotificationChannel(channel);
return new Notification.Builder(context, channel.getId())
.setOngoing(true)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle("Running in background")
.setContentText("microG " + context.getClass().getSimpleName() + " is running in background.")
.build();

View File

@ -9,6 +9,9 @@
<uses-sdk tools:overrideLibrary="com.db.williamchart" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application>
<activity
android:name="org.microg.gms.nearby.core.ui.ExposureNotificationsConfirmActivity"
@ -26,6 +29,7 @@
android:theme="@style/Theme.AppCompat.DayNight">
<intent-filter android:priority="-100">
<action android:name="com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>

View File

@ -92,9 +92,11 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
}
reportedExposures.removeAll()
reportedExposures.addPreference(reportedExposuresNone)
if (mergedExposures.isNullOrEmpty()) {
reportedExposures.addPreference(reportedExposuresNone)
reportedExposuresNone.isVisible = true
} else {
reportedExposuresNone.isVisible = false
for (exposure in mergedExposures) {
val minAttenuation = exposure.subs.map { it.attenuation }.minOrNull() ?: exposure.attenuation
val nearby = exposure.attenuation < 63 || minAttenuation < 55

View File

@ -5,18 +5,26 @@
package org.microg.gms.nearby.core.ui
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.ResultReceiver
import android.provider.Settings
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.lifecycleScope
import org.microg.gms.nearby.exposurenotification.*
import org.microg.gms.ui.getApplicationInfoIfExists
class ExposureNotificationsConfirmActivity : AppCompatActivity() {
private var resultCode: Int = RESULT_CANCELED
set(value) {
@ -44,6 +52,8 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() {
findViewById<TextView>(R.id.grant_permission_summary).text = getString(R.string.exposure_confirm_permission_description, selfApplicationInfo?.loadLabel(packageManager)
?: packageName)
checkPermissions()
checkBluetooth()
checkLocation()
}
CONFIRM_ACTION_STOP -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_stop_title)
@ -72,8 +82,28 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() {
findViewById<Button>(R.id.grant_permission_button).setOnClickListener {
requestPermissions()
}
findViewById<Button>(R.id.enable_bluetooth_button).setOnClickListener {
requestBluetooth()
}
findViewById<Button>(R.id.enable_location_button).setOnClickListener {
requestLocation()
}
}
override fun onResume() {
super.onResume()
if (permissionNeedsHandling) checkPermissions()
if (bluetoothNeedsHandling) checkBluetooth()
if (locationNeedsHandling) checkLocation()
}
private fun updateButton() {
findViewById<Button>(android.R.id.button1).isEnabled = !permissionNeedsHandling && !bluetoothNeedsHandling && !locationNeedsHandling
}
// Permissions
private var permissionNeedsHandling: Boolean = false
private var permissionRequestCode = 33
private val permissions by lazy {
if (Build.VERSION.SDK_INT >= 29) {
arrayOf("android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION")
@ -81,22 +111,70 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() {
arrayOf("android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION")
}
}
private var requestCode = 33
private fun checkPermissions() {
val needRequest = Build.VERSION.SDK_INT >= 23 && permissions.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
findViewById<Button>(android.R.id.button1).isEnabled = !needRequest
findViewById<View>(R.id.grant_permission_view).visibility = if (needRequest) View.VISIBLE else View.GONE
permissionNeedsHandling = Build.VERSION.SDK_INT >= 23 && permissions.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
findViewById<View>(R.id.grant_permission_view).visibility = if (permissionNeedsHandling) View.VISIBLE else View.GONE
updateButton()
}
private fun requestPermissions() {
if (Build.VERSION.SDK_INT >= 23) {
requestPermissions(permissions, ++requestCode)
requestPermissions(permissions, ++permissionRequestCode)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == this.requestCode) checkPermissions()
if (requestCode == this.permissionRequestCode) checkPermissions()
}
// Bluetooth
private var bluetoothNeedsHandling: Boolean = false
private var bluetoothRequestCode = 112
private fun checkBluetooth() {
val adapter = BluetoothAdapter.getDefaultAdapter()
bluetoothNeedsHandling = adapter?.isEnabled != true
findViewById<View>(R.id.enable_bluetooth_view).visibility = if (adapter?.isEnabled == false) View.VISIBLE else View.GONE
updateButton()
}
private fun requestBluetooth() {
val adapter = BluetoothAdapter.getDefaultAdapter()
findViewById<View>(R.id.enable_bluetooth_spinner).visibility = View.VISIBLE
findViewById<View>(R.id.enable_bluetooth_button).visibility = View.INVISIBLE
lifecycleScope.launchWhenStarted {
if (adapter != null && !adapter.enableAsync(this@ExposureNotificationsConfirmActivity)) {
requestBluetoothViaIntent()
} else {
checkBluetooth()
}
findViewById<View>(R.id.enable_bluetooth_spinner).visibility = View.INVISIBLE
findViewById<View>(R.id.enable_bluetooth_button).visibility = View.VISIBLE
}
}
private fun requestBluetoothViaIntent() {
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(intent, ++bluetoothRequestCode)
}
// Location
private var locationNeedsHandling: Boolean = false
private var locationRequestCode = 231
private fun checkLocation() {
locationNeedsHandling = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager)
findViewById<View>(R.id.enable_location_view).visibility = if (locationNeedsHandling) View.VISIBLE else View.GONE
updateButton()
}
private fun requestLocation() {
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivityForResult(intent, ++locationRequestCode)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == bluetoothRequestCode) checkBluetooth()
}
override fun finish() {

View File

@ -12,16 +12,16 @@ import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.util.Log
import android.view.View
import androidx.core.location.LocationManagerCompat
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 org.microg.gms.nearby.exposurenotification.AdvertiserService
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
import org.microg.gms.nearby.exposurenotification.ScannerService
import org.microg.gms.nearby.exposurenotification.getExposureNotificationsServiceInfo
import org.microg.gms.nearby.exposurenotification.*
import org.microg.gms.ui.AppIconPreference
import org.microg.gms.ui.getApplicationInfoIfExists
import org.microg.gms.ui.navigate
@ -37,6 +37,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureAppsNone: Preference
private lateinit var collectedRpis: Preference
private lateinit var advertisingId: Preference
private var turningBluetoothOn: Boolean = false
private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() }
@ -63,8 +64,19 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
}
exposureBluetoothOff.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivity(intent)
lifecycleScope.launchWhenStarted {
turningBluetoothOn = true
it.isVisible = false
val adapter = BluetoothAdapter.getDefaultAdapter()
if (adapter != null && !adapter.enableAsync(requireContext())) {
turningBluetoothOn = false
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(intent, 144)
} else {
turningBluetoothOn = false
updateStatus()
}
}
true
}
@ -88,23 +100,6 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
handler.removeCallbacks(updateContentRunnable)
}
private fun isLocationEnabled(): Boolean {
val lm = requireContext().getSystemService(LOCATION_SERVICE) as LocationManager
var gpsEnabled = false
var networkEnabled = false
try {
gpsEnabled = lm.isProviderEnabled(LocationManager.GPS_PROVIDER)
} catch (ex: Exception) {
}
try {
networkEnabled = lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (ex: Exception) {
}
return gpsEnabled || networkEnabled
}
private fun updateStatus() {
lifecycleScope.launchWhenResumed {
handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL)
@ -114,8 +109,8 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
val bluetoothSupported = ScannerService.isSupported(requireContext())
val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(requireContext()) else bluetoothSupported
exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !isLocationEnabled()
exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null
exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(requireContext().getSystemService(LOCATION_SERVICE) as LocationManager)
exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn
exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false
exposureBluetoothNoAdvertisement.isVisible = enabled && bluetoothSupported == true && advertisingSupported != true

View File

@ -5,6 +5,7 @@
-->
<LinearLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -41,23 +42,40 @@
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
<RelativeLayout
android:id="@+id/grant_permission_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="?attr/colorAccent"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/grant_permission_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignTop="@id/grant_permission_summary"
android:layout_alignBottom="@id/grant_permission_summary"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_alert"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/grant_permission_summary"
style="@style/TextAppearance.AppCompat.Small.Inverse"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@id/grant_permission_icon"
android:layout_weight="1"
android:padding="16dp"
android:text="@string/exposure_confirm_permission_description" />
<Button
@ -65,11 +83,121 @@
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="16dp"
android:layout_below="@id/grant_permission_summary"
android:layout_alignLeft="@id/grant_permission_summary"
android:layout_marginLeft="-16dp"
android:text="@string/exposure_confirm_permission_button"
android:textColor="?android:attr/textColorPrimaryInverse" />
</LinearLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/enable_bluetooth_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="?attr/colorAccent"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/enable_bluetooth_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignTop="@id/enable_bluetooth_summary"
android:layout_alignBottom="@id/enable_bluetooth_summary"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_bluetooth_off"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/enable_bluetooth_summary"
style="@style/TextAppearance.AppCompat.Small.Inverse"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@id/enable_bluetooth_icon"
android:layout_weight="1"
android:text="@string/exposure_confirm_bluetooth_description" />
<Button
android:id="@+id/enable_bluetooth_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/enable_bluetooth_summary"
android:layout_alignLeft="@id/enable_bluetooth_summary"
android:layout_marginLeft="-16dp"
android:text="@string/exposure_confirm_button"
android:textColor="?android:attr/textColorPrimaryInverse" />
<ProgressBar
android:id="@+id/enable_bluetooth_spinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@id/enable_bluetooth_button"
android:layout_alignTop="@id/enable_bluetooth_button"
android:layout_alignRight="@id/enable_bluetooth_button"
android:layout_alignBottom="@id/enable_bluetooth_button"
android:indeterminate="true"
android:indeterminateTint="?attr/colorPrimary"
android:padding="8dp"
android:visibility="invisible" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/enable_location_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="?attr/colorAccent"
android:clipToPadding="false"
android:paddingLeft="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/enable_location_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignTop="@id/enable_location_summary"
android:layout_alignBottom="@id/enable_location_summary"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_location_off"
app:tint="?attr/colorPrimary" />
<TextView
android:id="@+id/enable_location_summary"
style="@style/TextAppearance.AppCompat.Small.Inverse"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@id/enable_location_icon"
android:layout_weight="1"
android:text="@string/exposure_confirm_location_description" />
<Button
android:id="@+id/enable_location_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/enable_location_summary"
android:layout_alignLeft="@id/enable_location_summary"
android:layout_marginLeft="-16dp"
android:text="@string/exposure_confirm_button"
android:textColor="?android:attr/textColorPrimaryInverse" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"

View File

@ -7,8 +7,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="service_name_exposure">Exposure Notifications</string>
<string name="pref_exposure_enable_info_summary">Öffne eine App, die Exposure Notifications unterstützt, um diese zu aktivieren.</string>
<string name="pref_exposure_error_bluetooth_off_summary">Aktiviere Bluetooth, um Exposure Notifications zu nutzen.</string>
<string name="pref_exposure_error_location_off_summary">Aktiviere Ortungsdienste, um Exposure Notifications zu nutzen.</string>
<string name="pref_exposure_error_bluetooth_off_title">Bluetooth einschalten</string>
<string name="pref_exposure_error_location_off_title">Standortzugriff verwalten</string>
<string name="pref_exposure_error_bluetooth_unsupported_summary">Leider ist dein Gerät nicht mit Exposure Notifications kompatibel.</string>
<string name="pref_exposure_error_bluetooth_no_advertise_summary">Leider ist dein Gerät nicht vollständig mit Exposure Notifications kompatibel. Du wirst Warnungen über Risikokontakte erhalten, aber nicht andere Benachrichtigen können.</string>
<string name="prefcat_exposure_apps_title">Apps, die Exposure Notifications nutzen</string>
@ -56,4 +56,9 @@ Das Datum, die Zeitdauer und die Signalstärke, die einem Kontakt zugeordnet wur
Deine Identität oder das Testergebnis werden nicht geteilt."</string>
<string name="exposure_confirm_keys_button">Teilen</string>
<string name="exposure_confirm_permission_description"><xliff:g example="microG Services Core">%1$s</xliff:g> benötigt zusätzliche Berechtigungen.</string>
<string name="exposure_confirm_permission_button">Erteilen</string>
<string name="exposure_confirm_bluetooth_description">Bluetooth muss eingeschaltet sein.</string>
<string name="exposure_confirm_location_description">Standortzugriff muss eingeschaltet sein.</string>
<string name="exposure_confirm_button">Aktivieren</string>
</resources>

View File

@ -17,8 +17,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="service_name_exposure">Exposure Notifications</string>
<string name="pref_exposure_enable_info_summary">To enable Exposure Notifications, open any app supporting it.</string>
<string name="pref_exposure_error_bluetooth_off_summary">To use Exposure Notifications, enable Bluetooth in system settings.</string>
<string name="pref_exposure_error_location_off_summary">To use Exposure Notifications, enable Location access in system settings.</string>
<string name="pref_exposure_error_bluetooth_off_title">Enable Bluetooth</string>
<string name="pref_exposure_error_location_off_title">Open Location settings</string>
<string name="pref_exposure_error_bluetooth_unsupported_summary">Unfortunately, your device is not compatible with Exposure Notifications.</string>
<string name="pref_exposure_error_bluetooth_no_advertise_summary">Unfortunately, your device is only partially compatible with Exposure Notifications. You can be notified for risk contacts but won\'t be able to notify others.</string>
<string name="prefcat_exposure_apps_title">Apps using Exposure Notifications</string>
@ -66,6 +66,9 @@ The date, duration, and signal strength associated with an exposure will be shar
Your identity or test result won&apos;t be shared with other people."</string>
<string name="exposure_confirm_keys_button">Share</string>
<string name="exposure_confirm_permission_description">You need to grant permissions to <xliff:g example="microG Services Core">%1$s</xliff:g> for Exposure Notifications to function correctly.</string>
<string name="exposure_confirm_permission_description"><xliff:g example="microG Services Core">%1$s</xliff:g> needs additional permissions.</string>
<string name="exposure_confirm_permission_button">Grant</string>
<string name="exposure_confirm_bluetooth_description">Bluetooth needs to be enabled.</string>
<string name="exposure_confirm_location_description">Location access is required.</string>
<string name="exposure_confirm_button">Enable</string>
</resources>

View File

@ -18,14 +18,16 @@
<Preference
android:icon="@drawable/ic_bluetooth_off"
android:key="pref_exposure_error_bluetooth_off"
android:summary="@string/pref_exposure_error_bluetooth_off_summary"
android:title="@string/pref_exposure_error_bluetooth_off_title"
android:summary="@string/exposure_confirm_bluetooth_description"
app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" />
<Preference
android:icon="@drawable/ic_location_off"
android:key="pref_exposure_error_location_off"
android:summary="@string/pref_exposure_error_location_off_summary"
android:title="@string/pref_exposure_error_location_off_title"
android:summary="@string/exposure_confirm_location_description"
app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" />
@ -60,8 +62,8 @@
android:title="@string/pref_exposure_collected_rpis_title"
tools:summary="@string/pref_exposure_collected_rpis_summary" />
<Preference
android:key="pref_exposure_advertising_id"
android:enabled="false"
android:key="pref_exposure_advertising_id"
android:title="@string/pref_exposure_advertising_id_title"
tools:summary="9a799d68-925f-4c0c-a73c-b418f22a1250" />
</PreferenceCategory>

View File

@ -20,7 +20,9 @@
android:enabled="false"
android:key="pref_exposure_app_report_none"
android:order="0"
android:title="@string/list_no_item_none" />
android:title="@string/list_no_item_none"
app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" />
<Preference
android:enabled="false"
android:key="pref_exposure_app_report_updated"

View File

@ -24,6 +24,7 @@
<service android:name="org.microg.gms.nearby.exposurenotification.ScannerService" />
<service android:name="org.microg.gms.nearby.exposurenotification.AdvertiserService" />
<service android:name="org.microg.gms.nearby.exposurenotification.CleanupService" />
<service android:name="org.microg.gms.nearby.exposurenotification.NotifyService" />
<service android:name="org.microg.gms.nearby.exposurenotification.ExposureNotificationService">
<intent-filter>

View File

@ -10,15 +10,17 @@ import android.app.AlarmManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_ONE_SHOT
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothAdapter.*
import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseSettings.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.*
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
@ -36,7 +38,7 @@ class AdvertiserService : LifecycleService() {
private var advertising = false
private var wantStartAdvertising = false
private val advertiser: BluetoothLeAdvertiser?
get() = BluetoothAdapter.getDefaultAdapter()?.bluetoothLeAdvertiser
get() = getDefaultAdapter()?.bluetoothLeAdvertiser
private val alarmManager: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
private val callback: AdvertiseCallback = object : AdvertiseCallback() {
@ -54,10 +56,10 @@ class AdvertiserService : LifecycleService() {
private var setCallback: Any? = null
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> stopOrRestartAdvertising()
BluetoothAdapter.STATE_ON -> startAdvertisingIfNeeded()
if (intent?.action == ACTION_STATE_CHANGED) {
when (intent.getIntExtra(EXTRA_STATE, -1)) {
STATE_TURNING_OFF, STATE_OFF -> stopOrRestartAdvertising()
STATE_ON -> startAdvertisingIfNeeded()
}
}
}
@ -67,7 +69,7 @@ class AdvertiserService : LifecycleService() {
override fun onCreate() {
super.onCreate()
registerReceiver(trigger, IntentFilter().also { it.addAction("android.bluetooth.adapter.action.STATE_CHANGED") })
registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -234,11 +236,11 @@ class AdvertiserService : LifecycleService() {
}
fun isSupported(context: Context): Boolean? {
val adapter = BluetoothAdapter.getDefaultAdapter()
val adapter = getDefaultAdapter()
return when {
adapter == null -> false
Build.VERSION.SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true
adapter.state != BluetoothAdapter.STATE_ON -> null
adapter.state != STATE_ON -> null
adapter.bluetoothLeAdvertiser != null -> true
else -> false
}

View File

@ -23,7 +23,7 @@ class CleanupService : LifecycleService() {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
Log.d(TAG, "CleanupService.start: $intent")
super.onStartCommand(intent, flags, startId)
if (isNeeded(this)) {
if (isNeeded(this, true)) {
lifecycleScope.launchWhenStarted {
withContext(Dispatchers.IO) {
var workPending = true
@ -51,9 +51,9 @@ class CleanupService : LifecycleService() {
}
companion object {
fun isNeeded(context: Context): Boolean {
fun isNeeded(context: Context, now: Boolean = false): Boolean {
return ExposurePreferences(context).let {
it.enabled && it.lastCleanup < System.currentTimeMillis() - CLEANUP_INTERVAL
(it.enabled && !now) || it.lastCleanup < System.currentTimeMillis() - CLEANUP_INTERVAL
}
}
}

View File

@ -7,9 +7,8 @@ package org.microg.gms.nearby.exposurenotification
import android.app.Activity
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.bluetooth.BluetoothAdapter
import android.content.*
import android.os.*
import android.util.Log
import androidx.lifecycle.Lifecycle
@ -19,6 +18,8 @@ import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.exposurenotification.*
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.internal.*
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeout
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.common.Constants
@ -71,7 +72,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
}
private suspend fun confirmPermission(permission: String): Status {
private suspend fun confirmPermission(permission: String, force: Boolean = false): Status {
return ExposureDatabase.with(context) { database ->
when {
tempGrantedPermissions.contains(packageName to permission) -> {
@ -79,7 +80,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
tempGrantedPermissions.remove(packageName to permission)
Status.SUCCESS
}
database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission) -> {
!force && database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission) -> {
Status.SUCCESS
}
!hasConfirmActivity() -> {
@ -103,11 +104,16 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
override fun start(params: StartParams) {
lifecycleScope.launchWhenStarted {
val isAuthorized = ExposureDatabase.with(context) { it.isAppAuthorized(packageName) }
val adapter = BluetoothAdapter.getDefaultAdapter()
val status = if (isAuthorized && ExposurePreferences(context).enabled) {
Status.SUCCESS
} else if (adapter == null) {
Status(FAILED_NOT_SUPPORTED, "No Bluetooth Adapter available.")
} else {
val status = confirmPermission(CONFIRM_ACTION_START)
val status = confirmPermission(CONFIRM_ACTION_START, !adapter.isEnabled)
if (status.isSuccess) {
val context = context
adapter.enableAsync(context)
ExposurePreferences(context).enabled = true
ExposureDatabase.with(context) { database ->
database.authorizeApp(packageName)

View File

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.withTimeout
suspend fun BluetoothAdapter.enableAsync(context: Context): Boolean {
val deferred = CompletableDeferred<Unit>()
val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(receiverContext: Context?, intent: Intent?) {
if (intent?.action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
if (state == BluetoothAdapter.STATE_ON) deferred.complete(Unit)
}
}
}
context.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
if (!isEnabled) {
try {
enable()
withTimeout(5000) { deferred.await() }
} catch (e: Exception) {
Log.w(TAG, "Failed enabling Bluetooth")
}
}
context.unregisterReceiver(receiver)
return isEnabled
}

View File

@ -0,0 +1,128 @@
/*
* 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.*
import android.bluetooth.BluetoothAdapter
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.location.LocationManager
import android.os.Build
import android.util.Log
import android.util.TypedValue
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import androidx.lifecycle.LifecycleService
import org.microg.gms.common.ForegroundServiceContext
import org.microg.gms.nearby.core.R
class NotifyService : LifecycleService() {
private val notificationId = NotifyService::class.java.name.hashCode()
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
updateNotification()
}
}
@TargetApi(26)
private fun createNotificationChannel(): String {
val channel = NotificationChannel("exposure-notifications", "Exposure Notifications", NotificationManager.IMPORTANCE_HIGH)
channel.setSound(null, null)
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
channel.setShowBadge(true)
if (Build.VERSION.SDK_INT >= 29) {
channel.setAllowBubbles(false)
}
channel.vibrationPattern = LongArray(0)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
return channel.id
}
@TargetApi(21)
private fun updateNotification() {
val location = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager)
val bluetooth = BluetoothAdapter.getDefaultAdapter()?.state.let { it != BluetoothAdapter.STATE_ON && it != BluetoothAdapter.STATE_TURNING_ON }
Log.d(TAG, "notify: location: $location, bluetooth: $bluetooth")
val text: String = when {
location && bluetooth -> getString(R.string.exposure_notify_off_bluetooth_location)
location -> getString(R.string.exposure_notify_off_location)
bluetooth -> getString(R.string.exposure_notify_off_bluetooth)
else -> {
NotificationManagerCompat.from(this).cancel(notificationId)
return
}
}
if (Build.VERSION.SDK_INT >= 26) {
NotificationCompat.Builder(this, createNotificationChannel())
} else {
NotificationCompat.Builder(this)
}.apply {
val typedValue = TypedValue()
try {
var resolved = theme.resolveAttribute(androidx.appcompat.R.attr.colorError, typedValue, true)
if (!resolved && Build.VERSION.SDK_INT >= 26) resolved = theme.resolveAttribute(android.R.attr.colorError, typedValue, true)
color = if (resolved) {
ContextCompat.getColor(this@NotifyService, typedValue.resourceId)
} else {
Color.RED
}
if (Build.VERSION.SDK_INT >= 26) setColorized(true)
} catch (e: Exception) {
// Ignore
}
setSmallIcon(R.drawable.ic_virus_outline)
setContentTitle(getString(R.string.exposure_notify_off_title))
setContentText(text)
setStyle(NotificationCompat.BigTextStyle())
try {
val intent = Intent(Constants.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName }
intent.resolveActivity(packageManager)
setContentIntent(PendingIntent.getActivity(this@NotifyService, notificationId, Intent(Constants.ACTION_EXPOSURE_NOTIFICATION_SETTINGS).apply { `package` = packageName }, PendingIntent.FLAG_UPDATE_CURRENT))
} catch (e: Exception) {
// Ignore
}
}.let {
NotificationManagerCompat.from(this).notify(notificationId, it.build())
}
}
override fun onCreate() {
super.onCreate()
registerReceiver(trigger, IntentFilter().apply {
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
if (Build.VERSION.SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION)
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
})
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
Log.d(TAG, "NotifyService.start: $intent")
super.onStartCommand(intent, flags, startId)
updateNotification()
return Service.START_STICKY
}
override fun onDestroy() {
super.onDestroy()
NotificationManagerCompat.from(this).cancel(notificationId)
unregisterReceiver(trigger)
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).let { it.enabled }
}
}
}

View File

@ -9,8 +9,6 @@ import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothAdapter.*
import android.bluetooth.le.*
import android.content.BroadcastReceiver
@ -50,7 +48,7 @@ class ScannerService : LifecycleService() {
}
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
if (intent?.action == ACTION_STATE_CHANGED) {
when (intent.getIntExtra(EXTRA_STATE, -1)) {
STATE_TURNING_OFF, STATE_OFF -> stopScan()
STATE_ON -> startScanIfNeeded()
@ -104,7 +102,7 @@ class ScannerService : LifecycleService() {
override fun onCreate() {
super.onCreate()
registerReceiver(trigger, IntentFilter().also { it.addAction("android.bluetooth.adapter.action.STATE_CHANGED") })
registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
}
override fun onDestroy() {
@ -174,10 +172,10 @@ class ScannerService : LifecycleService() {
}
fun isSupported(context: Context): Boolean? {
val adapter = BluetoothAdapter.getDefaultAdapter()
val adapter = getDefaultAdapter()
return when {
adapter == null -> false
adapter.state != BluetoothAdapter.STATE_ON -> null
adapter.state != STATE_ON -> null
adapter.bluetoothLeScanner != null -> true
else -> false
}

View File

@ -29,5 +29,9 @@ class ServiceTrigger : BroadcastReceiver() {
Log.d(TAG, "Trigger ${CleanupService::class.java}")
serviceContext.startService(Intent(context, CleanupService::class.java))
}
if (NotifyService.isNeeded(context)) {
Log.d(TAG, "Trigger ${NotifyService::class.java}")
serviceContext.startService(Intent(context, NotifyService::class.java))
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exposure_notify_off_title">Exposure Notifications deaktiviert</string>
<string name="exposure_notify_off_bluetooth">Bluetooth muss eingeschaltet sein, um Exposure Notifications zu nutzen.</string>
<string name="exposure_notify_off_location">Standortzugriff muss eingeschaltet sein, um Exposure Notifications zu nutzen.</string>
<string name="exposure_notify_off_bluetooth_location">Bluetooth und Standortzugriff müssen eingeschaltet sein, um Exposure Notifications zu nutzen.</string>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exposure_notify_off_title">Exposure Notifications inactive</string>
<string name="exposure_notify_off_bluetooth">Bluetooth needs to be enabled to receive Exposure Notifications.</string>
<string name="exposure_notify_off_location">Location access is required to receive Exposure Notifications.</string>
<string name="exposure_notify_off_bluetooth_location">Bluetooth and Location access need to be enabled to receive Exposure Notifications.</string>
</resources>