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:
parent
3cef088d12
commit
40a34f0596
5 changed files with 141 additions and 83 deletions
|
@ -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.
|
||||
|
|
|
@ -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,10 +395,42 @@ 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() {
|
||||
private fun invalidateActions() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
notification.updatePlaying(playbackManager.isPlaying)
|
||||
|
||||
if (settings.useAltNotifAction) {
|
||||
|
@ -387,9 +438,13 @@ class MediaSessionComponent(
|
|||
} else {
|
||||
notification.updateRepeatMode(playbackManager.repeatMode)
|
||||
}
|
||||
} else {
|
||||
// Update custom actions instead of the notification
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
if (!provider.isBusy) {
|
||||
callback.onPostNotification(notification, "new notification actions")
|
||||
callback.onPostNotification(notification, PostingReason.ACTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue