## The release build (nobody has made one yet) Every APK in this port is `assembleArm64Debug` — every `deploy.sh`, every `dev.sh`, every walk, every build on Kyle's phone. The generated project turns **R8 on for `release` and only for `release`**: in `src-tauri/gen/android/app/build.gradle.kts` the debug type is `isMinifyEnabled = false` and names no proguard files, and the release type is `isMinifyEnabled = true` plus `proguard-android-optimize.txt` and every `**/*.pro` it finds in the app directory. R8 is the one thing that can break this app in a way no build can see. Tauri's Android plugins are reached by NAME, at three layers, none of which R8 can trace: - `register_android_plugin("com.soulstream.storage", "StoragePlugin")` becomes the string `com/soulstream/storage/StoragePlugin` and goes to `FindClass`; the instance is constructed through the JNI signature `(Landroid/app/Activity;)V`. - `PluginHandle.indexMethods()` walks the class and its superclasses with `getDeclaredMethods()` and files every `@Command` under `commands[method.name]`, every `@ActivityCallback` under `startActivityCallbackMethods[method.name]`. `invoke()` looks up the name Rust or the WebView asked for and throws `No command pickFolder found for plugin …` when it is not there. - Jackson fills every `@InvokeArg` class FIELD by field. So a minified build compiles, installs, launches and shows Home — and then a tap on Library folder, or the card's next button, does nothing at all. Four plugin modules expose eleven commands, four activity callbacks and four argument classes that way. ```bash bash src-tauri/android/build.sh --release # or SS_ANDROID_RELEASE=1 bash src-tauri/android/build.sh adb install -r src-tauri/gen/android/app/build/outputs/apk/arm64/release/app-arm64-release.apk ``` With no argument `build.sh` is what it has always been — the same cargo line, the same `assembleArm64Debug -x rustBuildArm64Debug`, the same APK path — so `deploy.sh`, `dev.sh` and `ship.sh` cannot reach the release path by accident. The flag changes four things and nothing else: `--release` and `--features tauri/devtools` on the cargo line, the `release` profile's `.so`, `assembleArm64Release -x rustBuildArm64Release`, and a sign step at the end. **Where the keep rules live, and why in two places.** Each plugin carries its own `consumer-rules.pro` in `crates/tauri-plugin-*/android`, registered by one `consumerProguardFiles("consumer-rules.pro")` line in its `build.gradle.kts` — the same shape `tauri-android` and `tauri-plugin-dialog` already use in this build. (`tauri-plugin-opener` ships a `consumer-rules.pro` and never registers it; the file is inert. That is the mistake this line exists to avoid.) Consumer rules travel with the plugin, so a regenerated `gen/android` keeps them, and they are inert in the debug APK because a consumer file only does anything when the consuming app minifies. What belongs to no single plugin is in `src-tauri/android/proguard-soulstream.pro`, which `build.sh --release` copies into `src-tauri/gen/android/app/` for the `fileTree(".")` glob to pick up — it cannot simply live there, because `gen/` is generated and gitignored and `tauri android init` rewrites it. That file holds three things: the runtime annotation attributes, a `-dontwarn` pair for two `java.beans` classes jackson-databind references and Android does not have (R8 treats a missing class as a build *error*), and a keep for four `@Command` methods declared on `app.tauri.plugin.Plugin` itself — `registerListener`, `removeListener`, `checkPermissions`, `requestPermissions`. Tauri's own rule matches classes annotated `@TauriPlugin` and the members they declare; the base class carries no such annotation, so nothing names those four, and `registerListener` is what the lock-screen card's buttons and the download notice's Cancel/Open ride on. That last rule is a precaution, not a diagnosed bug — it is six lines, and it removes the question. **What already keeps the wry bridge, and must not be thrown away.** The `**/*.pro` in that glob is recursive, and the file it reaches that nobody thinks about is `app/src/main/java/com/soulstream/app/generated/proguard-wry.pro`, written beside the generated Kotlin by wry's own build script. It is what keeps `Rust`'s native methods, `WryActivity.setWebView`/`getVersion`/`getId`, `Ipc`'s `@JavascriptInterface` methods and `RustWebView.evalScript` — the entire JNI bridge between the Rust core and the WebView, all of it reached by string from Rust. `proguard-tauri.pro` beside it keeps `TauriActivity.getPluginManager()`. Neither is ours and neither is in git; both are regenerated. Narrow that glob to a named list and the app stops at a black screen. The four plugins are the gap because they are the only name-reached surface nobody else wrote a rule for. **What is different about a release build, beyond minification.** - `usesCleartextTraffic` flips to `"false"` (debug sets the placeholder to `"true"`). The page's own origin is `http://tauri.localhost`, which Tauri serves itself, so this should not bite — but it is the first thing to check if the WebView comes up blank. - Not debuggable, so `run-as com.soulstream.app` is gone: the documented way of pushing test music into the app's private Library folder does not work on a release build. Point it at a folder in shared storage instead. - DevTools exist only because `build.sh --release` passes `--features tauri/devtools`; wry compiles `setWebContentsDebuggingEnabled` under `cfg(any(debug_assertions, feature = "devtools"))`. Without it there is no `webview_devtools_remote_` socket and `smoke.mjs --phone` has nothing to attach to. - The APK Gradle produces is `app-arm64-release-unsigned.apk` — there is no `signingConfigs` block in the generated project. `build.sh --release` zipaligns it and signs it with `~/.android/debug.keystore` (alias `androiddebugkey`, password `android`), the **same key the debug builds use**, so `adb install -r` replaces the app in place. Sign it with any other key and that install fails; the obvious next move — uninstall, then install — deletes the app's data, which on Kyle's phone means his settings, his account and his queue. This is a test build, not a distributable one. - `cargo build --release` fills a second `target/aarch64-linux-android/release/` tree; the debug artifacts are untouched, but that cache roughly doubles. - Both build types package `app/src/main/jniLibs/arm64-v8a/libapp_lib.so`, and a release run leaves the release library sitting there. `deploy.sh` copies over it unconditionally, so the next debug deploy is correct — but a Gradle run by hand in between would package the release library into a debug APK. - If lint fails on something unrelated, run Gradle by hand with `-x lintVitalArm64Release`; AGP 8 splits that into an analyze task and a report task, so `-x lintVitalAnalyzeArm64Release` may be the one that actually has to go. Untested — no release build has run here. **What to smoke-test on a release build, and only on a release build.** The walks do not cover this: the pipeline reinstalls the debug APK constantly, and what minification breaks is exactly the part that is hands rather than CDP — pickers, notifications and a share sheet. Every plugin command, one at a time: - **storage** (six commands, four activity callbacks) — pick a library folder (You → Library folder) and check the picked path comes back; the all-files access row and the system screen its button opens; pick an image (a playlist cover); pick a file (a playlist import); export a playlist, which is `createDocument` and has no JS-facing command at all — it is called from Rust, so it is the one a hand test forgets. - **media-session** — play something and watch the card appear (`update`); its prev, play, next and stop, which arrive through `registerListener`; stop playing and watch it go (`clear`). - **transfer-notice** — start a Save and watch the notice appear (`update`); Cancel and Open on the notice, also `registerListener`; let it finish and watch it go (`clear`). - **share** — share plain text to SoulStream from another app (`pendingShare`). A command that has been renamed answers `No command found for plugin `, which lands in logcat rather than on screen: `adb logcat --pid=$(adb shell pidof com.soulstream.app)`.