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 permissionRequestCode = 33
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")
} else {
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.content.Context.LOCATION_SERVICE
import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
@ -31,6 +32,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureEnableInfo: Preference
private lateinit var exposureBluetoothOff: Preference
private lateinit var exposureLocationOff: Preference
private lateinit var exposureNearbyNotGranted: Preference
private lateinit var exposureBluetoothUnsupported: Preference
private lateinit var exposureBluetoothNoAdvertisement: Preference
private lateinit var exposureApps: PreferenceCategory
@ -41,6 +43,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() }
private var permissionRequestCode = 33
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications)
@ -50,6 +53,7 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo
exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff
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
exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement
exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps
@ -80,12 +84,28 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
true
}
exposureNearbyNotGranted.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN")
requestPermissions(nearbyPermissions, ++permissionRequestCode)
true
}
collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(requireContext(), R.id.openExposureRpis)
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() {
super.onResume()
@ -110,6 +130,11 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
val bluetoothSupported = ScannerService.isSupported(appContext)
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)
exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn
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_location_description">Location access is required.</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>

View File

@ -31,6 +31,14 @@
app:isPreferenceVisible="false"
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
android:icon="@drawable/ic_alert"
android:key="pref_exposure_error_bluetooth_unsupported"

View File

@ -147,7 +147,11 @@ class AdvertiserService : LifecycleService() {
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
.setConnectable(false)
.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 {
nextSend = nextSend.coerceAtMost(180000)
val settings = Builder()
@ -156,7 +160,11 @@ class AdvertiserService : LifecycleService() {
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
.setConnectable(false)
.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 }
sendingBytes = payload
@ -204,7 +212,11 @@ class AdvertiserService : LifecycleService() {
advertising = false
if (Build.VERSION.SDK_INT >= 26) {
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 {
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_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.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.graphics.Color
import android.location.LocationManager
import android.os.Build
@ -53,9 +54,14 @@ class NotifyService : LifecycleService() {
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 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 {
permissionNeedsHandling -> getString(R.string.exposure_notify_off_nearby)
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)
@ -105,6 +111,7 @@ class NotifyService : LifecycleService() {
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
if (Build.VERSION.SDK_INT >= 19) addAction(LocationManager.MODE_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")
seenAdvertisements = 0
wakeLock.acquire()
scanner.startScan(
listOf(ScanFilter.Builder()
.setServiceUuid(SERVICE_UUID)
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
.build()),
ScanSettings.Builder().build(),
callback
)
try {
scanner.startScan(
listOf(ScanFilter.Builder()
.setServiceUuid(SERVICE_UUID)
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
.build()),
ScanSettings.Builder().build(),
callback
)
} catch (e: SecurityException) {
Log.e(TAG, "Couldn't start ScannerService, need android.permission.BLUETOOTH_SCAN permission.")
}
scanning = true
lastStartTime = System.currentTimeMillis()
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_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_nearby">Exposure Notifications require additional permissions to work</string>
</resources>