Added
-
πΈ Optional: allow the Power + Volume Down screenshot while Lock Mode is on (#277). Lock Mode blocks the hardware screenshot combo for everyone using the device (#172), by design. Security > Lock Mode gains Allow Screenshots With Hardware Buttons (Device Owner only, off by default, so nothing changes unless you turn it on). When on, Lock Mode no longer sets the Device Owner screen-capture policy, so anyone in front of the device can capture the screen. It takes effect the next time Lock Mode starts. The setting is part of the config export/import (
security.allowHardwareScreenshot), the backup and ADB (allow_hardware_screenshot), and Reset to defaults clears it. Whether the combo then works depends on the tablet: not tested on hardware, in particular not on Huawei (HarmonyOS 3), Samsung or Xiaomi tablets. -
π¨ Dashboard mode: adjustable icon size and a color per tile. Tile icons were fixed at 56 dp, which is small on a tablet used by visitors (e.g. a waiting room). Settings > Dashboard gains an Icon Size setting (Small, Medium, Large, Extra large) that scales the icon, the tile and its label, and the number of columns follows. Each tile can also be given a background color from the palette in the tile editor, behind the letter, the favicon or the image; Auto keeps the previous label-based color. Medium is the default and renders exactly as before, so existing dashboards are unchanged. The size is part of the config export/import (
general.dashboardIconSize); the color is stored on each tile (iconColor). Raised by a user on GitHub. -
π External App mode: the Dim screensaver can stay over the app instead of switching back to FreeKiosk (#266). Until now the screensaver brought FreeKiosk to the foreground and relaunched the app on wake, so an app like Home Assistant reloaded every time. The new Keep the app in front option (Settings > Display > Screensaver, Dim style only, off by default) lays a dimming layer over the app, which stays open; the first tap only wakes the screen and never presses anything underneath. Motion and proximity wake are not available with this option, and the 5-tap escape still works.
-
πΉ Live MJPEG camera stream for Home Assistant (#226, PR #228 by @cocoke).
GET /api/camera/streamserves the camera as an endless MJPEG stream, for the MJPEG IP Camera integration, VLC or a plain<img>tag. Opt-in under Settings > Advanced > REST API > Live Camera Stream, off by default: the endpoint answers503until it is on. Camera, frame rate, quality and width have defaults in the settings and can be overridden per request. A camera accepts one client at a time, so while a stream runs, motion detection works from its frames instead of opening the sensor itself; it keeps the sensitivity setting but does not behave exactly like the usual detection. Streaming costs bandwidth and CPU: about 5 Mbit/s and two thirds of a core at 1280Γ960, quality 60, 10 fps, per the contributor's measurement. -
π One Camera Rotation setting, and a
rotateparameter on the photo and the stream (#142, #253). Settings > Advanced > REST API > Camera Rotation (-1for automatic, or0,90,180,270) applies to the REST photo, the MQTT camera snapshots and the live stream alike, for cameras the automatic orientation gets wrong and for wall-mounted tablets. Both endpoints takerotate=auto|0|90|180|270to override it per request; any other value is a400. -
π§° Two provisioning scripts, for boxes where Device Owner and boot launch fail with errors that name the wrong thing. Provisioning an MXQ/Droidlogic TV box (Android 9, SDK 28) failed three times over, and none of the three errors pointed at its cause.
dpm set-device-ownerreports every failure with the same opaqueRuntimeException: Can't set package ... as device owner, so the operator is left guessing between accounts, secondary users, an existing owner and half a dozen other preconditions.scripts/set-device-owner.ps1checks each one separately and names the blocker.On that box the blocker was none of the usual ones: the ROM omits the
android.software.device_adminsystem feature, soDevicePolicyManagerServiceruns withmHasFeature = falseandsetDeviceOwner()returnsfalseunconditionally. Worse,setActiveAdmin()is a silent no-op on the same flag anddpm set-active-adminprintsSuccess:regardless, because it never checks the return value β which is why the obvious diagnostic says the admin was registered whiledumpsys device_policystays empty.-Fixdeclares the feature in/system/etc/permissionsand reboots, on auserdebug/engROM that grantsadb root.With Device Owner set and both Launch on Boot and Set FreeKiosk as default launcher switched on, the app still never came up after a reboot, for two further and independent reasons.
ACTION_BOOT_COMPLETEDis never delivered to third-party manifest receivers on this ROM, soBootReceivernever runs; hand-deliveringQUICKBOOT_POWERONto the same receiver starts the kiosk correctly in four seconds, so the app-side logic was never at fault. And the Home intent is resolved while user 0 is still locked, which filters outMainActivity(notdirectBootAware) and hands Home to the OEM launcher β the Device Owner policy is registered correctly, it is simply never consulted, and Android does not re-resolve Home after unlock.pm disable-useron the OEM launcher does not help either: the ROM re-enables it at boot.scripts/install-boot-autostart.ps1installs an init hook that startsMainActivityonce user 0 unlocks.β οΈ Both
/systemchanges are per-box provisioning steps, not properties of the image: neither survives a factory reset or an OTA that rewrites/system, and neither is possible at all on auserbuild withoutadb root. The boot hook is a workaround for a bad ROM, not a product fix β adirectBootAwareHome activity would address the second cause properly in the app, and.HomeActivityis the natural candidate. β οΈ Also worth knowing when clearing a device down:dpm remove-active-adminrefuses a non-test admin, so on a release APK the Device Owner cannot be removed over ADB at all and a factory reset is the only supported route; the script says so rather than reporting a bareSecurityException.scripts/device/*is pinned to LF in.gitattributes, because a CRLF shebang is not a valid interpreter path and init will not parse a CRLF.rcβ on Windows the repo's* text=autowould otherwise hand a teammate a script that fails on-device for reasons having nothing to do with the ROM.Verified on hardware, on one box only. The feature patch was confirmed by
dpm set-device-ownersucceeding after the reboot, and the boot hook by three consecutive reboots reaching a launched kiosk on the first attempt each time. Everything here is Windows tooling and documentation: no app, Gradle or manifest code is touched and nothing ships in the APK. What has not been exercised is the confirmation prompt added toset-device-owner.ps1after the box was unplugged (the scripts parse and the y/N matching is unit-tested, but the prompt has not run against a device), the-Fixbranch driven end to end through the script rather than by hand, and any ROM other than this one β the failure modes are documented as observed on a Droidlogic box, not as general truths about Android TV. -
π«π· The app is available in French (PR #241 by @stephanerosso59-debug). Settings > General > Language switches between English and French at once; without a choice, the app follows the device language, and the choice survives a restart. Every screen of the app is translated, from the PIN screen to the settings, including the features added since the contribution was written. The French wording follows FreeKiosk Cloud's: Device Owner stays in English and enrolment is enrΓ΄lement. Texts sent to the REST API, MQTT and FreeKiosk Cloud stay in English, like the errors returned to web pages, and a few native Android messages (toasts, notifications) are not translated yet.
-
π¨οΈ Web pages now print silently on a USB ESC/POS printer, with no print dialog (PR #267 by @daniel-rolo-robotics). A kiosk web app had no way to print unattended: Android's print framework always shows the system dialog, print services included, and nobody is standing at the tablet to tap it.
Settings > General > Printinggains an Enable Silent Printing switch, which drives an ESC/POS printer directly. It sits under its own Silent Printing heading, apart from the existingwindow.print()switch, which moves under a Window Printing heading and is renamed from Allow Printing to Enable Window Printing. Turning on Silent Printing shows the attached printer's name, command set,VID:PIDand paper state, with settings for print width, feed and cut, and a test page that proves the transport, the driver and the print width with no web app involved.A page prints through
window.FreeKiosk.silentPrinter:print()prints it through its own print stylesheet, so its fonts, its language and its QR codes come along, andprintImage()prints a PNG or JPEG the page renders itself.window.print()is untouched and still belongs to Enable Window Printing and the Android dialog, so one page can use both: A4 through the dialog, receipts silently. Every call returns a promise, andgetStatus()reports the printer and its paper before a page promises a ticket β a kiosk that reports a print that never came out is worse than one that says nothing. Rejections carry codes (PAPER_OUT,NO_PRINTER,ORIGIN_NOT_ALLOWEDβ¦), and a Restrict printing by origin switch limits the API to a list of origins; left off, any displayed page may print.Width is configured in dots, the unit the printer actually addresses, so no paper size or dot pitch is assumed anywhere. Pages are laid out so one CSS pixel is one printer dot, so a print layout is designed against the dot width in
pxrather than against physical units the browser and the printer would disagree about.Printers are matched by the USB Printer Class rather than a table of vendor ids, which is what makes unfamiliar hardware work; a vendor-specific interface with a bulk OUT endpoint is driven too, minus status reporting. Identity and paper state come from the printer itself, through the USB Printer Class requests
GET_DEVICE_ID(an IEEE 1284 device ID string) andGET_PORT_STATUS. Pages print as rasters rather than text, because text mode depends on per-model code pages β which is how Β£, β¬ and accented characters turn to garbage β and on native QR commands many cheap printers lack. Transport and command set sit behind two interfaces, so Bluetooth SPP, TCP:9100 and Star's graphics mode can follow without disturbing the rest.β οΈ One step has to happen before Lock Mode: plug the printer in and tick "Always open" when Android asks which app should handle it. That is the only USB grant which survives a reboot, or the re-enumeration a powered USB-C adapter causes when its power is plugged or unplugged β lock task suppresses the alternative dialog, and the Grant access button in Settings only lasts until the next unplug. Three more bounds are known: printing is USB only for now; the command set is ESC/POS only, so a label printer that speaks only ZPL or TSPL will not print; and
PrintDocumentAdapter's result callbacks have package-private constructors, soandroid/print/PrintAdapterBridge.ktsits in the framework's package to subclass them; should a future Android refuse that,silentPrinter.print()reportsPAGE_RENDER_FAILEDandprintImagekeeps working. Verified on hardware: printed end-to-end on an Xprinter C58H attached to an HONOR Pad X8b LTE running Android 16, from a production web app whose confirmation page prints a collection slip β an inline-SVG QR code and text β throughsilentPrinter.print()and its own@media printstylesheet. The printer needed no model-specific code and no entry inusb_printer_filter.xml, the default 384 dots was right for its 58mm paper with nothing to adjust, and Restrict printing by origin rejected a page outside the list withORIGIN_NOT_ALLOWED. Two parts of the surface were not exercised by that run:printImage(), which shares its dithering, encoding and transport with the path that was, and the paper state βgetStatus()'spaperfield and thePAPER_OUTrejection β which depends on the printer answering the Printer Class status request and is consulted nowhere in the printing path itself.
Changed
- βοΈ FreeKiosk Cloud moves from closed to open beta. Sign-up no longer requires an invitation; self-service device add-ons (up to 500 devices) are available from the dashboard. The feature itself is unchanged: still opt-in, still free and MIT for the app regardless of cloud enrollment.
- π·οΈ Allow Printing is now labelled Enable Window Printing (Settings > General > Printing), next to the new silent printing switch. Same setting, same stored value: a device that had it on keeps it on.
Fixed
- π Settings gestures: the Kiosk-screen signal no longer depends on the Activity being attached (#289). The five-tap and Volume Up shortcuts only work once the Kiosk screen has told the native side it is showing. That message was passed through the current Activity and silently dropped when there was none, with no second try, which would leave both gestures off until the app was restarted. The state is now kept without depending on the Activity. Reported on a Giada DN74 (Android 11) where both gestures stayed dead after a cold boot. This is a possible cause found by reading the code: it could not be reproduced on an Android 15 emulator, including after a real reboot with the old code, so it is not confirmed that this is what that tablet hit.
- π½ Drop-down lists (
<select>) open under their field again, instead of across the whole screen, on tablets. On a tablet the list opened as wide as the screen and stuck to the top edge, whatever the position of the field, and the same page looked right in a plain browser. The Android WebView anchors that list on a view it adds inside the page and expects a layout pass to place it, but React Native swallows the layout request, so the view stayed at 0,0 with no size. The WebView container now lays itself out again when a layout is requested below it. Checked on an Android 15 emulator set up as a tablet (1920x1200): the list opens at the same place and the same size as in a plain WebView. Not tested on the reporting tablets (Honor X8A, Lenovo TB328FU). - βοΈ A setting kept at 0 is no longer reported to FreeKiosk Cloud as its default. The config the tablet reports used
parseFloat(value) || default, so a stored 0 came back as the default: camera rotation 0 as -1 (automatic), the MQTT screenshot width 0 ("no limit") as 1280, the WebView back button at the left or top edge as 2 % and 10 %, a brightness of 0 as 50 %. The cloud then saw a config different from the one it had pushed, never marked it as applied and kept pushing it, ignoring changes made on the tablet. A missing or unreadable value still falls back to the default. - βοΈ Unenroll tells FreeKiosk Cloud even on a poor connection, and can no longer hang. The call that tells the cloud a tablet has left was a single request with no time limit and no second chance: a connection that stalled kept the Leave Cloud Management button spinning, and a tablet that was briefly offline never told the cloud, which then kept listing a device that was gone. Each try now stops after 8 seconds, and a network failure or a server error gets up to three tries; any answer below 500 is final. The local wipe always follows, whatever the cloud answers, so Unenroll still works against an older cloud that does not know the call (a 401, 403 or 404 is not retried). On a recent cloud the device then stays in the dashboard, greyed out, with an alert, until an administrator deletes it. Nothing changes on the tablet's side of the screen or in its settings.
- π Managed apps opened from a web page are no longer pulled back to FreeKiosk after about 10 seconds (#282). In Website and Media mode the watchdog relaunched FreeKiosk as soon as another app was in front; only External App mode tolerated one. A managed app (the ones lock task allows, e.g. Maps from a
geo:link, Telegram fromtg:, the dialer fromtel:) is now left alone. Anything that is not a managed app, or an unknown foreground (Usage Access not granted), still brings FreeKiosk back as before. The foreground app is read from usage events, since the 5-second window used so far reports nothing once an app has been in front for longer than that. β οΈ Lock task blocks the Home key, so the user leaves the managed app with Back; an app with no way out is no longer cut short by the watchdog. Checked on an Android 15 emulator as Device Owner with Settings as managed app: it stays open for 90 seconds (it was pulled back after about 10), Back returns to FreeKiosk with the lock still active, and without Usage Access or after a crash with the launcher in front FreeKiosk is relaunched as before. Not tested on the reporting tablet (Galaxy Tab A11) or with a real link opened from a page. - βοΈ Config export, import and FreeKiosk Cloud sync now carry 18 settings they used to drop. A config exported from one tablet and imported on another, or pushed from the cloud, left these at their defaults: Allow Remote Screenshots (
security.allowRemoteScreenshot), the Restart Button (general.restartButton), the Live Camera Stream (advanced.cameraStream), and the MQTT screenshot and camera publishing (advanced.mqtt.screenshot,advanced.mqtt.camera). Language and Beta Updates stay out of the export on purpose: they describe the tablet, not the kiosk setup. The cloud dashboard editor does not offer these settings yet. - πΎ Backup now includes four settings it was missing: the Proximity Screensaver, the Restart Button and its long-press duration, and the Dashboard icon size. A restore from a backup brought them back to their defaults.
- π§Ή Reset Settings now clears 27 settings it used to leave behind. The list of settings Reset removes had not followed the settings added since: Screen Lock Compatibility, Block Factory Reset, Default Launcher, Intercom Mode, Allow Remote Screenshots, Proximity Screensaver, the six Live Camera Stream settings, Print Paper Size, Disable User Zoom, Pause Web Media When Hidden, Beta Updates and the eleven Media Player settings kept their value after a reset. Block Factory Reset and Default Launcher are re-applied from the stored value at every launch, so their restriction is lifted at the next start. The language is deliberately kept.
- π§± FreeKiosk is no longer killed by Android 15 for "Too many Binders sent to SYSTEM" in External App mode (#281). The overlay that carries the return button was removed and re-added every 3 seconds to stay above camera apps (#121). Each cycle creates a window whose Binder the system keeps until FreeKiosk next garbage-collects, which an idle app does rarely. Measured on an Android 15 emulator in External App mode: the Binders the system holds for FreeKiosk grew by exactly 20 per minute (one per cycle), and a forced garbage collection brought them back down. The re-pin now runs every 30 seconds and is skipped while the screen is off. β οΈ A camera app that draws over the return button can now hide it for up to 30 seconds instead of 3. Not yet confirmed on the reporting device (Redmi Pad SE), where the kill took 16 to 20 hours to appear.
- π The WebView no longer stays on "Loading..." after a config applied over ADB (#280). FreeKiosk restarted by launching the new instance and killing its own process straight after, so the new process asked for a WebView renderer while Android was still tearing down the old one; on a slow device (Android 11, WebView 106) that request stayed pending until a reboot. The relaunch is now scheduled with
AlarmManager1.5 s after the process exits, and thePendingIntentopts in to background activity starts: without that, Android 14 and later (target SDK 36) block the launch from a dead process and the app stayed closed after provisioning. Checked on an Android 15 emulator; not yet tested on the reporting device (Pexar PX-110). - π
URL Planner compared dates in UTC, so a dated event started or ended at the wrong hour.
isEventActivetook today's date fromtoISOString()(UTC) but the time from the local clock. In summer in the UK an event starting on the 12th began at 01:00 and one ending on the 25th ran until 01:00 on the 26th; at UTC+14 an event dated for the local day never activated. It now uses the local calendar date. Reproduced on an Android 15 emulator at UTC+14 before the change (event of the local day never shown, event of the previous local day shown). After the change,isEventActiverun underTZ=Pacific/Kiritimatishows the local-day event active and the previous-day one inactive; not re-run on the emulator, and the switch back to the base URL when an event ends was not observed. - π External App mode without Device Owner no longer pins FreeKiosk (#275). Screen pinning was started at launch whatever the display mode, and without Device Owner there is no whitelist, so the pinned FreeKiosk task was the only one allowed in front.
MainActivityandKioskModule.startLockTasknow skip pinning in that case. β οΈ Not verified on hardware (no Bigme B6 here): that pinning is what kept the external app behind FreeKiosk is deduced from the code, not observed. The PIN keyboard that never appeared in the report is not explained by this. Without Device Owner the external app is no longer locked by pinning; Device Owner remains the way to lock it. - β¬οΈ An in-app update could sit on its "Downloading" dialog for ever (#274). The dialog could not be closed and only went away when Android reported the download finished; a download that stalled (no network, a download manager waiting or restricted) never reported anything, so the settings screen was stuck. The dialog now shows the progress, or what Android is waiting for, and has a Cancel button; a download that makes no progress for 90 seconds is stopped with a message saying so. On a Device Owner tablet, a silent install that Android refuses after the download is now reported on screen instead of only in the logs.
- πΆ On Android 10 and 11, a Device Owner tablet joined a Wi-Fi network only for FreeKiosk (#273). The Wi-Fi picker used an app-scoped request there, so the network never became the tablet's saved, default Wi-Fi until it was joined again from Android's settings. A Device Owner now saves and selects the network the way Android 12+ already did, updating it in place when the name is already saved. Without Device Owner, Android 10/11 still only offer the app-scoped connection. The picker also no longer reports a failure, and forgets the saved password, when the network has no internet access: once it is the default Wi-Fi, the connection counts as made.
- πͺ After a reboot with lock mode off, the 5-tap escape could be missing over the external app. When FreeKiosk is not a locked kiosk, its boot receiver launched it again about four seconds after boot, although Android had already started it as the home app and it had already opened the external app. That second launch pulled FreeKiosk back over the app, and the resume was taken for a return from it, which stopped the 5-tap overlay. The boot receiver now leaves an already running FreeKiosk alone, and an involuntary return no longer stops the overlay, as the native side already intended.
- π A local
file://page could not be set from the settings screen (#239). Saving refused everyfile://URL with "This type of URL is not allowed", a check older than local file support, so a local page could only be set over ADB. It is now accepted once General > PDF Viewer > Inline PDF Viewer is on, the setting that gives the browser file access; with it off, the message says which setting to turn on.javascript:anddata:URLs are still refused. The FAQ also pointed to "Advanced > PDF Viewer", which does not exist. - β¨οΈ REST API and MQTT settings sent over ADB only took effect on the second launch. On start-up the REST server and the MQTT client read their settings in parallel with the step that moves the ADB config into storage, and could win the race:
rest_api_enabled,mqtt_broker_urland the rest of their settings were stored, but the server and the client had already started with the old values. They now wait for the ADB config to be applied first. - π After a restart, FreeKiosk could crash on launch and leave the external app in front with no 5-tap escape. When FreeKiosk's process dies (the restart after an ADB config, or Android killing it for memory), Android restarts its background heartbeat service in the new process. If FreeKiosk was already in the foreground by then, React Native refused to start the task and threw, which killed the process again: the external app stayed in front, the 5-tap overlay never came back, and Android could show "FreeKiosk keeps stopping". The task may now start in the foreground and simply does nothing there, since the foreground heartbeat already runs on its own.
- π· Camera photos came out rotated 90Β° (#253). The REST photo and the MQTT camera snapshots were served as the raw sensor frame. They are now turned upright from the camera and screen orientation, with the pixels rotated rather than an EXIF tag, since Home Assistant's generic camera and MQTT image entities do not read EXIF. Some sensors are mounted against the orientation Android documents (a Xiaomi front camera was measured 180Β° off); on those, set Camera Rotation (or pass
rotate=) to the value that fits. If you had compensated for the old rotation (a CSS rotate on a dashboard card, for instance), remove it, or passrotate=0to get the raw frame back on the REST photo. Motion detection is unaffected.