diff --git a/app/build.gradle b/app/build.gradle index bfb5aace..c216b66f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -118,7 +118,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.core:core-ktx:1.5.0' - implementation 'androidx.fragment:fragment-ktx:1.3.4' + implementation 'androidx.fragment:fragment-ktx:1.3.5' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 6fbdac6f..ab5cb108 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -25,7 +25,6 @@ import android.view.View import android.widget.TextView import android.widget.Toast import androidx.activity.viewModels -import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -249,38 +248,78 @@ class MainActivity : AppCompatActivity(), Logging, } - /** 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 + */ + fun getMissingMessage(): String? { + val renamedPermissions = mapOf( + // Older versions of android don't know about these permissions - ignore failure to grant + Manifest.permission.ACCESS_COARSE_LOCATION to null, + Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND to null, + Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND to null, + Manifest.permission.ACCESS_FINE_LOCATION to getString(R.string.location) + ) + + val deniedPermissions = getMinimumPermissions().mapNotNull { + if(renamedPermissions.containsKey(it)) + renamedPermissions[it] + else // No localization found - just show the nasty android string + it + } + + return if(deniedPermissions.isEmpty()) + null + else { + val asEnglish = deniedPermissions.joinToString(" & ") + + getString(R.string.permission_missing).format(asEnglish) + } + } + /** Possibly prompt user to grant permissions * * @return true if we already have the needed permissions */ private fun requestPermission(missingPerms: List = getMinimumPermissions()): Boolean = if (missingPerms.isNotEmpty()) { - missingPerms.forEach { - // Permission is not granted - // Should we show an explanation? - if (ActivityCompat.shouldShowRequestPermissionRationale(this, it)) { - // FIXME - // Show an explanation to the user *asynchronously* -- don't block - // this thread waiting for the user's response! After the user - // sees the explanation, try again to request the permission. - } + val shouldShow = missingPerms.filter { + ActivityCompat.shouldShowRequestPermissionRationale(this, it) } - // Ask for all the missing perms - ActivityCompat.requestPermissions( - this, - missingPerms.toTypedArray(), - DID_REQUEST_PERM - ) + fun doRequest() { + info("requesting permissions") + // Ask for all the missing perms + ActivityCompat.requestPermissions( + this, + missingPerms.toTypedArray(), + DID_REQUEST_PERM + ) + } + + if (shouldShow.isNotEmpty()) { + // 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(getMissingMessage()) + .setNeutralButton("Cancel (no radio access)") { _, _ -> + error("User bailed due to permissions") + } + .setPositiveButton("Allow (will show dialog)") { _, _ -> + doRequest() + } + .show() + } else { + info("Permissions $missingPerms missing, no need to show dialog, just asking OS") + doRequest() + } - // DID_REQUEST_PERM is an - // app-defined int constant. The callback method gets the - // result of the request. - error("Permissions missing, asked user to grant") false } else { // Permission has already been granted @@ -295,20 +334,11 @@ class MainActivity : AppCompatActivity(), Logging, */ @SuppressLint("InlinedApi") // This function is careful to work with old APIs correctly fun warnMissingPermissions(): Boolean { - // Older versions of android don't know about these permissions - ignore failure to grant - val ignoredPermissions = setOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND, - Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND - ) + val message = getMissingMessage() - val deniedPermissions = getMinimumPermissions().filter { name -> - !ignoredPermissions.contains(name) - } - - return if (deniedPermissions.isNotEmpty()) { - errormsg("Denied permissions: ${deniedPermissions.joinToString(",")}") - showToast(R.string.permission_missing) + return if (message != null) { + errormsg("Denied permissions: $message") + showToast(message) true } else false 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 7a2544ea..0d3255b3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -35,6 +35,7 @@ import com.geeksville.mesh.MainActivity import com.geeksville.mesh.R import com.geeksville.mesh.RadioConfigProtos import com.geeksville.mesh.android.bluetoothManager +import com.geeksville.mesh.android.hasBackgroundPermission import com.geeksville.mesh.android.usbManager import com.geeksville.mesh.databinding.SettingsFragmentBinding import com.geeksville.mesh.model.UIViewModel @@ -460,6 +461,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { requireContext().getSystemService(CompanionDeviceManager::class.java) } + private val myActivity get() = requireActivity() as MainActivity + override fun onDestroy() { guiJob.cancel() super.onDestroy() @@ -564,7 +567,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { // Update the status string (highest priority messages first) val info = model.myNodeInfo.value val statusText = binding.scanStatusText + val permissionsWarning = myActivity.getMissingMessage() when { + permissionsWarning != null -> + statusText.text = permissionsWarning + region == RadioConfigProtos.RegionCode.Unset -> statusText.text = getString(R.string.must_set_region) @@ -643,6 +650,24 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { requireActivity().hideKeyboard() } + binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked -> + if (view.isPressed && isChecked) { // We want to ignore changes caused by code (as opposed to the user) + debug("User changed location tracking to $isChecked") + view.isChecked = + myActivity.hasBackgroundPermission() // Don't check the box until the system setting changes + if (!view.isChecked) + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.background_required) + .setMessage(R.string.why_background_required) + .setNeutralButton(R.string.cancel) { _, _ -> + debug("Decided not to report a bug") + } + .setPositiveButton(getString(R.string.show_system_settings)) { _, _ -> + myActivity.requestBackgroundPermission() + } + .show() + } + } val app = (requireContext().applicationContext as GeeksvilleApplication) @@ -689,7 +714,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { binding.scanStatusText.setText(R.string.starting_pairing) b.isChecked = - scanModel.onSelected(requireActivity() as MainActivity, device) + scanModel.onSelected(myActivity, device) if (!b.isSelected) binding.scanStatusText.text = getString(R.string.please_pair) @@ -812,8 +837,6 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { .setSingleDevice(false) .build() - //val mainActivity = requireActivity() as MainActivity - // When the app tries to pair with the Bluetooth device, show the // appropriate pairing request dialog to the user. deviceManager.associate( @@ -901,7 +924,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { } locationSettingsResponse.addOnSuccessListener { - if(!it.locationSettingsStates.isBleUsable || !it.locationSettingsStates.isLocationUsable) + if (!it.locationSettingsStates.isBleUsable || !it.locationSettingsStates.isLocationUsable) weNeedAccess() else debug("We have location access") @@ -953,15 +976,16 @@ class SettingsFragment : ScreenFragment("Settings"), Logging { if (!hasCompanionDeviceApi) scanModel.startScan() - val activity = requireActivity() as MainActivity + // system permissions might have changed while we were away + binding.provideLocationCheckbox.isChecked = myActivity.hasBackgroundPermission() - activity.registerReceiver(updateProgressReceiver, updateProgressFilter) + myActivity.registerReceiver(updateProgressReceiver, updateProgressFilter) // Keep reminding user BLE is still off - val hasUSB = activity?.let { SerialInterface.findDrivers(it).isNotEmpty() } ?: true + val hasUSB = SerialInterface.findDrivers(myActivity).isNotEmpty() if (!hasUSB) { - // First warn about permissions, and then if needed warn abotu settings - if(!activity.warnMissingPermissions()) { + // First warn about permissions, and then if needed warn about settings + if (!myActivity.warnMissingPermissions()) { // Warn user if BLE is disabled if (scanModel.bluetoothAdapter?.isEnabled != true) { Toast.makeText( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85adfc27..debdce91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -35,7 +35,7 @@ You have analytics disabled. Unfortunately our map provider (mapbox) requires analytics to be allowed for their \'free\' plan. So we have turned off the map view.\n\n If you would like to see the map, you\'ll need to turn on analytics in the Settings pane (also, for the time being you might need to force restart the application).\n\n If you are interested in us paying for mapbox (or switching to a different map provider), please post in meshtastic.discourse.group - A required permission is missing, Meshtastic won\'t be able to work properly. Please enable in Android application settings. + Meshtastic needs %s permission granted. Without this Android will not allow connecting to the LoRa bluetooth radios. Radio was sleeping, could not change channel Report Bug Report a bug @@ -108,4 +108,9 @@ Dark System default Choose theme + Background location access required + Show system settings + In order to enable this feature, you must grant this application "allow location access all the time" permission. This allows meshtastic to read your location while the application is in the background, so that it can send your location to other members of your mesh. + Required permissions + location