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 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue