feat(android): persist CacheSettings + PlayerModule reads from AuthStore
Foundation for audit #5 Storage settings. CacheSettings carries the four user-tunable cache prefs (liked/rolling caps, prefetch window, cache-liked toggle) mirroring Flutter's cache_settings_provider.dart field-for-field. Defaults match Flutter (5 GiB per bucket, prefetch window = 5, cache-liked = true). Persistence rides AuthSessionEntity (the de-facto single-row prefs table) as a JSON blob in a new cacheSettingsJson column. DB version bump 4→5; destructive migration per the pre-release policy. PlayerModule.provideCacheConfig now snapshots AuthStore.cacheSettings at injection time. SimpleCache is constructed once per process, so limit changes from the Settings UI (next commit) take effect on next app launch. Documented in the @Provides KDoc. Storage section UI lands in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.fabledsword.minstrel.auth
|
package com.fabledsword.minstrel.auth
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.cache.audiocache.CacheSettings
|
||||||
import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao
|
import com.fabledsword.minstrel.cache.db.dao.AuthSessionDao
|
||||||
import com.fabledsword.minstrel.cache.db.entities.AuthSessionEntity
|
import com.fabledsword.minstrel.cache.db.entities.AuthSessionEntity
|
||||||
import com.fabledsword.minstrel.di.ApplicationScope
|
import com.fabledsword.minstrel.di.ApplicationScope
|
||||||
@@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -46,6 +48,11 @@ class AuthStore @Inject constructor(
|
|||||||
private val clientIdState = MutableStateFlow<String?>(null)
|
private val clientIdState = MutableStateFlow<String?>(null)
|
||||||
val clientId: StateFlow<String?> = clientIdState.asStateFlow()
|
val clientId: StateFlow<String?> = clientIdState.asStateFlow()
|
||||||
|
|
||||||
|
private val cacheSettingsState = MutableStateFlow(CacheSettings.DEFAULT)
|
||||||
|
val cacheSettings: StateFlow<CacheSettings> = cacheSettingsState.asStateFlow()
|
||||||
|
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
dao.observe().collect { row ->
|
dao.observe().collect { row ->
|
||||||
@@ -54,10 +61,18 @@ class AuthStore @Inject constructor(
|
|||||||
userJsonState.value = row?.userJson
|
userJsonState.value = row?.userJson
|
||||||
themeModeState.value = row?.themeMode
|
themeModeState.value = row?.themeMode
|
||||||
clientIdState.value = row?.clientId
|
clientIdState.value = row?.clientId
|
||||||
|
cacheSettingsState.value = decodeCacheSettings(row?.cacheSettingsJson)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun decodeCacheSettings(raw: String?): CacheSettings {
|
||||||
|
if (raw.isNullOrEmpty()) return CacheSettings.DEFAULT
|
||||||
|
return runCatching {
|
||||||
|
json.decodeFromString(CacheSettings.serializer(), raw)
|
||||||
|
}.getOrDefault(CacheSettings.DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
fun setSessionCookie(value: String?) {
|
fun setSessionCookie(value: String?) {
|
||||||
sessionCookieState.value = value
|
sessionCookieState.value = value
|
||||||
scope.launch { persistCookie(value) }
|
scope.launch { persistCookie(value) }
|
||||||
@@ -83,6 +98,12 @@ class AuthStore @Inject constructor(
|
|||||||
scope.launch { persistClientId(value) }
|
scope.launch { persistClientId(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCacheSettings(value: CacheSettings) {
|
||||||
|
cacheSettingsState.value = value
|
||||||
|
val encoded = json.encodeToString(CacheSettings.serializer(), value)
|
||||||
|
scope.launch { persistCacheSettings(encoded) }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun persistCookie(value: String?) {
|
private suspend fun persistCookie(value: String?) {
|
||||||
if (dao.get() == null) {
|
if (dao.get() == null) {
|
||||||
dao.upsert(currentEntity().copy(sessionCookie = value))
|
dao.upsert(currentEntity().copy(sessionCookie = value))
|
||||||
@@ -123,6 +144,14 @@ class AuthStore @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun persistCacheSettings(json: String) {
|
||||||
|
if (dao.get() == null) {
|
||||||
|
dao.upsert(currentEntity().copy(cacheSettingsJson = json))
|
||||||
|
} else {
|
||||||
|
dao.setCacheSettingsJson(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun currentEntity(): AuthSessionEntity = AuthSessionEntity(
|
private fun currentEntity(): AuthSessionEntity = AuthSessionEntity(
|
||||||
id = ROW_ID,
|
id = ROW_ID,
|
||||||
sessionCookie = sessionCookieState.value,
|
sessionCookie = sessionCookieState.value,
|
||||||
@@ -130,6 +159,10 @@ class AuthStore @Inject constructor(
|
|||||||
userJson = userJsonState.value,
|
userJson = userJsonState.value,
|
||||||
themeMode = themeModeState.value,
|
themeMode = themeModeState.value,
|
||||||
clientId = clientIdState.value,
|
clientId = clientIdState.value,
|
||||||
|
cacheSettingsJson = json.encodeToString(
|
||||||
|
CacheSettings.serializer(),
|
||||||
|
cacheSettingsState.value,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
package com.fabledsword.minstrel.cache.audiocache
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
private const val FIVE_GIB_BYTES = 5L * 1024 * 1024 * 1024
|
||||||
|
private const val DEFAULT_PREFETCH_WINDOW = 5
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User-tunable audio cache settings. Mirrors Flutter's `CacheSettings`
|
||||||
|
* (cache_settings_provider.dart) field-for-field. Persisted as a JSON
|
||||||
|
* blob on the auth_session single-row table via [AuthStore].
|
||||||
|
*
|
||||||
|
* - [likedCapBytes]: budget for cached files of liked tracks. 0 means
|
||||||
|
* unlimited. Effective at next app start (SimpleCache is built once
|
||||||
|
* per process).
|
||||||
|
* - [rollingCapBytes]: budget for incidental / auto-prefetch downloads.
|
||||||
|
* Same 0=unlimited / restart-to-apply caveat.
|
||||||
|
* - [prefetchWindow]: 1..10. Number of next-tracks the prefetcher
|
||||||
|
* pre-downloads. Has no effect until the prefetcher lands (separate
|
||||||
|
* audit follow-up) — persisted now so the user can pre-configure.
|
||||||
|
* - [cacheLikedTracks]: when true, liking a track should also pin its
|
||||||
|
* audio to the liked-cache bucket. Has no effect until pin-on-like
|
||||||
|
* lands (separate follow-up).
|
||||||
|
*
|
||||||
|
* Defaults match Flutter: 5 GiB per bucket, prefetch=5, cache-liked=true.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class CacheSettings(
|
||||||
|
val likedCapBytes: Long = FIVE_GIB_BYTES,
|
||||||
|
val rollingCapBytes: Long = FIVE_GIB_BYTES,
|
||||||
|
val prefetchWindow: Int = DEFAULT_PREFETCH_WINDOW,
|
||||||
|
val cacheLikedTracks: Boolean = true,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT: CacheSettings = CacheSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity
|
|||||||
CachedHomeIndexEntity::class,
|
CachedHomeIndexEntity::class,
|
||||||
AuthSessionEntity::class,
|
AuthSessionEntity::class,
|
||||||
],
|
],
|
||||||
version = 4,
|
version = 5,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@TypeConverters(MinstrelTypeConverters::class)
|
@TypeConverters(MinstrelTypeConverters::class)
|
||||||
|
|||||||
+4
@@ -38,4 +38,8 @@ interface AuthSessionDao {
|
|||||||
/** Partial update: change only the stable client install id. */
|
/** Partial update: change only the stable client install id. */
|
||||||
@Query("UPDATE auth_session SET clientId = :clientId WHERE id = 0")
|
@Query("UPDATE auth_session SET clientId = :clientId WHERE id = 0")
|
||||||
suspend fun setClientId(clientId: String?)
|
suspend fun setClientId(clientId: String?)
|
||||||
|
|
||||||
|
/** Partial update: change only the serialized cache settings. */
|
||||||
|
@Query("UPDATE auth_session SET cacheSettingsJson = :json WHERE id = 0")
|
||||||
|
suspend fun setCacheSettingsJson(json: String?)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -29,4 +29,11 @@ data class AuthSessionEntity(
|
|||||||
val userJson: String? = null,
|
val userJson: String? = null,
|
||||||
val themeMode: String? = null,
|
val themeMode: String? = null,
|
||||||
val clientId: String? = null,
|
val clientId: String? = null,
|
||||||
|
/**
|
||||||
|
* JSON-encoded CacheSettings (cache/audiocache/CacheSettings.kt).
|
||||||
|
* Null = use defaults. Stored as a JSON blob rather than four
|
||||||
|
* individual columns to keep schema changes cheap when the
|
||||||
|
* CacheSettings shape evolves.
|
||||||
|
*/
|
||||||
|
val cacheSettingsJson: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.fabledsword.minstrel.player
|
package com.fabledsword.minstrel.player
|
||||||
|
|
||||||
|
import com.fabledsword.minstrel.auth.AuthStore
|
||||||
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
import com.fabledsword.minstrel.cache.audiocache.CacheConfig
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -21,7 +22,23 @@ import javax.inject.Singleton
|
|||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object PlayerModule {
|
object PlayerModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CacheConfig snapshot from the user's persisted CacheSettings.
|
||||||
|
* SimpleCache is constructed once per process, so changes via the
|
||||||
|
* Settings UI take effect on next app launch (not mid-flight).
|
||||||
|
* AuthStore's StateFlow hydrates async from Room — when the
|
||||||
|
* factory @Provides runs, the value is either the persisted
|
||||||
|
* setting (if Room finished first) or CacheSettings.DEFAULT.
|
||||||
|
* Worst case the user's tuning lands on next-next launch, which
|
||||||
|
* is acceptable for a single-user device pref.
|
||||||
|
*/
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCacheConfig(): CacheConfig = CacheConfig()
|
fun provideCacheConfig(authStore: AuthStore): CacheConfig {
|
||||||
|
val s = authStore.cacheSettings.value
|
||||||
|
return CacheConfig(
|
||||||
|
likedCapBytes = s.likedCapBytes,
|
||||||
|
rollingCapBytes = s.rollingCapBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user