diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 267dbdc7d..1041f0708 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -189,7 +189,7 @@ class MainActivity : BaseActivity(), Logging, /** Get the minimum permissions our app needs to run correctly */ - private fun getMinimumPermissions(): List { + private fun getMinimumPermissions(): Array { val perms = mutableListOf( Manifest.permission.WAKE_LOCK @@ -218,18 +218,12 @@ class MainActivity : BaseActivity(), Logging, /** Ask the user to grant Bluetooth scan/discovery permission */ fun requestScanPermission() = requestPermission(getScanPermissions(), true) - /** Ask the user to grant foreground location permission */ - fun requestLocationPermission() = requestPermission(getLocationPermissions()) - - /** Ask the user to grant background location permission */ - fun requestBackgroundPermission() = requestPermission(getBackgroundPermissions()) - /** * @return a localized string warning user about missing permissions. Or null if everything is find */ @SuppressLint("InlinedApi") fun getMissingMessage( - missingPerms: List = getMinimumPermissions() + missingPerms: Array = getMinimumPermissions() ): String? { val renamedPermissions = mapOf( // Older versions of android don't know about these permissions - ignore failure to grant @@ -262,7 +256,7 @@ class MainActivity : BaseActivity(), Logging, * @return true if we already have the needed permissions */ private fun requestPermission( - missingPerms: List = getMinimumPermissions(), + missingPerms: Array = getMinimumPermissions(), shouldShowDialog: Boolean = false ): Boolean = if (missingPerms.isNotEmpty()) { @@ -275,7 +269,7 @@ class MainActivity : BaseActivity(), Logging, // Ask for all the missing perms ActivityCompat.requestPermissions( this, - missingPerms.toTypedArray(), + missingPerms, DID_REQUEST_PERM ) } 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 61f4f1cc7..8a9edf947 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -38,20 +38,26 @@ fun Context.hasCompanionDeviceApi(): Boolean = packageManager.hasSystemFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP) else false +/** + * @return true if the device has a GPS receiver + */ +fun Context.hasGps(): Boolean = + packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) + /** * return a list of the permissions we don't have */ -fun Context.getMissingPermissions(perms: List) = perms.filter { +fun Context.getMissingPermissions(perms: List): Array = perms.filter { ContextCompat.checkSelfPermission( this, it ) != PackageManager.PERMISSION_GRANTED -} +}.toTypedArray() /** * Bluetooth connect permissions (or empty if we already have what we need) */ -fun Context.getConnectPermissions(): List { +fun Context.getConnectPermissions(): Array { val perms = mutableListOf() /* TODO - wait for targetSdkVersion 31 @@ -70,7 +76,7 @@ fun Context.hasConnectPermission() = getConnectPermissions().isEmpty() /** * Bluetooth scan/discovery permissions (or empty if we already have what we need) */ -fun Context.getScanPermissions(): List { +fun Context.getScanPermissions(): Array { val perms = mutableListOf() /* TODO - wait for targetSdkVersion 31 @@ -95,7 +101,7 @@ fun Context.hasScanPermission() = getScanPermissions().isEmpty() /** * Camera permission (or empty if we already have what we need) */ -fun Context.getCameraPermissions(): List { +fun Context.getCameraPermissions(): Array { val perms = mutableListOf(Manifest.permission.CAMERA) return getMissingPermissions(perms) @@ -107,7 +113,7 @@ fun Context.hasCameraPermission() = getCameraPermissions().isEmpty() /** * Location permission (or empty if we already have what we need) */ -fun Context.getLocationPermissions(): List { +fun Context.getLocationPermissions(): Array { val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION) return getMissingPermissions(perms) @@ -119,8 +125,8 @@ fun Context.hasLocationPermission() = getLocationPermissions().isEmpty() /** * A list of missing background location permissions (or empty if we already have what we need) */ -fun Context.getBackgroundPermissions(): List { - val perms = mutableListOf() +fun Context.getBackgroundPermissions(): Array { + val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION) if (Build.VERSION.SDK_INT >= 29) // only added later perms.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION) diff --git a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt index 8dc873265..443dbc530 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/ChannelFragment.kt @@ -1,6 +1,5 @@ package com.geeksville.mesh.ui -import android.Manifest import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.ColorMatrix @@ -25,6 +24,7 @@ import com.geeksville.mesh.AppOnlyProtos import com.geeksville.mesh.ChannelProtos import com.geeksville.mesh.ConfigProtos import com.geeksville.mesh.R +import com.geeksville.mesh.android.getCameraPermissions import com.geeksville.mesh.android.hasCameraPermission import com.geeksville.mesh.databinding.ChannelFragmentBinding import com.geeksville.mesh.model.Channel @@ -219,12 +219,11 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { debug("Camera permission denied") } .setPositiveButton(getString(R.string.accept)) { _, _ -> - requestPermissionAndScanLauncher.launch(Manifest.permission.CAMERA) + requestPermissionAndScanLauncher.launch(requireContext().getCameraPermissions()) } .show() } - private fun mlkitScan() { debug("Starting ML Kit QR code scanner") val options = GmsBarcodeScannerOptions.Builder() @@ -368,8 +367,8 @@ class ChannelFragment : ScreenFragment("Channel"), Logging { } private val requestPermissionAndScanLauncher = - registerForActivityResult(ActivityResultContracts.RequestPermission()) { allowed -> - if (allowed) zxingScan() + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + if (permissions.entries.all { it.value == true }) zxingScan() } // Register zxing launcher and result handler 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 492b30d14..890900792 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -9,9 +9,9 @@ import android.companion.AssociationRequest import android.companion.BluetoothDeviceFilter import android.companion.CompanionDeviceManager import android.content.* -import android.content.pm.PackageManager import android.hardware.usb.UsbDevice import android.hardware.usb.UsbManager +import android.location.LocationManager import android.net.nsd.NsdServiceInfo import android.os.* import android.view.LayoutInflater @@ -48,9 +48,6 @@ import com.geeksville.mesh.service.SoftwareUpdateService import com.geeksville.util.anonymize import com.geeksville.util.exceptionReporter import com.geeksville.util.exceptionToSnackbar -import com.google.android.gms.location.LocationRequest -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.LocationSettingsRequest import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.hoho.android.usbserial.driver.UsbSerialDriver @@ -553,19 +550,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { private val bluetoothViewModel: BluetoothViewModel by activityViewModels() private val model: UIViewModel by activityViewModels() - // FIXME - move this into a standard GUI helper class - private val guiJob = Job() - @Inject internal lateinit var usbRepository: UsbRepository private val myActivity get() = requireActivity() as MainActivity - override fun onDestroy() { - guiJob.cancel() - super.onDestroy() - } - private fun doFirmwareUpdate() { model.meshService?.let { service -> @@ -804,11 +793,9 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked -> if (view.isPressed && isChecked) { // We want to ignore changes caused by code (as opposed to the user) // Don't check the box until the system setting changes - view.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission() + view.isChecked = myActivity.hasBackgroundPermission() - if (!myActivity.hasLocationPermission()) // Make sure we have location permission (prerequisite) - myActivity.requestLocationPermission() - else if (!myActivity.hasBackgroundPermission()) + if (!myActivity.hasBackgroundPermission()) MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.background_required) .setMessage(R.string.why_background_required) @@ -816,7 +803,12 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { debug("User denied background permission") } .setPositiveButton(getString(R.string.accept)) { _, _ -> - myActivity.requestBackgroundPermission() + // Make sure we have location permission (prerequisite) + if (!myActivity.hasLocationPermission()) { + requestLocationAndBackgroundLauncher.launch(myActivity.getLocationPermissions()) + } else { + requestBackgroundAndCheckLauncher.launch(myActivity.getBackgroundPermissions()) + } } .show() if (view.isChecked) { @@ -951,6 +943,23 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } + private val requestLocationAndBackgroundLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + if (permissions.entries.all { it.value == true }) { + // Older versions of android only need Location permission + if (myActivity.hasBackgroundPermission()) { + binding.provideLocationCheckbox.isChecked = true + } else requestBackgroundAndCheckLauncher.launch(myActivity.getBackgroundPermissions()) + } + } + + private val requestBackgroundAndCheckLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + if (permissions.entries.all { it.value == true }) { + binding.provideLocationCheckbox.isChecked = true + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -967,62 +976,23 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } } - // If the user has not turned on location access throw up a toast warning + // If the user has not turned on location access throw up a warning private fun checkLocationEnabled( warningReason: String = getString(R.string.location_disabled_warning) ) { + val locationManager = + myActivity.getSystemService(Context.LOCATION_SERVICE) as LocationManager + var gpsEnabled = false - val hasGps: Boolean = - myActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) + try { + gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } catch (ex: Throwable) { + debug("LocationManager GPS_PROVIDER error: ${ex.message}") + } - // FIXME If they don't have google play for now we don't check for location enabled - if (hasGps && isGooglePlayAvailable(requireContext())) { - // We do this painful process because LocationManager.isEnabled is only SDK28 or latest - val builder = LocationSettingsRequest.Builder() - builder.setNeedBle(true) - - val request = LocationRequest.create().apply { - priority = LocationRequest.PRIORITY_HIGH_ACCURACY - } - builder.addLocationRequest(request) // Make sure we are granted high accuracy permission - - val locationSettingsResponse = LocationServices.getSettingsClient(requireActivity()) - .checkLocationSettings(builder.build()) - - fun weNeedAccess(warningReason: String) { - warn("Telling user we need need location access") - showSnackbar(warningReason) - } - - locationSettingsResponse.addOnSuccessListener { - if (!it.locationSettingsStates?.isBleUsable!! || !it.locationSettingsStates?.isLocationUsable!!) - weNeedAccess(warningReason) - else - debug("We have location access") - } - - locationSettingsResponse.addOnFailureListener { - errormsg("Failed to get location access") - // We always show the toast regardless of what type of exception we receive. Because even non - // resolvable api exceptions mean user still needs to fix something. - - ///if (exception is ResolvableApiException) { - - // Location settings are not satisfied, but this can be fixed - // by showing the user a dialog. - - // Show the dialog by calling startResolutionForResult(), - // and check the result in onActivityResult(). - // exception.startResolutionForResult(this@MainActivity, REQUEST_CHECK_SETTINGS) - - // For now just punt and show a dialog - - // The context might be gone (if activity is going away) by the time this handler is called - weNeedAccess(warningReason) - - //} else - // Exceptions.report(exception) - } + if (myActivity.hasGps() && !gpsEnabled) { + warn("Telling user we need need location access") + showSnackbar(warningReason) } } @@ -1063,7 +1033,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { scanModel.setupScan() // system permissions might have changed while we were away - binding.provideLocationCheckbox.isChecked = myActivity.hasLocationPermission() && myActivity.hasBackgroundPermission() && (model.provideLocation.value ?: false) && isGooglePlayAvailable(requireContext()) + binding.provideLocationCheckbox.isChecked = myActivity.hasBackgroundPermission() && (model.provideLocation.value ?: false) && isGooglePlayAvailable(requireContext()) myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter)