From 2aa33c8d561303fe739cf393172cb270ade0b559 Mon Sep 17 00:00:00 2001 From: James Rich Date: Thu, 18 Jun 2026 12:19:12 -0500 Subject: [PATCH] style: apply spotless formatting to permission changes Co-Authored-By: Claude Opus 4.8 --- .../kotlin/org/meshtastic/app/map/MapView.kt | 6 +++-- .../kotlin/org/meshtastic/app/map/MapView.kt | 9 ++++--- .../core/barcode/BarcodeScannerProvider.kt | 4 +++ .../core/ui/util/PermissionRequestTracker.kt | 10 ++++---- .../meshtastic/core/ui/util/PlatformUtils.kt | 25 +++++++++---------- .../ui/component/PermissionRecoveryCard.kt | 5 ++-- .../core/ui/util/PermissionStatus.kt | 25 +++++++++---------- .../connections/AndroidScannerViewModel.kt | 6 ++--- 8 files changed, 48 insertions(+), 42 deletions(-) diff --git a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt index 16b50c1da..dba44146b 100644 --- a/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/fdroid/kotlin/org/meshtastic/app/map/MapView.kt @@ -67,8 +67,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger -import org.meshtastic.core.ui.util.PermissionStatus -import org.meshtastic.core.ui.util.rememberLocationPermissionState import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource @@ -129,7 +127,9 @@ import org.meshtastic.core.ui.icon.Layers import org.meshtastic.core.ui.icon.Lens import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.PinDrop +import org.meshtastic.core.ui.util.PermissionStatus import org.meshtastic.core.ui.util.formatAgo +import org.meshtastic.core.ui.util.rememberLocationPermissionState import org.meshtastic.core.ui.util.showToast import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState import org.meshtastic.feature.map.LastHeardFilter @@ -636,9 +636,11 @@ fun MapView( onToggleLocationTracking = { when { locationPermission.isGranted -> map.toggleMyLocation() + // Permanently denied: the system won't prompt again, so send the user to settings. locationPermission.status == PermissionStatus.PERMANENTLY_DENIED -> locationPermission.openAppSettings() + else -> { triggerLocationToggleAfterPermission = true locationPermission.request() diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt index be2c3fb73..28f46cb35 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt @@ -56,8 +56,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.touchlab.kermit.Logger -import org.meshtastic.core.ui.util.PermissionStatus -import org.meshtastic.core.ui.util.rememberLocationPermissionState import com.google.android.gms.location.LocationCallback import com.google.android.gms.location.LocationRequest import com.google.android.gms.location.LocationResult @@ -130,8 +128,10 @@ import org.meshtastic.core.ui.icon.Map import org.meshtastic.core.ui.icon.MeshtasticIcons import org.meshtastic.core.ui.icon.TripOrigin import org.meshtastic.core.ui.theme.TracerouteColors +import org.meshtastic.core.ui.util.PermissionStatus import org.meshtastic.core.ui.util.formatAgo import org.meshtastic.core.ui.util.formatPositionTime +import org.meshtastic.core.ui.util.rememberLocationPermissionState import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState import org.meshtastic.feature.map.LastHeardFilter import org.meshtastic.feature.map.component.MapButton @@ -700,8 +700,11 @@ fun MapView( followPhoneBearing = false } } + // Permanently denied: the system won't prompt again, so send the user to settings to recover. - locationPermission.status == PermissionStatus.PERMANENTLY_DENIED -> locationPermission.openAppSettings() + locationPermission.status == PermissionStatus.PERMANENTLY_DENIED -> + locationPermission.openAppSettings() + else -> { triggerLocationToggleAfterPermission = true locationPermission.request() diff --git a/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt b/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt index e3d850176..ed1aa8e0b 100644 --- a/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt +++ b/core/barcode/src/main/kotlin/org/meshtastic/core/barcode/BarcodeScannerProvider.kt @@ -76,10 +76,12 @@ fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner { LaunchedEffect(cameraPermission.status) { when { !pendingScan -> Unit + cameraPermission.isGranted -> { showDialog = true pendingScan = false } + // The request completed without a grant — surface a recovery card instead of failing silently. cameraPermission.status != PermissionStatus.NOT_REQUESTED -> { showPermissionRecovery = true @@ -120,7 +122,9 @@ fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner { override fun startScan() { when (currentStatus.value) { PermissionStatus.GRANTED -> showDialog = true + PermissionStatus.PERMANENTLY_DENIED -> showPermissionRecovery = true + else -> { pendingScan = true cameraPermission.request() diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PermissionRequestTracker.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PermissionRequestTracker.kt index f86e992d1..5b15e0486 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PermissionRequestTracker.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PermissionRequestTracker.kt @@ -21,14 +21,14 @@ import android.content.Context /** * Persists, per Android permission string, whether the app has ever completed a runtime request for it. * - * This flag is the disambiguator required by [computePermissionStatus]: `shouldShowRequestPermissionRationale` - * returns `false` both before the first prompt and after a permanent denial, so a persisted "has been requested" - * marker is the only way to tell the two apart. + * This flag is the disambiguator required by [computePermissionStatus]: `shouldShowRequestPermissionRationale` returns + * `false` both before the first prompt and after a permanent denial, so a persisted "has been requested" marker is the + * only way to tell the two apart. * * Deliberately backed by [android.content.SharedPreferences] rather than DataStore: the flag is read synchronously * inside composition (in the same pass as the rationale check) and written synchronously from a permission-result - * callback. DataStore's asynchronous `Flow` model would introduce a read-after-write race on exactly the transition - * the permission state machine hinges on. + * callback. DataStore's asynchronous `Flow` model would introduce a read-after-write race on exactly the transition the + * permission state machine hinges on. */ internal class PermissionRequestTracker(context: Context) { private val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt index fe7736e8e..4abc85a90 100644 --- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt +++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt @@ -334,16 +334,15 @@ actual fun rememberOpenAppSettings(): () -> Unit { } @Composable -actual fun rememberLocationPermissionState(): PermissionUiState = - rememberRuntimePermissionState( - permissions = - arrayOf( - android.Manifest.permission.ACCESS_FINE_LOCATION, - android.Manifest.permission.ACCESS_COARSE_LOCATION, - ), - // Coarse-only grants are an accepted degraded mode, so any granted permission counts. - requireAll = false, - ) +actual fun rememberLocationPermissionState(): PermissionUiState = rememberRuntimePermissionState( + permissions = + arrayOf( + android.Manifest.permission.ACCESS_FINE_LOCATION, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + ), + // Coarse-only grants are an accepted degraded mode, so any granted permission counts. + requireAll = false, +) @Composable actual fun rememberBluetoothPermissionState(): PermissionUiState { @@ -397,9 +396,9 @@ private fun rememberGrantedPermissionState(): PermissionUiState { } /** - * Shared engine behind every `rememberXxxPermissionState()`. Computes the [PermissionStatus] from the live grant - * state, the persisted "has-been-requested" flag, and `shouldShowRequestPermissionRationale`, refreshing on - * `ON_RESUME` (return from settings) and immediately after a request completes. + * Shared engine behind every `rememberXxxPermissionState()`. Computes the [PermissionStatus] from the live grant state, + * the persisted "has-been-requested" flag, and `shouldShowRequestPermissionRationale`, refreshing on `ON_RESUME` + * (return from settings) and immediately after a request completes. * * @param requireAll when true, all [permissions] must be granted to count as [PermissionStatus.GRANTED]; when false, * any single grant suffices (used by location so a coarse-only grant is accepted — R7). diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PermissionRecoveryCard.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PermissionRecoveryCard.kt index 8767e2b59..727c39db7 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PermissionRecoveryCard.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/PermissionRecoveryCard.kt @@ -45,11 +45,10 @@ import org.meshtastic.core.ui.util.PermissionUiState /** * A reusable error-state card for a missing runtime permission. Generalizes the compass warning/recovery pattern so * every feature presents context plus a single, context-correct recovery action: - * * - [PermissionStatus.NOT_REQUESTED] / [PermissionStatus.DENIED_CAN_RETRY] — a "Grant permission" button that * re-launches the in-context request. - * - [PermissionStatus.PERMANENTLY_DENIED] — an "Open settings" button (user-initiated recovery) since the system - * will no longer show the dialog. + * - [PermissionStatus.PERMANENTLY_DENIED] — an "Open settings" button (user-initiated recovery) since the system will + * no longer show the dialog. * - [PermissionStatus.GRANTED] — renders nothing. * * @param rationale a feature-specific explanation of why the permission is needed. diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PermissionStatus.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PermissionStatus.kt index a87d4b1fd..3a3f7e8e1 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PermissionStatus.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/util/PermissionStatus.kt @@ -19,13 +19,12 @@ package org.meshtastic.core.ui.util /** * The UX-relevant state of a runtime permission, as recommended by the Android permissions guidance * (https://developer.android.com/training/permissions/requesting). - * * - [GRANTED] — the permission is held; proceed. * - [NOT_REQUESTED] — the user has never been prompted; request directly (no rationale needed yet). - * - [DENIED_CAN_RETRY] — the user denied once but the system will still show the dialog; show a - * rationale and offer to re-request. - * - [PERMANENTLY_DENIED] — the system will no longer show the dialog ("Don't allow" twice, or - * "Don't ask again"); the only recovery is the app's settings screen. + * - [DENIED_CAN_RETRY] — the user denied once but the system will still show the dialog; show a rationale and offer to + * re-request. + * - [PERMANENTLY_DENIED] — the system will no longer show the dialog ("Don't allow" twice, or "Don't ask again"); the + * only recovery is the app's settings screen. */ enum class PermissionStatus { GRANTED, @@ -38,13 +37,13 @@ enum class PermissionStatus { * Pure classifier for a runtime permission's UX state. Kept platform-agnostic and side-effect-free so it can be * unit-tested in `commonTest` without an Android `Activity`. * - * **Invariant:** [hasRequested] MUST reflect a *completed* request — it should be persisted from the launcher's - * result callback, never merely when `launch()` is invoked. On Android, `launch()` does not show a dialog once a - * permission is permanently denied, and a user can background the app before a dialog resolves; setting the flag - * pre-emptively would misclassify a first-run user as [PERMANENTLY_DENIED]. + * **Invariant:** [hasRequested] MUST reflect a *completed* request — it should be persisted from the launcher's result + * callback, never merely when `launch()` is invoked. On Android, `launch()` does not show a dialog once a permission is + * permanently denied, and a user can background the app before a dialog resolves; setting the flag pre-emptively would + * misclassify a first-run user as [PERMANENTLY_DENIED]. * - * Note that [shouldShowRationale] is `false` both *before* the first prompt and *after* a permanent denial — which - * is exactly why [hasRequested] is required to disambiguate the two cases. + * Note that [shouldShowRationale] is `false` both *before* the first prompt and *after* a permanent denial — which is + * exactly why [hasRequested] is required to disambiguate the two cases. */ fun computePermissionStatus(granted: Boolean, hasRequested: Boolean, shouldShowRationale: Boolean): PermissionStatus = when { @@ -56,8 +55,8 @@ fun computePermissionStatus(granted: Boolean, hasRequested: Boolean, shouldShowR /** * A reactive snapshot of a runtime permission plus the actions a caller can take. Produced by the - * `rememberXxxPermissionState()` composables and recomputed on `ON_RESUME` so it stays fresh when the user returns - * from a permission dialog or the system settings screen. + * `rememberXxxPermissionState()` composables and recomputed on `ON_RESUME` so it stays fresh when the user returns from + * a permission dialog or the system settings screen. */ data class PermissionUiState(val status: PermissionStatus, val request: () -> Unit, val openAppSettings: () -> Unit) { val isGranted: Boolean diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index 4b89a34db..eb1602eae 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -24,9 +24,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.jetbrains.compose.resources.getString import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.bonding_failed_permissions -import org.meshtastic.core.resources.usb_permission_denied import org.meshtastic.core.ble.BluetoothRepository import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.model.util.anonymize @@ -37,6 +34,9 @@ import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.resources.Res +import org.meshtastic.core.resources.bonding_failed_permissions +import org.meshtastic.core.resources.usb_permission_denied import org.meshtastic.feature.connections.model.AndroidUsbDeviceData import org.meshtastic.feature.connections.model.DeviceListEntry import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase