Added
- ๐ Two more languages: Polish and Simplified Chinese. Settings > General > Language now lists Polski and ็ฎไฝไธญๆ, nine languages in all. With no language chosen, a device set to Polish gets Polish, and a device set to any Chinese (
zh_CN,zh_TW,zh-Hant-HK) gets Simplified Chinese, there being no Traditional Chinese translation. Polish uses its four plural forms. The texts follow a glossary shared with FreeKiosk Cloud and the website, which get the same two languages, so a setting has the same name in all three; no native speaker has reviewed them yet. Checked on an Android 15 tablet emulator: in each language the PIN screen and the five settings tabs, scrolled to the end, showed no English text apart from Bluetooth and Device Owner, the language list showed the nine names, and removing the stored language brought English back. Not checked on screen: the one plural text (ESC/POS feed lines), covered by the locale tests only. - ๐งญ Dashboard: full-screen tiles with two-finger gestures. Dashboard > Tile Pages gains three options, all off by default. Auto-hide navigation bar (hide after 1โ30 s, default 4): a tile page fills the screen; swipe down with two fingers to show the bar (with the status bar, if on) as an overlay, up to hide it. Swipe between tiles: swipe left with two fingers for the next tile, right for the previous one, wrapping around. Keep tiles loaded: up to 5 tiles load in the background when the dashboard starts and stay loaded, so switching is instant and each page keeps its state (uses more memory; hidden tiles have their media paused, which stays paused when the tile is shown again, and cannot speak or print; if one tile's page crashes, all kept tiles reload). The swipes are recognised natively, so nothing is injected into the pages, and they only observe the touches: pages, the N-tap settings access and the inactivity return behave as before. A page that uses two-finger pans (maps, zoomable images) can trigger them. The settings are part of the config export/import (
general.dashboardNavAutoHide,general.dashboardSwipeBetweenTiles,general.dashboardKeepTilesLoaded) and the backup. Contributed by @richiela. - ๐จ Multi-App mode: choose the background colour, the heading and the logo of the home screen (#287, #162). Settings > General > Multi-App appearance (External App mode, several apps): a colour from the palette or
#RRGGBB, the heading text, and a logo picked from the device, each of which can be hidden; the heading and the app labels switch to a contrasting text colour on their own. Synced with FreeKiosk Cloud asexternalApp.appearance; the logo is a file on the tablet, so a backup or a cloud config keeps the setting but not the image. Values read back from storage or a config are checked first: a colour that is not#RRGGBBfalls back to the default, the heading is cut at 100 characters, and only a localfile://logo is used. Checked on an Android 15 emulator with two apps: the heading showed the chosen text instead of FreeKiosk, disappeared when hidden, and a config with an invalid colour and a remote logo URL showed the default colour, the text and no logo, with no crash; the section is in Settings > General. Contributed by @chrios. - ๐ Always Load from the Network: an updated site no longer shows old images, scripts or style sheets (#263). A reload reused any cached file still within its
max-age, so a site that shipped a new logo under the same address showed its new page with the old logo until the cache entry expired. Settings > General > Auto Reload > Always Load from the Network, off by default (ADBalways_load_from_network, synced with FreeKiosk Cloud asgeneral.alwaysLoadFromNetwork), clears the WebView cache at the start of every page load, whoever starts it: the page itself (location.reload(),<meta http-equiv="refresh">), areloadcommand or FreeKiosk. It costs data, and a page can no longer show from cache while the network is down. Switching the WebView toLOAD_NO_CACHE, the first idea, does not do it: on an Android 15 emulator a file cached for an hour was not fetched again, on a reload or after an app restart. Checked on that emulator with a page whose image is cached for an hour, reloaded 4 to 5 times each way: onmainthe image was never fetched again; with the option on it was fetched on every load and shown every time, with no network error in the log; with it off nothing changes. On the final 2.1.0-beta.2 build, with the option just switched on, 2 of 4 runs still served the first one or two loads after the restart from the cache (8 loads gave 6 image requests), while the others fetched on every load; not explained. Reported by @jac00sch. - ๐ Boot without Device Owner: a loading screen instead of the system launcher, and a lock screen without PIN no longer holds the kiosk back. Without Device Owner, FreeKiosk came to the front only on
BOOT_COMPLETEDplus a 3 s wait. Android sendsBOOT_COMPLETEDonce the system launcher has settled, so the launcher showed in the meantime; on an Amazon Fire HD 8 (KFTRPWI, Fire OS 7.3, Android 9)MainActivitystarted 16.5 s after FreeKiosk's process. A native loading screen (the look of the Device Owner boot screen, without lock task) now comes up atLOCKED_BOOT_COMPLETEDand again atBOOT_COMPLETED, and finishes as soon asMainActivityhas started. Since an activity is already in front, Android does not start the launcher at all. The early one needs to know whether auto-launch is on before credential-encrypted storage is readable, soBOOT_COMPLETEDwrites that to device-protected storage and it applies from the second boot after the update. It is never shown over a secure lock screen, so the PIN can still be entered (#222). A lock screen without PIN, pattern or password is now dismissed when the loading screen orMainActivitycomes to the front, as Fully Kiosk's Force Screen Unlock does; a secure one is left alone. The 3 s wait beforeMainActivity(v1.2.1, "important for Android 9") is unchanged, now a named constant, and covered by the loading screen. Measured on that Fire, seconds aftervoldstarted: before, the launcher showed from 13.8 s andMainActivitystarted at 29.0 s; after, the loading screen was in front at 15.6 s (0.9 s after the boot animation), the launcher was never started, the swipe lock screen went away without a touch, andMainActivityfollowed at 23.8 s, with the 3 s wait set to 0 for that test, so about 3 s later with this change as it is. Raising the priority ofBootReceiverwas tried and did not bringBOOT_COMPLETEDforward. Android 10 and later, without Device Owner: Android refuses the start from the boot receiver (BAL_BLOCK) unless the FreeKiosk accessibility service is enabled, so with it off nothing comes to the front at boot, as in 2.1.0-beta.1. Checked on an Android 15 emulator with the service on and a swipe lock screen: the loading screen was in front of the launcher about 10 s after boot and the lock screen was gone (with 2.1.0-beta.1 it stayed up for the 70 s watched); with a PIN lock screen, no loading screen showed over it. Not checked: other devices and Android versions, Device Owner installs (their path is unchanged), External App mode, FreeKiosk as the HOME app (the loading screen is skipped whenMainActivityalready runs), and a secure lock screen at boot. - ๐ The dim screensaver can fade out slowly instead of going dark at once. Settings > Display > Screensaver > Fade Out, 0 to 30 seconds, 0 (off, the old instant dim) by default. With a fade set, FreeKiosk lowers the brightness in steps from the current level to the screensaver level over that time, and when the screensaver level is black (or FreeKiosk does not manage brightness), the black layer fades in over the same time. Waking stays instant and stops a fade still in progress. In a dark room the screen lights what the front camera sees, so camera motion is ignored during the fade and for 4 seconds after it: otherwise the fade itself counts as motion and wakes the screensaver. A tap still wakes it, and proximity wake is unaffected. It applies to the Dim style shown by FreeKiosk itself; it is not offered with Keep the app in front in External App mode, where a native overlay does the dimming. Part of the device backup and of the config synced with FreeKiosk Cloud (
display.screensaver.fadeSeconds). - ๐ Screen Orientation: lock FreeKiosk to portrait or landscape (#189). Settings > Display > Screen Orientation: Follow the device (the default, as before), Portrait, Landscape, Portrait (upside down) or Landscape (reversed). ADB
screen_orientation(auto,portrait,landscape,reverse_portrait,reverse_landscape), synced with FreeKiosk Cloud asdisplay.screenOrientation. It locks FreeKiosk's own screen (Website, Dashboard and Media modes), applied as the activity resumes so the other orientation does not flash first, and is not offered on a TV or in External App mode, where the app in front decides. Android 16: on screens of 600 dp and more, Android ignores the orientation an app asks for once it targets Android 16, which FreeKiosk does; the manifest now carries the opt-out Google documents for this (PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY), which Google describes as temporary. Checked on an Android 15 tablet emulator: each value gave the expected display rotation (0, 90, 180, 270), Portrait held when the emulator was rotated while Follow the device turned with it, External App mode stayed unlocked with no flip on resume even with Landscape stored, and the ADB key applied. Not checked on Android 16: an Android 16 tablet image would not boot on the test machine, so whether the opt-out restores the lock there is unverified. - โจ๏ธ No on-screen keyboard, for pages filled by a barcode scanner (#148). Settings > Display > Keyboard Mode now offers No On-Screen Keyboard next to Default, Force Numeric and Smart Detection (ADB
keyboard_modenone). FreeKiosk setsinputmode="none"on the page's text fields, text areas and editable elements, including those added later, and focuses a field again if it already had the focus, so the keyboard does not open while a keyboard-wedge scanner still types into the field. It applies to the web page in Website mode only: FreeKiosk's own PIN and settings keep their keyboard, a frame inside the page is not covered, and Android offers no way to hide the keyboard of another app. Checked on an Android 15 emulator with a page whose field hasautofocus: in Default mode a tap on the field opened the keyboard; in the new mode it stayed closed, and text sent as a scanner would (adb shell input text) still landed in the field. - ๐ The loading error screen can leave the URL out (#217). Settings > General > Auto Reload > Hide the URL on the error screen, off by default (ADB
hide_error_url, synced with FreeKiosk Cloud asgeneral.hideErrorUrl). With it on, a page that fails to load shows Loading Error and the reload notice, not the address it tried. This keeps a back-end address off a public screen; it does not protect the back-end itself. Checked on an Android 15 emulator against a closed port: off, the screen showedURL: http://10.0.2.2:9/...; on, only Loading Error. - โธ๏ธ URL Rotation can wait while someone uses the page (#235). Settings > General > URL Rotation > Pause while someone uses the page, with Resume after (seconds), 60 by default and at least 5 (ADB
url_rotation_pause_on_touchandurl_rotation_resume_delay, synced with FreeKiosk Cloud asgeneral.urlRotation.pauseOnTouchandresumeDelay). While it is on, a change that falls within the resume delay of the last touch or key press waits, and the rotation goes on a full interval after that delay. Only touches and key presses count: a page that scrolls by itself does not hold the rotation, and the rotation's own page changes do not either. Checked on an Android 15 emulator with two pages every 10 seconds and a 20-second resume delay, reading the app's log: with the option on, no change while the page was touched every 2 seconds for 30 seconds, then the first change 28 seconds after the last touch and the next one 10 seconds later; with it off, a change every 10 seconds, touches or not. Found while testing, not changed: the WebView does not always request the page again when the rotation switches back to one it has shown, so a server log is not a reliable way to count the changes. - ๐๏ธ Media Playlist: Remove all (#269). Settings > General > Media Playlist has a Remove all button, shown once the playlist holds two items or more, that empties it after one confirmation, where a 300-item playlist used to take 300 taps. Files copied to the device are now deleted when you save, not when you remove them: removing a local item used to delete its file at once, so leaving the settings without saving kept a playlist that pointed at a file already gone. Removing one item or all of them now only changes the list, and Save deletes the copied files the playlist no longer holds. An empty playlist still cannot be saved in Media mode, so the flow is: remove all, add the new items, save. Checked on an Android 15 emulator with a playlist of two copied files and one URL: Cancel kept the three items; confirming emptied the list while both files and the saved playlist stayed untouched until Save; adding a URL and saving left only that URL in storage and deleted both files.
- ๐ณ Web pages can vibrate the device (#194).
navigator.vibrate()returnedtruebut did nothing: Android's WebView needs the app to hold theVIBRATEpermission, which FreeKiosk did not declare (Chromium logs Failed to use vibrate API, requires VIBRATE permission). It is declared now, a normal permission granted at install, with no prompt and no setting. As in any browser, Chromium only lets a page vibrate after the user touched it or pressed a key, so a scan from a keyboard-wedge scanner counts but a message arriving over the network does not. Checked on an Android 15 emulator with a page that callsnavigator.vibrate([300, 100, 300])on a tap: before, nothing reached the vibrator and Chromium logged the missing permission; after, Android's vibrator history shows the two 300 ms pulses from FreeKiosk. Not checked on a physical device (the emulator does not vibrate). - ๐งพ Silent Printing can take
window.print(): a page you cannot change prints on the receipt printer with no dialog (docs/silent-printing.md). Settings > General > Printing > Silent Printing > Send window.print() to this printer, off by default. Until now the receipt printer only answeredwindow.FreeKiosk.silentPrinter.print(), so a hosted service that prints withwindow.print()(a ticket kiosk, in the support request behind this) could only open the Android print dialog, which nobody taps on an unattended kiosk. With the option on, everywindow.print()of the page goes to the receipt printer instead, whatever Enable Window Printing says, through the same path assilentPrinter.print(): same print stylesheet, same width, same Restrict printing by origin list. It needs Enable Silent Printing and does nothing without it. It is part of the device backup and of the config synced with FreeKiosk Cloud (general.silentPrintWindowPrint, additive: a config that does not carry it leaves the device's value alone), and the cloud's config editor has the switch too (RushB-fr/freekiosk-cloud,feat/silent-print-window-print). Checked on an Android 15 emulator: with the option on and Window Printing off,window.print()and a hidden frame'sprint()reached the silent printer (NO_PRINTER: No USB printer attached, the emulator has none) and opened no dialog; with the option off,window.print()opened the dialog as before; with the option on and Silent Printing off, nothing changed. The settings screen shows the switch under Enable Silent Printing with the stored value. The help text under Silent Printing, which saidwindow.print()was not affected, now describes the option. Not checked: a real ESC/POS printer (so what lands on paper from awindow.print()page), the hosted page of the request, and saving the switch from the settings screen (the emulator first asked for a PIN to be set). - ๐ Motion can wake the screen only for a face. Settings > Display > Screensaver > Motion Detection > Only wake for a face, off by default and shown only when motion detection is on. It uses Android's built-in
android.media.FaceDetector(no Google Play Services, no new dependency), which finds upright frontal faces; it tries the four image rotations, the last successful one first, since the camera image may not be upright. Motion opens a 10-second window and a frame with a face inside it triggers, so someone who walks up and then stands still still wakes the screen, while a face on a poster alone does not, since nothing moves (any motion in the room, such as a light change, can still open the window). Face detection runs only inside that window, and the camera's warm-up frames after each start do not open it. It applies to waking from the screensaver, the motion check before the screensaver starts, and the MQTT/RESTmotionDetectedstate; movement measured on a running camera stream has no face check and does not wake while the option is on. It is part of the device backup and of the config synced with FreeKiosk Cloud (display.motionDetection.requireFace, additive: a config that does not carry it leaves the device's value alone); the cloud's config editor does not have the switch yet. Labels are translated in all 7 app languages. - ๐ MQTT can switch auto-brightness. A new switch
.../set/auto_brightness(ON/OFF), also announced to Home Assistant as Auto Brightness, does what REST'sPOST /api/autoBrightness/enableand/disablealready did. MQTT could readautoBrightness.enabledin the state but not change it.ONkeeps the minimum and maximum set in Settings > Display, where REST without a body resets them to 10 and 100; REST is unchanged. Covered by a unit test of the command (stored levels kept, 10/100 when none are stored, sent levels and 0 used as given). Not checked on a device yet.
Changed
- ๐ The WebView's loading overlay follows the device's dark theme. It was always white with a blue spinner, so a dark dashboard flashed white on every load. With the device set to dark (the system dark theme, or
adb shell cmd uimode night yeson a device without a switch for it), it is now black with a light spinner, text and settings button; on a light device nothing changes. The app window behind it already followed the dark theme throughTheme.AppCompat.DayNight. Checked on an Amazon Fire HD 8 (Android 9): in dark mode, a restart of the app and a reboot showed no white screen between the boot screen and the page; in light mode the app window was white before the page, as before. Not caught on screen: the overlay itself, which was gone before a screenshot could be taken on this tablet, so its dark colours are checked by the type check only. - ๐ The language choice moved to the bottom of Settings > General (#305). It was the first section, and with seven languages its list pushed Display Mode, the setting most people open the tab for, below the fold. It now comes last, just above Back to Kiosk; it still applies as soon as a language is picked. Checked on an Android 15 emulator: the tab opens on Display Mode, and Language is the last section.
Fixed
- ๐พ Settings can be saved when the screensaver delay is not a whole number of minutes. The delay is stored in milliseconds and edited in minutes. A value set by ADB or FreeKiosk Cloud that the field cannot show (10 or 90 seconds) was rounded down when the settings opened: 10 s showed as 0, and Save then refused everything with Please enter a valid inactivity delay, even with the screensaver section hidden; 90 s showed as 1 and, by the code, was quietly saved back as 60 s. A field you did not touch is now saved back as it was, and it shows 0.17 or 1.5 minutes instead of 0 or 1. Typing a value still takes a whole number of minutes, 1 or more. Checked on an Android 15 tablet emulator: with 10 s and with 90 s stored, a brightness change and Save worked and left the delay at 10000 and 90000 ms (before, the first was refused); unit tests cover the typed cases. Not checked: typing a new delay into the field on a device.
- ๐บ Android TV: a key of the remote wakes the screensaver. A TV has no touch screen, so once the screensaver was on, only a REST or MQTT
wakecould end it. Any key of the remote (D-pad, OK, Enter, Back, Menu, letters) now wakes it, except volume, power and Home, and the key is not also delivered to the page behind the screensaver, so an OK does not click a button there; the next key goes through as usual. Five presses of Back still open the PIN screen, with or without the screensaver, and the PIN screen and Settings keep their keys. Phones and tablets are unchanged. Checked on an Android 16 TV emulator (not Device Owner): before, D-pad down, OK, Enter, Menu, a letter and Back left the screensaver on and the page received the keys; after, each woke it at once and none reached the page, the next key did, the volume keys did not wake it, the screensaver came back after about 8 s of idle (delay 10 s), five Backs opened the PIN screen with the screensaver showing, and the PIN screen and Settings answered the D-pad as before. On an Android 15 tablet emulator, keyboard keys still do not wake it and a tap does. Not checked: a real TV remote, and a TV that is Device Owner. - ๐๏ธ Android TV: a slider moved with the D-pad is kept when you save. The value of a settings slider only reached the settings screen when a finger lifted off it, which a D-pad never does: Fade Out showed 20 s, Save said Configuration saved, and the old value came back on reopening. A value that has stopped changing for 0.4 s is now taken too; lifting a finger still applies it at once, and a change still waiting when the slider goes away is handed over. Checked on an Android 16 TV emulator: Fade Out (10 s to 20 s) and page zoom (100 % to 150 %) were lost on reopening before the change and kept after it; on an Android 15 tablet emulator, a touch drag of the brightness slider still saves (0.78). Known limit: a Save pressed within about 0.2 s of the last D-pad press still loses that last change (measured with a burst of keys that no remote produces: reaching Save takes several presses); in one burst of ten presses a few were also dropped, which was not investigated. Not checked: a real remote.
- ๐ Setting the PIN or a key over ADB no longer writes it in clear in the log. The ADB config printed every pending value, so
--es pinand--es rest_api_keyshowed inlogcat(tagFreeKiosk-ADB,Pending: @kiosk_pin = 1234), next to a line that masked the same value. The PIN, the REST API key, the MQTT credentials and the cloud token now show as***. Found while testing 2.1.0-beta.2: on an Android 15 emulator,--es pin 1234 --es rest_api_key โฆlogged both in clear before the change and***after, on a Device Owner tablet and on one without. A device that was configured this way with an earlier version still has the values in its log buffer until it is overwritten. - ๐ A page that scrolls itself no longer wakes the screensaver. The WebView reports every scroll and click on the page so the inactivity timer can restart, and while the screensaver was on, that report woke it, though the screensaver covers the page and nobody could have touched it. Since #344 the Return to Start Page runs behind the screensaver, and its Scroll to top on a page already at the start address is exactly such a scroll: the screensaver went off by itself. A page that scrolls or clicks on its own (a news ticker, a carousel) did the same. While the screensaver is on, these reports from the page are now ignored; a tap on the screen wakes it as before, as do motion, proximity and a REST/MQTT
wake. Covered by a test of the Kiosk screen (a page scroll keeps the screensaver on, a tap wakes it). - โฑ๏ธ Return to Start Page also fires while the screensaver is on (#344). With the screensaver due before the return (say after 1 and 5 minutes), the return never came: the timer stopped while the screensaver was on and started again from zero when it went off, so the next user woke the screensaver to the previous user's page. The timer now counts from the last touch on the page whether the screensaver is on or not, and goes back to the start page (or the dashboard grid) behind it. Waking the screensaver, by touch, motion, proximity or a REST/MQTT
wake, does not count as a touch on the page. Leaving for the PIN page or Settings still stops the timer, and coming back starts it again. - ๐ REST: the
/api/autoBrightnessendpoints of the docs now exist (#346).POST /api/autoBrightness/enable,POST /api/autoBrightness/disableandGET /api/autoBrightnessare indocs/rest-api.mdbut the server had no route for them and answered 404. They now work as documented:enableanddisableneed Allow Remote Control,GETdoes not; a level or offset that is not a number from 0 to 100 is refused with 400 and changes nothing; a body without levels uses the documented 10 and 100. Theoffsetofenablewas also dropped before it reached the app: it now arrives, and without one the current offset is kept. Checked on an Android 15 emulator with the REST API on:GETreturned the stored levels;enablewith{"min":5,"max":60,"offset":10}stored 0.05, 0.6 and 0.1 and switched it on;max: 500got a 400 and left everything as it was;disableswitched it off;enablewith no body gave 10 and 100 and kept the offset; a wrong key was refused. Covered by the first unit test of the REST server (KioskHttpServerAutoBrightnessTest, 7 cases, all failing without the change). Not checked on a device: Allow Remote Control off (unit test only). - ๐ REST and MQTT report the auto-brightness minimum and maximum again (#341). They always read 0 and 1: the app keeps both levels as 0-1 and passed them on that way, and the REST server and the MQTT client read them as integers, so 0.15 became 0 and 1.0 became 1. They are now passed as 0-100, as
brightnessalready was and asPOST /api/autoBrightness/enabletakes them, so the status shows 15 and 80 for levels of 15 % and 80 %. Not checked on a device yet. - โ๏ธ Cloud enrollment now fails clearly when the Android Keystore cannot hold the credentials, instead of a success that left the device offline (#321). On some boards (reported on a Rockchip RK3568) the Keystore refuses the write.
saveCloudCredentials()swallowed the error and enrollment reported success: the enrollment token was spent, the cloud showed a device that never came online and counted toward the device limit, and the kiosk settings and PIN had been wiped for nothing. The credentials are now stored first and read back; if they do not hold, enrollment stops with This device could not store its cloud credentials in the Android Keystore. Enrollment was cancelled., tells the cloud with the key it holds in memory (POST /api/v1/devices/{id}/unenroll/) so the key is revoked, and leaves the local settings as they were. The credentials are never written outside the Keystore: a device API key in app storage would be a bearer token anyone with a backup or root could replay (the same rule as the MQTT password, #258). Checked with unit tests: a refused write and a write that does not read back both fail the enrollment, call the unenroll endpoint with the key and leave the settings in place, and a normal enrollment is unchanged. Not checked on a device whose Keystore refuses the write. Based on the diagnosis and pull request #322 by @Josh2111. - ๐๏ธ Speech recognition works again in web pages on Android 11 and later (#313). The WebView's Web Speech API (
SpeechRecognition) binds the system speech recognition service, which an app targeting Android 11 or later only sees once it declares it: FreeKiosk now listsandroid.speech.RecognitionServicein its package visibility queries. Checked that the declaration is in the built APK; dictation in a page was not tried. Contributed by @henrikekblad. - ๐ Screen pinning: the Android status bar and gesture handle no longer stay on screen after a pull-down. With Lock Mode on and no Device Owner (screen pinning), pulling down on the transient status bar is blocked, which cancels the timer that hides transient bars; on some devices nothing restarts it, so the bars stayed over the page until the app restarted. Re-applying immersive mode cannot clear them (the app already requests them hidden), and the app cannot see them in its insets. A swipe that starts at the top or bottom edge and moves inward now schedules a clean-up 4 s and 10 s later that asks for the bars to be shown and hidden in the same frame, which ends the transient state. Dialogs, the power menu and print jobs keep their existing handling. Android 11+. Contributed by @richiela.
- ๐ท Motion detection no longer restarts whenever the Kiosk screen renders (#332). KioskScreen hands the motion detector a new callback on every render, and a new callback restarted detection: the native reference frame was reset and the 1.5-second start delay and the camera warm-up ran again. The detector now reads the callback through a ref and keeps running. One restart was doing useful work: the one when the screensaver comes on. The screensaver dims the screen, and in a dark room the camera sees that as movement (on an Amazon Fire HD 8, 46 % of the image changed two seconds after activation), so the restart that took the dimmed frame as the new reference kept the screensaver from waking itself. That is now done on purpose: the detector takes a new reference frame and skips its warm-up frames whenever the screensaver turns on or off, without the start delay. Checked with Jest tests that fail without the change (a new callback caused two native resets; a screensaver change caused none and the dimmed frame woke it).
- โฒ๏ธ The screensaver timer no longer runs behind the PIN page or after the Kiosk screen is gone (#334). The
clickthat follows the last tap of the tap-to-settings sequence reached the Kiosk screen after it had opened the PIN page, and armed the inactivity timer again; and nothing cleared that timer when Settings returned with a reset, which unmounts the Kiosk screen. So a timer fired one minute after the tap on a screen that no longer existed (logcat showed a motion pre-check with no camera), and while the Kiosk screen was still under the PIN page it could start the screensaver there. The timer is now only armed while the Kiosk screen is in front, is cleared when it unmounts, and is armed again when the screen comes back in front, which the back button on the PIN page did not do before. Checked with a Jest test that fails without the change (a timer firing after unmount, one armed by a page interaction behind the PIN page, none after coming back). Not checked on a device with the change. - ๐ The screensaver comes back after a REST/MQTT wake (#336). The handlers for the
wakeandscreen_oncommands, and for the screen turning back on during the screensaver, are registered once when the Kiosk screen mounts, and kept the timer function of that first moment, when the screensaver still read as off. They cleared the inactivity timer and started none, so after a remote wake the screensaver did not return until someone touched the screen. They now use the current one. Checked with a Jest test that fails without the change (no expiry afterwake, afterscreen_on, or after the screen turned on). Not checked on a device with the change. - ๐ Auto Wake on Screen Off works again on a Samsung with notifications off: FreeKiosk now asks for the notification permission (#317). On a Galaxy Tab A11+ 5G (Android 16) a short press of the Power button left the screen off with Auto Wake on. The reporter's log showed Samsung's power manager cancelling FreeKiosk's wake lock (
Screen__On : Cancel (notifications are disabled : com.freekiosk)); after they turned the app's notifications on, the lock was honoured and the screen came back. FreeKiosk declaredPOST_NOTIFICATIONSbut never asked for it, so on Android 13 and later its notifications stayed off. It now grants the permission under Device Owner without a prompt, like camera and location, and otherwise asks for it together with the other runtime permissions (location, Bluetooth, Wi-Fi, camera) in a single request: Android keeps one pending request per screen, so the separate requests hid the notification prompt until several launches later. The other wake-ups that use the same kind of wake lock (the screen scheduler, a remote wake) may have been affected the same way; that is not verified. Two smaller fixes in the same area: the other half of auto-wake (setTurnScreenOn, keyguard, brightness) never ran, because it reached the activity throughReactNativeHost, which throws under the New Architecture, so the receiver now holds the activity and, if the screen is still off half a second after the wake lock, brings FreeKiosk to the front, the path Android recommends; the keyboard is dismissed again when the screen goes off for the same reason. Checked on an Android 15 tablet emulator (Device Owner): with the permission revoked, the previous build still had it denied after a restart and this one had it granted; the bring-to-front fallback was checked there with the permission change forced on (am compat enable 216114297): before, a Power press left the screen off; after, it came back within half a second, three times out of three. On an Android 15 emulator without Device Owner, one launch showed the four dialogs in a row (camera, location, nearby devices, notifications), where asking one by one needed five launches to reach the notification one; denying location still left the others to be shown. Not checked: the reporter's Samsung with this build (they confirmed that turning notifications on by hand fixes it), External App mode, and a secure lock screen. - ๐ A page answered with a 4xx status is now shown, as a browser would (#327). Any HTTP error on the main page replaced it with FreeKiosk's error screen, so a page served with a 403 or a 404 never showed its own content, and a site behind Cloudflare's anti-bot check, served as a 403, could never run it. A 4xx page now renders and runs its scripts; a 5xx and a network error still show the error screen. Changed: with Auto Reload on, a 4xx used to be retried every 5 seconds; it now stays on screen. Checked on an Android 15 emulator with Auto Reload on: a 403 and a 404 page rendered and ran their script, with one request each instead of one every 5 seconds; a 503 still showed the error screen and was retried; the five taps still opened the PIN on a rendered 403 page. Not checked against a real Cloudflare challenge. Contributed by @JohnnYoru.
- ๐ก MQTT: the device state is published again after a reconnect (#328). Three paths schedule a CONNECT:
connect(), the backoff after a lost connection, and the WiFi callback, which also fires right after it is registered. When two of them overlapped, the second CONNECT reached a client that was already connecting or connected; HiveMQ failed it, and the failure handler set_isConnected = falseon the live connection. From then on the periodic state stopped and every immediate publish was skipped (Not connected, skipping status publish), while theset/#subscription stayed, so commands still worked and the controller kept showing a frozen state.sendConnect()now sends a CONNECT only while the client is disconnected, and a failed CONNECT no longer marks a connected client as down. Seen on an Amazon Fire HD 8 (KFTRPWI, Android 9) with 2.1.0-beta.1 against an aedes broker: after a reconnect exactly one state arrived and then none for several minutes, while the REST API showed the screensaver going on and off, and an MQTT brightness command was carried out with no state following. Checked on an Android 15 emulator against an aedes 1.2 broker, with WiFi and mobile data off for 40 s and then WiFi on: before the change the client reconnected through the WiFi callback, the backoff CONNECT still pending failed 13 s later with MQTT client is already connected or connecting, and no state came for the next 100 s while availability stayedonline, aset/brightnessbeing carried out with no state after it; after it, that CONNECT is skipped (CONNECT skipped, client is CONNECTED), the state keeps coming and a command is followed by a state. A broker stopped for 20 s while WiFi stayed up still reconnects on the backoff. Not checked: a physical device, and a Fire OS tablet. - ๐ Setting the REST API key over ADB now replaces the key, and no longer leaves it in plain text (#325).
--es rest_api_keywent to AsyncStorage while the server reads its key from the Keychain, and falls back to AsyncStorage only when the Keychain holds none. So on a device that already had a key, the ADB value was never used, the old key kept working, and the new one stayed readable in the app's storage. It now goes to the Keychain like the PIN and the MQTT password, and the plain-text copy is cleared; a key that cannot be stored keeps the pending config for a retry, as for the other secrets (#258). Checked on an Android 15 emulator whose Keychain already held a key: before, the ADB key was refused (401); after, two rotations in a row each made the new key work (200) and the previous one fail (401), with no@kiosk_rest_api_keyleft in AsyncStorage, and the PIN passed in the same command was still stored. Not checked with a Keystore that refuses the key. - ๐ท The REST API photo, the camera stream and MQTT camera snapshots now work while another app is in front (reported from FreeKiosk Cloud). Since Android 11, an app that is not on screen can use the camera only from a foreground service declared for the camera. FreeKiosk had none, so
/api/camera/photoanswered 503 (Camera not available) and the stream failed as soon as an external app was shown, or the screen was off. A camera-type foreground service now runs while the REST API or MQTT camera snapshots are on and the camera permission is granted. It is started while FreeKiosk is in front (on resume, and on pause, the moment another app comes in front) and only holds the type: it never opens the camera, so motion detection and Android's camera indicator are unchanged. It shows a silent, minimised notification (Remote camera available). If Android refuses the camera type (permission withdrawn in between), the service falls back and stops, and the camera works only with FreeKiosk in front, as before. Not in Play Store builds (-Pplaystore): Play reviews that service type, so the permission and the service are stripped there and nothing changes; the QR provisioning build keeps it. Checked on an Android 15 tablet emulator: with 2.1.0-beta.1, after 20 s behind Android Settings, photos from both cameras answered 503 (Camera "1" disabled by policy); with the change both returned a JPEG, the MJPEG stream sent 27 frames in 6 s, and a photo worked with the screen off; with the REST API and MQTT snapshots off no service runs and no notification shows; a setting changed inside the app is applied when another app comes in front, both ways; without the camera permission no service starts, nothing crashes and the API answers 503 as before, also when the service is forced to start. The manifests of the default, Play Store and QR provisioning builds were checked for what they declare. On an Android 7 emulator the service starts and nothing crashes; the photo itself was not exercised there. Not checked: Android 11 to 14 (the report is on Android 14), a physical device, a camera already in use by the app in front (that app keeps priority), and MQTT snapshots end to end (only the service start). - โฌ
๏ธ Back no longer leaves the kiosk on Android 13 to 15, so the page is no longer reloaded (#324, #246, part of #221). Since 1.2.3 the manifest opted into Android's new Back dispatch (
enableOnBackInvokedCallback), but below Android 16 nothing registered for it (React Native only adds its own callback on Android 16), so Android itself finished the activity on the Back key or the back gesture. The kiosk screen's Back handler, which swallows Back, was never reached, and FreeKiosk came back with its start page loaded again, which is what loses the state of a web app. The flag is removed: Android 13 to 15 send Back toonBackPressed()as Android 12 and earlier do, and Android 16 keeps React Native's callback. Checked on an Android 15 tablet emulator in WebView mode, not locked: before the change, Back closed FreeKiosk to the home screen; after it, the Back key and the edge back gesture (gesture navigation) left FreeKiosk in front with the page as it was (same load time, in-page state kept), and Back still closes the PIN screen. Not checked: Lock Mode with Device Owner, Android 13, 14 and 16, and a physical tablet. Not changed: External App mode, where Back goes to the other app and FreeKiosk never sees it (#221 stays open for that). - ๐จ๏ธ Printing a hidden frame now prints, the way react-to-print and print-js do it (#283). These libraries copy what to print into a hidden
<iframe>and call that frame'sprint(), which Android's WebView ignores: nothing happened, no dialog and no request reached the app, since FreeKiosk only intercepted the main page'swindow.print(). A same-origin frame'sprint()is now caught, when the page reads the frame'scontentWindow(how the libraries reach it) and once a frame has loaded (a print button inside the frame), and the frame's styles and content are copied into the page for the time of the print, with the values typed in its fields and the drawing of its canvases; images get up to 3 seconds to load. The page is then printed through the Android dialog, or through the receipt printer when Silent Printing takeswindow.print(). The copy stays while the dialog is open, since the dialog renders the page again each time its options change, and goes once the print job is over: the app asks the native print module every second (PrintModule.isPrintActive, bounded to two minutes as before). A cross-origin frame still does not print, and the page's own print stylesheet still applies to the copy. Checked on an Android 15 emulator with a test page that prints as react-to-print does (hidden frame, content written into it,contentWindow.print(), frame removed right after): the dialog preview showed the frame's content in its own style with the typed value, and none of the page; the copy was gone within 2 seconds of closing the dialog;window.print()of the page still printed the page. Not checked: react-to-print and print-js themselves (only their mechanism), and a page whose print stylesheet hides everything but one of its elements. - ๐ REST API: a request that follows one with a body no longer fails on a kept-alive connection (#315). The server keeps connections open but never skipped the body of a request that its endpoint did not read (
POST /api/screen/onwith{}, a request refused for its API key...), so the next request on that connection was read from the middle of it: HTTP 400, a dropped connection or garbage. Home Assistant'srest_commandreuses its connection, so ascreen/onfollowed by abrightnessfailed on the second call. The body is now read once, after the key check, whatever the endpoint does with it, and a request refused for its API key getsConnection: close. Reproduced on an Android 15 emulator (screen/onwith a body thenbrightness:[200, 400]; a UTF-8 body:IncompleteRead; with a wrong key:[401, 400]) and checked after the change: every sequence answers as it should, 60 mixed requests on one connection, 6 clients at once, accents and CJK characters kept in the body, a 60 KB body, andOPTIONS,404and405unchanged. Not tried on the Rockchip RK3588 tablet of the report. - ๐ REST API:
GET /api/brightnessno longer reports the brightness from before the last change (#314). The kiosk screen reads the brightness back right after setting it, but the window brightness is applied on the UI thread a moment after the call returns. On a slow tablet the read ran first and saw the previous value, which then stayed until the next change (set 80, read 20). A brightness that was requested and not yet applied is now what the read returns. This depends on how busy the UI thread is, so it does not show by itself on an emulator: with the UI thread held for 400 ms before each change, 2.1.0-beta.1 answered20 -> 42, 80 -> 20, 35 -> 10, 90 -> 35(always one behind, even after 2 s) and the fixed build answers20, 80, 35, 90; without the hold both answer correctly. The volume is not stale:set 30, read 26is a rounding (on the emulator the music stream has 15 steps, and 4 of 15 is 26%), the level the system really holds. Not tried on the Rockchip RK3588 tablet of the report. - ๐ PDF Viewer: a PDF saved the way FileSaver.js does it now opens in the viewer (#311, follow-up to #295). The #295 hooks only saw a download started by a click on a link or by
window.open. A save that dispatches the click on a detached link (what FileSaver.js does, and many sites with it) went to the native download listener, which gave the viewer theblob:address: unreadable from another page, hence Cannot Load PDF ... blob:.... The listener now asks the page, which is still alive, for the bytes, and the app opens them as for #295. A PDF is also kept for a minute after the page revokes its address, since the listener only asks a moment later. Reproduced then checked on an Android 15 emulator with a test page: a detached link (FileSaver style), the same with the address revoked in the same instant, and a blob created in a frame (not seen by the hooks) now open in the viewer, where the first two ended in the error; a link click,window.openand an ordinary http PDF behave as before, and a blob that is not a PDF is still left alone. Not checked: the site of the report (only the mechanism was reproduced, on a test page) and the Lenovo TB-8505F. Not changed: closing the viewer reloads the previous page, which on a single-page app that does not load directly (such as/examen-blanc) can land on a broken page. - โฐ The screen schedule's wake no longer fails on tablets that do not allow exact alarms (#307, a regression of 2.1.0-beta.1). 2.1.0-beta.1 set the wake as an exact alarm, and said it needed no extra permission: that was wrong. Android refuses it to an app without the permission (
Caller com.freekiosk needs to hold SCHEDULE_EXACT_ALARM or USE_EXACT_ALARM, seen on a Galaxy Tab A11+), so the wake alarm was not set at all and the screen stayed off. The app now declaresUSE_EXACT_ALARM(granted at install on Android 13+, andSCHEDULE_EXACT_ALARMon Android 12), checks that exact alarms are allowed, and falls back to the previous inexact alarm when they are not or when the system refuses anyway, so a wake is never lost. Play Store builds do not declare the permission (Play restricts it to alarm and calendar apps) and keep the inexact alarm, which can be late by up to an hour. Checked on an Android 15 emulator: with the permission, and the battery exemption removed, the alarm window is 0 (exactAllowReason=policy_permission) and the wake fired at 23:49:00.021 for 23:49:00; in a Play Store build without the exemption, the app logs the fallback, sets the inexact alarm (window of 1 h) and it fires; turning the schedule off cancels the exact alarm. Not checked on the Galaxy Tab of the report, on Android 12, or over a real night.