mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-03 21:53:55 -04:00
refactor permissions
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package com.geeksville.mesh
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.content.*
|
||||
@@ -34,7 +33,7 @@ import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.android.ServiceClient
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.android.getMissingPermissions
|
||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||
import com.geeksville.mesh.model.BTScanModel
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
@@ -116,16 +115,7 @@ eventually:
|
||||
val utf8: Charset = Charset.forName("UTF-8")
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : BaseActivity(), Logging,
|
||||
ActivityCompat.OnRequestPermissionsResultCallback {
|
||||
|
||||
companion object {
|
||||
// const val REQUEST_ENABLE_BT = 10
|
||||
const val DID_REQUEST_PERM = 11
|
||||
// const val RC_SIGN_IN = 12 // google signin completed
|
||||
// const val SELECT_DEVICE_REQUEST_CODE = 13
|
||||
// const val CREATE_CSV_FILE = 14
|
||||
}
|
||||
class MainActivity : BaseActivity(), Logging {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
||||
@@ -139,6 +129,15 @@ class MainActivity : BaseActivity(), Logging,
|
||||
@Inject
|
||||
internal lateinit var radioInterfaceService: RadioInterfaceService
|
||||
|
||||
private val requestPermissionsLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (!permissions.entries.all { it.value }) {
|
||||
errormsg("User denied permissions")
|
||||
showSnackbar(getString(R.string.permission_missing_31))
|
||||
}
|
||||
bluetoothViewModel.permissionsUpdated()
|
||||
}
|
||||
|
||||
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
|
||||
|
||||
// private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings
|
||||
@@ -188,74 +187,28 @@ class MainActivity : BaseActivity(), Logging,
|
||||
/** Get the minimum permissions our app needs to run correctly
|
||||
*/
|
||||
private fun getMinimumPermissions(): Array<String> {
|
||||
val perms = mutableListOf(
|
||||
Manifest.permission.WAKE_LOCK
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
// We only need this for logging to capture files for the simulator - turn off for most users
|
||||
// Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
// 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)
|
||||
} else {
|
||||
perms.add(Manifest.permission.BLUETOOTH)
|
||||
}
|
||||
*/
|
||||
perms.add(Manifest.permission.BLUETOOTH)
|
||||
|
||||
// Some old phones complain about requesting perms they don't understand
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
perms.add(Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
|
||||
perms.add(Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
|
||||
}
|
||||
|
||||
return getMissingPermissions(perms)
|
||||
}
|
||||
|
||||
/** Ask the user to grant Bluetooth scan/discovery permission */
|
||||
fun requestScanPermission() = requestPermission(getScanPermissions(), true)
|
||||
|
||||
/**
|
||||
* @return a localized string warning user about missing permissions. Or null if everything is find
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getMissingMessage(
|
||||
missingPerms: Array<String> = getMinimumPermissions()
|
||||
): 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),
|
||||
Manifest.permission.BLUETOOTH_CONNECT to "Bluetooth"
|
||||
)
|
||||
|
||||
val deniedPermissions = missingPerms.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
|
||||
* @param shouldShowDialog usually false in cases where we've already shown a dialog elsewhere we skip it.
|
||||
* @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<String> = getMinimumPermissions(),
|
||||
shouldShowDialog: Boolean = false
|
||||
shouldShowDialog: Boolean = true
|
||||
): Boolean =
|
||||
if (missingPerms.isNotEmpty()) {
|
||||
val shouldShow = missingPerms.filter {
|
||||
@@ -265,11 +218,7 @@ class MainActivity : BaseActivity(), Logging,
|
||||
fun doRequest() {
|
||||
info("requesting permissions")
|
||||
// Ask for all the missing perms
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
missingPerms,
|
||||
DID_REQUEST_PERM
|
||||
)
|
||||
requestPermissionsLauncher.launch(missingPerms)
|
||||
}
|
||||
|
||||
if (shouldShow.isNotEmpty() && shouldShowDialog) {
|
||||
@@ -280,7 +229,7 @@ class MainActivity : BaseActivity(), Logging,
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.required_permissions))
|
||||
.setMessage(getMissingMessage(missingPerms))
|
||||
.setMessage(getString(R.string.permission_missing_31))
|
||||
.setNeutralButton(R.string.cancel) { _, _ ->
|
||||
warn("User bailed due to permissions")
|
||||
}
|
||||
@@ -300,81 +249,6 @@ class MainActivity : BaseActivity(), Logging,
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remind user he's disabled permissions we need
|
||||
*
|
||||
* @return true if we did warn
|
||||
*/
|
||||
@SuppressLint("InlinedApi") // This function is careful to work with old APIs correctly
|
||||
fun warnMissingPermissions(): Boolean {
|
||||
val message = getMissingMessage()
|
||||
|
||||
return if (message != null) {
|
||||
errormsg("Denied permissions: $message")
|
||||
showSnackbar(message)
|
||||
true
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
when (requestCode) {
|
||||
DID_REQUEST_PERM -> {
|
||||
// If request is cancelled, the result arrays are empty.
|
||||
if ((grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED)
|
||||
) {
|
||||
// Permission is granted. Continue the action or workflow
|
||||
// in your app.
|
||||
|
||||
// yay!
|
||||
} else {
|
||||
// Explain to the user that the feature is unavailable because
|
||||
// the features requires a permission that the user has denied.
|
||||
// At the same time, respect the user's decision. Don't link to
|
||||
// system settings in an effort to convince the user to change
|
||||
// their decision.
|
||||
warnMissingPermissions()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// ignore other requests
|
||||
}
|
||||
}
|
||||
|
||||
bluetoothViewModel.permissionsUpdated()
|
||||
}
|
||||
|
||||
|
||||
private fun sendTestPackets() {
|
||||
exceptionReporter {
|
||||
val m = model.meshService!!
|
||||
|
||||
// Do some test operations
|
||||
val testPayload = "hello world".toByteArray()
|
||||
m.send(
|
||||
DataPacket(
|
||||
"+16508675310",
|
||||
testPayload,
|
||||
Portnums.PortNum.PRIVATE_APP_VALUE
|
||||
)
|
||||
)
|
||||
m.send(
|
||||
DataPacket(
|
||||
"+16508675310",
|
||||
testPayload,
|
||||
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask user to rate in play store
|
||||
private fun askToRate() {
|
||||
exceptionReporter { // Got one IllegalArgumentException from inside this lib, but we don't want to crash our app because of bugs in this optional feature
|
||||
@@ -417,23 +291,6 @@ class MainActivity : BaseActivity(), Logging,
|
||||
|
||||
/// Set theme
|
||||
setUITheme(prefs)
|
||||
|
||||
/* not yet working
|
||||
// Configure sign-in to request the user's ID, email address, and basic
|
||||
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
|
||||
val gso =
|
||||
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
|
||||
.requestEmail()
|
||||
.build()
|
||||
|
||||
// Build a GoogleSignInClient with the options specified by gso.
|
||||
UIState.googleSignInClient = GoogleSignIn.getClient(this, gso);
|
||||
|
||||
*/
|
||||
|
||||
/* setContent {
|
||||
MeshApp()
|
||||
} */
|
||||
setContentView(binding.root)
|
||||
|
||||
initToolbar()
|
||||
|
||||
@@ -55,48 +55,22 @@ fun Context.getMissingPermissions(perms: List<String>): Array<String> = perms.fi
|
||||
}.toTypedArray()
|
||||
|
||||
/**
|
||||
* Bluetooth connect permissions (or empty if we already have what we need)
|
||||
* Bluetooth permissions (or empty if we already have what we need)
|
||||
*/
|
||||
fun Context.getConnectPermissions(): Array<String> {
|
||||
fun Context.getBluetoothPermissions(): Array<String> {
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
/* 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)
|
||||
} else {
|
||||
perms.add(Manifest.permission.BLUETOOTH)
|
||||
}
|
||||
*/
|
||||
return getMissingPermissions(perms)
|
||||
}
|
||||
|
||||
/** @return true if the user already has Bluetooth connect permission */
|
||||
fun Context.hasConnectPermission() = getConnectPermissions().isEmpty()
|
||||
|
||||
/**
|
||||
* Bluetooth scan/discovery permissions (or empty if we already have what we need)
|
||||
*/
|
||||
fun Context.getScanPermissions(): Array<String> {
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
/* TODO - wait for targetSdkVersion 31
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
perms.add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
} else if (!BluetoothInterface.hasCompanionDeviceApi(this)) {
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
||||
}
|
||||
*/
|
||||
if (!hasCompanionDeviceApi()) {
|
||||
perms.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
perms.add(Manifest.permission.BLUETOOTH_ADMIN)
|
||||
}
|
||||
|
||||
return getMissingPermissions(perms)
|
||||
}
|
||||
|
||||
/** @return true if the user already has Bluetooth scan/discovery permission */
|
||||
fun Context.hasScanPermission() = getScanPermissions().isEmpty()
|
||||
fun Context.hasBluetoothPermission() = getBluetoothPermissions().isEmpty()
|
||||
|
||||
/**
|
||||
* Camera permission (or empty if we already have what we need)
|
||||
|
||||
@@ -131,7 +131,7 @@ class BTScanModel @Inject constructor(
|
||||
private val bluetoothAdapter = context.bluetoothManager?.adapter
|
||||
private val deviceManager get() = context.deviceManager
|
||||
val hasCompanionDeviceApi get() = context.hasCompanionDeviceApi()
|
||||
private val hasConnectPermission get() = application.hasConnectPermission()
|
||||
private val hasBluetoothPermission get() = application.hasBluetoothPermission()
|
||||
private val usbManager get() = context.usbManager
|
||||
|
||||
var selectedAddress: String? = null
|
||||
|
||||
@@ -9,7 +9,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.android.hasConnectPermission
|
||||
import com.geeksville.mesh.android.hasBluetoothPermission
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -66,7 +66,7 @@ class BluetoothRepository @Inject constructor(
|
||||
@SuppressLint("MissingPermission")
|
||||
internal suspend fun updateBluetoothState() {
|
||||
val newState: BluetoothState = bluetoothAdapterLazy.get()?.takeIf {
|
||||
application.hasConnectPermission().also { hasPerms ->
|
||||
application.hasBluetoothPermission().also { hasPerms ->
|
||||
if (!hasPerms) errormsg("Still missing needed bluetooth permissions")
|
||||
}
|
||||
}?.let { adapter ->
|
||||
|
||||
@@ -179,19 +179,16 @@ 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
|
||||
|
||||
connected == MeshService.ConnectionState.CONNECTED -> {
|
||||
when (connected) {
|
||||
MeshService.ConnectionState.CONNECTED -> {
|
||||
statusText.text = if (region.number == 0) getString(R.string.must_set_region)
|
||||
else getString(R.string.connected_to).format(info?.firmwareString ?: "unknown")
|
||||
}
|
||||
connected == MeshService.ConnectionState.DISCONNECTED ->
|
||||
MeshService.ConnectionState.DISCONNECTED ->
|
||||
statusText.text = getString(R.string.not_connected)
|
||||
connected == MeshService.ConnectionState.DEVICE_SLEEP ->
|
||||
MeshService.ConnectionState.DEVICE_SLEEP ->
|
||||
statusText.text = getString(R.string.connected_sleeping)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,14 +477,40 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
||||
|
||||
initCommonUI()
|
||||
|
||||
val requestPermissionAndScanLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
|
||||
if (permissions.entries.all { it.value }) {
|
||||
checkLocationEnabled()
|
||||
scanLeDevice()
|
||||
} else {
|
||||
errormsg("User denied scan permissions")
|
||||
showSnackbar(getString(R.string.permission_missing))
|
||||
}
|
||||
}
|
||||
|
||||
binding.changeRadioButton.setOnClickListener {
|
||||
debug("User clicked changeRadioButton")
|
||||
if (!myActivity.hasScanPermission()) {
|
||||
myActivity.requestScanPermission()
|
||||
} else {
|
||||
checkBTEnabled()
|
||||
if (!scanModel.hasCompanionDeviceApi) checkLocationEnabled()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user