Five things land together — same surface, all the data/settings flow:
SettingsStore additions
themeMode: System / Light / Dark (default System)
cacheEnabled: Bool (default true). Single toggle gates both the
subs feed cache (FeedCacheStore) and the new search cache.
Theme override
StrawActivity reads Settings.themeMode and bypasses
isSystemInDarkTheme when Light or Dark is chosen. Changes apply
immediately via StateFlow recomposition — no restart needed.
SearchCacheStore (new)
SharedPreferences JSON of the last 30 searches with 20 items each
(~150 KB cap). Serialized via @Serializable StreamItem (already
serializable since vc=33).
Reactive search
SearchViewModel.onQueryChange now scans the merged corpus (saved
searches + subs feed cache, ~1500 records max) as the user types
past 2 chars. Matches on title or uploader (case-insensitive),
dedup by URL, cap 60. UI shows a "Cached results · …" hint when
the visible list is from cache so users know it's not the network
result yet.
submit() now paints any matching cached query immediately, then
kicks the network. Network results overwrite cache on success and
the cached preview survives network failures so offline users
still see something.
Cache opt-out
Settings switch "Enable local cache" wipes both stores when
flipped off. FeedCacheVM + SearchVM both short-circuit their
read/write paths when the flag is false.
Log dump
util/LogDump captures this PID's logcat (-d -v threadtime --pid=)
to cacheDir, returns a chooser Intent via FileProvider. New
Settings → "Export logs…" button. Toast surfaces the failure
reason if the dump command itself fails (sandbox-restricted
devices etc.).
FileProvider declared in manifest with cache-path "logs"; XML at
res/xml/file_paths.xml. Authority = ${applicationId}.fileprovider.
Two follow-ups on vc=29 (the cutout fix is bundled in too — vc=29
never shipped standalone):
Minibar
Was rendering UNDER the system nav buttons / gesture pill because
StrawActivity uses enableEdgeToEdge() and the minibar was aligned
BottomCenter with no inset awareness. Added
navigationBarsPadding() to the MinibarOverlay Column so it lifts
above the system bar in both gesture-nav and 3-button-nav modes.
Icon
v1 had two overlapping shapes (tilted lime parallelogram + white
play triangle) that fought each other and read as visual noise at
launcher size. Replaced with a single bold white play triangle on
the deep-green (#166534) background — one strong silhouette,
reads at any size, says "video app" without ceremony. PNG fallbacks
re-rendered at all five mipmap densities.
Three layout/playback fixes on vc=26-27 feedback plus the long-deferred
app icon.
Layout — VideoDetailScreen
Player surface now fills the screen width (no 16dp side gutters).
Outer Column dropped its 16dp padding; the player Box hangs off the
full width with no rounded corners — NewPipe/YouTube look. Everything
below (title, chips, button row, description, related list) goes back
into an inner Column with 16dp horizontal + 12dp vertical padding so
the body still reads correctly.
Bottom inset: Spacer(windowInsetsBottomHeight(WindowInsets.navigationBars))
appended at the end of the scrollable column. Last related video can
scroll up past the gesture pill / 3-button nav instead of being
obscured by it. (Plain navigationBarsPadding would have pushed the
whole surface up and left a dead band.)
Black-video fix
vc=27's Background button disabled the video track on the controller
via setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true) and that override
is sticky. Returning to a video left it audio-only with a black
surface. Added a LaunchedEffect(controller, streamUrl) that resets
TrackSelectionParameters to defaults on every entry into detail — if
the user opened a video page, they want video. The audio-only
fullscreen toggle and the Background button still set the override
for the duration of that session; they just no longer leak.
App icon
Replaced the Android default placeholder (sym_def_app_icon, which
fdroid was failing to render anyway) with a proper adaptive icon:
Background: #166534 deep green (sulkta.com brand)
Foreground: tilted lime parallelogram + white play triangle
(literal "straw" nod + video-app affordance)
Adaptive XML in mipmap-anydpi-v26/. PNG fallbacks rendered via
rsvg-convert at all five mipmap densities (mdpi/hdpi/xhdpi/xxhdpi/
xxxhdpi) for pre-API-26 devices. Manifest now points at
@mipmap/ic_launcher and @mipmap/ic_launcher_round.
User-visible:
- BUG: 'clicking a second item went back to the first video' — VideoDetailViewModel
guard was short-circuiting on Activity-scoped ViewModel reuse. Now tracks
loadedUrl and only skips when the requested URL matches.
- BUG: PiP / window mode now auto-enters on Home gesture (Android 12+ via
setAutoEnterEnabled). Manual PiP button reports failure cause via Toast.
- HOME REDESIGN: replaced 3-tab bottom nav with hamburger ModalNavigationDrawer.
Default view = sub feed. Drawer items = Subscriptions / History / Search /
Settings. Top-left hamburger like normal Android apps.
Audit pass #2 (Opus max-effort) — CRIT + HIGH fixes shipped:
- CRIT-1: PlaybackService now calls startForeground() inside onStartCommand
with a media-playback notification + channel. Pre-fix could throw
ForegroundServiceDidNotStartInTimeException on Android 12+ and crash-kill.
- CRIT-2: AndroidManifest service exported=false. Previously any installed
app could craft an Intent and drive playback from attacker URLs.
- HIGH-1: 🎧 background handoff stops the activity player before starting
the service so we don't dual-host two ExoPlayers + MediaSessions.
- HIGH-2: onStartCommand returns START_NOT_STICKY and tears down on null
intent; no more crash-restart-crash loop after OS kills.
- HIGH-3: stop service on STATE_ENDED / STATE_IDLE via Player.Listener.
onTaskRemoved checks playbackState properly so we don't hold WAKE_LOCK
forever after a video ends in background.
- HIGH-4: Downloader validates scheme=https + googlevideo/youtube host
before handing the URL to DownloadManager.
- HIGH-5: filename sanitization extended to ASCII control chars, DEL,
Unicode bidi-override block, leading-dot, trailing whitespace.
- HIGH-6: SubscriptionFeedViewModel cancels prior in-flight refresh,
caps parallelism at 8 via Semaphore, applies 15s per-channel timeout.
- HIGH-7: sub feed error banner now shows above cached items when refresh
fails (previously hidden, looked indistinguishable from success).
- HIGH-8: PlayerViewModel falls back to lowest-available stream when no
stream is under the max-resolution ceiling (was: silent black screen).
- HIGH-9: network_security_config explicit cleartextTrafficPermitted='false'
on the RYD domain-config block (doesn't inherit from base-config).
- MED-1: PlaybackService.onDestroy nulls field before releasing session to
close a race with onGetSession during teardown.
- MED-6: Downloader catches enqueue exceptions, returns -1L, caller toasts
'download refused (bad URL)' instead of crashing.
Deferred (audit said 'can wait'): MED-2..5, MED-7..11, HIGH-10 UX consistency.
Four polish items, all visible:
1. Fixed RYD URL: was returnyoutubedislike.com (the website, 404s on /votes)
→ returnyoutubedislikeapi.com (the actual API).
2. Bundled Sectigo "Public Server Authentication CA DV R36" intermediate
as an additional trust anchor (network_security_config.xml) for the
.com web host — the API uses Google Trust Services already so this
is defensive, in case we ever fetch the landing page.
3. Added intent-filter for YouTube URLs (VIEW + SEND) on StrawActivity.
Single-task launchMode; pickYouTubeUrl() routes the initial Intent
to Screen.VideoDetail(url) instead of Home.
4. stripHtml() utility removes <br>, </p>, and other tags from
NewPipeExtractor's description. Chapter timestamps now render
readably; raw HTML gone.
Also added Log.d to SponsorBlockClient and RydClient for visible
verification (StrawSb / StrawRyd tags). PlayerScreen now shows a
"SB: N segments" overlay chip in the top-left so the user can see
that SponsorBlock is armed.
Verified on Android 14 emulator with "Me at the zoo" intent:
- RYD: 👍 18.9M / 👎 424.2K renders
- Intent: direct URL → VideoDetail (no Home pass-through)
- HTML: chapter timestamps render clean