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()
|
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()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
TopBar(
|
TopBar(
|
||||||
staffName = staffName,
|
staffName = staffName,
|
||||||
|
|||||||
Reference in New Issue
Block a user