Straw full-audit fix batch (2026-07-04). Confirmed HIGH/MED findings: - Updater self-brick: drop the fdroid.sulkta.com leaf+E7 SPKI pins (LE rotates the leaf every ~90d + intermediates from a pool → a routine renewal was guaranteed to miss both pins and silently kill the only in-app update path). System-CA validation + the install-time APK signature gate remain. Also distinguish "index unreachable" from "up to date", add a 30s callTimeout + bounded index read, guard the API-26 NotificationChannel, and setOnlyAlertOnce. - Request POST_NOTIFICATIONS at runtime so A13+ update/media notifications actually post (was declared but never requested → silent no-op). - isDebuggable=false on the shipped debug variant (closes ADB run-as dump of watch/search history + subs; no package-id cutover). - Crash fixes: distinctBy(url) on moreFromChannel + related (duplicate LazyColumn key); back-handling driven by live nav depth so it survives moveTaskToBack on A12+; PlaylistsStore per-store caps + off-main hydration (hostile import ANR); SponsorBlock staleness fence via NowPlaying.refreshIfCurrent. - CacheCap.nearest() snaps up to the nearest finite cap (defaults no longer resolve to Unlimited/100k on fresh installs). - RYD/SB FFI shims wrap the uniffi call (swallow a core panic per contract). - Rust wrapper: release panic="unwind" (was "abort", which defeated UniFFI catch_unwind → any core panic = whole-app SIGABRT) + a 60s wall-clock timeout on every blocking extractor call (unblocks the caller, bounds thread pileup). Pairs with the strawcore-core reliability fix.
364 lines
23 KiB
Kotlin
364 lines
23 KiB
Kotlin
/*
|
||
* SPDX-FileCopyrightText: 2026 Sulkta
|
||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||
*/
|
||
|
||
const val STRAW_SDK_COMPILE_MAJOR = 36
|
||
const val STRAW_SDK_COMPILE_MINOR = 1
|
||
const val STRAW_SDK_TARGET = 35
|
||
|
||
// Sulkta fork — Straw
|
||
//
|
||
// vc=91 / 0.1.0-CY — reliability + security fix sprint (straw audit 2026-07-04):
|
||
// * The in-app updater no longer self-bricks on a routine cert renewal. It
|
||
// used to pin the fdroid.sulkta.com leaf SPKI + the LE "E7" intermediate,
|
||
// but the leaf rotates every ~90-day renewal and LE rotates intermediates
|
||
// from a pool (they explicitly say don't pin them), so a normal renewal was
|
||
// GUARANTEED to miss both pins — silently killing the ONLY update path,
|
||
// with the fix reachable only through the channel the break just killed.
|
||
// Dropped pinning: default system-CA validation still blocks a MITM, and a
|
||
// swapped APK can't install over Straw anyway (the package installer
|
||
// verifies it against our signing key). Also: the check now distinguishes
|
||
// "couldn't reach the index" from "up to date" (no more "checked just now /
|
||
// up to date" while actually blind), added a 30s call timeout + a bounded
|
||
// index read, guarded the API-26 NotificationChannel (was a crash on 7.x),
|
||
// and setOnlyAlertOnce so a pending update doesn't re-nag every tick.
|
||
// * Android 13+ update/media notifications actually appear now:
|
||
// POST_NOTIFICATIONS was manifest-declared but never requested at runtime,
|
||
// so nm.notify() was a silent no-op on 13+. Requested once on cold start.
|
||
// * The published APK is no longer debuggable (isDebuggable=false on the
|
||
// shipped debug variant) — closes an ADB/USB `run-as` dump of watch/search
|
||
// history + the subscription list. No package-id cutover; updates in place.
|
||
// * Crash fixes: (a) opening a video whose channel repeats a videoId in its
|
||
// first ~20 entries hard-crashed Compose on a duplicate LazyColumn key —
|
||
// moreFromChannel + related now distinctBy url; (b) in-app back died
|
||
// permanently after one back-to-Home on Android 12+ (moveTaskToBack doesn't
|
||
// destroy the activity, so the disabled callback never re-enabled) — the
|
||
// callback is now driven by live nav depth instead of disable-and-redispatch;
|
||
// (c) the local-playlists store was uncapped, so a hostile NewPipe import
|
||
// could persist a huge blob that ANR'd every cold start — added per-store
|
||
// caps (playlists / items / string length) + off-main hydration like
|
||
// ResumePositionsStore; (d) a SponsorBlock fetch that resolves after you
|
||
// skip to another video no longer hijacks the now-playing item (staleness
|
||
// fence).
|
||
// * Cache-size caps that first-launch defaulted to "Unlimited" (100k rows,
|
||
// because nearest() was exact-match-or-Unlimited over a set the defaults
|
||
// weren't in) now snap up to the nearest finite cap.
|
||
// * The RYD + SponsorBlock FFI shims wrap the uniffi call, so a core panic
|
||
// (now catchable — see below) is swallowed to null/empty per their contract
|
||
// instead of crashing video-detail load.
|
||
// * Rust FFI wrapper: release profile is panic="unwind" (was "abort" — abort
|
||
// defeated UniFFI's catch_unwind + tokio's spawn_blocking JoinError capture,
|
||
// turning ANY panic in the extractor core into a whole-app SIGABRT), and
|
||
// every blocking extractor call is wrapped in a 60s wall-clock timeout so a
|
||
// wedged JS/HTTP path unblocks the caller instead of spinning forever and
|
||
// piling detached threads toward tokio's 512 blocking-pool cap. Pairs with
|
||
// the strawcore-core reliability fix (bounded rquickjs 64MiB/2MiB/5s +
|
||
// invalidate-on-failure player.js self-heal).
|
||
//
|
||
// vc=90 / 0.1.0-CX — cold-start hydration + status-bar bleed fix + RYD FFI slim:
|
||
// * ResumePositionsStore + EnrichmentStore no longer JSON-decode on the main
|
||
// thread in Application.onCreate. Resume was the heaviest cold-start cost in
|
||
// the app — at its 100k-entry cap the blob is multi-MB and the decode runs
|
||
// ~50-100 ms on a low-end device, all on Main before the first frame. Both
|
||
// now seed an empty StateFlow and hydrate from disk on the store's own
|
||
// PrefsWriter single-thread dispatcher. Mutations that arrive before the
|
||
// hydrate finishes DEFER themselves onto that same FIFO dispatcher (a
|
||
// `hydrated` flag gate) so they re-run after the load on fully-loaded state
|
||
// — correct for adds AND clears (a naive `loaded + live` merge would
|
||
// resurrect a clear that landed on the empty seed). Subscriptions/Playlists/
|
||
// History/Settings stay eager: tiny (sub-ms) and rendered directly, so
|
||
// seeding them empty would flash an empty list every cold start. (audit 2.1)
|
||
// * Fixed the video page leaking details/related rows into the status-bar
|
||
// strip above the player when scrolled. The detail body fills the screen
|
||
// from y=0 (under the status bar) and the player only starts at the
|
||
// status-bar inset, so a leading Spacer-as-item let rows scroll up into the
|
||
// uncovered strip over the clock/signal. topPadding is now a layout inset on
|
||
// the LazyColumn itself, so the scroll viewport begins + clips at the
|
||
// player's bottom edge — rows can't enter the strip. (a tester, on-device.)
|
||
// * RYD: dropped the dead `rating` + `viewCount` fields from the strawcore
|
||
// `RydVotes` FFI Record + the Kotlin shim. Only likes/dislikes were ever
|
||
// rendered; RYD's rating just restates the like/dislike ratio and its
|
||
// viewCount is a stale aggregate that conflicts with the real YouTube count
|
||
// the detail screen already shows — surfacing either would be redundant or
|
||
// misleading, so they no longer cross the FFI. (audit #2 L-9)
|
||
//
|
||
// vc=89 / 0.1.0-CW — pagination-burst fix + finish the SP-write serialization:
|
||
// * Hide-shorts no longer drains a whole channel/search in one burst. The
|
||
// infinite-scroll trigger is computed from the FILTERED list while loadMore()
|
||
// appends to the UNFILTERED one, so a shorts-heavy feed with hide-shorts ON
|
||
// kept the trigger hot (the filter stripped each fetched page back to
|
||
// ~nothing) and auto-fetched every page back-to-back to the end of the
|
||
// continuation. Now it keeps loading while pages are productive (the filtered
|
||
// list grows) and stops after 3 consecutive pages that add nothing visible;
|
||
// toggling hide-shorts off grows the list and resumes. Applies to both Search
|
||
// and Channel. (audit D-3)
|
||
// * Finished the vc=88 PrefsWriter migration: the remaining four SP stores
|
||
// (Enrichment, SearchCache, Playlists, FeedCache) now serialize their writes
|
||
// through the same single-thread-per-store dispatcher. EnrichmentStore is the
|
||
// one that mattered — put() runs 8-wide concurrently from the feed-enrichment
|
||
// fan-out, so its apply() ordering genuinely raced (a real M-2 instance the
|
||
// audit's four-store scope missed). No SP store is left on a bare
|
||
// sp.edit().apply() now.
|
||
// No user-visible behavior change beyond the pagination bound.
|
||
//
|
||
// vc=88 / 0.1.0-CV — deferred-hygiene sweep (audit #2 leftovers, no behavior change):
|
||
// * SharedPreferences writes across every store (Settings, History, Subs,
|
||
// ResumePositions) now route through one PrefsWriter — a single-thread
|
||
// dispatcher per store — so the on-disk apply() order matches the in-memory
|
||
// CAS order. Previously a Main-thread toggle and an IO-thread settings/history
|
||
// import could land their apply() out of order, and ResumePositions detached
|
||
// write-ordering entirely via a fresh globalScope.launch per write; a stale
|
||
// value could then win the next cold-start load. Each write reads the live
|
||
// StateFlow so disk always converges to the latest in-memory state. (M-2)
|
||
// * Settings storage-usage sampling (File.length() ×4 + Coil diskCache.size)
|
||
// moved off the composition/Main thread into a LaunchedEffect on
|
||
// Dispatchers.IO — was real synchronous FS I/O stalling the first Settings
|
||
// frame. (L-14)
|
||
// * Dead code + stale comments from the vc=85 SB/RYD→Rust migration: Http.kt
|
||
// trimmed to just STRAW_USER_AGENT (the OkHttp client + bounded-body reader
|
||
// went to Rust); reconciled the network_security_config / feed.rs /
|
||
// SubscriptionFeedViewModel / net.rs / CI comments with reality. recencyScore
|
||
// now overflow-guards a crafted relative-date. (L-2 / L-4..L-8 / L-15 / L-16)
|
||
//
|
||
// vc=87 / 0.1.0-CU — audit #2 fix sprint (closes two vc=86 regressions + more):
|
||
// * FIX a duplicate-key crash: the Subs feed, Search, and Channel lists key
|
||
// their LazyColumn by the video url, but the sources weren't fully deduped
|
||
// — a video that appears twice (a collab/cross-post in two subscribed
|
||
// feeds, or the same videoId across two search/channel shelves) produced a
|
||
// duplicate Compose key → IllegalArgumentException → screen crash. Now the
|
||
// subs merge + the Search/Channel FIRST page dedup by url (the continuation
|
||
// paths already did). The vc=86 key additions only deduped the append path.
|
||
// * Complete the IosSafe end-of-chunk roll: the vc=86 change set
|
||
// chunkRemaining=0 on inner EOF expecting the NEXT read() to roll, but
|
||
// Media3 stops calling read() after a -1, so a LENGTH_UNSET inner still
|
||
// truncated to the first chunk. Now the roll happens WITHIN read() (loop),
|
||
// with a progress guard so a no-progress chunk can't spin.
|
||
// * SearchCacheStore.clear() is now atomic (updateAndGet), so a concurrent
|
||
// record() can't resurrect a just-cleared entry on disk.
|
||
// * Gate the YtRelated autoplay branch through isAllowedYtUrl to match the
|
||
// stated universal-allowlist invariant; no-op-guard setLatestKnownVersion;
|
||
// drop two unused TrackSelectionParameters imports.
|
||
// (Deferred to a follow-up: SharedPreferences write-ordering serialization
|
||
// across the stores, the migration's dead Http.kt sweep, and a stale-comment
|
||
// pass — all low-risk hygiene, none a crash.)
|
||
//
|
||
// vc=86 / 0.1.0-CT — audit-fix sprint (code-audit HIGH H2 + 5 MED/LOW):
|
||
// * Audio-only toggle no longer drops your max-resolution cap. Both the
|
||
// fullscreen button and the detail "Audio" pill rebuilt the track-
|
||
// selection params from a fresh Builder, wiping the data-saver ceiling;
|
||
// they now buildUpon() the existing params so the cap survives.
|
||
// * Subscriptions "Refresh" button no longer sticks at "..." forever on a
|
||
// warm restart within the cache TTL — refreshIfStale now clears the
|
||
// initial loading seed when it decides nothing needs refreshing.
|
||
// * Search + Channel result lists carry a stable item key (the video url)
|
||
// so appending a page / filtering shorts stops re-binding rows to new
|
||
// data (which re-triggered thumbnail loads + shifted scroll).
|
||
// * iOS-safe data source: the unknown-length (LENGTH_UNSET) chunk path now
|
||
// rolls forward to the next Range chunk at inner-EOF instead of
|
||
// re-reading the exhausted source forever (was truncating playback to
|
||
// the first chunk on any inner that reports no length).
|
||
// * strawcore channel_feed_rss now PROPAGATES the real failure (network /
|
||
// HTTP / parse) instead of collapsing every error to an empty list, so a
|
||
// broken channel fetch is distinguishable from "no new videos" (the
|
||
// bulk subscription_feed keeps its per-channel tolerance).
|
||
// * Feed recency: a clock-skewed future upload emits "0 seconds ago"
|
||
// (parses to top) instead of "just now" (which the Kotlin recency parser
|
||
// couldn't read → sank the item to the bottom).
|
||
//
|
||
// vc=85 / 0.1.0-CS — image caching + SB/RYD → Rust + crash/autoplay fixes:
|
||
// * Thumbnails + channel icons stay cached. Coil's default disk cache is
|
||
// only 2% of the phone's FREE space, so on a storage-tight device the
|
||
// subs feed (the most image-heavy screen) thrashed its cache and
|
||
// re-downloaded thumbnails on every visit — the "each load takes a
|
||
// second" lag. Pinned an explicit 256 MB image disk cache + a sized
|
||
// memory cache via a SingletonImageLoader.Factory.
|
||
// * SponsorBlock + Return-YouTube-Dislike clients moved out of Kotlin
|
||
// into Rust (strawcore fetchSponsorSegments / fetchRydVotes) — network
|
||
// + JSON belong behind the FFI, not in the UI layer. SponsorBlock keeps
|
||
// its privacy-preserving SHA-256 hash-prefix lookup. Kotlin is now a
|
||
// thin shim mapping the FFI records onto the existing SbSegment/RydVotes
|
||
// domain types; behavior identical. (Migration #2 of "all backend → Rust".)
|
||
// * FIX a crash: extract_channel_id() sliced the channel URL using a
|
||
// length derived from a lowercased copy of itself — to_lowercase() can
|
||
// change byte length on non-ASCII, so a channel URL with any non-ASCII
|
||
// tail could panic across the FFI and abort the app on a feed refresh.
|
||
// Now matches the prefix case-insensitively against the original with
|
||
// length + char-boundary guards.
|
||
// * FIX autoplay hijack: advancing to the next video resolves over ~500ms;
|
||
// if you manually started a different video in that window, autoplay
|
||
// would still fire and replace your choice with the stale next-up. Added
|
||
// a staleness fence (same class as the vc=83 minibar fix).
|
||
//
|
||
// vc=84 / 0.1.0-CR — name + stream-picker → Rust:
|
||
// * The app is now just "Straw" in the launcher, not "Straw debug" —
|
||
// we're past the debug-branding phase. (The package id stays
|
||
// com.sulkta.straw.debug under the hood so updates install in place
|
||
// and nobody loses their subscriptions/history; dropping the suffix
|
||
// for real is a separate release-track cutover.)
|
||
// * The stream-selection logic (pick the playable video/audio/combined
|
||
// URLs from an extraction, honoring the resolution cap) moved out of
|
||
// Kotlin into the Rust strawcore layer (resolvePlayback). The Kotlin
|
||
// side is now a thin shim that supplies the cap and attaches
|
||
// SponsorBlock segments. Behavior is identical — same URLs picked,
|
||
// same data-saver fallback — it's just backend logic living in Rust
|
||
// where it belongs. First step of moving all backend logic to Rust.
|
||
//
|
||
// vc=83 / 0.1.0-CQ — fix: swapping videos from the minibar kept playing
|
||
// the old one:
|
||
// * With a video minimized to the bottom minibar, picking a different
|
||
// video updated the title, details and related list but the OLD video
|
||
// kept playing. The inline player's resolve→play effect read the
|
||
// shared (activity-scoped) view-model's `resolved` stream without
|
||
// checking it actually belonged to the newly-opened video. For one
|
||
// frame after the swap the view-model still holds the previous video's
|
||
// resolved URLs, so playback was claimed under the NEW url while
|
||
// streaming the OLD media — and the correct re-fire was then swallowed
|
||
// by the "already playing this url" short-circuit, so the new video
|
||
// never started. Restored the loadedUrl fence (the same guard
|
||
// VideoDetailBody and every ViewModel already use) that the vc=75
|
||
// expandable-player rearchitect had dropped from the inline wiring.
|
||
//
|
||
// vc=82 / 0.1.0-CP — subscription-feed enrichment goes lightweight:
|
||
// * Filling in a feed row's view count + duration used to run the FULL
|
||
// stream extraction per item (the same path opening a video does):
|
||
// Android /player, the JS signature/nsig deobfuscation, an extra WEB
|
||
// /player metadata round-trip, plus stream/manifest/caption parsing —
|
||
// then threw all of it away except those two numbers. On a refresh
|
||
// that touched dozens of items that was dozens of redundant heavy
|
||
// round-trips.
|
||
// * Now it calls a new strawcore stream_metadata() path that does only
|
||
// the Android /player fetch + the videoDetails read those two fields
|
||
// come from, skipping the JS eval, the extra WEB round-trip, iOS, and
|
||
// stream extraction. The values are identical (they come from the same
|
||
// videoDetails the full path used), the feed just stops paying for
|
||
// work it discarded. (strawcore 30f24d2.)
|
||
//
|
||
// vc=81 / 0.1.0-CO — perf-audit app-side batch (no behavior change):
|
||
// * Search: the reactive cache-preview filter no longer runs on the
|
||
// main thread on every keystroke. It walked the entire cached-
|
||
// results pool (thousands of items on a heavy user) inline; now each
|
||
// keystroke debounces ~150ms and the scan runs on Dispatchers.Default.
|
||
// A submit cancels the pending preview so it can't clobber live
|
||
// results.
|
||
// * Subscription feed: the merge memoizes the relative-upload-date
|
||
// parse by string, so the recency regex runs once per distinct
|
||
// "N days ago" value instead of once per item (~3000 per merge on a
|
||
// 200-sub feed) — across hydration, every refresh, and each
|
||
// background-enrichment emit.
|
||
//
|
||
// vc=80 / 0.1.0-CN — strawcore extraction perf (Rust batch):
|
||
// * The extractor borrows the streamingData subtree out of the Android
|
||
// + iOS player responses instead of deep-cloning the largest part of
|
||
// each response, and merges format objects by reference rather than
|
||
// cloning all ~20-40 of them per video open.
|
||
// * Channel pages fetch their Home + Videos tabs concurrently, so
|
||
// opening a channel costs one network round-trip of latency instead
|
||
// of two.
|
||
// * Response bodies decode in place on the (overwhelmingly common)
|
||
// valid-UTF-8 path instead of always copying.
|
||
// No behavior change — purely allocation + latency wins in strawcore
|
||
// (strawcore 91d4824).
|
||
//
|
||
// vc=79 / 0.1.0-CM — perf-audit pass-2 (app-side slam-dunks):
|
||
// * FIX a wrong-thread crash: the headphone-disconnect settings watcher
|
||
// ran on the IO scope and touched the ExoPlayer (thread-affine to the
|
||
// Main thread it was built on) → "Player accessed on the wrong thread"
|
||
// latent on every session. Collector now runs on Dispatchers.Main.
|
||
// * recordSearch (json-encode + SharedPrefs write) moved off the Main
|
||
// thread into withContext(Dispatchers.IO).
|
||
//
|
||
// vc=78 / 0.1.0-CL — perf-audit batch 1 (app-side):
|
||
// * Autoplay-next no longer drops the user's max-resolution cap. The
|
||
// enter-video track-selection reset built params from scratch, wiping
|
||
// the data-saver ceiling on every URL change; now it re-enables the
|
||
// video track surgically + re-asserts applyMaxResolutionCap().
|
||
// * VideoDetail body is now a LazyColumn, not a verticalScroll Column.
|
||
// The related + more-from-channel lists recycle and defer each row's
|
||
// image decode + its two progress-overlay flow collectors until
|
||
// scrolled into view (was ~40 decodes + ~80 collectors mounted eagerly
|
||
// on every video open). Namespaced item keys; dialogs hoisted out.
|
||
//
|
||
// vc=77 / 0.1.0-CK — morph perf: static poster during collapse/morph:
|
||
// * The minibar + the whole collapse/expand morph now render the video's
|
||
// static poster, not the live TextureView. Scaling a live-playing
|
||
// TextureView through the morph's graphicsLayer every frame was the
|
||
// remaining sluggishness; the live PlayerView only mounts once settled
|
||
// fully expanded. Audio is unaffected (it's in the foreground service).
|
||
//
|
||
// vc=76 / 0.1.0-CJ — expandable-player smoothness pass:
|
||
// * The detail body no longer renders to an offscreen buffer every
|
||
// frame during the morph (CompositingStrategy.ModulateAlpha) — that
|
||
// offscreen alpha pass on the whole scroll subtree was the main
|
||
// cause of the sluggish feel.
|
||
// * Morph animation is now a snappy no-bounce spring instead of a
|
||
// 300ms FastOutSlowIn tween (which ramped slowly at both ends).
|
||
//
|
||
// vc=75 / 0.1.0-CI — expandable player (full rearchitect):
|
||
// * The video page and the bottom minibar are now ONE container that
|
||
// morphs continuously between them, both directions. Replaces the
|
||
// old separate Screen.VideoDetail page + MinibarOverlay (which just
|
||
// appeared/vanished). One fraction (0=minibar, 1=full page) drives a
|
||
// graphicsLayer scale+translate on a single mounted TextureView, so
|
||
// the morph runs in the render phase (smooth) and the same video
|
||
// surface stays live across the whole range — a true shared-element
|
||
// transition. Swipe the player down → it shrinks into the toolbar;
|
||
// swipe/tap the toolbar up → it grows back into the page.
|
||
// * Opening a video is no longer a nav push — it sets OpenVideo +
|
||
// expands. The browse screen underneath stays put, so collapsing
|
||
// drops you right back where you were.
|
||
// * Playback plumbing unchanged: shared controller, NowPlaying,
|
||
// setPlayingFrom, SponsorBlock, autoplay-next, PiP, background audio,
|
||
// and the true-fullscreen Player (⛶) all still key off NowPlaying.
|
||
//
|
||
// vc=73 / 0.1.0-CG — VideoDetail cleanup:
|
||
// * Inline player → TextureView surface so the swipe-down-to-minimize
|
||
// drag is smooth (a SurfaceView won't follow the Compose graphicsLayer
|
||
// transform — that was the stutter).
|
||
// * Description folded into a collapsible "Details" section, collapsed
|
||
// by default, sitting just above the recommendations.
|
||
// * Action buttons restyled into one tidy horizontally-scrollable row
|
||
// of uniform icon pills (dropped the redundant "Play").
|
||
//
|
||
// vc=23 / 0.1.0-AI — minibar + downloads UI + green theme:
|
||
// * MediaController/MediaSessionService unification — single ExoPlayer
|
||
// owned by PlaybackService, every UI surface is a controller client.
|
||
// Inline player on VideoDetail, fullscreen Player, and the new
|
||
// minibar overlay all drive the same underlying player; nothing
|
||
// restarts on screen transitions.
|
||
// * Persistent minibar overlay at the bottom of every non-Player
|
||
// screen whenever something is loaded. Tap → expand to fullscreen.
|
||
// Drag-down on fullscreen → minimize to minibar. ⌄ overlay button
|
||
// also minimizes. × on the minibar stops + clears.
|
||
// * Downloads page wired into the drawer.
|
||
// * Theme: forest-green primary palette in place of M3 default
|
||
// lavender / NewPipe red — modern, clean, distinct.
|
||
//
|
||
// vc=22 / 0.1.0-AH — V-2 player polish + local playlists:
|
||
// * Inline → fullscreen now hands off seek position. Tap Play (or the
|
||
// ⛶ pill on the inline player) while the inline is mid-track and
|
||
// the fullscreen Player picks up at the same point. Same handoff
|
||
// pattern as fullscreen → background from vc=21.
|
||
// * Local playlists: drawer entry "Playlists", "Save" button on
|
||
// VideoDetail. SharedPreferences-backed, no queue/autoplay yet
|
||
// (tap an entry to open VideoDetail as normal).
|
||
//
|
||
// vc=21 / 0.1.0-AG — player hand-off polish:
|
||
// * 🎧 background-audio button now captures the current position and
|
||
// resumes the foreground service from there instead of restarting.
|
||
// * HOME / recents button while on the player now hands off seamlessly
|
||
// to background audio (same position-preserving path) instead of
|
||
// auto-entering Picture-in-Picture. Manual PiP via the ⊟ overlay
|
||
// button is unchanged.
|
||
//
|
||
// vc=20 / 0.1.0-AF — channel-videos fix on top of the rust pipeline
|
||
// cutover. vc=19 returned empty subscription feeds because
|
||
// strawcore-core's channel_info wasn't doing the second browse for the
|
||
// Videos tab AND wasn't parsing the new lockupViewModel shape.
|
||
//
|
||
// vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
|
||
// strawcore-core (Sulkta-OSS/strawcore) via the UniFFI wrapper; no
|
||
// NewPipeExtractor in the runtime path.
|
||
const val STRAW_VERSION_CODE = 91
|
||
const val STRAW_VERSION_NAME = "0.1.0-CY"
|
||
const val STRAW_APPLICATION_ID = "com.sulkta.straw"
|