vc=59 cont: wire bg subs refresh + R8 keep + Settings UI
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.
This commit is contained in:
parent
c4bf7446c9
commit
aead95f1bc
4 changed files with 108 additions and 0 deletions
1
strawApp/proguard-rules.pro
vendored
1
strawApp/proguard-rules.pro
vendored
|
|
@ -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 { *; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> = _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<Boolean> = _bgFeedRefreshEnabled.asStateFlow()
|
||||
|
||||
private val _bgFeedRefreshInterval = MutableStateFlow(loadBgFeedInterval())
|
||||
val bgFeedRefreshInterval: StateFlow<BgFeedRefreshInterval> =
|
||||
_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<SbCategory> {
|
||||
val raw = sp.getStringSet(KEY_SB_CATS, null)
|
||||
return if (raw == null) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue