Twelve: Implement playback resumption

Paves the way to restore old queue/playback state
when the app is loaded after it has been killed

Change-Id: Ica0a2190ed8b9b226320a751be7d68b7a8b7c9f5
diff --git a/app/schemas/org.lineageos.twelve.database.TwelveDatabase/2.json b/app/schemas/org.lineageos.twelve.database.TwelveDatabase/2.json
new file mode 100644
index 0000000..e688f18
--- /dev/null
+++ b/app/schemas/org.lineageos.twelve.database.TwelveDatabase/2.json
@@ -0,0 +1,257 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "58b448c852bab0350bdd5a3eac0df202",
+    "entities": [
+      {
+        "tableName": "Playlist",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `last_modified` INTEGER NOT NULL, `track_count` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "trackCount",
+            "columnName": "track_count",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "playlist_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "Item",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`item_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `audio_uri` TEXT NOT NULL, `count` INTEGER NOT NULL DEFAULT 0)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "audioUri",
+            "columnName": "audio_uri",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "count",
+            "columnName": "count",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "item_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_Item_audio_uri",
+            "unique": true,
+            "columnNames": [
+              "audio_uri"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Item_audio_uri` ON `${TABLE_NAME}` (`audio_uri`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "PlaylistItemCrossRef",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `item_id` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `item_id`), FOREIGN KEY(`playlist_id`) REFERENCES `Playlist`(`playlist_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`item_id`) REFERENCES `Item`(`item_id`) ON UPDATE CASCADE ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "playlistId",
+            "columnName": "playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "itemId",
+            "columnName": "item_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "playlist_id",
+            "item_id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_PlaylistItemCrossRef_item_id",
+            "unique": false,
+            "columnNames": [
+              "item_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_PlaylistItemCrossRef_item_id` ON `${TABLE_NAME}` (`item_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "Playlist",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "playlist_id"
+            ],
+            "referencedColumns": [
+              "playlist_id"
+            ]
+          },
+          {
+            "table": "Item",
+            "onDelete": "CASCADE",
+            "onUpdate": "CASCADE",
+            "columns": [
+              "item_id"
+            ],
+            "referencedColumns": [
+              "item_id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "ResumptionPlaylist",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`resumption_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `start_index` INTEGER NOT NULL, `start_position_ms` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "resumption_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startIndex",
+            "columnName": "start_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startPositionMs",
+            "columnName": "start_position_ms",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": true,
+          "columnNames": [
+            "resumption_id"
+          ]
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "ResumptionItem",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_index` INTEGER NOT NULL, `resumption_playlist_id` INTEGER NOT NULL, `media_id` TEXT NOT NULL, PRIMARY KEY(`playlist_index`), FOREIGN KEY(`resumption_playlist_id`) REFERENCES `ResumptionPlaylist`(`resumption_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "index",
+            "columnName": "playlist_index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "resumptionPlaylistId",
+            "columnName": "resumption_playlist_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "mediaId",
+            "columnName": "media_id",
+            "affinity": "TEXT",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "playlist_index"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_ResumptionItem_playlist_index",
+            "unique": true,
+            "columnNames": [
+              "playlist_index"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ResumptionItem_playlist_index` ON `${TABLE_NAME}` (`playlist_index`)"
+          },
+          {
+            "name": "index_ResumptionItem_resumption_playlist_id",
+            "unique": false,
+            "columnNames": [
+              "resumption_playlist_id"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_ResumptionItem_resumption_playlist_id` ON `${TABLE_NAME}` (`resumption_playlist_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "ResumptionPlaylist",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "resumption_playlist_id"
+            ],
+            "referencedColumns": [
+              "resumption_id"
+            ]
+          }
+        ]
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '58b448c852bab0350bdd5a3eac0df202')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/lineageos/twelve/TwelveApplication.kt b/app/src/main/java/org/lineageos/twelve/TwelveApplication.kt
index afb705a..c6f12d5 100644
--- a/app/src/main/java/org/lineageos/twelve/TwelveApplication.kt
+++ b/app/src/main/java/org/lineageos/twelve/TwelveApplication.kt
@@ -11,11 +11,13 @@
 import com.google.android.material.color.DynamicColors
 import org.lineageos.twelve.database.TwelveDatabase
 import org.lineageos.twelve.repositories.MediaRepository
+import org.lineageos.twelve.repositories.ResumptionPlaylistRepository
 
 @androidx.annotation.OptIn(UnstableApi::class)
 class TwelveApplication : Application() {
     private val database by lazy { TwelveDatabase.getInstance(applicationContext) }
     val mediaRepository by lazy { MediaRepository(applicationContext, database) }
+    val resumptionPlaylistRepository by lazy { ResumptionPlaylistRepository(database) }
     val audioSessionId by lazy { Util.generateAudioSessionIdV21(applicationContext) }
 
     override fun onCreate() {
diff --git a/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt b/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
index c54709f..e125ed5 100644
--- a/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
+++ b/app/src/main/java/org/lineageos/twelve/database/TwelveDatabase.kt
@@ -6,6 +6,7 @@
 package org.lineageos.twelve.database
 
 import android.content.Context
+import androidx.room.AutoMigration
 import androidx.room.Database
 import androidx.room.Room
 import androidx.room.RoomDatabase
@@ -15,17 +16,28 @@
 import org.lineageos.twelve.database.dao.PlaylistDao
 import org.lineageos.twelve.database.dao.PlaylistItemCrossRefDao
 import org.lineageos.twelve.database.dao.PlaylistWithItemsDao
+import org.lineageos.twelve.database.dao.ResumptionPlaylistDao
 import org.lineageos.twelve.database.entities.Item
 import org.lineageos.twelve.database.entities.Playlist
 import org.lineageos.twelve.database.entities.PlaylistItemCrossRef
+import org.lineageos.twelve.database.entities.ResumptionItem
+import org.lineageos.twelve.database.entities.ResumptionPlaylist
 
 @Database(
     entities = [
+        /* Playlist */
         Playlist::class,
         Item::class,
         PlaylistItemCrossRef::class,
+
+        /* Resumption */
+        ResumptionItem::class,
+        ResumptionPlaylist::class,
     ],
-    version = 1,
+    version = 2,
+    autoMigrations = [
+        AutoMigration(from = 1, to = 2),
+    ],
 )
 @TypeConverters(UriConverter::class)
 abstract class TwelveDatabase : RoomDatabase() {
@@ -33,6 +45,7 @@
     abstract fun getPlaylistDao(): PlaylistDao
     abstract fun getPlaylistItemCrossRefDao(): PlaylistItemCrossRefDao
     abstract fun getPlaylistWithItemsDao(): PlaylistWithItemsDao
+    abstract fun getResumptionPlaylistDao(): ResumptionPlaylistDao
 
     companion object {
         @Volatile
diff --git a/app/src/main/java/org/lineageos/twelve/database/dao/ResumptionPlaylistDao.kt b/app/src/main/java/org/lineageos/twelve/database/dao/ResumptionPlaylistDao.kt
new file mode 100644
index 0000000..e56b933
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/dao/ResumptionPlaylistDao.kt
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Transaction
+import org.lineageos.twelve.database.entities.ResumptionPlaylistWithMediaItems
+
+@Dao
+@Suppress("FunctionName")
+interface ResumptionPlaylistDao {
+    /**
+     * Clear all resumption playlists.
+     */
+    @Query("DELETE FROM ResumptionPlaylist")
+    suspend fun _clearResumptionPlaylists()
+
+    /**
+     * Insert a resumption playlist.
+     */
+    @Query(
+        """
+            INSERT INTO ResumptionPlaylist (start_index, start_position_ms)
+            VALUES (:startIndex, :startPositionMs)
+        """
+    )
+    suspend fun _createResumptionPlaylist(startIndex: Int, startPositionMs: Long): Long
+
+    /**
+     * Add an item to a resumption playlist.
+     */
+    @Query(
+        """
+            INSERT INTO ResumptionItem (playlist_index, resumption_playlist_id, media_id)
+            VALUES (:index, :resumptionPlaylistId, :mediaItem)
+        """
+    )
+    suspend fun _addItemToResumptionPlaylist(
+        index: Long,
+        resumptionPlaylistId: Long,
+        mediaItem: String
+    )
+
+    /**
+     * Creates a new resumption playlist given a list of media items.
+     */
+    @Transaction
+    suspend fun createResumptionPlaylist(
+        startIndex: Int,
+        startPositionMs: Long,
+        mediaItems: List<String>
+    ) {
+        _clearResumptionPlaylists()
+
+        val id = _createResumptionPlaylist(startIndex, startPositionMs)
+
+        mediaItems.forEachIndexed { index, it ->
+            _addItemToResumptionPlaylist(index.toLong(), id, it)
+        }
+    }
+
+    /**
+     * Get resumption playlist with items
+     */
+    @Transaction
+    @Query("SELECT * FROM ResumptionPlaylist LIMIT 1")
+    suspend fun getResumptionPlaylistWithItems(): ResumptionPlaylistWithMediaItems?
+
+    /**
+     * Update resumption playlist.
+     */
+    @Query("UPDATE ResumptionPlaylist SET start_index = :currentMediaItemIndex, start_position_ms = :currentPosition")
+    suspend fun updateResumptionPlaylist(currentMediaItemIndex: Int, currentPosition: Long): Int
+}
diff --git a/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionItem.kt b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionItem.kt
new file mode 100644
index 0000000..bcdb9ba
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionItem.kt
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import androidx.room.PrimaryKey
+
+/**
+ * Resumption item.
+ *
+ * @param playlistIndex Index of this item in the playlist
+ * @param resumptionPlaylistId ID of the resumption playlist, this is only needed to easily build a
+ *   [ResumptionPlaylistWithMediaItems]
+ * @param mediaId ID of the media item
+ */
+@Entity(
+    indices = [
+        Index(value = ["playlist_index"], unique = true),
+        Index(value = ["resumption_playlist_id"]),
+    ],
+    foreignKeys = [
+        ForeignKey(
+            entity = ResumptionPlaylist::class,
+            parentColumns = ["resumption_id"],
+            childColumns = ["resumption_playlist_id"],
+            onDelete = ForeignKey.CASCADE,
+        ),
+    ],
+)
+data class ResumptionItem(
+    @PrimaryKey @ColumnInfo(name = "playlist_index") val playlistIndex: Long,
+    @ColumnInfo(name = "resumption_playlist_id") val resumptionPlaylistId: Long,
+    @ColumnInfo(name = "media_id") val mediaId: String,
+)
diff --git a/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylist.kt b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylist.kt
new file mode 100644
index 0000000..28bf3a0
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylist.kt
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class ResumptionPlaylist(
+    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "resumption_id") val id: Long,
+    @ColumnInfo(name = "start_index") val startIndex: Int,
+    @ColumnInfo(name = "start_position_ms") val startPositionMs: Long,
+)
diff --git a/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylistWithMediaItems.kt b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylistWithMediaItems.kt
new file mode 100644
index 0000000..c3cf682
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/database/entities/ResumptionPlaylistWithMediaItems.kt
@@ -0,0 +1,17 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.database.entities
+
+import androidx.room.Embedded
+import androidx.room.Relation
+
+data class ResumptionPlaylistWithMediaItems(
+    @Embedded val resumptionPlaylist: ResumptionPlaylist,
+    @Relation(
+        parentColumn = "resumption_id",
+        entityColumn = "resumption_playlist_id",
+    ) val items: List<ResumptionItem>,
+)
diff --git a/app/src/main/java/org/lineageos/twelve/models/ResumptionPlaylist.kt b/app/src/main/java/org/lineageos/twelve/models/ResumptionPlaylist.kt
new file mode 100644
index 0000000..9d83ef4
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/models/ResumptionPlaylist.kt
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.models
+
+/**
+ * A resumption playlist.
+ *
+ * @param mediaItemIds The list of audio media item IDs
+ * @param startIndex The start index, in the range [0..mediaItemIds.size)
+ * @param startPositionMs The playback position in milliseconds
+ */
+data class ResumptionPlaylist(
+    val mediaItemIds: List<String>,
+    val startIndex: Int = 0,
+    val startPositionMs: Long = 0L,
+) {
+    init {
+        require(startIndex in mediaItemIds.indices) { "Invalid start index" }
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/repositories/ResumptionPlaylistRepository.kt b/app/src/main/java/org/lineageos/twelve/repositories/ResumptionPlaylistRepository.kt
new file mode 100644
index 0000000..1f34a2b
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/repositories/ResumptionPlaylistRepository.kt
@@ -0,0 +1,50 @@
+package org.lineageos.twelve.repositories
+
+import org.lineageos.twelve.database.TwelveDatabase
+import org.lineageos.twelve.models.ResumptionPlaylist
+
+/**
+ * Manages the playlist used when the user wants to resume playback from the last queue.
+ */
+class ResumptionPlaylistRepository(val database: TwelveDatabase) {
+    /**
+     * Get the last resumption playlist or an empty one.
+     */
+    suspend fun getResumptionPlaylist() =
+        database.getResumptionPlaylistDao().getResumptionPlaylistWithItems()?.let {
+            ResumptionPlaylist(
+                it.items.sortedBy { item ->
+                    item.playlistIndex
+                }.map { item ->
+                    item.mediaId
+                },
+                it.resumptionPlaylist.startIndex,
+                it.resumptionPlaylist.startPositionMs,
+            )
+        } ?: ResumptionPlaylist(emptyList())
+
+    /**
+     * When the user changes the queue, create a new resumption playlist.
+     *
+     * @param mediaIds The list of audio media item IDs
+     * @param startIndex The start index
+     * @param startPositionMs The playback position in milliseconds
+     */
+    suspend fun onMediaItemsChanged(
+        mediaIds: List<String>,
+        startIndex: Int,
+        startPositionMs: Long,
+    ) = database.getResumptionPlaylistDao().createResumptionPlaylist(
+        startIndex,
+        startPositionMs,
+        mediaIds,
+    )
+
+    suspend fun onPlaybackPositionChanged(
+        startIndex: Int,
+        startPositionMs: Long,
+    ) = database.getResumptionPlaylistDao().updateResumptionPlaylist(
+        startIndex,
+        startPositionMs,
+    )
+}
diff --git a/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt b/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
index 3f233f3..d1bc190 100644
--- a/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
+++ b/app/src/main/java/org/lineageos/twelve/services/MediaRepositoryTree.kt
@@ -33,12 +33,6 @@
     fun getRootMediaItem() = rootMediaItem
 
     /**
-     * Get the resumption playlist of the tree.
-     * TODO
-     */
-    fun getResumptionPlaylist() = listOf<MediaItem>()
-
-    /**
      * Given a media ID, gets it's corresponding media item.
      */
     suspend fun getItem(mediaId: String) = when (mediaId) {
diff --git a/app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt b/app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt
index d93dca5..d97bfec 100644
--- a/app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt
+++ b/app/src/main/java/org/lineageos/twelve/services/PlaybackService.kt
@@ -27,6 +27,7 @@
 import androidx.media3.session.MediaSession
 import androidx.media3.session.SessionError
 import kotlinx.coroutines.guava.future
+import kotlinx.coroutines.launch
 import org.lineageos.twelve.MainActivity
 import org.lineageos.twelve.R
 import org.lineageos.twelve.TwelveApplication
@@ -46,14 +47,46 @@
         )
     }
 
+    private val resumptionPlaylistRepository by lazy {
+        (application as TwelveApplication).resumptionPlaylistRepository
+    }
+
     private val mediaLibrarySessionCallback = object : MediaLibrarySession.Callback {
         override fun onPlaybackResumption(
             mediaSession: MediaSession,
             controller: MediaSession.ControllerInfo
         ) = lifecycle.coroutineScope.future {
-            MediaSession.MediaItemsWithStartPosition(
-                mediaRepositoryTree.getResumptionPlaylist(), 0, 0
-            )
+            val resumptionPlaylist = resumptionPlaylistRepository.getResumptionPlaylist()
+
+            var startIndex = resumptionPlaylist.startIndex
+            var startPositionMs = resumptionPlaylist.startPositionMs
+
+            val mediaItems = resumptionPlaylist.mediaItemIds.mapIndexed { index, itemId ->
+                when (val mediaItem = mediaRepositoryTree.getItem(itemId)) {
+                    null -> {
+                        if (index == resumptionPlaylist.startIndex) {
+                            // The playback position is now invalid
+                            startPositionMs = 0
+
+                            // Let's try the next item, this is done automatically since
+                            // the next item will take this item's index
+                        } else if (index < resumptionPlaylist.startIndex) {
+                            // The missing media is before the start index, we have to offset
+                            // the start by 1 entry
+                            startIndex -= 1
+                        }
+
+                        null
+                    }
+
+                    else -> mediaItem
+                }
+            }.filterNotNull()
+
+            // Shouldn't be needed, but just to be sure
+            startIndex = startIndex.coerceIn(0, mediaItems.size - 1)
+
+            MediaSession.MediaItemsWithStartPosition(mediaItems, startIndex, startPositionMs)
         }
 
         override fun onGetLibraryRoot(
@@ -102,8 +135,18 @@
             startIndex: Int,
             startPositionMs: Long,
         ) = lifecycle.coroutineScope.future {
+            val resolvedMediaItems = mediaRepositoryTree.resolveMediaItems(mediaItems)
+
+            launch {
+                resumptionPlaylistRepository.onMediaItemsChanged(
+                    resolvedMediaItems.map { it.mediaId },
+                    startIndex,
+                    startPositionMs,
+                )
+            }
+
             MediaSession.MediaItemsWithStartPosition(
-                mediaRepositoryTree.resolveMediaItems(mediaItems),
+                resolvedMediaItems,
                 startIndex,
                 startPositionMs
             )
@@ -221,6 +264,16 @@
                 closeAudioEffectSession()
             }
         }
+
+        // Update startIndex and startPositionMs in resumption playlist.
+        if (events.containsAny(Player.EVENT_MEDIA_ITEM_TRANSITION)) {
+            lifecycle.coroutineScope.launch {
+                resumptionPlaylistRepository.onPlaybackPositionChanged(
+                    player.currentMediaItemIndex,
+                    player.currentPosition
+                )
+            }
+        }
     }
 
     private fun openAudioEffectSession() {