music: improve indexer callback impl
Improve the indexer callback system to be more coherent and efficient. This delegates the old Callback role to a new singular Callback and Controller roles. IndexerService also handles the loading process more gracefully, reducing the amount of time music loads take.
This commit is contained in:
parent
49a964705b
commit
bbf3b1778b
12 changed files with 177 additions and 132 deletions
|
@ -25,7 +25,6 @@ import coil.request.Disposable
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility to provide bitmaps in a manner less prone to race conditions.
|
* A utility to provide bitmaps in a manner less prone to race conditions.
|
||||||
|
@ -54,7 +53,6 @@ class BitmapProvider(private val context: Context) {
|
||||||
// Increment the generation value so that previous requests are invalidated.
|
// Increment the generation value so that previous requests are invalidated.
|
||||||
// This is a second safeguard to mitigate instruction-by-instruction race conditions.
|
// This is a second safeguard to mitigate instruction-by-instruction race conditions.
|
||||||
val generation = synchronized(this) { ++currentGeneration }
|
val generation = synchronized(this) { ++currentGeneration }
|
||||||
logD("new generation for ${song.rawName}: $generation $currentGeneration")
|
|
||||||
|
|
||||||
currentRequest?.run { disposable.dispose() }
|
currentRequest?.run { disposable.dispose() }
|
||||||
currentRequest = null
|
currentRequest = null
|
||||||
|
|
|
@ -23,14 +23,14 @@ import android.content.pm.PackageManager
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
|
import org.oxycblt.auxio.music.backend.Api21MediaStoreBackend
|
||||||
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
|
import org.oxycblt.auxio.music.backend.Api29MediaStoreBackend
|
||||||
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
|
import org.oxycblt.auxio.music.backend.Api30MediaStoreBackend
|
||||||
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
|
import org.oxycblt.auxio.util.logE
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auxio's media indexer.
|
* Auxio's media indexer.
|
||||||
|
@ -58,7 +58,8 @@ class Indexer {
|
||||||
private var indexingState: Indexing? = null
|
private var indexingState: Indexing? = null
|
||||||
|
|
||||||
private var currentGeneration = 0L
|
private var currentGeneration = 0L
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private var controller: Controller? = null
|
||||||
|
private var callback: Callback? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this instance is in an indeterminate state or not, where nothing has been previously
|
* Whether this instance is in an indeterminate state or not, where nothing has been previously
|
||||||
|
@ -67,19 +68,50 @@ class Indexer {
|
||||||
val isIndeterminate: Boolean
|
val isIndeterminate: Boolean
|
||||||
get() = lastResponse == null && indexingState == null
|
get() = lastResponse == null && indexingState == null
|
||||||
|
|
||||||
fun addCallback(callback: Callback) {
|
/** Register a [Controller] with this instance. */
|
||||||
|
fun registerController(controller: Controller) {
|
||||||
|
if (BuildConfig.DEBUG && this.controller != null) {
|
||||||
|
logW("Controller is already registered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this) { this.controller = controller }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Unregister a [Controller] with this instance. */
|
||||||
|
fun unregisterController(controller: Controller) {
|
||||||
|
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||||
|
logW("Given controller did not match current controller")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized(this) { this.controller = null }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerCallback(callback: Callback) {
|
||||||
|
if (BuildConfig.DEBUG && this.callback != null) {
|
||||||
|
logW("Callback is already registered")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val currentState =
|
val currentState =
|
||||||
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
|
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
|
||||||
|
|
||||||
callback.onIndexerStateChanged(currentState)
|
callback.onIndexerStateChanged(currentState)
|
||||||
callbacks.add(callback)
|
|
||||||
|
this.callback = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeCallback(callback: Callback) {
|
fun unregisterCallback(callback: Callback) {
|
||||||
callbacks.remove(callback)
|
if (BuildConfig.DEBUG && this.callback !== callback) {
|
||||||
|
logW("Given controller did not match current controller")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun index(context: Context) {
|
fun index(context: Context) {
|
||||||
val generation = synchronized(this) { ++currentGeneration }
|
val generation = synchronized(this) { ++currentGeneration }
|
||||||
|
|
||||||
val notGranted =
|
val notGranted =
|
||||||
|
@ -94,7 +126,7 @@ class Indexer {
|
||||||
val response =
|
val response =
|
||||||
try {
|
try {
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val library = withContext(Dispatchers.IO) { indexImpl(context, generation) }
|
val library = indexImpl(context, generation)
|
||||||
if (library != null) {
|
if (library != null) {
|
||||||
logD(
|
logD(
|
||||||
"Music indexing completed successfully in " +
|
"Music indexing completed successfully in " +
|
||||||
|
@ -119,9 +151,7 @@ class Indexer {
|
||||||
*/
|
*/
|
||||||
fun requestReindex() {
|
fun requestReindex() {
|
||||||
logD("Requesting reindex")
|
logD("Requesting reindex")
|
||||||
for (callback in callbacks) {
|
controller?.onStartIndexing()
|
||||||
callback.onRequestReindex()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -152,9 +182,8 @@ class Indexer {
|
||||||
indexingState?.let { State.Indexing(it) }
|
indexingState?.let { State.Indexing(it) }
|
||||||
?: lastResponse?.let { State.Complete(it) }
|
?: lastResponse?.let { State.Complete(it) }
|
||||||
|
|
||||||
for (callback in callbacks) {
|
controller?.onIndexerStateChanged(state)
|
||||||
callback.onIndexerStateChanged(state)
|
callback?.onIndexerStateChanged(state)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,9 +197,8 @@ class Indexer {
|
||||||
indexingState = null
|
indexingState = null
|
||||||
|
|
||||||
val state = State.Complete(response)
|
val state = State.Complete(response)
|
||||||
for (callback in callbacks) {
|
controller?.onIndexerStateChanged(state)
|
||||||
callback.onIndexerStateChanged(state)
|
callback?.onIndexerStateChanged(state)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,12 +405,10 @@ class Indexer {
|
||||||
* canceled for one reason or another.
|
* canceled for one reason or another.
|
||||||
*/
|
*/
|
||||||
fun onIndexerStateChanged(state: State?)
|
fun onIndexerStateChanged(state: State?)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
interface Controller : Callback {
|
||||||
* Called when some piece of code that cannot index music requests a reindex. Callbacks that
|
fun onStartIndexing()
|
||||||
* can index music should begin reindexing at this call.
|
|
||||||
*/
|
|
||||||
fun onRequestReindex() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Represents a backend that metadata can be extracted from. */
|
/** Represents a backend that metadata can be extracted from. */
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
import org.oxycblt.auxio.IntegerTable
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
|
|
||||||
|
class IndexerNotification(private val context: Context) :
|
||||||
|
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||||
|
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
context.getString(R.string.info_indexer_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_LOW)
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmallIcon(R.drawable.ic_indexer_24)
|
||||||
|
setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||||
|
setShowWhen(false)
|
||||||
|
setSilent(true)
|
||||||
|
setContentIntent(context.newMainPendingIntent())
|
||||||
|
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
setContentTitle(context.getString(R.string.info_indexer_channel_name))
|
||||||
|
setContentText(context.getString(R.string.lbl_indexing))
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun renotify() {
|
||||||
|
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
|
||||||
|
when (indexing) {
|
||||||
|
is Indexer.Indexing.Indeterminate -> {
|
||||||
|
logD("Updating state to $indexing")
|
||||||
|
setContentText(context.getString(R.string.lbl_indexing))
|
||||||
|
setProgress(0, 0, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
is Indexer.Indexing.Songs -> {
|
||||||
|
// Only update the notification every 50 songs to prevent excessive updates.
|
||||||
|
if (indexing.current % 50 == 0) {
|
||||||
|
logD("Updating state to $indexing")
|
||||||
|
setContentText(
|
||||||
|
context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
|
||||||
|
setProgress(indexing.total, indexing.current, false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,25 +17,17 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music
|
package org.oxycblt.auxio.music
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Service] that handles the music loading process.
|
* A [Service] that handles the music loading process.
|
||||||
|
@ -48,13 +40,13 @@ import org.oxycblt.auxio.util.newMainPendingIntent
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class IndexerService : Service(), Indexer.Callback {
|
class IndexerService : Service(), Indexer.Controller {
|
||||||
private val indexer = Indexer.getInstance()
|
private val indexer = Indexer.getInstance()
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val serviceJob = Job()
|
private val serviceJob = Job()
|
||||||
private val indexScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
private val updateScope = CoroutineScope(serviceJob + Dispatchers.Main)
|
private val updateScope = CoroutineScope(serviceJob + Dispatchers.IO)
|
||||||
|
|
||||||
private var isForeground = false
|
private var isForeground = false
|
||||||
private lateinit var notification: IndexerNotification
|
private lateinit var notification: IndexerNotification
|
||||||
|
@ -64,10 +56,10 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
|
|
||||||
notification = IndexerNotification(this)
|
notification = IndexerNotification(this)
|
||||||
|
|
||||||
indexer.addCallback(this)
|
indexer.registerController(this)
|
||||||
if (musicStore.library == null && indexer.isIndeterminate) {
|
if (musicStore.library == null && indexer.isIndeterminate) {
|
||||||
logD("No library present and no previous response, indexing music now")
|
logD("No library present and no previous response, indexing music now")
|
||||||
onRequestReindex()
|
onStartIndexing()
|
||||||
}
|
}
|
||||||
|
|
||||||
logD("Service created.")
|
logD("Service created.")
|
||||||
|
@ -83,10 +75,14 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
// cancelLast actually stops foreground for us as it updates the loading state to
|
// cancelLast actually stops foreground for us as it updates the loading state to
|
||||||
// null or completed.
|
// null or completed.
|
||||||
indexer.cancelLast()
|
indexer.cancelLast()
|
||||||
indexer.removeCallback(this)
|
indexer.unregisterController(this)
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStartIndexing() {
|
||||||
|
indexScope.launch { indexer.index(this@IndexerService) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||||
when (state) {
|
when (state) {
|
||||||
is Indexer.State.Complete -> {
|
is Indexer.State.Complete -> {
|
||||||
|
@ -98,7 +94,12 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
// have not already. Only when we are done updating the library will
|
// have not already. Only when we are done updating the library will
|
||||||
// the service stop it's foreground state.
|
// the service stop it's foreground state.
|
||||||
updateScope.launch {
|
updateScope.launch {
|
||||||
musicStore.updateLibrary(state.response.library)
|
// TODO: Update PlaybackStateManager here
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
musicStore.updateLibrary(state.response.library)
|
||||||
|
}
|
||||||
|
|
||||||
stopForegroundSession()
|
stopForegroundSession()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -133,10 +134,6 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestReindex() {
|
|
||||||
indexScope.launch { indexer.index(this@IndexerService) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopForegroundSession() {
|
private fun stopForegroundSession() {
|
||||||
if (isForeground) {
|
if (isForeground) {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
|
@ -144,61 +141,3 @@ class IndexerService : Service(), Indexer.Callback {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class IndexerNotification(private val context: Context) :
|
|
||||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
|
||||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
|
||||||
|
|
||||||
init {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel =
|
|
||||||
NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
context.getString(R.string.info_indexer_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_LOW)
|
|
||||||
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSmallIcon(R.drawable.ic_indexer_24)
|
|
||||||
setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
|
||||||
setShowWhen(false)
|
|
||||||
setSilent(true)
|
|
||||||
setContentIntent(context.newMainPendingIntent())
|
|
||||||
setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
||||||
setContentTitle(context.getString(R.string.info_indexer_channel_name))
|
|
||||||
setContentText(context.getString(R.string.lbl_indexing))
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun renotify() {
|
|
||||||
notificationManager.notify(IntegerTable.INDEXER_NOTIFICATION_CODE, build())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateIndexingState(indexing: Indexer.Indexing): Boolean {
|
|
||||||
when (indexing) {
|
|
||||||
is Indexer.Indexing.Indeterminate -> {
|
|
||||||
logD("Updating state to $indexing")
|
|
||||||
setContentText(context.getString(R.string.lbl_indexing))
|
|
||||||
setProgress(0, 0, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
is Indexer.Indexing.Songs -> {
|
|
||||||
// Only update the notification every 50 songs to prevent excessive updates.
|
|
||||||
if (indexing.current % 50 == 0) {
|
|
||||||
logD("Updating state to $indexing")
|
|
||||||
setContentText(
|
|
||||||
context.getString(R.string.fmt_indexing, indexing.current, indexing.total))
|
|
||||||
setProgress(indexing.total, indexing.current, false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.INDEXER"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
|
||||||
val state: StateFlow<Indexer.State?> = _state
|
val state: StateFlow<Indexer.State?> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
indexer.addCallback(this)
|
indexer.registerCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reindex() {
|
fun reindex() {
|
||||||
|
@ -44,6 +44,6 @@ class IndexerViewModel : ViewModel(), Indexer.Callback {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
indexer.removeCallback(this)
|
indexer.unregisterCallback(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +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 kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,15 +46,11 @@ class MusicStore private constructor() {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateLibrary(newLibrary: Library?) {
|
fun updateLibrary(newLibrary: Library?) {
|
||||||
// Ensure we are on the main thread when updating the library, as callbacks expect to
|
synchronized(this) {
|
||||||
// run in an stable app thread.
|
library = newLibrary
|
||||||
withContext(Dispatchers.Main) {
|
for (callback in callbacks) {
|
||||||
synchronized(this) {
|
callback.onLibraryChanged(library)
|
||||||
library = newLibrary
|
|
||||||
for (callback in callbacks) {
|
|
||||||
callback.onLibraryChanged(library)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,12 +105,12 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove a [PlaybackStateManager.Callback] bound to this instance. */
|
/** Remove a [Callback] bound to this instance. */
|
||||||
fun removeCallback(callback: Callback) {
|
fun removeCallback(callback: Callback) {
|
||||||
callbacks.remove(callback)
|
callbacks.remove(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Register a [PlaybackStateManager.Controller] with this instance. */
|
/** Register a [Controller] with this instance. */
|
||||||
fun registerController(controller: Controller) {
|
fun registerController(controller: Controller) {
|
||||||
if (BuildConfig.DEBUG && this.controller != null) {
|
if (BuildConfig.DEBUG && this.controller != null) {
|
||||||
logW("Controller is already registered")
|
logW("Controller is already registered")
|
||||||
|
@ -129,7 +129,7 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unregister a [PlaybackStateManager.Controller] with this instance. */
|
/** Unregister a [Controller] with this instance. */
|
||||||
fun unregisterController(controller: Controller) {
|
fun unregisterController(controller: Controller) {
|
||||||
if (BuildConfig.DEBUG && this.controller !== controller) {
|
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||||
logW("Given controller did not match current controller")
|
logW("Given controller did not match current controller")
|
||||||
|
|
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_repeat"
|
android:id="@+id/widget_repeat"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_change_repeat"
|
android:contentDescription="@string/desc_change_repeat"
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_prev"
|
android:id="@+id/widget_skip_prev"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_next"
|
android:id="@+id/widget_skip_next"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
|
@ -135,7 +135,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_shuffle"
|
android:id="@+id/widget_shuffle"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_prev"
|
android:id="@+id/widget_skip_prev"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
|
@ -107,7 +107,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_next"
|
android:id="@+id/widget_skip_next"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_prev"
|
android:id="@+id/widget_skip_prev"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_next"
|
android:id="@+id/widget_skip_next"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_repeat"
|
android:id="@+id/widget_repeat"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_change_repeat"
|
android:contentDescription="@string/desc_change_repeat"
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_prev"
|
android:id="@+id/widget_skip_prev"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
|
@ -95,7 +95,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_skip_next"
|
android:id="@+id/widget_skip_next"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
|
|
||||||
<android.widget.ImageButton
|
<android.widget.ImageButton
|
||||||
android:id="@+id/widget_shuffle"
|
android:id="@+id/widget_shuffle"
|
||||||
style="@style/Widget.Auxio.PlaybackButton.AppWidget"
|
style="@style/Widget.Auxio.MaterialButton.AppWidget"
|
||||||
android:layout_width="@dimen/size_btn"
|
android:layout_width="@dimen/size_btn"
|
||||||
android:layout_height="@dimen/size_btn"
|
android:layout_height="@dimen/size_btn"
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- A variant of PlaybackButton that plays along with AppWidget restrictions. -->
|
<!-- A variant of PlaybackButton that plays along with AppWidget restrictions. -->
|
||||||
<style name="Widget.Auxio.PlaybackButton.AppWidget" parent="Widget.AppCompat.Button.Borderless">
|
<style name="Widget.Auxio.MaterialButton.AppWidget" parent="Widget.AppCompat.Button.Borderless">
|
||||||
<item name="android:minHeight">@dimen/size_btn</item>
|
<item name="android:minHeight">@dimen/size_btn</item>
|
||||||
<item name="android:background">@drawable/ui_remote_ripple</item>
|
<item name="android:background">@drawable/ui_remote_ripple</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue