From aead95f1bc13d0827e7265d068564778ef558e5f Mon Sep 17 00:00:00 2001 From: Kayos Date: Tue, 26 May 2026 11:38:04 -0700 Subject: [PATCH] vc=59 cont: wire bg subs refresh + R8 keep + Settings UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundling the background-refresh worker (originally planned as vc=60) into the same release as cache controls — they're both storage-and-refresh user-facing knobs, ships cleaner together. - StrawApp.onCreate calls FeedRefreshScheduler.applyFromSettings - R8 keep rule for FeedRefreshWorker (same reason as UpdateCheckWorker — WorkManager instantiates via reflection) - Settings UI: 'Auto-refresh subs' toggle (default off) + interval chip-row (30min / 1h / 6h) shown when enabled. Lives in the existing Local cache section since it's the same storage-and-refresh theme. Worker calls uniffi.strawcore.subscriptionFeed which fans out 50 parallel RSS fetches in Rust — 50 subs refreshes in ~1-2s in the background. Writes per-channel into FeedCacheStore so next cold open of Subs paints instantly. --- strawApp/proguard-rules.pro | 1 + .../main/kotlin/com/sulkta/straw/StrawApp.kt | 4 ++ .../com/sulkta/straw/data/SettingsStore.kt | 48 ++++++++++++++++ .../straw/feature/settings/SettingsScreen.kt | 55 +++++++++++++++++++ 4 files changed, 108 insertions(+) diff --git a/strawApp/proguard-rules.pro b/strawApp/proguard-rules.pro index 97c631f42..fad37e118 100644 --- a/strawApp/proguard-rules.pro +++ b/strawApp/proguard-rules.pro @@ -86,4 +86,5 @@ # renames our UpdateCheckWorker the scheduler enqueues it but the # instantiation fails silently and no checks ever run. -keep class com.sulkta.straw.feature.update.UpdateCheckWorker { *; } +-keep class com.sulkta.straw.feature.feed.FeedRefreshWorker { *; } -keep class * extends androidx.work.ListenableWorker { *; } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/StrawApp.kt b/strawApp/src/main/kotlin/com/sulkta/straw/StrawApp.kt index c3226623a..a8c0c4794 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/StrawApp.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/StrawApp.kt @@ -14,6 +14,7 @@ import com.sulkta.straw.data.SearchCache import com.sulkta.straw.data.Settings import com.sulkta.straw.data.Subscriptions import com.sulkta.straw.feature.dataimport.SettingsImport +import com.sulkta.straw.feature.feed.FeedRefreshScheduler import com.sulkta.straw.feature.update.UpdateScheduler import com.sulkta.straw.feature.update.runUpdateCheck import com.sulkta.straw.util.strawLogW @@ -93,5 +94,8 @@ class StrawApp : Application() { if (Settings.get().autoUpdateCheck.value) { appScope.launch { runUpdateCheck(this@StrawApp) } } + // Background subs feed refresh — opt-in periodic WorkManager + // job that pre-warms FeedCache so cold open paints fresh. + FeedRefreshScheduler.applyFromSettings(this) } } diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/data/SettingsStore.kt b/strawApp/src/main/kotlin/com/sulkta/straw/data/SettingsStore.kt index 500c330ed..bda38ba0a 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/data/SettingsStore.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/data/SettingsStore.kt @@ -104,6 +104,18 @@ enum class CacheTtl(val label: String, val days: Int) { val ms: Long get() = days.toLong() * 24L * 60L * 60L * 1000L } +/** + * How often the background subs-feed-refresh worker polls. Defaults to + * 1h — tighter than that wastes battery without meaningful freshness + * gain (YouTube uploads aren't real-time). Background worker is OFF + * by default; opt-in via Settings. + */ +enum class BgFeedRefreshInterval(val label: String) { + M30("Every 30 minutes"), + H1("Every hour"), + H6("Every 6 hours"), +} + private const val PREFS = "straw_settings" private const val KEY_SB_CATS = "sb_categories_v1" private const val KEY_MAX_RES = "max_resolution_v1" @@ -125,6 +137,8 @@ private const val KEY_CACHE_HISTORY_SEARCHES = "cache_history_searches_v1" private const val KEY_CACHE_RESUME_POSITIONS = "cache_resume_positions_v1" private const val KEY_CACHE_SEARCH = "cache_search_v1" private const val KEY_CACHE_TTL = "cache_ttl_v1" +private const val KEY_BG_FEED_REFRESH_ENABLED = "bg_feed_refresh_enabled_v1" +private const val KEY_BG_FEED_REFRESH_INTERVAL = "bg_feed_refresh_interval_v1" class SettingsStore(context: Context) { private val sp: SharedPreferences = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) @@ -270,6 +284,21 @@ class SettingsStore(context: Context) { private val _cacheTtl = MutableStateFlow(loadCacheTtl()) val cacheTtl: StateFlow = _cacheTtl.asStateFlow() + /** + * Background subscription-feed refresh — WorkManager periodic job + * that pre-warms FeedCache so the next cold open paints a fresh + * feed without pull-to-refresh. Off by default; cell-network + * battery cost is the explicit opt-in. + */ + private val _bgFeedRefreshEnabled = MutableStateFlow( + sp.getBoolean(KEY_BG_FEED_REFRESH_ENABLED, false), + ) + val bgFeedRefreshEnabled: StateFlow = _bgFeedRefreshEnabled.asStateFlow() + + private val _bgFeedRefreshInterval = MutableStateFlow(loadBgFeedInterval()) + val bgFeedRefreshInterval: StateFlow = + _bgFeedRefreshInterval.asStateFlow() + fun toggle(cat: SbCategory) { // Atomic toggle via updateAndGet — see AUD-HIGH note in HistoryStore. val next = _sbCategories.updateAndGet { cur -> @@ -404,6 +433,18 @@ class SettingsStore(context: Context) { sp.edit().putString(KEY_CACHE_TTL, ttl.name).apply() } + fun setBgFeedRefreshEnabled(enabled: Boolean) { + if (_bgFeedRefreshEnabled.value == enabled) return + _bgFeedRefreshEnabled.value = enabled + sp.edit().putBoolean(KEY_BG_FEED_REFRESH_ENABLED, enabled).apply() + } + + fun setBgFeedRefreshInterval(interval: BgFeedRefreshInterval) { + if (_bgFeedRefreshInterval.value == interval) return + _bgFeedRefreshInterval.value = interval + sp.edit().putString(KEY_BG_FEED_REFRESH_INTERVAL, interval.name).apply() + } + private fun loadCap(key: String, default: Int): CacheCap = CacheCap.nearest(sp.getInt(key, default)) @@ -412,6 +453,13 @@ class SettingsStore(context: Context) { return CacheTtl.entries.firstOrNull { it.name == name } ?: CacheTtl.D30 } + private fun loadBgFeedInterval(): BgFeedRefreshInterval { + val name = sp.getString(KEY_BG_FEED_REFRESH_INTERVAL, null) + ?: return BgFeedRefreshInterval.H1 + return BgFeedRefreshInterval.entries.firstOrNull { it.name == name } + ?: BgFeedRefreshInterval.H1 + } + private fun loadCategories(): Set { val raw = sp.getStringSet(KEY_SB_CATS, null) return if (raw == null) { diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt index 0eeb56246..9f7486c11 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt @@ -47,10 +47,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.material3.FilterChip import com.sulkta.straw.BuildConfig import com.sulkta.straw.data.AutoUpdateInterval +import com.sulkta.straw.data.BgFeedRefreshInterval import com.sulkta.straw.data.CacheCap import com.sulkta.straw.data.CacheTtl import com.sulkta.straw.data.FeedCache import com.sulkta.straw.data.Resume +import com.sulkta.straw.feature.feed.FeedRefreshScheduler import com.sulkta.straw.feature.update.UpdateScheduler import com.sulkta.straw.feature.update.runUpdateCheck import com.sulkta.straw.util.formatRelativeSince @@ -532,6 +534,59 @@ fun SettingsScreen() { ) } + Spacer(modifier = Modifier.height(16.dp)) + Text( + "Background refresh", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + "Periodically pre-fetch the subs feed so the next time you " + + "open Straw the latest videos are already there. Off by " + + "default (battery cost on cell).", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + val bgEnabled by store.bgFeedRefreshEnabled.collectAsState() + Row( + modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + "Auto-refresh subs", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold, + ) + Switch( + checked = bgEnabled, + onCheckedChange = { checked -> + store.setBgFeedRefreshEnabled(checked) + FeedRefreshScheduler.applyFromSettings(context) + }, + ) + } + if (bgEnabled) { + val bgInterval by store.bgFeedRefreshInterval.collectAsState() + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + BgFeedRefreshInterval.entries.forEach { opt -> + FilterChip( + selected = bgInterval == opt, + onClick = { + store.setBgFeedRefreshInterval(opt) + FeedRefreshScheduler.applyFromSettings(context) + }, + label = { Text(opt.label) }, + ) + } + } + } + Spacer(modifier = Modifier.height(16.dp)) Text( "Cache & history limits",