settings: add option to force-restore state

Add an option to restore the previous playback state.

This allows me to avoid having to use force stop to restore a previous
state.
This commit is contained in:
OxygenCobalt 2022-07-04 14:22:49 -06:00
parent a15bc79cc9
commit 1730a73eac
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
15 changed files with 84 additions and 23 deletions

View file

@ -38,13 +38,14 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/** /**
* The single [AppCompatActivity] for Auxio. * The single [AppCompatActivity] for Auxio.
* *
* TODO: Add error screens. This likely has to be an external activity, so it is blocked by * TODO: Add error screens.
* eliminating exitProcess from the app.
* *
* TODO: Custom language support * TODO: Custom language support
* *
* TODO: Add multi-select * TODO: Add multi-select
* *
* TODO: Bug test runtime rescanning
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {

View file

@ -315,7 +315,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
} }
private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) { private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
updateFab()
binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE binding.homeIndexingAction.visibility = View.INVISIBLE

View file

@ -83,6 +83,7 @@ class BitmapProvider(private val context: Context) {
* Release this instance, canceling all image load jobs. This should be ran when the object is * Release this instance, canceling all image load jobs. This should be ran when the object is
* no longer used. * no longer used.
*/ */
@Synchronized
fun release() { fun release() {
currentRequest?.run { disposable.dispose() } currentRequest?.run { disposable.dispose() }
currentRequest = null currentRequest = null

View file

@ -119,10 +119,15 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback {
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
// PlaybackStateManager needs to be updated. We would do this in the // PlaybackStateManager needs to be updated. We would do this in the
// playback module, but this service could is the only component // playback module, but this service could be the only component capable
// capable of doing long-running background work as it stands. // of doing this at a particular point. Note that while it's certain
playbackManager.sanitize( // that PlaybackStateManager is initialized by now, it's best to be safe
PlaybackStateDatabase.getInstance(this@IndexerService), newLibrary) // and check first.
if (playbackManager.isInitialized) {
playbackManager.sanitize(
PlaybackStateDatabase.getInstance(this@IndexerService),
newLibrary)
}
} }
musicStore.updateLibrary(newLibrary) musicStore.updateLibrary(newLibrary)

View file

@ -30,6 +30,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/** [Item] variant that represents a music item. */ /** [Item] variant that represents a music item. */
sealed class Music : Item() { sealed class Music : Item() {
// TODO: Split ID into an ID derived from all fields and a persistent ID derived from stable fields
/** The raw name of this item. Null if unknown. */ /** The raw name of this item. Null if unknown. */
abstract val rawName: String? abstract val rawName: String?

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.app.Application import android.app.Application
import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -276,13 +275,22 @@ class PlaybackViewModel(application: Application) :
// --- SAVE/RESTORE FUNCTIONS --- // --- SAVE/RESTORE FUNCTIONS ---
/** Force save the current [PlaybackStateManager] state to the database. */ /** Force save the current [PlaybackStateManager] state to the database. */
fun savePlaybackState(context: Context, onDone: () -> Unit) { fun savePlaybackState(onDone: () -> Unit) {
viewModelScope.launch { viewModelScope.launch {
playbackManager.saveState(PlaybackStateDatabase.getInstance(context)) playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
onDone() onDone()
} }
} }
/** Force restore the last [PlaybackStateManager] saved state */
fun restorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val restored =
playbackManager.restoreState(PlaybackStateDatabase.getInstance(application))
onDone(restored)
}
}
/** An action delayed until the complete load of the music library. */ /** An action delayed until the complete load of the music library. */
sealed class DelayedAction { sealed class DelayedAction {
object RestoreState : DelayedAction() object RestoreState : DelayedAction()

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.requireBackgroundThread
* But that would needlessly bloat my app and has crippling bugs. * But that would needlessly bloat my app and has crippling bugs.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackStateDatabase(context: Context) : class PlaybackStateDatabase private constructor(context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**
* Master class (and possible god object) for the playback state. * Master class (and possible god object) for the playback state.
@ -357,17 +358,20 @@ class PlaybackStateManager private constructor() {
// --- PERSISTENCE FUNCTIONS --- // --- PERSISTENCE FUNCTIONS ---
/** Restore the state from the [database] */ /** Restore the state from the [database]. Returns if a state was restored. */
suspend fun restoreState(database: PlaybackStateDatabase) { suspend fun restoreState(database: PlaybackStateDatabase): Boolean {
val library = musicStore.library ?: return val library = musicStore.library ?: return false
val state = withContext(Dispatchers.IO) { database.read(library) } val state = withContext(Dispatchers.IO) { database.read(library) }
synchronized(this) { synchronized(this) {
if (state != null) { val exists = state != null
applyStateImpl(state) if (exists) {
applyStateImpl(unlikelyToBeNull(state))
} }
isInitialized = true isInitialized = true
return exists
} }
} }
@ -383,6 +387,8 @@ class PlaybackStateManager private constructor() {
suspend fun sanitize(database: PlaybackStateDatabase, newLibrary: MusicStore.Library) { suspend fun sanitize(database: PlaybackStateDatabase, newLibrary: MusicStore.Library) {
// Since we need to sanitize the state and re-save it for consistency, take the // Since we need to sanitize the state and re-save it for consistency, take the
// easy way out and just write a new state and restore from it. Don't really care. // easy way out and just write a new state and restore from it. Don't really care.
// FIXME: This hack actually creates bugs if a user were to save the state at just
// the right time, replace it with something that operates at runtime
logD("Sanitizing state") logD("Sanitizing state")
val state = synchronized(this) { makeStateImpl() } val state = synchronized(this) { makeStateImpl() }
@ -394,7 +400,7 @@ class PlaybackStateManager private constructor() {
synchronized(this) { synchronized(this) {
if (sanitizedState != null) { if (sanitizedState != null) {
applyStateImpl(state) applyStateImpl(sanitizedState)
} }
} }
} }

View file

@ -50,9 +50,9 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat]. * The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Add option to restore the previous state
*
* TODO: Add option to not restore state * TODO: Add option to not restore state
*
* TODO: Disable playback state options when music is loading
*/ */
@Suppress("UNUSED") @Suppress("UNUSED")
class SettingsListFragment : PreferenceFragmentCompat() { class SettingsListFragment : PreferenceFragmentCompat() {
@ -120,10 +120,16 @@ class SettingsListFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) { when (preference.key) {
getString(R.string.set_key_save_state) -> { getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState(requireContext()) { playbackModel.savePlaybackState { context?.showToast(R.string.lbl_state_saved) }
context?.showToast(R.string.lbl_state_saved)
}
} }
getString(R.string.set_key_restore_state) ->
playbackModel.restorePlaybackState { restored ->
if (restored) {
context?.showToast(R.string.lbl_state_restored)
} else {
context?.showToast(R.string.err_did_not_restore)
}
}
getString(R.string.set_key_reindex) -> { getString(R.string.set_key_reindex) -> {
indexerModel.reindex() indexerModel.reindex()
} }

View file

@ -25,7 +25,10 @@ import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDimenSizeSafe
/** [MaterialToolbar] that automatically fixes padding in order to align with the M3 specs. */ /**
* [MaterialToolbar] that automatically fixes padding in order to align with the M3 specs.
* @author OxygenCobalt
*/
class M3Toolbar : MaterialToolbar { class M3Toolbar : MaterialToolbar {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)

View file

@ -50,6 +50,7 @@
android:id="@+id/home_indexing_container" android:id="@+id/home_indexing_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="invisible"
android:layout_margin="@dimen/spacing_medium"> android:layout_margin="@dimen/spacing_medium">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout

View file

@ -24,6 +24,7 @@
<string name="set_key_repeat_pause" translatable="false">KEY_LOOP_PAUSE</string> <string name="set_key_repeat_pause" translatable="false">KEY_LOOP_PAUSE</string>
<string name="set_key_save_state" translatable="false">auxio_save_state</string> <string name="set_key_save_state" translatable="false">auxio_save_state</string>
<string name="set_key_restore_state" translatable="false">auxio_restore_state</string>
<string name="set_key_reindex" translatable="false">auxio_reindex</string> <string name="set_key_reindex" translatable="false">auxio_reindex</string>
<string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string> <string name="set_key_music_dirs" translatable="false">auxio_music_dirs</string>
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string> <string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>

View file

@ -53,7 +53,10 @@
<string name="lbl_bitrate">Bit rate</string> <string name="lbl_bitrate">Bit rate</string>
<string name="lbl_sample_rate">Sample rate</string> <string name="lbl_sample_rate">Sample rate</string>
<!-- Referring to playback state -->
<string name="lbl_state_saved">State saved</string> <string name="lbl_state_saved">State saved</string>
<!-- Referring to playback state -->
<string name="lbl_state_restored">State restored</string>
<!-- Limit to 10 characters --> <!-- Limit to 10 characters -->
<string name="lbl_shuffle_shortcut_short">Shuffle</string> <string name="lbl_shuffle_shortcut_short">Shuffle</string>
@ -131,6 +134,8 @@
<string name="set_content">Content</string> <string name="set_content">Content</string>
<string name="set_save">Save playback state</string> <string name="set_save">Save playback state</string>
<string name="set_save_desc">Save the current playback state now</string> <string name="set_save_desc">Save the current playback state now</string>
<string name="set_restore">Restore playback state</string>
<string name="set_restore_desc">Restore the previously saved playback state (if any)</string>
<string name="set_reindex">Reload music</string> <string name="set_reindex">Reload music</string>
<string name="set_reindex_desc">May wipe playback state</string> <string name="set_reindex_desc">May wipe playback state</string>
<string name="set_dirs">Music folders</string> <string name="set_dirs">Music folders</string>
@ -151,6 +156,8 @@
<string name="err_no_dirs">No Folders</string> <string name="err_no_dirs">No Folders</string>
<string name="err_bad_dir">This folder is not supported</string> <string name="err_bad_dir">This folder is not supported</string>
<string name="err_too_small">Auxio does not support this window size</string> <string name="err_too_small">Auxio does not support this window size</string>
<!-- Referring to playback state -->
<string name="err_did_not_restore">No state could be restored</string>
<!-- Hint Namespace | EditText Hints --> <!-- Hint Namespace | EditText Hints -->
<string name="hint_search_library">Search your library…</string> <string name="hint_search_library">Search your library…</string>
@ -190,11 +197,17 @@
<string name="def_widget_artist">Artist Name</string> <string name="def_widget_artist">Artist Name</string>
<!-- Codec Namespace | Format names --> <!-- Codec Namespace | Format names -->
<!-- "Audio" should be translated -->
<string name="cdc_mp3">MPEG-1 Audio</string> <string name="cdc_mp3">MPEG-1 Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_mp4">MPEG-4 Audio</string> <string name="cdc_mp4">MPEG-4 Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_ogg">Ogg Audio</string> <string name="cdc_ogg">Ogg Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_mka">Matroska Audio</string> <string name="cdc_mka">Matroska Audio</string>
<!-- "Advanced Audio Coding" can optionally be translated -->
<string name="cdc_aac">Advanced Audio Coding (AAC)</string> <string name="cdc_aac">Advanced Audio Coding (AAC)</string>
<!-- "Free Lossless Audio Codec" can optionally be translated -->
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string> <string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
<!-- Color Label namespace | Accent names --> <!-- Color Label namespace | Accent names -->
@ -219,9 +232,13 @@
<!-- Format Namespace | Value formatting/plurals --> <!-- Format Namespace | Value formatting/plurals -->
<string name="fmt_disc_no">Disc %d</string> <string name="fmt_disc_no">Disc %d</string>
<!-- Use your native country's abbreviation for decibel units. -->
<string name="fmt_db_pos">+%.1f dB</string> <string name="fmt_db_pos">+%.1f dB</string>
<!-- Use your native country's abbreviation for decibel units. -->
<string name="fmt_db_neg">-%.1f dB</string> <string name="fmt_db_neg">-%.1f dB</string>
<!-- Use your native country's abbreviation for bitrate units. -->
<string name="fmt_bitrate">%d kbps</string> <string name="fmt_bitrate">%d kbps</string>
<!-- Use your native country's abbreviation for hertz units. -->
<string name="fmt_sample_rate">%d Hz</string> <string name="fmt_sample_rate">%d Hz</string>
<string name="fmt_indexing">Loading your music library… (%1$d/%2$d)</string> <string name="fmt_indexing">Loading your music library… (%1$d/%2$d)</string>

View file

@ -161,6 +161,12 @@
app:summary="@string/set_save_desc" app:summary="@string/set_save_desc"
app:title="@string/set_save" /> app:title="@string/set_save" />
<Preference
app:iconSpaceReserved="false"
app:key="@string/set_key_restore_state"
app:summary="@string/set_restore_desc"
app:title="@string/set_restore" />
<Preference <Preference
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:key="@string/set_key_reindex" app:key="@string/set_key_reindex"

View file

@ -43,6 +43,11 @@ This is expected since reading from the audio database takes awhile, especially
This is a current limitation with the music loader. To remedy this, go to Settings -> Reload music whenever new songs are added. This is a current limitation with the music loader. To remedy this, go to Settings -> Reload music whenever new songs are added.
I hope to make the app rescan music on the fly eventually. I hope to make the app rescan music on the fly eventually.
#### Why does playback pause whenever music is reloaded?
Whenever the music library signifigantly changes, updating the player's data while it is still playing may result in
unwanted bugs or unexpected music playing. To safeguard against this, Auxio will pause whenever it reloads a new
music library.
#### There should be one artist, but instead I get a bunch of "Artist & Collaborator" artists! #### There should be one artist, but instead I get a bunch of "Artist & Collaborator" artists!
This likely means your tags are wrong. By default, Auxio will use the "album artist" tag for This likely means your tags are wrong. By default, Auxio will use the "album artist" tag for
grouping if present, falling back to the "artist" tag otherwise. If your music does not have grouping if present, falling back to the "artist" tag otherwise. If your music does not have