Files
SoulStream/handoff/rounds/ins-r43-doc-release.txt
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

138 lines
8.3 KiB
Plaintext

## 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_<pid>` 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 <name> found for plugin
<class>`, which lands in logcat rather than on screen:
`adb logcat --pid=$(adb shell pidof com.soulstream.app)`.