diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 0960c32ad..fea17b434 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 4673c2f8b..58a82eb27 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -315,7 +315,6 @@ class HomeFragment : ViewBindingFragment(), 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 diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index 2970305cb..c81c3f3e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/music/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/IndexerService.kt index 3a828a0d2..32df42a61 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/IndexerService.kt @@ -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) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 2c4d47b46..7b4a2d111 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -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? 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 ad7c0da61..75057f042 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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() 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 index 3749812ae..eaf9c7337 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -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) { 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 5fc894730..ae1f4391b 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 @@ -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) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 8809a4104..66ded97f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -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() } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/M3Toolbar.kt b/app/src/main/java/org/oxycblt/auxio/ui/M3Toolbar.kt index ed3028ddf..c7e023206 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/M3Toolbar.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/M3Toolbar.kt @@ -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) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index e9fab0c52..6b5d8293c 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -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"> KEY_LOOP_PAUSE auxio_save_state + auxio_restore_state auxio_reindex auxio_music_dirs auxio_include_dirs diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbe70408b..62013c2d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,7 +53,10 @@ Bit rate Sample rate + State saved + + State restored Shuffle @@ -131,6 +134,8 @@ Content Save playback state Save the current playback state now + Restore playback state + Restore the previously saved playback state (if any) Reload music May wipe playback state Music folders @@ -151,6 +156,8 @@ No Folders This folder is not supported Auxio does not support this window size + + No state could be restored Search your library… @@ -190,11 +197,17 @@ Artist Name + MPEG-1 Audio + MPEG-4 Audio + Ogg Audio + Matroska Audio + Advanced Audio Coding (AAC) + Free Lossless Audio Codec (FLAC) @@ -219,9 +232,13 @@ Disc %d + +%.1f dB + -%.1f dB + %d kbps + %d Hz Loading your music library… (%1$d/%2$d) diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index 091f13d99..c664c75ac 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -161,6 +161,12 @@ app:summary="@string/set_save_desc" app:title="@string/set_save" /> + + 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