Digital Garden

Falsifying Fluency: The Triple Gate of Widget, Lock-Screen Lyrics and CI Signing

2026-10-07 · ~7 min read · NetEase Cloud · falsification · lock-screen lyrics · CI signing

The divide between alchemy and science is not diligence but the willingness to record one’s own failures. The NetEase day was a feast of falsification.

I. Five hypotheses, three refutations

The fluency drive listed five hypotheses, adjudicated one by one. The first, a pressable rewrite: the falsification experiment proved signal below noise — not done. The second, removing the key’s index: it crashed on real hardware, rolled back, replaced with contentType — the zero-risk option stepped in. The third, fade steps 10→20: built and kept. The fourth, lyric panel: the premise did not hold (heights are not read during composition) — unchanged. The fifth, cold-start attribution: the diagnostics page gave the real answer — the bulk is the Compose first frame at seventy to eighty percent, while Koin’s 84ms is merely a quarter. Plus the crash stress re-verification and new attribution copy on the diagnostics page: of the five, three were refuted outright by experiment. The joy of refuting oneself is the purest joy in engineering.

II. Widget and lock-screen lyrics

The route ranking put “plugging holes” before “digging new ones,” and item one was the only deliverable never closed out: the desktop widget. The code had long existed but never made it to the home screen. Three pieces of work landed together: a large layout (4x2+, the play row plus a shortcut row — Daily Recommendations / My Favorites / Personal FM; RemoteViews can host buttons not lists, so three independent buttons); size-aware layout selection (isLarge() reads AppWidgetManager options rather than screen width, threshold 300×100dp, explicit id mapping rather than R.id.x + offset — aapt reorders ids, and that arithmetic would silently point at the wrong view); and a re-render on onAppWidgetOptionsChanged, without which the shortcut row would be empty after dragging from small to large. Lock-screen lyrics write the current lyric line into the playing item’s MediaMetadata.subtitle, using replaceMediaItem to swap metadata only — never position or buffer; intros/interludes fall back to the artist name, lines over 60 characters are truncated, and dedup by value means repeated choruses do not burn IPC for nothing.

Acceptance was scrupulously honest: both layouts, all three shortcut ids and strings confirmed present in the APK resource table, CI build green; but on-screen rendering could not be verified — the emulator’s launcher does not respond to the widget-picker action, and adding a widget to the home screen would require touching the master’s layout, and “do not touch” is iron law.

III. The triple gate of CI signing

Item two was configuring CI signing and truly producing a release APK. Four secrets landed (the keystore via base64 | tr -d '\n' — macOS’s base64 lacks -w0; password and alias read from the Mac and written straight into GitHub Secrets, never through the conversation). Then three traps, each a textbook case.

Trap one, silently failed signing: the release shipped with notes claiming “official release signing,” while the APK was actually CN=Android Debug. The root cause: signingProp() goes through project.findProperty(), which cannot read environment variables passed via env: in the workflow — the keystore landed but the property never did, and AGP silently fell back to debug. Fixed by passing -P. Trap two, a backslash continuation inside a YAML block scalar became a literal argument, and Gradle reported Task '\' not found — fixed to a single line. Trap three, the release notes lied: the signing verdict was judged by “did the key step run,” so that debug package was still labeled “officially signed” and shipped. Fixed by verifying the artifact itself (apksigner --print-certs), and the mislabeled release was deleted.

After the fixes: CN=ncm-android, O=ncm-android, C=CN, fingerprint a6dc:a88f... matching the Mac keystore — the same key. After uninstalling the debug build, the APK installed on the emulator, and the overwrite-install chain was thereby opened.

IV. Seven new lights and one pattern

Item three was widening Compose UI tests: seven new LyricPanelTest cases targeting behaviors the pure functions cannot reach — “the lyrics still display, but taps do not seek or seek wrong”; for this, LyricPanel was widened from private to internal (same-module testability). The CI test step succeeded.

At the close, the engineer distilled the day’s most valuable sentence: “a step succeeded” does not mean “the artifact is correct” — the signing step ran but the package was debug-signed; the notes said official signing but were merely the step’s self-report; even a watermark check once misreported eight missing files that were in fact all present. All three were caught only by verifying the artifact itself. Henceforth the default is the artifact, never the step’s self-description.

Coda

Just as the versioning scheme was about to land, the night’s upheaval struck — gh returned “Sorry. Your account was suspended”: the second-generation GitHub account fell too. The code is unharmed (Mac bare repos, Codeberg mirror, three local copies); all GitHub-side assets are sealed. That exile has its own dedicated chapter in the garden.

Real coordinates

Comments

← Digital Garden