diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 6144c8fe..f78f4fe8 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 { - val perms = mutableListOf( - Manifest.permission.WAKE_LOCK + val perms = mutableListOf() - // 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 = 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 = 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, - 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() 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 e430b5d6..098b6853 100644 --- a/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt +++ b/app/src/main/java/com/geeksville/mesh/android/ContextServices.kt @@ -55,48 +55,22 @@ fun Context.getMissingPermissions(perms: List): Array = 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 { +fun Context.getBluetoothPermissions(): Array { val perms = mutableListOf() /* 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 { - val perms = mutableListOf() - -/* 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) 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 21573405..4e6a71b8 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -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 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 c416e6b4..0188eaf6 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 @@ -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 -> 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 a5461b48..9e5c6aa4 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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() + } } } } diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index e0d26d82..1dd15048 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -113,7 +113,6 @@ 시스템 설정 보기 기능을위해서, 옵션에서 위치권한을 \"항상 허용\" 으로 설정해야합니다.앱이 닫혀있거나 사용중이지 않을때에도 메쉬태스틱이 당신의 스마트폰의 위치를 읽어 당신의 메쉬의 다른 사용자에게 위치를 전송할수있게합니다. 권한부여 필요 - 장소 취소 (no radio access) 허용 (대화에 보일것입니다) 메쉬에 현재 위치 공유 diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index f1d54757..a0d81a65 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -26,7 +26,7 @@ Are you sure you want to change the channel? All communication with other nodes will stop until you share the new channel settings. Odebrano nowy URL kanału Chcesz przełączyć na \'%s\' kanał? - Meshtastic potrzebuje %s zezwolenie i lokalizacja muszą być włączone, aby można było znaleźć nowe urządzenia przez Bluetooth. Możesz go później wyłączyć. + Meshtastic potrzebuje lokalizacja zezwolenie i lokalizacja muszą być włączone, aby można było znaleźć nowe urządzenia przez Bluetooth. Możesz go później wyłączyć. Radio było w trybie uśpienia, nie mogło zmienić kanału Zgłoś bug Zgłoś bug @@ -105,7 +105,6 @@ Lokalizacja w tle Pokaż ustawienia systemowe Wymagane uprawnienia - Lokalizacja Podaj lokalizację do sieci Usunąć wiadomość? diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 71d019c6..8393a968 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -26,7 +26,7 @@ Tem certeza que deseja mudar de canal? Toda comunicação com os outros dispositivos será interrompida até serem compartilhadas as novas configurações do canal. Novo link de canal recebido Deseja mudar para o canal \'%s\'? - Meshtastic precisa da permissão de %s e da localização ativada para encontrar novos dispositivos via bluetooth. Você pode desativar novamente depois. + Meshtastic precisa da permissão de localização e localização ativada para encontrar novos dispositivos via bluetooth. Você pode desativar novamente depois. Rádio estava em suspensão (sleep), não foi possível mudar de canal Informar Bug Informar um bug @@ -102,7 +102,6 @@ Exibir configurações do sistema Para este recurso, você deve conceder permissão para acessar Local com a opção \"Permitir o tempo todo\".\nIsto permite ao Meshtastic ler a localização do seu smartphone e enviar aos membros da sua mesh, mesmo quando o aplicativo está fechado ou não em uso. Permissões necessárias - localização Cancelar (sem acesso ao rádio) Permitir (exibe diálogo) Fornecer localização para mesh diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 78f552fe..4634d7a3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -25,7 +25,7 @@ Tem certeza que deseja mudar de canal? Todas as comunicações com outros nós serão interrompidas até que partilhe as novas configurações do canal. Novo Link Recebido do Canal Pretende mudar para o canal \'%s\' ? - Meshtastic precisa da permissão de %s e da localização ativada para encontrar novos dispositivos via bluetooth. Você pode desativar novamente depois. + Meshtastic precisa da permissão de localização e localização ativada para encontrar novos dispositivos via bluetooth. Você pode desativar novamente depois. O rádio estava a dormir, não conseguia mudar de canal Reportar Bug Reportar a bug @@ -102,7 +102,6 @@ Exibir configurações do sistema Para este recurso, você deve conceder permissão para acessar Local com a opção \"Permitir o tempo todo\".\nIsto permite ao Meshtastic ler a localização do seu smartphone e enviar aos membros da sua mesh, mesmo quando o aplicativo está fechado ou não em uso. Permissões necessárias - localização Cancelar (sem acesso ao rádio) Permitir (exibe diálogo) Fornecer localização para mesh diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 54d55657..40cfb548 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -107,7 +107,6 @@ Zobraziť nastavenia systému Pre túto možnosť musíte povoliť prístup ku polohe zariadenia v režime \"Vždy povolené\".\nTáto možnosť povolí aplikácii Meshtastic zistiť polohu Vášho zariadenia a odoslať ju členom Vašej siete aj keď je aplikácia Meshtastic vypnutá alebo sa nepoužíva. Požadované oprávnenia - poloha Zrušiť (žiaden dosah na vysielač) Povoliť (zobrazí potvrdzovací dialóg) Odoslať polohu do siete diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9e7e6aaa..de6e03f4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,7 +30,7 @@ Are you sure you want to change the channel? All communication with other nodes will stop until you share the new channel settings. New Channel URL received Do you want to switch to the \'%s\' channel? - Meshtastic needs %s permission and location must be turned on to find new devices via bluetooth. You can turn it off again afterwards. + Meshtastic needs location permission and location must be turned on to find new devices via Bluetooth. You can turn it off again afterwards. Radio was sleeping, could not change channel Report Bug Report a bug @@ -108,7 +108,6 @@ Show system settings For this feature, you must grant Location permission option \"Allow all the time\".\nThis allows Meshtastic to read your smartphone location and send it to other members of your mesh, even when the app is closed or not in use. Required permissions - location Cancel (no radio access) Allow (will show dialog) Provide location to mesh @@ -155,4 +154,5 @@ Are you sure you want to factory reset? This will clear all device configuration you have done. Bluetooth disabled. + Meshtastic needs Nearby devices permission to find and connect to devices via Bluetooth. You can turn it off when not in use.