Skip to content

FreeKiosk v2.0.0-beta.3 - Cloud Fixes

Pre-release
Pre-release

Choose a tag to compare

@valentingomy valentingomy released this 08 Sep 10:40

Added

  • 🖧 --es cloud_token now enrols a device from the ADB command line. The cloud's "Headless install (ADB)" snippet on the Add Device page has advertised --es cloud_token "TOKEN" since the cloud shipped, but no build ever read the extra: the intent only stored the PIN, so a scripted enrolment silently did nothing and the operator had no way to tell. The extra is now recognised and written to the same store the setup-wizard QR feeds (FreeKioskCloudEnrollment), so it is consumed by the existing CloudSyncService.consumePendingProvisioningEnrollment on first launch with nothing added on the JS side.

    --es cloud_url comes with it, optional, defaulting to https://cloud.freekiosk.app. That default is not cosmetic: the consumer returns early when the stored URL is empty, so a token on its own would have been stored and then ignored, which is the same silent failure in a new place. Passing the URL explicitly is what a self-hosted instance will need. The token joins pin and the MQTT credentials in ADB_SENSITIVE_KEYS, so it is masked in the ADB logging rather than printed to logcat. Not verified on a device: it compiles, and the store and its consumer are the ones the QR path already exercises. What needs checking is a real am start with a fresh token ending on an enrolled device. Reported by a beta tester who found the page advertising a flag that did not exist.

  • 🔄 A long-press button can now restart the WebView from the kiosk itself. Nothing on a locked device could recover a hung or stale page: the person standing in front of the screen has no access to Settings, so a dashboard that lost its WebSocket and stopped updating meant restarting FreeKiosk or rebooting the tablet, neither of which is practical on an unattended kiosk. A grey circular button in the top-right corner of WebView mode now fills a progress ring while it is held and restarts the WebView once the hold completes. A short tap does nothing, so a stray touch or a passer-by cannot trigger it. The hold duration is configurable from 1 to 10 seconds (5 by default) under Settings > General, and the ring is drawn with two clipped, rotated half-discs rather than by adding an SVG dependency. The button is hidden on the dashboard grid, where no WebView is mounted. Contributed by @paolo-lussignoli in #224, who also opened the request as #223.

  • 📸 Screenshots and camera snapshots are published over MQTT, and auto-discovered by Home Assistant (#182). Both were reachable only through the REST API, which meant hand-written camera: YAML with a still_image_url and the embedded HTTP server exposed on the network just to see a tablet's screen. Each enabled stream now publishes a JPEG on its own topic ({base}/{topicId}/image/screenshot, .../image/camera_front, .../image/camera_back) and advertises an image entity, a camera entity fed by the same topic (the first reads well in Markdown cards and notifications, the second in picture-glance), a capture button, an auto-publish switch and an interval number. One set per camera actually present on the device, so a tablet with no front sensor advertises no front entities. Seven new commands come with it, from screenshot_capture to camera_interval, and disabling a stream clears both its discovery config and its retained image so nothing stale is left on the broker.

    Everything is off by default and nothing is captured until it is switched on in Settings > MQTT. Captures and publishing are fully native, on a dedicated capture thread: that is deliberate, because both ScreenCapture and CameraPhotoModule block on latches while MQTT commands arrive on the main thread, and because it keeps working while the JS thread is suspended, the same reasoning as the existing screenOn and tts handling. ⚠️ Worth reading before enabling it: a screenshot publishes whatever the kiosk displays, a camera snapshot publishes whoever is in front of it, images are retained on the broker, and TLS is only used when the broker port is 8883. The MQTT documentation now says so and suggests the broker ACL to match. Contributed, and tested end to end against Mosquitto and Home Assistant on a Xiaomi tablet under Android 16, by @cocoke in #225.

    Four defects were fixed on top of the contribution during review, none of which surfaced in that run. stop() interrupted the capture thread with shutdownNow(), and CameraPhotoModule closes its CameraDevice only after the latch it waits on, so an MQTT disconnect during a camera capture left the camera held by the process, breaking motion detection and every later snapshot until a restart. restartTimers() was not synchronized while its runnables re-post themselves, so two interleaved calls could leave two timer chains publishing the same stream for ever. An unparseable interval payload was clamped up to the 5-second minimum instead of being ignored, putting the device on the fastest capture loop on a typo. And the nine new settings keys were missing from clearAll(), so camera publishing survived a full settings reset. Not verified on hardware past what the contributor measured: the screen-off case in particular is expected rather than tested, and the periodic camera path has not been left running for long on a battery-powered device.

    Two decisions worth naming, both taken after the merge. The button is off by default: it draws a visible control over the kiosk page and lets anyone in front of the screen restart it, so it is opt-in like every other WebView overlay. And it remounts the WebView rather than calling reload() on it, so it restarts on the configured URL instead of reloading whichever page the user had navigated to. That is deliberate rather than an oversight: the case this was asked for is a page that has stopped responding, where a remount also recovers a dead render process and a reload() can go unanswered. The settings text says so instead of promising a reload of the current page. ⚠️ Two bounds are known and not yet addressed. The position is fixed at the top-right corner, so the 50x50 dp square it occupies is no longer clickable on the page underneath, which matters for a web app whose own controls sit there (the WebView back button, by contrast, is positionable in percent). And the setting has no ADB key, so a fleet provisioned by script cannot switch it on. Not verified on hardware here: tsc is clean, npm test passes and ESLint reports no new finding, but nothing was run on a device on our side. The contributor reports testing the interaction on a Samsung Galaxy Active 5 running Android 16.

Fixed

  • 🛡️ The permission wizard told a Device Owner that everything was granted automatically, which is not true of two of them: the banner read "Managed mode (Device Owner). Permissions are granted automatically; you can finish right away.", so an operator provisioning a fleet closed it without granting anything. Display over other apps and Usage access are appops rather than runtime permissions, and Android exposes no way for a Device Owner to grant them to itself (setPermissionGrantState covers dangerous runtime permissions only). Without the overlay the blocking overlay and the secret tap zone never appear, so on a QR-provisioned device there was no way out of an external app; without usage access, external-app mode cannot detect and relaunch the foreground app. The banner now names those two and says why they still need a tap each. The wizard already listed both with a working Grant button that deep-links to the right system screen, so nothing else had to change.

    This does not make the rollout hands-off, it makes the wizard honest: those two taps per tablet remain. An exit path that does not depend on the overlay while an external app is in front would remove them, and that is design work rather than a fix. Reported by a beta tester provisioning tablets for external-app kiosks.

  • 📌 A QR-provisioned device landed on the stock launcher, so FreeKiosk never started: the wizard completed and the enrollment token was stored, but Home pinning threw and the tablet booted to the system launcher with FreeKiosk installed and idle, which costs the zero-touch enrollment its trigger. DeviceAdminReceiver.kt carried import android.app.admin.DeviceAdminReceiver alongside its own class DeviceAdminReceiver. In Kotlin an explicit import outranks a same-package declaration, so every DeviceAdminReceiver::class.java in that one file resolved to the framework class, and pinHomeLauncher handed DevicePolicyManager a component named com.freekiosk/android.app.admin.DeviceAdminReceiver, which it rejected: SecurityException: Admin ComponentInfo{...} does not exist or is not owned by uid. KioskModule builds the same component and always worked, because no other file in the project carries that import, which is why pinning succeeded as soon as the app was opened by hand. The supertype is now spelled out inline instead of imported, so the name resolves locally; verified in the bytecode, where the class literal is com/freekiosk/DeviceAdminReceiver rather than the framework one.

    This defect is older than the 2.0.0-beta.2 provisioning fix and survived it untouched. It only became visible because beta.2 replaced an empty catch {} with a real log line, which is the whole argument for not swallowing exceptions. Diagnosed down to the shadowed import, with the log line and the fix, by a beta tester provisioning a fleet of Galaxy Tab S10 FE.