BooxUltimatum

How it works

What BooxUltimatum asks of the tablet, part by part, with the file it lives in and the test behind it.

Device
BOOX Note Air6 COne unit, the author's own
Firmware
4.3, Android 162026-08-17_12-29_4.3-rel_0817_fd4f8e9fe
Status
Verified here, untested elsewhereBoox interfaces are unofficial here; an update can change them

Access tiers, and why no root

Every feature and tweak declares the least access it needs, and the app degrades gracefully without it. The tier is part of each tweak's definition in code, and the app checks it before offering the change.

TierWhat it isWhat it unlocks
T0Just the app, as installedHome screen, sleep screen (Sleep image mode) and power-off screen, Instant ink, battery measurement, the tablet-wide font, the settings hub, and becoming the home screen through Android's own confirmation. For restricted apps, links to Android's battery page and Boox's hidden App Freeze page.
T1Permissions granted once from a computer with adbReading battery statistics in the app, showing or hiding status bar icons tablet-wide, and setting-based tweaks
T2Shizuku: a small service with the rights of the shell user (uid 2000)Most tweaks, reading and lifting Boox's background restriction for every app, one-tap home switching, the network name in the header, Sleep now, the Over Transparent sleep mode
T3RootOut of scope, by decision on 2026-09-25

The one-off T1 grants:

adb shell pm grant app.booxultimatum android.permission.WRITE_SECURE_SETTINGS
adb shell pm grant app.booxultimatum android.permission.DUMP
adb shell pm grant app.booxultimatum android.permission.READ_LOGS
adb shell appops set app.booxultimatum GET_USAGE_STATS allow

The repository bundles these as scripts: tools/host/grant-permissions.ps1 (or .sh) grants T1 once, and tools/host/start-shizuku.ps1 (or .sh) starts Shizuku after each reboot. Shizuku needs adb to start, and firmware 4.3 hides the Wireless debugging switch, so for now that takes a computer.

Tablet-wide, the status bar can have icons shown or hidden (icon_blacklist, T1). The custom Wi-Fi and battery designs are drawn in the home screen's own header: replacing the system's icon artwork needs a system overlay, which the shell can't create (cmd overlay fabricate answers "must be root").

For T2, the app binds a Shizuku user service (ShellService, over AIDL) that runs commands as the shell user. That helper costs about 66 MB, and as a home screen the app process lives forever, so the service is unbound and its process removed after 45 seconds without calls. Shizuku itself stops on every reboot. With the tablet plugged into a computer, tools\host\start-shizuku.ps1 restarts it; firmware 4.3 hides Wireless debugging, so restarting it from the tablet alone isn't possible yet.

Why no root

On older Boox models the bootloader shipped unlocked. On this one it doesn't: the capture reads verifiedbootstate=green, flash.locked=1, vbmeta.device_state=locked, with ro.oem_unlock_supported empty and SELinux enforcing. No public firehose loader is known for its Qualcomm QCS6690 either. Root isn't a given, so on 2026-09-25 the project decided to target T0 to T2 and nothing above. Nothing is flashed, nothing touches the system partitions, and the bootloader stays locked.

The limit is real and sometimes costs a feature. Android 16 refuses shell writes to DeviceConfig flags outside its allowlist, so a planned Enter deep sleep sooner tweak was dropped rather than shipped broken.

The home screen's resource budget

The launcher had one hard requirement besides being good: it must never compete with the Boox launcher for memory or CPU. Measured on 2026-09-26 with dumpsys meminfo and top -d 15 on home:

BooxUltimatum homeBoox home (com.onyx)
Memory (PSS), in front64–132 MB, about 60 MB of it window buffers just after start455 MB
Memory (PSS), behind29–44 MBn/a
CPU while idle on home0.0 %0.2 %

