Set up

Build the kiosk and set up a frame

On this page

This guide is for people whose PF1007L already has root ADB (see Unlock your PF1007L) and whose server is running (see Install the frame server). When you finish, the frame boots to your logo, then shows your dashboard full screen, and you have claimed it in the admin.

You do the setup once per frame. You build the kiosk app once per change.

Before you start

Warning

Keep the signing keystore forever. Updates to the kiosk install only if they are signed with the same key. If you lose the keystore, you must uninstall the kiosk (undo device owner first) on every frame and start again.

Warning

Make the release key before your first build. The key that approves self-updates is baked into the app, so a build made without it cannot self-update. See Step 5.

  • Root ADB has no password. Anyone who can reach port 5555 on the frame controls it. Set ADB_ALLOW in step 1.
  • Every write is yours to confirm. scripts/provision.sh prints what it will do and waits for y before the first write. Read it.
  • Use the tested firmware. Package names and file paths below come from the January 2026 firmware (AEEZO/astar_m86_8723cs:6.0.1/MOB30R/20260113). Check each one on other builds.

You need on your computer: adb, Docker and Lando for the included kiosk build (or a JDK and the Android build tools, see below), openssl, unzip and php. Run everything in a WSL shell, in the repository root.

The map

flowchart TD
    A["1. Fill in .env"] --> B["2. Get the files you provide"]
    B --> C["3. Connect to the frame over ADB"]
    C --> D["4. Prepare the system partition<br/>Wi-Fi ADB, WebView, certificate"]
    D --> E["5. Create your keys"]
    E --> F["6. Build the kiosk"]
    F --> G["7. Install the kiosk<br/>and set device owner"]
    G --> H["8. Set the clock source"]
    H --> I["9. Install the boot script"]
    I --> J["10. Boot logo and animation<br/>optional"]
    J --> K["11. Connect Wi-Fi and claim"]

Step 1: Fill in .env

Copy .env.example to .env in the repository root if you have not already:

cp .env.example .env
chmod 600 .env

.env holds your keystore password, so keep it private. Git ignores it.

The file is KEY=value, one per line. These rules come from the strict reader in scripts/lib/env.sh:

  • Blank lines and lines that start with # are allowed. Anything else must be KEY=value for a known key.
  • A value may be wrapped in one pair of single or double quotes. The quotes are removed.
  • There is no variable expansion, no backticks and no escapes.
  • A line with an unknown key stops the script with unknown key. A line with a Windows line ending stops it with control characters (CRLF line endings?).
  • KEYSTORE, RELEASE_KEY and RELEASE_PUBKEY may start with ~/.
Key Read by Needed What it does
FRAME_SERVER_URL kiosk/build.sh Always, for a build The address of your server. It must be HTTPS, end with a slash, and match frames_url in the server's config.php. Example: https://frames.example.com/frames/.
KIOSK_PIN kiosk/build.sh Always, for a build The factory PIN for the frame's on-screen panel, 4 to 8 digits. Only a salted hash goes into the app. You can set a different PIN per frame in the admin later.
KEYSTORE kiosk/build.sh Optional Path to the signing keystore. Default: ~/.android/frame-kiosk.keystore. Keep it under your home folder: Lando sees only that.
KEYSTORE_PASS kiosk/build.sh Always, for a build Password for the keystore and its key (both use the same one). Choose your own.
RELEASE_PUBKEY kiosk/build.sh For self-update Path to the public half of your release key. It is built into the app. Without it, the app refuses every self-update.
RELEASE_KEY kiosk/build.sh For self-update Path to the private half of your release key. The build uses it to write a .sig file next to the APK. Without it, no .sig is written.
OWNER_NAME kiosk/build.sh Optional Name shown on the pairing screen, in "Read this code to ...".
ADB_ALLOW scripts/provision.sh boot-script For the boot script IPv4 addresses or CIDR ranges that may reach ADB over Wi-Fi, separated by commas or spaces. Example: 192.0.2.20,198.51.100.0/28. Empty means no limit.
NTP_SERVER scripts/provision.sh time For time The time server the frame uses before it has a clock. Example: pool.ntp.org. Letters, digits, dots and hyphens only.
TZ scripts/provision.sh time For time The frame's time zone. Example: America/New_York.
USBIP_BUSID scripts/boot-ums.sh Unlocking only The USB/IP bus ID of the port the frame is plugged into. Not needed here.

