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.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.view.KeyEvent
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
@ -35,7 +33,6 @@ import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song 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.setMediaItem(MediaItem.fromUri(song.id.toURI()))
player.prepare() player.prepare()
pushMetadataToSession(song)
notification.setMetadata( notification.setMetadata(
this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify this, song, settingsManager.colorizeNotif, ::startForegroundOrNotify
) )
@ -354,27 +349,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
onSeek(playbackManager.position) 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. * 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. * 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 package org.oxycblt.auxio.playback.system
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.SystemClock import android.os.SystemClock
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
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
class PlaybackSessionConnector( class PlaybackSessionConnector(
private val context: Context, private val context: Context,
private val mediaSession: MediaSessionCompat private val mediaSession: MediaSessionCompat
) : PlaybackStateManager.Callback { ) : PlaybackStateManager.Callback, MediaSessionCompat.Callback() {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val emptyMetadata = MediaMetadataCompat.Builder().build() private val emptyMetadata = MediaMetadataCompat.Builder().build()
@ -20,9 +22,9 @@ class PlaybackSessionConnector(
.setActions(ACTIONS) .setActions(ACTIONS)
private var playerState = PlaybackStateCompat.STATE_NONE private var playerState = PlaybackStateCompat.STATE_NONE
private var playerPosition = playbackManager.position
init { init {
mediaSession.setCallback(this)
playbackManager.addCallback(this) playbackManager.addCallback(this)
onSongUpdate(playbackManager.song) onSongUpdate(playbackManager.song)
@ -34,6 +36,62 @@ class PlaybackSessionConnector(
playbackManager.removeCallback(this) 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?) { override fun onSongUpdate(song: Song?) {
if (song == null) { if (song == null) {
mediaSession.setMetadata(emptyMetadata) mediaSession.setMetadata(emptyMetadata)
@ -67,18 +125,31 @@ class PlaybackSessionConnector(
) )
} }
override fun onPositionUpdate(position: Long) { override fun onSeek(position: Long) {
playerPosition = position
updateState() updateState()
} }
// --- MISC ---
private fun setPlayerState(state: Int) { private fun setPlayerState(state: Int) {
playerState = state playerState = state
updateState() 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() { private fun updateState() {
state.setState(playerState, playerPosition, 1.0f, SystemClock.elapsedRealtime()) state.setState(playerState, playbackManager.position, 1.0f, SystemClock.elapsedRealtime())
mediaSession.setPlaybackState(state.build()) mediaSession.setPlaybackState(state.build())
} }

View file

@ -28,13 +28,13 @@
<!-- <!--
These exact flags, in this exact order, in this exact formatting somehow make 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"> <!-- @formatter:off -->
com.google.android.material.theme.MaterialComponentsViewInflater <item name="viewInflaterClass">com.google.android.material.theme.MaterialComponentsViewInflater</item>
</item>
<item name="alertDialogTheme">@style/ThemeOverlay.MaterialComponents.Dialog.Alert</item> <item name="alertDialogTheme">@style/ThemeOverlay.MaterialComponents.Dialog.Alert</item>
<item name="materialAlertDialogTheme">@style/Theme.CustomDialog</item> <item name="materialAlertDialogTheme">@style/Theme.CustomDialog</item>
<!-- @formatter:on -->
</style> </style>
<!-- Toolbar theme --> <!-- Toolbar theme -->