From f27215a4be8baee45884318512a8005e6bafda08 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Mon, 23 Jan 2023 12:45:55 -0700 Subject: [PATCH] playback: use room for persistence Finally give up and use Room to persist the playback state. This should make dependency injection much easier, and the implementation isn't exactly the *worst*, as I was already using "raw" data structures for the old queue database. --- app/build.gradle | 7 + .../java/org/oxycblt/auxio/MainActivity.kt | 2 + .../auxio/playback/PlaybackSettings.kt | 283 ++++++++------- .../auxio/playback/PlaybackViewModel.kt | 11 +- .../playback/persist/PersistenceConverters.kt | 34 ++ .../playback/persist/PersistenceDatabase.kt | 189 ++++++++++ .../playback/persist/PersistenceRepository.kt | 134 +++++++ .../auxio/playback/{state => queue}/Queue.kt | 2 +- .../auxio/playback/queue/QueueViewModel.kt | 1 - .../playback/state/PlaybackStateDatabase.kt | 335 ------------------ .../playback/state/PlaybackStateManager.kt | 22 +- .../playback/system/MediaSessionComponent.kt | 2 +- .../auxio/playback/system/PlaybackService.kt | 13 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 2 +- build.gradle | 6 +- 15 files changed, 536 insertions(+), 507 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt rename app/src/main/java/org/oxycblt/auxio/playback/{state => queue}/Queue.kt (99%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt diff --git a/app/build.gradle b/app/build.gradle index 45bf5f5ea..594f354eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id "androidx.navigation.safeargs.kotlin" id "com.diffplug.spotless" id "kotlin-parcelize" + id 'kotlin-kapt' } android { @@ -93,6 +94,12 @@ dependencies { // Preferences implementation "androidx.preference:preference-ktx:1.2.0" + // Database + def room_version = '2.4.3' + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + // --- THIRD PARTY --- // Exoplayer diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 201abbd18..b1550b8af 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -48,6 +48,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * * TODO: Unit testing * + * TODO: Migrate to value classes FOR ALL ENUMS + * * @author Alexander Capehart (OxygenCobalt) */ class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt index 8c3d36b7a..91acc839c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -66,153 +66,150 @@ interface PlaybackSettings : Settings { fun onNotificationActionChanged() {} } - private class Real(context: Context) : Settings.Real(context), PlaybackSettings { - override val inListPlaybackMode: MusicMode - get() = - MusicMode.fromIntCode( - sharedPreferences.getInt( - getString(R.string.set_key_in_list_playback_mode), Int.MIN_VALUE)) - ?: MusicMode.SONGS - - override val inParentPlaybackMode: MusicMode? - get() = - MusicMode.fromIntCode( - sharedPreferences.getInt( - getString(R.string.set_key_in_parent_playback_mode), Int.MIN_VALUE)) - - override val barAction: ActionMode - get() = - ActionMode.fromIntCode( - sharedPreferences.getInt(getString(R.string.set_key_bar_action), Int.MIN_VALUE)) - ?: ActionMode.NEXT - - override val notificationAction: ActionMode - get() = - ActionMode.fromIntCode( - sharedPreferences.getInt( - getString(R.string.set_key_notif_action), Int.MIN_VALUE)) - ?: ActionMode.REPEAT - - override val headsetAutoplay: Boolean - get() = - sharedPreferences.getBoolean(getString(R.string.set_key_headset_autoplay), false) - - override val replayGainMode: ReplayGainMode - get() = - ReplayGainMode.fromIntCode( - sharedPreferences.getInt( - getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) - ?: ReplayGainMode.DYNAMIC - - override var replayGainPreAmp: ReplayGainPreAmp - get() = - ReplayGainPreAmp( - sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_with), 0f), - sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_without), 0f)) - set(value) { - sharedPreferences.edit { - putFloat(getString(R.string.set_key_pre_amp_with), value.with) - putFloat(getString(R.string.set_key_pre_amp_without), value.without) - apply() - } - } - - override val keepShuffle: Boolean - get() = sharedPreferences.getBoolean(getString(R.string.set_key_keep_shuffle), true) - - override val rewindWithPrev: Boolean - get() = sharedPreferences.getBoolean(getString(R.string.set_key_rewind_prev), true) - - override val pauseOnRepeat: Boolean - get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false) - - override fun migrate() { - // "Use alternate notification action" was converted to an ActionMode setting in 3.0.0. - if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) { - logD("Migrating $OLD_KEY_ALT_NOTIF_ACTION") - - val mode = - if (sharedPreferences.getBoolean(OLD_KEY_ALT_NOTIF_ACTION, false)) { - ActionMode.SHUFFLE - } else { - ActionMode.REPEAT - } - - sharedPreferences.edit { - putInt(getString(R.string.set_key_notif_action), mode.intCode) - remove(OLD_KEY_ALT_NOTIF_ACTION) - apply() - } - } - - // PlaybackMode was converted to MusicMode in 3.0.0 - - fun Int.migratePlaybackMode() = - when (this) { - // Convert PlaybackMode into MusicMode - IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS - IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS - IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS - IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES - else -> null - } - - if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE") - - val mode = - sharedPreferences - .getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS) - .migratePlaybackMode() - ?: MusicMode.SONGS - - sharedPreferences.edit { - putInt(getString(R.string.set_key_in_list_playback_mode), mode.intCode) - remove(OLD_KEY_LIB_PLAYBACK_MODE) - apply() - } - } - - if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) { - logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE") - - val mode = - sharedPreferences - .getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE) - .migratePlaybackMode() - - sharedPreferences.edit { - putInt( - getString(R.string.set_key_in_parent_playback_mode), - mode?.intCode ?: Int.MIN_VALUE) - remove(OLD_KEY_DETAIL_PLAYBACK_MODE) - apply() - } - } - } - - override fun onSettingChanged(key: String, listener: Listener) { - when (key) { - getString(R.string.set_key_replay_gain), - getString(R.string.set_key_pre_amp_with), - getString(R.string.set_key_pre_amp_without) -> - listener.onReplayGainSettingsChanged() - getString(R.string.set_key_notif_action) -> listener.onNotificationActionChanged() - } - } - - private companion object { - const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" - const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" - const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode" - } - } - companion object { /** * Get a framework-backed implementation. * @param context [Context] required. */ - fun from(context: Context): PlaybackSettings = Real(context) + fun from(context: Context): PlaybackSettings = RealPlaybackSettings(context) + } +} + +private class RealPlaybackSettings(context: Context) : + Settings.Real(context), PlaybackSettings { + override val inListPlaybackMode: MusicMode + get() = + MusicMode.fromIntCode( + sharedPreferences.getInt( + getString(R.string.set_key_in_list_playback_mode), Int.MIN_VALUE)) + ?: MusicMode.SONGS + + override val inParentPlaybackMode: MusicMode? + get() = + MusicMode.fromIntCode( + sharedPreferences.getInt( + getString(R.string.set_key_in_parent_playback_mode), Int.MIN_VALUE)) + + override val barAction: ActionMode + get() = + ActionMode.fromIntCode( + sharedPreferences.getInt(getString(R.string.set_key_bar_action), Int.MIN_VALUE)) + ?: ActionMode.NEXT + + override val notificationAction: ActionMode + get() = + ActionMode.fromIntCode( + sharedPreferences.getInt(getString(R.string.set_key_notif_action), Int.MIN_VALUE)) + ?: ActionMode.REPEAT + + override val headsetAutoplay: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_headset_autoplay), false) + + override val replayGainMode: ReplayGainMode + get() = + ReplayGainMode.fromIntCode( + sharedPreferences.getInt(getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) + ?: ReplayGainMode.DYNAMIC + + override var replayGainPreAmp: ReplayGainPreAmp + get() = + ReplayGainPreAmp( + sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_with), 0f), + sharedPreferences.getFloat(getString(R.string.set_key_pre_amp_without), 0f)) + set(value) { + sharedPreferences.edit { + putFloat(getString(R.string.set_key_pre_amp_with), value.with) + putFloat(getString(R.string.set_key_pre_amp_without), value.without) + apply() + } + } + + override val keepShuffle: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_keep_shuffle), true) + + override val rewindWithPrev: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_rewind_prev), true) + + override val pauseOnRepeat: Boolean + get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false) + + override fun migrate() { + // "Use alternate notification action" was converted to an ActionMode setting in 3.0.0. + if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) { + logD("Migrating $OLD_KEY_ALT_NOTIF_ACTION") + + val mode = + if (sharedPreferences.getBoolean(OLD_KEY_ALT_NOTIF_ACTION, false)) { + ActionMode.SHUFFLE + } else { + ActionMode.REPEAT + } + + sharedPreferences.edit { + putInt(getString(R.string.set_key_notif_action), mode.intCode) + remove(OLD_KEY_ALT_NOTIF_ACTION) + apply() + } + } + + // PlaybackMode was converted to MusicMode in 3.0.0 + + fun Int.migratePlaybackMode() = + when (this) { + // Convert PlaybackMode into MusicMode + IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS + IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS + IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS + IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES + else -> null + } + + if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE") + + val mode = + sharedPreferences + .getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS) + .migratePlaybackMode() + ?: MusicMode.SONGS + + sharedPreferences.edit { + putInt(getString(R.string.set_key_in_list_playback_mode), mode.intCode) + remove(OLD_KEY_LIB_PLAYBACK_MODE) + apply() + } + } + + if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE") + + val mode = + sharedPreferences + .getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE) + .migratePlaybackMode() + + sharedPreferences.edit { + putInt( + getString(R.string.set_key_in_parent_playback_mode), + mode?.intCode ?: Int.MIN_VALUE) + remove(OLD_KEY_DETAIL_PLAYBACK_MODE) + apply() + } + } + } + + override fun onSettingChanged(key: String, listener: PlaybackSettings.Listener) { + when (key) { + getString(R.string.set_key_replay_gain), + getString(R.string.set_key_pre_amp_with), + getString(R.string.set_key_pre_amp_without) -> listener.onReplayGainSettingsChanged() + getString(R.string.set_key_notif_action) -> listener.onNotificationActionChanged() + } + } + + private companion object { + const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" + const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" + const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode" } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 097a9fab9..797165942 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -26,8 +26,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.playback.persist.PersistenceRepository +import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.* -import org.oxycblt.auxio.util.context /** * An [AndroidViewModel] that provides a safe UI frontend for the current playback state. @@ -38,6 +39,7 @@ class PlaybackViewModel(application: Application) : private val musicSettings = MusicSettings.from(application) private val playbackSettings = PlaybackSettings.from(application) private val playbackManager = PlaybackStateManager.getInstance() + private val persistenceRepository = PersistenceRepository.from(application) private val musicStore = MusicStore.getInstance() private var lastPositionJob: Job? = null @@ -428,7 +430,7 @@ class PlaybackViewModel(application: Application) : */ fun savePlaybackState(onDone: (Boolean) -> Unit) { viewModelScope.launch { - val saved = playbackManager.saveState(PlaybackStateDatabase.getInstance(context)) + val saved = playbackManager.saveState(persistenceRepository) onDone(saved) } } @@ -439,7 +441,7 @@ class PlaybackViewModel(application: Application) : */ fun wipePlaybackState(onDone: (Boolean) -> Unit) { viewModelScope.launch { - val wiped = playbackManager.wipeState(PlaybackStateDatabase.getInstance(context)) + val wiped = playbackManager.wipeState(persistenceRepository) onDone(wiped) } } @@ -451,8 +453,7 @@ class PlaybackViewModel(application: Application) : */ fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) { viewModelScope.launch { - val restored = - playbackManager.restoreState(PlaybackStateDatabase.getInstance(context), true) + val restored = playbackManager.restoreState(persistenceRepository, true) onDone(restored) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt new file mode 100644 index 000000000..395a22e16 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback.persist + +import androidx.room.TypeConverter +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.playback.state.RepeatMode + +/** + * Defines conversions used in the persistence table. + * @author Alexander Capehart (OxygenCobalt) + */ +object PersistenceConverters { + /** @see [Music.UID.toString] */ + @TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString() + + /** @see [Music.UID.fromString]*/ + @TypeConverter fun toMusicUid(string: String?) = string?.let(Music.UID::fromString) +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt new file mode 100644 index 000000000..ad0aa1351 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback.persist + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.oxycblt.auxio.music.Music +import org.oxycblt.auxio.playback.state.RepeatMode + +/** + * Provides raw access to the database storing the persisted playback state. + * @author Alexander Capehart + */ +@Database( + entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class], + version = 1, + exportSchema = false) +@TypeConverters(PersistenceConverters::class) +abstract class PersistenceDatabase : RoomDatabase() { + /** + * Get the current [PlaybackStateDao]. + * @return A [PlaybackStateDao] providing control of the database's playback state tables. + */ + abstract fun playbackStateDao(): PlaybackStateDao + + /** + * Get the current [QueueDao]. + * @return A [QueueDao] providing control of the database's queue tables. + */ + abstract fun queueDao(): QueueDao + + companion object { + @Volatile private var INSTANCE: PersistenceDatabase? = null + + /** + * Get/create the shared instance of this database. + * @param context [Context] required. + */ + fun getInstance(context: Context): PersistenceDatabase { + val instance = INSTANCE + if (instance != null) { + return instance + } + + synchronized(this) { + val newInstance = + Room.databaseBuilder( + context, + PersistenceDatabase::class.java, + "auxio_playback_persistence.db") + .fallbackToDestructiveMigration() + .fallbackToDestructiveMigrationOnDowngrade() + .build() + INSTANCE = newInstance + return newInstance + } + } + } +} + +/** + * Provides control of the persisted playback state table. + * @author Alexander Capehart (OxygenCobalt) + */ +@Dao +interface PlaybackStateDao { + /** + * Get the previously persisted [PlaybackState]. + * @return The previously persisted [PlaybackState], or null if one was not present. + */ + @Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0") + suspend fun getState(): PlaybackState? + + /** Delete any previously persisted [PlaybackState]s. */ + @Query("DELETE FROM ${PlaybackState.TABLE_NAME}") suspend fun nukeState() + + /** + * Insert a new [PlaybackState] into the database. + * @param state The [PlaybackState] to insert. + */ + @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertState(state: PlaybackState) +} + +/** + * Provides control of the persisted queue state tables. + * @author Alexander Capehart (OxygenCobalt) + */ +@Dao +interface QueueDao { + /** + * Get the previously persisted queue heap. + * @return A list of persisted [QueueHeapItem]s wrapping each heap item. + */ + @Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List + + /** + * Get the previously persisted queue mapping. + * @return A list of persisted [QueueMappingItem]s wrapping each heap item. + */ + @Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}") + suspend fun getMapping(): List + + /** Delete any previously persisted queue heap entries. */ + @Query("DELETE FROM ${QueueHeapItem.TABLE_NAME}") suspend fun nukeHeap() + + /** Delete any previously persisted queue mapping entries. */ + @Query("DELETE FROM ${QueueMappingItem.TABLE_NAME}") suspend fun nukeMapping() + + /** + * Insert new heap entries into the database. + * @param heap The list of wrapped [QueueHeapItem]s to insert. + */ + @Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertHeap(heap: List) + + /** + * Insert new mapping entries into the database. + * @param mapping The list of wrapped [QueueMappingItem] to insert. + */ + @Insert(onConflict = OnConflictStrategy.ABORT) + suspend fun insertMapping(mapping: List) +} + +/** + * A raw representation of the persisted playback state. + * @author Alexander Capehart + */ +@Entity(tableName = PlaybackState.TABLE_NAME) +data class PlaybackState( + @PrimaryKey val id: Int, + val index: Int, + val positionMs: Long, + val repeatMode: RepeatMode, + val songUid: Music.UID, + val parentUid: Music.UID? +) { + companion object { + const val TABLE_NAME = "playback_state" + } +} + +/** + * A raw representation of the an individual item in the persisted queue's heap. + * @author Alexander Capehart + */ +@Entity(tableName = QueueHeapItem.TABLE_NAME) +data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) { + companion object { + const val TABLE_NAME = "queue_heap" + } +} + +/** + * A raw representation of the heap indices at a particular position in the persisted queue. + * @author Alexander Capehart + */ +@Entity(tableName = QueueMappingItem.TABLE_NAME) +data class QueueMappingItem( + @PrimaryKey val id: Int, + val orderedIndex: Int, + val shuffledIndex: Int +) { + companion object { + const val TABLE_NAME = "queue_mapping" + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt new file mode 100644 index 000000000..018ebcb32 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceRepository.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback.persist + +import android.content.Context +import org.oxycblt.auxio.music.MusicParent +import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.playback.queue.Queue +import org.oxycblt.auxio.playback.state.RepeatMode +import org.oxycblt.auxio.util.logD + +/** + * Manages the persisted playback state in a structured manner. + * @author Alexander Capehart (OxygenCobalt) + */ +interface PersistenceRepository { + /** + * Read the previously persisted [SavedState]. + * @param library The [Library] required to de-serialize the [SavedState]. + */ + suspend fun readState(library: Library): SavedState? + + /** + * Persist a new [SavedState]. + * @param state The [SavedState] to persist. + */ + suspend fun saveState(state: SavedState?) + + /** + * A condensed representation of the playback state that can be persisted. + * @param parent The [MusicParent] item currently being played from. + * @param queueState The [Queue.SavedState] + * @param positionMs The current position in the currently played song, in ms + * @param repeatMode The current [RepeatMode]. + */ + data class SavedState( + val parent: MusicParent?, + val queueState: Queue.SavedState, + val positionMs: Long, + val repeatMode: RepeatMode, + ) + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): PersistenceRepository = RealPersistenceRepository(context) + } +} + +private class RealPersistenceRepository(private val context: Context) : PersistenceRepository { + private val database: PersistenceDatabase by lazy { PersistenceDatabase.getInstance(context) } + private val playbackStateDao: PlaybackStateDao by lazy { database.playbackStateDao() } + private val queueDao: QueueDao by lazy { database.queueDao() } + + override suspend fun readState(library: Library): PersistenceRepository.SavedState? { + val playbackState = playbackStateDao.getState() ?: return null + val heap = queueDao.getHeap() + val mapping = queueDao.getMapping() + + val orderedMapping = mutableListOf() + val shuffledMapping = mutableListOf() + for (entry in mapping) { + orderedMapping.add(entry.orderedIndex) + shuffledMapping.add(entry.shuffledIndex) + } + + val parent = playbackState.parentUid?.let { library.find(it) } + + return PersistenceRepository.SavedState( + parent = parent, + queueState = + Queue.SavedState( + heap.map { library.find(it.uid) }, + orderedMapping, + shuffledMapping, + playbackState.index, + playbackState.songUid), + positionMs = playbackState.positionMs, + repeatMode = playbackState.repeatMode) + } + + override suspend fun saveState(state: PersistenceRepository.SavedState?) { + // Only bother saving a state if a song is actively playing from one. + // This is not the case with a null state. + playbackStateDao.nukeState() + queueDao.nukeHeap() + queueDao.nukeMapping() + logD("Cleared state") + if (state != null) { + // Transform saved state into raw state, which can then be written to the database. + val playbackState = + PlaybackState( + id = 0, + index = state.queueState.index, + positionMs = state.positionMs, + repeatMode = state.repeatMode, + songUid = state.queueState.songUid, + parentUid = state.parent?.uid) + playbackStateDao.insertState(playbackState) + + // Convert the remaining queue information do their database-specific counterparts. + val heap = + state.queueState.heap.mapIndexed { i, song -> + QueueHeapItem(i, requireNotNull(song).uid) + } + queueDao.insertHeap(heap) + val mapping = + state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed { + i, + pair -> + QueueMappingItem(i, pair.first, pair.second) + } + queueDao.insertMapping(mapping) + logD("Wrote state") + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt rename to app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt index 6638901fd..2419203b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/Queue.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/Queue.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.playback.state +package org.oxycblt.auxio.playback.queue import kotlin.random.Random import kotlin.random.nextInt diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 71bbe02e9..947a9490f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -24,7 +24,6 @@ import org.oxycblt.auxio.list.adapter.BasicListInstructions import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.playback.state.Queue /** * A [ViewModel] that manages the current queue state and allows navigation through the queue. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt deleted file mode 100644 index 717750dbf..000000000 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.playback.state - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.provider.BaseColumns -import androidx.core.database.getIntOrNull -import androidx.core.database.sqlite.transaction -import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.music.library.Library -import org.oxycblt.auxio.util.* - -/** - * A [SQLiteDatabase] that persists the current playback state for future app lifecycles. - * @author Alexander Capehart (OxygenCobalt) - */ -class PlaybackStateDatabase private constructor(context: Context) : - SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { - - override fun onCreate(db: SQLiteDatabase) { - // Here, we have to split the database into two tables. One contains the queue with - // an indefinite amount of items, and the other contains only one entry consisting - // of the non-queue parts of the state, such as the playback position. - db.createTable(TABLE_STATE) { - append("${BaseColumns._ID} INTEGER PRIMARY KEY,") - append("${PlaybackStateColumns.INDEX} INTEGER NOT NULL,") - append("${PlaybackStateColumns.POSITION} LONG NOT NULL,") - append("${PlaybackStateColumns.REPEAT_MODE} INTEGER NOT NULL,") - append("${PlaybackStateColumns.SONG_UID} STRING,") - append("${PlaybackStateColumns.PARENT_UID} STRING") - } - - db.createTable(TABLE_QUEUE_HEAP) { - append("${BaseColumns._ID} INTEGER PRIMARY KEY,") - append("${QueueHeapColumns.SONG_UID} STRING NOT NULL") - } - - db.createTable(TABLE_QUEUE_MAPPINGS) { - append("${BaseColumns._ID} INTEGER PRIMARY KEY,") - append("${QueueMappingColumns.ORDERED_INDEX} INT NOT NULL,") - append("${QueueMappingColumns.SHUFFLED_INDEX} INT") - } - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) - - private fun nuke(db: SQLiteDatabase) { - logD("Nuking database") - db.apply { - execSQL("DROP TABLE IF EXISTS $TABLE_STATE") - execSQL("DROP TABLE IF EXISTS $TABLE_QUEUE_HEAP") - execSQL("DROP TABLE IF EXISTS $TABLE_QUEUE_MAPPINGS") - onCreate(this) - } - } - - // --- INTERFACE FUNCTIONS --- - - /** - * Read a persisted [SavedState] from the database. - * @param library [Library] required to restore [SavedState]. - * @return A persisted [SavedState], or null if one could not be found. - */ - fun read(library: Library): SavedState? { - requireBackgroundThread() - // Read the saved state and queue. If the state is non-null, that must imply an - // existent, albeit possibly empty, queue. - val rawState = readRawPlaybackState() ?: return null - val rawQueueState = readRawQueueState(library) - // Restore parent item from the music library. If this fails, then the playback mode - // reverts to "All Songs", which is considered okay. - val parent = rawState.parentUid?.let { library.find(it) } - return SavedState( - parent = parent, - queueState = - Queue.SavedState( - heap = rawQueueState.heap, - orderedMapping = rawQueueState.orderedMapping, - shuffledMapping = rawQueueState.shuffledMapping, - index = rawState.index, - songUid = rawState.songUid), - positionMs = rawState.positionMs, - repeatMode = rawState.repeatMode) - } - - private fun readRawPlaybackState() = - readableDatabase.queryAll(TABLE_STATE) { cursor -> - if (!cursor.moveToFirst()) { - // Empty, nothing to do. - return@queryAll null - } - - val indexIndex = cursor.getColumnIndexOrThrow(PlaybackStateColumns.INDEX) - val posIndex = cursor.getColumnIndexOrThrow(PlaybackStateColumns.POSITION) - val repeatModeIndex = cursor.getColumnIndexOrThrow(PlaybackStateColumns.REPEAT_MODE) - val songUidIndex = cursor.getColumnIndexOrThrow(PlaybackStateColumns.SONG_UID) - val parentUidIndex = cursor.getColumnIndexOrThrow(PlaybackStateColumns.PARENT_UID) - RawPlaybackState( - index = cursor.getInt(indexIndex), - positionMs = cursor.getLong(posIndex), - repeatMode = RepeatMode.fromIntCode(cursor.getInt(repeatModeIndex)) - ?: RepeatMode.NONE, - songUid = Music.UID.fromString(cursor.getString(songUidIndex)) - ?: return@queryAll null, - parentUid = cursor.getString(parentUidIndex)?.let(Music.UID::fromString)) - } - - private fun readRawQueueState(library: Library): RawQueueState { - val heap = mutableListOf() - readableDatabase.queryAll(TABLE_QUEUE_HEAP) { cursor -> - if (cursor.count == 0) { - // Empty, nothing to do. - return@queryAll - } - - val songIndex = cursor.getColumnIndexOrThrow(QueueHeapColumns.SONG_UID) - while (cursor.moveToNext()) { - heap.add(Music.UID.fromString(cursor.getString(songIndex))?.let(library::find)) - } - } - logD("Successfully read queue of ${heap.size} songs") - - val orderedMapping = mutableListOf() - val shuffledMapping = mutableListOf() - readableDatabase.queryAll(TABLE_QUEUE_MAPPINGS) { cursor -> - if (cursor.count == 0) { - // Empty, nothing to do. - return@queryAll - } - - val orderedIndex = cursor.getColumnIndexOrThrow(QueueMappingColumns.ORDERED_INDEX) - val shuffledIndex = cursor.getColumnIndexOrThrow(QueueMappingColumns.SHUFFLED_INDEX) - while (cursor.moveToNext()) { - orderedMapping.add(cursor.getInt(orderedIndex)) - cursor.getIntOrNull(shuffledIndex)?.let(shuffledMapping::add) - } - } - - return RawQueueState(heap, orderedMapping.filterNotNull(), shuffledMapping.filterNotNull()) - } - - /** - * Clear the previous [SavedState] and write a new one. - * @param state The new [SavedState] to write, or null to clear the database entirely. - */ - fun write(state: SavedState?) { - requireBackgroundThread() - // Only bother saving a state if a song is actively playing from one. - // This is not the case with a null state. - if (state != null) { - // Transform saved state into raw state, which can then be written to the database. - val rawPlaybackState = - RawPlaybackState( - index = state.queueState.index, - positionMs = state.positionMs, - repeatMode = state.repeatMode, - songUid = state.queueState.songUid, - parentUid = state.parent?.uid) - writeRawPlaybackState(rawPlaybackState) - val rawQueueState = - RawQueueState( - heap = state.queueState.heap, - orderedMapping = state.queueState.orderedMapping, - shuffledMapping = state.queueState.shuffledMapping) - writeRawQueueState(rawQueueState) - logD("Wrote state") - } else { - writeRawPlaybackState(null) - writeRawQueueState(null) - logD("Cleared state") - } - } - - private fun writeRawPlaybackState(rawPlaybackState: RawPlaybackState?) { - writableDatabase.transaction { - delete(TABLE_STATE, null, null) - - if (rawPlaybackState != null) { - val stateData = - ContentValues(7).apply { - put(BaseColumns._ID, 0) - put(PlaybackStateColumns.SONG_UID, rawPlaybackState.songUid.toString()) - put(PlaybackStateColumns.POSITION, rawPlaybackState.positionMs) - put(PlaybackStateColumns.PARENT_UID, rawPlaybackState.parentUid?.toString()) - put(PlaybackStateColumns.INDEX, rawPlaybackState.index) - put(PlaybackStateColumns.REPEAT_MODE, rawPlaybackState.repeatMode.intCode) - } - - insert(TABLE_STATE, null, stateData) - } - } - } - - private fun writeRawQueueState(rawQueueState: RawQueueState?) { - writableDatabase.writeList(rawQueueState?.heap ?: listOf(), TABLE_QUEUE_HEAP) { i, song -> - ContentValues(2).apply { - put(BaseColumns._ID, i) - put(QueueHeapColumns.SONG_UID, unlikelyToBeNull(song).uid.toString()) - } - } - - val combinedMapping = - rawQueueState?.run { - if (shuffledMapping.isNotEmpty()) { - orderedMapping.zip(shuffledMapping) - } else { - orderedMapping.map { Pair(it, null) } - } - } - - writableDatabase.writeList(combinedMapping ?: listOf(), TABLE_QUEUE_MAPPINGS) { i, pair -> - ContentValues(3).apply { - put(BaseColumns._ID, i) - put(QueueMappingColumns.ORDERED_INDEX, pair.first) - put(QueueMappingColumns.SHUFFLED_INDEX, pair.second) - } - } - } - - /** - * A condensed representation of the playback state that can be persisted. - * @param parent The [MusicParent] item currently being played from. - * @param queueState The [Queue.SavedState] - * @param positionMs The current position in the currently played song, in ms - * @param repeatMode The current [RepeatMode]. - */ - data class SavedState( - val parent: MusicParent?, - val queueState: Queue.SavedState, - val positionMs: Long, - val repeatMode: RepeatMode, - ) - - /** A lower-level form of [SavedState] that contains individual field-based information. */ - private data class RawPlaybackState( - /** @see Queue.SavedState.index */ - val index: Int, - /** @see SavedState.positionMs */ - val positionMs: Long, - /** @see SavedState.repeatMode */ - val repeatMode: RepeatMode, - /** - * The [Music.UID] of the [Song] that was originally in the queue at [index]. This can be - * used to restore the currently playing item in the queue if the index mapping changed. - */ - val songUid: Music.UID, - /** @see SavedState.parent */ - val parentUid: Music.UID? - ) - - /** A lower-level form of [Queue.SavedState] that contains heap and mapping information. */ - private data class RawQueueState( - /** @see Queue.SavedState.heap */ - val heap: List, - /** @see Queue.SavedState.orderedMapping */ - val orderedMapping: List, - /** @see Queue.SavedState.shuffledMapping */ - val shuffledMapping: List - ) - - /** Defines the columns used in the playback state table. */ - private object PlaybackStateColumns { - /** @see RawPlaybackState.index */ - const val INDEX = "queue_index" - /** @see RawPlaybackState.positionMs */ - const val POSITION = "position" - /** @see RawPlaybackState.repeatMode */ - const val REPEAT_MODE = "repeat_mode" - /** @see RawPlaybackState.songUid */ - const val SONG_UID = "song_uid" - /** @see RawPlaybackState.parentUid */ - const val PARENT_UID = "parent" - } - - /** Defines the columns used in the queue heap table. */ - private object QueueHeapColumns { - /** @see Music.UID */ - const val SONG_UID = "song_uid" - } - - /** Defines the columns used in the queue mapping table. */ - private object QueueMappingColumns { - /** @see Queue.SavedState.orderedMapping */ - const val ORDERED_INDEX = "ordered_index" - /** @see Queue.SavedState.shuffledMapping */ - const val SHUFFLED_INDEX = "shuffled_index" - } - - companion object { - private const val DB_NAME = "auxio_playback_state.db" - private const val DB_VERSION = 9 - private const val TABLE_STATE = "playback_state" - private const val TABLE_QUEUE_HEAP = "queue_heap" - private const val TABLE_QUEUE_MAPPINGS = "queue_mapping" - - @Volatile private var INSTANCE: PlaybackStateDatabase? = null - - /** - * Get a singleton instance. - * @return The (possibly newly-created) singleton instance. - */ - fun getInstance(context: Context): PlaybackStateDatabase { - val currentInstance = INSTANCE - - if (currentInstance != null) { - return currentInstance - } - - synchronized(this) { - val newInstance = PlaybackStateDatabase(context.applicationContext) - INSTANCE = newInstance - return newInstance - } - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 734966f89..9e33877a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -23,6 +23,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.library.Library +import org.oxycblt.auxio.playback.persist.PersistenceRepository +import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -387,11 +389,11 @@ class PlaybackStateManager private constructor() { /** * Restore the previously saved state (if any) and apply it to the playback state. - * @param database The [PlaybackStateDatabase] to load from. + * @param repository The [PersistenceRepository] to load from. * @param force Whether to do a restore regardless of any prior playback state. * @return If the state was restored, false otherwise. */ - suspend fun restoreState(database: PlaybackStateDatabase, force: Boolean): Boolean { + suspend fun restoreState(repository: PersistenceRepository, force: Boolean): Boolean { if (isInitialized && !force) { // Already initialized and not forcing a restore, nothing to do. return false @@ -401,7 +403,7 @@ class PlaybackStateManager private constructor() { val internalPlayer = internalPlayer ?: return false val state = try { - withContext(Dispatchers.IO) { database.read(library) } + withContext(Dispatchers.IO) { repository.readState(library) } } catch (e: Exception) { logE("Unable to restore playback state.") logE(e.stackTraceToString()) @@ -432,16 +434,16 @@ class PlaybackStateManager private constructor() { /** * Save the current state. - * @param database The [PlaybackStateDatabase] to save the state to. + * @param database The [PersistenceRepository] to save the state to. * @return If state was saved, false otherwise. */ - suspend fun saveState(database: PlaybackStateDatabase): Boolean { + suspend fun saveState(database: PersistenceRepository): Boolean { logD("Saving state to DB") // Create the saved state from the current playback state. val state = synchronized(this) { queue.toSavedState()?.let { - PlaybackStateDatabase.SavedState( + PersistenceRepository.SavedState( parent = parent, queueState = it, positionMs = playerState.calculateElapsedPositionMs(), @@ -449,7 +451,7 @@ class PlaybackStateManager private constructor() { } } return try { - withContext(Dispatchers.IO) { database.write(state) } + withContext(Dispatchers.IO) { database.saveState(state) } true } catch (e: Exception) { logE("Unable to save playback state.") @@ -460,13 +462,13 @@ class PlaybackStateManager private constructor() { /** * Clear the current state. - * @param database The [PlaybackStateDatabase] to clear te state from + * @param repository The [PersistenceRepository] to clear the state from * @return If the state was cleared, false otherwise. */ - suspend fun wipeState(database: PlaybackStateDatabase) = + suspend fun wipeState(repository: PersistenceRepository) = try { logD("Wiping state") - withContext(Dispatchers.IO) { database.write(null) } + withContext(Dispatchers.IO) { repository.saveState(null) } true } catch (e: Exception) { logE("Unable to wipe playback state.") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index b6e34615b..22bf52611 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -35,9 +35,9 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 08b820a5e..e063b7c99 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -48,9 +48,9 @@ import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.playback.persist.PersistenceRepository import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.InternalPlayer -import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.service.ForegroundManager @@ -95,6 +95,7 @@ class PlaybackService : private val musicStore = MusicStore.getInstance() private lateinit var musicSettings: MusicSettings private lateinit var playbackSettings: PlaybackSettings + private lateinit var persistenceRepository: PersistenceRepository // State private lateinit var foregroundManager: ForegroundManager @@ -147,6 +148,7 @@ class PlaybackService : // Initialize the core service components musicSettings = MusicSettings.from(this) playbackSettings = PlaybackSettings.from(this) + persistenceRepository = PersistenceRepository.from(this) foregroundManager = ForegroundManager(this) // Initialize any listener-dependent components last as we wouldn't want a listener race // condition to cause us to load music before we were fully initialize. @@ -331,9 +333,7 @@ class PlaybackService : // to save the current state as it's not long until this service (and likely the whole // app) is killed. logD("Saving playback state") - saveScope.launch { - playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService)) - } + saveScope.launch { playbackManager.saveState(persistenceRepository) } } } @@ -348,10 +348,7 @@ class PlaybackService : when (action) { // Restore state -> Start a new restoreState job is InternalPlayer.Action.RestoreState -> { - restoreScope.launch { - playbackManager.restoreState( - PlaybackStateDatabase.getInstance(this@PlaybackService), false) - } + restoreScope.launch { playbackManager.restoreState(persistenceRepository, false) } } // Shuffle all -> Start new playback from all songs is InternalPlayer.Action.ShuffleAll -> { diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index 180165d97..18261b40b 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -28,9 +28,9 @@ import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.extractor.SquareFrameTransform import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.queue.Queue import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getDimenPixels diff --git a/build.gradle b/build.gradle index 5c8ab0dea..4cf2d2bd6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ buildscript { - ext.kotlin_version = '1.7.21' - ext.navigation_version = "2.5.3" + ext { + kotlin_version = '1.7.21' + navigation_version = "2.5.3" + } repositories { google()