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:
parent
1ddff8c6d3
commit
ab28fb6323
3 changed files with 80 additions and 81 deletions
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
Loading…
Reference in a new issue