music: rework loading management
Rework loading management into a new object, Indexer, which should act as the base for all future loading changes. Indexer tries to solve a two issues with music loading: 1. The issue of what occurs when tasks must be restarted or cancelled, which is common with music loading. 2. Trying to find a good strategy to mediate between service and ViewModel components in a sensible manner. Indexer also rolls in a lot of the universal music loading code alongside this, as much of MusicStore's loading state went unused by app components and only raised technical challenges when reworking it.
This commit is contained in:
parent
66b95cef42
commit
e3708bf5f5
12 changed files with 245 additions and 204 deletions
|
@ -26,8 +26,8 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
|
import org.oxycblt.auxio.music.IndexerViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
|
@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.launch
|
||||||
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: IndexerViewModel by activityViewModels()
|
||||||
private var callback: DynamicBackPressedCallback? = null
|
private var callback: DynamicBackPressedCallback? = null
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentMainBinding.inflate(inflater)
|
||||||
|
@ -73,7 +73,7 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
// Initialize music loading. Do it here so that it shows on every fragment that this
|
// Initialize music loading. Do it here so that it shows on every fragment that this
|
||||||
// one contains.
|
// one contains.
|
||||||
// TODO: Move this to a service [automatic rescanning]
|
// TODO: Move this to a service [automatic rescanning]
|
||||||
musicModel.loadMusic(requireContext())
|
musicModel.index(requireContext())
|
||||||
|
|
||||||
launch { navModel.mainNavigationAction.collect(::handleMainNavigation) }
|
launch { navModel.mainNavigationAction.collect(::handleMainNavigation) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleExploreNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleExploreNavigation) }
|
||||||
|
|
|
@ -44,9 +44,9 @@ import org.oxycblt.auxio.home.list.SongListFragment
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Indexer
|
||||||
|
import org.oxycblt.auxio.music.IndexerViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.DisplayMode
|
import org.oxycblt.auxio.ui.DisplayMode
|
||||||
|
@ -70,7 +70,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val indexerModel: IndexerViewModel by activityViewModels()
|
||||||
|
|
||||||
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
private var storagePermissionLauncher: ActivityResultLauncher<String>? = null
|
||||||
private var sortItem: MenuItem? = null
|
private var sortItem: MenuItem? = null
|
||||||
|
@ -81,7 +81,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
// Build the permission launcher here as you can only do it in onCreateView/onCreate
|
||||||
storagePermissionLauncher =
|
storagePermissionLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||||
musicModel.reloadMusic(requireContext())
|
indexerModel.reindex(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeToolbar.apply {
|
binding.homeToolbar.apply {
|
||||||
|
@ -123,7 +123,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
launch { homeModel.isFastScrolling.collect(::updateFastScrolling) }
|
launch { homeModel.isFastScrolling.collect(::updateFastScrolling) }
|
||||||
launch { homeModel.currentTab.collect(::updateCurrentTab) }
|
launch { homeModel.currentTab.collect(::updateCurrentTab) }
|
||||||
launch { homeModel.recreateTabs.collect(::handleRecreateTabs) }
|
launch { homeModel.recreateTabs.collect(::handleRecreateTabs) }
|
||||||
launch { musicModel.loadState.collect(::handleLoadEvent) }
|
launch { indexerModel.state.collect(::handleIndexerState) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,8 +178,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
|
|
||||||
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
// Make sure an update here doesn't mess up the FAB state when it comes to the
|
||||||
// loader response.
|
// loader response.
|
||||||
val state = musicModel.loadState.value
|
val state = indexerModel.state.value
|
||||||
if (!(state is MusicStore.LoadState.Complete && state.response is MusicStore.Response.Ok)) {
|
if (!(state is Indexer.State.Complete && state.response is Indexer.Response.Ok)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,18 +257,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoadEvent(state: MusicStore.LoadState?) {
|
private fun handleIndexerState(state: Indexer.State?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
if (state is MusicStore.LoadState.Complete) {
|
if (state is Indexer.State.Complete) {
|
||||||
handleLoaderResponse(binding, state.response)
|
handleLoaderResponse(binding, state.response)
|
||||||
} else {
|
} else {
|
||||||
handleLoadEvent(binding, state)
|
handleLoadingState(binding, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoaderResponse(binding: FragmentHomeBinding, response: MusicStore.Response) {
|
private fun handleLoaderResponse(binding: FragmentHomeBinding, response: Indexer.Response) {
|
||||||
if (response is MusicStore.Response.Ok) {
|
if (response is Indexer.Response.Ok) {
|
||||||
binding.homeFab.show()
|
binding.homeFab.show()
|
||||||
binding.homeLoadingContainer.visibility = View.INVISIBLE
|
binding.homeLoadingContainer.visibility = View.INVISIBLE
|
||||||
binding.homePager.visibility = View.VISIBLE
|
binding.homePager.visibility = View.VISIBLE
|
||||||
|
@ -280,30 +280,29 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
logD("Received non-ok response $response")
|
logD("Received non-ok response $response")
|
||||||
|
|
||||||
when (response) {
|
when (response) {
|
||||||
is MusicStore.Response.Ok -> error("Unreachable")
|
is Indexer.Response.Ok -> error("Unreachable")
|
||||||
is MusicStore.Response.Err -> {
|
is Indexer.Response.Err -> {
|
||||||
logD("Received Response.Err")
|
|
||||||
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
binding.homeLoadingStatus.textSafe = getString(R.string.err_load_failed)
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_load_failed)
|
||||||
binding.homeLoadingAction.apply {
|
binding.homeLoadingAction.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = getString(R.string.lbl_retry)
|
text = getString(R.string.lbl_retry)
|
||||||
setOnClickListener { musicModel.reloadMusic(requireContext()) }
|
setOnClickListener { indexerModel.reindex(requireContext()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MusicStore.Response.NoMusic -> {
|
is Indexer.Response.NoMusic -> {
|
||||||
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_music)
|
binding.homeLoadingStatus.textSafe = getString(R.string.err_no_music)
|
||||||
binding.homeLoadingAction.apply {
|
binding.homeLoadingAction.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
text = getString(R.string.lbl_retry)
|
text = getString(R.string.lbl_retry)
|
||||||
setOnClickListener { musicModel.reloadMusic(requireContext()) }
|
setOnClickListener { indexerModel.reindex(requireContext()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is MusicStore.Response.NoPerms -> {
|
is Indexer.Response.NoPerms -> {
|
||||||
val launcher =
|
val launcher =
|
||||||
requireNotNull(storagePermissionLauncher) {
|
requireNotNull(storagePermissionLauncher) {
|
||||||
"Cannot access permission launcher while in non-view state"
|
"Cannot access permission launcher while detached"
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
binding.homeLoadingProgress.visibility = View.INVISIBLE
|
||||||
|
@ -320,14 +319,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoadEvent(binding: FragmentHomeBinding, event: MusicStore.LoadState?) {
|
private fun handleLoadingState(binding: FragmentHomeBinding, event: Indexer.State?) {
|
||||||
binding.homeFab.hide()
|
binding.homeFab.hide()
|
||||||
binding.homePager.visibility = View.INVISIBLE
|
binding.homePager.visibility = View.INVISIBLE
|
||||||
binding.homeLoadingContainer.visibility = View.VISIBLE
|
binding.homeLoadingContainer.visibility = View.VISIBLE
|
||||||
binding.homeLoadingProgress.visibility = View.VISIBLE
|
binding.homeLoadingProgress.visibility = View.VISIBLE
|
||||||
binding.homeLoadingAction.visibility = View.INVISIBLE
|
binding.homeLoadingAction.visibility = View.INVISIBLE
|
||||||
|
|
||||||
if (event is MusicStore.LoadState.Indexing) {
|
if (event is Indexer.State.Loading) {
|
||||||
binding.homeLoadingStatus.textSafe =
|
binding.homeLoadingStatus.textSafe =
|
||||||
getString(R.string.fmt_indexing, event.current, event.total)
|
getString(R.string.fmt_indexing, event.current, event.total)
|
||||||
binding.homeLoadingProgress.apply {
|
binding.homeLoadingProgress.apply {
|
||||||
|
|
|
@ -134,9 +134,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||||
if (response is MusicStore.Response.Ok) {
|
if (library != null) {
|
||||||
val library = response.library
|
|
||||||
_songs.value = settingsManager.libSongSort.songs(library.songs)
|
_songs.value = settingsManager.libSongSort.songs(library.songs)
|
||||||
_albums.value = settingsManager.libAlbumSort.albums(library.albums)
|
_albums.value = settingsManager.libAlbumSort.albums(library.albums)
|
||||||
_artists.value = settingsManager.libArtistSort.artists(library.artists)
|
_artists.value = settingsManager.libArtistSort.artists(library.artists)
|
||||||
|
|
|
@ -15,18 +15,23 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.indexer
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.oxycblt.auxio.music.Album
|
import androidx.core.content.ContextCompat
|
||||||
import org.oxycblt.auxio.music.Artist
|
import kotlinx.coroutines.Dispatchers
|
||||||
import org.oxycblt.auxio.music.Genre
|
import kotlinx.coroutines.delay
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
|
||||||
|
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
|
||||||
|
import org.oxycblt.auxio.music.backend.ExoPlayerBackend
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auxio's media indexer.
|
* Auxio's media indexer.
|
||||||
|
@ -40,13 +45,93 @@ import org.oxycblt.auxio.util.logD
|
||||||
* 3. Using the songs to build the library, which primarily involves linking up all data objects
|
* 3. Using the songs to build the library, which primarily involves linking up all data objects
|
||||||
* with their corresponding parents/children.
|
* with their corresponding parents/children.
|
||||||
*
|
*
|
||||||
* This class in particular handles 3 primarily. For the code that handles 1 and 2, see the other
|
* This class in particular handles 3 primarily. For the code that handles 1 and 2, see the
|
||||||
* files in the module.
|
* [Backend] implementations.
|
||||||
|
*
|
||||||
|
* This class also fulfills the role of maintaining the current music loading state, which seems
|
||||||
|
* like a job for [MusicStore] but in practice is only really leveraged by the components that
|
||||||
|
* directly work with music loading, making such redundant.
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
object Indexer {
|
class Indexer {
|
||||||
fun index(context: Context, callback: MusicStore.LoadCallback): MusicStore.Library? {
|
private var state: State? = null
|
||||||
|
private var currentGeneration: Long = 0
|
||||||
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
|
fun addCallback(callback: Callback) {
|
||||||
|
callback.onIndexerStateChanged(state)
|
||||||
|
callbacks.add(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCallback(callback: Callback) {
|
||||||
|
callbacks.remove(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun index(context: Context) {
|
||||||
|
val generation = synchronized(this) { ++currentGeneration }
|
||||||
|
|
||||||
|
val notGranted =
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
|
if (notGranted) {
|
||||||
|
emitState(State.Complete(Response.NoPerms), generation)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response =
|
||||||
|
try {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
val library = withContext(Dispatchers.IO) { indexImpl(context, generation) }
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitState(State.Complete(response), generation)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Cancel" the last job by making it unable to send further state updates. This should be
|
||||||
|
* called if an object that called [index] is about to be destroyed and thus will have it's task
|
||||||
|
* canceled, in which it would be useful for any ongoing loading process to not accidentally
|
||||||
|
* corrupt the current state.
|
||||||
|
*/
|
||||||
|
fun cancelLast() {
|
||||||
|
synchronized(this) {
|
||||||
|
currentGeneration++
|
||||||
|
emitState(null, currentGeneration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitState(newState: State?, generation: Long) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (currentGeneration == generation) {
|
||||||
|
state = newState
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onIndexerStateChanged(newState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the proper music loading process. [generation] must be a truthful value of the generation
|
||||||
|
* calling this function.
|
||||||
|
*/
|
||||||
|
private fun indexImpl(context: Context, generation: Long): MusicStore.Library? {
|
||||||
|
emitState(State.Query, generation)
|
||||||
|
|
||||||
// Establish the backend to use when initially loading songs.
|
// Establish the backend to use when initially loading songs.
|
||||||
val mediaStoreBackend =
|
val mediaStoreBackend =
|
||||||
when {
|
when {
|
||||||
|
@ -54,7 +139,7 @@ object Indexer {
|
||||||
else -> Api21MediaStoreBackend()
|
else -> Api21MediaStoreBackend()
|
||||||
}
|
}
|
||||||
|
|
||||||
val songs = buildSongs(context, ExoPlayerBackend(mediaStoreBackend), callback)
|
val songs = buildSongs(context, ExoPlayerBackend(mediaStoreBackend), generation)
|
||||||
if (songs.isEmpty()) {
|
if (songs.isEmpty()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -88,11 +173,7 @@ object Indexer {
|
||||||
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
* [buildGenres] functions must be called with the returned list so that all songs are properly
|
||||||
* linked up.
|
* linked up.
|
||||||
*/
|
*/
|
||||||
private fun buildSongs(
|
private fun buildSongs(context: Context, backend: Backend, generation: Long): List<Song> {
|
||||||
context: Context,
|
|
||||||
backend: Backend,
|
|
||||||
callback: MusicStore.LoadCallback
|
|
||||||
): List<Song> {
|
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
|
|
||||||
var songs =
|
var songs =
|
||||||
|
@ -101,7 +182,9 @@ object Indexer {
|
||||||
"Successfully queried media database " +
|
"Successfully queried media database " +
|
||||||
"in ${System.currentTimeMillis() - start}ms")
|
"in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
||||||
backend.loadSongs(context, cursor, callback)
|
backend.loadSongs(context, cursor) { count, total ->
|
||||||
|
emitState(State.Loading(count, total), generation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate songs to prevent (most) deformed music clones
|
// Deduplicate songs to prevent (most) deformed music clones
|
||||||
|
@ -213,6 +296,25 @@ object Indexer {
|
||||||
return genres
|
return genres
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Represents the current indexer state. */
|
||||||
|
sealed class State {
|
||||||
|
object Query : State()
|
||||||
|
data class Loading(val current: Int, val total: Int) : State()
|
||||||
|
data class Complete(val response: Response) : State()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents the possible outcomes of a loading process. */
|
||||||
|
sealed class Response {
|
||||||
|
data class Ok(val library: MusicStore.Library) : Response()
|
||||||
|
data class Err(val throwable: Throwable) : Response()
|
||||||
|
object NoMusic : Response()
|
||||||
|
object NoPerms : Response()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onIndexerStateChanged(state: State?)
|
||||||
|
}
|
||||||
|
|
||||||
/** Represents a backend that metadata can be extracted from. */
|
/** Represents a backend that metadata can be extracted from. */
|
||||||
interface Backend {
|
interface Backend {
|
||||||
/** Query the media database for a basic cursor. */
|
/** Query the media database for a basic cursor. */
|
||||||
|
@ -222,7 +324,26 @@ object Indexer {
|
||||||
fun loadSongs(
|
fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
callback: MusicStore.LoadCallback
|
onAddSong: (count: Int, total: Int) -> Unit
|
||||||
): Collection<Song>
|
): Collection<Song>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile private var INSTANCE: Indexer? = null
|
||||||
|
|
||||||
|
/** Get the process-level instance of [Indexer]. */
|
||||||
|
fun getInstance(): Indexer {
|
||||||
|
val currentInstance = INSTANCE
|
||||||
|
|
||||||
|
if (currentInstance != null) {
|
||||||
|
return currentInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this) {
|
||||||
|
val newInstance = Indexer()
|
||||||
|
INSTANCE = newInstance
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -25,45 +25,52 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/** A [ViewModel] that represents the current music indexing state. */
|
/** A ViewModel representing the current music indexing state. */
|
||||||
class MusicViewModel : ViewModel(), MusicStore.LoadCallback {
|
class IndexerViewModel : ViewModel(), Indexer.Callback {
|
||||||
|
private val indexer = Indexer.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val _loadState = MutableStateFlow<MusicStore.LoadState?>(null)
|
private val _state = MutableStateFlow<Indexer.State?>(null)
|
||||||
val loadState: StateFlow<MusicStore.LoadState?> = _loadState
|
val state: StateFlow<Indexer.State?> = _state
|
||||||
|
|
||||||
private var isBusy = false
|
init {
|
||||||
|
indexer.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/** Initiate the indexing process. */
|
||||||
* Initiate the loading process. This is done here since HomeFragment will be the first fragment
|
fun index(context: Context) {
|
||||||
* navigated to and because SnackBars will have the best UX here.
|
if (state.value != null) {
|
||||||
*/
|
logD("Loader is already loading/is completed, not reloading")
|
||||||
fun loadMusic(context: Context) {
|
|
||||||
if (_loadState.value != null || isBusy) {
|
|
||||||
logD("Loader is busy/already completed, not reloading")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isBusy = true
|
indexImpl(context)
|
||||||
_loadState.value = null
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
musicStore.load(context, this@MusicViewModel)
|
|
||||||
isBusy = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reload the music library. Note that this call will result in unexpected behavior in the case
|
* Reload the music library. Note that this call will result in unexpected behavior in the case
|
||||||
* that music is reloaded after a loading process has already exceeded.
|
* that music is reloaded after a loading process has already exceeded.
|
||||||
*/
|
*/
|
||||||
fun reloadMusic(context: Context) {
|
fun reindex(context: Context) {
|
||||||
logD("Reloading music library")
|
logD("Reloading music library")
|
||||||
_loadState.value = null
|
indexImpl(context)
|
||||||
loadMusic(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoadStateChanged(state: MusicStore.LoadState?) {
|
private fun indexImpl(context: Context) {
|
||||||
_loadState.value = state
|
viewModelScope.launch { indexer.index(context) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||||
|
_state.value = state
|
||||||
|
|
||||||
|
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
|
||||||
|
musicStore.library = state.response.library
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
indexer.cancelLast()
|
||||||
|
indexer.removeCallback(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,8 +23,8 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.indexer.id3v2GenreName
|
import org.oxycblt.auxio.music.backend.id3v2GenreName
|
||||||
import org.oxycblt.auxio.music.indexer.withoutArticle
|
import org.oxycblt.auxio.music.backend.withoutArticle
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
|
|
@ -17,20 +17,11 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.core.content.ContextCompat
|
import org.oxycblt.auxio.music.backend.useQuery
|
||||||
import java.lang.Exception
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.oxycblt.auxio.music.indexer.Indexer
|
|
||||||
import org.oxycblt.auxio.music.indexer.useQuery
|
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.logE
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main storage for music items. Getting an instance of this object is more complicated as it
|
* The main storage for music items. Getting an instance of this object is more complicated as it
|
||||||
|
@ -40,22 +31,19 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicStore private constructor() {
|
class MusicStore private constructor() {
|
||||||
private var response: Response? = null
|
|
||||||
val library: Library?
|
|
||||||
get() =
|
|
||||||
response?.let { currentResponse ->
|
|
||||||
if (currentResponse is Response.Ok) {
|
|
||||||
currentResponse.library
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
|
var library: Library? = null
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onLibraryChanged(library)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Add a callback to this instance. Make sure to remove it when done. */
|
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
response?.let(callback::onMusicUpdate)
|
callback.onLibraryChanged(library)
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,53 +52,7 @@ class MusicStore private constructor() {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
/** Represents a library of music owned by [MusicStore]. */
|
||||||
suspend fun load(context: Context, callback: LoadCallback): Response {
|
|
||||||
logD("Starting initial music load")
|
|
||||||
|
|
||||||
callback.onLoadStateChanged(null)
|
|
||||||
val newResponse =
|
|
||||||
withContext(Dispatchers.IO) { loadImpl(context, callback) }.also { response = it }
|
|
||||||
|
|
||||||
callback.onLoadStateChanged(LoadState.Complete(newResponse))
|
|
||||||
for (responseCallbacks in callbacks) {
|
|
||||||
responseCallbacks.onMusicUpdate(newResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadImpl(context: Context, callback: LoadCallback): Response {
|
|
||||||
val notGranted =
|
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
|
||||||
PackageManager.PERMISSION_DENIED
|
|
||||||
|
|
||||||
if (notGranted) {
|
|
||||||
return Response.NoPerms
|
|
||||||
}
|
|
||||||
|
|
||||||
val response =
|
|
||||||
try {
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
val library = Indexer.index(context, callback)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Library(
|
data class Library(
|
||||||
val genres: List<Genre>,
|
val genres: List<Genre>,
|
||||||
val artists: List<Artist>,
|
val artists: List<Artist>,
|
||||||
|
@ -138,35 +80,9 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Represents the current state of the loading process. */
|
|
||||||
sealed class LoadState {
|
|
||||||
data class Indexing(val current: Int, val total: Int) : LoadState()
|
|
||||||
data class Complete(val response: Response) : LoadState()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A callback for events that occur during the loading process. This is used by [load] in order
|
|
||||||
* to have a separate callback interface that is more efficient for rapid-fire updates.
|
|
||||||
*/
|
|
||||||
interface LoadCallback {
|
|
||||||
/**
|
|
||||||
* Called when the state of the loading process changes. A value of null represents the
|
|
||||||
* beginning of a loading process.
|
|
||||||
*/
|
|
||||||
fun onLoadStateChanged(state: LoadState?)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Represents the possible outcomes of a loading process. */
|
|
||||||
sealed class Response {
|
|
||||||
data class Ok(val library: Library) : Response()
|
|
||||||
data class Err(val throwable: Throwable) : Response()
|
|
||||||
object NoMusic : Response()
|
|
||||||
object NoPerms : Response()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A callback for awaiting the loading of music. */
|
/** A callback for awaiting the loading of music. */
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onMusicUpdate(response: Response)
|
fun onLibraryChanged(library: Library?)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.indexer
|
package org.oxycblt.auxio.music.backend
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
|
@ -31,12 +31,12 @@ import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import java.util.concurrent.Future
|
import java.util.concurrent.Future
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.Indexer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Indexer.Backend] that leverages ExoPlayer's metadata retrieval system to index metadata.
|
* A [OldIndexer.Backend] that leverages ExoPlayer's metadata retrieval system to index metadata.
|
||||||
*
|
*
|
||||||
* Normally, leveraging ExoPlayer's metadata system would be a terrible idea, as it is horrifically
|
* Normally, leveraging ExoPlayer's metadata system would be a terrible idea, as it is horrifically
|
||||||
* slow. However, if we parallelize it, we can get similar throughput to other metadata extractors,
|
* slow. However, if we parallelize it, we can get similar throughput to other metadata extractors,
|
||||||
|
@ -62,7 +62,7 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
override fun loadSongs(
|
override fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
callback: MusicStore.LoadCallback
|
onAddSong: (count: Int, total: Int) -> Unit
|
||||||
): Collection<Song> {
|
): Collection<Song> {
|
||||||
// Metadata retrieval with ExoPlayer is asynchronous, so a callback may at any point
|
// Metadata retrieval with ExoPlayer is asynchronous, so a callback may at any point
|
||||||
// add a completed song to the list. To prevent a crash in that case, we use the
|
// add a completed song to the list. To prevent a crash in that case, we use the
|
||||||
|
@ -90,8 +90,7 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
AudioCallback(audio) {
|
AudioCallback(audio) {
|
||||||
runningTasks[index] = null
|
runningTasks[index] = null
|
||||||
songs.add(it)
|
songs.add(it)
|
||||||
callback.onLoadStateChanged(
|
onAddSong(songs.size, cursor.count)
|
||||||
MusicStore.LoadState.Indexing(songs.size, cursor.count))
|
|
||||||
},
|
},
|
||||||
// Normal JVM dispatcher will suffice here, as there is no IO work
|
// Normal JVM dispatcher will suffice here, as there is no IO work
|
||||||
// going on (and there is no cost from switching contexts with executors)
|
// going on (and there is no cost from switching contexts with executors)
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.indexer
|
package org.oxycblt.auxio.music.backend
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
|
@ -41,11 +41,19 @@ fun <R> ContentResolver.useQuery(
|
||||||
block: (Cursor) -> R
|
block: (Cursor) -> R
|
||||||
): R? = queryCursor(uri, projection, selector, args)?.use(block)
|
): R? = queryCursor(uri, projection, selector, args)?.use(block)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For some reason the album art URI namespace does not have a member in [MediaStore], but it still
|
||||||
|
* works since at least API 21.
|
||||||
|
*/
|
||||||
|
private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart")
|
||||||
|
|
||||||
/** Converts a [Long] Audio ID into a URI to that particular audio file. */
|
/** Converts a [Long] Audio ID into a URI to that particular audio file. */
|
||||||
val Long.audioUri: Uri
|
val Long.audioUri: Uri
|
||||||
get() =
|
get() = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
||||||
ContentUris.withAppendedId(
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, requireNotNull(this))
|
/** Converts a [Long] Album ID into a URI pointing to MediaStore-cached album art. */
|
||||||
|
val Long.albumCoverUri: Uri
|
||||||
|
get() = ContentUris.withAppendedId(EXTERNAL_ALBUM_ART_URI, this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
|
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
|
||||||
|
@ -105,7 +113,8 @@ val String.id3v2GenreName: String
|
||||||
|
|
||||||
return substring(1 until lastIndex).toIntOrNull()?.run {
|
return substring(1 until lastIndex).toIntOrNull()?.run {
|
||||||
genreConstantTable.getOrNull(this)
|
genreConstantTable.getOrNull(this)
|
||||||
} ?: this
|
}
|
||||||
|
?: this
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current name is fine.
|
// Current name is fine.
|
|
@ -15,18 +15,16 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.indexer
|
package org.oxycblt.auxio.music.backend
|
||||||
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.Indexer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
|
@ -90,8 +88,9 @@ import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is
|
* Represents a [OldIndexer.Backend] that loads music from the media database ([MediaStore]). This
|
||||||
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
|
* is not a fully-featured class by itself, and it's API-specific derivatives should be used
|
||||||
|
* instead.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class MediaStoreBackend : Indexer.Backend {
|
abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
|
@ -131,7 +130,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
override fun loadSongs(
|
override fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
callback: MusicStore.LoadCallback
|
onAddSong: (count: Int, total: Int) -> Unit
|
||||||
): Collection<Song> {
|
): Collection<Song> {
|
||||||
// Note: We do not actually update the callback with an Indexing state, this is because
|
// Note: We do not actually update the callback with an Indexing state, this is because
|
||||||
// loading music from MediaStore tends to be quite fast, with the only bottlenecks being
|
// loading music from MediaStore tends to be quite fast, with the only bottlenecks being
|
||||||
|
@ -280,9 +279,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
_year = year,
|
_year = year,
|
||||||
_albumName = requireNotNull(album) { "Malformed audio: No album name" },
|
_albumName = requireNotNull(album) { "Malformed audio: No album name" },
|
||||||
_albumCoverUri =
|
_albumCoverUri =
|
||||||
ContentUris.withAppendedId(
|
requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri,
|
||||||
EXTERNAL_ALBUM_ART_URI,
|
|
||||||
requireNotNull(albumId) { "Malformed audio: No album id" }),
|
|
||||||
_artistName = artist,
|
_artistName = artist,
|
||||||
_albumArtistName = albumArtist,
|
_albumArtistName = albumArtist,
|
||||||
_genreName = genre)
|
_genreName = genre)
|
||||||
|
@ -304,12 +301,6 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
*/
|
*/
|
||||||
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
|
||||||
|
|
||||||
/**
|
|
||||||
* For some reason the album art URI namespace does not have a member in [MediaStore], but
|
|
||||||
* it still works since at least API 21.
|
|
||||||
*/
|
|
||||||
private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The basic projection that works across all versions of android. Is incomplete, hence why
|
* The basic projection that works across all versions of android. Is incomplete, hence why
|
||||||
* sub-implementations should be used instead.
|
* sub-implementations should be used instead.
|
|
@ -318,11 +318,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore
|
||||||
_repeatMode.value = repeatMode
|
_repeatMode.value = repeatMode
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicUpdate(response: MusicStore.Response) {
|
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||||
if (response is MusicStore.Response.Ok) {
|
if (library != null) {
|
||||||
val action = pendingDelayedAction
|
val action = pendingDelayedAction
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
performActionImpl(action, response.library)
|
performActionImpl(action, library)
|
||||||
pendingDelayedAction = null
|
pendingDelayedAction = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,10 @@ import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
import org.oxycblt.auxio.music.Indexer
|
||||||
|
import org.oxycblt.auxio.music.IndexerViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
|
@ -64,7 +64,7 @@ class SearchFragment :
|
||||||
private val searchModel: SearchViewModel by viewModels()
|
private val searchModel: SearchViewModel by viewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val indexerModel: IndexerViewModel by activityViewModels()
|
||||||
|
|
||||||
private val searchAdapter = SearchAdapter(this)
|
private val searchAdapter = SearchAdapter(this)
|
||||||
private var imm: InputMethodManager? = null
|
private var imm: InputMethodManager? = null
|
||||||
|
@ -109,7 +109,7 @@ class SearchFragment :
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
launch { searchModel.searchResults.collect(::updateResults) }
|
launch { searchModel.searchResults.collect(::updateResults) }
|
||||||
launch { musicModel.loadState.collect(::handleLoadState) }
|
launch { indexerModel.state.collect(::handleIndexerState) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,8 +171,8 @@ class SearchFragment :
|
||||||
requireImm().hide()
|
requireImm().hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoadState(state: MusicStore.LoadState?) {
|
private fun handleIndexerState(state: Indexer.State?) {
|
||||||
if (state is MusicStore.LoadState.Complete && state.response is MusicStore.Response.Ok) {
|
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
|
||||||
searchModel.refresh(requireContext())
|
searchModel.refresh(requireContext())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue