playback: cleanup components
Fix miscellanious bugs and clean the component code. Currently the components are in a strange state. They are a big ball of mud with inconsistent lifecycles and callbacks. I want to find a way to unify them under a single lifecycle, but the competing nature of them makes this extremely difficult.
This commit is contained in:
parent
6adc5f8715
commit
d57f980148
4 changed files with 107 additions and 135 deletions
|
@ -23,38 +23,50 @@ 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 androidx.media.session.MediaButtonReceiver
|
||||
import com.google.android.exoplayer2.Player
|
||||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], and
|
||||
* [PlaybackStateManager].
|
||||
*/
|
||||
class PlaybackSessionConnector(
|
||||
private val context: Context,
|
||||
private val player: Player,
|
||||
private val mediaSession: MediaSessionCompat
|
||||
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() {
|
||||
class MediaSessionComponent(private val context: Context, private val player: Player) :
|
||||
PlaybackStateManager.Callback,
|
||||
Player.Listener,
|
||||
SettingsManager.Callback,
|
||||
MediaSessionCompat.Callback() {
|
||||
private val playbackManager = PlaybackStateManager.getInstance()
|
||||
private val emptyMetadata = MediaMetadataCompat.Builder().build()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val mediaSession = MediaSessionCompat(context, context.packageName)
|
||||
|
||||
val token: MediaSessionCompat.Token
|
||||
get() = mediaSession.sessionToken
|
||||
|
||||
init {
|
||||
mediaSession.setCallback(this)
|
||||
playbackManager.addCallback(this)
|
||||
settingsManager.addCallback(this)
|
||||
player.addListener(this)
|
||||
|
||||
onSongChanged(playbackManager.song)
|
||||
onPlayingChanged(playbackManager.isPlaying)
|
||||
}
|
||||
|
||||
fun handleMediaButtonIntent(intent: Intent) {
|
||||
MediaButtonReceiver.handleIntent(mediaSession, intent)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
playbackManager.removeCallback(this)
|
||||
settingsManager.removeCallback(this)
|
||||
player.removeListener(this)
|
||||
mediaSession.release()
|
||||
}
|
||||
|
||||
// --- MEDIASESSION CALLBACKS ---
|
||||
|
@ -111,10 +123,6 @@ class PlaybackSessionConnector(
|
|||
onSongChanged(playbackManager.song)
|
||||
}
|
||||
|
||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
||||
onSongChanged(playbackManager.song)
|
||||
}
|
||||
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||
onSongChanged(playbackManager.song)
|
||||
}
|
||||
|
@ -152,18 +160,42 @@ class PlaybackSessionConnector(
|
|||
invalidateSessionState()
|
||||
}
|
||||
|
||||
// -- EXOPLAYER CALLBACKS ---
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||
mediaSession.setRepeatMode(
|
||||
when (repeatMode) {
|
||||
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
|
||||
RepeatMode.TRACK -> PlaybackStateCompat.REPEAT_MODE_ONE
|
||||
RepeatMode.ALL -> PlaybackStateCompat.REPEAT_MODE_ALL
|
||||
})
|
||||
}
|
||||
|
||||
override fun onEvents(player: Player, events: Player.Events) {
|
||||
if (events.containsAny(
|
||||
Player.EVENT_POSITION_DISCONTINUITY,
|
||||
Player.EVENT_PLAYBACK_STATE_CHANGED,
|
||||
Player.EVENT_PLAY_WHEN_READY_CHANGED,
|
||||
Player.EVENT_IS_PLAYING_CHANGED,
|
||||
Player.EVENT_REPEAT_MODE_CHANGED,
|
||||
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||
invalidateSessionState()
|
||||
}
|
||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||
mediaSession.setShuffleMode(
|
||||
if (isShuffled) {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
} else {
|
||||
PlaybackStateCompat.SHUFFLE_MODE_NONE
|
||||
})
|
||||
}
|
||||
|
||||
// -- SETTINGSMANAGER CALLBACKS --
|
||||
|
||||
override fun onShowCoverUpdate(showCovers: Boolean) {
|
||||
onSongChanged(playbackManager.song)
|
||||
}
|
||||
|
||||
override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
|
||||
onSongChanged(playbackManager.song)
|
||||
}
|
||||
|
||||
// -- EXOPLAYER CALLBACKS --
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
invalidateSessionState()
|
||||
}
|
||||
|
||||
// --- MISC ---
|
||||
|
@ -205,6 +237,8 @@ class PlaybackSessionConnector(
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val emptyMetadata = MediaMetadataCompat.Builder().build()
|
||||
|
||||
const val ACTIONS =
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
|
@ -27,24 +27,38 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.app.NotificationCompat.MediaStyle
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.coil.loadBitmap
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.newBroadcastIntent
|
||||
import org.oxycblt.auxio.util.newMainIntent
|
||||
|
||||
/**
|
||||
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have to be
|
||||
* delivered manually.
|
||||
* The unified notification for [PlaybackService]. Due to the nature of how this notification is
|
||||
* used, it is *not self-sufficient*. Updates have to be delivered manually, as to prevent state
|
||||
* inconsistency when the foreground state is started.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
class PlaybackNotification
|
||||
private constructor(private val context: Context, mediaToken: MediaSessionCompat.Token) :
|
||||
class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
|
||||
NotificationCompat.Builder(context, CHANNEL_ID) {
|
||||
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.info_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
setSmallIcon(R.drawable.ic_auxio)
|
||||
setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
setShowWhen(false)
|
||||
|
@ -58,11 +72,11 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
|
|||
addAction(buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next))
|
||||
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit))
|
||||
|
||||
setStyle(MediaStyle().setMediaSession(mediaToken).setShowActionsInCompactView(1, 2, 3))
|
||||
setStyle(MediaStyle().setMediaSession(sessionToken).setShowActionsInCompactView(1, 2, 3))
|
||||
}
|
||||
|
||||
// Don't connect to PlaybackStateManager here. This is because it's possible for this
|
||||
// notification to not be updated by PlaybackStateManager before PlaybackService pushes
|
||||
// the notification, resulting in invalid metadata.
|
||||
fun renotify() {
|
||||
notificationManager.notify(IntegerTable.NOTIFICATION_CODE, build())
|
||||
}
|
||||
|
||||
// --- STATE FUNCTIONS ---
|
||||
|
@ -71,13 +85,15 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
|
|||
* Set the metadata of the notification using [song].
|
||||
* @param onDone What to do when the loading of the album art is finished
|
||||
*/
|
||||
fun setMetadata(song: Song, onDone: () -> Unit) {
|
||||
fun updateMetadata(song: Song, parent: MusicParent?, onDone: () -> Unit) {
|
||||
setContentTitle(song.resolveName(context))
|
||||
setContentText(song.resolveIndividualArtistName(context))
|
||||
|
||||
// On older versions of android [API <24], show the song's album on the subtext instead of
|
||||
// the current mode, as that makes more sense for the old style of media notifications.
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
// Starting in API 24, the subtext field changed semantics from being below the content
|
||||
// text to being above the title.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setSubText(parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
||||
} else {
|
||||
setSubText(song.resolveName(context))
|
||||
}
|
||||
|
||||
|
@ -90,28 +106,20 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
|
|||
}
|
||||
|
||||
/** Set the playing icon on the notification */
|
||||
fun setPlaying(isPlaying: Boolean) {
|
||||
fun updatePlaying(isPlaying: Boolean) {
|
||||
mActions[2] = buildPlayPauseAction(context, isPlaying)
|
||||
}
|
||||
|
||||
/** Update the first action to reflect the [repeatMode] given. */
|
||||
fun setRepeatMode(repeatMode: RepeatMode) {
|
||||
fun updateRepeatMode(repeatMode: RepeatMode) {
|
||||
mActions[0] = buildRepeatAction(context, repeatMode)
|
||||
}
|
||||
|
||||
/** Update the first action to reflect whether the queue is shuffled or not */
|
||||
fun setShuffled(isShuffled: Boolean) {
|
||||
fun updateShuffled(isShuffled: Boolean) {
|
||||
mActions[0] = buildShuffleAction(context, isShuffled)
|
||||
}
|
||||
|
||||
/** Apply the current [parent] to the header of the notification. */
|
||||
fun setParent(parent: MusicParent?) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
||||
|
||||
// A blank parent always means that the mode is ALL_SONGS
|
||||
setSubText(parent?.resolveName(context) ?: context.getString(R.string.lbl_all_songs))
|
||||
}
|
||||
|
||||
// --- NOTIFICATION ACTION BUILDERS ---
|
||||
|
||||
private fun buildPlayPauseAction(
|
||||
|
@ -161,24 +169,5 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
|
|||
|
||||
companion object {
|
||||
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK"
|
||||
|
||||
/** Build a new instance of [PlaybackNotification]. */
|
||||
fun from(
|
||||
context: Context,
|
||||
notificationManager: NotificationManager,
|
||||
mediaSession: MediaSessionCompat
|
||||
): PlaybackNotification {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel =
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.info_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
return PlaybackNotification(context, mediaSession.sessionToken)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,19 +17,13 @@
|
|||
|
||||
package org.oxycblt.auxio.playback.system
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlayer
|
||||
import com.google.android.exoplayer2.MediaItem
|
||||
|
@ -56,7 +50,6 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.widgets.WidgetController
|
||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||
|
@ -71,25 +64,16 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
* This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
|
||||
* therefore there's no need to bind to it to deliver commands.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
|
||||
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
|
||||
*
|
||||
* TODO: Move hasPlayed to here as well.
|
||||
*/
|
||||
class PlaybackService :
|
||||
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||
// Player components
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private lateinit var connector: PlaybackSessionConnector
|
||||
private val replayGainProcessor = ReplayGainAudioProcessor()
|
||||
|
||||
// Notification components
|
||||
private lateinit var notification: PlaybackNotification
|
||||
private lateinit var notificationManager: NotificationManager
|
||||
|
||||
// System backend components
|
||||
private lateinit var notificationComponent: NotificationComponent
|
||||
private lateinit var mediaSessionComponent: MediaSessionComponent
|
||||
private lateinit var widgets: WidgetController
|
||||
private val systemReceiver = PlaybackReceiver()
|
||||
|
||||
|
@ -126,8 +110,8 @@ class PlaybackService :
|
|||
// --- SYSTEM SETUP ---
|
||||
|
||||
widgets = WidgetController(this)
|
||||
mediaSession = MediaSessionCompat(this, packageName).apply { isActive = true }
|
||||
connector = PlaybackSessionConnector(this, player, mediaSession)
|
||||
mediaSessionComponent = MediaSessionComponent(this, player)
|
||||
notificationComponent = NotificationComponent(this, mediaSessionComponent.token)
|
||||
|
||||
// Then the notification/headset callbacks
|
||||
IntentFilter().apply {
|
||||
|
@ -145,11 +129,6 @@ class PlaybackService :
|
|||
registerReceiver(systemReceiver, this)
|
||||
}
|
||||
|
||||
// --- NOTIFICATION SETUP ---
|
||||
|
||||
notificationManager = getSystemServiceSafe(NotificationManager::class)
|
||||
notification = PlaybackNotification.from(this, notificationManager, mediaSession)
|
||||
|
||||
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||
|
||||
playbackManager.addCallback(this)
|
||||
|
@ -168,7 +147,7 @@ class PlaybackService :
|
|||
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
|
||||
// Workaround to get GadgetBridge and other apps that blindly query for
|
||||
// ACTION_MEDIA_BUTTON working.
|
||||
MediaButtonReceiver.handleIntent(mediaSession, intent)
|
||||
mediaSessionComponent.handleMediaButtonIntent(intent)
|
||||
}
|
||||
|
||||
return START_NOT_STICKY
|
||||
|
@ -188,8 +167,7 @@ class PlaybackService :
|
|||
|
||||
serviceJob.cancel()
|
||||
player.release()
|
||||
connector.release()
|
||||
mediaSession.release()
|
||||
mediaSessionComponent.release()
|
||||
widgets.release()
|
||||
|
||||
playbackManager.removeCallback(this)
|
||||
|
@ -273,7 +251,8 @@ class PlaybackService :
|
|||
logD("Setting player to ${song.rawName}")
|
||||
player.setMediaItem(MediaItem.fromUri(song.uri))
|
||||
player.prepare()
|
||||
notification.setMetadata(song, ::startForegroundOrNotify)
|
||||
notificationComponent.updateMetadata(
|
||||
song, playbackManager.parent, ::startForegroundOrNotify)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -285,20 +264,20 @@ class PlaybackService :
|
|||
|
||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||
player.playWhenReady = isPlaying
|
||||
notification.setPlaying(isPlaying)
|
||||
notificationComponent.updatePlaying(isPlaying)
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
|
||||
override fun onRepeatChanged(repeatMode: RepeatMode) {
|
||||
if (!settingsManager.useAltNotifAction) {
|
||||
notification.setRepeatMode(repeatMode)
|
||||
notificationComponent.updateRepeatMode(repeatMode)
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffledChanged(isShuffled: Boolean) {
|
||||
if (settingsManager.useAltNotifAction) {
|
||||
notification.setShuffled(isShuffled)
|
||||
notificationComponent.updateShuffled(isShuffled)
|
||||
startForegroundOrNotify()
|
||||
}
|
||||
}
|
||||
|
@ -309,18 +288,11 @@ class PlaybackService :
|
|||
|
||||
// --- SETTINGSMANAGER OVERRIDES ---
|
||||
|
||||
override fun onColorizeNotifUpdate(doColorize: Boolean) {
|
||||
playbackManager.song?.let { song ->
|
||||
connector.onSongChanged(song)
|
||||
notification.setMetadata(song, ::startForegroundOrNotify)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNotifActionUpdate(useAltAction: Boolean) {
|
||||
if (useAltAction) {
|
||||
notification.setShuffled(playbackManager.isShuffled)
|
||||
notificationComponent.updateShuffled(playbackManager.isShuffled)
|
||||
} else {
|
||||
notification.setRepeatMode(playbackManager.repeatMode)
|
||||
notificationComponent.updateRepeatMode(playbackManager.repeatMode)
|
||||
}
|
||||
|
||||
startForegroundOrNotify()
|
||||
|
@ -328,14 +300,15 @@ class PlaybackService :
|
|||
|
||||
override fun onShowCoverUpdate(showCovers: Boolean) {
|
||||
playbackManager.song?.let { song ->
|
||||
connector.onSongChanged(song)
|
||||
notification.setMetadata(song, ::startForegroundOrNotify)
|
||||
notificationComponent.updateMetadata(
|
||||
song, playbackManager.parent, ::startForegroundOrNotify)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
|
||||
playbackManager.song?.let { song ->
|
||||
notification.setMetadata(song, ::startForegroundOrNotify)
|
||||
notificationComponent.updateMetadata(
|
||||
song, playbackManager.parent, ::startForegroundOrNotify)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,20 +371,12 @@ class PlaybackService :
|
|||
logD("Starting foreground/notifying")
|
||||
|
||||
if (!isForeground) {
|
||||
// Specify that this is a media service, if supported.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
IntegerTable.NOTIFICATION_CODE,
|
||||
notification.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||
} else {
|
||||
startForeground(IntegerTable.NOTIFICATION_CODE, notification.build())
|
||||
}
|
||||
startForeground(IntegerTable.NOTIFICATION_CODE, notificationComponent.build())
|
||||
|
||||
isForeground = true
|
||||
} else {
|
||||
// If we are already in foreground just update the notification
|
||||
notificationManager.notify(IntegerTable.NOTIFICATION_CODE, notification.build())
|
||||
notificationComponent.renotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -424,18 +389,6 @@ class PlaybackService :
|
|||
saveScope.launch { playbackManager.saveStateToDatabase(this@PlaybackService) }
|
||||
}
|
||||
|
||||
data class Metadata(
|
||||
val title: String,
|
||||
val album: String,
|
||||
val artist: String,
|
||||
val album_artist: String,
|
||||
val genre: String,
|
||||
val parent: String,
|
||||
val year: Int,
|
||||
val track: Int?,
|
||||
val albumCover: Bitmap
|
||||
)
|
||||
|
||||
/** A [BroadcastReceiver] for receiving general playback events from the system. */
|
||||
private inner class PlaybackReceiver : BroadcastReceiver() {
|
||||
private var initialHeadsetPlugEventHandled = false
|
||||
|
|
|
@ -66,10 +66,6 @@ class WidgetController(private val context: Context) :
|
|||
widget.update(context, playbackManager)
|
||||
}
|
||||
|
||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
||||
widget.update(context, playbackManager)
|
||||
}
|
||||
|
||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||
widget.update(context, playbackManager)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue