request BLUETOOTH_SCAN and ADVERTISE permissions on Android 12

This also adds a warning notification when the app doesn't have the
required permission after an OS update.
This commit is contained in:
Marcus Hoffmann 2021-11-08 18:28:27 +01:00 committed by Marvin W
parent 4a5c98491b
commit 6cfc0aa255
9 changed files with 80 additions and 15 deletions

View File

@ -105,7 +105,11 @@ class ExposureNotificationsConfirmActivity : AppCompatActivity() {
private var permissionNeedsHandling: Boolean = false private var permissionNeedsHandling: Boolean = false
private var permissionRequestCode = 33 private var permissionRequestCode = 33
private val permissions by lazy { private val permissions by lazy {
if (Build.VERSION.SDK_INT >= 29) { if (Build.VERSION.SDK_INT >= 31){
arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN", "android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION")
}
else if (Build.VERSION.SDK_INT >= 29) {
arrayOf("android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION") arrayOf("android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION")
} else { } else {
arrayOf("android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION") arrayOf("android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION")

View File

@ -8,12 +8,13 @@ package org.microg.gms.nearby.core.ui
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.content.Context.LOCATION_SERVICE import android.content.Context.LOCATION_SERVICE
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager import android.location.LocationManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.provider.Settings import android.provider.Settings
import android.util.Log import androidx.core.content.ContextCompat
import android.view.View
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -31,6 +32,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureEnableInfo: Preference private lateinit var exposureEnableInfo: Preference
private lateinit var exposureBluetoothOff: Preference private lateinit var exposureBluetoothOff: Preference
private lateinit var exposureLocationOff: Preference private lateinit var exposureLocationOff: Preference
private lateinit var exposureNearbyNotGranted: Preference
private lateinit var exposureBluetoothUnsupported: Preference private lateinit var exposureBluetoothUnsupported: Preference
private lateinit var exposureBluetoothNoAdvertisement: Preference private lateinit var exposureBluetoothNoAdvertisement: Preference
private lateinit var exposureApps: PreferenceCategory private lateinit var exposureApps: PreferenceCategory
@ -41,6 +43,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private val handler = Handler() private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() } private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() } private val updateContentRunnable = Runnable { updateContent() }
private var permissionRequestCode = 33
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications) addPreferencesFromResource(R.xml.preferences_exposure_notifications)
@ -50,6 +53,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo
exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff
exposureLocationOff = preferenceScreen.findPreference("pref_exposure_error_location_off") ?: exposureLocationOff exposureLocationOff = preferenceScreen.findPreference("pref_exposure_error_location_off") ?: exposureLocationOff
exposureNearbyNotGranted = preferenceScreen.findPreference("pref_exposure_error_nearby_not_granted") ?: exposureNearbyNotGranted
exposureBluetoothUnsupported = preferenceScreen.findPreference("pref_exposure_error_bluetooth_unsupported") ?: exposureBluetoothUnsupported exposureBluetoothUnsupported = preferenceScreen.findPreference("pref_exposure_error_bluetooth_unsupported") ?: exposureBluetoothUnsupported
exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement
exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps
@ -80,12 +84,28 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
true true
} }
exposureNearbyNotGranted.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN")
requestPermissions(nearbyPermissions, ++permissionRequestCode)
true
}
collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener { collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openExposureRpis) findNavController().navigate(requireContext(), R.id.openExposureRpis)
true true
} }
} }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == this.permissionRequestCode) {
updateStatus()
// Tell the NotifyService that it should update the notification
val intent = Intent(NOTIFICATION_UPDATE_ACTION)
requireContext().sendBroadcast(intent)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -110,6 +130,11 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
val bluetoothSupported = ScannerService.isSupported(appContext) val bluetoothSupported = ScannerService.isSupported(appContext)
val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(appContext) else bluetoothSupported val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(appContext) else bluetoothSupported
val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN")
val nearbyPermissionsGranted = Build.VERSION.SDK_INT >= 31 || nearbyPermissions.all {
ContextCompat.checkSelfPermission(appContext, it) == PackageManager.PERMISSION_GRANTED
}
exposureNearbyNotGranted.isVisible = enabled && !nearbyPermissionsGranted
exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(appContext.getSystemService(LOCATION_SERVICE) as LocationManager) exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(appContext.getSystemService(LOCATION_SERVICE) as LocationManager)
exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn
exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false

View File

@ -76,4 +76,6 @@ Your identity or test result won&apos;t be shared with other people."</string>
<string name="exposure_confirm_bluetooth_description">Bluetooth needs to be enabled.</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_location_description">Location access is required.</string>
<string name="exposure_confirm_button">Enable</string> <string name="exposure_confirm_button">Enable</string>
<string name="pref_exposure_error_nearby_not_granted_title">New Permissions required</string>
<string name="pref_exposure_error_nearby_not_granted_description">Tap to grant required permissions to Exposure Notifications</string>
</resources> </resources>

View File

@ -31,6 +31,14 @@
app:isPreferenceVisible="false" app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" /> tools:isPreferenceVisible="true" />
<Preference
android:icon="@drawable/ic_info_outline"
android:key="pref_exposure_error_nearby_not_granted"
android:title="@string/pref_exposure_error_nearby_not_granted_title"
android:summary="@string/pref_exposure_error_nearby_not_granted_description"
app:isPreferenceVisible="false"
tools:isPreferenceVisible="true" />
<Preference <Preference
android:icon="@drawable/ic_alert" android:icon="@drawable/ic_alert"
android:key="pref_exposure_error_bluetooth_unsupported" android:key="pref_exposure_error_bluetooth_unsupported"

View File

@ -147,7 +147,11 @@ class AdvertiserService : LifecycleService() {
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW) .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
.setConnectable(false) .setConnectable(false)
.build() .build()
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback) try {
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
} catch (e: SecurityException) {
Log.e(TAG, "Couldn't start advertising: Need android.permission.BLUETOOTH_ADVERTISE permission.", )
}
} else { } else {
nextSend = nextSend.coerceAtMost(180000) nextSend = nextSend.coerceAtMost(180000)
val settings = Builder() val settings = Builder()
@ -156,7 +160,11 @@ class AdvertiserService : LifecycleService() {
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW) .setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
.setConnectable(false) .setConnectable(false)
.build() .build()
advertiser.startAdvertising(settings, data, callback) try {
advertiser.startAdvertising(settings, data, callback)
} catch (e: SecurityException) {
Log.e(TAG, "Couldn't start advertising: Need android.permission.BLUETOOTH_ADVERTISE permission.", )
}
} }
synchronized(this) { advertising = true } synchronized(this) { advertising = true }
sendingBytes = payload sendingBytes = payload
@ -204,7 +212,11 @@ class AdvertiserService : LifecycleService() {
advertising = false advertising = false
if (Build.VERSION.SDK_INT >= 26) { if (Build.VERSION.SDK_INT >= 26) {
wantStartAdvertising = true wantStartAdvertising = true
advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback) try {
advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback)
} catch (e: SecurityException) {
Log.i(TAG, "Tried calling stopAdvertisingSet without android.permission.BLUETOOTH_ADVERTISE permission.", )
}
} else { } else {
advertiser?.stopAdvertising(callback) advertiser?.stopAdvertising(callback)
} }

