mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-28 15:35:41 -04:00
style: apply spotless formatting to permission changes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user