From c1d367bac2a09c0318eb059ccc39e95678547a01 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Fri, 8 May 2026 00:20:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(android-terminal):=20Phase=20F=20=E2=80=94?= =?UTF-8?q?=20kiosk,=20immersive,=20queued-action=20toast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes out the Android terminal plan (Phases A → F). - MainActivity: * Immersive mode via WindowInsetsControllerCompat — hides status + navigation bars, swipe-to-reveal with auto-hide (BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE). Re-applied in onResume so a transient overlay can't leave the bars visible. * Lock Task Mode — startLockTask() in onResume. The manifest already declares lockTaskMode="if_whitelisted", so this enters kiosk on a properly MDM-provisioned tablet and is a silent no-op (caught SecurityException) on dev / unprovisioned devices. - TerminalScreen: * SnackbarHost pinned at the bottom; LaunchedEffect(actionResult) shows "Queued — will sync when back online" whenever an offline action gets queued, so the cashier has explicit feedback beyond the top-bar pending pill. * 4 new locale strings (en/fr/de/lb) for the snackbar copy. - Manifest cleanup: dropped redundant tools:replace from the debug AndroidManifest overlay (networkSecurityConfig isn't set in the main manifest, so the replace directive was a no-op + emitted a warning). Skipped from the plan: a custom splash screen (the existing theme background renders for the cold-start frame; adding the splashscreen library is polish for a follow-up) and per-action success toasts (the action sheet closing + balance refresh + recent-feed update are adequate confirmation). Verified by ./gradlew assembleDebug — clean build. Co-Authored-By: Claude Opus 4.7 (1M context) --- clients/terminal-android/app/build.gradle.kts | 6 +++ .../app/src/debug/AndroidManifest.xml | 6 +-- .../lu/rewardflow/terminal/MainActivity.kt | 50 +++++++++++++++++++ .../terminal/ui/terminal/TerminalScreen.kt | 27 ++++++++++ .../app/src/main/res/values-de/strings.xml | 4 ++ .../app/src/main/res/values-fr/strings.xml | 4 ++ .../app/src/main/res/values-lb/strings.xml | 4 ++ .../app/src/main/res/values/strings.xml | 4 ++ 8 files changed, 101 insertions(+), 4 deletions(-) diff --git a/clients/terminal-android/app/build.gradle.kts b/clients/terminal-android/app/build.gradle.kts index 0a539454..dc897b9d 100644 --- a/clients/terminal-android/app/build.gradle.kts +++ b/clients/terminal-android/app/build.gradle.kts @@ -70,6 +70,12 @@ dependencies { debugImplementation(libs.compose.ui.tooling) // Networking (Retrofit + Moshi) + // Note: Moshi 1.15.x prints a one-line "Kapt support … deprecated" warning + // during hiltJavaCompileDebug because Hilt's javac compile auto-discovers + // its annotation processor. It's purely cosmetic — KSP runs the codegen + // and the warning has no effect on output. Cure (moshix) needed Kotlin + // 2.1.21+, which would cascade into bumping KSP / Compose BOM. Living + // with the warning until Moshi 2.0 ships KSP-native. implementation(libs.retrofit) implementation(libs.retrofit.moshi) implementation(libs.okhttp) diff --git a/clients/terminal-android/app/src/debug/AndroidManifest.xml b/clients/terminal-android/app/src/debug/AndroidManifest.xml index 4ca45d05..3090055b 100644 --- a/clients/terminal-android/app/src/debug/AndroidManifest.xml +++ b/clients/terminal-android/app/src/debug/AndroidManifest.xml @@ -1,10 +1,8 @@ - + + android:networkSecurityConfig="@xml/network_security_config" /> diff --git a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/MainActivity.kt b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/MainActivity.kt index 92c3e855..2b4e5afd 100644 --- a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/MainActivity.kt +++ b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/MainActivity.kt @@ -1,6 +1,9 @@ package lu.rewardflow.terminal +import android.app.ActivityManager +import android.content.Context import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -8,6 +11,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import dagger.hilt.android.AndroidEntryPoint import lu.rewardflow.terminal.ui.RewardFlowNavHost import lu.rewardflow.terminal.ui.theme.RewardFlowTheme @@ -18,6 +24,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + configureImmersiveMode() setContent { RewardFlowTheme { @@ -30,4 +37,47 @@ class MainActivity : ComponentActivity() { } } } + + override fun onResume() { + super.onResume() + // Re-apply immersive each time we come back from a transient overlay + // (action sheets, system dialogs) — they sometimes restore the bars. + configureImmersiveMode() + enterLockTaskIfPossible() + } + + /** Hide both the status bar and the navigation bar; reveal them on + * swipe and auto-hide again. Standard Android "immersive sticky" + * behavior, expressed via the Compat APIs. */ + private fun configureImmersiveMode() { + WindowCompat.setDecorFitsSystemWindows(window, false) + val controller = WindowInsetsControllerCompat(window, window.decorView) + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + /** + * Promote the activity to Lock Task Mode (kiosk). + * + * Manifest has ``android:lockTaskMode="if_whitelisted"``, so this + * call is only honored when a Device Owner / DPC app has whitelisted + * our package. In dev — and on stock retail tablets without MDM + * provisioning — the call is a silent no-op, which is the right + * behavior. On a properly provisioned tablet, this prevents the + * cashier from leaving the app via Home / Recents / notifications. + */ + private fun enterLockTaskIfPossible() { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + if (am.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_NONE) { + try { + startLockTask() + } catch (e: SecurityException) { + // Whitelist isn't in place — fine, run as a regular app. + Log.d("MainActivity", "Lock task not whitelisted: ${e.message}") + } catch (e: IllegalStateException) { + Log.d("MainActivity", "Cannot enter lock task: ${e.message}") + } + } + } } diff --git a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalScreen.kt b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalScreen.kt index 542b1f78..19188168 100644 --- a/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalScreen.kt +++ b/clients/terminal-android/app/src/main/java/lu/rewardflow/terminal/ui/terminal/TerminalScreen.kt @@ -24,11 +24,16 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -95,6 +100,15 @@ private fun TerminalContent( onLockScreen: () -> Unit, viewModel: TerminalViewModel, ) { + val snackbarHostState = remember { SnackbarHostState() } + val queuedMessage = stringResource(R.string.snackbar_queued_for_sync) + + LaunchedEffect(state.actionResult) { + if (state.actionResult is ActionResult.Queued) { + snackbarHostState.showSnackbar(message = queuedMessage) + } + } + Column(modifier = Modifier.fillMaxSize()) { TopBar( staffName = staffName, @@ -155,6 +169,19 @@ private fun TerminalContent( onCancel = viewModel::dismissScanner, ) } + + // Snackbar host pinned at the bottom — always present so it can fire + // over any of the dialogs/overlays above. + Box(modifier = Modifier.fillMaxSize()) { + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp), + ) { data -> + Snackbar(snackbarData = data) + } + } } @Composable diff --git a/clients/terminal-android/app/src/main/res/values-de/strings.xml b/clients/terminal-android/app/src/main/res/values-de/strings.xml index fce460d5..53f3b44e 100644 --- a/clients/terminal-android/app/src/main/res/values-de/strings.xml +++ b/clients/terminal-android/app/src/main/res/values-de/strings.xml @@ -72,4 +72,8 @@ Wird geladen… Ja Nein + + + In der Warteschlange — synchronisiert, sobald wieder online + Schließen diff --git a/clients/terminal-android/app/src/main/res/values-fr/strings.xml b/clients/terminal-android/app/src/main/res/values-fr/strings.xml index 34510efe..13b926bc 100644 --- a/clients/terminal-android/app/src/main/res/values-fr/strings.xml +++ b/clients/terminal-android/app/src/main/res/values-fr/strings.xml @@ -72,4 +72,8 @@ Chargement… Oui Non + + + En file d\'attente — synchronisation au retour de la connexion + Fermer diff --git a/clients/terminal-android/app/src/main/res/values-lb/strings.xml b/clients/terminal-android/app/src/main/res/values-lb/strings.xml index f2c16dc9..5797a808 100644 --- a/clients/terminal-android/app/src/main/res/values-lb/strings.xml +++ b/clients/terminal-android/app/src/main/res/values-lb/strings.xml @@ -72,4 +72,8 @@ Lueden… Jo Nee + + + An der Schlaang — synchroniséiert sou bal nees online + Zoumaachen diff --git a/clients/terminal-android/app/src/main/res/values/strings.xml b/clients/terminal-android/app/src/main/res/values/strings.xml index 38711156..1f5adc3b 100644 --- a/clients/terminal-android/app/src/main/res/values/strings.xml +++ b/clients/terminal-android/app/src/main/res/values/strings.xml @@ -72,4 +72,8 @@ Loading… Yes No + + + Queued — will sync when back online + Dismiss