From f3a0c44460b8af3eecf07dfe5a7275566d5d1c86 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 22 May 2026 16:35:10 -0400 Subject: [PATCH] feat(android): port cached_resume_state + fix CacheSource enum (M8 4.2 slice 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related fixes from re-reading the Drift source for slice 8: 1. The plan called this slice "last_played" but the Drift table is `cached_resume_state` — kept Drift's name for cross-reference during the port. Single-row JSON-blob pattern (queue, currentIndex, positionMs, source) — ResumeController (Phase 6.5) handles the Kotlin-side encode/decode so the schema stays stable across resume-shape evolution. 2. CacheSource enum was incomplete: Task 4.1 ported only 3 of the 5 Drift variants. Added AUTO_LIKED + AUTO_PLAYLIST (used by the auto-cache prefetcher to tag cached files by reason — drives the bucket eviction priority order INCIDENTAL > AUTO_PREFETCH > AUTO_PLAYLIST > AUTO_LIKED > MANUAL). No data migration needed — schema version is still 1 and we have no real users yet. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/cache/db/AppDatabase.kt | 4 +++ .../minstrel/cache/db/TypeConverters.kt | 11 ++++++-- .../cache/db/dao/CachedResumeStateDao.kt | 23 +++++++++++++++ .../db/entities/CachedResumeStateEntity.kt | 28 +++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/dao/CachedResumeStateDao.kt create mode 100644 android/app/src/main/java/com/fabledsword/minstrel/cache/db/entities/CachedResumeStateEntity.kt 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(), +)