The figures come from choices, not from luck:

  • No polling. Receivers and the network callback are registered only between onStart and onStop; the page refreshes on ACTION_TIME_TICK, battery and connectivity broadcasts while visible, and package callbacks.
  • An 8 MB icon cache, trimmed when the UI is hidden.
  • The wallpaper decoded at half resolution in RGB_565, and only when the file changes.
  • Widget hosting that listens only while visible; weather refreshed only while visible and at most hourly.
  • The Shizuku helper released after 45 seconds of idle.

Compatible by construction

It uses only public launcher APIs (LauncherApps for apps, shortcuts and badged icons, AppWidgetHost for widgets), with no reflection and no hidden APIs. Recents belong to com.android.onyxquickstep, and gestures, NaviBall and EinkWise to SystemUI, so they behave the same under either home. The launcher ships disabled as a HOME alias, the Boox home is journaled before the first switch, and from a computer this always brings it back:

adb shell cmd package set-home-activity --user 0 com.onyx/.StartupActivity

Verified as the default home on the tablet: Home and Back, app shortcuts, the Boox entry points (Library, Notes, Storage), Recents, and recovery after the process is killed.

The sleep screen

Boox's sleep screen is a doze dream, com.onyx/com.onyx.common.dream.OnyxDaydreamService. Its style and pictures live in com.onyx's private MMKV, which no other app can read or write. But the always-running com.onyx process has a dynamic, exported receiver that needs no permission, and Boox Settings uses it too:

am broadcast -a onyx.action.SCREENSAVER \
  --ei type 16 \
  --es file '/storage/emulated/0/Pictures/booxultimatum-sleep.png' \
  --ez show_result_hint false

Type 16 switches the style to Image, with that file as its only picture. This broadcast is part of Onyx's public SDK (ScreenSaverUtils, used by OnyxAndroidDemo's screensaver sample), and other open-source apps such as KOReader cover tools use it too. The app sends it from its own uid, which was verified on the tablet, so Sleep image mode is T0; with Shizuku it repeats it from the shell. Restore sends type 16 with Boox's own /system/media/standby-1.png, because the previous style can't be read back, and offers Boox's screensaver settings beside it. The app never sends it while the tablet is asleep, because other developers report that doing so blanks the sleep screen to white; a switch that falls due then waits for the next wake.

The same broadcast with type 17 sets the power-off picture. BooxUltimatum renders the chosen face in portrait, without the battery and put-down time that would be stale by the next power-on, and sends it on request. Boox saves its own copy (logcat: SaveShutdownImageAction: Save shutdown image succeed, for both the normal and the charging variant), so it changes only when set again; restore sends Boox's /system/media/shutdown-default.png.

Read fresh at every sleep, never while asleep

A file directly in /sdcard/Pictures/ is used in place and decoded afresh each time the tablet goes to sleep, so overwriting it between sleeps changes the next sleep screen. A file anywhere else is copied once and later overwrites are ignored. Files above 10 MB are dropped. Once asleep, nothing is read again, and Boox itself only redraws its own clock overlay, every 5 minutes. So by default the face is kept current while the tablet is awake, and only then:

  1. While awakeA non-wakeup inexact alarm (ELAPSED_REALTIME) every 1, 5, 15 or 30 minutes, plus battery, time, date and screen-on broadcasts the system sends anyway. Coalesced for 2 s.
  2. Each refreshThe face's content and settings are fingerprinted; unchanged means no render, no encode, no write. The other orientation is rendered too and kept ready.
  3. Going to sleepONYX_SYSTEM_GOING_TO_SLEEP arrives tens of milliseconds before Boox decodes. If the picture doesn't match the rotation, the ready one is written at once.
  4. AsleepNothing is read. The alarm never fires; a missed one is delivered on wake. Unless live updates are on, below.

Live updates while asleep New

