Use

Update the kiosk

On this page

This page is for the person who builds and sends new versions of the kiosk app. When you finish, you can put a new kiosk build on a frame, roll it out to the rest, and recover when a build goes wrong, even when the frame is hundreds of miles away.

You install the first kiosk on a frame over ADB. After that, the frame can update itself from a release you upload to your server. The server only stores the file and tells each frame which build it should have. The frame decides whether to install it.

The ways to update a frame

Method Use it for Needs Section
ADB, first install A frame that has no kiosk yet. USB or network ADB. Provisioning, step 7
ADB, manual update A frame next to you, or one that cannot self-update. ADB, the same keystore. Update over ADB
Admin upload The normal way. The admin, a built APK and its .sig. Upload in the admin
Shell on the server A server where you have a shell, or for scripting. bin/kiosk-release.php. Add a release from the shell

The admin upload and the shell command both store a release. You then choose which frame gets it in the admin.

How an update travels

sequenceDiagram
    autonumber
    participant You as You (offline)
    participant Admin as Admin + server
    participant Frame as Frame (kiosk)
    participant Root as Root watchdog
    You->>You: Build APK, sign with APK keystore
    You->>You: Sign hash + version code with offline release key
    You->>Admin: Upload APK and .sig (password re-check)
    You->>Admin: Target ONE frame first
    Admin->>Frame: Offer release
    Frame->>Frame: Verify release signature (key baked into app)
    Frame->>Frame: Verify SHA-256, size, plain ZIP (Janus-safe)
    Frame->>Frame: Verify package, higher version, same signing certificate
    Frame->>Frame: Wait until the screen is quiet
    Frame->>Frame: Install silently
    Frame->>Admin: Report running new build
    Admin->>Admin: Mark release "Proven"
    You->>Admin: Roll out to all
    Note over Frame,Root: If the new build is not healthy after about 15 minutes
    Root->>Frame: Reinstall last known good build
    Frame->>Admin: Report the failed release ("unhealthy")

Four things keep this safe to send and safe to get wrong:

  1. Two signatures. Android checks the APK certificate. The frame also checks a second, separate signature from an offline ECDSA key that never touches the server. A stolen admin password cannot push a build you did not sign.
  2. The frame decides. It refuses downgrades, other packages, mismatched certificates, and files with anything glued in front of the ZIP.
  3. Staged rollout. A release is marked Proven only after a frame has downloaded it and reported running it. Only then can you roll it out to everyone.
  4. A root watchdog. If a new build never becomes healthy, the frame reinstalls the last one that was, on its own. A crashing build costs roughly 15 minutes.

Two keys, two jobs

APK keystore Release key
What it is An RSA key in a Java keystore, default ~/.android/frame-kiosk.keystore An EC P-256 key as a PEM file, default ~/.android/frame-release-key.pem
What it signs The kiosk app (the APK) The text <sha256>:<versionCode> of each build
Who checks it Android, when installing an update The kiosk, from the public key baked into the app. The server too, if you set kiosk_release_pubkey.
Lose it, and Every frame needs the kiosk uninstalled and installed again Every frame needs one more adb install with a new public key
Keep it Backed up, private, off the server Backed up, private, offline, off the server

Make both before your first build. The commands are in Provisioning, step 5, and the .env keys are KEYSTORE, KEYSTORE_PASS, RELEASE_KEY and RELEASE_PUBKEY.

The release signature is ECDSA with SHA-256 over the ASCII text <sha256 hex of the APK>:<versionCode>, for example ab12...ef:15. The .sig file holds the base64 of its DER encoding.

To let the server reject bad signatures early, put the public key in config.php:

php
'kiosk_release_pubkey' => "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n",

Without it the server stores the signature as given, and only the frame checks it.

Build a release

  1. Raise the version code. Every release needs a higher number than the last. Either edit android:versionCode in kiosk/AndroidManifest.xml, or pass it as an option.

  2. Build:

    cd kiosk
    lando build-kiosk -- --version-code 16 --version-name 16
    

The build prints:

built kiosk/build/frame-kiosk.apk
versionCode 16
sha256      <64 hex characters>
signature   kiosk/build/frame-kiosk.apk.sig (upload with the APK)

You now have two files in kiosk/build/: frame-kiosk.apk and frame-kiosk.apk.sig. Warnings that begin WARNING: mean a release key setting is missing, and the build then cannot self-update. Provisioning, step 6 lists every build option.

Optional: check the signature yourself with OpenSSL. This is standard OpenSSL and is not part of the project's scripts:

printf '%s' '<sha256 from the build>:16' > msg.txt
base64 -d kiosk/build/frame-kiosk.apk.sig > sig.der
openssl dgst -sha256 -verify ~/.android/frame-release-pub.pem -signature sig.der msg.txt

It prints Verified OK.

Rules for every release

  • A higher version code than the last. Android and the kiosk both refuse a lower one. To go back, see Roll back by rebuilding.
  • Data compatibility. A rollback keeps the app's saved data. A release must not write preferences or files that the previous release cannot read, or a rollback will crash.
  • Sign it with the same keystore. A different key makes the frame refuse the update (cert).
  • Never upload a test build. See Keystore and release key hygiene.

