Skip to content

Commit 63c48ec

Browse files
committed
fix(permissions): don't presist Granted to memory; rely on system level checks
Handles "only this time" properly Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent f40800d commit 63c48ec

7 files changed

Lines changed: 95 additions & 52 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/MainActivity.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.getcode.opencode.exchange.Exchange
4444
import com.getcode.solana.rpc.RpcConfig
4545
import com.getcode.ui.testing.LocalUiTesting
4646
import com.getcode.util.permissions.PermissionChecker
47+
import com.getcode.util.permissions.ProvidePermissionChecker
4748
import com.getcode.util.resources.LocalResources
4849
import com.getcode.util.resources.LocalSystemSettings
4950
import com.getcode.util.resources.ResourceHelper
@@ -163,11 +164,13 @@ class MainActivity : FragmentActivity() {
163164
LocalAppUpdater provides appUpdater,
164165
LocalUiTesting provides intent.getBooleanExtra(UI_TEST, false),
165166
) {
166-
Rinku {
167-
App(
168-
tipsEngine = tipsEngine,
169-
solanaRpcConfig = solanaRpcConfig,
170-
)
167+
ProvidePermissionChecker(permissionChecker) {
168+
Rinku {
169+
App(
170+
tipsEngine = tipsEngine,
171+
solanaRpcConfig = solanaRpcConfig,
172+
)
173+
}
171174
}
172175
}
173176
}

apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,6 @@ internal fun Scanner() {
5454
mutableStateOf<Boolean?>(null)
5555
}
5656

