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 basic awareness of multi-value vorbis tags [#197, dependent on this feature]
- Added Last Added sorting - Added Last Added sorting
- Search now takes sort tags and file names in account [#184] - Search now takes sort tags and file names in account [#184]
- Added option to clear playback state in settings
#### What's Fixed #### What's Fixed
- Fixed default material theme being used before app shows up - Fixed default material theme being used before app shows up

View file

@ -94,7 +94,7 @@ dependencies {
// Exoplayer // Exoplayer
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE PRE-BUILD SCRIPT. // 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. // 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"]) implementation fileTree(dir: "libs", include: ["extension-*.aar"])
// Image loading // Image loading

View file

@ -24,6 +24,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Auxio.App" android:theme="@style/Theme.Auxio.App"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:appCategory="audio"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<activity <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 // Loading quality covers basically means to parse the file metadata ourselves
// and then extract the cover. // and then extract the cover.
@ -88,30 +88,10 @@ abstract class BaseFetcher : Fetcher {
// for a manual parser. // for a manual parser.
// However, Samsung seems to cripple this class as to force people to use their ad-infested // 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 // 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. // we have to add even more layers of redundancy to make sure we can extract a cover.
val result = fetchAospMetadataCovers(context, album) // Thanks Samsung. Prick.
if (result != null) { fetchAospMetadataCovers(context, album)
return result ?: fetchExoplayerCover(context, album) ?: fetchMediaStoreCovers(context, album)
}
// 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
}
private fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? { private fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? {
MediaMetadataRetriever().apply { MediaMetadataRetriever().apply {

View file

@ -21,6 +21,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.SystemClock import android.os.SystemClock
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
@ -44,8 +45,6 @@ import org.oxycblt.auxio.util.logD
* *
* @author OxygenCobalt * @author OxygenCobalt
* *
* TODO: Queue functionality
*
* TODO: Remove the player callback once smooth seeking is implemented * TODO: Remove the player callback once smooth seeking is implemented
*/ */
class MediaSessionComponent( class MediaSessionComponent(
@ -58,11 +57,14 @@ class MediaSessionComponent(
PlaybackStateManager.Callback, PlaybackStateManager.Callback,
Settings.Callback { Settings.Callback {
interface Callback { interface Callback {
fun onPostNotification(notification: NotificationComponent?) fun onPostNotification(notification: NotificationComponent?, reason: String)
} }
private val mediaSession = 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 playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context, this) private val settings = Settings(context, this)
@ -94,18 +96,25 @@ class MediaSessionComponent(
// --- PLAYBACKSTATEMANAGER CALLBACKS --- // --- PLAYBACKSTATEMANAGER CALLBACKS ---
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
updateMediaMetadata(playbackManager.song, parent)
}
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
updateMediaMetadata(playbackManager.song, playbackManager.parent) 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?) { private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
if (song == null) { if (song == null) {
mediaSession.setMetadata(emptyMetadata) mediaSession.setMetadata(emptyMetadata)
callback.onPostNotification(null) callback.onPostNotification(null, "song update")
return return
} }
@ -163,11 +172,29 @@ class MediaSessionComponent(
val metadata = builder.build() val metadata = builder.build()
mediaSession.setMetadata(metadata) mediaSession.setMetadata(metadata)
notification.updateMetadata(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) { override fun onPlayingChanged(isPlaying: Boolean) {
invalidateSessionState() invalidateSessionState()
invalidateNotificationActions() invalidateNotificationActions()
@ -274,12 +301,7 @@ class MediaSessionComponent(
private fun invalidateSessionState() { private fun invalidateSessionState() {
logD("Updating media session playback state") logD("Updating media session playback state")
// There are two unfixable issues with this code: // Note: Due to metadata updates being delayed but playback remaining ongoing, the position
// 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
// will be wonky until we can upload a duration. Again, this ties back to how I must // 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. // aggressively batch notification updates to prevent rate-limiting.
// Android 13 seems to resolve these, but I'm still stuck with these issues below that // Android 13 seems to resolve these, but I'm still stuck with these issues below that
@ -288,13 +310,8 @@ class MediaSessionComponent(
val state = val state =
PlaybackStateCompat.Builder() PlaybackStateCompat.Builder()
.setActions(ACTIONS) .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) .setBufferedPosition(player.bufferedPosition)
.setActiveQueueItemId(playbackManager.index.toLong())
val playerState = val playerState =
if (playbackManager.isPlaying) { if (playbackManager.isPlaying) {
@ -318,7 +335,7 @@ class MediaSessionComponent(
} }
if (!provider.isBusy) { if (!provider.isBusy) {
callback.onPostNotification(notification) callback.onPostNotification(notification, "new notification actions")
} }
} }

View file

@ -271,7 +271,7 @@ class PlaybackService :
player.playWhenReady = isPlaying player.playWhenReady = isPlaying
} }
override fun onPostNotification(notification: NotificationComponent?) { override fun onPostNotification(notification: NotificationComponent?, reason: String) {
if (notification == null) { if (notification == null) {
// This case is only here if I ever need to move foreground stopping from // This case is only here if I ever need to move foreground stopping from
// the player code to the notification code. // the player code to the notification code.
@ -280,7 +280,7 @@ class PlaybackService :
} }
if (hasPlayed) { if (hasPlayed) {
logD("Updating notification") logD("Updating notification [Reason: $reason]")
if (!foregroundManager.tryStartForeground(notification)) { if (!foregroundManager.tryStartForeground(notification)) {
notification.post() 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 # 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. # 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" FLAC_VERSION = "1.3.2"
FATAL="\033[1;31m" FATAL="\033[1;31m"