playback: add event handling to mediasession

Add event handling to the MediaSession. This completes the new
PlaybackSessionConnector class and possibly addresses the issue raised
in #20.
This commit is contained in:
OxygenCobalt 2021-06-01 10:16:30 -06:00
parent 1ddff8c6d3
commit ab28fb6323
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 80 additions and 81 deletions

View file

@ -12,9 +12,7 @@ import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.MediaItem
@ -35,7 +33,6 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song
@ -220,8 +217,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
player.setMediaItem(MediaItem.fromUri(song.id.toURI()))
player.prepare()
pushMetadataToSession(song)
notification.setMetadata(
this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify
)
@ -354,27 +349,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
onSeek(playbackManager.position)
}
/**
* Upload the song metadata to the [MediaSessionCompat], so that things such as album art
* show up on the lock screen.
*/
private fun pushMetadataToSession(song: Song) {
val builder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, song.album.artist.name)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, song.album.artist.name)
.putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, song.album.artist.name)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, song.album.artist.name)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.album.name)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration)
loadBitmap(this, song) { bitmap ->
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
mediaSession.setMetadata(builder.build())
}
}
/**
* Start polling the position on a coroutine.
*/
@ -449,52 +423,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
}
}
/**
* Handle a media button intent.
*/
private fun handleMediaButtonEvent(event: Intent): Boolean {
val item = event.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
if (item != null && item.action == KeyEvent.ACTION_DOWN) {
return when (item.keyCode) {
// Play/Pause if any of the keys are play/pause
KeyEvent.KEYCODE_MEDIA_PAUSE, KeyEvent.KEYCODE_MEDIA_PLAY,
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> {
playbackManager.setPlaying(!playbackManager.isPlaying)
true
}
// Go to the next song is the key is next
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playbackManager.next()
true
}
// Go to the previous song if the key is back
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playbackManager.prev()
true
}
// Rewind if the key is rewind
KeyEvent.KEYCODE_MEDIA_REWIND -> {
playbackManager.rewind()
true
}
// Stop the service entirely if the key was stop/close
KeyEvent.KEYCODE_MEDIA_STOP, KeyEvent.KEYCODE_MEDIA_CLOSE -> {
stopSelf()
true
}
else -> false
}
}
return false
}
/**
* A [BroadcastReceiver] for receiving system events from the media notification or the headset.
*/

View file

@ -1,18 +1,20 @@
package org.oxycblt.auxio.playback.system
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager
class PlaybackSessionConnector(
private val context: Context,
private val mediaSession: MediaSessionCompat
) : PlaybackStateManager.Callback {
) : PlaybackStateManager.Callback, MediaSessionCompat.Callback() {
private val playbackManager = PlaybackStateManager.getInstance()
private val emptyMetadata = MediaMetadataCompat.Builder().build()
@ -20,9 +22,9 @@ class PlaybackSessionConnector(
.setActions(ACTIONS)
private var playerState = PlaybackStateCompat.STATE_NONE
private var playerPosition = playbackManager.position
init {
mediaSession.setCallback(this)
playbackManager.addCallback(this)
onSongUpdate(playbackManager.song)
@ -34,6 +36,62 @@ class PlaybackSessionConnector(
playbackManager.removeCallback(this)
}
// --- MEDIASESSION CALLBACKS ---
override fun onPlay() {
playbackManager.setPlaying(true)
}
override fun onPause() {
playbackManager.setPlaying(false)
}
override fun onSkipToNext() {
playbackManager.next()
}
override fun onSkipToPrevious() {
playbackManager.prev()
}
override fun onSeekTo(position: Long) {
// Set the state to buffering to prevent weird delays on the duration counter when seeking.
// And yes, STATE_PAUSED is the only state that works with this code. Because of course it is.
setPlayerState(PlaybackStateCompat.STATE_PAUSED)
playbackManager.seekTo(position)
setPlayerState(getPlayerState())
}
override fun onRewind() {
playbackManager.rewind()
}
override fun onSetRepeatMode(repeatMode: Int) {
val mode = when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL
PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK
else -> LoopMode.NONE
}
playbackManager.setLoopMode(mode)
}
override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.setShuffling(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP,
true
)
}
override fun onStop() {
// Get the service to shut down with the ACTION_EXIT intent
context.sendBroadcast(Intent(PlaybackNotification.ACTION_EXIT))
}
// --- PLAYBACKSTATEMANAGER CALLBACKS ---
override fun onSongUpdate(song: Song?) {
if (song == null) {
mediaSession.setMetadata(emptyMetadata)
@ -67,18 +125,31 @@ class PlaybackSessionConnector(
)
}
override fun onPositionUpdate(position: Long) {
playerPosition = position
override fun onSeek(position: Long) {
updateState()
}
// --- MISC ---
private fun setPlayerState(state: Int) {
playerState = state
updateState()
}
private fun getPlayerState(): Int {
if (playbackManager.song == null) {
return PlaybackStateCompat.STATE_STOPPED
}
return if (playbackManager.isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
}
}
private fun updateState() {
state.setState(playerState, playerPosition, 1.0f, SystemClock.elapsedRealtime())
state.setState(playerState, playbackManager.position, 1.0f, SystemClock.elapsedRealtime())
mediaSession.setPlaybackState(state.build())
}

View file

@ -28,13 +28,13 @@
<!--
These exact flags, in this exact order, in this exact formatting somehow make
the dialogs use the nicer material style. Please do not touch them.
the dialogs use the nicer material style. Please do not touch this or format it.
-->
<item name="viewInflaterClass">
com.google.android.material.theme.MaterialComponentsViewInflater
</item>
<!-- @formatter:off -->
<item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
<item name="alertDialogTheme">@style/ThemeOverlay.MaterialComponents.Dialog.Alert</item>
<item name="materialAlertDialogTheme">@style/Theme.CustomDialog</item>
<!-- @formatter:on -->
</style>
<!-- Toolbar theme -->