From b419830629b650006a658d72f23022aa4c80db43 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Wed, 5 Aug 2026 13:30:48 -0500 Subject: [PATCH] Android: the boot notice now clears, and kiosk survives a reboot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two field reports from a customer running the player on Android x86. THE "STARTING DISPLAY…" BANNER NEVER CLEARED. Relauncher launches the activity directly when the overlay permission is granted — the normal kiosk setup — and THEN posts the notification, deliberately, so a device that could not auto-launch still has a tappable way back. On a device where the launch DID work, that ordering posts the prompt after onCreate has already cancelled it, and nothing cancels it again: a permanent banner over content that is already playing. They sent a photo of exactly that. Cancelling in onCreate only ever closed half the race. It now also clears on every foreground: if the player is on screen, a "Starting display…" prompt is stale by definition, whoever posted it and whenever. KIOSK MODE DID NOT SURVIVE A REBOOT. startLockTask() is a runtime call on the Activity, and nothing persisted the operator's intent — so a locked panel came back up unlocked, silently, and the only symptom is that someone can suddenly leave the app. The flag is now written BEFORE the lock is attempted, so a device that reboots mid-call still comes back in the state that was asked for, and a lock that fails is retried on the next start rather than forgotten. Restored in onStart rather than onCreate because lock-task can be dropped on some transitions. Also theirs: an "Exit kiosk mode" entry in the PIN menu, shown ONLY when locked. With kiosk on and no other input, that menu is the only way out of a panel, and a menu entry that does nothing is worse than no entry. Builds clean: versionCode 100, v1 JAR signature intact. Reported by chris@chris-pc. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL --- .../com/remotedisplay/player/MainActivity.kt | 84 +++++++++++++++++-- android/app/src/main/res/values/strings.xml | 2 + 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt index cb2448c..b4cf7b6 100644 --- a/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt +++ b/android/app/src/main/java/com/remotedisplay/player/MainActivity.kt @@ -784,14 +784,8 @@ class MainActivity : AppCompatActivity() { "screen_on" -> Log.w("MainActivity", "screen_on: no privileged wake path on a non-rooted panel — no-op") // #161 Tier-2 (all no-op off-owner via STPolicy): kiosk lock-task, time/tz, status bar, // uninstall block. Device owner enters lock-task silently; others get screen-pinning. - "kiosk_lock" -> { - stPolicy().setLockTaskAllowed(true) - try { startLockTask() } catch (e: Throwable) { Log.w("MainActivity", "startLockTask: ${e.message}") } - } - "kiosk_unlock" -> { - try { stopLockTask() } catch (e: Throwable) { Log.w("MainActivity", "stopLockTask: ${e.message}") } - stPolicy().setLockTaskAllowed(false) - } + "kiosk_lock" -> setKioskMode(true) + "kiosk_unlock" -> setKioskMode(false) "set_time" -> { val ms = payload?.optLong("millis", 0L) ?: 0L; if (ms > 0) stPolicy().setTime(ms) } "set_timezone" -> { val tz = payload?.optString("timezone", "") ?: ""; if (tz.isNotEmpty()) stPolicy().setTimeZone(tz) } "status_bar" -> stPolicy().setStatusBarDisabled(payload?.optBoolean("disabled", true) ?: true) @@ -1138,10 +1132,20 @@ class MainActivity : AppCompatActivity() { getString(R.string.settings_exit) ) + // Requested from the field: with kiosk on, the PIN menu is the ONLY way back out on a + // panel with no other input. Shown only when locked — an entry that does nothing is worse + // than no entry, and this menu is already long. + val kiosk = kioskModeEnabled() + val menu = if (kiosk) items + getString(R.string.settings_exit_kiosk) else items + AlertDialog.Builder(this) .setTitle(getString(R.string.settings_title)) - .setItems(items) { _, which -> + .setItems(menu) { _, which -> when (which) { + items.size -> { // the appended exit-kiosk entry; only present when locked + setKioskMode(false) + Toast.makeText(this, getString(R.string.settings_exit_kiosk_done), Toast.LENGTH_LONG).show() + } 0 -> showChangeServerDialog(serverUrl) 1 -> { config.clearDeviceCredentials() @@ -1157,6 +1161,48 @@ class MainActivity : AppCompatActivity() { .show() } + /* + * Kiosk lock, remembered. + * + * startLockTask() is a runtime call on the Activity — it does not survive a reboot. A panel + * locked from the dashboard came back up unlocked, with nothing to say so, and the only way + * to notice was that someone could suddenly leave the app. Reported from the field. + * + * The flag is written BEFORE the lock is attempted so a device that reboots mid-call still + * comes back in the state the operator asked for; a lock that then fails is retried on the + * next start rather than being forgotten. + */ + private fun setKioskMode(enabled: Boolean) { + try { + getSharedPreferences("screentinker", Context.MODE_PRIVATE) + .edit().putBoolean("kiosk_enabled", enabled).apply() + } catch (e: Throwable) { Log.w("MainActivity", "kiosk pref: ${e.message}") } + + if (enabled) { + stPolicy().setLockTaskAllowed(true) + try { startLockTask() } catch (e: Throwable) { Log.w("MainActivity", "startLockTask: ${e.message}") } + } else { + try { stopLockTask() } catch (e: Throwable) { Log.w("MainActivity", "stopLockTask: ${e.message}") } + stPolicy().setLockTaskAllowed(false) + } + } + + private fun kioskModeEnabled(): Boolean = try { + getSharedPreferences("screentinker", Context.MODE_PRIVATE).getBoolean("kiosk_enabled", false) + } catch (e: Throwable) { false } + + /** Re-enter lock task after a restart, if that is the state the operator left it in. */ + private fun restoreKioskMode() { + if (!kioskModeEnabled()) return + stPolicy().setLockTaskAllowed(true) + try { + startLockTask() + Log.i("MainActivity", "Kiosk mode restored after restart") + } catch (e: Throwable) { + Log.w("MainActivity", "Kiosk restore failed: ${e.message}") + } + } + // #161: device-policy wrapper (degrades safely off-tier — every Tier-2 call no-ops when not owner). private fun stPolicy() = com.remotedisplay.player.admin.STPolicy(this) // #160 Track-A: no-device-owner system control (media volume, brightness, screen-off timeout). @@ -1337,6 +1383,26 @@ class MainActivity : AppCompatActivity() { override fun onStart() { super.onStart() if (::mediaPlayer.isInitialized) mediaPlayer.onAppForegrounded() + + // Clear the boot "Starting display…" prompt EVERY time the player becomes visible, not + // just in onCreate. + // + // Relauncher launches the activity directly when the overlay permission is granted — the + // normal kiosk setup — and THEN posts the notification, deliberately, so a device that + // could not auto-launch still has a tappable way back. On a device where the launch DID + // work, that ordering means the prompt is posted after onCreate already cancelled it, and + // nothing clears it again: a permanent "Starting display…" banner over content that is + // already playing. Reported from the field with a photo of exactly that. + // + // If the player is on screen, the prompt is stale by definition — so clearing it here is + // correct regardless of who posted it or when. + (getSystemService(Context.NOTIFICATION_SERVICE) as? android.app.NotificationManager)?.cancel(999) + + // Re-enter kiosk if that is how the operator left it. Done in onStart rather than onCreate + // because lock-task can be dropped by the system on some transitions, and a panel that + // quietly stopped being locked is the failure people notice only when someone walks out of + // the app. + restoreKioskMode() } override fun onDestroy() { diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 783206c..3733afc 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ Or scan the provisioning QR from the dashboard after a factory reset (tap the setup-wizard Welcome screen 6×). Device owner is optional — the app works fully without it. Re-check Device + Exit kiosk mode + Kiosk mode off. It stays off until re-enabled from the dashboard. Exit app Save Exit ScreenTinker?