playback: migrate media session to android 13

Migrate MediaSessionComponent to android 13.

This was primarily implementing custom actions with the media session
and adding some extra bug fixes that I was already planning. I was
really hoping that google fixes the nightmarish mess that was the
previous MediaStyle notification, where I had to update the session
and then the notification in a tight dance with clever tricks to not
get rate-limited, but nope. I still have to do exactly the same thing
as beforehand, but with even extra insanity due to the custom actions.
This commit is contained in:
Alexander Capehart 2022-08-16 15:41:47 -06:00
parent 3cef088d12
commit 40a34f0596
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 141 additions and 83 deletions

View file

@ -2,6 +2,12 @@
## dev
#### What's New
- Added Android 13 support [#129]
- Switch to new storage permissions
- Add monochrome icon
- Fix issue where widget covers would not load
#### What's Improved
- Playback bar now has a marquee effect
- Added a way to access the system equalizer from the playback menu.

View file

@ -21,6 +21,7 @@ import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.support.v4.media.MediaDescriptionCompat
@ -40,15 +41,20 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
/**
* The component managing the [MediaSessionCompat] instance.
* The component managing the [MediaSessionCompat] instance, alongside the [NotificationComponent]
*
* Media3 is a joke. It tries so hard to be "hElpfUl" and implement so many fundamental behaviors
* into a one-size-fits-all package that it only ends up causing unending bugs and frustration. The
* queue system is horribly designed, the notification code is outdated, and the overstretched
* abstractions result in terrible performance bottlenecks and insane state bugs..
* MediaSession is easily one of the most poorly thought out APIs in Android. It tries to hard to be
* hElpfUl and implement so many fundamental behaviors into a one-size-fits-all package that it only
* ends up causing unending bugs and frustrating. The queue system is horribly designed, the
* playback state system has unending coherency bugs, and the overstretched abstractions result in
* god-awful performance bottlenecks and insane state bugs.
*
* Show me a way to adapt my internal queue into the new system and I will change my mind, but
* otherwise, I will stick with my normal system that works correctly.
* The sheer absurdity of the hoops we have jump through to get this working in an okay manner is
* the reason why Auxio only mirrors a saner playback state to the media session instead of relying
* on it. I thought that Android 13 would at least try to make the state more coherent, but NOPE.
* You still have to do a delicate dance of posting notifications and updating the session state
* while also keeping in mind the absurd rate limiting system in place just to have a sort-of
* coherent state. And even then it will break if you skip too much.
*
* @author OxygenCobalt
*
@ -64,7 +70,13 @@ class MediaSessionComponent(
PlaybackStateManager.Callback,
Settings.Callback {
interface Callback {
fun onPostNotification(notification: NotificationComponent?, reason: String)
fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
}
enum class PostingReason {
METADATA,
ACTIONS,
POSITION
}
private val mediaSession =
@ -126,7 +138,7 @@ class MediaSessionComponent(
private fun updateMediaMetadata(song: Song?, parent: MusicParent?) {
if (song == null) {
mediaSession.setMetadata(emptyMetadata)
callback.onPostNotification(null, "song update")
callback.onPostNotification(null, PostingReason.METADATA)
return
}
@ -184,7 +196,7 @@ class MediaSessionComponent(
val metadata = builder.build()
mediaSession.setMetadata(metadata)
notification.updateMetadata(metadata)
callback.onPostNotification(notification, "song update")
callback.onPostNotification(notification, PostingReason.METADATA)
}
})
}
@ -211,7 +223,7 @@ class MediaSessionComponent(
override fun onPlayingChanged(isPlaying: Boolean) {
invalidateSessionState()
invalidateNotificationActions()
invalidateActions()
}
override fun onRepeatChanged(repeatMode: RepeatMode) {
@ -222,7 +234,7 @@ class MediaSessionComponent(
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
})
invalidateNotificationActions()
invalidateActions()
}
override fun onShuffledChanged(isShuffled: Boolean) {
@ -233,7 +245,7 @@ class MediaSessionComponent(
PlaybackStateCompat.SHUFFLE_MODE_NONE
})
invalidateNotificationActions()
invalidateActions()
}
// --- SETTINGSMANAGER CALLBACKS ---
@ -243,7 +255,7 @@ class MediaSessionComponent(
context.getString(R.string.set_key_show_covers),
context.getString(R.string.set_key_quality_covers) ->
updateMediaMetadata(playbackManager.song, playbackManager.parent)
context.getString(R.string.set_key_alt_notif_action) -> invalidateNotificationActions()
context.getString(R.string.set_key_alt_notif_action) -> invalidateActions()
}
}
@ -262,7 +274,7 @@ class MediaSessionComponent(
// when we invalidate the state (that will cause us to be rate-limited), and also not
// always when we seek (that will also cause us to be rate-limited). Someone looked at
// this system and said it was well-designed.
callback.onPostNotification(notification, "position discontinuity")
callback.onPostNotification(notification, PostingReason.POSITION)
}
}
@ -280,7 +292,7 @@ class MediaSessionComponent(
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
super.onPlayFromSearch(query, extras)
// STUB: Unimplemented
// STUB: Unimplemented, no media browser
}
override fun onAddQueueItem(description: MediaDescriptionCompat?) {
@ -345,6 +357,14 @@ class MediaSessionComponent(
}
}
override fun onCustomAction(action: String?, extras: Bundle?) {
super.onCustomAction(action, extras)
// Service already handles intents from the old notification actions, easier to
// plug into that system.
context.sendBroadcast(Intent(action))
}
override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT))
@ -360,7 +380,6 @@ class MediaSessionComponent(
// aggressively batch notification updates to prevent rate-limiting.
// Android 13 seems to resolve these, but I'm still stuck with these issues below that
// version.
// TODO: Add the custom actions for Android 13
val state =
PlaybackStateCompat.Builder()
.setActions(ACTIONS)
@ -376,20 +395,56 @@ class MediaSessionComponent(
state.setState(playerState, player.currentPosition, 1.0f, SystemClock.elapsedRealtime())
// Android 13+ leverages custom actions.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val extraAction =
if (settings.useAltNotifAction) {
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_change_repeat),
if (playbackManager.isShuffled) {
R.drawable.ic_shuffle_on_24
} else {
R.drawable.ic_shuffle_off_24
})
} else {
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INC_REPEAT_MODE,
context.getString(R.string.desc_change_repeat),
playbackManager.repeatMode.icon)
}
val exitAction =
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_EXIT,
context.getString(R.string.desc_exit),
R.drawable.ic_close_24)
.build()
state.addCustomAction(extraAction.build())
state.addCustomAction(exitAction)
}
mediaSession.setPlaybackState(state.build())
}
private fun invalidateNotificationActions() {
notification.updatePlaying(playbackManager.isPlaying)
private fun invalidateActions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
notification.updatePlaying(playbackManager.isPlaying)
if (settings.useAltNotifAction) {
notification.updateShuffled(playbackManager.isShuffled)
if (settings.useAltNotifAction) {
notification.updateShuffled(playbackManager.isShuffled)
} else {
notification.updateRepeatMode(playbackManager.repeatMode)
}
} else {
notification.updateRepeatMode(playbackManager.repeatMode)
// Update custom actions instead of the notification
invalidateSessionState()
}
if (!provider.isBusy) {
callback.onPostNotification(notification, "new notification actions")
callback.onPostNotification(notification, PostingReason.ACTIONS)
}
}

View file

@ -106,15 +106,40 @@ class PlaybackService :
override fun onCreate() {
super.onCreate()
foregroundManager = ForegroundManager(this)
// --- PLAYER SETUP ---
replayGainProcessor = ReplayGainAudioProcessor(this)
player = newPlayer()
// Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
arrayOf(
MediaCodecAudioRenderer(
this,
MediaCodecSelector.DEFAULT,
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor),
LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
}
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
player =
ExoPlayer.Builder(this, audioRenderer)
.setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
.setWakeMode(C.WAKE_MODE_LOCAL)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true)
.build()
player.addListener(this)
playbackManager.registerController(this)
positionScope.launch {
while (true) {
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
@ -122,7 +147,8 @@ class PlaybackService :
}
}
// --- SYSTEM SETUP ---
settings = Settings(this, this)
foregroundManager = ForegroundManager(this)
widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, player, this)
@ -144,9 +170,6 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP ---
settings = Settings(this, this)
playbackManager.registerController(this)
logD("Service created")
}
@ -281,6 +304,26 @@ class PlaybackService :
}
}
private fun broadcastAudioEffectAction(event: String) {
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
/** Stop the foreground state and hide the notification */
private fun stopAndSave() {
hasPlayed = false
if (foregroundManager.tryStopForeground()) {
logD("Saving playback state")
saveScope.launch {
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
}
}
}
override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs)
@ -293,7 +336,10 @@ class PlaybackService :
player.playWhenReady = isPlaying
}
override fun onPostNotification(notification: NotificationComponent?, reason: String) {
override fun onPostNotification(
notification: NotificationComponent?,
reason: MediaSessionComponent.PostingReason
) {
if (notification == null) {
// This case is only here if I ever need to move foreground stopping from
// the player code to the notification code.
@ -321,55 +367,6 @@ class PlaybackService :
// --- OTHER FUNCTIONS ---
/** Create the [ExoPlayer] instance. */
private fun newPlayer(): ExoPlayer {
// Since Auxio is a music player, only specify an audio renderer to save
// battery/apk size/cache size
val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ ->
arrayOf(
MediaCodecAudioRenderer(
this,
MediaCodecSelector.DEFAULT,
handler,
audioListener,
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
replayGainProcessor),
LibflacAudioRenderer(handler, audioListener, replayGainProcessor))
}
// Enable constant bitrate seeking so that certain MP3s/AACs are seekable
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
return ExoPlayer.Builder(this, audioRenderer)
.setMediaSourceFactory(DefaultMediaSourceFactory(this, extractorsFactory))
.setWakeMode(C.WAKE_MODE_LOCAL)
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build(),
true)
.build()
}
private fun broadcastAudioEffectAction(event: String) {
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
/** Stop the foreground state and hide the notification */
private fun stopAndSave() {
if (foregroundManager.tryStopForeground()) {
logD("Saving playback state")
saveScope.launch {
playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService))
}
}
}
/** A [BroadcastReceiver] for receiving general playback events from the system. */
private inner class PlaybackReceiver : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false

View file

@ -111,7 +111,6 @@ class WidgetComponent(private val context: Context) :
RoundedCornersTransformation(cornerRadius.toFloat()))
} else {
// Divide by two to really make sure we aren't hitting the memory limit.
builder.size(computeSize(sw, sh, 2f))
}
}

View file

@ -250,6 +250,7 @@
<string name="desc_change_repeat">Change repeat mode</string>
<string name="desc_shuffle">Turn shuffle on or off</string>
<string name="desc_shuffle_all">Shuffle all songs</string>
<string name="desc_exit">Stop playback</string>
<string name="desc_clear_queue_item">Remove this queue song</string>
<string name="desc_queue_handle">Move this queue song</string>