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.
138 lines
8.3 KiB
Plaintext
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)`.
|