mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-30 00:15:48 -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.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.touchlab.kermit.Logger
|
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 com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.StringResource
|
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.Lens
|
||||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||||
import org.meshtastic.core.ui.icon.PinDrop
|
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.formatAgo
|
||||||
|
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||||
import org.meshtastic.core.ui.util.showToast
|
import org.meshtastic.core.ui.util.showToast
|
||||||
import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
|
import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
|
||||||
import org.meshtastic.feature.map.LastHeardFilter
|
import org.meshtastic.feature.map.LastHeardFilter
|
||||||
@@ -636,9 +636,11 @@ fun MapView(
|
|||||||
onToggleLocationTracking = {
|
onToggleLocationTracking = {
|
||||||
when {
|
when {
|
||||||
locationPermission.isGranted -> map.toggleMyLocation()
|
locationPermission.isGranted -> map.toggleMyLocation()
|
||||||
|
|
||||||
// Permanently denied: the system won't prompt again, so send the user to settings.
|
// Permanently denied: the system won't prompt again, so send the user to settings.
|
||||||
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
||||||
locationPermission.openAppSettings()
|
locationPermission.openAppSettings()
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
triggerLocationToggleAfterPermission = true
|
triggerLocationToggleAfterPermission = true
|
||||||
locationPermission.request()
|
locationPermission.request()
|
||||||
|
|||||||
@@ -56,8 +56,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.touchlab.kermit.Logger
|
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.LocationCallback
|
||||||
import com.google.android.gms.location.LocationRequest
|
import com.google.android.gms.location.LocationRequest
|
||||||
import com.google.android.gms.location.LocationResult
|
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.MeshtasticIcons
|
||||||
import org.meshtastic.core.ui.icon.TripOrigin
|
import org.meshtastic.core.ui.icon.TripOrigin
|
||||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
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.formatAgo
|
||||||
import org.meshtastic.core.ui.util.formatPositionTime
|
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.BaseMapViewModel.MapFilterState
|
||||||
import org.meshtastic.feature.map.LastHeardFilter
|
import org.meshtastic.feature.map.LastHeardFilter
|
||||||
import org.meshtastic.feature.map.component.MapButton
|
import org.meshtastic.feature.map.component.MapButton
|
||||||
@@ -700,8 +700,11 @@ fun MapView(
|
|||||||
followPhoneBearing = false
|
followPhoneBearing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permanently denied: the system won't prompt again, so send the user to settings to recover.
|
// 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 -> {
|
else -> {
|
||||||
triggerLocationToggleAfterPermission = true
|
triggerLocationToggleAfterPermission = true
|
||||||
locationPermission.request()
|
locationPermission.request()
|
||||||
|
|||||||
@@ -76,10 +76,12 @@ fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner {
|
|||||||
LaunchedEffect(cameraPermission.status) {
|
LaunchedEffect(cameraPermission.status) {
|
||||||
when {
|
when {
|
||||||
!pendingScan -> Unit
|
!pendingScan -> Unit
|
||||||
|
|
||||||
cameraPermission.isGranted -> {
|
cameraPermission.isGranted -> {
|
||||||
showDialog = true
|
showDialog = true
|
||||||
pendingScan = false
|
pendingScan = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// The request completed without a grant — surface a recovery card instead of failing silently.
|
// The request completed without a grant — surface a recovery card instead of failing silently.
|
||||||
cameraPermission.status != PermissionStatus.NOT_REQUESTED -> {
|
cameraPermission.status != PermissionStatus.NOT_REQUESTED -> {
|
||||||
showPermissionRecovery = true
|
showPermissionRecovery = true
|
||||||
@@ -120,7 +122,9 @@ fun rememberBarcodeScanner(onResult: (String?) -> Unit): BarcodeScanner {
|
|||||||
override fun startScan() {
|
override fun startScan() {
|
||||||
when (currentStatus.value) {
|
when (currentStatus.value) {
|
||||||
PermissionStatus.GRANTED -> showDialog = true
|
PermissionStatus.GRANTED -> showDialog = true
|
||||||
|
|
||||||
PermissionStatus.PERMANENTLY_DENIED -> showPermissionRecovery = true
|
PermissionStatus.PERMANENTLY_DENIED -> showPermissionRecovery = true
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
pendingScan = true
|
pendingScan = true
|
||||||
cameraPermission.request()
|
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.
|
* 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`
|
* This flag is the disambiguator required by [computePermissionStatus]: `shouldShowRequestPermissionRationale` returns
|
||||||
* returns `false` both before the first prompt and after a permanent denial, so a persisted "has been requested"
|
* `false` both before the first prompt and after a permanent denial, so a persisted "has been requested" marker is the
|
||||||
* marker is the only way to tell the two apart.
|
* only way to tell the two apart.
|
||||||
*
|
*
|
||||||
* Deliberately backed by [android.content.SharedPreferences] rather than DataStore: the flag is read synchronously
|
* 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
|
* 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
|
* callback. DataStore's asynchronous `Flow` model would introduce a read-after-write race on exactly the transition the
|
||||||
* the permission state machine hinges on.
|
* permission state machine hinges on.
|
||||||
*/
|
*/
|
||||||
internal class PermissionRequestTracker(context: Context) {
|
internal class PermissionRequestTracker(context: Context) {
|
||||||
private val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val prefs = context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|||||||
@@ -334,16 +334,15 @@ actual fun rememberOpenAppSettings(): () -> Unit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberLocationPermissionState(): PermissionUiState =
|
actual fun rememberLocationPermissionState(): PermissionUiState = rememberRuntimePermissionState(
|
||||||
rememberRuntimePermissionState(
|
permissions =
|
||||||
permissions =
|
arrayOf(
|
||||||
arrayOf(
|
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
||||||
android.Manifest.permission.ACCESS_FINE_LOCATION,
|
android.Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||||
android.Manifest.permission.ACCESS_COARSE_LOCATION,
|
),
|
||||||
),
|
// Coarse-only grants are an accepted degraded mode, so any granted permission counts.
|
||||||
// Coarse-only grants are an accepted degraded mode, so any granted permission counts.
|
requireAll = false,
|
||||||
requireAll = false,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberBluetoothPermissionState(): PermissionUiState {
|
actual fun rememberBluetoothPermissionState(): PermissionUiState {
|
||||||
@@ -397,9 +396,9 @@ private fun rememberGrantedPermissionState(): PermissionUiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared engine behind every `rememberXxxPermissionState()`. Computes the [PermissionStatus] from the live grant
|
* Shared engine behind every `rememberXxxPermissionState()`. Computes the [PermissionStatus] from the live grant state,
|
||||||
* state, the persisted "has-been-requested" flag, and `shouldShowRequestPermissionRationale`, refreshing on
|
* the persisted "has-been-requested" flag, and `shouldShowRequestPermissionRationale`, refreshing on `ON_RESUME`
|
||||||
* `ON_RESUME` (return from settings) and immediately after a request completes.
|
* (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,
|
* @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).
|
* 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
|
* 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:
|
* every feature presents context plus a single, context-correct recovery action:
|
||||||
*
|
|
||||||
* - [PermissionStatus.NOT_REQUESTED] / [PermissionStatus.DENIED_CAN_RETRY] — a "Grant permission" button that
|
* - [PermissionStatus.NOT_REQUESTED] / [PermissionStatus.DENIED_CAN_RETRY] — a "Grant permission" button that
|
||||||
* re-launches the in-context request.
|
* re-launches the in-context request.
|
||||||
* - [PermissionStatus.PERMANENTLY_DENIED] — an "Open settings" button (user-initiated recovery) since the system
|
* - [PermissionStatus.PERMANENTLY_DENIED] — an "Open settings" button (user-initiated recovery) since the system will
|
||||||
* will no longer show the dialog.
|
* no longer show the dialog.
|
||||||
* - [PermissionStatus.GRANTED] — renders nothing.
|
* - [PermissionStatus.GRANTED] — renders nothing.
|
||||||
*
|
*
|
||||||
* @param rationale a feature-specific explanation of why the permission is needed.
|
* @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
|
* The UX-relevant state of a runtime permission, as recommended by the Android permissions guidance
|
||||||
* (https://developer.android.com/training/permissions/requesting).
|
* (https://developer.android.com/training/permissions/requesting).
|
||||||
*
|
|
||||||
* - [GRANTED] — the permission is held; proceed.
|
* - [GRANTED] — the permission is held; proceed.
|
||||||
* - [NOT_REQUESTED] — the user has never been prompted; request directly (no rationale needed yet).
|
* - [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
|
* - [DENIED_CAN_RETRY] — the user denied once but the system will still show the dialog; show a rationale and offer to
|
||||||
* rationale and offer to re-request.
|
* re-request.
|
||||||
* - [PERMANENTLY_DENIED] — the system will no longer show the dialog ("Don't allow" twice, or
|
* - [PERMANENTLY_DENIED] — the system will no longer show the dialog ("Don't allow" twice, or "Don't ask again"); the
|
||||||
* "Don't ask again"); the only recovery is the app's settings screen.
|
* only recovery is the app's settings screen.
|
||||||
*/
|
*/
|
||||||
enum class PermissionStatus {
|
enum class PermissionStatus {
|
||||||
GRANTED,
|
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
|
* 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`.
|
* unit-tested in `commonTest` without an Android `Activity`.
|
||||||
*
|
*
|
||||||
* **Invariant:** [hasRequested] MUST reflect a *completed* request — it should be persisted from the launcher's
|
* **Invariant:** [hasRequested] MUST reflect a *completed* request — it should be persisted from the launcher's result
|
||||||
* result callback, never merely when `launch()` is invoked. On Android, `launch()` does not show a dialog once a
|
* callback, never merely when `launch()` is invoked. On Android, `launch()` does not show a dialog once a permission is
|
||||||
* permission is permanently denied, and a user can background the app before a dialog resolves; setting the flag
|
* permanently denied, and a user can background the app before a dialog resolves; setting the flag pre-emptively would
|
||||||
* pre-emptively would misclassify a first-run user as [PERMANENTLY_DENIED].
|
* misclassify a first-run user as [PERMANENTLY_DENIED].
|
||||||
*
|
*
|
||||||
* Note that [shouldShowRationale] is `false` both *before* the first prompt and *after* a permanent denial — which
|
* Note that [shouldShowRationale] is `false` both *before* the first prompt and *after* a permanent denial — which is
|
||||||
* is exactly why [hasRequested] is required to disambiguate the two cases.
|
* exactly why [hasRequested] is required to disambiguate the two cases.
|
||||||
*/
|
*/
|
||||||
fun computePermissionStatus(granted: Boolean, hasRequested: Boolean, shouldShowRationale: Boolean): PermissionStatus =
|
fun computePermissionStatus(granted: Boolean, hasRequested: Boolean, shouldShowRationale: Boolean): PermissionStatus =
|
||||||
when {
|
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
|
* 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
|
* `rememberXxxPermissionState()` composables and recomputed on `ON_RESUME` so it stays fresh when the user returns from
|
||||||
* from a permission dialog or the system settings screen.
|
* a permission dialog or the system settings screen.
|
||||||
*/
|
*/
|
||||||
data class PermissionUiState(val status: PermissionStatus, val request: () -> Unit, val openAppSettings: () -> Unit) {
|
data class PermissionUiState(val status: PermissionStatus, val request: () -> Unit, val openAppSettings: () -> Unit) {
|
||||||
val isGranted: Boolean
|
val isGranted: Boolean
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ import kotlinx.coroutines.flow.onEach
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.getString
|
import org.jetbrains.compose.resources.getString
|
||||||
import org.koin.core.annotation.KoinViewModel
|
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.ble.BluetoothRepository
|
||||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||||
import org.meshtastic.core.model.util.anonymize
|
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.RadioPrefs
|
||||||
import org.meshtastic.core.repository.ServiceRepository
|
import org.meshtastic.core.repository.ServiceRepository
|
||||||
import org.meshtastic.core.repository.UiPrefs
|
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.AndroidUsbDeviceData
|
||||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||||
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
|
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
|
||||||
|
|||||||
Reference in New Issue
Block a user