diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 5b0c54131..a9c3b5e00 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment -import org.oxycblt.auxio.util.logW +import org.oxycblt.auxio.util.logD /** * A wrapper around the home fragment that shows the playback fragment and controls the more @@ -122,32 +122,30 @@ class MainFragment : ViewBindingFragment() { // Error, show the error to the user is MusicStore.Response.Err -> { - logW("Received Error") - - val errorRes = - when (response.kind) { - MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music - MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms - MusicStore.ErrorKind.FAILED -> R.string.err_load_failed + logD("Received Response.Err") + Snackbar.make(binding.root, R.string.err_load_failed, Snackbar.LENGTH_INDEFINITE) + .apply { + setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) } + show() } - - val snackbar = - Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE) - - when (response.kind) { - MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> { - snackbar.setAction(R.string.lbl_retry) { - musicModel.reloadMusic(requireContext()) - } + } + is MusicStore.Response.NoMusic -> { + logD("Received Response.NoMusic") + Snackbar.make(binding.root, R.string.err_no_music, Snackbar.LENGTH_INDEFINITE) + .apply { + setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) } + show() } - MusicStore.ErrorKind.NO_PERMS -> { - snackbar.setAction(R.string.lbl_grant) { + } + is MusicStore.Response.NoPerms -> { + logD("Received Response.NoPerms") + Snackbar.make(binding.root, R.string.err_no_perms, Snackbar.LENGTH_INDEFINITE) + .apply { + setAction(R.string.lbl_grant) { permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } + show() } - } - - snackbar.show() } null -> {} } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 44dae3e3a..5e2c8d344 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -31,6 +31,7 @@ import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.unlikelyToBeNull /** * ViewModel that stores data for the [DetailFragment]s. This includes: @@ -40,6 +41,7 @@ import org.oxycblt.auxio.util.logD * @author OxygenCobalt */ class DetailViewModel : ViewModel() { + private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() private val mCurrentAlbum = MutableLiveData() @@ -87,9 +89,9 @@ class DetailViewModel : ViewModel() { fun setAlbumId(id: Long) { if (mCurrentAlbum.value?.id == id) return - val musicStore = MusicStore.requireInstance() + val library = unlikelyToBeNull(musicStore.library) val album = - requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album id provided " } + requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " } mCurrentAlbum.value = album refreshAlbumData(album) @@ -97,16 +99,18 @@ class DetailViewModel : ViewModel() { fun setArtistId(id: Long) { if (mCurrentArtist.value?.id == id) return - val musicStore = MusicStore.requireInstance() - val artist = requireNotNull(musicStore.artists.find { it.id == id }) {} + val library = unlikelyToBeNull(musicStore.library) + val artist = + requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" } mCurrentArtist.value = artist refreshArtistData(artist) } fun setGenreId(id: Long) { if (mCurrentGenre.value?.id == id) return - val musicStore = MusicStore.requireInstance() - val genre = requireNotNull(musicStore.genres.find { it.id == id }) + val library = unlikelyToBeNull(musicStore.library) + val genre = + requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" } mCurrentGenre.value = genre refreshGenreData(genre) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index a31b81d5a..109f9d6c5 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -20,8 +20,6 @@ package org.oxycblt.auxio.home import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -38,7 +36,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. * @author OxygenCobalt */ -class HomeViewModel : ViewModel(), SettingsManager.Callback { +class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback { + private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() private val mSongs = MutableLiveData(listOf()) @@ -78,15 +77,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { val fastScrolling: LiveData = mFastScrolling init { + musicStore.addCallback(this) settingsManager.addCallback(this) - - viewModelScope.launch { - val musicStore = MusicStore.awaitInstance() - mSongs.value = settingsManager.libSongSort.songs(musicStore.songs) - mAlbums.value = settingsManager.libAlbumSort.albums(musicStore.albums) - mArtists.value = settingsManager.libArtistSort.artists(musicStore.artists) - mGenres.value = settingsManager.libGenreSort.genres(musicStore.genres) - } } /** Update the current tab based off of the new ViewPager position. */ @@ -142,6 +134,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { // --- OVERRIDES --- + override fun onMusicUpdate(response: MusicStore.Response) { + if (response is MusicStore.Response.Ok) { + val library = response.library + mSongs.value = settingsManager.libSongSort.songs(library.songs) + mAlbums.value = settingsManager.libAlbumSort.albums(library.albums) + mArtists.value = settingsManager.libArtistSort.artists(library.artists) + mGenres.value = settingsManager.libGenreSort.genres(library.genres) + } + } + override fun onLibTabsUpdate(libTabs: Array) { tabs = visibleTabs mRecreateTabs.value = true @@ -149,6 +151,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { override fun onCleared() { super.onCleared() + musicStore.addCallback(this) settingsManager.removeCallback(this) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt rename to app/src/main/java/org/oxycblt/auxio/music/Indexer.kt index 33cd1c50b..bf12b5485 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Indexer.kt @@ -89,15 +89,24 @@ import org.oxycblt.auxio.util.logD * * @author OxygenCobalt */ -class MusicLoader { - data class Library( - val genres: List, - val artists: List, - val albums: List, - val songs: List - ) +object Indexer { + /** + * The album_artist MediaStore field has existed since at least API 21, but until API 30 it was + * a proprietary extension for Google Play Music and was not documented. Since this field + * probably works on all versions Auxio supports, we suppress the warning about using a + * possibly-unsupported constant. + */ + @Suppress("InlinedApi") + private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST - fun load(context: Context): Library? { + /** + * Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See + * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. + */ + private val Context.contentResolverSafe: ContentResolver + get() = applicationContext.contentResolver + + fun run(context: Context): MusicStore.Library? { val songs = loadSongs(context) if (songs.isEmpty()) return null @@ -118,16 +127,9 @@ class MusicLoader { } } - return Library(genres, artists, albums, songs) + return MusicStore.Library(genres, artists, albums, songs) } - /** - * Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See - * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. - */ - private val Context.contentResolverSafe: ContentResolver - get() = applicationContext.contentResolver - /** * Does the initial query over the song database, including excluded directory checks. The songs * returned by this function are **not** well-formed. The companion [buildAlbums], @@ -404,15 +406,4 @@ class MusicLoader { return genreSongs.ifEmpty { null } } - - companion object { - /** - * The album_artist MediaStore field has existed since at least API 21, but until API 30 it - * was a proprietary extension for Google Play Music and was not documented. Since this - * field probably works on all versions Auxio supports, we suppress the warning about using - * a possibly-unsupported constant. - */ - @Suppress("InlinedApi") - private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST - } } 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 602c9f802..1856588f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -29,9 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull // --- MUSIC MODELS --- -/** - * [Item] variant that represents a music item. - */ +/** [Item] variant that represents a music item. */ sealed class Music : Item() { /** The raw name of this item. Null if unknown. */ abstract val rawName: String? @@ -116,8 +114,8 @@ data class Song( get() = internalMediaStoreArtistName ?: album.artist.rawName /** - * Resolve the artist name for this song in particular. First uses the artist tag, and - * then falls back to the album artist tag (i.e parent artist name) + * Resolve the artist name for this song in particular. First uses the artist tag, and then + * falls back to the album artist tag (i.e parent artist name) */ fun resolveIndividualArtistName(context: Context) = internalMediaStoreArtistName ?: album.artist.resolveName(context) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 71d0f722a..82ae473a3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -38,76 +38,95 @@ import org.oxycblt.auxio.util.logE * @author OxygenCobalt */ class MusicStore private constructor() { - private var mGenres = listOf() - val genres: List - get() = mGenres + private var response: Response? = null + val library: Library? + get() = + response?.let { currentResponse -> + if (currentResponse is Response.Ok) { + currentResponse.library + } else { + null + } + } - private var mArtists = listOf() - val artists: List - get() = mArtists + private val callbacks = mutableListOf() - private var mAlbums = listOf() - val albums: List - get() = mAlbums + fun addCallback(callback: Callback) { + callbacks.add(callback) + } - private var mSongs = listOf() - val songs: List - get() = mSongs + fun removeCallback(callback: Callback) { + callbacks.remove(callback) + } /** Load/Sort the entire music library. Should always be ran on a coroutine. */ - private fun load(context: Context): Response { + suspend fun index(context: Context): Response { logD("Starting initial music load") + val newResponse = withContext(Dispatchers.IO) { indexImpl(context) }.also { response = it } + for (callback in callbacks) { + callback.onMusicUpdate(newResponse) + } + return newResponse + } + private fun indexImpl(context: Context): Response { val notGranted = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED if (notGranted) { - return Response.Err(ErrorKind.NO_PERMS) + return Response.NoPerms } - try { - val start = System.currentTimeMillis() + val response = + try { + val start = System.currentTimeMillis() + val library = Indexer.run(context) + if (library != null) { + logD( + "Music load completed successfully in ${System.currentTimeMillis() - start}ms") + Response.Ok(library) + } else { + logE("No music found") + Response.NoMusic + } + } catch (e: Exception) { + logE("Music loading failed.") + logE(e.stackTraceToString()) + Response.Err(e) + } - val loader = MusicLoader() - val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC) - - mSongs = library.songs - mAlbums = library.albums - mArtists = library.artists - mGenres = library.genres - - logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms") - } catch (e: Exception) { - logE("Music loading failed.") - logE(e.stackTraceToString()) - return Response.Err(ErrorKind.FAILED) - } - - return Response.Ok(this) + return response } - /** Find a song in a faster manner using an ID for its album as well. */ - fun findSongFast(songId: Long, albumId: Long): Song? { - return albums.find { it.id == albumId }?.songs?.find { it.id == songId } - } - - /** - * Find a song for a [uri], this is similar to [findSongFast], but with some kind of content - * uri. - * @return The corresponding [Song] for this [uri], null if there isn't one. - */ - fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? { - resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor - -> - cursor.moveToFirst() - val fileName = - cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - - return songs.find { it.fileName == fileName } + data class Library( + val genres: List, + val artists: List, + val albums: List, + val songs: List + ) { + /** Find a song in a faster manner using an ID for its album as well. */ + fun findSongFast(songId: Long, albumId: Long): Song? { + return albums.find { it.id == albumId }?.songs?.find { it.id == songId } } - return null + /** + * Find a song for a [uri], this is similar to [findSongFast], but with some kind of content + * uri. + * @return The corresponding [Song] for this [uri], null if there isn't one. + */ + fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? { + resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { + cursor -> + cursor.moveToFirst() + val fileName = + cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + + return songs.find { it.fileName == fileName } + } + + return null + } } /** @@ -117,93 +136,31 @@ class MusicStore private constructor() { * TODO: Add the exception to the "FAILED" ErrorKind */ sealed class Response { - class Ok(val musicStore: MusicStore) : Response() - class Err(val kind: ErrorKind) : Response() + class Ok(val library: Library) : Response() + class Err(throwable: Throwable) : Response() + object NoMusic : Response() + object NoPerms : Response() } - enum class ErrorKind { - NO_PERMS, - NO_MUSIC, - FAILED + interface Callback { + fun onMusicUpdate(response: Response) } companion object { - @Volatile private var RESPONSE: Response? = null + @Volatile private var INSTANCE: MusicStore? = null - /** - * Initialize the loading process for this instance. This must be ran on a background - * thread. If the instance has already been loaded successfully, then it will be returned - * immediately. - */ - suspend fun initInstance(context: Context): Response { - val currentInstance = RESPONSE + fun getInstance(): MusicStore { + val currentInstance = INSTANCE - if (currentInstance is Response.Ok) { + if (currentInstance != null) { return currentInstance } - val response = - withContext(Dispatchers.IO) { - val response = MusicStore().load(context) - synchronized(this) { RESPONSE = response } - response - } - - return response - } - - /** - * Await the successful creation of a [MusicStore] instance. The co-routine calling this - * will block until the successful creation of a [MusicStore], in which it will then be - * returned. - */ - suspend fun awaitInstance() = - withContext(Dispatchers.Default) { - // We have to do a withContext call so we don't block the JVM thread - val musicStore: MusicStore - - while (true) { - val response = RESPONSE - - if (response is Response.Ok) { - musicStore = response.musicStore - break - } - } - - musicStore + synchronized(this) { + val newInstance = MusicStore() + INSTANCE = newInstance + return newInstance } - - /** - * Maybe get a MusicStore instance. This is useful if you are running code while the loading - * process may still be going on. - * - * @return null if the music store instance is still loading or if the loading process has - * encountered an error. An instance is returned otherwise. - */ - fun maybeGetInstance(): MusicStore? { - val currentInstance = RESPONSE - - return if (currentInstance is Response.Ok) { - currentInstance.musicStore - } else { - null - } - } - - /** - * Require a MusicStore instance. This function is dangerous and should only be used if it's - * guaranteed that the caller's code will only be called after the initial loading process. - */ - fun requireInstance(): MusicStore { - return requireNotNull(maybeGetInstance()) { - "Required MusicStore instance was not available" - } - } - - /** Check if this instance has successfully loaded or not. */ - fun loaded(): Boolean { - return maybeGetInstance() != null } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index ae13ee2a2..4b391d2af 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -25,12 +25,18 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.oxycblt.auxio.util.logD -class MusicViewModel : ViewModel() { +class MusicViewModel : ViewModel(), MusicStore.Callback { + private val musicStore = MusicStore.getInstance() + private val mLoaderResponse = MutableLiveData(null) val loaderResponse: LiveData = mLoaderResponse private var isBusy = false + init { + musicStore.addCallback(this) + } + /** * Initiate the loading process. This is done here since HomeFragment will be the first fragment * navigated to and because SnackBars will have the best UX here. @@ -45,7 +51,7 @@ class MusicViewModel : ViewModel() { mLoaderResponse.value = null viewModelScope.launch { - val result = MusicStore.initInstance(context) + val result = musicStore.index(context) mLoaderResponse.value = result isBusy = false } @@ -56,4 +62,13 @@ class MusicViewModel : ViewModel() { mLoaderResponse.value = null loadMusic(context) } + + override fun onMusicUpdate(response: MusicStore.Response) { + mLoaderResponse.value = response + } + + override fun onCleared() { + super.onCleared() + musicStore.removeCallback(this) + } } 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 4b29bc494..744c10952 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -51,6 +51,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS. */ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { + private val musicStore = MusicStore.getInstance() + private val settingsManager = SettingsManager.getInstance() + private val playbackManager = PlaybackStateManager.getInstance() + // Playback private val mSong = MutableLiveData() private val mParent = MutableLiveData() @@ -94,9 +98,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { val playbackMode: LiveData get() = mMode - private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() - init { playbackManager.addCallback(this) @@ -166,7 +167,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { */ fun playWithUri(uri: Uri, context: Context) { // Check if everything is already running to run the URI play - if (playbackManager.isRestored && MusicStore.loaded()) { + if (playbackManager.isRestored && musicStore.library != null) { playWithUriInternal(uri, context) } else { logD("Cant play this URI right now, waiting") @@ -178,9 +179,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { /** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */ private fun playWithUriInternal(uri: Uri, context: Context) { logD("Playing with uri $uri") - - val musicStore = MusicStore.maybeGetInstance() ?: return - musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) } + val library = musicStore.library ?: return + library.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) } } /** Shuffle all songs */ 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 600dbe3bc..b3d7cf2ba 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 @@ -105,7 +105,7 @@ class PlaybackStateDatabase(context: Context) : * @param musicStore Required to transform database songs/parents into actual instances * @return The stored [SavedState], null if there isn't one. */ - fun readState(musicStore: MusicStore): SavedState? { + fun readState(library: MusicStore.Library): SavedState? { requireBackgroundThread() var state: SavedState? = null @@ -124,16 +124,16 @@ class PlaybackStateDatabase(context: Context) : cursor.moveToFirst() val song = - cursor.getLongOrNull(songIndex)?.let { id -> musicStore.songs.find { it.id == id } } + cursor.getLongOrNull(songIndex)?.let { id -> library.songs.find { it.id == id } } val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS val parent = cursor.getLongOrNull(parentIndex)?.let { id -> when (mode) { - PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id } - PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id } - PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id } + PlaybackMode.IN_GENRE -> library.genres.find { it.id == id } + PlaybackMode.IN_ARTIST -> library.artists.find { it.id == id } + PlaybackMode.IN_ALBUM -> library.albums.find { it.id == id } PlaybackMode.ALL_SONGS -> null } } @@ -186,7 +186,7 @@ class PlaybackStateDatabase(context: Context) : * Read a list of queue items from this database. * @param musicStore Required to transform database songs into actual song instances */ - fun readQueue(musicStore: MusicStore): MutableList { + fun readQueue(library: MusicStore.Library): MutableList { requireBackgroundThread() val queue = mutableListOf() @@ -198,8 +198,10 @@ class PlaybackStateDatabase(context: Context) : val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH) while (cursor.moveToNext()) { - musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex)) - ?.let { song -> queue.add(song) } + library.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let { + song -> + queue.add(song) + } } } 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 feb4a86fc..43121ab5e 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 @@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logE * @author OxygenCobalt */ class PlaybackStateManager private constructor() { + private val musicStore = MusicStore.getInstance() + private val settingsManager = SettingsManager.getInstance() + // Playback private var mSong: Song? = null set(value) { @@ -124,8 +127,6 @@ class PlaybackStateManager private constructor() { val hasPlayed: Boolean get() = mHasPlayed - private val settingsManager = SettingsManager.getInstance() - // --- CALLBACKS --- private val callbacks = mutableListOf() @@ -154,8 +155,7 @@ class PlaybackStateManager private constructor() { when (mode) { PlaybackMode.ALL_SONGS -> { - val musicStore = MusicStore.maybeGetInstance() ?: return - + val musicStore = musicStore.library ?: return mParent = null mQueue = musicStore.songs.toMutableList() } @@ -211,10 +211,11 @@ class PlaybackStateManager private constructor() { /** Shuffle all songs. */ fun shuffleAll() { - val musicStore = MusicStore.maybeGetInstance() ?: return + logD("RETARD. ${musicStore.library}") + val library = musicStore.library ?: return mPlaybackMode = PlaybackMode.ALL_SONGS - mQueue = musicStore.songs.toMutableList() + mQueue = library.songs.toMutableList() mParent = null setShuffling(true, keepSong = false) @@ -370,13 +371,13 @@ class PlaybackStateManager private constructor() { * @param keepSong Whether the current song should be kept as the queue is un-shuffled */ private fun resetShuffle(keepSong: Boolean) { - val musicStore = MusicStore.maybeGetInstance() ?: return + val library = musicStore.library ?: return val lastSong = mSong mQueue = when (mPlaybackMode) { PlaybackMode.ALL_SONGS -> - settingsManager.libSongSort.songs(musicStore.songs).toMutableList() + settingsManager.libSongSort.songs(library.songs).toMutableList() PlaybackMode.IN_ALBUM -> settingsManager.detailAlbumSort.album(mParent as Album).toMutableList() PlaybackMode.IN_ARTIST -> @@ -496,7 +497,7 @@ class PlaybackStateManager private constructor() { suspend fun restoreFromDatabase(context: Context) { logD("Getting state from DB") - val musicStore = MusicStore.maybeGetInstance() ?: return + val library = musicStore.library ?: return val start: Long val playbackState: PlaybackStateDatabase.SavedState? val queue: MutableList @@ -504,8 +505,8 @@ class PlaybackStateManager private constructor() { withContext(Dispatchers.IO) { start = System.currentTimeMillis() val database = PlaybackStateDatabase.getInstance(context) - playbackState = database.readState(musicStore) - queue = database.readQueue(musicStore) + playbackState = database.readState(library) + queue = database.readQueue(library) } // Get off the IO coroutine since it will cause LiveData updates to throw an exception 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 ad51b0b15..13ea89306 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 @@ -76,6 +76,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider * * TODO: Move all external exposal from passing around PlaybackStateManager to passing around the * MediaMetadata instance. Generally makes it easier to encapsulate this class. + * + * TODO: Move restore and file opening to service */ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 2e114da2b..9a1a486c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -115,11 +115,6 @@ class SearchFragment : ViewBindingFragment(), MenuItemLis musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse) } - override fun onResume() { - super.onResume() - searchModel.setNavigating(false) - } - override fun onDestroyBinding(binding: FragmentSearchBinding) { super.onDestroyBinding(binding) binding.searchRecycler.adapter = null diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index e9205aece..6e733636d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -40,8 +40,10 @@ import org.oxycblt.auxio.util.logD * @author OxygenCobalt */ class SearchViewModel : ViewModel() { + private val musicStore = MusicStore.getInstance() + private val settingsManager = SettingsManager.getInstance() + private val mSearchResults = MutableLiveData(listOf()) - private var mIsNavigating = false private var mFilterMode: DisplayMode? = null private var mLastQuery: String? = null @@ -51,8 +53,6 @@ class SearchViewModel : ViewModel() { val filterMode: DisplayMode? get() = mFilterMode - private val settingsManager = SettingsManager.getInstance() - init { mFilterMode = settingsManager.searchFilterMode } @@ -61,10 +61,10 @@ class SearchViewModel : ViewModel() { * Use [query] to perform a search of the music library. Will push results to [searchResults]. */ fun search(context: Context, query: String?) { - val musicStore = MusicStore.maybeGetInstance() mLastQuery = query - if (query.isNullOrEmpty() || musicStore == null) { + val library = musicStore.library + if (query.isNullOrEmpty() || library == null) { logD("No music/query, ignoring search") mSearchResults.value = listOf() return @@ -80,28 +80,28 @@ class SearchViewModel : ViewModel() { // Note: a filter mode of null means to not filter at all. if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) { - musicStore.artists.filterByOrNull(context, query)?.let { artists -> + library.artists.filterByOrNull(context, query)?.let { artists -> results.add(Header(-1, R.string.lbl_artists)) results.addAll(sort.artists(artists)) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) { - musicStore.albums.filterByOrNull(context, query)?.let { albums -> + library.albums.filterByOrNull(context, query)?.let { albums -> results.add(Header(-2, R.string.lbl_albums)) results.addAll(sort.albums(albums)) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) { - musicStore.genres.filterByOrNull(context, query)?.let { genres -> + library.genres.filterByOrNull(context, query)?.let { genres -> results.add(Header(-3, R.string.lbl_genres)) results.addAll(sort.genres(genres)) } } if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) { - musicStore.songs.filterByOrNull(context, query)?.let { songs -> + library.songs.filterByOrNull(context, query)?.let { songs -> results.add(Header(-4, R.string.lbl_songs)) results.addAll(sort.songs(songs)) } @@ -181,9 +181,4 @@ class SearchViewModel : ViewModel() { return sb.toString() } - - /** Update the current navigation status to [isNavigating] */ - fun setNavigating(isNavigating: Boolean) { - mIsNavigating = isNavigating - } } diff --git a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml index b0e2cf22d..51760b045 100644 --- a/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml +++ b/app/src/main/res/layout-sw600dp/fragment_playback_panel.xml @@ -18,7 +18,7 @@