- π§ͺ Note on the cloud integration: the groundwork for FreeKiosk Cloud (enrollment, heartbeat, remote commands, OTA updates, QR provisioning) is compiled into this build but switched off behind a build flag (
CLOUD_ENABLED), so its settings section is hidden and none of its code runs. It is neither released nor supported yet, which is why it is not listed below. It will be documented in a later release.
Added
- ποΈ Proximity wake (hardware sensor): New Display β Screensaver β "Enable Proximity Wake" toggle (
ProximityDetectionModule) that wakes the screen when a hand or body comes within a few centimeters of the front proximity sensor. It behaves like camera motion (cancels the pre-check window or dismisses an active screensaver) but is a short-range binary hardware signal, so it never false-triggers on lighting/scene changes and uses almost no battery. The listener runs only during the motion pre-check and while the screensaver is showing, and only on devices that actually have the sensor (auto-detected; the toggle is disabled with an explanatory note on tablets without one). Ignored during scheduled sleep.
Changed
- π¨ App design refreshed to match the FreeKiosk Cloud dashboard: The light theme was re-aligned with the cloud's brand: primary blue
#2b7fff, navy text (#1b2a4d), soft blue-tinted surfaces, aligned status colors, larger corner radii (cards 16, fields/buttons 12) and softer premium card/button shadows with hairline borders. Throughout the UI, legacy emoji used as icons were replaced with proper MaterialCommunityIcons vector icons: settings switches, info-box titles, tab bar, the Wi-Fi/Bluetooth/Audio dialogs, lock-screen quick controls, Android-settings shortcuts, the first-run welcome and error/return overlays, the media-player empty state, and the schedule/event and backup-restore editors. Decorative emoji were stripped from labels, statuses and native alert titles. Native Picker dropdowns and injected HTML media-control glyphs are unchanged.
Fixed
-
π‘ Screen no longer reliably turned on by
screen_on/ screensaver / scheduler wake on Android 12+: The old wake path (a deprecatedACQUIRE_CAUSES_WAKEUPwake lock plussetTurnScreenOnon an already-created activity) stopped actually lighting the display afterlockNow()on Android 12+ and several OEM ROMs, so remotescreen_on, screensaver dismissal and the sleep scheduler could leave the screen dark.ScreenController.turnScreenOn()now also raises a high-importance full-screen-intent notification (the alarm / incoming-call pattern) that makes the system wake the screen and bringMainActivityto the front, with a directstartActivityas belt-and-suspenders. Added theUSE_FULL_SCREEN_INTENTpermission (on Android 14 without the special access it is demoted to a heads-up notification, which still wakes the screen). Note: touch still cannot wake a truly-off screen, this only drives the app-initiated wake paths. -
π Screen stayed dark after a dim screensaver was dismissed: The dim screensaver forces the brightness down to
screensaverBrightness(often 0); on deactivation FreeKiosk went straight back to auto-brightness without first restoring a visible level, so the display could stay black until the next brightness change. It now restores the default brightness before handing back to auto-brightness. The same explicit restore was added to the proximity/motion wake paths. -
π "Back Button Behavior" was ignored in External App mode (always relaunched instantly) (#220): In external-app mode, Test Mode and Delayed Return had no effect: pressing the system Back button always relaunched the external app immediately, so the user could never exit to settings and no countdown ever appeared. Root cause: the
back_button_modesetting was only consulted inMainActivity.onBackPressed(), which runs only when FreeKiosk has focus, but in external-app mode the external app owns the Back button. When the user backed out of the app, the involuntary-return fast-path inonResume()(added for #106/#203) relaunched the app unconditionally, overriding the setting. Fixed by gating that native fast-path onback_button_mode == "immediate": only Immediate Return hard-relaunches from native, while Test and Delayed Return (timer) now keep FreeKiosk in the foreground and let the existing JSAppStatehandler apply the correct behavior (stay on FreeKiosk / show the countdown, which works because JS timers are not frozen while FreeKiosk stays foregrounded). β οΈ Note for existing fleets: the setting now actually takes effect, and its default is Test Mode (Back stays on FreeKiosk). If you relied on the old always-relaunch behavior, set Security β Back Button Behavior β Immediate Return. Reported by @RSuarezAl. -
π Device permanently stuck on "Starting kioskβ¦" after reboot with a secure lock screen (#222): On a Device Owner device that also has a native secure lock (PIN/pattern/password), FreeKiosk hung forever on the loading screen after a reboot if
BootLockActivitystarted before the user unlocked. ItsSHOW_WHEN_LOCKED/DISMISS_KEYGUARDflags occlude (do not dismiss) a secure keyguard, so the user cannot enter their credential, credential-encrypted (CE) storage never unlocks, andMainActivity(notdirectBootAware) can never launch. The opt-in "System screen-lock compatibility" (#199) avoids it by deferring at boot, but it is off by default, so a fresh secure device deadlocked. A reactive safety net was added first: after ~8s of failed hand-off, with a secure keyguard confirmed (KeyguardManager.isDeviceSecure()) and the user still locked (UserManager.isUserUnlocked()),BootLockActivitysteps aside so the keyguard is revealed; the user unlocks and the kiosk starts atBOOT_COMPLETED. Runtime testing showed that net never fired, and fixing it took three more defects, all in this release: (1)finish()alone did nothing, because lock task disables the keyguard unlessLOCK_TASK_FEATURE_KEYGUARDis set (the platform behaviour #208 documents), so the activity went away and still no lock screen appeared: the net now callsstopLockTask()first, which is safe precisely here since the branch requires a secure lock to be set, so what the user gets is the system asking for their PIN; (2) the poll loop driving all of this stopped after about one second, because its hand-off check inferred "MainActivity took over" fromBootLockActivitylosing window focus, which is exactly what a secure keyguard taking focus at boot produces, so it declared the hand-off done, called afinish()that lock task absorbed, and stopped polling with nothing left watching.MainActivitynow publishes a flag when it really starts, and the loop keeps polling after everyfinish()since an absorbed one is no longer hypothetical; (3) the retry that was supposed to relaunchMainActivityevery 5s disarmed itself, becausestartActivity()returns a refusal code rather than throwing when the system blocks the launch atLOCKED_BOOT_COMPLETED, so the "launched" flag was set on a launch that never happened. A last-resort watchdog on its own thread now backs the whole sequence, and the diagnostic logging it carries is emitted in release builds past 5s of stall (normal boots stay silent), because the previous instrumentation usedDebugLog.d, which is stripped from release: a device stuck in the field left no trace at all. Reported by @potofcoffee. -
π REST API silently dropped non-ASCII text:
/api/ttsspoke English but stayed silent on Chinese, Korean, Japanese, Arabicβ¦ (#115): Any text sent in a POST body (TTS, toast, keyboard text, URLβ¦) lost all of its multibyte UTF-8 characters, so/api/ttswithδ½ ε₯½/μλ νμΈμproduced no speech while English worked and mixed text spoke only the Latin part. It was not a TTS/locale problem (language detection was already correct): the embedded HTTP server (NanoHTTPD 2.3.1) decodes the request body using the charset from theContent-Typeheader and defaults to US-ASCII when the client sends none, which corrupts every non-ASCII character before it ever reaches the TTS engine. Earlier fixes (thelanguageparameter, script auto-detection) addressed the wrong layer, so the bug persisted. Fixed inparseBody()by forcing UTF-8 when the request did not declare a charset (JSON is UTF-8 by spec) before the body is read: it only adds a charset, never changes the media type, so form-urlencoded / multipart handling is untouched and pure-ASCII bodies are byte-identical. One fix covers every POST endpoint. (Workaround on older builds: sendContent-Type: application/json; charset=utf-8.) Reported by @nowpast and @alogblog. -
π
POST /api/modereturnedsuccessbut the switch did not take effect until you touched the screen (#209 follow-up): After the runtime display-mode switch was wired in beta.5, switching out of an external app (to WebView or media player) via REST/MQTT often did nothing until the user pressed Home/Back, sometimes relaunched the external app on its own a moment later, and could leave later mode changes silently ignored. Root cause: the switch is performed in the JSonSetModehandler, but while FreeKiosk is backgrounded behind the external app the JS thread is frozen, so the command was queued and never ran (thebringToFront()call lives inside that frozen handler). Fixed by bringing FreeKiosk to the foreground from native code the moment asetModetowebview/media_playerarrives, which resumes the JS thread so the switch completes on its own.blockAutoRelaunchis set first soonResumedoes not take the involuntary-return fast-path and relaunch the app being left (the "app relaunches after switching to webview" report). Switching to external_app is unchanged. A related report was also fixed: the hidden "return to settings" overlay button reappeared in WebView after such a switch, because the WebView return-button visibility defaulted to visible inloadSettings()while the Settings toggle and storage default to hidden; the defaults are now aligned to hidden. Reported by @alogblog and @RSuarezAl. -
πΈ Screenshots failed in multi-app mode (REST
/api/screenshotand the cloudscreenshotcommand) (#229): With Device Owner + multi-app mode, capturing the screen returned "Screenshot not available" as soon as a managed app was in the foreground. Root cause: the single capture path did aPixelCopyon FreeKiosk's own Activity window, but managed apps are launched in their own task (FLAG_ACTIVITY_NEW_TASK), so our Activity is stopped and itsViewRootImplsurface released, makingPixelCopythrow "Window doesn't have a backing surface!" (and, had it succeeded, it would have captured an empty FreeKiosk window rather than the app on screen). Added a second capture path through the accessibility service (AccessibilityService.takeScreenshot(), Android 11+, newcanTakeScreenshotcapability), which captures the real display whatever app is in front. FreeKiosk now uses PixelCopy while it is in the foreground (cheap, not rate-limited) and the accessibility path otherwise, each falling back to the other. Note that the Device Owner screen-capture policy applied in Lock Mode (#172, blocks Power+Volume Down) also blacks out that system capture: a new Security β "Allow Remote Screenshots" toggle (Device Owner, default off) lets FreeKiosk lift the policy for the fraction of a second the capture takes and restore it immediately after. With the toggle off, the behaviour is unchanged and the API now answers with the actual reason instead of a bare "not available". Reported by @23575437. -
π Home Assistant showed stale state: the Screen Power toggle snapped back to ON a few seconds after switching the screen off (#155): The screen did turn off, but the MQTT state topic is retained and was only refreshed by the periodic publish, so Home Assistant kept serving the previous value for up to 30 seconds and its toggle reverted in the meantime. Two fixes: the tablet now publishes its status from native code on the real
ACTION_SCREEN_ON/ACTION_SCREEN_OFFbroadcast (ScreenStateReceiver), which is the authoritative moment and works whatever changed the screen state (MQTT command, power button, scheduler, screensaver) and while the JS thread is suspended afterlockNow(); and every MQTT command now schedules a status republish ~600 ms after execution, debounced, so brightness, volume and the other entities stop lagging by up to 30 seconds too. β οΈ Unchanged: without Device Owner or the accessibility service, Android does not let an app power the display off, FreeKiosk only dims it to 0, and the state then correctly reports the screen as on. Reported by @RubinBonBon8833, with the beta.5 repro from @LamerTex. -
π MQTT screensaver switch could turn the screensaver off but never on (#232): Sending
ONonly re-enabled the screensaver setting, so the screen went dark after the configured inactivity timeout instead of immediately, whileOFFcorrectly dismissed a running screensaver. The Home Assistant switch mirrorsscreen.screensaverActive(is the screensaver showing right now), soONnow activates it on the spot. The command is also honoured when "Keep Screen On" is off: it was silently dropped in that case, which left the toggle flipping back with nothing in the logs. An explicit remote command is not the automatic inactivity path that the system's own sleep timer supersedes. Reported by @LamerTex. -
π Device went "unavailable" in Home Assistant after a couple of hours on battery (#234): The MQTT client holds a CPU wake lock and a WiFi lock while connected, but once the tablet is unplugged and idle Android's Doze still defers its network, the broker misses the keepalive and publishes the LWT. The battery-optimization exemption is the only reliable fix and nothing outside the cloud service and the permission wizard ever asked for it, so a Home-Assistant-only user never got it. The MQTT settings section now checks the exemption (new native
isIgnoringBatteryOptimizations), warns when it is missing, and offers a one-tap request; turning MQTT on asks for it too. Note that the system dialog cannot open while Lock Mode holds the device in lock task, so grant it before enabling Lock Mode or over ADB:adb shell dumpsys deviceidle whitelist +com.freekiosk. A tablet on permanent power never enters Doze. Reported by @LamerTex. -
π‘οΈ MQTT without Lock Mode had no foreground service, so Android killed the app and the device stayed "unavailable" (#234 follow-up): The battery-optimization warning added above treats Doze, but Doze was not the whole story.
KioskWatchdogServiceonly started when Lock Mode was enabled, andOverlayServiceonly in External App mode, so a Home Assistant user running FreeKiosk as a plain WebView dashboard had no foreground service at all: once the screen went off the process was an ordinary background app, and aggressive OEM battery managers killed it, taking the broker connection with it for good. This also explains why the reporter's fallback (using the screensaver instead of a real screen-off, which keeps the device out of Doze entirely) still went unavailable. The watchdog now runs in one of two modes: the existing kiosk guard (unchanged, relaunches FreeKiosk after an OOM kill), or a new keep-alive mode started when MQTT is enabled without Lock Mode, whose only job is to hold the process up. Keep-alive mode never relaunches anything (FreeKiosk is a normal app there and the user is free to leave it) and it stops itself when MQTT is switched off. The mode is persisted, so aSTART_STICKYrestart cannot come back as a kiosk guard behind an admin who just exited. Users in this configuration will see a new silent, minimum-priority notification, which is the price of not being killed. β οΈ Note for the curious: the WiFi lock the MQTT client holds is not what keeps it alive.WIFI_MODE_FULL_HIGH_PERFis deprecated and the platform silently replaces it withWIFI_MODE_FULL_LOW_LATENCY, which per the SDK is only active while the screen is on and the app is in the foreground. Screen-off operation rests on the CPU wake lock, the process staying alive, and the Doze exemption. -
πΊ Kiosk did not come back after an Android TV woke from standby (#197):
KioskWatchdogServicedecided whether FreeKiosk was still running by walkingActivityManager.getAppTasks(), which stays true as long as the task exists in recents. It therefore could not tell "backgrounded behind the TV launcher" from "running", and never relaunched after a standby/wake cycle (it also answered "running" after an OOM kill whenever the task survived in recents, which is the very case it was written for in #96). It now checks process importance, and aSCREEN_ONreceiver inside the service runs the check on wake instead of waiting for the next 10 second tick. Contributed by @BoussonKarel, tested on a Xiaomi MiTV-MSSP3 (Android 9). Three guards were added on top, because a check that finally reports real foreground state reaches code paths that were previously unreachable: the watchdog no longer relaunches anything while the display is off (the process sits atIMPORTANCE_FOREGROUND_SERVICEduring standby, so it would otherwise have firedstartActivityevery 15 seconds for the whole night, fightingscreen_offand the sleep scheduler); the External App / multi-app guard now identifies the foreground app withUsageStatsManagerinstead ofgetAppTasks(), which only ever listed FreeKiosk's own tasks and so could never see an app launched in its own task (without this, FreeKiosk would have yanked itself in front of the running app every cycle, and multi-app mode was fully exposed since it has no single configured package); and when the foreground app cannot be determined at all, because usage access was not granted, the watchdog now does nothing rather than relaunch on a guess. -
πΈ
getUserMedia()failed with no prompt when the web page asked for the camera or the microphone (#219): The bundled WebView patch auto-grants media permission requests at the web layer, which is what lets a page usegetUserMedia()without a per-page prompt. Doing that, it also removed the upstream code path that asked Android for the matching runtime permission, and nothing else in FreeKiosk requestedCAMERA/RECORD_AUDIOoutside the permission wizard (which only runs during cloud enrollment). A Play Store install that never went through the wizard therefore had the web layer say yes and the Android layer say no, with no dialog and nothing in the logs. The patch now requests whatever is missing before granting, keeping the unconditional web-layer grant so the Fire OS behaviour it was written for is untouched. β οΈ Android suppresses runtime permission dialogs while a device is in lock task, so camera and microphone access must be granted before enabling Lock Mode, or over ADB:adb shell pm grant com.freekiosk android.permission.CAMERA. This is also why the reporter saw it work with Lock Mode off and fail with it on. Note that WebUSB is not supported by the Android WebView at all, for any kiosk app: it is a Chrome API absent from the system WebView component. Reported by @reframelab. -
π Hardened the REST/MQTT command wiring against a dead listener (#231):
ApiService.initialize()returned early when it had already been initialized, keeping the callbacks captured on the first mount and ignoring the ones passed by the caller, while the screen's cleanup path callsdestroy(), which removes the native command listener. Any remount ordering where the new screen initializes before the old one cleans up therefore left the app with no listener at all, or with commands driving a component tree that is no longer on screen: the command is accepted and acknowledged, and nothing visibly happens. It now always adopts the latest callbacks and re-subscribes when the listener is gone. Whether this is what produced the reported symptom (the WebView no longer following MQTT URL updates after cancelling the admin PIN prompt) is not confirmed, and the report itself is worth re-reading in light of the #155 fix above: the Home Assistantnavigate_urlentity updates optimistically as soon as you write to it, so seeing the new URL in Home Assistant never proved the tablet had applied it. Now that the device republishes its state right after each command, the entity snapping back to the previous URL makes that visible immediately. Reported by @jdgarzonv. -
π The battery optimization exemption could not be granted on a pinned kiosk (#234 follow-up): The exemption is the only cure for Doze cutting an MQTT device off the broker on battery, and no Android app can grant it to itself, so it goes through a system dialog. Lock task blocks any activity outside the whitelist, which meant the one-tap button in the MQTT settings did nothing at all on the very devices most likely to need it, leaving
adb shell dumpsys deviceidle whitelist +com.freekioskas the only route. FreeKiosk now whitelists the dialog's package for the few seconds it is on screen and restores the previous whitelist as soon as the app is back in the foreground (with a 60 second timeout, and a restore on failure). This is the same technique already used for the Wi-Fi and Bluetooth system dialogs. If the process dies while the dialog is up, the nextstartLockTask()rebuilds the whitelist from scratch, so the opening cannot outlive a restart. -
π± Status bar and navigation bar stayed visible in single-app mode until you opened the settings, exited or rebooted (#237):
MainActivity.onDestroy()restored the permissive lock-task feature set (HOME | OVERVIEW | NOTIFICATIONS | GLOBAL_ACTIONS) unconditionally. That is correct on a deliberate exit, butonDestroy()also fires when the system destroys the activity while an external app holds the foreground, which is the normal state in single-app mode: FreeKiosk sits in the background and is a prime candidate for being reclaimed. From that moment the device was running lock task with the status bar, the notification panel, Home and Overview all enabled, and nothing restored the restrictive set untilMainActivitywas recreated, hence the bars coming back only on settings / exit / reboot. The call is now gated on a deliberate exit, using the same flag the watchdog check right below already relies on to tell an intentional exit from a system kill. Nothing is lost on the deliberate path:exitKioskMode()callsdisableKioskRestrictions()itself before stopping lock task. Reported by @vukjure.