Update over ADB

For a frame next to you, or one that cannot self-update, install a newly built APK over the old one. It must be signed with the same keystore:

adb install -r kiosk/build/frame-kiosk.apk

Use -s <serial> to pick a device. This keeps the app's data and the device owner setting. Use the same command when you change the baked-in release public key, KIOSK_PIN or FRAME_SERVER_URL, because each is built into the app.

A frame updated over ADB has no .sig check. Nothing but you and Android is involved.

Upload in the admin

  1. In the admin, select Library.
  2. Under Kiosk app, select Upload release.
  3. Choose frame-kiosk.apk as the APK file (up to 16 MiB).
  4. Choose frame-kiosk.apk.sig as the Release signature.
  5. Add Notes (optional) if you like, up to 500 characters.
  6. Select Upload. If you have not entered your password in the last five minutes, the admin asks for it.

You see "Release uploaded". The table lists the release with its size, who uploaded it, how many frames run it, and whether it is Proven or Not yet proven.

The server checks the file before it keeps it:

  • It is a plain ZIP, with nothing hidden before or after the entries (Janus-safe).
  • Its package is com.calevans.frame.
  • Its version code is a number from 1 to 2,100,000,000.
  • If you set kiosk_release_pubkey, the signature verifies.
  • No release with the same file hash or version code already exists.

The server does not check the APK's signing certificate. The frame does.

Uploaded builds are stored in var/kiosk/, named by their SHA-256 hash. Back the folder up with var/frames.db.

If the upload fails, the box shows the reason. A 413 means a size limit. See Troubleshooting.

Add a release from the shell

On the server, you can add a release without the admin. Run it as the web user (it refuses root), from the folder that holds app/:

sudo -u www-data php app/bin/kiosk-release.php add frame-kiosk.apk [frame-kiosk.apk.sig] [--notes "text"]
sudo -u www-data php app/bin/kiosk-release.php list
  • The signature file defaults to <apk>.sig.
  • --notes takes up to 500 characters.
  • The web user must be able to read both files. Copy them to a folder it can read first.
  • It makes the same checks as the admin upload, refuses symlinks and files that are not regular files, and stops at 16 MiB.
  • On success it prints version_code, version_name, sha256 and id.
  • Failures go to standard error with the admin's codes: duplicate_sha256, duplicate_version_code, not_apk and others.
  • The upload is logged as cli:<your login> (the SUDO_USER name when you use sudo).

list prints one line per release, with tabs between the columns: ID, version code, version name, SHA-256, size, upload time (UTC), uploaded by, and proven or unproven.

Choosing which frame gets the release is still done in the admin.

Choose which frame gets a release

Always target one frame first.

  1. Open the frame in the admin, then Details.
  2. Find the Kiosk version row. It shows the build the frame runs ("Build 15").
  3. In the list, choose the release. Each entry reads like 16 (build 16). No update cancels. It asks for your password.

You see "Update queued". The frame installs the release the next time its screen is quiet.

The row shows the state:

Pill Meaning
Up to date The frame runs the target, or newer.
Update pending The frame has been offered the release and has not installed it yet.
Waiting for a report The frame has not yet said which build it runs.
Update failed See Failure states.
No rollback watchdog The frame's boot script has no watchdog. See below.

A frame with no rollback watchdog can only be sent a release that has already run on another frame. The server refuses with "this frame has no rollback watchdog, so it can only take a release that already runs on another frame". Send the first copy to a frame that has the watchdog. Install the boot script on every frame before you use self-update on it.

What the frame checks

The frame installs a release only if all of these hold. Otherwise it holds a failure that the admin shows.

  • The detached release signature verifies against the public key baked into the app.
  • The file's SHA-256 and size match the offer, it is at most 32 MiB, and it is a plain ZIP.
  • It is the kiosk package. Its version code is the offered one and is higher than the installed one. A frame never downgrades itself.
  • Its signing certificates are exactly the installed ones.
  • Nobody is using the frame: the network is up, the PIN panel and Wi-Fi setup are closed, and the screen is dimmed (bedtime or Dim) or untouched for 30 minutes. The frame checks every 30 seconds.

The server lets each frame download a release at most 12 times an hour.

Staged rollout and proven releases

A release is Proven once a frame has downloaded it in full and then reported running that version or newer. Only a proven release can be rolled out to everyone.

  1. Target one frame (above).
  2. Wait. Check that its Kiosk version row shows Up to date, and look at the frame's Screen to see the dashboard is fine.
  3. In Library, under Kiosk app, the release now says Proven.
  4. Select Roll out to all on that release. It asks for your password.

The admin says how many frames will update, and how many are already on it or queued. The button is disabled before then, with the hint "Target one frame first and let it prove this release".

Roll out to all offers the release to every active frame that reports an older build, unless that frame is already pointed at this release or a newer one.

When you claim a new frame, the server points it at the newest release that has already run on a frame. If no release has run yet, the frame stays on the build you installed.

The rollback watchdog