The sleep screen is Android's doze dream, com.onyx/.common.dream.OnyxDaydreamService, fixed by config_dozeComponent. It never re-reads the picture, but it registers an exported receiver for onyx_dream_refresh: any app can send it, and the dream wakes the display for about 1.2 s (onyx.action.DISPLAY_CHANGED_STATE, state 2, then 3). Frames drawn while it dozes are held until then. The lock screen hides every app window except an accessibility overlay, so an accessibility service that reads nothing holds the face.

  1. Going to sleep4 s after Onyx has drawn and dozed, the first live face goes up, over the charging bar too.
  2. Every stepAn exact RTC_WAKEUP alarm on the round minute: render, send the refresh, show the face when Onyx reports the display on. About 1.4 s awake. A full repaint every sixth update.
  3. WakingThe overlay is removed at once, so the lock screen and PIN pad are never covered.

Boox's power manager (android.onyx.pm.OnyxAlarmHelper) clears wake-up alarms at sleep unless the app is on its full-access list, which it joins when its background use changes to allowed. Verified on battery on firmware 4.3; the battery cost is still to be measured.

Both orientations, prepared

Boox centre-crops the picture to the rotation it sleeps in: 1860 × 2480 in portrait, 2480 × 1860 at rotation 90 or 270. A face drawn only after a turn left a few seconds in which a sleep showed the old shape cropped. Every refresh therefore also encodes the other orientation into noBackupFilesDir/sleep-cache, and a rotation, seen through a display listener before the configuration reaches the process, swaps that file in at once, with a fresh render 1.5 s later. Tested by rotating and sleeping within 0.2 s in both directions: the picture was written 100 ms after Boox's going-to-sleep broadcast and 260 ms before the dream decoded it.

Boox's bar while charging

Boox draws a status bar (battery, “Press power button to wake up”) along the bottom of any sleep screen unless Screensaver › Bottom Status Bar is off, and always while the tablet charges. In com.onyx the check is isShowBottomStatusBar() || isBatteryCharging(), and it reads the real charger, so dumpsys battery unplug doesn't hide it. Faces don't reserve the strip; the studio asks owners to turn the bar off and says why it still shows on the charger.

Over Transparent is T2

Boox's Transparent style snapshots the screen and can lay a sticker PNG over it, saved as Pictures/Sticker/sticker_<timestamp>.png and reloaded at every sleep. The shell user can overwrite it and the app can't, so this mode copies BooxUltimatum's plate over the newest sticker through Shizuku, after backing up and journaling Boox's own.

Still to confirm on the tablet: that MediaStore overwrites made with mode "wt" are picked up at the next sleep, the Boox clock zone used for the layout guide (the top 22 %), and render and encode times per face at full size.

Instant ink

Experimental

Boox Notes gets its ink under the nib in about 10 ms because SurfaceFlinger draws the stroke itself. Its touchReader and touchConsumer threads read the pen's event node and push small HANDWRITE partial updates to the e-paper driver (/dev/ebc, open only to root and SurfaceFlinger). Apps don't draw that ink; they only arm it, with private ISurfaceComposer transactions for the pen state, the region, the stroke and the repaint.

This is a documented Onyx feature. App developers get it through Onyx's pen SDK (onyxsdk-pen, TouchHelper), which turns it on inside the calling app's own view. Under the hood the SDK calls the firmware helper android.onyx.ViewUpdateHelper, after lifting Android's hidden-API check with double reflection. Two things are not in the SDK, and they are what Instant ink adds: arming the path for another app that doesn't include the SDK, and handing the stroke back to that app after the lift. On firmware 4.3 (Android 16) the SDK's route is also closed to third-party apps (both calls are denied as api=blocked), so BooxUltimatum tries the firmware helper first and otherwise sends the same SurfaceFlinger transactions itself.

Any process can arm that path for whichever app is in front. BooxUltimatum does it from its ordinary app uid; the Shizuku route in the code is only a fallback for firmware that refuses the direct one. The other app is untouched and still receives every pen event.

One stroke

  1. ReadyAs soon as Instant ink is on, a session is opened and paused at once. On firmware 4.3 a session opened as the pen arrives misses the stroke that follows, while a paused one resumes instantly, even at the touch.
  2. HoverThis pen reports hover as BTN_TOOL_BRUSH, only about 45 ms before BTN_TOUCH on a quick stroke, and a quick first touch comes in the same batch. The app in front is read then, from usage events. If it's chosen, the session resumes and, as Onyx's SDK does at hover, ENABLE_POST with 0 holds the app's own frames back, so the first point of the stroke lands on a held screen.
  3. TouchSurfaceFlinger draws the stroke, and holds the app's frames by itself from the touch (its HandlePenTrigger) until they're let through. The app still gets every pen event. Letting its frames through mid-stroke ends the preview for the rest of the stroke, so the app's drawing always waits for the lift.
  4. Lift + 500 msENABLE_POST with 1 lets the frames through, and the app's real stroke replaces the preview. The pen session stays open.
  5. Pen awaySwap at once and pause the session, so nothing of ours draws if the next app isn't a chosen one. The eraser end pauses it too, and so does the screen turning off. Over Boox's own apps, which run sessions of their own, it never ends a session.

A tap with the pen often opens another app while the pen stays in range, so the app in front is read again a few times after a tap. Keeping one session open is the point. Closing it and repainting also removes the preview, but a session reopened at pen-down lost the start of the stroke, and one reopened at hover missed quick strokes. HANDWRITING_REPAINT, REPAINT_EVERY_THING, SAVE_PEN_ATTACHED_FB, the auto-sync switch and the screen-note pipe were all tried one call at a time on a stuck preview, and none of them removed it. ENABLE_POST did, even as a quick hold-and-release when nothing had held the frames.

The region lives in the panel's landscape frame

SurfaceFlinger reads the handwriting region in the panel's own landscape frame, whatever the rotation. A portrait-shaped region of 1860 × 2480 gave no preview in the bottom quarter of a portrait screen, below y = 1860. So the region is always a square as large as the long side, 2480 × 2480, which covers the panel in every orientation.

The android.ui.ISurfaceComposer transactions the client uses, as found on firmware 4.3.
TransactionCodeUsed for
SET_PEN_STATE16711693Start, draw, pause or stop the session, with the owner pid
SET_REGION_LIMIT16711694The square region, long side by long side
SET_STROKE_STYLE / WIDTH / COLOR16711688 / 87 / 86Sent in that order, after the pen state goes to start: choosing a style resets the width to that style's default, so a width sent first was lost. 4 px, black, fountain pen (style 1) by default.
ENABLE_POST16711692Hold app frames (0) and release them (1): the swap
GET_PEN_STATE1048643Probe the route, and read the session state
REPAINT_EVERYTHING16711700Only in release, handing the panel back to normal drawing

The pen's event node (/dev/input/event5, onyx_emp_istaric_pen) is readable by apps on this firmware, but its name isn't: SELinux closes /sys/class/input to apps. So the reader opens every readable node, takes the first that reports a pen tool and remembers it. The reader thread sleeps in poll() until the pen moves, so nothing wakes the tablet, and a 30-second watchdog never leaves app frames held back.

What isn't done yet

  • The mechanism was found in test sessions from the shell and an in-app prototype, with the owner drawing in Sketchbook. The in-app service is built and starts cleanly, but hasn't yet been checked with a real pen.
  • The preview is Boox's black pen, so a coloured brush or the eraser only shows once the app's stroke takes over.
  • A stroke could lose the first few millimetres of its preview, about one in ten. Holding the app's frames from hover instead of pen-down fixed it in the owner's test on 2026-09-27; it's worth watching in other apps.
  • Pen strokes can't be simulated: input injects through InputDispatcher, which SurfaceFlinger's pen reader doesn't read. Every test needs someone drawing.

A second route, EinkWise's per-app handwriting mode (noteConfig), was tried on Sketchbook and didn't engage. Its config was written back byte for byte, and it isn't shipped.

The battery log

