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.
*
* TODO: Add error screens. This likely has to be an external activity, so it is blocked by
* eliminating exitProcess from the app.
* TODO: Add error screens.
*
* TODO: Custom language support
*
* TODO: Add multi-select
*
* TODO: Bug test runtime rescanning
*
* @author OxygenCobalt
*/
class MainActivity : AppCompatActivity() {

View file

@ -315,7 +315,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
updateFab()
binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE
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
* no longer used.
*/
@Synchronized
fun release() {
currentRequest?.run { disposable.dispose() }
currentRequest = null

View file

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

View file

@ -30,6 +30,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/** [Item] variant that represents a 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. */
abstract val rawName: String?

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.playback
import android.app.Application
import android.content.Context
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
@ -276,13 +275,22 @@ class PlaybackViewModel(application: Application) :
// --- SAVE/RESTORE FUNCTIONS ---
/** Force save the current [PlaybackStateManager] state to the database. */
fun savePlaybackState(context: Context, onDone: () -> Unit) {
fun savePlaybackState(onDone: () -> Unit) {
viewModelScope.launch {
playbackManager.saveState(PlaybackStateDatabase.getInstance(context))
playbackManager.saveState(PlaybackStateDatabase.getInstance(application))
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. */
sealed class 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.
* @author OxygenCobalt
*/
class PlaybackStateDatabase(context: Context) :
class PlaybackStateDatabase private constructor(context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
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.util.logD
import org.oxycblt.auxio.util.logW
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Master class (and possible god object) for the playback state.
@ -357,17 +358,20 @@ class PlaybackStateManager private constructor() {
// --- PERSISTENCE FUNCTIONS ---
/** Restore the state from the [database] */
suspend fun restoreState(database: PlaybackStateDatabase) {
val library = musicStore.library ?: return
/** Restore the state from the [database]. Returns if a state was restored. */
suspend fun restoreState(database: PlaybackStateDatabase): Boolean {
val library = musicStore.library ?: return false
val state = withContext(Dispatchers.IO) { database.read(library) }
synchronized(this) {
if (state != null) {
applyStateImpl(state)
val exists = state != null
if (exists) {
applyStateImpl(unlikelyToBeNull(state))
}
isInitialized = true
return exists
}
}
@ -383,6 +387,8 @@ class PlaybackStateManager private constructor() {
suspend fun sanitize(database: PlaybackStateDatabase, newLibrary: MusicStore.Library) {
// 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.
// 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")
val state = synchronized(this) { makeStateImpl() }
@ -394,7 +400,7 @@ class PlaybackStateManager private constructor() {
synchronized(this) {
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].
* @author OxygenCobalt
*
* TODO: Add option to restore the previous state
*
* TODO: Add option to not restore state
*
* TODO: Disable playback state options when music is loading
*/
@Suppress("UNUSED")
class SettingsListFragment : PreferenceFragmentCompat() {
@ -120,10 +120,16 @@ class SettingsListFragment : PreferenceFragmentCompat() {
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState(requireContext()) {
context?.showToast(R.string.lbl_state_saved)
}
playbackModel.savePlaybackState { 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) -> {
indexerModel.reindex()
}

View file

@ -25,7 +25,10 @@ import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R
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 {
constructor(context: Context) : super(context)

View file

@ -50,6 +50,7 @@
android:id="@+id/home_indexing_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="invisible"
android:layout_margin="@dimen/spacing_medium">
<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_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_music_dirs" translatable="false">auxio_music_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_sample_rate">Sample rate</string>
<!-- Referring to playback state -->
<string name="lbl_state_saved">State saved</string>
<!-- Referring to playback state -->
<string name="lbl_state_restored">State restored</string>
<!-- Limit to 10 characters -->
<string name="lbl_shuffle_shortcut_short">Shuffle</string>
@ -131,6 +134,8 @@
<string name="set_content">Content</string>
<string name="set_save">Save playback state</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_desc">May wipe playback state</string>
<string name="set_dirs">Music folders</string>
@ -151,6 +156,8 @@
<string name="err_no_dirs">No Folders</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>
<!-- Referring to playback state -->
<string name="err_did_not_restore">No state could be restored</string>
<!-- Hint Namespace | EditText Hints -->
<string name="hint_search_library">Search your library…</string>
@ -190,11 +197,17 @@
<string name="def_widget_artist">Artist Name</string>
<!-- Codec Namespace | Format names -->
<!-- "Audio" should be translated -->
<string name="cdc_mp3">MPEG-1 Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_mp4">MPEG-4 Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_ogg">Ogg Audio</string>
<!-- "Audio" should be translated -->
<string name="cdc_mka">Matroska Audio</string>
<!-- "Advanced Audio Coding" can optionally be translated -->
<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>
<!-- Color Label namespace | Accent names -->
@ -219,9 +232,13 @@
<!-- Format Namespace | Value formatting/plurals -->
<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>
<!-- Use your native country's abbreviation for decibel units. -->
<string name="fmt_db_neg">-%.1f dB</string>
<!-- Use your native country's abbreviation for bitrate units. -->
<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_indexing">Loading your music library… (%1$d/%2$d)</string>

View file

@ -161,6 +161,12 @@
app:summary="@string/set_save_desc"
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
app:iconSpaceReserved="false"
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.
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!
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