57-
var cameraStarted by remember {
58-
mutableStateOf(state.autoStartCamera == true)
59-
}
60-
6157
var cameraAvailable by remember {
6258
mutableStateOf(true)
6359
}
@@ -83,9 +79,6 @@ internal fun Scanner() {
8379
@SuppressLint("LocalContextGetResourceValueCall")
8480
BillContainer(
8581
isPaused = isPaused,
86-
isCameraReady = previewing == true,
87-
isCameraStarted = cameraStarted,
88-
onStartCamera = { cameraStarted = true },
8982
onAction = {
9083
when (it) {
9184
ScannerDecorItem.Give -> {
@@ -165,9 +158,6 @@ internal fun Scanner() {
165158

166159
Lifecycle.Event.ON_STOP -> {
167160
Timber.d("onStop")
168-
if (state.autoStartCamera == false) {
169-
cameraStarted = false
170-
}
171161
}
172162

173163
Lifecycle.Event.ON_PAUSE -> {

apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/bills/BillContainerView.kt

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ import com.flipcash.app.session.Grabbed
4646
import com.flipcash.app.session.LocalSessionController
4747
import com.flipcash.app.session.PutInWallet
4848
import com.flipcash.app.updates.LocalAppUpdater
49+
import com.getcode.ui.components.OnLifecycleEvent
50+
import androidx.lifecycle.Lifecycle
4951
import com.flipcash.features.scanner.R
5052
import com.getcode.manager.BottomBarAction
5153
import com.getcode.manager.BottomBarManager
@@ -63,17 +65,13 @@ import kotlinx.coroutines.delay
6365
@Composable
6466
internal fun BillContainer(
6567
modifier: Modifier = Modifier,
66-
isCameraReady: Boolean,
67-
isCameraStarted: Boolean,
6868
isPaused: Boolean,
6969
scannerView: @Composable () -> Unit,
70-
onStartCamera: () -> Unit,
7170
onAction: (ScannerDecorItem) -> Unit
7271
) {
7372
val session = LocalSessionController.current!!
7473
val context = LocalContext.current
7574
val onPermissionResult = { result: PermissionResult ->
76-
session.onCameraPermissionResult(result)
7775
if (result == PermissionResult.PermanentlyDenied) {
7876
BottomBarManager.showError(
7977
title = context.getString(R.string.action_allowCameraAccess),
@@ -100,6 +98,15 @@ internal fun BillContainer(
10098
val state by session.state.collectAsState()
10199
val billState by session.billState.collectAsState()
102100

101+
val autoStart = state.autoStartCamera == true
102+
var cameraStarted by remember { mutableStateOf(autoStart) }
103+
104+
OnLifecycleEvent { _, event ->
105+
if (event == Lifecycle.Event.ON_STOP && !autoStart) {
106+
cameraStarted = false
107+
}
108+
}
109+
103110
Box(
104111
modifier = Modifier
105112
.fillMaxSize()
@@ -117,25 +124,36 @@ internal fun BillContainer(
117124
// waiting for update
118125
}
119126

120-
state.isCameraPermissionGranted == true || state.isCameraPermissionGranted == null -> {
121-
if (state.autoStartCamera == null) {
122-
// waiting for result
123-
} else if (!state.autoStartCamera!! && !isCameraStarted) {
124-
CameraDisabledView(modifier = Modifier.fillMaxSize()) {
125-
onStartCamera()
127+
else ->{
128+
when (cameraPermission.status) {
129+
PermissionResult.Denied -> {
130+
CameraDisabledView(modifier = Modifier.fillMaxSize()) {
131+
cameraPermission.launch()
132+
}
133+
}
134+
PermissionResult.NotRequested -> {
135+
CameraPermissionsMissingView(
136+
modifier = Modifier.fillMaxSize(),
137+
backgroundColor = Color.Black,
138+
onClick = { cameraPermission.launch() }
139+
)
140+
}
141+
PermissionResult.Granted -> {
142+
if (!cameraStarted) {
143+
CameraDisabledView(modifier = Modifier.fillMaxSize()) {
144+
cameraStarted = true
145+
}
146+
} else {
147+
scannerView()
148+
}
149+
}
150+
PermissionResult.PermanentlyDenied -> {
151+
CameraDisabledView(modifier = Modifier.fillMaxSize()) {
152+
context.launchAppSettings()
153+
}
126154
}
127-
} else {
128-
scannerView()
129155
}
130156
}
131-
132-
else -> {
133-
CameraPermissionsMissingView(
134-
modifier = Modifier.fillMaxSize(),
135-
backgroundColor = Color.Black,
136-
onClick = { cameraPermission.launch() }
137-
)
138-
}
139157
}
140158

141159
val updatedState by rememberUpdatedState(state)

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/SessionController.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@ interface SessionController {
2626
fun onAppInForeground()
2727
fun onAppInBackground()
2828
fun onCameraScanning(scanning: Boolean)
29-
fun onCameraPermissionResult(result: PermissionResult)
3029
fun showBill(bill: Bill)
3130
fun dismissBill(action: BillDeterminationResult)
3231
fun onCodeScan(code: ScannableKikCode)
3332
fun openCashLink(cashLink: String?)
3433
}
3534

3635
data class SessionState(
37-
val isCameraPermissionGranted: Boolean? = null,
3836
val vibrateOnScan: Boolean = false,
3937
val giveableBalance: Fiat? = null,
4038
val logScanTimes: Boolean = false,

apps/flipcash/shared/session/src/main/kotlin/com/flipcash/app/session/internal/RealSessionController.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,6 @@ class RealSessionController @Inject constructor(
289289
_state.update { it.copy(isCameraUp = scanning) }
290290
}
291291

292-
override fun onCameraPermissionResult(result: PermissionResult) {
293-
_state.update { it.copy(isCameraPermissionGranted = result == PermissionResult.Granted) }
294-
}
295-
296292
override fun showBill(bill: Bill) {
297293
if (bill.amount.nativeAmount.decimalValue == 0.0) return
298294
val owner = userManager.accountCluster ?: return

libs/permissions/public/src/main/kotlin/com/getcode/util/permissions/PermissionChecker.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,36 @@ private class FakePermissionChecker(
5555
internal val LocalPermissionChecker: ProvidableCompositionLocal<PermissionChecker> =
5656
staticCompositionLocalOf { DefaultPermissionChecker }
5757

58+
/**
59+
* Provides the given [PermissionChecker] to the composition tree.
60+
*
61+
* Call this from your activity's `setContent` block to connect the Hilt-injected
62+
* [AndroidPermissionChecker] to [rememberPermission].
63+
*
64+
* Without this, the default checker (which treats all permissions as denied) is used,
65+
* and permission state falls back entirely to SharedPreferences — which can become
66+
* stale if the user revokes a permission via system Settings.
67+
*
68+
* Usage:
69+
* ```
70+
* setContent {
71+
* ProvidePermissionChecker(permissionChecker) {
72+
* App()
73+
* }
74+
* }
75+
* ```
76+
*/
77+
@Composable
78+
fun ProvidePermissionChecker(
79+
checker: PermissionChecker,
80+
content: @Composable () -> Unit,
81+
) {
82+
CompositionLocalProvider(
83+
LocalPermissionChecker provides checker,
84+
content = content,
85+
)
86+
}
87+
5888
/**
5989
* Provides a [FakePermissionChecker] for use in tests and Compose previews.
6090
*

libs/permissions/public/src/main/kotlin/com/getcode/util/permissions/Permissions.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,34 @@ private const val PREFS_NAME = "permissions"
2525
* Returns [PermissionResult.NotRequested] if no result has been persisted,
2626
* meaning the permission has never been requested in any session.
2727
*/
28-
private fun SharedPreferences.restoreResult(permission: String): PermissionResult =
28+
/**
29+
* Restores the last known denial state for [permission] from SharedPreferences.
30+
*
31+
* [PermissionResult.Granted] is never persisted — the OS is the sole source of truth
32+
* for granted state. This avoids stale "Granted" entries when the user selects
33+
* "Only this time" or revokes the permission via system Settings.
34+
*/
35+
private fun SharedPreferences.restoreDenialState(permission: String): PermissionResult =
2936
when (getString(permission, null)) {
3037
"Denied" -> PermissionResult.Denied
3138
"PermanentlyDenied" -> PermissionResult.PermanentlyDenied
32-
"Granted" -> PermissionResult.Granted
3339
else -> PermissionResult.NotRequested
3440
}
3541

3642
/**
37-
* Persists [result] for [permission] to SharedPreferences.
38-
* [PermissionResult.NotRequested] is a no-op — absence of a key represents this state.
43+
* Persists denial state for [permission] to SharedPreferences.
44+
*
45+
* Only [PermissionResult.Denied] and [PermissionResult.PermanentlyDenied] are stored.
46+
* [PermissionResult.Granted] clears any prior denial record.
47+
* [PermissionResult.NotRequested] is a no-op.
3948
*/
4049
private fun SharedPreferences.Editor.persistResult(permission: String, result: PermissionResult) {
41-
val key = when (result) {
42-
PermissionResult.Denied -> "Denied"
43-
PermissionResult.PermanentlyDenied -> "PermanentlyDenied"
44-
PermissionResult.Granted -> "Granted"
50+
when (result) {
51+
PermissionResult.Denied -> putString(permission, "Denied")
52+
PermissionResult.PermanentlyDenied -> putString(permission, "PermanentlyDenied")
53+
PermissionResult.Granted -> remove(permission)
4554
PermissionResult.NotRequested -> return
4655
}
47-
putString(permission, key)
4856
}
4957

5058
/**
@@ -88,7 +96,7 @@ fun rememberPermission(
8896
when {
8997
checker.isGranted(config.permission) -> PermissionResult.Granted
9098
!config.requiresRuntimeRequest -> PermissionResult.Granted
91-
else -> prefs.restoreResult(config.permission)
99+
else -> prefs.restoreDenialState(config.permission)
92100
}
93101
}
94102

@@ -110,7 +118,7 @@ fun rememberPermission(
110118
// Returning from system Settings — re-evaluate from source of truth
111119
handle.status = when {
112120
checker.isGranted(config.permission) -> PermissionResult.Granted
113-
else -> prefs.restoreResult(config.permission)
121+
else -> prefs.restoreDenialState(config.permission)
114122
}
115123
}
116124
}

0 commit comments

Comments
 (0)