diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt index f9b5a1b0..f78325a9 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/AppDatabase.kt @@ -9,6 +9,7 @@ import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao import com.fabledsword.minstrel.cache.db.dao.CachedLikeDao import com.fabledsword.minstrel.cache.db.dao.CachedMutationDao import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistDao +import com.fabledsword.minstrel.cache.db.dao.CachedResumeStateDao import com.fabledsword.minstrel.cache.db.dao.CachedPlaylistTrackDao import com.fabledsword.minstrel.cache.db.dao.CachedQuarantineDao import com.fabledsword.minstrel.cache.db.dao.CachedTrackDao @@ -19,6 +20,7 @@ import com.fabledsword.minstrel.cache.db.entities.CachedArtistEntity import com.fabledsword.minstrel.cache.db.entities.CachedLikeEntity import com.fabledsword.minstrel.cache.db.entities.CachedMutationEntity import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistEntity +import com.fabledsword.minstrel.cache.db.entities.CachedResumeStateEntity import com.fabledsword.minstrel.cache.db.entities.CachedPlaylistTrackEntity import com.fabledsword.minstrel.cache.db.entities.CachedQuarantineEntity import com.fabledsword.minstrel.cache.db.entities.CachedTrackEntity @@ -49,6 +51,7 @@ import com.fabledsword.minstrel.cache.db.entities.SyncMetadataEntity CachedQuarantineEntity::class, AudioCacheIndexEntity::class, CachedMutationEntity::class, + CachedResumeStateEntity::class, ], version = 1, exportSchema = true, @@ -65,4 +68,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun cachedQuarantineDao(): CachedQuarantineDao abstract fun audioCacheIndexDao(): AudioCacheIndexDao abstract fun cachedMutationDao(): CachedMutationDao + abstract fun cachedResumeStateDao(): CachedResumeStateDao } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/TypeConverters.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/TypeConverters.kt index 289cc15a..2ac1e6a7 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/TypeConverters.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/TypeConverters.kt @@ -5,13 +5,18 @@ import kotlinx.datetime.Instant /** * Source category for a cached audio file. Mirrors the Drift `CacheSource` - * enum used by the Flutter client's `audio_cache_index` table. + * enum used by the Flutter client's `audio_cache_index` table 1:1. * * - `MANUAL`: user explicitly pinned (e.g. "save for offline") - * - `INCIDENTAL`: cached as a side effect of playback streaming + * - `AUTO_LIKED`: auto-cached because the track is liked + * - `AUTO_PLAYLIST`: auto-cached because it's in a pinned playlist * - `AUTO_PREFETCH`: pre-warmed by background prefetch heuristics + * - `INCIDENTAL`: cached as a side effect of playback streaming + * + * Eviction priority order (first to evict): INCIDENTAL → AUTO_PREFETCH + * → AUTO_PLAYLIST → AUTO_LIKED → MANUAL. */ -enum class CacheSource { MANUAL, INCIDENTAL, AUTO_PREFETCH } +enum class CacheSource { MANUAL, AUTO_LIKED, AUTO_PLAYLIST, AUTO_PREFETCH, INCIDENTAL } /** * Cross-cutting Room type converters. Registered on `@Database` via diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedResumeStateDao.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedResumeStateDao.kt new file mode 100644 index 00000000..48931e28 --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedResumeStateDao.kt @@ -0,0 +1,23 @@ +package com.fabledsword.minstrel.cache.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.fabledsword.minstrel.cache.db.entities.CachedResumeStateEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface CachedResumeStateDao { + @Query("SELECT * FROM cached_resume_state WHERE id = 1") + suspend fun get(): CachedResumeStateEntity? + + @Query("SELECT * FROM cached_resume_state WHERE id = 1") + fun observe(): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(row: CachedResumeStateEntity) + + @Query("DELETE FROM cached_resume_state") + suspend fun clear() +} diff --git a/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedResumeStateEntity.kt b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedResumeStateEntity.kt new file mode 100644 index 00000000..1066006d --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedResumeStateEntity.kt @@ -0,0 +1,28 @@ +package com.fabledsword.minstrel.cache.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * Single-row snapshot of the last playback session — queue (as JSON), + * current index, position, and source tag. Mirrors + * `flutter_client/lib/cache/db.dart`'s `CachedResumeState` Drift table. + * + * Lets a torn-down session (the player's idle/dismissed teardown) + * resume on next launch; without it the headset / lock-screen play + * button has nothing to act on. The Flutter codebase persists this on + * every queue change and reads on cold start. + * + * The `json` field carries the entire ResumeState shape (queue trackrefs, + * currentIndex, positionMs, sourceTag). Kotlin-side deserialization + * happens in the ResumeController (Phase 6.5) so the schema stays + * stable across resume-shape changes. + */ +@Entity(tableName = "cached_resume_state") +data class CachedResumeStateEntity( + @PrimaryKey val id: Int = 1, + val json: String, + val updatedAt: Instant = Clock.System.now(), +)