music: re-add music browsing
This commit is contained in:
parent
69070e7b13
commit
b1e871c6e1
5 changed files with 383 additions and 326 deletions
|
@ -23,7 +23,6 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
@ -37,16 +36,16 @@ import org.oxycblt.auxio.music.service.MusicServiceFragment
|
||||||
import org.oxycblt.auxio.playback.service.PlaybackServiceFragment
|
import org.oxycblt.auxio.playback.service.PlaybackServiceFragment
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
class AuxioService : MediaBrowserServiceCompat(), ForegroundListener, MusicServiceFragment.Invalidator {
|
||||||
@Inject lateinit var mediaSessionFragment: PlaybackServiceFragment
|
@Inject lateinit var playbackFragment: PlaybackServiceFragment
|
||||||
|
|
||||||
@Inject lateinit var indexingFragment: MusicServiceFragment
|
@Inject lateinit var musicFragment: MusicServiceFragment
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
setSessionToken(mediaSessionFragment.attach(this))
|
sessionToken = playbackFragment.attach(this)
|
||||||
indexingFragment.attach(this)
|
musicFragment.attach(this, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
@ -63,26 +62,31 @@ class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
||||||
|
|
||||||
private fun onHandleForeground(intent: Intent?) {
|
private fun onHandleForeground(intent: Intent?) {
|
||||||
val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1
|
val startId = intent?.getIntExtra(INTENT_KEY_START_ID, -1) ?: -1
|
||||||
indexingFragment.start()
|
musicFragment.start()
|
||||||
mediaSessionFragment.start(startId)
|
playbackFragment.start(startId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
mediaSessionFragment.handleTaskRemoved()
|
playbackFragment.handleTaskRemoved()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
indexingFragment.release()
|
musicFragment.release()
|
||||||
mediaSessionFragment.release()
|
playbackFragment.release()
|
||||||
|
sessionToken = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onGetRoot(
|
override fun onGetRoot(
|
||||||
clientPackageName: String,
|
clientPackageName: String,
|
||||||
clientUid: Int,
|
clientUid: Int,
|
||||||
rootHints: Bundle?
|
rootHints: Bundle?
|
||||||
): BrowserRoot? = null
|
): BrowserRoot = musicFragment.getRoot()
|
||||||
|
|
||||||
|
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
|
||||||
|
musicFragment.getItem(itemId, result)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLoadChildren(
|
override fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String,
|
||||||
|
@ -93,13 +97,8 @@ class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
||||||
parentId: String,
|
parentId: String,
|
||||||
result: Result<MutableList<MediaItem>>,
|
result: Result<MutableList<MediaItem>>,
|
||||||
options: Bundle
|
options: Bundle
|
||||||
) {
|
) = musicFragment.getChildren(parentId, result)
|
||||||
super.onLoadChildren(parentId, result, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadItem(itemId: String, result: Result<MediaItem>) {
|
|
||||||
super.onLoadItem(itemId, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSearch(
|
override fun onSearch(
|
||||||
query: String,
|
query: String,
|
||||||
|
@ -120,7 +119,7 @@ class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateForeground(change: ForegroundListener.Change) {
|
override fun updateForeground(change: ForegroundListener.Change) {
|
||||||
val mediaNotification = mediaSessionFragment.notification
|
val mediaNotification = playbackFragment.notification
|
||||||
if (mediaNotification != null) {
|
if (mediaNotification != null) {
|
||||||
if (change == ForegroundListener.Change.MEDIA_SESSION) {
|
if (change == ForegroundListener.Change.MEDIA_SESSION) {
|
||||||
startForeground(mediaNotification.code, mediaNotification.build())
|
startForeground(mediaNotification.code, mediaNotification.build())
|
||||||
|
@ -128,7 +127,7 @@ class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
||||||
// Nothing changed, but don't show anything music related since we can always
|
// Nothing changed, but don't show anything music related since we can always
|
||||||
// index during playback.
|
// index during playback.
|
||||||
} else {
|
} else {
|
||||||
indexingFragment.createNotification {
|
musicFragment.createNotification {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
startForeground(it.code, it.build())
|
startForeground(it.code, it.build())
|
||||||
isForeground = true
|
isForeground = true
|
||||||
|
@ -140,6 +139,10 @@ class AuxioService : MediaBrowserServiceCompat(), ForegroundListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun invalidateMusic(mediaId: String) {
|
||||||
|
notifyChildrenChanged(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var isForeground = false
|
var isForeground = false
|
||||||
private set
|
private set
|
||||||
|
|
146
app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt
Normal file
146
app/src/main/java/org/oxycblt/auxio/music/service/Indexer.kt
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 Auxio Project
|
||||||
|
* IndexerServiceFragment.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.PowerManager
|
||||||
|
import coil.ImageLoader
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
|
import org.oxycblt.auxio.ForegroundListener
|
||||||
|
import org.oxycblt.auxio.ForegroundServiceNotification
|
||||||
|
import org.oxycblt.auxio.music.IndexingState
|
||||||
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
class Indexer
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@ApplicationContext override val workerContext: Context,
|
||||||
|
private val playbackManager: PlaybackStateManager,
|
||||||
|
private val musicRepository: MusicRepository,
|
||||||
|
private val musicSettings: MusicSettings,
|
||||||
|
private val imageLoader: ImageLoader
|
||||||
|
) :
|
||||||
|
MusicRepository.IndexingWorker,
|
||||||
|
MusicRepository.IndexingListener,
|
||||||
|
MusicRepository.UpdateListener,
|
||||||
|
MusicSettings.Listener {
|
||||||
|
private val indexJob = Job()
|
||||||
|
private val indexScope = CoroutineScope(indexJob + Dispatchers.IO)
|
||||||
|
private var currentIndexJob: Job? = null
|
||||||
|
private var foregroundListener: ForegroundListener? = null
|
||||||
|
private val wakeLock =
|
||||||
|
workerContext
|
||||||
|
.getSystemServiceCompat(PowerManager::class)
|
||||||
|
.newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
|
||||||
|
|
||||||
|
fun attach(listener: ForegroundListener) {
|
||||||
|
foregroundListener = listener
|
||||||
|
musicSettings.registerListener(this)
|
||||||
|
musicRepository.addUpdateListener(this)
|
||||||
|
musicRepository.addIndexingListener(this)
|
||||||
|
musicRepository.registerWorker(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
musicSettings.registerListener(this)
|
||||||
|
musicRepository.addIndexingListener(this)
|
||||||
|
musicRepository.addUpdateListener(this)
|
||||||
|
musicRepository.removeIndexingListener(this)
|
||||||
|
foregroundListener = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requestIndex(withCache: Boolean) {
|
||||||
|
logD("Starting new indexing job (previous=${currentIndexJob?.hashCode()})")
|
||||||
|
// Cancel the previous music loading job.
|
||||||
|
currentIndexJob?.cancel()
|
||||||
|
// Start a new music loading job on a co-routine.
|
||||||
|
currentIndexJob = musicRepository.index(this, withCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val scope = indexScope
|
||||||
|
|
||||||
|
override fun onIndexingStateChanged() {
|
||||||
|
foregroundListener?.updateForeground(ForegroundListener.Change.INDEXER)
|
||||||
|
val state = musicRepository.indexingState
|
||||||
|
if (state is IndexingState.Indexing) {
|
||||||
|
wakeLock.acquireSafe()
|
||||||
|
} else {
|
||||||
|
wakeLock.releaseSafe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
|
logD("Music changed, updating shared objects")
|
||||||
|
// Wipe possibly-invalidated outdated covers
|
||||||
|
imageLoader.memoryCache?.clear()
|
||||||
|
// Clear invalid models from PlaybackStateManager. This is not connected
|
||||||
|
// to a listener as it is bad practice for a shared object to attach to
|
||||||
|
// the listener system of another.
|
||||||
|
playbackManager.toSavedState()?.let { savedState ->
|
||||||
|
playbackManager.applySavedState(
|
||||||
|
savedState.copy(
|
||||||
|
heap =
|
||||||
|
savedState.heap.map { song ->
|
||||||
|
song?.let { deviceLibrary.findSong(it.uid) }
|
||||||
|
}),
|
||||||
|
true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIndexingSettingChanged() {
|
||||||
|
super.onIndexingSettingChanged()
|
||||||
|
musicRepository.requestIndex(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. */
|
||||||
|
private fun PowerManager.WakeLock.acquireSafe() {
|
||||||
|
// Avoid unnecessary acquire calls.
|
||||||
|
if (!wakeLock.isHeld) {
|
||||||
|
logD("Acquiring wake lock")
|
||||||
|
// Time out after a minute, which is the average music loading time for a medium-sized
|
||||||
|
// library. If this runs out, we will re-request the lock, and if music loading is
|
||||||
|
// shorter than the timeout, it will be released early.
|
||||||
|
acquire(WAKELOCK_TIMEOUT_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. */
|
||||||
|
private fun PowerManager.WakeLock.releaseSafe() {
|
||||||
|
// Avoid unnecessary release calls.
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
logD("Releasing wake lock")
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val WAKELOCK_TIMEOUT_MS = 60 * 1000L
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,16 +19,13 @@
|
||||||
package org.oxycblt.auxio.music.service
|
package org.oxycblt.auxio.music.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
import androidx.media3.common.MediaMetadata
|
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -40,8 +37,6 @@ import org.oxycblt.auxio.music.Playlist
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
import org.oxycblt.auxio.util.getPlural
|
import org.oxycblt.auxio.util.getPlural
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
enum class Category(val id: String, @StringRes val nameRes: Int, @DrawableRes val bitmapRes: Int?) {
|
enum class Category(val id: String, @StringRes val nameRes: Int, @DrawableRes val bitmapRes: Int?) {
|
||||||
ROOT("root", R.string.info_app_name, null),
|
ROOT("root", R.string.info_app_name, null),
|
||||||
|
@ -109,9 +104,15 @@ sealed interface MediaSessionUID {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typealias Sugar = Bundle.(Context) -> Unit
|
||||||
|
|
||||||
|
fun header(@StringRes nameRes: Int): Sugar = {
|
||||||
|
putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it.getString(nameRes))
|
||||||
|
}
|
||||||
|
|
||||||
fun Category.toMediaItem(context: Context): MediaItem {
|
fun Category.toMediaItem(context: Context): MediaItem {
|
||||||
// TODO: Make custom overflow menu for compat
|
// TODO: Make custom overflow menu for compat
|
||||||
val style =
|
val extras =
|
||||||
Bundle().apply {
|
Bundle().apply {
|
||||||
putInt(
|
putInt(
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
|
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM,
|
||||||
|
@ -121,32 +122,41 @@ fun Category.toMediaItem(context: Context): MediaItem {
|
||||||
val description = MediaDescriptionCompat.Builder()
|
val description = MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
.setTitle(context.getString(nameRes))
|
.setTitle(context.getString(nameRes))
|
||||||
|
.setExtras(extras)
|
||||||
if (bitmapRes != null) {
|
if (bitmapRes != null) {
|
||||||
val bitmap = BitmapFactory.decodeResource(context.resources, bitmapRes)
|
val bitmap = BitmapFactory.decodeResource(context.resources, bitmapRes)
|
||||||
description.setIconBitmap(bitmap)
|
description.setIconBitmap(bitmap)
|
||||||
}
|
}
|
||||||
return MediaItem(description.build(), MediaItem.FLAG_BROWSABLE)
|
return MediaItem(description.build(), MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
fun Song.toMediaItem(context: Context, parent: MusicParent?): MediaItem {
|
|
||||||
|
fun Song.toMediaItem(context: Context, parent: MusicParent? = null, vararg sugar: Sugar): MediaItem {
|
||||||
|
val mediaSessionUID =
|
||||||
|
if (parent == null) {
|
||||||
|
MediaSessionUID.SingleItem(uid)
|
||||||
|
} else {
|
||||||
|
MediaSessionUID.ChildItem(parent.uid, uid)
|
||||||
|
}
|
||||||
|
val extras = Bundle().apply { sugar.forEach { this.it(context) } }
|
||||||
|
val description = MediaDescriptionCompat.Builder()
|
||||||
|
.setMediaId(mediaSessionUID.toString())
|
||||||
|
.setTitle(name.resolve(context))
|
||||||
|
.setSubtitle(artists.resolveNames(context))
|
||||||
|
.setDescription(album.name.resolve(context))
|
||||||
|
.setIconUri(album.cover.single.mediaStoreCoverUri)
|
||||||
|
.setMediaUri(uri)
|
||||||
|
.setExtras(extras)
|
||||||
|
.build()
|
||||||
|
return MediaItem(description, MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Album.toMediaItem(context: Context, parent: MusicParent? = null, vararg sugar: Sugar): MediaItem {
|
||||||
val mediaSessionUID =
|
val mediaSessionUID =
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
MediaSessionUID.SingleItem(uid)
|
MediaSessionUID.SingleItem(uid)
|
||||||
} else {
|
} else {
|
||||||
MediaSessionUID.ChildItem(parent.uid, uid)
|
MediaSessionUID.ChildItem(parent.uid, uid)
|
||||||
}
|
}
|
||||||
val description = MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(mediaSessionUID.toString())
|
|
||||||
.setTitle(name.resolve(context))
|
|
||||||
.setSubtitle(artists.resolveNames(context))
|
|
||||||
.setDescription(album.name.resolve(context))
|
|
||||||
.setIconUri(album.cover.single.mediaStoreCoverUri)
|
|
||||||
.setMediaUri(uri)
|
|
||||||
.build()
|
|
||||||
return MediaItem(description, MediaItem.FLAG_PLAYABLE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Album.toMediaItem(context: Context): MediaItem {
|
|
||||||
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
|
||||||
val description = MediaDescriptionCompat.Builder()
|
val description = MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(mediaSessionUID.toString())
|
.setMediaId(mediaSessionUID.toString())
|
||||||
.setTitle(name.resolve(context))
|
.setTitle(name.resolve(context))
|
||||||
|
@ -156,7 +166,7 @@ fun Album.toMediaItem(context: Context): MediaItem {
|
||||||
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Artist.toMediaItem(context: Context): MediaItem {
|
fun Artist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
||||||
val counts =
|
val counts =
|
||||||
context.getString(
|
context.getString(
|
||||||
|
@ -180,7 +190,7 @@ fun Artist.toMediaItem(context: Context): MediaItem {
|
||||||
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Genre.toMediaItem(context: Context): MediaItem {
|
fun Genre.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
||||||
val counts =
|
val counts =
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
|
@ -197,7 +207,7 @@ fun Genre.toMediaItem(context: Context): MediaItem {
|
||||||
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
return MediaItem(description, MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Playlist.toMediaItem(context: Context): MediaItem {
|
fun Playlist.toMediaItem(context: Context, vararg sugar: Sugar): MediaItem {
|
||||||
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
val mediaSessionUID = MediaSessionUID.SingleItem(uid)
|
||||||
val counts =
|
val counts =
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
|
|
|
@ -45,100 +45,71 @@ import org.oxycblt.auxio.search.SearchEngine
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class MediaItemBrowser
|
class MusicBrowser
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val listSettings: ListSettings
|
private val listSettings: ListSettings
|
||||||
) : MusicRepository.UpdateListener {
|
) : MusicRepository.UpdateListener {
|
||||||
private val browserJob = Job()
|
|
||||||
private val searchScope = CoroutineScope(browserJob + Dispatchers.Default)
|
|
||||||
private val searchSubscribers = mutableMapOf<String, String>()
|
|
||||||
private val searchResults = mutableMapOf<String, Deferred<SearchEngine.Items>>()
|
|
||||||
private var invalidator: Invalidator? = null
|
|
||||||
|
|
||||||
interface Invalidator {
|
interface Invalidator {
|
||||||
fun invalidate(ids: Map<String, Int>)
|
fun invalidateMusic(ids: Set<String>)
|
||||||
|
|
||||||
fun invalidate(controller: String, query: String, itemCount: Int)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var invalidator: Invalidator? = null
|
||||||
|
|
||||||
fun attach(invalidator: Invalidator) {
|
fun attach(invalidator: Invalidator) {
|
||||||
this.invalidator = invalidator
|
this.invalidator = invalidator
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
browserJob.cancel()
|
|
||||||
invalidator = null
|
|
||||||
musicRepository.removeUpdateListener(this)
|
musicRepository.removeUpdateListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
val deviceLibrary = musicRepository.deviceLibrary
|
val deviceLibrary = musicRepository.deviceLibrary
|
||||||
var invalidateSearch = false
|
val invalidate = mutableSetOf<String>()
|
||||||
val invalidate = mutableMapOf<String, Int>()
|
|
||||||
if (changes.deviceLibrary && deviceLibrary != null) {
|
if (changes.deviceLibrary && deviceLibrary != null) {
|
||||||
MediaSessionUID.Category.DEVICE_MUSIC.forEach {
|
Category.DEVICE_MUSIC.forEach {
|
||||||
invalidate[it.toString()] = getCategorySize(it, musicRepository)
|
invalidate.add(MediaSessionUID.CategoryItem(it).toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceLibrary.albums.forEach {
|
deviceLibrary.albums.forEach {
|
||||||
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
||||||
invalidate[id] = it.songs.size
|
invalidate.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceLibrary.artists.forEach {
|
deviceLibrary.artists.forEach {
|
||||||
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
||||||
invalidate[id] = it.songs.size + it.explicitAlbums.size + it.implicitAlbums.size
|
invalidate.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceLibrary.genres.forEach {
|
deviceLibrary.genres.forEach {
|
||||||
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
||||||
invalidate[id] = it.songs.size + it.artists.size
|
invalidate.add(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidateSearch = true
|
|
||||||
}
|
}
|
||||||
val userLibrary = musicRepository.userLibrary
|
val userLibrary = musicRepository.userLibrary
|
||||||
if (changes.userLibrary && userLibrary != null) {
|
if (changes.userLibrary && userLibrary != null) {
|
||||||
MediaSessionUID.Category.USER_MUSIC.forEach {
|
Category.USER_MUSIC.forEach {
|
||||||
invalidate[it.toString()] = getCategorySize(it, musicRepository)
|
invalidate.add(MediaSessionUID.CategoryItem(it).toString())
|
||||||
}
|
}
|
||||||
userLibrary.playlists.forEach {
|
userLibrary.playlists.forEach {
|
||||||
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
val id = MediaSessionUID.SingleItem(it.uid).toString()
|
||||||
invalidate[id] = it.songs.size
|
invalidate.add(id)
|
||||||
}
|
}
|
||||||
invalidateSearch = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invalidate.isNotEmpty()) {
|
if (invalidate.isNotEmpty()) {
|
||||||
invalidator?.invalidate(invalidate)
|
invalidator?.invalidateMusic(invalidate)
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidateSearch) {
|
|
||||||
for (entry in searchResults.entries) {
|
|
||||||
searchResults[entry.key]?.cancel()
|
|
||||||
}
|
|
||||||
searchResults.clear()
|
|
||||||
|
|
||||||
for (entry in searchSubscribers.entries) {
|
|
||||||
if (searchResults[entry.value] != null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
searchResults[entry.value] = searchTo(entry.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val root: MediaItem
|
|
||||||
get() = MediaSessionUID.Category.ROOT.toMediaItem(context)
|
|
||||||
|
|
||||||
fun getItem(mediaId: String): MediaItem? {
|
fun getItem(mediaId: String): MediaItem? {
|
||||||
val music =
|
val music =
|
||||||
when (val uid = MediaSessionUID.fromString(mediaId)) {
|
when (val uid = MediaSessionUID.fromString(mediaId)) {
|
||||||
is MediaSessionUID.Category -> return uid.toMediaItem(context)
|
is MediaSessionUID.CategoryItem -> return uid.category.toMediaItem(context)
|
||||||
is MediaSessionUID.SingleItem ->
|
is MediaSessionUID.SingleItem ->
|
||||||
musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) }
|
musicRepository.find(uid.uid)?.let { musicRepository.find(it.uid) }
|
||||||
|
|
||||||
|
@ -158,15 +129,14 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getChildren(parentId: String, page: Int, pageSize: Int): List<MediaItem>? {
|
fun getChildren(parentId: String): List<MediaItem>? {
|
||||||
val deviceLibrary = musicRepository.deviceLibrary
|
val deviceLibrary = musicRepository.deviceLibrary
|
||||||
val userLibrary = musicRepository.userLibrary
|
val userLibrary = musicRepository.userLibrary
|
||||||
if (deviceLibrary == null || userLibrary == null) {
|
if (deviceLibrary == null || userLibrary == null) {
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = getMediaItemList(parentId, deviceLibrary, userLibrary) ?: return null
|
return getMediaItemList(parentId, deviceLibrary, userLibrary)
|
||||||
return items.paginate(page, pageSize)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMediaItemList(
|
private fun getMediaItemList(
|
||||||
|
@ -175,32 +145,34 @@ constructor(
|
||||||
userLibrary: UserLibrary
|
userLibrary: UserLibrary
|
||||||
): List<MediaItem>? {
|
): List<MediaItem>? {
|
||||||
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
|
return when (val mediaSessionUID = MediaSessionUID.fromString(id)) {
|
||||||
is MediaSessionUID.Category -> {
|
is MediaSessionUID.CategoryItem -> {
|
||||||
when (mediaSessionUID) {
|
when (mediaSessionUID.category) {
|
||||||
MediaSessionUID.Category.ROOT ->
|
Category.ROOT ->
|
||||||
MediaSessionUID.Category.IMPORTANT.map { it.toMediaItem(context) }
|
Category.IMPORTANT.map { it.toMediaItem(context) }
|
||||||
|
|
||||||
MediaSessionUID.Category.SONGS ->
|
Category.MORE -> TODO()
|
||||||
|
|
||||||
|
Category.SONGS ->
|
||||||
listSettings.songSort.songs(deviceLibrary.songs).map {
|
listSettings.songSort.songs(deviceLibrary.songs).map {
|
||||||
it.toMediaItem(context, null)
|
it.toMediaItem(context, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSessionUID.Category.ALBUMS ->
|
Category.ALBUMS ->
|
||||||
listSettings.albumSort.albums(deviceLibrary.albums).map {
|
listSettings.albumSort.albums(deviceLibrary.albums).map {
|
||||||
it.toMediaItem(context)
|
it.toMediaItem(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSessionUID.Category.ARTISTS ->
|
Category.ARTISTS ->
|
||||||
listSettings.artistSort.artists(deviceLibrary.artists).map {
|
listSettings.artistSort.artists(deviceLibrary.artists).map {
|
||||||
it.toMediaItem(context)
|
it.toMediaItem(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSessionUID.Category.GENRES ->
|
Category.GENRES ->
|
||||||
listSettings.genreSort.genres(deviceLibrary.genres).map {
|
listSettings.genreSort.genres(deviceLibrary.genres).map {
|
||||||
it.toMediaItem(context)
|
it.toMediaItem(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
MediaSessionUID.Category.PLAYLISTS ->
|
Category.PLAYLISTS ->
|
||||||
userLibrary.playlists.map { it.toMediaItem(context) }
|
userLibrary.playlists.map { it.toMediaItem(context) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,25 +195,25 @@ constructor(
|
||||||
return when (val item = musicRepository.find(uid)) {
|
return when (val item = musicRepository.find(uid)) {
|
||||||
is Album -> {
|
is Album -> {
|
||||||
val songs = listSettings.albumSongSort.songs(item.songs)
|
val songs = listSettings.albumSongSort.songs(item.songs)
|
||||||
songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) }
|
songs.map { it.toMediaItem(context, item, header(R.string.lbl_songs))}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Artist -> {
|
is Artist -> {
|
||||||
val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums)
|
val albums = ARTIST_ALBUMS_SORT.albums(item.explicitAlbums + item.implicitAlbums)
|
||||||
val songs = listSettings.artistSongSort.songs(item.songs)
|
val songs = listSettings.artistSongSort.songs(item.songs)
|
||||||
albums.map { it.toMediaItem(context).withHeader(R.string.lbl_albums) } +
|
albums.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) } +
|
||||||
songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) }
|
songs.map { it.toMediaItem(context, item, header(R.string.lbl_songs)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is Genre -> {
|
is Genre -> {
|
||||||
val artists = GENRE_ARTISTS_SORT.artists(item.artists)
|
val artists = GENRE_ARTISTS_SORT.artists(item.artists)
|
||||||
val songs = listSettings.genreSongSort.songs(item.songs)
|
val songs = listSettings.genreSongSort.songs(item.songs)
|
||||||
artists.map { it.toMediaItem(context).withHeader(R.string.lbl_artists) } +
|
artists.map { it.toMediaItem(context, header(R.string.lbl_songs)) } +
|
||||||
songs.map { it.toMediaItem(context, null).withHeader(R.string.lbl_songs) }
|
songs.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is Playlist -> {
|
is Playlist -> {
|
||||||
item.songs.map { it.toMediaItem(context, item).withHeader(R.string.lbl_songs) }
|
item.songs.map { it.toMediaItem(context, null, header(R.string.lbl_songs)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is Song,
|
is Song,
|
||||||
|
@ -249,121 +221,91 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MediaItem.withHeader(@StringRes res: Int): MediaItem {
|
// suspend fun prepareSearch(query: String, controller: String) {
|
||||||
val oldExtras = mediaMetadata.extras ?: Bundle()
|
// searchSubscribers[controller] = query
|
||||||
val newExtras =
|
// val existing = searchResults[query]
|
||||||
Bundle(oldExtras).apply {
|
// if (existing == null) {
|
||||||
putString(
|
// val new = searchTo(query)
|
||||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
// searchResults[query] = new
|
||||||
context.getString(res)
|
// new.await()
|
||||||
)
|
// } else {
|
||||||
}
|
// val items = existing.await()
|
||||||
return buildUpon()
|
// invalidator?.invalidate(controller, query, items.count())
|
||||||
.setMediaMetadata(mediaMetadata.buildUpon().setExtras(newExtras).build())
|
// }
|
||||||
.build()
|
// }
|
||||||
}
|
//
|
||||||
|
// suspend fun getSearchResult(
|
||||||
private fun getCategorySize(
|
// query: String,
|
||||||
category: MediaSessionUID.Category,
|
// page: Int,
|
||||||
musicRepository: MusicRepository
|
// pageSize: Int,
|
||||||
): Int {
|
// ): List<MediaItem>? {
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return 0
|
// val deferred = searchResults[query] ?: searchTo(query).also { searchResults[query] = it }
|
||||||
val userLibrary = musicRepository.userLibrary ?: return 0
|
// return deferred.await().concat().paginate(page, pageSize)
|
||||||
return when (category) {
|
// }
|
||||||
MediaSessionUID.Category.ROOT -> MediaSessionUID.Category.IMPORTANT.size
|
//
|
||||||
MediaSessionUID.Category.SONGS -> deviceLibrary.songs.size
|
// private fun SearchEngine.Items.concat(): MutableList<MediaItem> {
|
||||||
MediaSessionUID.Category.ALBUMS -> deviceLibrary.albums.size
|
// val music = mutableListOf<MediaItem>()
|
||||||
MediaSessionUID.Category.ARTISTS -> deviceLibrary.artists.size
|
// if (songs != null) {
|
||||||
MediaSessionUID.Category.GENRES -> deviceLibrary.genres.size
|
// music.addAll(songs.map { it.toMediaItem(context, null) })
|
||||||
MediaSessionUID.Category.PLAYLISTS -> userLibrary.playlists.size
|
// }
|
||||||
}
|
// if (albums != null) {
|
||||||
}
|
// music.addAll(albums.map { it.toMediaItem(context) })
|
||||||
|
// }
|
||||||
suspend fun prepareSearch(query: String, controller: ControllerInfo) {
|
// if (artists != null) {
|
||||||
searchSubscribers[controller] = query
|
// music.addAll(artists.map { it.toMediaItem(context) })
|
||||||
val existing = searchResults[query]
|
// }
|
||||||
if (existing == null) {
|
// if (genres != null) {
|
||||||
val new = searchTo(query)
|
// music.addAll(genres.map { it.toMediaItem(context) })
|
||||||
searchResults[query] = new
|
// }
|
||||||
new.await()
|
// if (playlists != null) {
|
||||||
} else {
|
// music.addAll(playlists.map { it.toMediaItem(context) })
|
||||||
val items = existing.await()
|
// }
|
||||||
invalidator?.invalidate(controller, query, items.count())
|
// return music
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// private fun SearchEngine.Items.count(): Int {
|
||||||
suspend fun getSearchResult(
|
// var count = 0
|
||||||
query: String,
|
// if (songs != null) {
|
||||||
page: Int,
|
// count += songs.size
|
||||||
pageSize: Int,
|
// }
|
||||||
): List<MediaItem>? {
|
// if (albums != null) {
|
||||||
val deferred = searchResults[query] ?: searchTo(query).also { searchResults[query] = it }
|
// count += albums.size
|
||||||
return deferred.await().concat().paginate(page, pageSize)
|
// }
|
||||||
}
|
// if (artists != null) {
|
||||||
|
// count += artists.size
|
||||||
private fun SearchEngine.Items.concat(): MutableList<MediaItem> {
|
// }
|
||||||
val music = mutableListOf<MediaItem>()
|
// if (genres != null) {
|
||||||
if (songs != null) {
|
// count += genres.size
|
||||||
music.addAll(songs.map { it.toMediaItem(context, null) })
|
// }
|
||||||
}
|
// if (playlists != null) {
|
||||||
if (albums != null) {
|
// count += playlists.size
|
||||||
music.addAll(albums.map { it.toMediaItem(context) })
|
// }
|
||||||
}
|
// return count
|
||||||
if (artists != null) {
|
// }
|
||||||
music.addAll(artists.map { it.toMediaItem(context) })
|
//
|
||||||
}
|
// private fun searchTo(query: String) =
|
||||||
if (genres != null) {
|
// searchScope.async {
|
||||||
music.addAll(genres.map { it.toMediaItem(context) })
|
// if (query.isEmpty()) {
|
||||||
}
|
// return@async SearchEngine.Items()
|
||||||
if (playlists != null) {
|
// }
|
||||||
music.addAll(playlists.map { it.toMediaItem(context) })
|
// val deviceLibrary = musicRepository.deviceLibrary ?: return@async SearchEngine.Items()
|
||||||
}
|
// val userLibrary = musicRepository.userLibrary ?: return@async SearchEngine.Items()
|
||||||
return music
|
// val items =
|
||||||
}
|
// SearchEngine.Items(
|
||||||
|
// deviceLibrary.songs,
|
||||||
private fun SearchEngine.Items.count(): Int {
|
// deviceLibrary.albums,
|
||||||
var count = 0
|
// deviceLibrary.artists,
|
||||||
if (songs != null) {
|
// deviceLibrary.genres,
|
||||||
count += songs.size
|
// userLibrary.playlists
|
||||||
}
|
// )
|
||||||
if (albums != null) {
|
// val results = searchEngine.search(items, query)
|
||||||
count += albums.size
|
// for (entry in searchSubscribers.entries) {
|
||||||
}
|
// if (entry.value == query) {
|
||||||
if (artists != null) {
|
// invalidator?.invalidate(entry.key, query, results.count())
|
||||||
count += artists.size
|
// }
|
||||||
}
|
// }
|
||||||
if (genres != null) {
|
// results
|
||||||
count += genres.size
|
// }
|
||||||
}
|
|
||||||
if (playlists != null) {
|
|
||||||
count += playlists.size
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchTo(query: String) =
|
|
||||||
searchScope.async {
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
return@async SearchEngine.Items()
|
|
||||||
}
|
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return@async SearchEngine.Items()
|
|
||||||
val userLibrary = musicRepository.userLibrary ?: return@async SearchEngine.Items()
|
|
||||||
val items =
|
|
||||||
SearchEngine.Items(
|
|
||||||
deviceLibrary.songs,
|
|
||||||
deviceLibrary.albums,
|
|
||||||
deviceLibrary.artists,
|
|
||||||
deviceLibrary.genres,
|
|
||||||
userLibrary.playlists
|
|
||||||
)
|
|
||||||
val results = searchEngine.search(items, query)
|
|
||||||
for (entry in searchSubscribers.entries) {
|
|
||||||
if (entry.value == query) {
|
|
||||||
invalidator?.invalidate(entry.key, query, results.count())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<MediaItem>.paginate(page: Int, pageSize: Int): List<MediaItem>? {
|
private fun List<MediaItem>.paginate(page: Int, pageSize: Int): List<MediaItem>? {
|
||||||
if (page == Int.MAX_VALUE) {
|
if (page == Int.MAX_VALUE) {
|
||||||
|
|
|
@ -19,7 +19,12 @@
|
||||||
package org.oxycblt.auxio.music.service
|
package org.oxycblt.auxio.music.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
|
import androidx.media.MediaBrowserServiceCompat.BrowserRoot
|
||||||
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
|
import androidx.media.utils.MediaConstants
|
||||||
|
import android.support.v4.media.MediaBrowserCompat.MediaItem
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -35,54 +40,65 @@ import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
class MusicServiceFragment
|
class MusicServiceFragment
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
@ApplicationContext override val workerContext: Context,
|
@ApplicationContext context: Context,
|
||||||
private val playbackManager: PlaybackStateManager,
|
private val indexer: Indexer,
|
||||||
|
private val browser: MusicBrowser,
|
||||||
private val musicRepository: MusicRepository,
|
private val musicRepository: MusicRepository,
|
||||||
private val musicSettings: MusicSettings,
|
private val musicSettings: MusicSettings,
|
||||||
private val contentObserver: SystemContentObserver,
|
private val contentObserver: SystemContentObserver,
|
||||||
private val imageLoader: ImageLoader
|
) : MusicBrowser.Invalidator, MusicSettings.Listener {
|
||||||
) :
|
private val indexingNotification = IndexingNotification(context)
|
||||||
MusicRepository.IndexingWorker,
|
private val observingNotification = ObservingNotification(context)
|
||||||
MusicRepository.IndexingListener,
|
private var invalidator: Invalidator? = null
|
||||||
MusicRepository.UpdateListener,
|
|
||||||
MusicSettings.Listener {
|
|
||||||
private val indexJob = Job()
|
|
||||||
private val indexScope = CoroutineScope(indexJob + Dispatchers.IO)
|
|
||||||
private var currentIndexJob: Job? = null
|
|
||||||
private val indexingNotification = IndexingNotification(workerContext)
|
|
||||||
private val observingNotification = ObservingNotification(workerContext)
|
|
||||||
private var foregroundListener: ForegroundListener? = null
|
private var foregroundListener: ForegroundListener? = null
|
||||||
private val wakeLock =
|
|
||||||
workerContext
|
|
||||||
.getSystemServiceCompat(PowerManager::class)
|
|
||||||
.newWakeLock(
|
|
||||||
PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":IndexingComponent")
|
|
||||||
|
|
||||||
fun attach(listener: ForegroundListener) {
|
interface Invalidator {
|
||||||
foregroundListener = listener
|
fun invalidateMusic(mediaId: String)
|
||||||
musicSettings.registerListener(this)
|
}
|
||||||
musicRepository.addUpdateListener(this)
|
|
||||||
musicRepository.addIndexingListener(this)
|
fun attach(foregroundListener: ForegroundListener, invalidator: Invalidator) {
|
||||||
musicRepository.registerWorker(this)
|
this.invalidator = invalidator
|
||||||
|
indexer.attach(foregroundListener)
|
||||||
|
browser.attach(this)
|
||||||
contentObserver.attach()
|
contentObserver.attach()
|
||||||
|
musicSettings.registerListener(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
|
musicSettings.unregisterListener(this)
|
||||||
contentObserver.release()
|
contentObserver.release()
|
||||||
musicSettings.registerListener(this)
|
browser.release()
|
||||||
musicRepository.addIndexingListener(this)
|
indexer.release()
|
||||||
musicRepository.addUpdateListener(this)
|
invalidator = null
|
||||||
musicRepository.removeIndexingListener(this)
|
}
|
||||||
foregroundListener = null
|
|
||||||
|
|
||||||
|
override fun invalidateMusic(ids: Set<String>) {
|
||||||
|
ids.forEach { mediaId ->
|
||||||
|
requireNotNull(invalidator) { "Invalidator not available" }.invalidateMusic(mediaId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onObservingChanged() {
|
||||||
|
super.onObservingChanged()
|
||||||
|
// Make sure we don't override the service state with the observing
|
||||||
|
// notification if we were actively loading when the automatic rescanning
|
||||||
|
// setting changed. In such a case, the state will still be updated when
|
||||||
|
// the music loading process ends.
|
||||||
|
if (musicRepository.indexingState == null) {
|
||||||
|
logD("Not loading, updating idle session")
|
||||||
|
foregroundListener?.updateForeground(ForegroundListener.Change.INDEXER)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
if (musicRepository.indexingState == null) {
|
if (musicRepository.indexingState == null) {
|
||||||
requestIndex(true)
|
musicRepository.requestIndex(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,84 +124,24 @@ constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun requestIndex(withCache: Boolean) {
|
fun getRoot() = BrowserRoot(Category.ROOT.id, null)
|
||||||
logD("Starting new indexing job (previous=${currentIndexJob?.hashCode()})")
|
|
||||||
// Cancel the previous music loading job.
|
|
||||||
currentIndexJob?.cancel()
|
|
||||||
// Start a new music loading job on a co-routine.
|
|
||||||
currentIndexJob = musicRepository.index(this, withCache)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val scope = indexScope
|
fun getItem(mediaId: String, result: MediaBrowserServiceCompat.Result<MediaItem>) =
|
||||||
|
result.dispatch { browser.getItem(mediaId) }
|
||||||
|
|
||||||
override fun onIndexingStateChanged() {
|
fun getChildren(mediaId: String, result: MediaBrowserServiceCompat.Result<MutableList<MediaItem>>) =
|
||||||
foregroundListener?.updateForeground(ForegroundListener.Change.INDEXER)
|
result.dispatch { browser.getChildren(mediaId)?.toMutableList() }
|
||||||
val state = musicRepository.indexingState
|
|
||||||
if (state is IndexingState.Indexing) {
|
private fun <T> MediaBrowserServiceCompat.Result<T>.dispatch(body: () -> T?) {
|
||||||
wakeLock.acquireSafe()
|
try {
|
||||||
} else {
|
val result = body()
|
||||||
wakeLock.releaseSafe()
|
if (result == null) {
|
||||||
|
logW("Result is null")
|
||||||
|
}
|
||||||
|
sendResult(result)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logD("Error while dispatching: $e")
|
||||||
|
sendResult(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
|
||||||
logD("Music changed, updating shared objects")
|
|
||||||
// Wipe possibly-invalidated outdated covers
|
|
||||||
imageLoader.memoryCache?.clear()
|
|
||||||
// Clear invalid models from PlaybackStateManager. This is not connected
|
|
||||||
// to a listener as it is bad practice for a shared object to attach to
|
|
||||||
// the listener system of another.
|
|
||||||
playbackManager.toSavedState()?.let { savedState ->
|
|
||||||
playbackManager.applySavedState(
|
|
||||||
savedState.copy(
|
|
||||||
heap =
|
|
||||||
savedState.heap.map { song ->
|
|
||||||
song?.let { deviceLibrary.findSong(it.uid) }
|
|
||||||
}),
|
|
||||||
true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIndexingSettingChanged() {
|
|
||||||
super.onIndexingSettingChanged()
|
|
||||||
musicRepository.requestIndex(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onObservingChanged() {
|
|
||||||
super.onObservingChanged()
|
|
||||||
// Make sure we don't override the service state with the observing
|
|
||||||
// notification if we were actively loading when the automatic rescanning
|
|
||||||
// setting changed. In such a case, the state will still be updated when
|
|
||||||
// the music loading process ends.
|
|
||||||
if (currentIndexJob == null) {
|
|
||||||
logD("Not loading, updating idle session")
|
|
||||||
foregroundListener?.updateForeground(ForegroundListener.Change.INDEXER)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. */
|
|
||||||
private fun PowerManager.WakeLock.acquireSafe() {
|
|
||||||
// Avoid unnecessary acquire calls.
|
|
||||||
if (!wakeLock.isHeld) {
|
|
||||||
logD("Acquiring wake lock")
|
|
||||||
// Time out after a minute, which is the average music loading time for a medium-sized
|
|
||||||
// library. If this runs out, we will re-request the lock, and if music loading is
|
|
||||||
// shorter than the timeout, it will be released early.
|
|
||||||
acquire(WAKELOCK_TIMEOUT_MS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. */
|
|
||||||
private fun PowerManager.WakeLock.releaseSafe() {
|
|
||||||
// Avoid unnecessary release calls.
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
logD("Releasing wake lock")
|
|
||||||
release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val WAKELOCK_TIMEOUT_MS = 60 * 1000L
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue