mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 22:00:25 +01:00
482 lines
26 KiB
Markdown
482 lines
26 KiB
Markdown
# Installing APKs (Lepton)
|
||
|
||
The confidence labels are the same as in [ssh.md](ssh.md). Android apps run
|
||
in **Lepton**, Valve's Waydroid-based container. Lepton is built for games,
|
||
not general Android use
|
||
([GamingOnLinux](https://www.gamingonlinux.com/2026/09/lepton-from-valve-to-run-android-games-on-linux-is-now-open-source/)).
|
||
|
||
**VR streaming clients:** WiVRn 26.9 and ALVR 20.14.1 install, but both fail
|
||
OpenXR instance creation on the checked Frame because its Android runtime lacks
|
||
`XR_KHR_convert_timespec_time` (**verified** 2026-09-28, SteamOS 0.4.1,
|
||
BUILD_ID 20260925.6191901). They are not Frame Control dependencies. See
|
||
[the feasibility results and options](linux-vr-streaming.md).
|
||
|
||
## Install from the Mac: one app, one Lepton instance (verified 2026-09-25)
|
||
|
||
Use Frame Control's **Android apps** section (search, Install, Test, Report), drop
|
||
an `.apk` on **Send to Frame**, or:
|
||
|
||
```sh
|
||
./scripts/install-apk.sh some-app.apk # own instance, Steam shortcut
|
||
python3 ui/frame_android.py list|launch|stop|remove|probe <package>
|
||
```
|
||
|
||
Each APK becomes its own app, the way T3 Code is set up (see the instance
|
||
section below), instead of going into Lepton Development:
|
||
|
||
1. `ui/frame_apk.py` reads the package, label, version, ABIs and icon
|
||
(a stdlib parser of the binary manifest and resource table, so no Android SDK). APKs that need
|
||
API > 30 or have no `arm64-v8a` build are refused.
|
||
2. The APK, `frame/android/lepton-app.sh` (as `launch.sh`), `instance.id`,
|
||
`meta.json`, the icon and the `lepton-show-flatscreen` marker go to
|
||
`~/Applications/Android/<package>/` on the Frame.
|
||
3. A non-Steam shortcut is added through Steam's CEF debug port
|
||
(`frame/android/steam_shortcuts.py`), with no Steam restart.
|
||
4. Launching the shortcut runs Lepton directly with `SteamAppId` set to the
|
||
instance id (`2800000000 + crc32(package) % 70000000`). That's a
|
||
"steamlaunch" context, so app data in `compatdata/<id>/internal` survives
|
||
restarts and updates, and each app gets its own SteamVR panel. Several can
|
||
run at once alongside Lepton Development, each in its own container
|
||
(`lepton-steamlaunch-<id>`, ADB on 5556, 5557, …).
|
||
|
||
Split APKs (a base plus `split_*.apk` files) install as one app:
|
||
`python3 ui/frame_android.py install base.apk split1.apk …`. Lepton runs one
|
||
APK, so splits that add native libraries or assets are merged into the base
|
||
and re-signed; density and language splits are refused. See
|
||
[quest-import.md](quest-import.md#split-apks). Not yet tried on a real Frame.
|
||
|
||
Verified with AntennaPod and Tabletop Tools: installed in about 7 s, launched
|
||
from the shortcut, stopped, and relaunched with their data intact. ADB and
|
||
Lepton Development aren't involved.
|
||
|
||
`--dev` keeps the old path: ADB into Lepton Development over an SSH tunnel
|
||
(first free Mac port from 15555), which starts Lepton Development if needed.
|
||
Apps installed that way are deleted when it exits (see below). No pairing or
|
||
"Allow debugging?" prompt is needed for either path.
|
||
|
||
Lepton Development must be installed once. Over SSH,
|
||
`ssh frame 'steam steam://install/3056000'` queues it, but the install still
|
||
needs to be confirmed or started in the headset.
|
||
|
||
## When an app needs a newer Android
|
||
|
||
Lepton is Android 11 (API 30), with arm64-v8a only. If Frame Control refuses
|
||
an APK, it shows compatible versions from F-Droid's main and archive repos and
|
||
IzzyOnDroid. It shows at most eight version names, newest first, preferring an
|
||
arm64-only build, and says how many compatible builds it found in total.
|
||
Each index is reduced to its compatible builds once a day and cached (about
|
||
16 MB). The first lookup takes about 30 s and 100 MB of memory; later ones are
|
||
instant.
|
||
Choose **Install** to download a listed version, verify its SHA-256 against
|
||
the index, and install it as its own app.
|
||
|
||
You can also inspect a file or look up a package from the command line:
|
||
|
||
```sh
|
||
python3 ui/frame_android.py info some-app.apk
|
||
python3 ui/frame_android.py versions some-app.apk
|
||
python3 ui/frame_android.py versions org.example.app
|
||
```
|
||
|
||
The search links open APKMirror, APKPure, Uptodown, F-Droid and GitHub. Pick a
|
||
version whose minimum is Android 11 or lower and that has an arm64-v8a build
|
||
(or no native code). Frame Control does not fetch APKs from those search sites.
|
||
Older versions may lack fixes, and being installable does not guarantee an
|
||
app will run: see the missing services below. Android may refuse a downgrade
|
||
or an update signed by a different publisher; removing the app deletes its data.
|
||
|
||
## Installed apps disappear when Lepton Development closes (verified 2026-09-25)
|
||
|
||
Lepton Development runs in a throwaway "dev" context. When it exits for any
|
||
reason (you close it, or it crashes), the launcher script
|
||
`~/.local/share/Steam/steamapps/common/Lepton/lepton` calls
|
||
`clear_baked_app_data "non steamlaunch container"` and **deletes every app
|
||
installed over ADB**. The journal shows `Clearing baked app data due to non
|
||
steamlaunch container`, and `pm list packages -3` is empty afterwards.
|
||
|
||
The script skips the wipe when `LEPTON_NO_CLEANUP` is set
|
||
(`liblepton/liblepton.sh`, `clear_baked_app_data`). To keep your apps, set
|
||
Lepton Development's Steam launch options to:
|
||
|
||
```
|
||
LEPTON_NO_CLEANUP=1 %command%
|
||
```
|
||
|
||
(Steam → Library → Lepton Development → Properties → Launch Options.) Inferred
|
||
from the script, not yet tested across a restart.
|
||
|
||
## Which APKs work (verified 2026-09-25, SteamOS build 20260922.6101926)
|
||
|
||
Lepton is LineageOS 18.1 (`lepton_arm64_only`): Android 11, API 30,
|
||
`abilist=arm64-v8a` only, Mesa (Turnip, Adreno 750) with GLES 3.2 and
|
||
Vulkan 1.4. About 30 F-Droid apps were installed and opened on the Frame to
|
||
check each rule. The results are in the compatibility database (see compat-db/README.md).
|
||
|
||
**Won't install** (the installer refuses):
|
||
|
||
| Rule | Seen on device |
|
||
|---|---|
|
||
| `minSdkVersion` > 30 | `INSTALL_FAILED_OLDER_SDK: Requires newer sdk version #33 (current version is #30)` |
|
||
| Native code without `arm64-v8a` (32-bit ARM or x86 only) | `INSTALL_FAILED_NO_MATCHING_ABIS` |
|
||
|
||
**Crash on launch.** Lepton has no `clipboard` system service, so
|
||
`getSystemService(CLIPBOARD_SERVICE)` returns null:
|
||
|
||
| What | Result |
|
||
|---|---|
|
||
| **Jetpack Compose UI < 1.11** | Crashes as soon as a Compose screen appears: `null cannot be cast to non-null type android.content.ClipboardManager` in `AndroidComposeView`. Seen with 1.5, 1.6, 1.7, 1.8 and 1.10 apps. |
|
||
| Jetpack Compose UI 1.11, 1.12, 1.13 | **Works.** Six apps opened fine, including Aurora Store and NewPipe. |
|
||
| Old Compose, but the first screen uses classic Views | Opens (FoCal, Compose 1.3), and crashes only on Compose screens |
|
||
| SDL2 apps, including Kivy | Crash: SDL calls `ClipboardManager.addPrimaryClipChangedListener` at start-up |
|
||
| Godot 4.3 | Crashes (clipboard cast). Godot 4.6.1 works. |
|
||
|
||
**Works:** classic Android Views apps, Flutter (2 of 2), libGDX (2 of 2),
|
||
Compose 1.11+, Firebase-using apps. React Native: 2 of 3 opened; one
|
||
(controlloid) died with SIGSEGV on the Hermes JS thread. A Qt 6 app
|
||
(AusweisApp) failed on a missing libc++ symbol.
|
||
|
||
**Missing pieces:** an app may open but fail when you use one of these:
|
||
|
||
- No Google Play Services.
|
||
- No activity for `VIEW` of web links, `OPEN_DOCUMENT`/`GET_CONTENT` (no file
|
||
picker), `IMAGE_CAPTURE`, or text-to-speech. WebView (Chromium 152) is there.
|
||
- No Downloads or Contacts providers.
|
||
- Missing system services also include `accessibility`, `vibrator`, `phone`,
|
||
`print`, `usb`, `nfc` and `autofill`. The declared features lack
|
||
`touchscreen.multitouch`, `bluetooth_le` and `telephony`.
|
||
- No on-screen keyboard (IME) is installed. How text entry reaches Android
|
||
apps in the headset hasn't been checked.
|
||
|
||
**Lepton itself can crash.** Three times during testing, the graphics HAL
|
||
(`android.hardware.graphics.composer@2.1-service`) aborted right after an app
|
||
crashed. SurfaceFlinger died, the whole `lepton-dev` container exited, and the
|
||
installed apps were wiped (see above). A retry of the same app worked, so it's
|
||
intermittent rather than app-specific.
|
||
|
||
**How good are the predictions?** In a random sample of 12 apps rated "Should
|
||
work", all 12 installed, opened and were still running 12 s later (two needed
|
||
a retry because Lepton crashed mid-install). "Opened" isn't the same as fully
|
||
working: see the missing pieces above.
|
||
|
||
## Catalogue and compatibility reports
|
||
|
||
Frame Control's **Android apps** section lists every F-Droid app with a
|
||
verdict: Works on Frame, Should work, Might work, Probably crashes, or Won't
|
||
work, with the reasons. As of 2026-09-25 that's 30 working, 3,323 should work,
|
||
223 might work, 723 probably crash (mostly Compose < 1.11) and 156 won't
|
||
install. Each app is rated on its newest version that Lepton can install,
|
||
because F-Droid often publishes separate per-ABI builds and the newest is
|
||
frequently x86_64. **Install** downloads the APK (SHA-256 checked against the
|
||
F-Droid index) and sets it up as its own instance.
|
||
|
||
No ProtonDB-style database for sideloaded Android apps on the Frame existed
|
||
as of 2026-09-25. [Steam Frame Hub](https://verified.steamframehub.com/)
|
||
collects community reports for Steam games only and has no public API, and
|
||
Valve's "Great on Frame" badges and each Steam app's `recommended_runtime`
|
||
(for example `lepton-stable`) also cover Steam games only
|
||
([VR.org](https://vr.org/articles/steam-frame-lepton-android-runtime-52-of-130-certified-2026)).
|
||
So Frame Control keeps its own. Your reports are saved on your Mac; the
|
||
maintainer's copy also syncs them to a private Lakebed database.
|
||
**Test** records whether the app stays up in its own instance, and **Report**
|
||
(for any APK, F-Droid or not) records whether it worked, how it was run, where it came from, and notes, each with the SteamOS and Lepton build ids.
|
||
See
|
||
[compat-db/README.md](../compat-db/README.md) and
|
||
[apk-catalog/README.md](../apk-catalog/README.md).
|
||
|
||
## Store and Library in Frame Control
|
||
|
||
The Android tab has two views. **Store** finds apps across the sources (F-Droid,
|
||
IzzyOnDroid, curated GitHub releases, itch.io). **Library** shows the apps on the
|
||
Frame as cards, with Play and a menu for Test, Report and Remove; the display
|
||
settings and recent reports are below it.
|
||
|
||
Store search cards and app details also show **Reported not working** or
|
||
**Reported issues** when saved compatibility reports say so. Details include
|
||
the report date, version and notes. These are advisories: Install remains
|
||
available unless the selected build has an existing compatibility blocker
|
||
(for example, no arm64-v8a build or Android API above 30). Reports never clear
|
||
those blockers. Apps reported not working are excluded from Home's featured
|
||
app and recommendation shelves; search and category results still show them.
|
||
An unrated install failure caused by the wrong APK architecture or Android API
|
||
is ignored, using the same rules as the catalogue. A person's rating takes
|
||
priority over automated results, with the latest rating winning.
|
||
|
||
Reports warm in a background worker at server start. Search and details read
|
||
only the last complete in-memory snapshot and schedule a single background
|
||
refresh after 60 seconds; they never wait for disk, Keychain, a subprocess or
|
||
the report database. During initial loading they show the usual build verdict;
|
||
after a refresh, the next search or detail request sees the reports. Failed
|
||
refreshes keep the previous snapshot and retry after another 60 seconds.
|
||
|
||
**Availability:** public copies benefit from their own saved reports, including
|
||
reports for apps outside the bundled catalogue, across every source with the
|
||
same package name. This does not give public users the private shared ratings.
|
||
The checked-in `apk-catalog/site/apps.js` contains predictions and no tested
|
||
report verdicts, so it cannot supply a useful public report snapshot. No private
|
||
reports, free-text notes or credentials are exported or published. The existing
|
||
maintainer loader still uses its configured database access in the background;
|
||
the search feature adds no sharing, endpoint or network destination. Tests leave
|
||
the hook off by default (`FRAME_COMPAT_SEARCH_REPORTS=0` also disables it in
|
||
server subprocesses) and use synthetic reports to exercise refresh behavior.
|
||
|
||
The store groups apps by what a headset is good for, not by F-Droid's list:
|
||
VR, Games, Emulators, Watch & listen, Read & learn, Create, Work, Web & chat
|
||
and Tools (`ui/apk_sources/_categories.py`). A VR app is always in VR. Otherwise
|
||
F-Droid's own categories decide, the finer one first (an app in *Multimedia* and
|
||
*Social Network* is Web & chat).
|
||
|
||
Apps built around a phone are hidden from browsing unless **Show phone-only
|
||
apps** is on; a search always finds them, labelled *Phone app*. Hidden: apps in
|
||
a category that only makes sense in a phone's own system (launchers, keyboards,
|
||
wallpapers, Xposed modules), apps in a phone-side category (calls and SMS, VPNs,
|
||
fitness, alarms, system tools) unless a useful category says otherwise
|
||
(FluffyChat is in Phone & SMS, but it's a messenger), apps named for a phone
|
||
part (a widget, a launcher, a battery tool), and apps that need hardware the
|
||
Frame lacks (an IR blaster, a watch, NFC, root). They would install and run in
|
||
Lepton, but do nothing useful on the Frame. In the October 2026 indexes that is
|
||
about 1,600 of F-Droid's 4,500 apps and 550 of IzzyOnDroid's 1,300.
|
||
|
||
Without a search, each source offers its best 300 for the chosen category:
|
||
apps with a banner first, then with screenshots, then the newest.
|
||
|
||
## The store in the headset: Frame Control's store as a panel (inferred, not yet run on the Frame)
|
||
|
||
The **APK Store** panel is the Store above, drawn for the laser: the same
|
||
sources, categories and artwork, with Install, Open, Stop and Remove, opened as
|
||
its own floating panel:
|
||
|
||
```sh
|
||
scripts/store-on-frame.sh open # copy the server over if it changed, start it, open the panel
|
||
scripts/store-on-frame.sh close
|
||
```
|
||
|
||
`open` copies the same bundle the iPhone app uses
|
||
(`ios/scripts/make_frame_bundle.py`) to
|
||
`~/.local/share/frame-control/store-server/<version>` on the Frame. It then
|
||
starts `ui/server.py` there with `FRAME_LOCAL=1` on `127.0.0.1:47812`, with a
|
||
fresh `X-Frame-UI` key. Chromium XR (or the `org.chromium.Chromium` Flatpak)
|
||
opens `/store` (`ui/store.html`) in a 1280×800 app window, tagged as panel
|
||
`2001947812` ([panels.md](panels.md)). Downloads, SHA-256 checks and installs
|
||
all run on the Frame, so the Mac can go away after `open`.
|
||
|
||
The page has a category rail (Home, the store's categories, Installed), eight
|
||
art cards a page, and a featured VR app on Home. It turns pages instead of
|
||
scrolling, because a tagged panel gets mouse clicks and a drag would select
|
||
text; the thumbstick (a mouse wheel) turns pages, and an on-screen keyboard is
|
||
there for search. **Checked on the Mac only** (2026-10-14, 1280×800, against
|
||
the real sources): Home, categories, Installed, the detail sheet. **Still to
|
||
check in the headset:** that the panel opens and takes laser clicks, that an
|
||
install completes when run on the Frame, and that **Open** brings up the app's
|
||
panel.
|
||
|
||
## In-headset app store: F-Droid 1.17 (verified 2026-09-25)
|
||
|
||
F-Droid 1.23 uses an old Compose and crashes on launch. **F-Droid 1.17.2**, the
|
||
newest archived build without Compose, runs, loads the full catalogue (the
|
||
first repo update takes about 90s), and can install apps. F-Droid 2.0 uses
|
||
Compose 1.12, so it should work, but it hasn't been tried. The catalogue's
|
||
Install button for F-Droid installs 1.17.2. To let F-Droid install apps without
|
||
a settings prompt, with the tunnel open:
|
||
|
||
```sh
|
||
adb -s $S shell appops set org.fdroid.fdroid REQUEST_INSTALL_PACKAGES allow
|
||
```
|
||
|
||
## Handy commands
|
||
|
||
Open a tunnel by hand (use any free local port):
|
||
|
||
```sh
|
||
ssh -f -N -M -S /tmp/frame-adb.sock -L 127.0.0.1:15555:127.0.0.1:5555 frame
|
||
adb connect 127.0.0.1:15555
|
||
S=127.0.0.1:15555
|
||
# when done: adb disconnect $S; ssh -S /tmp/frame-adb.sock -O exit frame
|
||
```
|
||
|
||
Then:
|
||
|
||
```sh
|
||
adb -s $S shell pm list packages -3 # installed third-party apps
|
||
adb -s $S shell monkey -p <pkg> -c android.intent.category.LAUNCHER 1
|
||
# If monkey exits with -5 (it did for T3 Code), start the activity directly:
|
||
adb -s $S shell am start -W -n "$(adb -s $S shell cmd package resolve-activity --brief -c android.intent.category.LAUNCHER <pkg> | tail -n 1)"
|
||
adb -s $S logcat -d -b crash # why an app died
|
||
adb -s $S uninstall <pkg>
|
||
adb -s $S exec-out screencap -p > shot.png # the Lepton window
|
||
```
|
||
|
||
## Reaching a Mac service from Lepton (T3 Code v2, verified 2026-09-25)
|
||
|
||
Lepton runs in podman with `pasta` networking. It has **its own loopback**, so
|
||
a port on the Frame's `127.0.0.1` isn't visible as `127.0.0.1` inside Android.
|
||
But pasta runs with `--map-gw`, so the **gateway address inside Lepton
|
||
(`192.168.1.1` on the home network) maps to the Frame host's loopback**.
|
||
|
||
T3 Code v2 on the Mac listens only on `127.0.0.1:3873`. To reach it:
|
||
|
||
1. Keep `ssh -N -R 127.0.0.1:3873:127.0.0.1:3873 frame` running on the Mac,
|
||
for example from a LaunchAgent with `KeepAlive`, so launchd restarts it if
|
||
it drops.
|
||
2. In the app on the Frame, the environment host is `192.168.1.1:3873`.
|
||
|
||
The app on the Frame was built from the T3 Code v2 nightly source with `expo prebuild` and `gradlew assembleRelease
|
||
-PreactNativeArchitectures=arm64-v8a`, using Homebrew `openjdk@17` and the
|
||
`android-commandlinetools` SDK. It's signed with the debug key.
|
||
|
||
To pair again, issue a one-time code on the Mac and type it into
|
||
**Add environment**:
|
||
|
||
```sh
|
||
A="/Applications/T3 Code (V2 Preview).app"
|
||
ELECTRON_RUN_AS_NODE=1 "$A/Contents/MacOS/T3 Code (Alpha)" \
|
||
"$A/Contents/Resources/app.asar/apps/server/dist/bin.mjs" \
|
||
auth pairing create --base-dir "$HOME/.t3-v2" --ttl 15m --label "Steam Frame"
|
||
```
|
||
|
||
The app is deleted whenever Lepton Development closes (see above), so reinstall
|
||
it afterwards or set `LEPTON_NO_CLEANUP=1`. The gateway address comes from the Frame's network when Lepton starts. On a
|
||
different network, check it with `adb shell ip route` and edit the host.
|
||
|
||
## Lepton Development forgets apps; give an app its own instance (verified 2026-09-25)
|
||
|
||
**Lepton Development wipes every installed app when it exits.** Its launcher
|
||
logs `Clearing baked app data due to non steamlaunch container`, unless
|
||
`LEPTON_NO_CLEANUP` is set. A Steam-style launch (with `SteamAppId` set) is a
|
||
"steamlaunch" context and keeps its data:
|
||
|
||
- App data lives in `STEAM_COMPAT_DATA_PATH/internal/<package>` (symlinked to
|
||
`/data/data/<package>`) and survives everything, including APK updates.
|
||
- `STEAM_COMPAT_DATA_PATH/baked` is Lepton's Android snapshot. It's rebuilt when
|
||
the APK changes, or when the app exits within 30 seconds of starting.
|
||
- `STEAM_COMPAT_DATA_PATH` must be under `~/.local/share/Steam` (use
|
||
`steamapps/compatdata/<id>`). Only that tree is mounted in the container. Put
|
||
it anywhere else and the symlinks dangle, so the app crashes with
|
||
`ENOENT` on its first file write.
|
||
- Lepton runs apps headless unless an empty `lepton-show-flatscreen` file sits
|
||
next to the APK (`STEAM_COMPAT_INSTALL_PATH`).
|
||
- Several instances can run at once. Each gets ADB on `5555 + offset`
|
||
(`podman ps --format "{{.Names}} {{.Labels.adb_port}}"`).
|
||
- Outside Steam, Lepton's `setpgid --foreground` re-exec fails with no
|
||
terminal. Set `IS_PARENT=true` and start it with `setsid --wait`.
|
||
|
||
[`frame/t3code/launch.sh`](../frame/t3code/launch.sh) does all this for T3
|
||
Code (context `steamlaunch-2873873873`; ADB is the first free `5555 + n`, e.g. 5557). It needs the Steam client
|
||
running (it mounts `~/.steam/steam.pipe`).
|
||
|
||
**T3 Code in the Steam library (verified 2026-09-25).** The wrapper lives on
|
||
the Frame at `~/Applications/T3Code/launch.sh`, with `t3code.apk` and the
|
||
flatscreen marker next to it. It's a non-Steam shortcut called "T3 Code"
|
||
(shortcut app id `3130509679`). Launching it from Steam gets its own SteamVR
|
||
panel, `valve.steam.desktopgame.3130509679`, and opens already paired.
|
||
|
||
- The shortcut was added without restarting Steam, through Steam's CEF debug
|
||
port (`127.0.0.1:8080` on the Frame, target `SharedJSContext`):
|
||
`SteamClient.Apps.AddShortcut(name, exe, "", "")`, then `SetShortcutName`
|
||
and `SetShortcutStartDir`. `steam steam://addnonsteamgame/<path>` only logged
|
||
the URL and added nothing.
|
||
- To launch it over SSH: `steam steam://rungameid/13445436691150012416`, which
|
||
is `(3130509679 << 32) | 0x02000000`.
|
||
- Steam sets `STEAM_FOSSILIZE_DUMP_PATH` for shortcut launches but not
|
||
`STEAM_COMPAT_SHADER_PATH`. Lepton then dies with "unbound variable", so the
|
||
wrapper sets both.
|
||
- To update T3, replace `t3code.apk`. Lepton rebuilds its snapshot on the next
|
||
launch, and the pairing survives.
|
||
|
||
## Crashing apps can take down the headset session (verified 2026-09-25)
|
||
|
||
Some apps crash Android's graphics composer HAL, which kills the Lepton
|
||
container. On 2026-09-25 a batch crash-test also coincided with `steamvr.service`
|
||
restarting "on client request", which stops and SIGKILLs `gamescope-session`.
|
||
After one of those kills, gamescope crash-looped about once a second on
|
||
`rendervulkan.cpp:2181 ... Assertion '!modifiers.empty()'` because it kept
|
||
attaching to the SteamVR processes orphaned from the dead session. The fix
|
||
without sudo was to `for p in vrdashboard vrcompositor vrserver; do pkill -TERM -x $p; done` (pkill takes one pattern). The
|
||
next session then started SteamVR fresh and recovered within a minute.
|
||
|
||
## Android display: resolution, UI scale, text size (verified 2026-09-25, SteamOS 0.3.0, build 20260922.6101926)
|
||
|
||
Each running Lepton instance has its own ADB port on the Frame, assigned at
|
||
launch: 5555 is Lepton Development, and own-instance apps take the next free
|
||
port (T3 Code was on 5557). Find them with `ss -ltn` (5555–5599) and identify
|
||
each with `pm list packages -3`. Both instances reported `Physical size:
|
||
1920x1080`. Their densities were 180 dpi (Lepton Development) and 213 dpi
|
||
(T3 Code), and `settings get system font_scale` returned `null` (1.0).
|
||
|
||
These all apply immediately and read back as set. Tested on Lepton Development
|
||
only:
|
||
|
||
```sh
|
||
adb -s $S shell wm size 2560x1440 # or: wm size reset
|
||
adb -s $S shell wm density 240 # or: wm density reset
|
||
adb -s $S shell settings put system font_scale 1.15
|
||
adb -s $S shell settings delete system font_scale
|
||
```
|
||
|
||
After a `wm` reset, Android writes `font_scale=1.0` back asynchronously, so a
|
||
single delete that follows one reads back `1.0`. A second delete a second later
|
||
leaves it `null`. Frame Control's **Android display** card does this for you
|
||
(`/api/android/display`).
|
||
|
||
**Inferred, not yet checked in the headset:** a bigger Android resolution with
|
||
density scaled to match (2560×1440 at 4/3 of the density) gives sharper text,
|
||
because gamescope scales Lepton's surface to fit the same panel. Also unverified:
|
||
whether the settings survive the app or its Lepton instance relaunching.
|
||
Lepton Development rebuilds its Android data on exit, so there they probably
|
||
don't.
|
||
|
||
## Expansion files and save backups
|
||
|
||
SideQuest-inspired CLI helpers install local OBB files into an already-running
|
||
app instance and back up/restore a stopped instance's private app data. See
|
||
[SideQuest features and limits](sidequest.md) for commands, archive scope and
|
||
verification status. These paths have offline coverage; real Frame storage and
|
||
permissions remain unverified. They do not change APK install or launch behavior.
|
||
|
||
|
||
## Local APK/OBB backups and Android bundles
|
||
|
||
In **Android apps → Import Android game → Choose game files…** (or **Send to Frame**), choose one
|
||
base APK, any supported splits, and its matching `main.<version>.<package>.obb` and/or
|
||
`patch.<version>.<package>.obb` files together. The desktop app also discovers
|
||
matching OBB files beside a lone selected APK. Browser uploads use only the
|
||
files you selected. Installed apps can receive OBB-only imports.
|
||
|
||
You can instead select **one `.xapk`, `.apks`, or `.apkm` archive** containing
|
||
one base APK, optional supported splits and matching OBBs. These extensions describe
|
||
containers, not a guarantee that their contents are supported. OBBs may be
|
||
at the archive root, under `<package>/`, or under `Android/obb/<package>/`;
|
||
one enclosing backup folder is allowed. Known manifest/icon/signature
|
||
sidecars are ignored; the APK manifest determines the package. Unknown files,
|
||
unsafe paths, links, encrypted entries, duplicate paths (including case
|
||
collisions), more than 4,096 entries, or more than 16 GiB unpacked are refused.
|
||
Staging also keeps 64 MiB of free disk space. Payloads still have the regular
|
||
100-file/99-OBB limit. Ordinary `.zip` files keep the PC-title import route;
|
||
extract an Android ZIP yourself and select its APK/OBBs.
|
||
|
||
The importer previews the app and files before **Import game & data**.
|
||
Confirming authorizes installing the app, starting it to prepare Android,
|
||
checking the running package/version, copying OBBs through the existing
|
||
verified data helper, and closing the game. Open it from Steam afterwards.
|
||
If setup fails, read the error and retry the same backup; do not uninstall
|
||
first, since that deletes saves. An app that cannot start may also prevent
|
||
data setup. Uploaded and extracted staging is removed on cancellation,
|
||
inspection failure, or job completion/failure; abandoned imports expire
|
||
through the existing one-hour staging lifecycle. Original local files stay intact.
|
||
|
||
**Limits:** this backup importer requires exactly one base APK. For split sets,
|
||
each split must identify the same package and version, and pass the existing
|
||
[Quest split merger](quest-import.md#split-apks). It refuses resource, code and
|
||
feature splits and does not choose among device variants. Merging re-signs the
|
||
APK, which can affect licences. It does not restore `Android/data`, private app
|
||
data, saves or entitlements. These split imports have local fixture and merger
|
||
tests, but no real split game has been installed with this backup route on the
|
||
Frame.
|
||
|
||
For issue #102, the RE4 report only establishes that an APK attempt failed.
|
||
Missing OBBs are a hypothesis; the edition/build, exact files, failing step,
|
||
and runtime compatibility remain unknown. Record those details, Frame Control
|
||
and SteamOS versions, and sanitized server/crash errors before diagnosing it.
|
||
`python3 ui/frame_android.py info /path/to/game.apk` inspects the APK locally.
|
||
Successful installation or fixture tests do not establish RE4 compatibility;
|
||
OBBs cannot fix ABI/API, VrApi/OpenXR, entitlement or Android-service failures.
|