playback: expose queue in mediasession
Expose the queue in the MediaSession, at least I hope. The queue is still not mutable. Don't feel comfortable implementing that until I rework the in-app queue UI.
This commit is contained in:
parent
6381815fd9
commit
0b1f0c3cda
7 changed files with 51 additions and 52 deletions
|
@ -11,6 +11,7 @@ at the cost of longer loading times
|
|||
- Added basic awareness of multi-value vorbis tags [#197, dependent on this feature]
|
||||
- Added Last Added sorting
|
||||
- Search now takes sort tags and file names in account [#184]
|
||||
- Added option to clear playback state in settings
|
||||
|
||||
#### What's Fixed
|
||||
- Fixed default material theme being used before app shows up
|
||||
|
|
|
@ -94,7 +94,7 @@ dependencies {
|
|||
// Exoplayer
|
||||
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE PRE-BUILD SCRIPT.
|
||||
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:2.18.0"
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:2.18.1"
|
||||
implementation fileTree(dir: "libs", include: ["extension-*.aar"])
|
||||
|
||||
// Image loading
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Auxio.App"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:appCategory="audio"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<activity
|
||||
|
|
|
@ -79,7 +79,7 @@ abstract class BaseFetcher : Fetcher {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? {
|
||||
private suspend fun fetchQualityCovers(context: Context, album: Album) =
|
||||
// Loading quality covers basically means to parse the file metadata ourselves
|
||||
// and then extract the cover.
|
||||
|
||||
|
@ -88,30 +88,10 @@ abstract class BaseFetcher : Fetcher {
|
|||
// for a manual parser.
|
||||
// However, Samsung seems to cripple this class as to force people to use their ad-infested
|
||||
// music app which relies on proprietary OneUI extensions instead of AOSP. That means
|
||||
// we have to have another layer of redundancy to retain quality. Thanks Samsung. Prick.
|
||||
val result = fetchAospMetadataCovers(context, album)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
|
||||
// Our next fallback is to rely on ExoPlayer's largely half-baked and undocumented
|
||||
// metadata system.
|
||||
val exoResult = fetchExoplayerCover(context, album)
|
||||
if (exoResult != null) {
|
||||
return exoResult
|
||||
}
|
||||
|
||||
// If the previous two failed, we resort to MediaStore's covers despite it literally
|
||||
// going against the point of this setting. The previous two calls are just too unreliable
|
||||
// and we can't do any filesystem traversing due to scoped storage.
|
||||
val mediaStoreResult = fetchMediaStoreCovers(context, album)
|
||||
if (mediaStoreResult != null) {
|
||||
return mediaStoreResult
|
||||
}
|
||||
|
||||
// There is no cover we could feasibly fetch. Give up.
|
||||
return null
|
||||
}
|
||||
// we have to add even more layers of redundancy to make sure we can extract a cover.
|
||||
// Thanks Samsung. Prick.
|
||||
fetchAospMetadataCovers(context, album)
|
||||
?: fetchExoplayerCover(context, album) ?: fetchMediaStoreCovers(context, album)
|
||||
|
||||
private fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? {
|
||||
MediaMetadataRetriever().apply {
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.SystemClock
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
|
@ -44,8 +45,6 @@ import org.oxycblt.auxio.util.logD
|
|||
*
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Queue functionality
|
||||
*
|
||||
* TODO: Remove the player callback once smooth seeking is implemented
|
||||
*/
|
||||
class MediaSessionComponent(
|
||||
|
@ -58,11 +57,14 @@ class MediaSessionComponent(
|
|||
PlaybackStateManager.Callback,
|
||||
Settings.Callback {
|
||||
interface Callback {
|
||||
fun onPostNotification(notification: NotificationComponent?)
|
||||
fun onPostNotification(notification: NotificationComponent?, reason: String)
|
||||
}
|
||||
|
||||
private val mediaSession =
|
||||
MediaSessionCompat(context, context.packageName).apply { isActive = true }
|
||||
MediaSessionCompat(context, context.packageName).apply {
|
||||
isActive = true
|
||||
setQueueTitle(context.getString(R.string.lbl_queue))
|
||||
}
|
||||
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val settings = Settings(context, this)
|
||||
|
@ -94,18 +96,25 @@ class MediaSessionComponent(
|
|||
|
||||
// --- PLAYBACKSTATEMANAGER CALLBACKS ---
|
||||
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||
updateMediaMetadata(playbackManager.song, parent)
|
||||
}
|
||||
|
||||
override fun onIndexMoved(index: Int) {
|
||||
updateMediaMetadata(playbackManager.song, playbackManager.parent)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
||||
updateQueue(queue)
|
||||
}
|
||||
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||
updateMediaMetadata(playbackManager.song, parent)
|
||||
updateQueue(queue)
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
|
||||
if (song == null) {
|
||||
mediaSession.setMetadata(emptyMetadata)
|
||||
callback.onPostNotification(null)
|
||||
callback.onPostNotification(null, "song update")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -163,11 +172,29 @@ class MediaSessionComponent(
|
|||
val metadata = builder.build()
|
||||
mediaSession.setMetadata(metadata)
|
||||
notification.updateMetadata(metadata)
|
||||
callback.onPostNotification(notification)
|
||||
callback.onPostNotification(notification, "song update")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateQueue(queue: List<Song>) {
|
||||
val queueItems =
|
||||
queue.mapIndexed { i, song ->
|
||||
val description =
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setMediaId(song.id.toString())
|
||||
.setTitle(song.resolveName(context))
|
||||
.setSubtitle(song.resolveIndividualArtistName(context))
|
||||
.setIconUri(song.album.coverUri) // Use lower-quality covers for speed
|
||||
.setMediaUri(song.uri)
|
||||
.build()
|
||||
|
||||
MediaSessionCompat.QueueItem(description, i.toLong())
|
||||
}
|
||||
|
||||
mediaSession.setQueue(queueItems)
|
||||
}
|
||||
|
||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||
invalidateSessionState()
|
||||
invalidateNotificationActions()
|
||||
|
@ -274,12 +301,7 @@ class MediaSessionComponent(
|
|||
private fun invalidateSessionState() {
|
||||
logD("Updating media session playback state")
|
||||
|
||||
// There are two unfixable issues with this code:
|
||||
// 1. If the position is changed while paused (from the app), the position just won't
|
||||
// update unless I re-post the notification. However, I cannot do such without being
|
||||
// rate-limited. I cannot believe android rate-limits media notifications when they
|
||||
// have to be updated as often as they need to.
|
||||
// 2. Due to metadata updates being delayed but playback remaining ongoing, the position
|
||||
// Note: Due to metadata updates being delayed but playback remaining ongoing, the position
|
||||
// will be wonky until we can upload a duration. Again, this ties back to how I must
|
||||
// aggressively batch notification updates to prevent rate-limiting.
|
||||
// Android 13 seems to resolve these, but I'm still stuck with these issues below that
|
||||
|
@ -288,13 +310,8 @@ class MediaSessionComponent(
|
|||
val state =
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(ACTIONS)
|
||||
.addCustomAction(
|
||||
PlaybackStateCompat.CustomAction.Builder(
|
||||
PlaybackService.ACTION_INC_REPEAT_MODE,
|
||||
context.getString(R.string.desc_change_repeat),
|
||||
R.drawable.ic_repeat_off_24)
|
||||
.build())
|
||||
.setBufferedPosition(player.bufferedPosition)
|
||||
.setActiveQueueItemId(playbackManager.index.toLong())
|
||||
|
||||
val playerState =
|
||||
if (playbackManager.isPlaying) {
|
||||
|
@ -318,7 +335,7 @@ class MediaSessionComponent(
|
|||
}
|
||||
|
||||
if (!provider.isBusy) {
|
||||
callback.onPostNotification(notification)
|
||||
callback.onPostNotification(notification, "new notification actions")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -271,7 +271,7 @@ class PlaybackService :
|
|||
player.playWhenReady = isPlaying
|
||||
}
|
||||
|
||||
override fun onPostNotification(notification: NotificationComponent?) {
|
||||
override fun onPostNotification(notification: NotificationComponent?, reason: String) {
|
||||
if (notification == null) {
|
||||
// This case is only here if I ever need to move foreground stopping from
|
||||
// the player code to the notification code.
|
||||
|
@ -280,7 +280,7 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
if (hasPlayed) {
|
||||
logD("Updating notification")
|
||||
logD("Updating notification [Reason: $reason]")
|
||||
if (!foregroundManager.tryStartForeground(notification)) {
|
||||
notification.post()
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import re
|
|||
|
||||
# WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND
|
||||
# THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
|
||||
EXO_VERSION = "2.18.0"
|
||||
EXO_VERSION = "2.18.1"
|
||||
FLAC_VERSION = "1.3.2"
|
||||
|
||||
FATAL="\033[1;31m"
|
||||
|
|
Loading…
Reference in a new issue