The boot script device/frame-fixmac.sh carries the line # frame-watchdog: 1. The kiosk reports it, and the admin shows No rollback watchdog for frames without it.

How it works:

  1. The kiosk writes healthy (its version code) after it has been in front for 5 minutes without crashing, or after its first good server answer plus a loaded dashboard.
  2. Root copies the installed APK to /data/misc/frame/kiosk-good.apk (root-only) each time healthy equals the installed version. On the first boot with the script, the installed build is copied.
  3. If a new version was installed and is still not healthy after about 15 minutes, root reinstalls that copy with pm install -r -d, once.
  4. The admin shows Update failed with "the new build did not start properly and was rolled back".

A crashing build therefore costs you roughly 15 minutes, then the frame is back on the last build that worked.

Root reads only the digits of healthy and the to= line of files/update/state from the app's folder, ignores symlinks and anything else, and never writes into the app's folder. The server can therefore at most cause a rollback to the last build that ran healthy on that frame.

Failure states

When a frame cannot install a release, the Kiosk version row says Update failed and gives the reason.

Reason in the admin What it means What to do
the download failed The frame could not fetch the file. Check that the frame is online and var/kiosk/ is readable by the web user. Select Clear and retry.
the file was the wrong size The size does not match the offer. Delete the release and upload it again.
the file did not match its hash The stored file differs from the offer. Delete the release and upload it again.
the file is not a plain APK The ZIP has extra data in it. Rebuild and upload again.
the file is for another app The package is not the kiosk. Upload the right APK.
the file is not newer than the installed app The version code is not higher. Rebuild with a higher version code.
the file's version is not the one offered The APK's version code differs from the release record. Delete the release and upload again.
the signing certificate differs from the installed app The build was signed with a different keystore. Rebuild with the original keystore. If the original is lost, see Lost keystore.
the release signature did not verify The signature does not match, or the frame has a different public key baked in. Check RELEASE_KEY and RELEASE_PUBKEY. A frame built with another public key needs an adb install -r of a build with the right one.
the new build did not start properly and was rolled back The watchdog reinstalled the old build. Find out why the new build failed. Fix it, raise the version code, and release again.
the app is not the device owner The kiosk lost its device owner status. Redo Provisioning, step 7.
Android refused the install (status N) Android returned an install error. Read adb logcat on the frame.

When you have fixed the cause, select Clear and retry on the Kiosk version row. It asks for your password and says "Cleared. The frame will try again." Until then the frame does not try the same file again.

A failure belongs to one file. A new release with a different file is offered afresh.

Delete a release

In Library, under Kiosk app, select Delete on the release. It asks for your password, then removes the record and the file.

The admin refuses while a frame still targets the release: "a frame still targets this release; clear its target first". Set that frame's Kiosk version to No update first.

Roll back by rebuilding

Android and the kiosk both refuse a lower version code, so you cannot send an old build as it was. To go back, build the old code again with a higher number and release that:

git checkout <old commit> -- kiosk/src kiosk/res
cd kiosk && lando build-kiosk -- --version-code 21 --version-name 21-rollback

(KIOSK_VERSION_CODE and KIOSK_VERSION_NAME in the environment work too when you build without Lando.) The new APK has a new SHA-256 and its own signature file. Upload it, target one frame, and roll it out as usual.

Keystore and release key hygiene

  • Back up both keys somewhere private. Keep the release key off the server, out of the repository and out of any container image. Do not give either key to the server, to CI, or to anyone else.
  • Never sign a throwaway or test build with the real keys, and never upload one as a release. The only exception is the --test-crash build, used for the watchdog test on your own frame.
  • Lost the release key: every frame needs one more adb install -r of a build with a new public key.
  • Lost the keystore: you cannot update in place. On every frame you must undo device owner, uninstall the kiosk, and start again. See Provisioning.

After a watchdog test

A --test-crash build (lando build-kiosk -- --test-crash --version-code <N>) crashes on purpose, but only while root has run setprop debug.frame.testcrash 1. That property is cleared at reboot. With SELinux off, any app on the frame could set it, so the gate means only that a compromised server alone cannot trigger the crash.

The build is signed with your real keys, so retire it as soon as the test is done:

  1. Delete that release in the admin.
  2. Remove the build: rm -rf kiosk/build, and any copy of the crash APK and its .sig that you saved.
  3. Ship a real release with a higher version code to every frame. The crash build is then refused as not_newer everywhere. Do this before any frame leaves your control.

What the server logs

Uploads, targets, roll-outs and cleared failures go to the PHP error log, for example:

frame admin: kiosk release uploaded user=yourname ip=203.0.113.9 version_code=15 sha256=...
frame admin: kiosk target set user=yourname ip=203.0.113.9 frame=1a2b3c4d version_code=15 sha256=...
frame admin: kiosk rollout user=yourname ip=203.0.113.9 version_code=15 sha256=... frames=3
frame admin: kiosk failure cleared user=yourname ip=203.0.113.9 frame=1a2b3c4d sha256=... reason=hash

Other events: release deleted, target cleared, target set on claim, and upload rejected (with the reason).

Next: Troubleshooting and recovery.