android: the phone, on the real core — the mockup shell, playback, the media card, the catalog, the resolver, Save, the dev flavour

The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.

Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.

The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.

The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
This commit is contained in:
KeelrLeek committed 2026-09-13 09:30:06 -05:00
1 parent 972dd103cb
commit bb828ff2dc
102 files changed
+12258 -830

No files matched your search

+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# The detour build (ANDROID.md): cargo → strip → copy → gradle. Run from anywhere.
set -euo pipefail
cd /f/Coding/SoulStream
source src-tauri/android/env.sh
echo "== cargo $(date +%T)"
cargo build --lib -p app --target aarch64-linux-android --features tauri/custom-protocol 2>&1 | grep -v "^\s*Compiling\|^\s*Fresh"
# the grep above hides cargo's exit code; a failed compile must not package
# the previous library as if it were this one
test "${PIPESTATUS[0]}" -eq 0 || { echo "cargo failed — not packaging"; exit 1; }
SO=target/aarch64-linux-android/debug/libapp_lib.so
test -f "$SO" || { echo "no .so built"; exit 1; }
echo "== strip $(date +%T)"
DEST=src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a
mkdir -p "$DEST"
cp "$SO" "$DEST/libapp_lib.so"
"$BIN/llvm-strip.exe" --strip-debug "$DEST/libapp_lib.so"
ls -la "$DEST/libapp_lib.so"
echo "== gradle $(date +%T)"
cd src-tauri/gen/android
./gradlew.bat assembleArm64Debug -x rustBuildArm64Debug -q 2>&1 | grep -v "^$" | tail -20 || true
cd /f/Coding/SoulStream
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
ls -la "$APK"
echo "== done $(date +%T)"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# frontend → detour build → install on the connected phone → launch → screenshot
set -euo pipefail
cd /f/Coding/SoulStream
export MSYS_NO_PATHCONV=1
ADB="$(cygpath -u "$LOCALAPPDATA")/Android/Sdk/platform-tools/adb.exe"
echo "== vite $(date +%T)"
npx vite build 2>&1 | grep -i "error\|built in" || true
bash src-tauri/android/build.sh
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
echo "== install $(date +%T)"
"$ADB" install -r "$APK" 2>&1 | tail -2
# the media card is a notification; a debug install grants the permission up
# front so the first play never stops on the system's dialog
"$ADB" shell pm grant com.soulstream.app android.permission.POST_NOTIFICATIONS 2>/dev/null || true
"$ADB" shell am force-stop com.soulstream.app
# launch and screenshot only when the phone is ours to drive: SoulStream or
# the launcher on screen. Never take the screen from another app.
FOCUS=$("$ADB" shell dumpsys window 2>/dev/null | grep -a "mCurrentFocus" | head -1)
if echo "$FOCUS" | grep -qi "soulstream\|launcher"; then
"$ADB" shell am start -n com.soulstream.app/.MainActivity >/dev/null
sleep 5
OUT="${SS_SHOT_DIR:-$(cygpath -u "$TEMP")}"
"$ADB" exec-out screencap -p > "$OUT/px-1.png"
echo "== shot $(date +%T) -> $OUT/px-1.png"
else
echo "== installed; not launched (the phone is in use: $FOCUS)"
fi
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# The DEV flavour (ANDROID.md → "Iterating without cargo"): the detour build
# WITHOUT tauri's custom-protocol feature, so the library embeds the dev URL
# (http://localhost:1421) instead of dist/. On the phone, `adb reverse` makes
# the phone's localhost:1421 THIS machine's Vite dev server over the USB
# cable — no LAN address, no firewall hole, hot reload — and cargo runs only
# when Rust changes. Both flavours share target/: switching flavours
# recompiles tauri and the app crate once (~3 min), the leaf crates stay
# fresh — cheaper than a second 15 GB cache on this drive.
#
# bash src-tauri/android/dev.sh # build (when Rust changed) + install + tunnel + launch
# bash src-tauri/android/dev.sh --no-build # reinstall the last dev library, re-tunnel, launch
# bash src-tauri/android/dev.sh --build-only # just the library (the phone is busy or absent)
# bash src-tauri/android/deploy.sh # the embedded build again (what the phone keeps)
#
# Then start the "frontend" dev server (npm run dev / the Browser pane's
# "frontend" launch config). The phone needs it running — and the cable.
set -euo pipefail
cd /f/Coding/SoulStream
export MSYS_NO_PATHCONV=1
source src-tauri/android/env.sh
if [ "${1:-}" != "--no-build" ]; then
echo "== cargo (dev flavour) $(date +%T)"
cargo build --lib -p app --target aarch64-linux-android 2>&1 | grep -v "^\s*Compiling\|^\s*Fresh"
test "${PIPESTATUS[0]}" -eq 0 || { echo "cargo failed — not packaging"; exit 1; }
[ "${1:-}" = "--build-only" ] && { echo "== built $(date +%T)"; exit 0; }
fi
SO=target/aarch64-linux-android/debug/libapp_lib.so
test -f "$SO" || { echo "no dev .so built"; exit 1; }
echo "== strip $(date +%T)"
DEST=src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a
mkdir -p "$DEST"
cp "$SO" "$DEST/libapp_lib.so"
"$BIN/llvm-strip.exe" --strip-debug "$DEST/libapp_lib.so"
echo "== gradle $(date +%T)"
cd src-tauri/gen/android
./gradlew.bat assembleArm64Debug -x rustBuildArm64Debug -q 2>&1 | grep -v "^$" | tail -20 || true
cd /f/Coding/SoulStream
APK=src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk
echo "== install $(date +%T)"
"$ADB" install -r "$APK" 2>&1 | tail -1
"$ADB" shell pm grant com.soulstream.app android.permission.POST_NOTIFICATIONS 2>/dev/null || true
# the tunnel: the phone's localhost:1421 → this machine's 1421 (Vite)
"$ADB" reverse tcp:1421 tcp:1421
"$ADB" shell am force-stop com.soulstream.app
FOCUS=$("$ADB" shell dumpsys window 2>/dev/null | grep -a "mCurrentFocus" | head -1)
if echo "$FOCUS" | grep -qi "soulstream\|launcher"; then
"$ADB" shell am start -n com.soulstream.app/.MainActivity >/dev/null
echo "== launched $(date +%T) — serve the frontend on 1421 (npm run dev) if it is not up"
else
echo "== installed; not launched (the phone is in use: $FOCUS)"
fi
+23
View File
@@ -0,0 +1,23 @@
# The Android toolchain, for the detour scripts (build.sh, dev.sh). Sourced,
# never run. A bash session started before the user variables were set does
# not see them, so everything is spelled out here.
SDK="$(cygpath -u "$LOCALAPPDATA")/Android/Sdk"
NDKV=28.2.13676358
BIN="$SDK/ndk/$NDKV/toolchains/llvm/prebuilt/windows-x86_64/bin"
export ANDROID_HOME="$(cygpath -w "$SDK")"
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export NDK_HOME="$(cygpath -w "$SDK/ndk/$NDKV")"
export ANDROID_NDK_HOME="$NDK_HOME"
export ANDROID_NDK_ROOT="$NDK_HOME"
export JAVA_HOME="C:\Program Files\Java\jdk-17"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$(cygpath -w "$BIN/aarch64-linux-android26-clang.cmd")"
export CC_aarch64_linux_android="$(cygpath -w "$BIN/aarch64-linux-android26-clang.cmd")"
export CXX_aarch64_linux_android="$(cygpath -w "$BIN/aarch64-linux-android26-clang++.cmd")"
export AR_aarch64_linux_android="$(cygpath -w "$BIN/llvm-ar.exe")"
export PATH="$SDK/platform-tools:$PATH"
# tauri-build regenerates gen/android/tauri.settings.gradle and
# app/tauri.build.gradle.kts — the lines that include every plugin's Android
# module — ONLY when the CLI's variable names the project. Without it a new
# plugin (the media session) compiled fine and never reached the APK.
export TAURI_ANDROID_PROJECT_PATH="$(cygpath -w "$PWD/src-tauri/gen/android")"
ADB="$SDK/platform-tools/adb.exe"