Files
minstrel/android/app
bvandeusenandClaude Opus 5 5044e7a055
android / Build + lint + test (push) Failing after 3m7s
fix(home): hold the updating veil until Home actually settles — #2327
The "Updating your mixes…" veil wiped on and straight back off before the
update finished, and a number of churn paths never raised it at all.

Three reasons it lowered early. refreshBehindVeil held it for
refresh().join() + a flat 500ms, but finishing the network pull is nowhere
near the end of the visible work: refreshIndex writes only the section id
lists, then each tile hydrates through MetadataProvider (null → skeleton →
album), and only then does the cover art load. Second, updatingInternal was
a plain Boolean cleared in a finally — reconnect and playlist.system_rebuilt
routinely arrive together, so whichever pull finished first wiped the veil
off while the other was still running. Third, refresh() swallowed every
failure in runCatching, so join() returned "fine" after a failed pull: veil
off, content unchanged, no retry.

So the veil's lifetime is now driven by watching the screen instead of by a
guess. UpdateVeilController raises, runs the work (retrying behind the veil),
then holds until the content signature has been unchanged for a quiet window
AND nothing is still loading — floored by a minimum hold so it cannot flash,
capped by a hard ceiling so it cannot strand, and with overlapping triggers
folded into one session rather than racing it. Giving up is silent and sets
no latch: the reconnect-driven recovery and the freshness sweeper keep
retrying afterwards exactly as before.

Cover art was the most visible pop-in and the refresh coroutine cannot see
it, so the composition reports it upward: ServerImage — the single choke
point behind CoverTile for every album/artist/playlist cover — counts its
in-flight loads into an ArtSettleTracker the veil waits on. Art also
crossfades now (set once on the ImageLoader, so it applies app-wide) with
the placeholder fading out over the same window, which softens the pop
everywhere the veil isn't involved.

Underneath all of it, the churn is largely no longer generated. replaceSection
was delete-then-insert per section, un-transacted, so observeBySection emitted
emptyList() — a visible collapse — before refilling, seven times in sequence.
It is now one @Transaction across all sections (Room notifies once, on commit,
so the empty gap is never observed), and the index flow dedups on the id list,
so a section whose contents did not move no longer tears down and rebuilds
every tile's hydration flow. fetchedAt is restamped on every write, which is
why the dedup compares ids rather than rows. Same fix CachedQuarantineDao
already carried for the same reason.

Trigger set widened per the operator's call: the initial load over a warm
cache (a full re-pull that churned every section completely unveiled), manual
pull-to-refresh, scan.run_finished (Home never reacted to it at all), and the
playlist.created/updated/deleted/tracks_changed kinds. The veil waits for
content to be on screen before raising, so a genuinely cold load still gets
its skeleton rather than an opaque panel over nothing.

refreshError is now cleared on success rather than at the start of each
attempt — with retries, clearing it up front made a failing cold start flash
the "Welcome to Minstrel" empty state between attempts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 20:31:30 -04:00
..