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:
OxygenCobalt 2022-07-25 10:39:13 -06:00
parent 6381815fd9
commit 0b1f0c3cda
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 51 additions and 52 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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")
}
}

View file

@ -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()
}

View file

@ -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"