feat(android-terminal): Phase D.5 — auto-lock idle timer
Some checks failed
Some checks failed
Wraps the terminal screen in an IdleTracker that observes pointer-down events and fires onLockScreen after 2 min of silence (per spec). Each tap restarts the timer via a LaunchedEffect re-launch. awaitFirstDown(requireUnconsumed = false) lets us observe touches without intercepting them, so children (text fields, buttons, lists, keypad) keep working normally. Body extracted into a private TerminalContent so the wrapper stays tidy. Caveat: Compose AlertDialogs render in a separate window, so the timer keeps ticking through an open dialog. This is correct behavior — a cashier who walks away mid-dialog should be locked out — and the dialog disposes when the NavHost pops the terminal route on lock. Documented in IdleTracker.kt. Verified by ./gradlew assembleDebug — clean build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package lu.rewardflow.terminal.ui.terminal
|
||||
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Wraps a screen and fires [onIdle] after [timeoutMillis] of no pointer
|
||||
* activity. Each touch-down restarts the timer. Disposing the wrapper
|
||||
* cancels the pending timer.
|
||||
*
|
||||
* Used by the terminal screen to auto-lock back to PIN after 2 minutes
|
||||
* of inactivity, per the implementation plan. Camera flows (QR scanner
|
||||
* overlay) don't generate pointer events, but those flows are short-
|
||||
* lived and the user is clearly using the device anyway — we accept
|
||||
* the trade-off rather than wire a separate "in-active-flow" guard.
|
||||
*/
|
||||
@Composable
|
||||
fun IdleTracker(
|
||||
timeoutMillis: Long,
|
||||
onIdle: () -> Unit,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
var lastActivity by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
|
||||
LaunchedEffect(lastActivity) {
|
||||
delay(timeoutMillis)
|
||||
onIdle()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
// requireUnconsumed=false → observe even when a child
|
||||
// consumes the gesture (otherwise we'd miss every tap
|
||||
// on a button / text field, which is most of them).
|
||||
awaitFirstDown(requireUnconsumed = false)
|
||||
lastActivity = System.currentTimeMillis()
|
||||
}
|
||||
},
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,28 @@ fun TerminalScreen(
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
IdleTracker(
|
||||
timeoutMillis = AUTO_LOCK_TIMEOUT_MS,
|
||||
onIdle = onLockScreen,
|
||||
) {
|
||||
TerminalContent(
|
||||
state = state,
|
||||
staffName = staffName,
|
||||
onLockScreen = onLockScreen,
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val AUTO_LOCK_TIMEOUT_MS = 2 * 60 * 1000L // 2 minutes per spec
|
||||
|
||||
@Composable
|
||||
private fun TerminalContent(
|
||||
state: TerminalUiState,
|
||||
staffName: String,
|
||||
onLockScreen: () -> Unit,
|
||||
viewModel: TerminalViewModel,
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
TopBar(
|
||||
staffName = staffName,
|
||||
|
||||
Reference in New Issue
Block a user