ADB_ALLOW takes effect only when the boot script is installed (step 9). Include the address of the computer you use for ADB. USB ADB is never limited, so you can always recover over the cable.

Here is a filled-in example:

FRAME_SERVER_URL=https://frames.example.com/frames/
KIOSK_PIN=4821
KEYSTORE=~/.android/frame-kiosk.keystore
KEYSTORE_PASS=a-long-password-of-your-own
RELEASE_KEY=~/.android/frame-release-key.pem
RELEASE_PUBKEY=~/.android/frame-release-pub.pem
OWNER_NAME=Alex
ADB_ALLOW=192.0.2.20,198.51.100.0/28
NTP_SERVER=pool.ntp.org
TZ=America/New_York

Note

TZ in .env sets the time zone of the Android system on the frame. The time zone the admin uses for the schedule and photo pick time is a separate setting: Time zone in the frame's Schedule section. See Use the admin.

Step 2: Get the files you provide

The repository does not carry third-party binaries. Download these:

File Why Where to get it
WebView 106 APK (arm, 32-bit) The frame's stock WebView 44 is too old for modern pages. 106 is the newest that runs on Android 6. The LineageOS prebuilt WebView for ARM: LineageOS/android_external_chromium-webview_prebuilt_arm. The version tested is 106.0.5249.126, from the prebuilt commit 4b0ae494a7dd, with SHA-256 e2edc1f89ec1608b169da9e7ffc037190b70c7825f0be29694f4cae719980580. Check the hash, whatever source you use.
A launcher APK (optional) A second home app for maintenance. Nothing in this guide needs it, and the kiosk has no way to open it. Skip it and use ADB. Any launcher that runs on Android 6. The author tested Rootless Pixel Launcher (amirz.rootless.nexuslauncher) and does not provide a download.
ISRG Root X1 certificate Android 6.0.1 does not trust Let's Encrypt. Without this, a Let's Encrypt site fails to load on the frame. letsencrypt.org/certs/isrgrootx1.pem

Put the files in a folder outside the repository, for example ~/frame-files/. Check the WebView hash:

sha256sum ~/frame-files/webview.apk

The WebView prebuilts and the Let's Encrypt root keep their own licenses.

Step 3: Connect to the frame

Plug the frame into your computer and attach it to WSL (usbipd attach --wsl --busid <id> from PowerShell, as in the unlock guide). Then in WSL:

adb devices
adb shell id

You expect uid=0(root). All steps below work over USB ADB. After the kiosk is installed and the frame is on Wi-Fi (step 11), you can connect over the network instead:

adb connect 192.0.2.50:5555

If both USB and network connections are open, add -s <serial> to adb and to scripts/provision.sh.

Step 4: Prepare the system partition

Wrap every /system change in a read-write remount and a read-only remount, and keep a .bak of any file you replace.

Turn on ADB over Wi-Fi

adb shell 'mount -o remount,rw /system
cp -p /system/build.prop /system/build.prop.bak
echo service.adb.tcp.port=5555 >> /system/build.prop
mount -o remount,ro /system'

Disable the vendor apps

com.yhk.qeota is the over-the-air updater. Do it first, and never press "Check for updates" in the stock app: an update could overwrite /system and undo your work. com.DeviceTest and com.android.gl2jni are factory leftovers.

for p in com.yhk.qeota com.DeviceTest com.android.gl2jni; do
  adb shell "am force-stop $p; pm disable $p; pm hide $p"
done

Do not disable the stock home app, com.efercro.os.aimor, yet. Step 7 does it, after the kiosk is installed.

Install WebView 106

Android 6 does not unpack native libraries for apps in /system, and WebView 106 stores them compressed. So you unpack them on your computer and push them.

