music: hide indexer impl
Hide the implementation of Indexer behind an interface. This is in preparation for dependency injection.
This commit is contained in:
parent
b34e6fdc8a
commit
41bc6f9dfc
4 changed files with 210 additions and 207 deletions
|
@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.system.Indexer
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class MusicViewModel : ViewModel(), Indexer.Listener {
|
class MusicViewModel : ViewModel(), Indexer.Listener {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.get()
|
||||||
|
|
||||||
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
|
private val _indexerState = MutableStateFlow<Indexer.State?>(null)
|
||||||
/** The current music loading state, or null if no loading is going on. */
|
/** The current music loading state, or null if no loading is going on. */
|
||||||
|
|
|
@ -44,23 +44,15 @@ import org.oxycblt.auxio.util.logW
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class Indexer private constructor() {
|
interface Indexer {
|
||||||
@Volatile private var lastResponse: Result<Library>? = null
|
|
||||||
@Volatile private var indexingState: Indexing? = null
|
|
||||||
@Volatile private var controller: Controller? = null
|
|
||||||
@Volatile private var listener: Listener? = null
|
|
||||||
|
|
||||||
/** Whether music loading is occurring or not. */
|
/** Whether music loading is occurring or not. */
|
||||||
val isIndexing: Boolean
|
val isIndexing: Boolean
|
||||||
get() = indexingState != null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this instance has not completed a loading process and is not currently loading music.
|
* Whether this instance has not completed a loading process and is not currently loading music.
|
||||||
* This often occurs early in an app's lifecycle, and consumers should try to avoid showing any
|
* This often occurs early in an app's lifecycle, and consumers should try to avoid showing any
|
||||||
* state when this flag is true.
|
* state when this flag is true.
|
||||||
*/
|
*/
|
||||||
val isIndeterminate: Boolean
|
val isIndeterminate: Boolean
|
||||||
get() = lastResponse == null && indexingState == null
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a [Controller] for this instance. This instance will handle any commands to start
|
* Register a [Controller] for this instance. This instance will handle any commands to start
|
||||||
|
@ -68,19 +60,7 @@ class Indexer private constructor() {
|
||||||
* [Listener] methods to initialize the instance with the current state.
|
* [Listener] methods to initialize the instance with the current state.
|
||||||
* @param controller The [Controller] to register. Will do nothing if already registered.
|
* @param controller The [Controller] to register. Will do nothing if already registered.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun registerController(controller: Controller)
|
||||||
fun registerController(controller: Controller) {
|
|
||||||
if (BuildConfig.DEBUG && this.controller != null) {
|
|
||||||
logW("Controller is already registered")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the controller with the current state.
|
|
||||||
val currentState =
|
|
||||||
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
|
|
||||||
controller.onIndexerStateChanged(currentState)
|
|
||||||
this.controller = controller
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister the [Controller] from this instance, prevent it from recieving any further
|
* Unregister the [Controller] from this instance, prevent it from recieving any further
|
||||||
|
@ -88,15 +68,7 @@ class Indexer private constructor() {
|
||||||
* @param controller The [Controller] to unregister. Must be the current [Controller]. Does
|
* @param controller The [Controller] to unregister. Must be the current [Controller]. Does
|
||||||
* nothing if invoked by another [Controller] implementation.
|
* nothing if invoked by another [Controller] implementation.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun unregisterController(controller: Controller)
|
||||||
fun unregisterController(controller: Controller) {
|
|
||||||
if (BuildConfig.DEBUG && this.controller !== controller) {
|
|
||||||
logW("Given controller did not match current controller")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.controller = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register the [Listener] for this instance. This can be used to receive rapid-fire updates to
|
* Register the [Listener] for this instance. This can be used to receive rapid-fire updates to
|
||||||
|
@ -104,19 +76,7 @@ class Indexer private constructor() {
|
||||||
* [Listener] methods to initialize the instance with the current state.
|
* [Listener] methods to initialize the instance with the current state.
|
||||||
* @param listener The [Listener] to add.
|
* @param listener The [Listener] to add.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun registerListener(listener: Listener)
|
||||||
fun registerListener(listener: Listener) {
|
|
||||||
if (BuildConfig.DEBUG && this.listener != null) {
|
|
||||||
logW("Listener is already registered")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize the listener with the current state.
|
|
||||||
val currentState =
|
|
||||||
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
|
|
||||||
listener.onIndexerStateChanged(currentState)
|
|
||||||
this.listener = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unregister a [Listener] from this instance, preventing it from recieving any further updates.
|
* Unregister a [Listener] from this instance, preventing it from recieving any further updates.
|
||||||
|
@ -124,15 +84,7 @@ class Indexer private constructor() {
|
||||||
* invoked by another [Listener] implementation.
|
* invoked by another [Listener] implementation.
|
||||||
* @see Listener
|
* @see Listener
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun unregisterListener(listener: Listener)
|
||||||
fun unregisterListener(listener: Listener) {
|
|
||||||
if (BuildConfig.DEBUG && this.listener !== listener) {
|
|
||||||
logW("Given controller did not match current controller")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.listener = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the indexing process. This should be done from in the background from [Controller]'s
|
* Start the indexing process. This should be done from in the background from [Controller]'s
|
||||||
|
@ -141,159 +93,22 @@ class Indexer private constructor() {
|
||||||
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
|
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
|
||||||
* be written, but no cache entries will be loaded into the new library.
|
* be written, but no cache entries will be loaded into the new library.
|
||||||
*/
|
*/
|
||||||
suspend fun index(context: Context, withCache: Boolean) {
|
suspend fun index(context: Context, withCache: Boolean)
|
||||||
val result =
|
|
||||||
try {
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
val library = indexImpl(context, withCache)
|
|
||||||
logD(
|
|
||||||
"Music indexing completed successfully in " +
|
|
||||||
"${System.currentTimeMillis() - start}ms")
|
|
||||||
Result.success(library)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
// Got cancelled, propagate upwards to top-level co-routine.
|
|
||||||
logD("Loading routine was cancelled")
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Music loading process failed due to something we have not handled.
|
|
||||||
logE("Music indexing failed")
|
|
||||||
logE(e.stackTraceToString())
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
|
||||||
emitCompletion(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request that the music library should be reloaded. This should be used by components that do
|
* Request that the music library should be reloaded. This should be used by components that do
|
||||||
* not manage the indexing process in order to signal that the [Controller] should call [index]
|
* not manage the indexing process in order to signal that the [Indexer.Controller] should call
|
||||||
* eventually.
|
* [index] eventually.
|
||||||
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
|
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
|
||||||
* [Controller].
|
* [Indexer.Controller].
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun requestReindex(withCache: Boolean)
|
||||||
fun requestReindex(withCache: Boolean) {
|
|
||||||
logD("Requesting reindex")
|
|
||||||
controller?.onStartIndexing(withCache)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the current loading state to signal that the instance is not loading. This should be
|
* Reset the current loading state to signal that the instance is not loading. This should be
|
||||||
* called by [Controller] after it's indexing co-routine was cancelled.
|
* called by [Controller] after it's indexing co-routine was cancelled.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
fun reset()
|
||||||
fun reset() {
|
|
||||||
logD("Cancelling last job")
|
|
||||||
emitIndexing(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal implementation of the music loading process.
|
|
||||||
* @param context [Context] required to load music.
|
|
||||||
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
|
|
||||||
* be written, but no cache entries will be loaded into the new library.
|
|
||||||
* @return A newly-loaded [Library].
|
|
||||||
* @throws NoPermissionException If [PERMISSION_READ_AUDIO] was not granted.
|
|
||||||
* @throws NoMusicException If no music was found on the device.
|
|
||||||
*/
|
|
||||||
private suspend fun indexImpl(context: Context, withCache: Boolean): Library {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
|
|
||||||
PackageManager.PERMISSION_DENIED) {
|
|
||||||
// No permissions, signal that we can't do anything.
|
|
||||||
throw NoPermissionException()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the chain of extractors. Each extractor builds on the previous and
|
|
||||||
// enables version-specific features in order to create the best possible music
|
|
||||||
// experience.
|
|
||||||
val cacheExtractor = CacheExtractor.from(context, withCache)
|
|
||||||
val mediaStoreExtractor = MediaStoreExtractor.from(context, cacheExtractor)
|
|
||||||
val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor)
|
|
||||||
val rawSongs = loadRawSongs(metadataExtractor).ifEmpty { throw NoMusicException() }
|
|
||||||
// Build the rest of the music library from the song list. This is much more powerful
|
|
||||||
// and reliable compared to using MediaStore to obtain grouping information.
|
|
||||||
val buildStart = System.currentTimeMillis()
|
|
||||||
val library = Library(rawSongs, MusicSettings.from(context))
|
|
||||||
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
|
||||||
return library
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a list of [Song]s from the device.
|
|
||||||
* @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw]
|
|
||||||
* instances.
|
|
||||||
* @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked
|
|
||||||
* with parent [Album], [Artist], and [Genre] items in order to be usable.
|
|
||||||
*/
|
|
||||||
private suspend fun loadRawSongs(metadataExtractor: MetadataExtractor): List<Song.Raw> {
|
|
||||||
logD("Starting indexing process")
|
|
||||||
val start = System.currentTimeMillis()
|
|
||||||
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
|
|
||||||
// how long a media database query will take.
|
|
||||||
emitIndexing(Indexing.Indeterminate)
|
|
||||||
val total = metadataExtractor.init()
|
|
||||||
yield()
|
|
||||||
|
|
||||||
// Note: We use a set here so we can eliminate song duplicates.
|
|
||||||
val rawSongs = mutableListOf<Song.Raw>()
|
|
||||||
metadataExtractor.extract().collect { rawSong ->
|
|
||||||
rawSongs.add(rawSong)
|
|
||||||
// Now we can signal a defined progress by showing how many songs we have
|
|
||||||
// loaded, and the projected amount of songs we found in the library
|
|
||||||
// (obtained by the extractors)
|
|
||||||
yield()
|
|
||||||
emitIndexing(Indexing.Songs(rawSongs.size, total))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finalize the extractors with the songs we have now loaded. There is no ETA
|
|
||||||
// on this process, so go back to an indeterminate state.
|
|
||||||
emitIndexing(Indexing.Indeterminate)
|
|
||||||
metadataExtractor.finalize(rawSongs)
|
|
||||||
logD(
|
|
||||||
"Successfully loaded ${rawSongs.size} raw songs in ${System.currentTimeMillis() - start}ms")
|
|
||||||
return rawSongs
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a new [State.Indexing] state. This can be used to signal the current state of the music
|
|
||||||
* loading process to external code. Assumes that the callee has already checked if they have
|
|
||||||
* not been canceled and thus have the ability to emit a new state.
|
|
||||||
* @param indexing The new [Indexing] state to emit, or null if no loading process is occurring.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
private fun emitIndexing(indexing: Indexing?) {
|
|
||||||
indexingState = indexing
|
|
||||||
// If we have canceled the loading process, we want to revert to a previous completion
|
|
||||||
// whenever possible to prevent state inconsistency.
|
|
||||||
val state =
|
|
||||||
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
|
|
||||||
controller?.onIndexerStateChanged(state)
|
|
||||||
listener?.onIndexerStateChanged(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Emit a new [State.Complete] state. This can be used to signal the completion of the music
|
|
||||||
* loading process to external code. Will check if the callee has not been canceled and thus has
|
|
||||||
* the ability to emit a new state
|
|
||||||
* @param result The new [Result] to emit, representing the outcome of the music loading
|
|
||||||
* process.
|
|
||||||
*/
|
|
||||||
private suspend fun emitCompletion(result: Result<Library>) {
|
|
||||||
yield()
|
|
||||||
// Swap to the Main thread so that downstream callbacks don't crash from being on
|
|
||||||
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
synchronized(this) {
|
|
||||||
// Do not check for redundancy here, as we actually need to notify a switch
|
|
||||||
// from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete.
|
|
||||||
lastResponse = result
|
|
||||||
indexingState = null
|
|
||||||
// Signal that the music loading process has been completed.
|
|
||||||
val state = State.Complete(result)
|
|
||||||
controller?.onIndexerStateChanged(state)
|
|
||||||
listener?.onIndexerStateChanged(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Represents the current state of [Indexer]. */
|
/** Represents the current state of [Indexer]. */
|
||||||
sealed class State {
|
sealed class State {
|
||||||
|
@ -394,17 +209,209 @@ class Indexer private constructor() {
|
||||||
* Get a singleton instance.
|
* Get a singleton instance.
|
||||||
* @return The (possibly newly-created) singleton instance.
|
* @return The (possibly newly-created) singleton instance.
|
||||||
*/
|
*/
|
||||||
fun getInstance(): Indexer {
|
fun get(): Indexer {
|
||||||
val currentInstance = INSTANCE
|
val currentInstance = INSTANCE
|
||||||
if (currentInstance != null) {
|
if (currentInstance != null) {
|
||||||
return currentInstance
|
return currentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
val newInstance = Indexer()
|
val newInstance = RealIndexer()
|
||||||
INSTANCE = newInstance
|
INSTANCE = newInstance
|
||||||
return newInstance
|
return newInstance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class RealIndexer : Indexer {
|
||||||
|
@Volatile private var lastResponse: Result<Library>? = null
|
||||||
|
@Volatile private var indexingState: Indexer.Indexing? = null
|
||||||
|
@Volatile private var controller: Indexer.Controller? = null
|
||||||
|
@Volatile private var listener: Indexer.Listener? = null
|
||||||
|
|
||||||
|
override val isIndexing: Boolean
|
||||||
|
get() = indexingState != null
|
||||||
|
|
||||||
|
override val isIndeterminate: Boolean
|
||||||
|
get() = lastResponse == null && indexingState == null
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun registerController(controller: Indexer.Controller) {
|
||||||
|
if (BuildConfig.DEBUG && this.controller != null) {
|
||||||
|
logW("Controller is already registered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the controller with the current state.
|
||||||
|
val currentState =
|
||||||
|
indexingState?.let { Indexer.State.Indexing(it) }
|
||||||
|
?: lastResponse?.let { Indexer.State.Complete(it) }
|
||||||
|
controller.onIndexerStateChanged(currentState)
|
||||||
|
this.controller = controller
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun unregisterController(controller: Indexer.Controller) {
|
||||||
|
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||||
|
logW("Given controller did not match current controller")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.controller = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun registerListener(listener: Indexer.Listener) {
|
||||||
|
if (BuildConfig.DEBUG && this.listener != null) {
|
||||||
|
logW("Listener is already registered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the listener with the current state.
|
||||||
|
val currentState =
|
||||||
|
indexingState?.let { Indexer.State.Indexing(it) }
|
||||||
|
?: lastResponse?.let { Indexer.State.Complete(it) }
|
||||||
|
listener.onIndexerStateChanged(currentState)
|
||||||
|
this.listener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun unregisterListener(listener: Indexer.Listener) {
|
||||||
|
if (BuildConfig.DEBUG && this.listener !== listener) {
|
||||||
|
logW("Given controller did not match current controller")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun index(context: Context, withCache: Boolean) {
|
||||||
|
val result =
|
||||||
|
try {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
val library = indexImpl(context, withCache)
|
||||||
|
logD(
|
||||||
|
"Music indexing completed successfully in " +
|
||||||
|
"${System.currentTimeMillis() - start}ms")
|
||||||
|
Result.success(library)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// Got cancelled, propagate upwards to top-level co-routine.
|
||||||
|
logD("Loading routine was cancelled")
|
||||||
|
throw e
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Music loading process failed due to something we have not handled.
|
||||||
|
logE("Music indexing failed")
|
||||||
|
logE(e.stackTraceToString())
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
emitCompletion(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun requestReindex(withCache: Boolean) {
|
||||||
|
logD("Requesting reindex")
|
||||||
|
controller?.onStartIndexing(withCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
override fun reset() {
|
||||||
|
logD("Cancelling last job")
|
||||||
|
emitIndexing(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun indexImpl(context: Context, withCache: Boolean): Library {
|
||||||
|
if (ContextCompat.checkSelfPermission(context, Indexer.PERMISSION_READ_AUDIO) ==
|
||||||
|
PackageManager.PERMISSION_DENIED) {
|
||||||
|
// No permissions, signal that we can't do anything.
|
||||||
|
throw Indexer.NoPermissionException()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the chain of extractors. Each extractor builds on the previous and
|
||||||
|
// enables version-specific features in order to create the best possible music
|
||||||
|
// experience.
|
||||||
|
val cacheExtractor = CacheExtractor.from(context, withCache)
|
||||||
|
val mediaStoreExtractor = MediaStoreExtractor.from(context, cacheExtractor)
|
||||||
|
val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor)
|
||||||
|
val rawSongs = loadRawSongs(metadataExtractor).ifEmpty { throw Indexer.NoMusicException() }
|
||||||
|
// Build the rest of the music library from the song list. This is much more powerful
|
||||||
|
// and reliable compared to using MediaStore to obtain grouping information.
|
||||||
|
val buildStart = System.currentTimeMillis()
|
||||||
|
val library = Library(rawSongs, MusicSettings.from(context))
|
||||||
|
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
|
||||||
|
return library
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadRawSongs(metadataExtractor: MetadataExtractor): List<Song.Raw> {
|
||||||
|
logD("Starting indexing process")
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
// Start initializing the extractors. Use an indeterminate state, as there is no ETA on
|
||||||
|
// how long a media database query will take.
|
||||||
|
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||||
|
val total = metadataExtractor.init()
|
||||||
|
yield()
|
||||||
|
|
||||||
|
// Note: We use a set here so we can eliminate song duplicates.
|
||||||
|
val rawSongs = mutableListOf<Song.Raw>()
|
||||||
|
metadataExtractor.extract().collect { rawSong ->
|
||||||
|
rawSongs.add(rawSong)
|
||||||
|
// Now we can signal a defined progress by showing how many songs we have
|
||||||
|
// loaded, and the projected amount of songs we found in the library
|
||||||
|
// (obtained by the extractors)
|
||||||
|
yield()
|
||||||
|
emitIndexing(Indexer.Indexing.Songs(rawSongs.size, total))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize the extractors with the songs we have now loaded. There is no ETA
|
||||||
|
// on this process, so go back to an indeterminate state.
|
||||||
|
emitIndexing(Indexer.Indexing.Indeterminate)
|
||||||
|
metadataExtractor.finalize(rawSongs)
|
||||||
|
logD(
|
||||||
|
"Successfully loaded ${rawSongs.size} raw songs in ${System.currentTimeMillis() - start}ms")
|
||||||
|
return rawSongs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a new [Indexer.State.Indexing] state. This can be used to signal the current state of
|
||||||
|
* the music loading process to external code. Assumes that the callee has already checked if
|
||||||
|
* they have not been canceled and thus have the ability to emit a new state.
|
||||||
|
* @param indexing The new [Indexer.Indexing] state to emit, or null if no loading process is
|
||||||
|
* occurring.
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
private fun emitIndexing(indexing: Indexer.Indexing?) {
|
||||||
|
indexingState = indexing
|
||||||
|
// If we have canceled the loading process, we want to revert to a previous completion
|
||||||
|
// whenever possible to prevent state inconsistency.
|
||||||
|
val state =
|
||||||
|
indexingState?.let { Indexer.State.Indexing(it) }
|
||||||
|
?: lastResponse?.let { Indexer.State.Complete(it) }
|
||||||
|
controller?.onIndexerStateChanged(state)
|
||||||
|
listener?.onIndexerStateChanged(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a new [Indexer.State.Complete] state. This can be used to signal the completion of the
|
||||||
|
* music loading process to external code. Will check if the callee has not been canceled and
|
||||||
|
* thus has the ability to emit a new state
|
||||||
|
* @param result The new [Result] to emit, representing the outcome of the music loading
|
||||||
|
* process.
|
||||||
|
*/
|
||||||
|
private suspend fun emitCompletion(result: Result<Library>) {
|
||||||
|
yield()
|
||||||
|
// Swap to the Main thread so that downstream callbacks don't crash from being on
|
||||||
|
// a background thread. Does not occur in emitIndexing due to efficiency reasons.
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
synchronized(this) {
|
||||||
|
// Do not check for redundancy here, as we actually need to notify a switch
|
||||||
|
// from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete.
|
||||||
|
lastResponse = result
|
||||||
|
indexingState = null
|
||||||
|
// Signal that the music loading process has been completed.
|
||||||
|
val state = Indexer.State.Complete(result)
|
||||||
|
controller?.onIndexerStateChanged(state)
|
||||||
|
listener?.onIndexerStateChanged(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
|
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.get()
|
||||||
private val musicRepository = MusicRepository.get()
|
private val musicRepository = MusicRepository.get()
|
||||||
private val playbackManager = PlaybackStateManager.get()
|
private val playbackManager = PlaybackStateManager.get()
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
|
|
|
@ -37,8 +37,11 @@ import org.oxycblt.auxio.music.Song
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
interface Queue {
|
interface Queue {
|
||||||
|
/** The index of the currently playing [Song] in the current mapping. */
|
||||||
val index: Int
|
val index: Int
|
||||||
|
/** The currently playing [Song]. */
|
||||||
val currentSong: Song?
|
val currentSong: Song?
|
||||||
|
/** Whether this queue is shuffled. */
|
||||||
val isShuffled: Boolean
|
val isShuffled: Boolean
|
||||||
/**
|
/**
|
||||||
* Resolve this queue into a more conventional list of [Song]s.
|
* Resolve this queue into a more conventional list of [Song]s.
|
||||||
|
@ -96,25 +99,18 @@ class EditableQueue : Queue {
|
||||||
@Volatile private var heap = mutableListOf<Song>()
|
@Volatile private var heap = mutableListOf<Song>()
|
||||||
@Volatile private var orderedMapping = mutableListOf<Int>()
|
@Volatile private var orderedMapping = mutableListOf<Int>()
|
||||||
@Volatile private var shuffledMapping = mutableListOf<Int>()
|
@Volatile private var shuffledMapping = mutableListOf<Int>()
|
||||||
/** The index of the currently playing [Song] in the current mapping. */
|
|
||||||
@Volatile
|
@Volatile
|
||||||
override var index = -1
|
override var index = -1
|
||||||
private set
|
private set
|
||||||
/** The currently playing [Song]. */
|
|
||||||
override val currentSong: Song?
|
override val currentSong: Song?
|
||||||
get() =
|
get() =
|
||||||
shuffledMapping
|
shuffledMapping
|
||||||
.ifEmpty { orderedMapping.ifEmpty { null } }
|
.ifEmpty { orderedMapping.ifEmpty { null } }
|
||||||
?.getOrNull(index)
|
?.getOrNull(index)
|
||||||
?.let(heap::get)
|
?.let(heap::get)
|
||||||
/** Whether this queue is shuffled. */
|
|
||||||
override val isShuffled: Boolean
|
override val isShuffled: Boolean
|
||||||
get() = shuffledMapping.isNotEmpty()
|
get() = shuffledMapping.isNotEmpty()
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve this queue into a more conventional list of [Song]s.
|
|
||||||
* @return A list of [Song] corresponding to the current queue mapping.
|
|
||||||
*/
|
|
||||||
override fun resolve() =
|
override fun resolve() =
|
||||||
if (currentSong != null) {
|
if (currentSong != null) {
|
||||||
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
|
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
|
||||||
|
|
Loading…
Reference in a new issue