github RushB-fr/freekiosk v2.0.0-beta.2
FreeKiosk v2.0.0-beta.2 - ADB Dashboard Provisioning & Boot Safety

pre-release3 hours ago

Added

  • πŸ—‚οΈ Dashboard mode can be provisioned over ADB. The ADB config intent worked off a fixed list of recognized keys, and the two the dashboard uses (@kiosk_dashboard_mode_enabled, @kiosk_dashboard_tiles) were not among them, so a fleet deployed by script could set a URL, a PIN and a lock package but had to have its tile grid entered by hand on every tablet. --ez dashboard_mode true and --es dashboard_tiles '[{"label":"Main","url":"https://..."}]' now work, as do the same two keys inside --es config. Tiles are normalized on the way in: id and order are generated when absent (a provisioning script should not have to invent stable ids or keep a counter), label falls back to the URL, an unknown iconMode falls back to favicon, and a tile with no url is dropped rather than written as an entry that navigates nowhere. Enabling the dashboard also sets display_mode to webview unless lock_package says otherwise, because KioskScreen only renders the grid under that mode and the setting would otherwise be written but never read. Not verified on a device: it compiles, and the path it writes through (SharedPreferences pending config, then multiSet into AsyncStorage) copies strings verbatim, so the serialized array arrives in the shape getDashboardTiles() parses. What needs checking on hardware is a full provisioning run ending on a working grid. Requested by e-mail by a user deploying tablets by script.

