From c3cc4120c0c75119179cb838e8b189d0d529ad19 Mon Sep 17 00:00:00 2001 From: freetlab Date: Fri, 7 Aug 2026 12:52:55 +0200 Subject: [PATCH] fix(android): stop the resource shrinker from deleting Dart-named drawables Root cause found, and it is not the stale build cache I claimed earlier. flutter clean was good hygiene and changed nothing here, because nothing was cached: the resources were being deliberately removed. Flutter's own Gradle plugin enables shrinking on every release build -- FlutterPlugin.kt, `releaseBuildType.isMinifyEnabled = true` and `isShrinkResources = true` -- no matter what app/build.gradle.kts says. The shrinker keeps what it can see referenced, and it cannot see `MediaControl.custom(androidIcon: 'drawable/ic_auto_eq_on')`: that is a string inside Dart, resolved at runtime through getIdentifier. So both equalizer icons were stripped from every release APK ever built. The evidence that pins it, from the APK pulled off the device: ic_stat_pluriwave present <- referenced as R.drawable from Kotlin, 4 call sites in the alarm notifications ic_auto_eq_on absent <- named only in a Dart string ic_auto_eq_off absent <- named only in a Dart string Same folder, same file shape, same commit range. The only difference is whether a real R.drawable reference exists, which is exactly what the shrinker looks for. The consequence was never a blank button. getResourceId returns 0 for an unresolvable name, PlaybackStateCompat.CustomAction.Builder throws on a 0 icon, and that throw aborts AudioService.setState before the media session is activated -- so Android Auto held a frozen, inactive session. Dead playback screen, play that never became pause, the app losing its pane to any app with a live session, audio playing "as if it were not the app". One shrunk file, four symptoms, since 31 July (2540556). Two protections, because they fail differently: - res/raw/keep.xml with tools:keep is the official mechanism for dynamically resolved resources and is what actually binds the shrinker; - RecursosResueltosPorNombre.kt gives them genuine R.drawable references, the same thing that kept ic_stat_pluriwave alive all along. station_art_* are kept too. They are reached the same way, through android.resource:// URIs built in Dart, and survived only by luck. Tests: 1165, unchanged -- this is a build-configuration fix, and no Dart test can see it. The CI resource guard is what verifies it now. --- .../pluriwave/RecursosResueltosPorNombre.kt | 44 +++++++++++++++++++ android/app/src/main/res/raw/keep.xml | 33 ++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 android/app/src/main/kotlin/es/freetimelab/pluriwave/RecursosResueltosPorNombre.kt create mode 100644 android/app/src/main/res/raw/keep.xml diff --git a/android/app/src/main/kotlin/es/freetimelab/pluriwave/RecursosResueltosPorNombre.kt b/android/app/src/main/kotlin/es/freetimelab/pluriwave/RecursosResueltosPorNombre.kt new file mode 100644 index 0000000..c520308 --- /dev/null +++ b/android/app/src/main/kotlin/es/freetimelab/pluriwave/RecursosResueltosPorNombre.kt @@ -0,0 +1,44 @@ +package es.freetimelab.pluriwave + +/** + * Anchors the drawables that only Dart names, so the Android build cannot + * decide they are unused. + * + * These icons are handed to `audio_service` as plain strings + * (`MediaControl.custom(androidIcon: 'drawable/ic_auto_eq_on')`) and resolved + * at runtime through `getResources().getIdentifier(...)`. Nothing on the + * Android side of the build ever mentions them, so as far as the resource + * pipeline is concerned they are dead weight — and they were dropped from + * every release APK. + * + * The damage was not a missing icon. `getResourceId` returns 0 for a name it + * cannot find, `PlaybackStateCompat.CustomAction.Builder` throws on a 0 icon, + * and that throw aborts `AudioService.setState` before the media session is + * ever activated. Android Auto was left holding a frozen, inactive session: + * dead playback screen, a play button that never became pause, the app losing + * its pane to whichever app did have a live session, and audio that played + * "as if it were not the app". One absent file, four symptoms, from 31 July + * (commit 2540556) until this. + * + * Verified rather than assumed. Pulling the installed APK off the device and + * reading its resource table showed `ic_stat_pluriwave` present and both + * equalizer icons absent — and `ic_stat_pluriwave` is the one drawable of the + * three that Kotlin references directly (`R.drawable.ic_stat_pluriwave`, four + * call sites across the alarm notifications). That contrast is the whole + * diagnosis: a real `R.drawable` reference survives, a name that exists only + * inside a Dart string does not. + * + * So this object is not defensive tidiness — it is the reference that was + * missing. Any future drawable that Dart resolves by name must be added here + * AND to the resource guard in `.gitea/workflows/build.yml`, which reads the + * built APK's resource table and fails the build if one of them is gone. + */ +@Suppress("unused") +internal object RecursosResueltosPorNombre { + val anclados: IntArray = + intArrayOf( + R.drawable.ic_auto_eq_on, + R.drawable.ic_auto_eq_off, + R.drawable.ic_stat_pluriwave, + ) +} diff --git a/android/app/src/main/res/raw/keep.xml b/android/app/src/main/res/raw/keep.xml new file mode 100644 index 0000000..8774827 --- /dev/null +++ b/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,33 @@ + + +