From 00de511907a98245f9924260bf09514f57a484e5 Mon Sep 17 00:00:00 2001 From: andrekir Date: Fri, 4 Nov 2022 18:31:18 -0300 Subject: [PATCH] update targetSdkVersion to 31 --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 12 +-- .../java/com/geeksville/mesh/MainActivity.kt | 101 ++++-------------- .../mesh/android/ContextServices.kt | 25 +++-- .../com/geeksville/mesh/model/BTScanModel.kt | 23 ++-- .../bluetooth/BluetoothRepository.kt | 4 +- .../repository/radio/BluetoothInterface.kt | 19 +--- .../mesh/service/MeshServiceNotifications.kt | 6 +- .../geeksville/mesh/ui/SettingsFragment.kt | 53 ++++----- 9 files changed, 99 insertions(+), 146 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bbb20451..70e4524f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,7 +42,7 @@ android { defaultConfig { applicationId "com.geeksville.mesh" minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works) - targetSdkVersion 30 + targetSdkVersion 31 versionCode 30000 // format is Mmmss (where M is 1+the numeric major number versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 936141e9..a7334fba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,23 +11,21 @@ android:name="android.hardware.location.gps" android:required="false" /> - - <!– API 31+ Bluetooth permissions –> + ---> - - + android:usesPermissionFlags="neverForLocation" + tools:targetApi="s" /> + diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index aa02eca0..0cf59c45 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -8,7 +8,10 @@ import android.content.pm.PackageManager import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager import android.net.Uri -import android.os.* +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.RemoteException import android.text.method.LinkMovementMethod import android.view.Menu import android.view.MenuItem @@ -19,20 +22,14 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.Toolbar -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.viewpager2.adapter.FragmentStateAdapter -import com.geeksville.mesh.android.BindFailedException -import com.geeksville.mesh.android.GeeksvilleApplication -import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.android.ServiceClient +import com.geeksville.mesh.android.* import com.geeksville.mesh.concurrent.handledLaunch -import com.geeksville.mesh.android.getMissingPermissions -import com.geeksville.mesh.android.isGooglePlayAvailable import com.geeksville.mesh.databinding.ActivityMainBinding import com.geeksville.mesh.model.BTScanModel import com.geeksville.mesh.model.BluetoothViewModel @@ -130,8 +127,9 @@ class MainActivity : BaseActivity(), Logging { registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (!permissions.entries.all { it.value }) { errormsg("User denied permissions") - showSnackbar(getString(R.string.permission_missing_31)) + showSnackbar(permissionMissing) } + requestedEnable = false bluetoothViewModel.permissionsUpdated() } @@ -181,71 +179,6 @@ class MainActivity : BaseActivity(), Logging { } } - /** Get the minimum permissions our app needs to run correctly - */ - private fun getMinimumPermissions(): Array { - val perms = mutableListOf() - - // We only need this for logging to capture files for the simulator - turn off for production - // perms.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) - -/* TODO - wait for targetSdkVersion 31 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - perms.add(Manifest.permission.BLUETOOTH_SCAN) - perms.add(Manifest.permission.BLUETOOTH_CONNECT) - } -*/ - return getMissingPermissions(perms) - } - - /** Possibly prompt user to grant permissions - * @param shouldShowDialog usually true, but in cases where we've already shown a dialog elsewhere we skip it. - * - * @return true if we already have the needed permissions - */ - private fun requestPermission( - missingPerms: Array = getMinimumPermissions(), - shouldShowDialog: Boolean = true - ): Boolean = - if (missingPerms.isNotEmpty()) { - val shouldShow = missingPerms.filter { - ActivityCompat.shouldShowRequestPermissionRationale(this, it) - } - - fun doRequest() { - info("requesting permissions") - // Ask for all the missing perms - requestPermissionsLauncher.launch(missingPerms) - } - - if (shouldShow.isNotEmpty() && shouldShowDialog) { - // DID_REQUEST_PERM is an - // app-defined int constant. The callback method gets the - // result of the request. - warn("Permissions $shouldShow missing, we should show dialog") - - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.required_permissions)) - .setMessage(getString(R.string.permission_missing_31)) - .setNeutralButton(R.string.cancel) { _, _ -> - warn("User bailed due to permissions") - } - .setPositiveButton(R.string.accept) { _, _ -> - doRequest() - } - .show() - } else { - info("Permissions $missingPerms missing, no need to show dialog, just asking OS") - doRequest() - } - - false - } else { - // Permission has already been granted - debug("We have our required permissions") - true - } - /// Ask user to rate in play store private fun askToRate() { exceptionReporter { // we don't want to crash our app because of bugs in this optional feature @@ -296,9 +229,6 @@ class MainActivity : BaseActivity(), Logging { handleIntent(intent) if (isGooglePlayAvailable(this)) askToRate() - - // if (!isInTestLab) - very important - even in test lab we must request permissions because we need location perms for some of our tests to pass - requestPermission() } private fun initToolbar() { @@ -728,11 +658,22 @@ class MainActivity : BaseActivity(), Logging { super.onStart() bluetoothViewModel.enabled.observe(this) { enabled -> - if (!enabled && !requestedEnable) { - if (!isInTestLab && scanModel.selectedBluetooth) { - requestedEnable = true + if (!enabled && !requestedEnable && scanModel.selectedBluetooth) { + requestedEnable = true + if (hasBluetoothPermission()) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) bleRequestEnable.launch(enableBtIntent) + } else { + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.required_permissions)) + .setMessage(permissionMissing) + .setNeutralButton(R.string.cancel) { _, _ -> + warn("User bailed due to permissions") + } + .setPositiveButton(R.string.accept) { _, _ -> + info("requesting permissions") + requestPermissionsLauncher.launch(getBluetoothPermissions()) } + .show() } } } diff --git a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt index a187ccf9..3f5837dc 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -8,14 +8,15 @@ import android.companion.CompanionDeviceManager import android.content.Context import android.content.pm.PackageManager import android.hardware.usb.UsbManager -import android.os.Build import androidx.core.content.ContextCompat import com.geeksville.mesh.MainActivity +import com.geeksville.mesh.R /** * @return null on platforms without a BlueTooth driver (i.e. the emulator) */ -val Context.bluetoothManager: BluetoothManager? get() = getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager? +val Context.bluetoothManager: BluetoothManager? + get() = getSystemService(Context.BLUETOOTH_SERVICE).takeIf { hasBluetoothPermission() } as? BluetoothManager? val Context.deviceManager: CompanionDeviceManager? get() { @@ -36,7 +37,7 @@ val Context.locationManager: LocationManager get() = requireNotNull(getSystemSer * @return true if CompanionDeviceManager API is present */ fun Context.hasCompanionDeviceApi(): Boolean = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP) else false @@ -51,6 +52,16 @@ fun Context.hasGps(): Boolean = locationManager.allProviders.contains(LocationMa fun Context.gpsDisabled(): Boolean = if (hasGps()) !locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) else false +/** + * return the text string of the permissions missing + */ +val Context.permissionMissing: String + get() = if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { + getString(R.string.permission_missing) + } else { + getString(R.string.permission_missing_31) + } + /** * return a list of the permissions we don't have */ @@ -67,12 +78,12 @@ fun Context.getMissingPermissions(perms: List): Array = perms.fi fun Context.getBluetoothPermissions(): Array { val perms = mutableListOf() -/* TODO - wait for targetSdkVersion 31 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { perms.add(Manifest.permission.BLUETOOTH_SCAN) perms.add(Manifest.permission.BLUETOOTH_CONNECT) + } else if (!hasCompanionDeviceApi()) { + perms.add(Manifest.permission.ACCESS_FINE_LOCATION) } -*/ return getMissingPermissions(perms) } @@ -109,7 +120,7 @@ fun Context.hasLocationPermission() = getLocationPermissions().isEmpty() fun Context.getBackgroundPermissions(): Array { val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION) - if (Build.VERSION.SDK_INT >= 29) // only added later + if (android.os.Build.VERSION.SDK_INT >= 29) // only added later perms.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) return getMissingPermissions(perms) diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index d027afe9..b10a9af5 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -128,10 +128,9 @@ class BTScanModel @Inject constructor( debug("BTScanModel cleared") } - private val bluetoothAdapter = context.bluetoothManager?.adapter private val deviceManager get() = context.deviceManager - val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi() - private val hasBluetoothPermission get() = application.hasBluetoothPermission() + val hasCompanionDeviceApi get() = application.hasCompanionDeviceApi() + val hasBluetoothPermission get() = application.hasBluetoothPermission() private val usbManager get() = context.usbManager var selectedAddress: String? = null @@ -219,8 +218,8 @@ class BTScanModel @Inject constructor( fun setupScan(): Boolean { selectedAddress = radioInterfaceService.getDeviceAddress() - return if (bluetoothAdapter == null || MockInterface.addressValid(context, usbRepository, "")) { - warn("No bluetooth adapter. Running under emulation?") + return if (MockInterface.addressValid(context, usbRepository, "")) { + warn("Running under emulator/test lab") val testnodes = listOf( DeviceListEntry("Included simulator", "m", true), @@ -273,14 +272,16 @@ class BTScanModel @Inject constructor( private var networkDiscovery: Job? = null fun startScan() { + _spinner.value = true + // Start Network Service Discovery (find TCP devices) networkDiscovery = nsdRepository.networkDiscoveryFlow() .onEach { addDevice(TCPDeviceListEntry(it)) } .launchIn(CoroutineScope(Dispatchers.Main)) - if (hasCompanionDeviceApi) { - startCompanionScan() - } else startClassicScan() + if (hasBluetoothPermission) { + if (hasCompanionDeviceApi) startCompanionScan() else startClassicScan() + } } @SuppressLint("MissingPermission") @@ -290,7 +291,6 @@ class BTScanModel @Inject constructor( if (bluetoothLeScanner != null) { // could be null if bluetooth is disabled debug("starting classic scan") - _spinner.value = true // filter and only accept devices that have our service val filter = @@ -373,7 +373,6 @@ class BTScanModel @Inject constructor( @SuppressLint("NewApi") private fun startCompanionScan() { debug("starting companion scan") - _spinner.value = true deviceManager?.associate( associationRequest(), @SuppressLint("NewApi") @@ -479,7 +478,11 @@ class BTScanModel @Inject constructor( } val permissionIntent = + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.S) { PendingIntent.getBroadcast(activity, 0, Intent(ACTION_USB_PERMISSION), 0) + } else { + PendingIntent.getBroadcast(activity, 0, Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE) + } val filter = IntentFilter(ACTION_USB_PERMISSION) activity.registerReceiver(usbReceiver, filter) usbManager.requestPermission(it.usb.device, permissionIntent) diff --git a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt index bd0c34ed..0a91fb5c 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/bluetooth/BluetoothRepository.kt @@ -56,7 +56,9 @@ class BluetoothRepository @Inject constructor( } fun getRemoteDevice(address: String): BluetoothDevice? { - return bluetoothAdapterLazy.get()?.takeIf { isValid(address) }?.getRemoteDevice(address) + return bluetoothAdapterLazy.get() + ?.takeIf { application.hasBluetoothPermission() && isValid(address) } + ?.getRemoteDevice(address) } fun getBluetoothLeScanner(): BluetoothLeScanner? { diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt index 3c843a9c..6224a587 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt @@ -1,12 +1,10 @@ package com.geeksville.mesh.repository.radio -import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattService -import android.bluetooth.BluetoothManager import android.content.Context import com.geeksville.mesh.android.Logging +import com.geeksville.mesh.android.bluetoothManager import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.repository.usb.UsbRepository import com.geeksville.mesh.service.* @@ -110,22 +108,15 @@ class BluetoothInterface( val BTM_FROMNUM_CHARACTER: UUID = UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453") - /// Get our bluetooth adapter (should always succeed except on emulator - private fun getBluetoothAdapter(context: Context): BluetoothAdapter? { - val bluetoothManager = - context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - return bluetoothManager.adapter - } - /** Return true if this address is still acceptable. For BLE that means, still bonded */ - @SuppressLint("NewApi", "MissingPermission") override fun addressValid( context: Context, usbRepository: UsbRepository, // Temporary until dependency injection transition is completed rest: String ): Boolean { - val allPaired = getBluetoothAdapter(context)?.bondedDevices.orEmpty() - .map { it.address }.toSet() + /// Get our bluetooth adapter (should always succeed except on emulator + val allPaired = context.bluetoothManager?.adapter?.bondedDevices.orEmpty() + .map { it.address }.toSet() return if (!allPaired.contains(rest)) { warn("Ignoring stale bond to ${rest.anonymize}") false @@ -170,7 +161,7 @@ class BluetoothInterface( init { // Note: this call does no comms, it just creates the device object (even if the // device is off/not connected) - val device = getBluetoothAdapter(context)?.getRemoteDevice(address) + val device = context.bluetoothManager?.adapter?.getRemoteDevice(address) if (device != null) { info("Creating radio interface service. device=${address.anonymize}") diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt index d56ad88b..d2f5f4da 100644 --- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt +++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt @@ -106,7 +106,11 @@ class MeshServiceNotifications( ) private val openAppIntent: PendingIntent by lazy { - PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0) + } else { + PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE) + } } /** diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 4f37591d..d03cc0a9 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -261,7 +261,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (permissions.entries.all { it.value }) { binding.provideLocationCheckbox.isChecked = true - } else debug("User denied background permission") + } else { + debug("User denied background permission") + showSnackbar(getString(R.string.why_background_required)) + } } val requestLocationAndBackgroundLauncher = @@ -271,7 +274,10 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (myActivity.hasBackgroundPermission()) { binding.provideLocationCheckbox.isChecked = true } else requestBackgroundAndCheckLauncher.launch(myActivity.getBackgroundPermissions()) - } else debug("User denied location permission") + } else { + debug("User denied location permission") + showSnackbar(getString(R.string.why_background_required)) + } } // init our region spinner @@ -506,37 +512,34 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { val requestPermissionAndScanLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> if (permissions.entries.all { it.value }) { - checkLocationEnabled() + checkBTEnabled() + if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled() scanLeDevice() } else { errormsg("User denied scan permissions") - showSnackbar(getString(R.string.permission_missing)) + showSnackbar(requireContext().permissionMissing) } + bluetoothViewModel.permissionsUpdated() } binding.changeRadioButton.setOnClickListener { debug("User clicked changeRadioButton") - checkBTEnabled() - if ((scanModel.hasCompanionDeviceApi)) { - scanLeDevice() - } else { - // Location is the only runtime permission for classic bluetooth scan - if (myActivity.hasLocationPermission()) { - checkLocationEnabled() - scanLeDevice() - } else { - MaterialAlertDialogBuilder(requireContext()) - .setTitle(getString(R.string.required_permissions)) - .setMessage(getString(R.string.permission_missing)) - .setNeutralButton(R.string.cancel) { _, _ -> - warn("User bailed due to permissions") - } - .setPositiveButton(R.string.accept) { _, _ -> - info("requesting scan permissions") - requestPermissionAndScanLauncher.launch(myActivity.getLocationPermissions()) - } - .show() - } + scanLeDevice() + if (scanModel.hasBluetoothPermission) { + checkBTEnabled() + if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled() + } else { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.required_permissions)) + .setMessage(requireContext().permissionMissing) + .setNeutralButton(R.string.cancel) { _, _ -> + warn("User bailed due to permissions") + } + .setPositiveButton(R.string.accept) { _, _ -> + info("requesting scan permissions") + requestPermissionAndScanLauncher.launch(myActivity.getBluetoothPermissions()) + } + .show() } } }