A store's library shows from its saved copy at once and is refreshed in
the background at most every six hours, or from the account row's
Refresh. The result is merged so unchanged cards keep their objects:
only changed cards recompose, and the grid's scroll and the pad's focus
stay. Shelves saved on an earlier run count as fresh for three hours.
Sizes are kept per game and version in filesDir/stores/<store>/
sizes.json, format-versioned, and looked up again only for a new
version. An installed card shows its size on disk with a small green
check after it.
Cards are a quarter smaller: 112 dp least width for shelf cards and grid
cells, the grid fitting as many columns as the width allows, so a
density setting changes the column count instead of the card size.
Epic's and Amazon's library answers carry no size, so their cards
showed none. A card on screen, or close to it, now has its size looked
up once - Epic's from the build manifest (the files this device
installs), Amazon's from its download manifest - three at a time in the
background, and kept in the store's prefs where the library reads it.
The size sits at the right of the title strip in the title's own type;
nothing shows until it is known, and a marker keeps the slot.
The dropdown used the menus' right-aligned placement and their 220 dp
minimum, so under a tab near the left it was pushed to the edge over
the rail. AnchoredMenu has a compact mode now - under its anchor, left
edges aligned, as wide as its entries between the tab's width and
260 dp - which the Library dropdown uses.
The All tab's count mixed owned games with whatever the shelves had
loaded (GOG: Library 33, All 120). A store now has Store and Library;
Library's dropdown filters All - every owned game, the default and the
tab's count - or Installed, and the tab reads the filter. The old All
view is gone. Amazon has Library alone. Search follows the filter; the
shoulder buttons cycle Store and Library.
The sticky chip row painted the theme's flat background colour, a near
black box against the front end's tinted background. It paints nothing
now; the content scrolling beneath is clipped at the row's edge and,
once scrolled, faded out there through its own alpha rather than a
colour laid over it.
The row actions are round icon buttons with their labels as
descriptions: pause, resume, play, retry, and a cancel that asks with a
red delete and an undo. Downloads under way stay on top; finished ones
sit below a line whose X clears them all - rows only, a failed
download's kept files stay for its game page's Clear - with installed
games as a compact row with Play. Records the pre-PR device checks.
Import saves and the two Export rows lose their descriptive subtitles,
in Manage saves and Cloud saves alike; the rows that carry live values
keep them. The three strings are removed from every locale.
It opened with a Checking row and re-laid out a second later, shifting
every row and the pad's focus. The switch, Upload and Download are now
there from the first frame - the switch focused, its state read from
the sidecar; the two actions disabled with an ellipsis until the cloud
check is back - and only their subtitles change after. A conflict turns
Upload and Download into Keep local and Keep cloud in place.
A GOG or Epic game's hero button reads Cloud saves and opens its saves
view, which now holds the Cloud saves switch with the sync status,
Upload, Download and the conflict actions, or No cloud saves when the
store keeps none. The switch is gone from the cards: GOG has no card
left, Epic's keeps its sign-in, offline and Resolve actions. Steam and
Custom games keep Manage saves. Records the device proof of cloud saves
with ELDERBORN.
Cloud saves no longer depend on the compat tool's exit hook alone,
which never runs when the session is closed from the drawer. The
pre-launch step marks the game dirty in the app's storage, and the
upload runs on the first of the exit request, the end of the session
(after teardown) or the app's next start for a mark a killed app left,
not while a session runs. The mark goes only after an upload went
through or a final skip; each logs its trigger. The planner test runs
under Robolectric for org.json.
Downloads keep going with the screen off: while one runs the service
holds a Wi-Fi lock (high-performance before Android 14, low-latency
from it) and a partial wake lock taken with a timeout and renewed with
the progress, both released as soon as nothing runs. The notification
has a real progress bar and the Downloads row's line (game, percent,
sizes, speed, time left; the stage while installing), Pause / Resume,
and a Cancel that first asks Delete / Keep for ten seconds. A paused
download leaves a notice to resume from; an installed game posts a
notice that opens it in Games, a failed one opens Downloads. On
Android 13 and later notifications are asked for once, the first time
an install starts from a screen.
5G hotspot run (ELDERBORN, GOG): the window grew 31 -> 84 while each request's
delay went from 3 s to 11 s and throughput stayed at 10-20 MB/s, and the Fastly
host froze at 49 MB for the rest of the run while gcdn carried everything, its
requests waiting out their timeout. Both live in the shared core, so GOG, Epic
and Amazon all get the fixes.
Latency: every request's time to first byte is measured; the lowest per-probe
mean is the run's baseline. A probe whose mean is 2.5x the baseline and at
least 250 ms above it stops growth (hold:latency) unless throughput rose 25 %;
if the delay keeps rising on the next probe the window gives back 15 %
(shrink:latency, then the usual cooldown). A fast link's TTFB stays within a
few round trips however wide the window gets, so it is not affected. The
fetch-window line gains ttfb, ttfb_base and stalled_hosts.
Stalls: bytes are counted per host as they arrive (whole-body mode now reads
the body piece by piece too). A host whose in-flight requests receive nothing
for 10 s while another host delivers has those requests cancelled and put
back at the head of the queue, and rests for 30 s (doubling per repeat, up to
5 min, reset by a success): host-stall host=... requeued=N. A cancelled
request is not held against the item.
Deadlines: whole-body requests keep one deadline for headers and body; a
stream may now sit at most 30 s without a byte (or the store's own timeout
when shorter), so a dead connection on a single-host run cannot hold an item.
Host tests: a simulated rising-delay link (hold, then x0.85 shrinks, never
grows again), a fast link (keeps doubling), and a two-host run where one host
freezes after 100 bytes (detected, items moved, well inside the timeout).
On device nothing ran for ELDERBORN: a GOG game without arguments has
no launcher .bat, Steam runs its exe, and droiddeck-store-launch only
looked for the .bat. It now recognises the exe too, looking through the
folders above it for the sidecar, and every path logs a result line.
A first launch has no prefix yet: the download is answered as deferred
and runs once Proton has made the prefix's user folder, within a minute,
without holding the launch.
Uploads are safe now. CloudPlan decides per file against this device's
baseline - the local and cloud state at the last sync: no upload before
a download has completed once, the first download lets the cloud win,
and a file changed on both sides is a conflict that Manage saves offers
to settle with Keep cloud or Keep local. The cloud copy is backed up
before an upload replaces it, as the local folder is before a download.
Wires the engine's new contract (0849a7c): the native start takes the
whole CDN set from the secure link, and the engine's periodic byte
progress - every 250 ms, files still in flight included - drives the
bar and the speed, so a large file no longer moves the bar in one jump.
The count shown adds what earlier runs and verified skips already
credited and never goes back.
Device report (ELDERBORN): the run used one CDN (used=1/1, fastly) at 12-13 MB/s
with the window pinned at its ceiling, and the progress bar jumped because
onProgress only fires when a whole file is finished.
nativeStart's third parameter becomes cdnBases: String[] - every urls[] entry of
the secure-link answer (or the one dependency store base). Bases are deduped by
host, each chunk item carries one URL per host and the fetch core spreads the
window across them with a per-host cap of max(6, ceil(workers / hosts)); one base
behaves as before. The start line logs hosts=a,b,c.
A ticker thread beside the fetch reports onBytes(done, total) every 250 ms when
the value moved: assembled bytes plus in-flight files (gen2: finished chunks;
gen1: the current attempt's pieces, reset on retry so nothing counts twice).
A listener without onBytes still works. JNI.md has the new signature and
callback. Epic (per ~1 MiB chunk) and Amazon (per written piece) already report
more often than 4/s.
GOG's secure link lists several CDN hosts and only the first was taken,
so every request went to one host. All are parsed now and kept as one
set, refreshed together when the link expires; the built-in pool takes
them in turn and the native engine gets the first until its contract
takes a list. The progress bar fill glides to each new value over a
quarter second, drawn without relayout.
Ported from Bannerlator's cloud-save managers. Before every launch the
cloud copy comes down where it is newer: droiddeck-store-launch asks the
app and waits up to 15 s. For a store game with cloud saves it tells the
compat tool so, and the tool waits for Proton instead of exec'ing it,
then asks for the upload without waiting. Newest wins per file, with an
MD5 check so a file the game only touched is not sent again; a download
first copies the folder to the app's storage (last three kept).
The save folder is GOG's remote-config location template (by the game's
Galaxy client id) or Epic's CloudSaveFolder catalog attribute, expanded
inside the game's Proton prefix or install folder by CloudSavePaths,
which refuses a path that leaves its boundary. GOG uploads with a token
issued to the game's own client (its secret is kept at install from now
on), Epic with signed write links. The GOG and Epic cards get a Cloud
saves switch kept in the sidecar; Manage saves shows the last sync with
Upload and Download, or No cloud saves.
Device-proven on bf81104: Metalstorm's EOS corrective action fell back to
a browser, the hand-off opened it in Android's browser, the user approved,
and the game signed in. With the overlay pointer set the game exited
while loading on every launch, so the overlay goes: no switch on the
Epic card, no download, no OverlayPath. A launch removes a pointer an
earlier build wrote, and the app deletes an earlier 656 MB download once;
an overlay field in an older sidecar is read past. The work is parked on
park/epic-eos-overlay. The browser hand-off, the Epic card's sign-in,
offline and Resolve actions, and BL_DEBUG_BROWSER stay.
A game launched from the Games tab with no session running sat at
Launching: the session put steam://rungameid on the client's command
line, the client took it seconds before its interface was up, and its
launch stopped waiting for a response to ShowInterstitials that nothing
gave. The same launch from inside Steam went through. The URL is now
held back and handed over through the in-session steam-game request once
gamescope's focus is the Steam interface and four seconds have passed,
or after two minutes regardless.
EOS's browser flow never reached Android because the hand-off script
was not in the APK: the build copies only usr/local/bin/droiddeck-*,
steam-compatibility and usr/bin/** of the overlay into the assets, so
usr/local/lib/droiddeck/browser/xdg-open was skipped, the session staged
nothing, the wrapper left the PATH alone, and Wine's winebrowser ran the
runtime's own xdg-open, which has no browser to open in a Steam session.
GE-Proton's winebrowser is Wine's own: its Browsers list, then xdg-open
by name, with no steam://openurl route.
The script is now usr/local/bin/droiddeck-open-url: named as Wine's
browser in the prefix by its absolute path, linked as xdg-open first on
the game's PATH, and reading the session channel from a file when Wine
does not pass BL_LAUNCH_DIR on. BL_DEBUG_BROWSER=1 adds Wine's shell and
winebrowser traces. The Epic card offers Resolve Epic sign-in, Epic's
account page behind its sign-in, where a pending corrective action is
presented.
With the overlay pointer set Metalstorm exits while loading; without it
the game signs in and runs. The overlay now defaults to off: the
sidecar's epic block carries a version, and a block from before it (the
old default wrote overlay=true unasked) reads as off, in the app and in
droiddeck-store-launch alike. The overlay is fetched only for a game that
turns it on.
The card's switches read the sidecar fresh, write it and read it back,
and show what was read back. droiddeck-store-launch waits for a
wineserver still holding the prefix before appending to user.reg - one
that exits rewrites the file and drops the line - reads the value back
and logs what the file holds. Its docstring is raw. The game-PATH
xdg-open logs each call as scheme://host only and accepts
steam://openurl.
Metalstorm's Player.log printed its environment, and with it the
exchange code: the launcher read the code into a variable the game
inherited. The variable (now with a plain name) is cleared on the same
line that starts the game - cmd has expanded it there already - so the
game gets the code as arguments and nothing of it in its environment.
A launcher from an earlier build is rewritten before the next Epic
launch, from the app and from the Steam-launch request alike.
Metalstorm's page showed its own name again under the hero, where the
store's description was just the title. Such a description is now no
description.
droiddeck-store-launch's overlay check raised on a folder it may not
read (the CI runner's /root); it now treats that as not installed, as it
must for any launch.
Epic games get an Epic card in the Games tab's launch settings: Epic
sign-in (EOS) on, Launch offline off, Epic overlay on. The choices live
in the game's sidecar and survive an update, so both launch paths read
the same thing - the app before its own launches and droiddeck-store-
launch for one from the Steam client: no code when sign-in is off or
offline, the overlay pointer written or removed per the switch.
Metalstorm's EOS sign-in asked for a one-time consent, could not load an
overlay to show it, and fell back to a browser flow that never finished.
Epic's own overlay component (the mechanism from Legendary's lfs/eos.py;
credits in EpicOverlay.kt) is now downloaded once with the account's
token into the runtime, after an Epic library sync or at the first
launch that finds it missing. At each launch droiddeck-store-launch
writes the one pointer, HKCU Software\Epic Games\EOS OverlayPath, into
the game's prefix, appended to user.reg as the DirectAudio key is, and
the app logs epic overlay installed=<build> prefix=<appid>.
Wine's browser is set to xdg-open, and on a game's PATH a small xdg-open
hands http(s) addresses to the app, which opens them in Android's
browser, so a device-code sign-in can be finished on the phone.
Metalstorm started from the Steam client's own Play button came up as a
guest: the exchange code was only minted when a launch began in the app.
The compat tool now runs droiddeck-store-launch on every real launch. It
finds the store launcher among Steam's arguments and, for an Epic game,
asks the app through the session's stores/req and stores/resp folders,
waiting up to five seconds. The app refreshes the token, mints the code
and writes it into the game's own folder; the code never crosses the
channel or the log. No answer and the game starts with its offline
identity; a code file older than five minutes is dropped first.
Every attempt logs epic launch id=... code=yes|no reason=...; a sign-in
that ran out dims the Epic chip and brings back the sign-in card.
Amazon has no public catalog, so its Store tab repeated the library row
for row and All was the library again. Its tabs are Installed and
Library; the open-on Store choice falls back to Library for it.
The shelves lose their subtitle lines. The chip row sits on the page's
background with room under it, and the content scrolling beneath is
clipped there, with a short fade where it meets the row.
The storefront's shelves and search leave out titles the store itself
rates or tags as adult unless Show mature content is on (Setup > Stores
and the cog). Library, Installed and owned games always show, and a title
without rating data stays. GOG's catalog gives ratings[].ageRating per
system and content tags; 17+ in any system or a mature / nsfw / sexual
content / nudity tag marks a title. Epic's store GraphQL carries no age
rating on an offer, so only an adult content tag would mark one.
On the device the menus' animations and controller navigation slowed for as
long as an Epic or GOG download ran: the engine's threads competed with the UI
at nice 0. Each thread the library starts now lowers itself first - the run's
worker (all three stores), the inflate/hash/write pool, the throughput
reporter, GOG's verify pass, the Epic assembler, and through on_thread_start
the tokio runtime's blocking pool (DNS). The JNI calling thread is the app's
and is left alone. Behaviour is otherwise unchanged; any process-worker count
from 1 up was and is accepted.
The plan/start line of every run logs the worker's actual nice value
(priority=nice10). libc becomes a direct dependency; it was already in the
tree through tokio at the pinned version.
Engines report per chunk or per file, and every report published a new
queue that the whole front end read - the rail badge was computed from
it in the root composition. Progress-only changes now reach the UI at
most four times a second (a stage or state change at once); each row and
card reads only its own entry; the list is replaced only when rows come,
go or change state; the badge count changes only when it changes; the
notification redraws at most once a second.
Download threads run at background priority - the queue's runner and
every manager's pool - and the tier leaves two cores for the UI. The
stores log is written in half-second batches.
The tab's count came from the installs on disk and its grid from the
library filtered by id, so two installs could read Installed (2) over an
empty grid. Both now come from installedCards: each install joined to its
library item by store id, or by title when the ids differ, else a card
made from the sidecar's own title and art.
A size of zero read as nothing ("Downloading 0% · / 1.1 GB"); it reads
0 B now, and small sizes in bytes. GOG and Amazon report the size on disk
from their manifests, shown beside the location as Epic's is. GOG writes
files in place while it downloads, so its Install stage has no file move
to count; it and Amazon's now count the finishing steps (the record and
launcher, the art, Steam) instead of sitting empty. GOG's plan line says
the window the tier gives the engine (Max = 96).
Epic checks chunks while it fetches and never enters Verify; its Verify
segment stayed empty while Install filled. A stage before the active one
is now drawn full whether or not it was entered.
The Downloads page's engine log is gone. StoresState.logLine still
redacts every line and writes it to logcat, and now appends it to an
app-private file per day, filesDir/stores/logs/stores-<date>.log: seven
days kept (pruned as the app starts), each file rolling to .1 near 2 MB,
written in order on one background thread. The session's Share logs zip
carries these files under stores/, so they can be attached to a report.
StoresState.logLine is where every store line goes, the native engines'
included; it now passes the text through StoreLog.redactLine before the
Downloads log and logcat see it. Each URL keeps its scheme, host and a
plain first path segment only - a GOG secure link carries its token in
the path - with no query, fragment or userinfo; token-like values and
Authorization / Cookie headers are blanked outside URLs as well.
Epic's check of the files already there belongs to Manifest (Checking
n/N), not a Verify at 100% before anything is fetched. Install measures
its own write rate and ETA from the assembler's bytes and shows files
n/N. A row shows the download as fetched (compressed) against its total,
and the size on disk beside the location; Epic keeps the disk size for
the card and page.
Cancel asks twice and removes everything the install wrote - folder,
sidecar, chunk caches, scratch - unless the folder is a finished install
being repaired, which keeps itself; the row reads Cancelled for three
seconds, then goes. Pause keeps everything. A failed row reads Failed
with what is kept and offers Resume and Clear, Clear deleting as Cancel
does; the game page offers Resume install and Clear for an unfinished
install. Library sync is a thin bar and its count.
A 13 GB Epic install spent three minutes assembling behind a still bar.
The entry now carries the active stage's own count - an amount and, when
it counts files, the files - and which stages are already behind it. Epic
reports its verify pass and its assembly (native and the fallback loop),
GOG its chunk-cache removal. The Downloads row draws one segment per
stage, passed ones full and the active one filling, and reads e.g.
Installing 42% · 120/515 files; speed and ETA show only while
downloading, and leaving Download clears the measured speed. Cards and
the game page follow the active stage's percent.
A store card is its art edge to edge: sharp above, and under its one line
a blurred, darkened copy of the same picture placed to meet it without a
seam (blur on API 31+, a stronger scrim below). A Downloads row is the
game's wide art cropped to the row with a dark wash from the left, its
copy in white there and the actions on the right; the thumbnail is gone.
The Install dialog is one plain line, Install <game>:, over one row per
place (icon, name, free space), Cancel small beside the title.
A store card is its art and one line: the title, and right-aligned the
one value the tab does not already say - a check when installed, Resume,
the download percentage, the install size, or Free / price / discount -
with a thin bar over the art while downloading. The source chip, the In
library chip and the card's button are gone; every action lives on the
game page, which A on a card opens.
Setup > Stores and the cog popup keep only their control labels; the
download-controls caption, the Downloads footnote, the sign-in blurbs and
footnote, the engine note and the Install-to caption are gone, and empty
states are a few words. The Install-to dialog is the game's name over
"Install to" and the two tiles. A Custom game's hero has no eyebrow; its
chip says it.
Reverts 01c7248. The store and Steam glyphs are gone with their drawables;
the source chips are the text chips again (GOG, Epic, Amazon, Steam,
Custom) in the store colours, and the Stores chip row has its coloured
dots back, dimmed while signed out.
GOG, Epic Games, Amazon Games and Steam glyphs from Simple Icons (CC0) as
vector drawables, a folder-plus for a Custom game, each in a small rounded
square in the source's colours with the name as its description. Rows,
store cards and Downloads show the mark alone; the Games hero and a store
game's page show the mark and the name. The store chips atop the Stores
page carry the marks in place of the dots, dimmed while signed out.
A half-installed Epic game on the card (files up to a 0-byte one, 9.1 GB
of .chunks, no sidecar) showed in Games as a Custom folder: the sidecar
was only written at the very end, the queue's row lives in memory, and
the scan took any sidecar-less folder under a store root for a hand-added
game.
The sidecar is now written when an install starts, in state installing,
and rewritten finished at its end; a finished install being repaired
keeps its record. Folders still installing, or with no sidecar, are never
scanned as games; the Stores card and page offer Resume install, which
reuses the folder (and an Epic .chunks beside it) instead of a fresh one.
Writing the launcher or the finished record fails the download with its
message; the art and the Steam side stay best-effort.
The Epic cache holds whole chunk windows shared with files this device
does not install, so it outgrows the game; the free-space check now runs
after the delta pass and counts the missing chunks and files on their own
volumes.
The Install-to dialog is a 440 dp card with two tiles that are the
action, one line of caption and Cancel in the title row; the cog popup
is capped at the same width.
The host tests caught the order: onProgress fired while the chunk only that
file used was still on disk. A progress event now means the file is written,
verified and its chunks released.
EpicNative.run passes chunkCacheDir after installDir, as the contract now
has it: "" keeps the cache beside the game, a path is the scratch cache in
the app's own storage that an install to a card uses. EpicNative.assemble
wraps the new nativeAssemble the same blocking way: the pending files are
written from the cache and each chunk goes right after its last use, then
the folder. The manager hands both runs the same path, uses the assembler
when it starts and keeps its own write loop as the fallback; the engine's
cache no longer has to sit beside a game on a card.