mkdir -p ~/frame-files/webview-libs
unzip -j ~/frame-files/webview.apk 'lib/armeabi-v7a/*.so' -d ~/frame-files/webview-libs
adb push ~/frame-files/webview.apk /data/local/tmp/webview.apk
adb push ~/frame-files/webview-libs /data/local/tmp/webview-libs
adb shell 'mount -o remount,rw /system
cp -p /system/app/webview/webview.apk /system/app/webview/webview.apk.bak
cp /data/local/tmp/webview.apk /system/app/webview/webview.apk
chmod 644 /system/app/webview/webview.apk
rm -rf /system/app/webview/oat
mkdir -p /system/app/webview/lib/arm
cp /data/local/tmp/webview-libs/*.so /system/app/webview/lib/arm/
chmod 755 /system/app/webview/lib /system/app/webview/lib/arm
chmod 644 /system/app/webview/lib/arm/*
chown -R root:root /system/app/webview/lib
mount -o remount,ro /system
rm -r /data/local/tmp/webview.apk /data/local/tmp/webview-libs'

Trust the Let's Encrypt root

Android looks up certificates by a hash of the subject, so the file name matters. Compute it:

openssl x509 -inform PEM -subject_hash_old -noout -in ~/frame-files/isrgrootx1.pem

It prints 6187b673. Copy the certificate to that name with .0 on the end and install it:

cp ~/frame-files/isrgrootx1.pem ~/frame-files/6187b673.0
adb push ~/frame-files/6187b673.0 /data/local/tmp/
adb shell 'mount -o remount,rw /system
cp /data/local/tmp/6187b673.0 /system/etc/security/cacerts/
chmod 644 /system/etc/security/cacerts/6187b673.0
mount -o remount,ro /system
rm /data/local/tmp/6187b673.0'

If your server uses a certificate from another authority that Android 6 does not trust, install that root the same way, with the name from its own hash.

Restart and check

adb reboot
adb wait-for-device
adb shell dumpsys package com.android.webview | grep versionName

You expect versionName=106.0.5249.126. Reconnect with adb disconnect and adb connect if you use ADB over the network: the old session shows "device offline" after a reboot.

Keep a record

Write down each change you make to a frame, with its undo. The .bak files above are the undo for build.prop and WebView.

Step 5: Create your keys

Two keys, kept apart. You make both once, on your own computer, before your first build.

Key What it is What it protects
APK keystore An RSA key in a Java keystore. Signs the kiosk app. Android accepts an app update only if it is signed with the same key as the installed app.
Release key An offline ECDSA P-256 key. Signs each release's SHA-256 and version code. A stolen server or admin login cannot push a build you did not sign.

Update the kiosk explains how they work together.

The APK keystore

If the keystore is missing, the build creates it with keytool on first use. To create it yourself first (same result), run this with your own password, which must equal KEYSTORE_PASS:

mkdir -p ~/.android
keytool -genkeypair -keystore ~/.android/frame-kiosk.keystore -alias frame \
  -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Frame Kiosk"

keytool asks for the password. Use it for both the keystore and the key. Then:

  • Back up the keystore file somewhere private. It is the only thing that lets you update the kiosk in place.
  • Set it to chmod 600, and keep it out of the repository.

The release key

The private key must be an unencrypted PKCS#8 PEM, which is what openssl genpkey writes:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ~/.android/frame-release-key.pem
openssl pkey -in ~/.android/frame-release-key.pem -pubout -out ~/.android/frame-release-pub.pem
chmod 600 ~/.android/frame-release-key.pem

Then set RELEASE_KEY and RELEASE_PUBKEY in .env to those two paths. The public key is baked into every build.

  • If RELEASE_PUBKEY is missing, that build refuses every self-update. It fails closed and reports release_sig.
  • If RELEASE_KEY is missing, no signature file is written.
  • Either way the build itself works.
  • If the two keys are not a pair, the build stops with RELEASE_KEY and RELEASE_PUBKEY are not a pair.

To let the server check signatures too, put the contents of the public key file in config.php as kiosk_release_pubkey. It is optional.

Keep the private key offline. Back it up somewhere private. Never put it on the server, in the repository, or in a container image. Losing it means one more adb install per frame with a new public key.

Step 6: Build the kiosk

  1. Start the build container. The first run downloads and installs the tools:

    cd kiosk
    lando start
    
  2. Build:

    lando build-kiosk
    

The script reads .env in the repository root, then:

  • writes the server address, your owner name and the PIN hash into the app,
  • creates the signing keystore at KEYSTORE if it does not exist,
  • builds, aligns and signs kiosk/build/frame-kiosk.apk (with both the v1 and v2 signature schemes),
  • if RELEASE_KEY is set, signs the build's SHA-256 and version code and writes kiosk/build/frame-kiosk.apk.sig.

It prints built kiosk/build/frame-kiosk.apk, then the versionCode and sha256. A line starting WARNING: means a release key setting is missing. That is fine for a first install, but set the release key before you rely on self-update.

Build options

Lando does not pass your shell's environment through, so give options as arguments after --:

lando build-kiosk -- --version-code 15 --version-name 15
Option Environment variable What it does
--version-code N KIOSK_VERSION_CODE The Android versionCode. A whole number from 1 to 2,100,000,000, no leading zero. Without it the build uses the value in kiosk/AndroidManifest.xml. Every release needs a higher number than the one before.
--version-name S KIOSK_VERSION_NAME The label shown in the admin. Letters, digits, ., _ and -, up to 64 characters. Default: the version code.
--test-crash KIOSK_TEST_CRASH=1 A build that crashes on purpose, for testing the rollback watchdog on your own frame. Never upload it as a real release. See Update the kiosk.

The environment variables work only when you build without Lando.

Build without Lando

The build also works on a machine that has the tools. They are the Ubuntu packages the Lando container installs:

sudo apt-get install --no-install-recommends aapt android-sdk-platform-23 apksigner dalvik-exchange zipalign openjdk-17-jdk-headless
bash kiosk/build.sh --version-code 15

The native route was not tested for this page.

Step 7: Install the kiosk and set device owner

scripts/provision.sh kiosk kiosk/build/frame-kiosk.apk

Use -s <serial> before kiosk to pick a device, for example scripts/provision.sh -s 192.0.2.50:5555 kiosk .... The script checks that the frame answers as root, then shows what it will do, and waits for y:

  • adb install -r the APK,
  • stop, disable and hide the stock home app, so the kiosk is the only home screen,
  • make the kiosk the device owner, which turns on lock-task mode and blocks factory reset, safe boot and new users. If the kiosk is already the owner, it skips this.

It stops with an error if there is more than one user or any account. To clear an account, run adb shell dumpsys account. It names the account type and the app that owns it. Remove the account in that app, or disable that app and reboot (Android drops accounts whose app is gone), then run the script again.

It does not reboot the frame. Reboot it yourself, then check:

adb reboot
adb shell dumpsys device_policy | grep -A2 'Device Owner'
adb shell dumpsys activity activities | grep -i locktask

You expect the kiosk package and mLockTaskModeState=LOCKED.

To update later, run adb install -r with a newly built APK signed with the same keystore, or from the admin. Uninstalling needs Undo device owner first.

If the Wi-Fi picker shows no networks, Android 6 may be hiding scan results because location is off. The kiosk normally turns it on. If not:

adb shell settings put secure location_providers_allowed +network

Step 8: Set the clock source

The frame has no battery-backed clock. Tell it which time server and time zone to use:

scripts/provision.sh time

It needs NTP_SERVER and TZ in .env. After confirmation it runs settings put global ntp_server and sets persist.sys.timezone, then prints both values back.

Step 9: Install the boot script

The boot script is device/frame-fixmac.sh. It runs as root on every boot. Install it after the kiosk, because it hands the backlight to the kiosk's user ID.

Optional: keep a specific MAC address

The Wi-Fi chip has no MAC address of its own, so the script gives each frame a fixed one: on first run it generates a random locally administered address and stores it in /data/misc/frame/mac. To use a particular address instead (for an existing DHCP reservation), set it before you install the script:

adb shell 'mkdir -p /data/misc/frame; chmod 700 /data/misc/frame
echo <mac in lowercase hex, like 02:aa:bb:cc:dd:ee> > /data/misc/frame/mac
chmod 600 /data/misc/frame/mac'

A factory reset deletes the file. Reserve the frame's address in your DHCP server either way.

Install the script

scripts/provision.sh boot-script

The command stops early if ADB_ALLOW holds anything other than IPv4 addresses and ranges, or if the frame has no timeout command (the rollback watchdog needs it). After confirmation it:

  • writes /system/etc/frame.conf with the kiosk package and your ADB_ALLOW,
  • keeps any older script as /system/bin/frame-fixmac.sh.v<N>,
  • installs device/frame-fixmac.sh as /system/bin/frame-fixmac.sh (0755 root:shell, so the kiosk can read its watchdog marker),
  • backs up install-recovery.sh as .bak and adds one line: exec /system/bin/sh /system/bin/frame-fixmac.sh. Android runs that file as root at every boot. The line must be exec, because Android kills background children,
  • checks the installed script against the repository copy.

It ends with "Installed and verified." The script takes effect after a reboot. Run it again after you change ADB_ALLOW.

To undo it, restore the old frame-fixmac.sh.v<N> and remove /system/etc/frame.conf.

What the boot script does

Watch it work with adb logcat -d -s frame-fixmac.

Feature What it does
Pins the Wi-Fi MAC The Wi-Fi driver ignores a configured address and picks a random one on every load. The script sets the stored one, checks every second for the first two minutes, then every 10 seconds. Android restarts Wi-Fi once during boot, so the log shows two corrections and two short Wi-Fi drops.
Hands the backlight to the kiosk It gives the kiosk's user ID ownership of /sys/class/disp/disp/attr/lcdbl on every boot. Without it, the Dim button only lowers brightness to 1 percent, under a black screen, instead of turning the backlight off. Check with adb shell stat -c %u /data/data/com.calevans.frame. The log line is lcdbl -> uid N. If you uninstall and reinstall the kiosk (not install -r), it gets a new user ID. Reboot afterwards.
Restores the clock After a power cut the frame starts in 1969. The script saves the time to /data/misc/frame/clock (every minute for the first two minutes after boot, then about every 10 minutes). If the frame starts with a clock before 2024, it sets the saved time, so HTTPS works before NTP answers. With no saved time, it restarts the kiosk once NTP has set the year. Look for clock restored in the log.
Limits ADB over Wi-Fi With ADB_ALLOW set, it builds an iptables chain that lets only those IPv4 addresses reach TCP port 5555 and drops the rest, including all of IPv6. Android's network service clears the rule when it starts, so the script puts it back whenever it goes missing. USB ADB is never filtered. With ADB_ALLOW empty, the list is off.
Fails closed on a bad entry If one ADB_ALLOW entry is not a valid IPv4 address or range, the script allows nobody over Wi-Fi (only the frame itself), logs bad ADB_ALLOW entry, and you must use USB ADB to fix it.
Recovers from the Home chooser Every 30 seconds, it checks whether Android is showing the "Complete action using" chooser (which appears if the default home app is lost) and starts the kiosk if so.
Rolls back a bad kiosk update The watchdog. See Update the kiosk. It needs timeout and dd from the frame's toolbox.

Note

The clock restore step is unverified on the tested firmware: the exact date syntax is assumed.

Step 10: Make the boot logo and animation (optional)

Start from a 1280x800 PNG. Back up the stock files first:

adb pull /system/media/bootanimation.zip stock-bootanimation.zip
adb pull /bootloader/bootlogo.bmp stock-bootlogo.bmp

Animation

Android shows the animation while it starts:

php tools/make-bootanimation.php my-logo-1280x800.png bootanimation.zip
adb push bootanimation.zip /data/local/tmp/
adb shell 'mount -o remount,rw /system
cp /data/local/tmp/bootanimation.zip /system/media/bootanimation.zip
chmod 644 /system/media/bootanimation.zip
chown root:root /system/media/bootanimation.zip
mount -o remount,ro /system
rm /data/local/tmp/bootanimation.zip'

The script writes an uncompressed zip, which Android's player requires. It shows one image until the frame has booted.

The bootloader shows the logo before Android starts. The panel is 800x1280 and used rotated, so rotate the image first. With ImageMagick on your own computer:

convert my-logo-1280x800.png -resize 1280x800^ -gravity center -extent 1280x800 -rotate 90 -type TrueColor -alpha off BMP3:bootlogo.bmp

The result is an 800x1280, 24-bit BMP of 3,072,054 bytes. Copy it over /bootloader/bootlogo.bmp on the small FAT partition mounted at /bootloader:

adb push bootlogo.bmp /data/local/tmp/
adb shell 'cp /data/local/tmp/bootlogo.bmp /bootloader/bootlogo.bmp; rm /data/local/tmp/bootlogo.bmp'

If /bootloader is read-only, remount it read-write first. This was not needed or checked. Do not leave a System Volume Information folder there if you ever mounted that partition on Windows: delete it.

To go back, push your saved stock-bootanimation.zip and stock-bootlogo.bmp the same way.

Step 11: Connect to Wi-Fi and claim the frame

  1. Reboot the frame: adb reboot.
  2. The kiosk opens its Wi-Fi picker on its own when the frame has no saved network and stays offline for 30 seconds. Select your network and enter the password. Other network... lets you type a hidden network's name.
  3. When the frame reaches your server, it shows a six-digit pairing code ("Read this code to ...", using OWNER_NAME).
  4. In the admin at https://frames.example.com/frames/admin/, sign in and select Claim a frame. Enter the six-digit code, select Find frame, enter a name for the frame, then select Claim frame. You see "Claimed. Turn on the widgets you want." The code is valid for 24 hours. See Use the admin for the details.
  5. Open the frame in the admin and turn on the widgets you want under Widgets. The frame loads its display within a minute. Widgets covers each one.
  6. To give the frame its own PIN, open the frame, go to Security, enter a New PIN and select Set PIN. Without one, the frame uses the KIOSK_PIN from the build. Reset to factory PIN goes back to it.
  7. To connect over the network from now on, find the frame's address in your router and run adb connect 192.0.2.50:5555.

Updates after you claim

When you claim a frame, the server points it at the newest release that has already run on a frame, and the frame moves itself to that release when its screen is quiet. If no release has run on a frame yet (a brand-new install), the frame stays on the build you installed. See Update the kiosk.

The maintenance panel

The kiosk has no on-screen exit by design. A hidden panel offers the few things you need on site.

  1. Hold one finger in the top-left corner of the screen (the first 120x120 pixels) for 10 seconds. A PIN pad opens. The hold works while the screen is dark.
  2. Enter the PIN. An info panel opens with the frame ID, app version, state, last server contact, network details and uptime.
  3. The panel offers Wi-Fi, Reload and Back, and nothing else.

After five wrong PINs in a row the pad locks for 5 minutes. Each further five wrong PINs lengthens the lock: 30 minutes, 2 hours, then 8 hours. A reboot during a lock restarts the lock from the beginning.

What the kiosk does

For reference, so nothing surprises you:

  • It is the home app. It runs full screen, stays on, and stays landscape.
  • It shows "Setting the clock..." until the year is set, and a "Can't reach ..." screen with the network name, address and frame ID while offline.
  • It opens the Wi-Fi picker by itself, with no PIN, when it has no saved network and has been offline for 30 seconds after boot, or when saved networks have not connected for five minutes. It closes the picker again if the old network returns while nobody is touching it.
  • Dim (the moon on the dashboard) turns the backlight fully off. The next tap wakes it and does not reach the page. The 10 second top-left hold works while dark.
  • It follows the frame's bedtime and wake schedule from the admin, in the frame's time zone, and saves it so it works offline.
  • It keeps the day's photos in its own storage, so they keep showing when the network or the photo host is down.

Undo device owner

dpm remove-active-admin refuses to remove a device owner on Android 6, and there is no screen for it. The kiosk has a hidden receiver that only root can call:

adb shell am broadcast -n com.calevans.frame/.AdbReceiver -a com.calevans.frame.action.RELEASE_DEVICE_OWNER

The result line says device owner released. After that, adb shell pm uninstall com.calevans.frame works.

If the app is broken and cannot receive the broadcast, delete the policy files as root and reboot. This is untested. Restrictions the owner set may remain in /data/system/users/0.xml, so check adb shell dumpsys user afterwards:

adb shell rm /data/system/device_owner.xml /data/system/device_policies.xml

Give a frame away

The kiosk has your server's address built in. Handing over the hardware does not hand over independence. Choose what the new owner gets:

  • The frame stays on your server. Claim it for them in your admin. They never need ADB.
  • The new owner runs their own server. They need their own kiosk build, signed with their own keystore. Android will not replace your build with theirs, so first undo device owner and uninstall the kiosk, then they run this guide from step 5. USB ADB is always open for this.

Either way, before you hand it over:

  1. In your admin, revoke the frame (More, then Revoke) or delete it, so your server stops serving it.

  2. Remove your saved Wi-Fi networks. This ends ADB over Wi-Fi, so USB ADB must still work:

    adb shell am broadcast -n com.calevans.frame/.AdbReceiver -a com.calevans.frame.action.FORGET_WIFI
    

    The result line says removed N of N saved networks. Within about a minute the kiosk opens its Wi-Fi picker, which is what the new owner sees.

  3. Power the frame off with adb shell reboot -p.

If Wi-Fi is off, the broadcast says so. Run adb shell svc wifi enable, wait a few seconds, and try again.

Next: Widgets.