music: improve indexer state management
Improve Indexer's state management by splitting up the current loading state and the last response. This is intended to resolve a bug where if the UI task and IndexerService are both killed, the Indexer state would become indeterminate and the library would not show. Resolve this by keeping track of whatever the last completed state was and falling back to it whenever the loading process is canceled.
This commit is contained in:
parent
08caa01dca
commit
09392ef381
10 changed files with 165 additions and 88 deletions
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## dev [v2.3.2, v2.4.0, or v3.0.0]
|
## dev [v2.3.2, v2.4.0, or v3.0.0]
|
||||||
|
|
||||||
|
#### Dev/Meta
|
||||||
|
- Moved music loading to a foreground service
|
||||||
|
|
||||||
## v2.3.1
|
## v2.3.1
|
||||||
|
|
||||||
#### What's Improved
|
#### What's Improved
|
||||||
|
|
|
@ -57,6 +57,7 @@ import org.oxycblt.auxio.util.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logTraceOrThrow
|
import org.oxycblt.auxio.util.logTraceOrThrow
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
import org.oxycblt.auxio.util.textSafe
|
import org.oxycblt.auxio.util.textSafe
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -260,10 +261,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
private fun handleIndexerState(state: Indexer.State?) {
|
private fun handleIndexerState(state: Indexer.State?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
if (state is Indexer.State.Complete) {
|
when (state) {
|
||||||
handleLoaderResponse(binding, state.response)
|
is Indexer.State.Complete -> handleLoaderResponse(binding, state.response)
|
||||||
} else {
|
is Indexer.State.Loading -> handleLoadingState(binding, state.loading)
|
||||||
handleLoadingState(binding, state)
|
null -> {
|
||||||
|
logW("Loading is in indeterminate state, doing nothing")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,25 +322,28 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLoadingState(binding: FragmentHomeBinding, event: Indexer.State?) {
|
private fun handleLoadingState(binding: FragmentHomeBinding, loading: Indexer.Loading) {
|
||||||
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 Indexer.State.Loading) {
|
when (loading) {
|
||||||
binding.homeLoadingStatus.textSafe =
|
is Indexer.Loading.Indeterminate -> {
|
||||||
getString(R.string.fmt_indexing, event.current, event.total)
|
|
||||||
binding.homeLoadingProgress.apply {
|
|
||||||
isIndeterminate = false
|
|
||||||
max = event.total
|
|
||||||
progress = event.current
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.homeLoadingStatus.textSafe = getString(R.string.lbl_loading)
|
binding.homeLoadingStatus.textSafe = getString(R.string.lbl_loading)
|
||||||
binding.homeLoadingProgress.isIndeterminate = true
|
binding.homeLoadingProgress.isIndeterminate = true
|
||||||
}
|
}
|
||||||
|
is Indexer.Loading.Songs -> {
|
||||||
|
binding.homeLoadingStatus.textSafe =
|
||||||
|
getString(R.string.fmt_indexing, loading.current, loading.total)
|
||||||
|
binding.homeLoadingProgress.apply {
|
||||||
|
isIndeterminate = false
|
||||||
|
max = loading.total
|
||||||
|
progress = loading.current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
|
|
@ -54,12 +54,17 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class Indexer {
|
class Indexer {
|
||||||
private var state: State? = null
|
private var lastResponse: Response? = null
|
||||||
|
private var loadingState: Loading? = null
|
||||||
|
|
||||||
private var currentGeneration: Long = 0
|
private var currentGeneration: Long = 0
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
callback.onIndexerStateChanged(state)
|
val currentState =
|
||||||
|
loadingState?.let { State.Loading(it) } ?: lastResponse?.let { State.Complete(it) }
|
||||||
|
|
||||||
|
callback.onIndexerStateChanged(currentState)
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +80,7 @@ class Indexer {
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
if (notGranted) {
|
if (notGranted) {
|
||||||
emitState(State.Complete(Response.NoPerms), generation)
|
emitCompletion(Response.NoPerms, generation)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,9 +103,13 @@ class Indexer {
|
||||||
Response.Err(e)
|
Response.Err(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
emitState(State.Complete(response), generation)
|
emitCompletion(response, generation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request that re-indexing should be done. This should be used by components that do not manage
|
||||||
|
* the indexing process to re-index music.
|
||||||
|
*/
|
||||||
fun requestReindex() {
|
fun requestReindex() {
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onRequestReindex()
|
callback.onRequestReindex()
|
||||||
|
@ -116,17 +125,41 @@ class Indexer {
|
||||||
fun cancelLast() {
|
fun cancelLast() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
currentGeneration++
|
currentGeneration++
|
||||||
emitState(null, currentGeneration)
|
emitLoading(null, currentGeneration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun emitState(newState: State?, generation: Long) {
|
private fun emitLoading(loading: Loading?, generation: Long) {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (currentGeneration == generation) {
|
if (currentGeneration != generation) {
|
||||||
state = newState
|
return
|
||||||
for (callback in callbacks) {
|
|
||||||
callback.onIndexerStateChanged(newState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadingState = loading
|
||||||
|
|
||||||
|
// If we have canceled the loading process, we want to revert to a previous completion
|
||||||
|
// whenever possible to prevent state inconsistency.
|
||||||
|
val state =
|
||||||
|
loadingState?.let { State.Loading(it) } ?: lastResponse?.let { State.Complete(it) }
|
||||||
|
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onIndexerStateChanged(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emitCompletion(response: Response, generation: Long) {
|
||||||
|
synchronized(this) {
|
||||||
|
if (currentGeneration != generation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastResponse = response
|
||||||
|
loadingState = null
|
||||||
|
|
||||||
|
val state = State.Complete(response)
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onIndexerStateChanged(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,7 +169,7 @@ class Indexer {
|
||||||
* calling this function.
|
* calling this function.
|
||||||
*/
|
*/
|
||||||
private fun indexImpl(context: Context, generation: Long): MusicStore.Library? {
|
private fun indexImpl(context: Context, generation: Long): MusicStore.Library? {
|
||||||
emitState(State.Query, generation)
|
emitLoading(Loading.Indeterminate, generation)
|
||||||
|
|
||||||
// Establish the backend to use when initially loading songs.
|
// Establish the backend to use when initially loading songs.
|
||||||
val mediaStoreBackend =
|
val mediaStoreBackend =
|
||||||
|
@ -188,9 +221,7 @@ class Indexer {
|
||||||
"Successfully queried media database " +
|
"Successfully queried media database " +
|
||||||
"in ${System.currentTimeMillis() - start}ms")
|
"in ${System.currentTimeMillis() - start}ms")
|
||||||
|
|
||||||
backend.loadSongs(context, cursor) { count, total ->
|
backend.loadSongs(context, cursor) { loading -> emitLoading(loading, generation) }
|
||||||
emitState(State.Loading(count, total), generation)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate songs to prevent (most) deformed music clones
|
// Deduplicate songs to prevent (most) deformed music clones
|
||||||
|
@ -304,11 +335,15 @@ class Indexer {
|
||||||
|
|
||||||
/** Represents the current indexer state. */
|
/** Represents the current indexer state. */
|
||||||
sealed class State {
|
sealed class State {
|
||||||
object Query : State()
|
data class Loading(val loading: Indexer.Loading) : State()
|
||||||
data class Loading(val current: Int, val total: Int) : State()
|
|
||||||
data class Complete(val response: Response) : State()
|
data class Complete(val response: Response) : State()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class Loading {
|
||||||
|
object Indeterminate : Loading()
|
||||||
|
class Songs(val current: Int, val total: Int) : Loading()
|
||||||
|
}
|
||||||
|
|
||||||
/** Represents the possible outcomes of a loading process. */
|
/** Represents the possible outcomes of a loading process. */
|
||||||
sealed class Response {
|
sealed class Response {
|
||||||
data class Ok(val library: MusicStore.Library) : Response()
|
data class Ok(val library: MusicStore.Library) : Response()
|
||||||
|
@ -318,6 +353,14 @@ class Indexer {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
/**
|
||||||
|
* Called when the current state of the Indexer changed.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Null means that no loading is going on, but no load has completed either.
|
||||||
|
* - [State.Complete] may represent a previous load, if the current loading process was
|
||||||
|
* canceled for one reason or another.
|
||||||
|
*/
|
||||||
fun onIndexerStateChanged(state: State?)
|
fun onIndexerStateChanged(state: State?)
|
||||||
fun onRequestReindex() {}
|
fun onRequestReindex() {}
|
||||||
}
|
}
|
||||||
|
@ -331,7 +374,7 @@ class Indexer {
|
||||||
fun loadSongs(
|
fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
onAddSong: (count: Int, total: Int) -> Unit
|
emitLoading: (Loading) -> Unit
|
||||||
): Collection<Song>
|
): Collection<Song>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -78,19 +78,20 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
|
||||||
stopForeground(true)
|
// cancelLast actually stops foreground for us as it updates the loading state to
|
||||||
isForeground = false
|
// null or completed.
|
||||||
|
|
||||||
indexer.removeCallback(this)
|
|
||||||
indexer.cancelLast()
|
indexer.cancelLast()
|
||||||
|
indexer.removeCallback(this)
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is Indexer.State.Complete -> {
|
is Indexer.State.Complete -> {
|
||||||
if (state.response is Indexer.Response.Ok) {
|
if (state.response is Indexer.Response.Ok && musicStore.library == null) {
|
||||||
// Load was completed successfully, so apply the new library.
|
// Load was completed successfully, so apply the new library if we
|
||||||
|
// have not already.
|
||||||
|
// TODO: Change null check for equality check [automatic rescanning]
|
||||||
musicStore.library = state.response.library
|
musicStore.library = state.response.library
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,12 +105,20 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
// database.
|
// database.
|
||||||
stopForegroundSession()
|
stopForegroundSession()
|
||||||
}
|
}
|
||||||
is Indexer.State.Query,
|
|
||||||
is Indexer.State.Loading -> {
|
is Indexer.State.Loading -> {
|
||||||
// We are loading, so we want to the enter the foreground state so that android
|
// When loading, we want to enter the foreground state so that android does
|
||||||
// does not kill our app. Note that while we would prefer to display the current
|
// not shut off the loading process. Note that while we will always post the
|
||||||
// loading progress, updates tend to be too rapid-fire for it too work well.
|
// notification when initially starting, we will not update the notification
|
||||||
startForegroundSession()
|
// unless it indicates that we have changed it.
|
||||||
|
val changed = notification.updateLoadingState(state.loading)
|
||||||
|
if (!isForeground) {
|
||||||
|
logD("Starting foreground session")
|
||||||
|
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification.build())
|
||||||
|
isForeground = true
|
||||||
|
} else if (changed) {
|
||||||
|
logD("Notification changed, re-posting notification")
|
||||||
|
notification.renotify()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
null -> {
|
null -> {
|
||||||
// Null is the indeterminate state that occurs on app startup or after
|
// Null is the indeterminate state that occurs on app startup or after
|
||||||
|
@ -124,14 +133,6 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
indexScope.launch { indexer.index(this@IndexerService) }
|
indexScope.launch { indexer.index(this@IndexerService) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startForegroundSession() {
|
|
||||||
if (!isForeground) {
|
|
||||||
logD("Starting foreground service")
|
|
||||||
startForeground(IntegerTable.INDEXER_NOTIFICATION_CODE, notification.build())
|
|
||||||
isForeground = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopForegroundSession() {
|
private fun stopForegroundSession() {
|
||||||
if (isForeground) {
|
if (isForeground) {
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
|
@ -140,7 +141,7 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class IndexerNotification(context: Context) :
|
private class IndexerNotification(private val context: Context) :
|
||||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||||
|
|
||||||
|
@ -166,6 +167,31 @@ private class IndexerNotification(context: Context) :
|
||||||
setProgress(0, 0, true)
|
setProgress(0, 0, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun renotify() {
|
||||||
|
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLoadingState(loading: Indexer.Loading): Boolean {
|
||||||
|
when (loading) {
|
||||||
|
is Indexer.Loading.Indeterminate -> {
|
||||||
|
setContentText(context.getString(R.string.lbl_loading))
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
is Indexer.Loading.Songs -> {
|
||||||
|
// Only update the notification every 50 songs to prevent excessive updates.
|
||||||
|
if (loading.current % 50 == 0) {
|
||||||
|
setContentText(
|
||||||
|
context.getString(R.string.fmt_indexing, loading.current, loading.total))
|
||||||
|
setProgress(loading.total, loading.current, false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,6 @@ 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.backend.id3v2GenreName
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ package org.oxycblt.auxio.music
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import org.oxycblt.auxio.music.backend.useQuery
|
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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.backend
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.ContentUris
|
import android.content.ContentUris
|
||||||
|
@ -98,27 +98,27 @@ val String.withoutArticle: String
|
||||||
*/
|
*/
|
||||||
val String.id3v2GenreName: String
|
val String.id3v2GenreName: String
|
||||||
get() {
|
get() {
|
||||||
if (isDigitsOnly()) {
|
val newName =
|
||||||
// ID3v1, just parse as an integer
|
when {
|
||||||
return genreConstantTable.getOrNull(toInt()) ?: this
|
// ID3v1, should just be digits
|
||||||
}
|
isDigitsOnly() -> genreConstantTable.getOrNull(toInt())
|
||||||
|
|
||||||
if (startsWith('(') && endsWith(')')) {
|
|
||||||
// ID3v2.3/ID3v2.4, parse out the parentheses and get the integer
|
// ID3v2.3/ID3v2.4, parse out the parentheses and get the integer
|
||||||
// Any genres formatted as "(CHARS)" will be ignored.
|
// Any genres formatted as "(CHARS)" will be ignored.
|
||||||
|
startsWith('(') && endsWith(')') -> {
|
||||||
|
|
||||||
// TODO: Technically, the spec for genres is far more complex here. Perhaps we
|
// TODO: Technically, the spec for genres is far more complex here. Perhaps we
|
||||||
// should copy mutagen's implementation?
|
// should copy mutagen's implementation?
|
||||||
// https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py
|
// https://github.com/quodlibet/mutagen/blob/master/mutagen/id3/_frames.py
|
||||||
|
|
||||||
return substring(1 until lastIndex).toIntOrNull()?.run {
|
substring(1 until lastIndex).toIntOrNull()?.let {
|
||||||
genreConstantTable.getOrNull(this)
|
genreConstantTable.getOrNull(it)
|
||||||
}
|
}
|
||||||
?: this
|
}
|
||||||
|
// Current name is fine
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current name is fine.
|
return newName ?: this
|
||||||
return this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -33,10 +33,13 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
import org.oxycblt.auxio.music.Indexer
|
import org.oxycblt.auxio.music.Indexer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.audioUri
|
||||||
|
import org.oxycblt.auxio.music.iso8601year
|
||||||
|
import org.oxycblt.auxio.music.no
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [OldIndexer.Backend] that leverages ExoPlayer's metadata retrieval system to index metadata.
|
* A [Indexer.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,
|
||||||
|
@ -46,12 +49,8 @@ import org.oxycblt.auxio.util.logW
|
||||||
* pitfalls given ExoPlayer's cozy relationship with native code. However, this backend should do
|
* pitfalls given ExoPlayer's cozy relationship with native code. However, this backend should do
|
||||||
* enough to eliminate such issues.
|
* enough to eliminate such issues.
|
||||||
*
|
*
|
||||||
* TODO: This class is currently not used, as there are a number of technical improvements that must
|
|
||||||
* be made first before it can be integrated.
|
|
||||||
*
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
@Suppress("UNUSED")
|
|
||||||
class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
private val runningTasks: Array<Future<TrackGroupArray>?> = arrayOfNulls(TASK_CAPACITY)
|
private val runningTasks: Array<Future<TrackGroupArray>?> = arrayOfNulls(TASK_CAPACITY)
|
||||||
|
|
||||||
|
@ -62,7 +61,7 @@ class ExoPlayerBackend(private val inner: MediaStoreBackend) : Indexer.Backend {
|
||||||
override fun loadSongs(
|
override fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
onAddSong: (count: Int, total: Int) -> Unit
|
emitLoading: (Indexer.Loading) -> 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,7 +89,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)
|
||||||
onAddSong(songs.size, cursor.count)
|
emitLoading(Indexer.Loading.Songs(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)
|
||||||
|
|
|
@ -26,7 +26,12 @@ import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import org.oxycblt.auxio.music.Indexer
|
import org.oxycblt.auxio.music.Indexer
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.albumCoverUri
|
||||||
|
import org.oxycblt.auxio.music.audioUri
|
||||||
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
import org.oxycblt.auxio.music.excluded.ExcludedDatabase
|
||||||
|
import org.oxycblt.auxio.music.no
|
||||||
|
import org.oxycblt.auxio.music.queryCursor
|
||||||
|
import org.oxycblt.auxio.music.useQuery
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -88,9 +93,8 @@ import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a [OldIndexer.Backend] that loads music from the media database ([MediaStore]). This
|
* Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is
|
||||||
* is not a fully-featured class by itself, and it's API-specific derivatives should be used
|
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
|
||||||
* instead.
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
abstract class MediaStoreBackend : Indexer.Backend {
|
abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
|
@ -130,7 +134,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
override fun loadSongs(
|
override fun loadSongs(
|
||||||
context: Context,
|
context: Context,
|
||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
onAddSong: (count: Int, total: Int) -> Unit
|
emitLoading: (Indexer.Loading) -> 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
|
||||||
|
|
|
@ -40,9 +40,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
/**
|
/**
|
||||||
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
* The ViewModel that provides a UI frontend for [PlaybackStateManager].
|
||||||
*
|
*
|
||||||
* **PLEASE Use this instead of [PlaybackStateManager], UIs are extremely volatile and this
|
* **PLEASE Use this instead of [PlaybackStateManager], UIs are extremely volatile and this provides
|
||||||
* provides an interface that properly sanitizes input and abstracts functions unlike the master
|
* an interface that properly sanitizes input and abstracts functions unlike the master class.**
|
||||||
* class.**
|
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue