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 ## 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 #### What's Improved
- Playback bar now has a marquee effect - Playback bar now has a marquee effect
- Added a way to access the system equalizer from the playback menu. - 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.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock import android.os.SystemClock
import android.support.v4.media.MediaDescriptionCompat import android.support.v4.media.MediaDescriptionCompat
@ -40,15 +41,20 @@ import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD 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 * MediaSession is easily one of the most poorly thought out APIs in Android. It tries to hard to be
* into a one-size-fits-all package that it only ends up causing unending bugs and frustration. The * hElpfUl and implement so many fundamental behaviors into a one-size-fits-all package that it only
* queue system is horribly designed, the notification code is outdated, and the overstretched * ends up causing unending bugs and frustrating. The queue system is horribly designed, the
* abstractions result in terrible performance bottlenecks and insane state bugs.. * 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 * The sheer absurdity of the hoops we have jump through to get this working in an okay manner is
* otherwise, I will stick with my normal system that works correctly. * 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 * @author OxygenCobalt
* *
@ -64,7 +70,13 @@ class MediaSessionComponent(
PlaybackStateManager.Callback, PlaybackStateManager.Callback,
Settings.Callback { Settings.Callback {
interface Callback { interface Callback {
fun onPostNotification(notification: NotificationComponent?, reason: String) fun onPostNotification(notification: NotificationComponent?, reason: PostingReason)
}
enum class PostingReason {
METADATA,
ACTIONS,
POSITION
} }
private val mediaSession = private val mediaSession =
@ -126,7 +138,7 @@ class MediaSessionComponent(
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, "song update") callback.onPostNotification(null, PostingReason.METADATA)
return return
} }
@ -184,7 +196,7 @@ 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, "song update") callback.onPostNotification(notification, PostingReason.METADATA)
} }
}) })
} }
@ -211,7 +223,7 @@ class MediaSessionComponent(
override fun onPlayingChanged(isPlaying: Boolean) { override fun onPlayingChanged(isPlaying: Boolean) {
invalidateSessionState() invalidateSessionState()
invalidateNotificationActions() invalidateActions()
} }
override fun onRepeatChanged(repeatMode: RepeatMode) { override fun onRepeatChanged(repeatMode: RepeatMode) {
@ -222,7 +234,7 @@ class MediaSessionComponent(
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
}) })
invalidateNotificationActions() invalidateActions()
} }
override fun onShuffledChanged(isShuffled: Boolean) { override fun onShuffledChanged(isShuffled: Boolean) {
@ -233,7 +245,7 @@ class MediaSessionComponent(
PlaybackStateCompat.SHUFFLE_MODE_NONE PlaybackStateCompat.SHUFFLE_MODE_NONE
}) })
invalidateNotificationActions() invalidateActions()
} }
// --- SETTINGSMANAGER CALLBACKS --- // --- SETTINGSMANAGER CALLBACKS ---
@ -243,7 +255,7 @@ class MediaSessionComponent(
context.getString(R.string.set_key_show_covers), context.getString(R.string.set_key_show_covers),
context.getString(R.string.set_key_quality_covers) -> context.getString(R.string.set_key_quality_covers) ->
updateMediaMetadata(playbackManager.song, playbackManager.parent) 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 // 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 // always when we seek (that will also cause us to be rate-limited). Someone looked at
// this system and said it was well-designed. // 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?) { override fun onPlayFromSearch(query: String?, extras: Bundle?) {
super.onPlayFromSearch(query, extras) super.onPlayFromSearch(query, extras)
// STUB: Unimplemented // STUB: Unimplemented, no media browser
} }
override fun onAddQueueItem(description: MediaDescriptionCompat?) { 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() { override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent // Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT)) context.sendBroadcast(Intent(PlaybackService.ACTION_EXIT))
@ -360,7 +380,6 @@ class MediaSessionComponent(
// 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
// version. // version.
// TODO: Add the custom actions for Android 13
val state = val state =
PlaybackStateCompat.Builder() PlaybackStateCompat.Builder()
.setActions(ACTIONS) .setActions(ACTIONS)
@ -376,10 +395,42 @@ class MediaSessionComponent(
state.setState(playerState, player.currentPosition, 1.0f, SystemClock.elapsedRealtime()) 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()) mediaSession.setPlaybackState(state.build())
} }
private fun invalidateNotificationActions() { private fun invalidateActions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
notification.updatePlaying(playbackManager.isPlaying) notification.updatePlaying(playbackManager.isPlaying)
if (settings.useAltNotifAction) { if (settings.useAltNotifAction) {
@ -387,9 +438,13 @@ class MediaSessionComponent(
} else { } else {
notification.updateRepeatMode(playbackManager.repeatMode) notification.updateRepeatMode(playbackManager.repeatMode)
} }
} else {
// Update custom actions instead of the notification
invalidateSessionState()
}
if (!provider.isBusy) { 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() { override fun onCreate() {
super.onCreate() super.onCreate()
foregroundManager = ForegroundManager(this)
// --- PLAYER SETUP ---
replayGainProcessor = ReplayGainAudioProcessor(this) 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) player.addListener(this)
playbackManager.registerController(this)
positionScope.launch { positionScope.launch {
while (true) { while (true) {
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition) playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
@ -122,7 +147,8 @@ class PlaybackService :
} }
} }
// --- SYSTEM SETUP --- settings = Settings(this, this)
foregroundManager = ForegroundManager(this)
widgetComponent = WidgetComponent(this) widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, player, this) mediaSessionComponent = MediaSessionComponent(this, player, this)
@ -144,9 +170,6 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
settings = Settings(this, this)
playbackManager.registerController(this)
logD("Service created") 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) { override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms") logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs) player.seekTo(positionMs)
@ -293,7 +336,10 @@ class PlaybackService :
player.playWhenReady = isPlaying player.playWhenReady = isPlaying
} }
override fun onPostNotification(notification: NotificationComponent?, reason: String) { override fun onPostNotification(
notification: NotificationComponent?,
reason: MediaSessionComponent.PostingReason
) {
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.
@ -321,55 +367,6 @@ class PlaybackService :
// --- OTHER FUNCTIONS --- // --- 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. */ /** A [BroadcastReceiver] for receiving general playback events from the system. */
private inner class PlaybackReceiver : BroadcastReceiver() { private inner class PlaybackReceiver : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false private var initialHeadsetPlugEventHandled = false

View file

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

View file

@ -250,6 +250,7 @@
<string name="desc_change_repeat">Change repeat mode</string> <string name="desc_change_repeat">Change repeat mode</string>
<string name="desc_shuffle">Turn shuffle on or off</string> <string name="desc_shuffle">Turn shuffle on or off</string>
<string name="desc_shuffle_all">Shuffle all songs</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_clear_queue_item">Remove this queue song</string>
<string name="desc_queue_handle">Move this queue song</string> <string name="desc_queue_handle">Move this queue song</string>