A tool that measures drain mustn't cause it. The log has no foreground service and no wakelock. It samples on AlarmManager.setInexactRepeating with ELAPSED_REALTIME, the non-wakeup variant, every 30 minutes: the system batches it with other work and only delivers it when the tablet is already awake, so it adds no wakeups at all.

The alarm is set only when it's absent. Re-scheduling an inexact repeating alarm restarts its countdown, which dumpsys alarm showed before and after opening the app, so scheduling on every open could have kept the log from ever sampling.

What a row holds

  • Light samples: level, charge counter in mAh, voltage, temperature, plugged, screen on, elapsed realtime and uptime, boot count, current, front light, Wi-Fi, Doze and Battery Saver. Between two samples, 1 − Δuptime/Δelapsed is the share of time the tablet spent suspended.
  • Transition rows: screen_on, screen_off, plug, unplug, level (a 1 % step on battery), doze_deep, doze_light and saver, from broadcasts the system sends anyway. Standby and use are split exactly.
  • Deep snapshots every 3 hours: Doze state, alarm wakeups by app and tag, wakelocks, CPU and Wi-Fi by uid, foreground time.
  • Opening the app adds a sample, at most one every 10 minutes.

Android 15 and later send BOOT_COMPLETED to an app the first time it starts after a force stop, so the log once recorded 33 “boot” rows without a single reboot. It now compares Settings.Global.BOOT_COUNT and records those as unstop. Files are CSV and JSONL in Android/data/app.booxultimatum/files/logs, pruned after 60 days, and shared as one zip with a README that explains the columns.

What it found so far

FindingHow it was read
Standby is already near the floor: 0–12 mA. Boox's own sleep turns Wi-Fi off within 0.5 s and suspends the processor within 30 s.batterystats history, two unplugged sleeps
Screen-on use drew 430–640 mA (12–17 %/h), with the processor awake 93 % of the screen-on time.dumpsys batterystats since unplug
TIME_TICK is a wakeup alarm on this firmware, and sys.onyx.idledelay=2500 lets the processor sleep between page turns when nothing else keeps it up.dumpsys alarm, getprop
Doze starts disabled at every boot (config_enableAutoPowerModes=false). Turning it on changed little: no Doze transition was seen in two unplugged sleeps.aapt2 dump resources, dumpsys deviceidle
BOOXDrop (com.onyx.easytransfer) had used 8 min 27 s of CPU in 4.5 h of uptime, the most of any Onyx process.top, TIME+ column

No before-and-after battery runs are recorded yet, and the first overnight unplugged run is still to do. Until they are, the app shows no savings. For comparison only, eWritable measured about 2 %/h idle and 10 %/h writing on the same 4.3 firmware.

The tablet font

Boox Settings › Display › Font Style changes the system font without root through a broadcast that SystemUI handles. BooxUltimatum sends the same one:

am broadcast -a onyx.action.font.replace.system \
  --ei font_lang 0 \
  --es args_path '/storage/emulated/0/fonts/<family>.ttf'

SystemUI writes the persist.sys.font.* properties and hot-reloads the font in every app, with no reboot. onyx.action.font.reset.default returns to the Boox default. The interface was learned by decompiling ChangeSystemFontAction and BroadcastHelper in Boox's own settings package, then confirmed from the shell and from the app with getprop before and after.

  1. The font file is copied to /sdcard/fonts through Shizuku, where SystemUI can read it. That's why the tablet-wide font is T2. NeoReader reads the same folder.
  2. The path in use before the first change is journaled, once.
  3. The broadcast is sent, then the property is read back for up to five seconds. Success is only reported once the tablet has actually switched.
  4. Restore previous sends the journaled path again, or the reset broadcast when it was the Boox default.

One surprise: the owner's system font was a variable Manrope, and Android hands apps its default instance, which is ExtraLight, so everything drawn in the system font looked thin. The home screen now loads the tablet font with explicit wght instances, one step heavier by default, and Use on the whole tablet comes with a weight choice.