View File

@ -42,3 +42,5 @@ const val CLEANUP_INTERVAL = 24 * 60 * 60 * 1000L
const val VERSION_1_0: Byte = 0x40 const val VERSION_1_0: Byte = 0x40
const val VERSION_1_1: Byte = 0x50 const val VERSION_1_1: Byte = 0x50
const val NOTIFICATION_UPDATE_ACTION = "org.microg.gms.nearby.UPDATE_NOTIFICATION"

View File

@ -12,6 +12,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Color import android.graphics.Color
import android.location.LocationManager import android.location.LocationManager
import android.os.Build import android.os.Build
@ -53,9 +54,14 @@ class NotifyService : LifecycleService() {
private fun updateNotification() { private fun updateNotification() {
val location = !LocationManagerCompat.isLocationEnabled(getSystemService(Context.LOCATION_SERVICE) as LocationManager) 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 } val bluetooth = BluetoothAdapter.getDefaultAdapter()?.state.let { it != BluetoothAdapter.STATE_ON && it != BluetoothAdapter.STATE_TURNING_ON }
Log.d(TAG, "notify: location: $location, bluetooth: $bluetooth") val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN")
val permissionNeedsHandling = Build.VERSION.SDK_INT >= 31 && nearbyPermissions.any {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
Log.d( TAG,"notify: location: $location, bluetooth: $bluetooth, permissionNeedsHandling: $permissionNeedsHandling")
val text: String = when { val text: String = when {
permissionNeedsHandling -> getString(R.string.exposure_notify_off_nearby)
location && bluetooth -> getString(R.string.exposure_notify_off_bluetooth_location) location && bluetooth -> getString(R.string.exposure_notify_off_bluetooth_location)
location -> getString(R.string.exposure_notify_off_location) location -> getString(R.string.exposure_notify_off_location)
bluetooth -> getString(R.string.exposure_notify_off_bluetooth) bluetooth -> getString(R.string.exposure_notify_off_bluetooth)
@ -105,6 +111,7 @@ class NotifyService : LifecycleService() {
addAction(BluetoothAdapter.ACTION_STATE_CHANGED) addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
if (Build.VERSION.SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION) if (Build.VERSION.SDK_INT >= 19) addAction(LocationManager.MODE_CHANGED_ACTION)
addAction(LocationManager.PROVIDERS_CHANGED_ACTION) addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
addAction(NOTIFICATION_UPDATE_ACTION)
}) })
} }

View File

@ -121,14 +121,18 @@ class ScannerService : LifecycleService() {
Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms") Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms")
seenAdvertisements = 0 seenAdvertisements = 0
wakeLock.acquire() wakeLock.acquire()
scanner.startScan( try {
listOf(ScanFilter.Builder() scanner.startScan(
.setServiceUuid(SERVICE_UUID) listOf(ScanFilter.Builder()
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)) .setServiceUuid(SERVICE_UUID)
.build()), .setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
ScanSettings.Builder().build(), .build()),
callback ScanSettings.Builder().build(),
) callback
)
} catch (e: SecurityException) {
Log.e(TAG, "Couldn't start ScannerService, need android.permission.BLUETOOTH_SCAN permission.")
}
scanning = true scanning = true
lastStartTime = System.currentTimeMillis() lastStartTime = System.currentTimeMillis()
handler.postDelayed(stopLaterRunnable, SCANNING_TIME_MS) handler.postDelayed(stopLaterRunnable, SCANNING_TIME_MS)

View File

@ -7,4 +7,5 @@
<string name="exposure_notify_off_bluetooth">Bluetooth needs to be enabled to receive Exposure Notifications.</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_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> <string name="exposure_notify_off_bluetooth_location">Bluetooth and Location access need to be enabled to receive Exposure Notifications.</string>
<string name="exposure_notify_off_nearby">Exposure Notifications require additional permissions to work</string>
</resources> </resources>