Fixed

  • 🀝 Device Owner QR provisioning failed on its very last step, leaving a tablet managed but never launched: on a factory-reset Galaxy Tab S10 FE (Android 16 / One UI 8.5) the APK downloaded, installed and became Device Owner, then the setup wizard reported "Couldn't set up device"; after a restart the tablet booted to the stock launcher with FreeKiosk installed but never launched, never enrolled and not pinned as Home. This was first misdiagnosed as Google's new approved-DPC allowlist blocking the install. It was not: firstInstallTime preceded any ADB action and dpm list-owners listed com.freekiosk, so the DPC install and the Device Owner assignment had both succeeded and the failure sat downstream of anything Play Protect polices. ACTION_ADMIN_POLICY_COMPLIANCE is the final provisioning step and the device is not provisioned until the DPC hands RESULT_OK back, but PolicyComplianceActivity.onCreate ran two operations before that hand-off and one of them was unguarded: reading the enrollment token out of the provisioning admin-extras bundle. Anything throwing there killed the activity before it could confirm, and a failed SharedPreferences write could therefore cost a tablet its entire provisioning.

    Four changes. setResult(RESULT_OK) now comes first and unconditionally, and the bookkeeping runs best-effort behind it: our own housekeeping must never be able to fail a device's enrolment. The admin extras are read without asserting their concrete type β€” the one-arg getParcelableExtra is deprecated and its inferred generic compiles to an unchecked cast, so a Bundle where a PersistableBundle was expected threw ClassCastException; both are now accepted. The extras-to-preferences logic, which existed twice with the same latent cast (once in the activity, once in DeviceAdminReceiver for the legacy PROFILE_PROVISIONING_COMPLETE path), is now one shared function. And pinHomeLauncher no longer swallows exceptions into an empty catch {}, which is precisely why a device landing on the stock launcher left nothing to diagnose; it logs its failures and says so when it is not Device Owner. Logging goes through android.util.Log rather than DebugLog, which is a no-op outside debug builds β€” this code path only ever runs on a release APK on a real tablet.

    ⚠️ The exact exception is still unconfirmed. A DropBox trace was requested from the reporter and had not arrived when this shipped; logcat buffers are in-memory and were lost to the reboot. The fix makes the hand-off independent of it either way, which is the point, but if the wizard was failing for a reason outside our activity then this is necessary and not sufficient. Not verified on hardware: it compiles, and what needs checking is a full factory-reset QR run reaching a launched, enrolled kiosk. Confidence is high on the wizard error being gone, lower on the Home pinning and the auto-launch β€” those may prove to be a second, separate defect that only becomes visible once the first is cleared, and the new logging is there to show it. Reported, diagnosed and corrected by a beta tester, who also ruled out the Play Protect hypothesis we were chasing.

  • πŸ”™ The power menu closed by itself after two seconds, so the device could not be powered off from the button (#248): with the power menu allowed, MainActivity.onResume() defers its lock-task re-entry rather than dismissing the menu, which is correct, but the deferral was a flat postDelayed(..., 2000L). Two seconds is less time than it takes to read a power menu and choose "Power off", so startLockTask() ran while the menu was still up and took it with it: the reporter could turn the screen off but never reboot or power down. The re-lock now waits for window focus instead, which is the signal that fires when the menu is actually dismissed and the same one the print-dialog handling in this file already uses (#249). In the common case that re-locks sooner than the old timer did, because focus returns as soon as the user taps or dismisses; in the slow case it no longer cuts them off mid-menu. The timer survives only as a bound, not as the mechanism: if focus never comes back, because the menu was left open and the user walked away, the re-lock happens anyway after 15s. ⚠️ That relaxes #98's "a reboot must not dodge the lock" by up to 15 seconds, against 2 before, and only on a device whose admin has explicitly allowed the power menu, after a deliberate power-button press. With the power button blocked, nothing changes at all. Not verified on hardware: it compiles and the reporter's device is not available here, so what needs checking is that a long press now shows a menu that stays up, and that a short press still only turns the screen off. Note that a short press turning the screen off is standard Android, not something FreeKiosk does, and that half of the report is expected behaviour. The second half of #248, no soft keyboard in the Android system settings, is not addressed: it does not reproduce here and the report does not yet say whether the device is Device Owner or which setting was turned off, so there is no diagnosis to act on. Reported by @metingul.

  • πŸ”… Brightness did not survive a screen-off cycle, and the reported value hid it (#242): brightness was applied as a window attribute only and Settings.System.SCREEN_BRIGHTNESS was never written, so every wake path handed the panel back to a system value FreeKiosk had never touched. On the reporter's Galaxy Tab A that value was 14/255, which is why a screen set to 80% came back at roughly 5% after any screen off and on, whether triggered by MQTT/REST, the sleep scheduler, or simply plugging the charger in (Samsung wakes the display on a power-state change). The reporter found three sites blanking the override to BRIGHTNESS_OVERRIDE_NONE; there were six, the same four lines copy-pasted through ScreenController, ScreenStateReceiver, KioskModule, ScreenSchedulerReceiver and AutoBrightnessModule. They are now one helper, BrightnessPrefs, which re-applies the level FreeKiosk was asked to hold instead of discarding it. With nothing stored it still blanks the override, which is the previous behaviour and the right one for a fresh install. The requested level is now kept in two places, on purpose: in SharedPreferences, because the wake paths are broadcast receivers running with no JS thread and AsyncStorage is out of reach (this mirrors what keep_screen_on already does), and in Settings.System.SCREEN_BRIGHTNESS, so the value the platform falls back to is the one that was actually asked for. That second write is what makes brightness survive an app restart or a reboot rather than only a screen-off cycle. As Device Owner it goes through DevicePolicyManager.setSystemSetting(), which covers SCREEN_BRIGHTNESS and needs no permission and no prompt; otherwise it needs WRITE_SETTINGS, which the user has to grant from the system screen (AutoBrightnessModule.requestWriteSystemBrightnessAccess() opens it). Adaptive brightness is switched to manual at the same time, because it would otherwise overwrite the value from the light sensor within a second. ⚠️ WRITE_SETTINGS is stripped from Play Store builds via the src/playstore overlay, same as the Doze exemption, so a Play build keeps the window override and the SharedPreferences mirror but not the reboot-proof part. Disabling brightness management entirely (#65) still works and now also clears the stored level, since re-applying it on every wake is exactly what that opt-out asks us to stop doing, and puts the adaptive-brightness mode back the way it was found rather than leaving it switched off for ever.

    Two guards worth naming, because re-applying a stored level is not free. A persisted level is floored at 2%: 0 is a legitimate transient value, it is how the screensaver makes the screen look off, but a terrible thing to store, and both the manual brightness slider and the REST setBrightness action accept it. Before this change a 0 self-healed on the next wake because the override was blanked; re-applying it instead would have brought the kiosk back black, for ever, with no recovery path, which is worse than the bug being fixed. The transient path is untouched, so the screensaver still dims to nothing. Second, when the effective-brightness read fails, status falls back to the requested level rather than to an initialiser, so a device where the native call does not work reports what the old code reported instead of a flat 50%.

    The reporter's secondary point, that the reported value did not match the panel, was correct and had three independent causes, not one. KioskScreen published the stored target to REST and MQTT, which is the brightness: 100 they saw while the panel sat at 14/255. AutoBrightnessModule.getBrightnessLevel() fell back to the last value it had been asked for whenever the window override was absent, which is precisely the moment the two diverge. And DeviceControlService.getScreenStatus() reported an in-memory field that only a method with no callers ever wrote, so it returned its 0.5 initialiser for the entire life of the process: since CloudSyncService sends status.screen.brightness on every 30-second heartbeat, the cloud dashboard showed 50% brightness for every device, always. That one was found while fixing this issue and was not in the report. All three now read the level actually in effect: the window override when one is set, the real system value otherwise. One bound worth stating: Android has no brightness-changed broadcast, so a change made outside FreeKiosk is only picked up on the next refresh (a new target, or the screensaver going on or off), not the instant it happens. Polling for it would cost a wake-up every few seconds on a battery-powered kiosk.

    Not verified on hardware: it compiles, npm test and tsc pass, and no new lint finding is introduced, but none of this has been run on a device. What needs checking is the reporter's own sequence, that is set 80%, screen off, screen on, and confirm the panel returns to 80% and that /api/status agrees with settings get system screen_brightness. Two things to watch specifically: Settings.System.SCREEN_BRIGHTNESS is assumed to run 0-255, which is conventional but not universal and has no public API exposing the real maximum (a panel with a higher range will read dimmer than requested rather than fail, which is why the window override is kept as the primary mechanism), and the Device Owner path via setSystemSetting() has not been exercised. Reported in detail, with the root cause already diagnosed, by @GarethBlain.

  • πŸ—οΈ Most ADB configuration keys were silently ignored (#240): the config intent worked off a hardcoded list of recognized keys, and a key outside it was dropped without a log, a toast or a non-zero exit, so am start printed Starting: Intent... and the operator had no way to know the setting had not applied. On the reported command, 34 of the 45 keys passed were ignored, including everything under return_*, url_filter_*, screen_scheduler_*, webview_zoom_*, keep_screen_on, pdf_viewer_enabled and display_mode itself. A second, separate cause hit kiosk_enabled: it was read with getBooleanExtra, so --es kiosk_enabled "true" passed a String, getBooleanExtra returned its false default, and the command turned Lock Mode off while appearing to turn it on. Three changes: (1) the recognized keys, the direct extras and the --es config JSON mapping are now derived from one shared table (ADB_SIMPLE_KEYS), so the three lists that used to drift apart cannot any more, and 78 keys are supported where the JSON mapping previously covered 31; (2) values are read as raw extras and stringified, so --es, --ez and --ei are interchangeable for every passthrough key; (3) unrecognized keys are named in a toast and in logcat. An explicit display_mode also now survives being passed alongside url, instead of being overwritten. Credentials (pin, rest_api_key, mqtt_password, mqtt_username) are masked in the new logging. Not verified on a device: it compiles and the reported command's 45 keys are all recognized, but a provisioning run confirming each setting lands in the UI has not been done. Reported by @tlindz.

  • πŸ”“ A stalled boot could leave a Device Owner device locked on "Starting kiosk…" forever (#243): BootLockActivity has carried a 2-minute safety timeout since it was introduced in #98 (April 2026), meant to hand the device back if MainActivity never takes over. It never worked. The timeout called finish() on its own, and lock task absorbs a finish(), the exact platform behaviour #222 diagnosed and worked around four months later by calling stopLockTask() first. That workaround was applied only to the #222 branch, twenty lines below, in the same function: stopLockTask() appears exactly once in the file's whole history. So both existing safety nets were gated on isDeviceSecure() == true && isUserUnlocked() == false, and a device with no secure lock screen armed neither of them, hit the timeout, absorbed it, and kept polling forever with no way out. The only escape was adb install -r -d from an already-authorized computer, which is out of reach for anyone who never authorized ADB on that device: the RSA prompt is itself an activity, and lock task blocks it. The timeout now calls stopLockTask() before finish(), once, and only when MainActivity.hasStarted is false. That bound matters: if MainActivity never started then React Native never ran, no kiosk configuration was ever applied, and lock task is guarding a black screen rather than a kiosk, so nothing is weakened. Where MainActivity has started, the previous behaviour is unchanged. ⚠️ This does relax the #98 guarantee ("a reboot must not dodge the lock") by up to two minutes, but only on a boot that has already failed, on a device where the kiosk never came up. Not verified on hardware: the stall could not be reproduced here, the same limitation as #222. The change compiles, and what needs checking on a device is that a healthy boot never reaches the timeout at all (hand-off normally happens in a second or two via isMainActivityReady()), so the new branch stays dead code on a normal boot. Reported by e-mail after a user was locked out of a Ulefone phone with ADB unauthorized.

Don't miss a new freekiosk release

NewReleases is sending notifications on new releases.