Reversible tweaks and the journal

A tweak is a small class with an id, a group, a tier, a risk, a way to read its state, an apply and a revert. Apply and revert never throw; failures come back as a readable message, and both outcomes are written to the journal.

  • The original is kept once. Before changing anything, a tweak stores the previous values only if none are stored yet, so applying twice never loses what the tablet had. The write is synchronous, because some changes (fonts, overlays) restart the process straight after.
  • Undo only undoes what the app did. A tweak that restores a recorded value offers Undo only when BooxUltimatum made the change. If the tablet already had that value, undoing would invent a state the owner never had.
  • Preset means the firmware already ships the tweak's value: the app shows it as on and offers no undo.
  • The journal lives in the app's private preferences, keeps the last 200 entries, and never leaves the device.

Round trip below means applied on the tablet, the state re-read as on, undone, and the original confirmed.

The 19 tweaks as verified on the Note Air6 C, firmware 4.3.
TweakTierMechanismStatus
apps.backgroundT2RUN_ANY_IN_BACKGROUND allow for user apps Boox restrictedRound trip 2026-09-25
doze.enableT2dumpsys deviceidle enableRound trip 2026-09-25; resets on reboot
doze.boox_allowlistT2Non-essential Onyx apps off the Doze allowlistRound trip still to record
bg.boox_restrictT2RUN_ANY_IN_BACKGROUND ignore for optional Onyx appsRound trip 2026-09-25
power.saverT2cmd power set-mode 1Applies only once unplugged
power.adaptiveT1App standby and adaptive battery onPreset
power.autosyncT0Master auto-sync offRound trip still to record
power.timeoutT2screen_off_timeout = 2 minutesRound trip 2026-09-25
power.stay_awakeT1stay_on_while_plugged_in = 0Preset
radio.wifi_scanT1wifi_scan_always_enabled = 0Preset
radio.ble_scanT1ble_scan_always_enabled = 0Round trip 2026-09-25
radio.wifi_wakeupT1wifi_wakeup_enabled = 0Round trip 2026-09-25
radio.locationT2Location offPreset
ui.serif_fontT2Noto Serif font overlayRound trip 2026-09-26
ui.animationsT1Animation scales = 0Preset
privacy.dnsT1Private DNS to dns.adguard-dns.comRound trip 2026-09-25
privacy.otaT2pm disable-user the OTA serviceRound trip still to record
privacy.storeT2pm disable-user the two Boox storesRound trip 2026-09-25
privacy.factoryT2pm disable-user the factory test appPreset

Outside the list, and journaled the same way: status-bar icons (icon_blacklist), the battery percentage, the default home app, the tablet font, the sleep screen, and fonts copied for NeoReader. Package tweaks use pm disable-user, which is reversible; nothing is ever removed from the system partitions.

Clean-room interoperability

BooxUltimatum contains no Onyx or BOOX code, SDKs or assets. The Boox interfaces it uses were learned from Onyx's public SDK documentation, by observing the tablet, and by reading how Boox's own apps call the firmware:

  • the sleep-picture and system-font broadcasts, which aren't publicly documented;
  • the SurfaceFlinger handwriting and post transactions behind Instant ink, which Onyx's pen SDK documents for an app's own views;
  • the EinkWise configuration it reads.

Only the facts are used. They are called through small clients written from scratch in core/sleep/, core/SystemFont.kt and core/ink/. Onyx's SDK itself, onyxsdk-pen and onyxsdk-device, is published as obfuscated binaries without source, so it isn't bundled. Because these interfaces aren't a stable public API, a firmware update can change them without notice; when something stops working, the app says so rather than guessing.

The dependencies are AndroidX and Jetpack Compose and the Shizuku API, all Apache-2.0. The one bundled asset is Archivo, under the SIL Open Font License. The app goes online for two things only: weather from Open-Meteo for the city you type, and Google Fonts when you open the fonts browser. There are no accounts, analytics, ads or trackers.

Help and history