WorkManager periodic worker hits the repo's index-v2.json, parses
the highest versionCode for our package, compares with
BuildConfig.VERSION_CODE. When newer: posts a notification with
ACTION_VIEW on the APK URL — Android's DownloadManager picks it up
and the system installer takes over. No INSTALL_PACKAGES perm
needed.
Settings:
- Check for updates toggle (default on — closing NewPipe's silent
staleness gap is the explicit motivation)
- Interval picker (1h / 6h / 24h, default 6h — WorkManager has a
15-min periodic floor anyway)
- Last-checked timestamp + 'update available' tag when caught-up
state is dirty
- Check now button — runs the same path as the worker so behaviors
stay identical
Cold start fires one check too so users see pending updates without
waiting a full interval.
R8 keep-rule for UpdateCheckWorker added — WorkManager instantiates
workers by name via reflection.
R8 (minify + resource-shrink) flipped on for BOTH debug AND release
variants — we publish the debug APK to fdroid (per existing
pipeline), and the audit-flagged Log.d strip discipline required R8
to actually run on the variant we ship.
New strawApp/proguard-rules.pro covers:
* UniFFI bindings (uniffi.strawcore.*) — reflective FFI dispatch
from Rust side, must survive minification
* JNA — Library subclasses reflectively loaded by name
* kotlinx-serialization @Serializable — generated $$serializer
companions, kept via both the package-anchored rule and the
annotation-wildcard rule for belt + suspenders
* Media3 session Parcelables (cross-process via Binder)
* Compose runtime + Strawcore exception hierarchy
Surface-handoff polish on inline ↔ fullscreen transitions:
setKeepContentOnPlayerReset(true) on both PlayerViews (inline in
VideoDetail + fullscreen Player). When the detaching view's player
is nulled on dispose, it holds the last rendered frame instead of
flashing black. The receiving view's surface takeover then renders
the next frame without the ~1-frame black gap. Round-4 audit
HIGH-5 was the closest writeup.
Expected APK-size win from R8: ~30-40%. Need real-device
verification post-install — the keep rules are best-effort and a
missing rule manifests as runtime ClassNotFoundException or
silently-broken kotlinx-serialization decoding.
Color palette pulled directly from sulkta.com's stylesheet — the same
greens used on the website now drive the app theme:
#166534 deep green (light-theme primary, top app bar background)
#4ade80 bright lime (dark-theme primary, accents in dark mode)
#86efac light green (primaryContainer in light theme)
#e8f5e8 pale green (secondary container tint)
#d97706 amber accent (tertiary)
#374137 olive gray (secondary on light, container on dark)
Replaces the made-up forest palette from vc=23 with the real Sulkta
brand. Same M3 tonal-role mapping so derived surfaces stay consistent.
TopAppBar redone NewPipe-style: solid deep-green bar with white
"straw" title, white hamburger + search icons. Clear bold header
instead of the previous white-with-a-pill-underneath layout.
Material Icons swapped in everywhere we had emoji:
drawer Person / History / PlaylistPlay / Download / Settings
minibar PlayArrow / Pause / Close
fullscreen Speed / Headphones / Videocam / Share /
PictureInPictureAlt / KeyboardArrowDown
Pulled in material-icons-extended (4 MB APK growth, all icons).
Consistent renders across vendors; no more emoji font fallback drift.
FeedRow gets a NewPipe-style duration pill burned into the bottom-right
of every thumbnail (mm:ss / h:mm:ss). Live streams / mixes with no
duration leave it off.
Audit deferred-MED items addressed:
MED-6: dropped the PlayerService STATE_ENDED auto-stop. Service
shutdown is now driven only by onTaskRemoved + the minibar's ×.
Removes the implicit "we'll never queue" assumption and is correct
for a future autoplay/queue feature.
LOW-7: DownloadsScreen adaptive poll — 1s while a download is
active, 5s when idle. No more wasted DB queries when nothing
is running.
Brings the Phase U Android-side integration onto a feature branch
where the rust wrapper now bridges to strawcore-core (the new NPE
port) instead of rustypipe.
* strawApp/build.gradle.kts — JNA dep + cargoBuild + cargoBuildHost
+ uniffiBindgen + the merge/compile task wiring. libs.newpipe
.extractor dropped.
* StrawApp.kt — uniffi.strawcore.initLogging(); NewPipe.init() gone
* 5 ViewModels (Search/VideoDetail/Player/Channel/Subscriptions)
swapped to call uniffi.strawcore.{search,streamInfo,channelInfo}
* PlayerScreen + PlaybackService adjusted for the new StreamInfo
shape (dash_mpd_url / hls_url instead of NPE's manifests)
* IosSafeHttpDataSource carried forward — strawcore-core gives us
Android-primary URLs so the iOS cap path is mostly dead code,
but kept as belt-and-suspenders for the rare HLS-fallback
* NewPipeDownloader.kt + util/Thumbnails.kt deleted
Files taken wholesale from sulkta branch — the wrapper's UniFFI
surface is identical between sulkta (rustypipe-backed) and the
current Phase-7-bridged-to-strawcore-core code, so Kotlin doesn't
care which extractor is under the hood.
Phase U (rustypipe Rust extractor) rolled back. Symptom: black screen
on play, root cause: rustypipe 0.11.4's JS deobfuscator can't parse
current YouTube player.js (YT changed the obfuscation pattern, no
upstream rustypipe release since June 2025). Switching clients
(Web → TV → Android/Ios) didn't help — the deobfuscator init fires
universally.
Kept in place for the future:
- rust/strawcore/ Cargo workspace + UniFFI scaffolding
- crafting-table runtime install (rustup + 4 Android targets +
cargo-ndk + NDK r27c)
- The U-2..U-5 commits in history (re-runnable when rustypipe is
fixed or we fork it).
Restored from commit 9550b207a (v0.1.0-T):
- NewPipe.init() in StrawApp.onCreate
- libs.newpipe.extractor + libs.squareup.okhttp deps
- NewPipeDownloader.kt + Thumbnails.kt
- ViewModels (Search/VideoDetail/Player/Channel/SubscriptionFeed) on
NewPipeExtractor calls
- VideoDetailScreen Download dialog using NewPipe's StreamInfo
Future-direction memo: openclaw-workspace/memory/project_rustypipe_fork.md
— fork plan + revival path for the Rust extractor when we're ready
to maintain it.
Verified working in the Android emulator: dQw4w9WgXcQ plays, ExoPlayer
reports state=PLAYING(3), position advancing, video surface rendering.
channel_info(url) UniFFI suspend fn. ChannelViewModel +
SubscriptionFeedViewModel both swap. NewPipeExtractor (Java) is OUT —
zero org.schabi.newpipe classes in the APK now.
Cleanup:
- NewPipeDownloader.kt deleted (was the OkHttp adapter)
- Thumbnails.kt deleted (rustypipe returns full URLs)
- NewPipe.init() dropped from StrawApp.onCreate
- libs.newpipe.extractor removed from build.gradle.kts
- STRAW_USER_AGENT + strawHttpClient() now live in net/Http.kt
- RydClient + SponsorBlockClient + PlayerScreen + PlaybackService all
read from net/Http.kt instead of the extractor package
rustypipe API quirks beat:
- channel_videos(id) is the right method (channel() doesn't exist)
- ChannelInfo struct = basic metadata; Channel<T> wrapper carries
name/avatar/banner + .content is the paginator of videos
- description is String (not Option), subscriber_count is Option<u64>
End state: strawApp Kotlin is ~UI + thin glue to strawcore. The Rust
core handles search / streamInfo / channel / channel_videos via UniFFI
suspend fns. Tokio + reqwest + rustls + rquickjs all packed in
libstrawcore.so (~6MB per ABI). APK 40MB total.
Wraps the existing Activity-owned ExoPlayer in a Media3 MediaSession. Side
effects:
- Lock-screen media controls (play/pause)
- System media notification with play/pause buttons
- Audio focus + ducking (other audio dims when straw plays)
- Bluetooth + headset hardware-button routing
- Plays nicely alongside other media apps' notifications
The ExoPlayer is also configured with AudioAttributes (USAGE_MEDIA +
CONTENT_TYPE_MOVIE) and handleAudioFocus=true so other apps can request
focus correctly (e.g., a phone call ducks/pauses straw).
Also scaffolded but NOT yet wired:
- PlaybackService extending MediaSessionService — the foundation for
true background-after-Activity-kill audio. Manifest entry + deps
added (media3-session 1.4.1, concurrent-futures-ktx 1.2.0). The
Activity-to-Service migration via MediaController is M-3 work.
- POST_NOTIFICATIONS, FOREGROUND_SERVICE,
FOREGROUND_SERVICE_MEDIA_PLAYBACK, WAKE_LOCK permissions in manifest.
For the user right now: lock the phone while a video plays — media
controls appear on the lock screen. Open another app — notification with
play/pause sits in the shade. Press a headset's play/pause — straw
responds. Pull the home screen — PiP keeps the video floating + audio
continues. Full screen-off-background-audio after killing the activity
arrives in M-3.
New: SettingsStore (SharedPreferences-lite, same pattern as HistoryStore)
exposes a Set<SbCategory> StateFlow.
7 SponsorBlock categories surfaced as toggle rows in a new Settings screen:
sponsor (on by default), selfpromo, intro, outro, interaction (reminders),
music_offtopic (talking in music videos), filler.
Wiring:
- StrawApp.onCreate: Settings.init(this)
- StrawHome: new Settings gear icon next to "straw v0.1.0" header. Tap
routes to Screen.Settings.
- PlayerViewModel.resolve: reads Settings.get().sbCategories on resolve.
Empty set = SponsorBlock skip disabled (no API call).
Material-icons-core dep added (1.7.5) for the Icons.Filled.Settings glyph.
Day-4 ideas: theme override, default audio-only playback, preferred quality,
clear history buttons.
Phase A: NewPipeExtractor + OkHttp Downloader wired in. Search bar +
LazyColumn results. Tap = navigate to detail.
Phase B: VideoDetail screen — StreamInfo metadata + Return YouTube
Dislike chips + description.
Phase C: Media3 ExoPlayer in Compose. Resolves StreamInfo to best
playable: DASH MPD → HLS → combined progressive → merged
videoOnly+audio.
Phase D: SponsorBlock SHA-256 prefix lookup. 250ms position-poll loop
inside PlayerScreen — exoPlayer.seekTo(segment.end) when entering
a sponsor segment. Toast on skip.
Phase E: Verified live on Android 14 emulator. linus tech tips search
returns real results with thumbnails; tapped result opens detail;
hit Play → video plays through ExoPlayer.
Architecture: everything in :strawApp for now (not pushed into :shared
yet — KMP refactor is day-3). Pure-state nav (sealed Screen + stack,
no nav library).
Known polish gaps (day-3): RYD chips render empty on some videos,
description has raw HTML (markdown render needed), no Koin DI yet,
no persistence.
GPL-3.0-or-later per upstream NewPipe.
Initial Sulkta fork of NewPipe with a new :strawApp module that ships a
clean Compose-based Android APK at applicationId com.sulkta.straw.
What's here:
- :strawApp — thin Android application shell, MaterialTheme + StrawHome
Composable. Lives alongside legacy :app so we don't break upstream.
- buildSrc — STRAW_APPLICATION_ID/VERSION constants alongside the existing
NEWPIPE_APPLICATION_ID_OLD/NEW.
- docs/sulkta — RECON.md (NewPipe codebase breakdown) + DECISIONS.md
(stack + scope decisions).
NewPipe's :shared KMP scaffold is at 892 LOC and renders nothing; this
fork picks up there and races ahead. Day-1 ships a hello APK; day-2 wires
NewPipeExtractor + Media3 player + SponsorBlock + Return YouTube Dislike.
GPL-3.0-or-later per upstream.