Android: the boot notice now clears, and kiosk survives a reboot

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
ScreenTinker 2026-08-05 13:30:48 -05:00
parent 803f4ec26d
commit b419830629
2 changed files with 77 additions and 9 deletions

View file

@ -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() {

View file

@ -25,6 +25,8 @@
<string name="hw_enroll_constraints">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.</string>
<string name="hw_recheck">Re-check</string>
<string name="settings_info_device">Device</string>
<string name="settings_exit_kiosk">Exit kiosk mode</string>
<string name="settings_exit_kiosk_done">Kiosk mode off. It stays off until re-enabled from the dashboard.</string>
<string name="settings_exit">Exit app</string>
<string name="settings_save">Save</string>
<string name="settings_exit_title">Exit ScreenTinker?</string>