Installs from the Android store, Import from my Quest and My Meta library
now go into one server-side queue and run one after another, instead of
being refused while another runs. The page shows the queue with Remove
for installs that haven't started; each queued install runs only on the
headset it was queued for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A game linking libovrplatformloader.so now installs with a warning ("uses
Meta's licence check; it may quit, hang, or run without online features
on the Frame. Frame Control won't remove the check."): linking the SDK
doesn't mean the check is enforced (VRC.Quest.Security.1 has only been
Recommended since 2021). 32-bit-only and API > 30 are still refused. The
VrApi warning says "may not run". The data step (copy into the open game,
then close it) becomes add_game_data, for reuse by My Meta library.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Streaming writers (Python's unseekable ZipFile, force_zip64) write ZIP64
data descriptors even for small files, so the descriptor width can't be
taken from the final sizes. The reader now accepts the signed or unsigned,
32- or 64-bit layout that matches the central directory and fits before
the next entry, preferring one that ends exactly there, then the width the
local header's ZIP64 metadata implies. Format tests cover streamed ZIP64
stored and deflated entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Installs made before records kept a version code no longer get replaced by
an older Quest copy: the installed code is read from app.apk on the Frame
(stdlib parser, only the manifest crosses the link); if it can't be read,
replacing needs allowDowngrade. The data-copy guard uses the same lookup.
- Split members are checked against both ZIP headers: same local name and
method, local sizes and checksum (or a validated data descriptor, with
ZIP64 extra fields understood) equal to the central directory, no overlap
with the next entry, and no trailing bytes after a deflate stream.
- The game is closed in a finally block, so a local OSError or Ctrl-C part-way
through a copy still closes it; the original exception is kept and a failed
close is reported separately.
- running_instances(strict=True) fails closed (a failed `podman ps` raises),
used by the Quest import; other callers keep the lenient listing.
- The dumpsys version check reads only the package's own block.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An older Quest copy no longer replaces a newer install on the Frame: installs
now record versionCode, and a downgrade is refused unless allowed
(--allow-downgrade, "allowDowngrade", or the panel's checkbox).
- The game-data copy only targets this package's own install record and
instance at the Quest's exact version code, and checks Android's own
dumpsys in that instance before copying.
- After the (possibly long) pulls the target is checked again, and the final
checks, the transfer and closing the game hold the shared install lock,
now reentrant so install_obb and install can be called under it.
- Closing the game is confirmed; a failed or incomplete close is reported
instead of claiming the game was closed.
- Split members are read from their raw bytes: stored members must be exactly
their declared size, deflated streams must end exactly there, then the
checksum; forged sizes with prefix checksums are refused.
- A split's resource table counts as empty only if no type chunk defines any
entry (bags included) and every chunk is understood.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Split merging only accepts splits holding lib/<abi>/*.so, assets/, their own
signature/manifest and an empty resource table. Code (classes*.dex), other
files, resource splits, feature splits and split manifests declaring
components or libraries are refused before anything reaches the Frame.
- Merged bytes are bounded (512 MB per file, 1 GB in all) from the headers and
again by bounded reads with a checksum, so a zip bomb or a forged header
can't fill memory.
- Import never starts the game. The first import installs it; game data (OBB,
Android/data) is copied by a second import while the person has the game
open, never replacing an open game's APK, then the game is closed.
- Android/data is streamed as a tar straight into the SSH transfer, not
staged; the space check counts split payloads uncompressed and data once.
- The panel shows small sizes in KB rather than "1 MB".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New ui/frame_quest.py (CLI: devices | list | import), GET/POST /api/quest and
an Import from my Quest panel on the Android page. It finds a Quest on this
computer's adb (never Frame Control's own tunnelled serials), lists third-party
apps with split, OBB and Android/data details, pulls the APKs into a per-user
cache and checks them before copying anything else: Meta's Platform SDK
licence check (libovrplatformloader.so) and 32-bit-only builds are refused,
VrApi-only builds flagged. It then installs through the normal APK path, adds
OBBs with the existing helper and, if asked, Android/data files, and always
removes the cache. No licence-check bypass is bundled, called or linked.
Tests use a fake adb (tests/fakeadb.py). Docs: docs/quest-import.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>