Block B — enrichment lifecycle drift:
* SubscriptionFeedViewModel tracks enrichJob, cancelled in refresh
+ clearInMemoryCache so spam-refresh and cache-toggle no longer
leave a globalScope coroutine writing to a destroyed _ui
* Enrich now runs on viewModelScope, channels snapshotted at job
start so the terminal merge doesn't read a stale subs list
* mergeFromCache moved off Main on both the refresh path AND the
init-hydration path — 750-item flatMap+sort+regex no longer
blocks the UI thread
* VideoDetailViewModel dual loadedUrl bookkeeping collapsed to
the UiState field only; the rejected-URL path also stamps
loadedUrl so the gate reads coherently
Block A — auto-update authenticity:
* AppUpdateClient pins the fdroid.sulkta.com leaf SPKI + the
Let's Encrypt E7 intermediate via OkHttp CertificatePinner
* file.name accepted only when matching ^/[A-Za-z0-9._-]+\.apk$
* versionCode clamped to (0, 10_000_000] before we trust the
'update available' notification — a hostile index can no longer
pin us to MAX_VALUE
Block C — captureResumePosition perf:
* ResumePositionsStore.record short-circuits when the existing
entry matches position+duration so the 5s poll's
before !== next guard actually skips the SP write
* JSON encode + SP write off Main via globalScope IO
Block D — Rust feed.rs hardening:
* Shared reqwest Client via OnceLock — 50 channels no longer
pay 50 TLS handshakes
* Response body capped at 2 MiB via bytes_stream — adversarial
feeds can't OOM the JVM
* parse_rss returns partial results on quick-xml errors instead
of nuking everything already parsed
* extract_channel_id widened (m./www./http(s)?/trailing path)
and validates exact 24-char UC<22 base64-ish>
* Skip entries with empty title/published
* iso_to_relative future dates → 'just now' (clock skew
no longer pins items to top)
* civil_to_days year clamp 1970..=2200 before the i64 arithmetic
* Redirect chain capped at 3
* Dropped the broken lexicographic sort on upload_date_relative
* Cap parsed entries at 50 per channel
MED batch:
* ThumbnailProgressOverlay uses derivedStateOf so only rows
whose specific entry changed recompose on the 5s positions tick
* EnrichmentStore.put short-circuits on identical view+duration
so re-enrich within TTL doesn't write SP
* EnrichmentStore.load prunes TTL-expired entries on hydration
* FeedRefreshWorker distinguishes transient (Result.retry) from
parse (Result.success) failures
* WorkManager interval coerceAtLeast(15L) on both schedulers
|
||
|---|---|---|
| .. | ||
| strawcore | ||
| Cargo.toml | ||
| README.md | ||
rust/ — strawcore: Rust YouTube core for Straw
Phase U- of the Straw build. Goal: replace the Java NewPipeExtractor dependency with a Rust core (rustypipe + tokio + reqwest), exposed to the Kotlin/Compose UI via UniFFI. Compose UI stays in Kotlin — only the YouTube/Innertube fetching layer moves to Rust.
Phases
| Phase | What |
|---|---|
| U-1 | Toolchain + UniFFI smoke test (hello_from_rust) round-tripping through JNA. No real APIs yet. |
| U-2 | rustypipe search. SearchViewModel calls the Rust core. |
| U-3 | rustypipe streamInfo + streams. VideoDetailViewModel + PlayerViewModel use it. |
| U-4 | rustypipe channel + tabs. ChannelViewModel + SubscriptionFeedViewModel. |
| U-5 | Rip NewPipeExtractor Java dep. Measure APK + cold-fetch latency before/after. |
| U-6 (stretch) | SponsorBlock + RYD HTTP through reqwest + tokio in the same lib. |
Build chain
crafting-table
├── rustup stable (target add: aarch64-linux-android, armv7-linux-androideabi,
│ x86_64-linux-android, i686-linux-android)
├── cargo-ndk (cross-compile helper)
├── android-sdk (ANDROID_HOME, sdkmanager, build-tools, platforms)
└── android-ndk (ANDROID_NDK_HOME, r27c LTS at /caches/android-sdk/ndk/...)
Gradle (strawApp/build.gradle.kts)
├── cargoBuild Exec task → cargo ndk -t <abi>... -o jniLibs/ build --release
├── uniffiBindgen Exec task → cargo run --bin uniffi-bindgen ... --library libstrawcore.so
└── source-set wiring generated Kotlin lands in strawApp/src/main/java/uniffi/strawcore/
Runtime (StrawApp.onCreate)
├── System.loadLibrary("strawcore")
└── uniffi.strawcore.initLogging()
Why UniFFI (and not raw JNI / JNA bindings)
- Hand-written JNI: tedious, error-prone, every type change is two files (Kotlin + Rust) that must stay in sync.
- Raw JNA: better, but you still hand-write the Kotlin side and worry about string ownership.
- UniFFI: write Rust, annotate with
#[uniffi::export], get a Kotlin shim generated. Strings, structs, enums, Result types,asyncfunctions all cross the boundary transparently. The runtime is JNA under the hood.
When in doubt
cargo check -p strawcore --target aarch64-linux-android— fast iteration.cargo run --bin uniffi-bindgen -- generate ...— regenerate Kotlin bindings.adb logcat -s strawcore— Rustlog::info!()lands here.aapt dump badging strawApp/build/outputs/apk/debug/strawApp-debug.apk— inspect what ABIs/native-libs the APK carries.