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:
OxygenCobalt 2022-04-29 19:53:58 -06:00
parent 6adc5f8715
commit d57f980148
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 107 additions and 135 deletions

View file

@ -23,38 +23,50 @@ 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 androidx.media.session.MediaButtonReceiver
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
* Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], and
* [PlaybackStateManager].
*/ */
class PlaybackSessionConnector( class MediaSessionComponent(private val context: Context, private val player: Player) :
private val context: Context, PlaybackStateManager.Callback,
private val player: Player, Player.Listener,
private val mediaSession: MediaSessionCompat SettingsManager.Callback,
) : PlaybackStateManager.Callback, Player.Listener, MediaSessionCompat.Callback() { MediaSessionCompat.Callback() {
private val playbackManager = PlaybackStateManager.getInstance() 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 { init {
mediaSession.setCallback(this) mediaSession.setCallback(this)
playbackManager.addCallback(this) playbackManager.addCallback(this)
settingsManager.addCallback(this)
player.addListener(this) player.addListener(this)
onSongChanged(playbackManager.song) onSongChanged(playbackManager.song)
onPlayingChanged(playbackManager.isPlaying) onPlayingChanged(playbackManager.isPlaying)
} }
fun handleMediaButtonIntent(intent: Intent) {
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
fun release() { fun release() {
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
settingsManager.removeCallback(this)
player.removeListener(this) player.removeListener(this)
mediaSession.release()
} }
// --- MEDIASESSION CALLBACKS --- // --- MEDIASESSION CALLBACKS ---
@ -111,10 +123,6 @@ class PlaybackSessionConnector(
onSongChanged(playbackManager.song) onSongChanged(playbackManager.song)
} }
override fun onQueueChanged(index: Int, queue: List<Song>) {
onSongChanged(playbackManager.song)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) { override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
onSongChanged(playbackManager.song) onSongChanged(playbackManager.song)
} }
@ -152,18 +160,42 @@ class PlaybackSessionConnector(
invalidateSessionState() 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) { override fun onShuffledChanged(isShuffled: Boolean) {
if (events.containsAny( mediaSession.setShuffleMode(
Player.EVENT_POSITION_DISCONTINUITY, if (isShuffled) {
Player.EVENT_PLAYBACK_STATE_CHANGED, PlaybackStateCompat.SHUFFLE_MODE_ALL
Player.EVENT_PLAY_WHEN_READY_CHANGED, } else {
Player.EVENT_IS_PLAYING_CHANGED, PlaybackStateCompat.SHUFFLE_MODE_NONE
Player.EVENT_REPEAT_MODE_CHANGED, })
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) { }
invalidateSessionState()
} // -- 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 --- // --- MISC ---
@ -205,6 +237,8 @@ class PlaybackSessionConnector(
} }
companion object { companion object {
private val emptyMetadata = MediaMetadataCompat.Builder().build()
const val ACTIONS = const val ACTIONS =
PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY or
PlaybackStateCompat.ACTION_PAUSE or PlaybackStateCompat.ACTION_PAUSE or

View file

@ -27,24 +27,38 @@ import androidx.annotation.DrawableRes
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat.MediaStyle import androidx.media.app.NotificationCompat.MediaStyle
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newBroadcastIntent
import org.oxycblt.auxio.util.newMainIntent import org.oxycblt.auxio.util.newMainIntent
/** /**
* The unified notification for [PlaybackService]. This is not self-sufficient, updates have to be * The unified notification for [PlaybackService]. Due to the nature of how this notification is
* delivered manually. * 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 * @author OxygenCobalt
*/ */
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class PlaybackNotification class NotificationComponent(private val context: Context, sessionToken: MediaSessionCompat.Token) :
private constructor(private val context: Context, mediaToken: MediaSessionCompat.Token) :
NotificationCompat.Builder(context, CHANNEL_ID) { NotificationCompat.Builder(context, CHANNEL_ID) {
private val notificationManager = context.getSystemServiceSafe(NotificationManager::class)
init { 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) setSmallIcon(R.drawable.ic_auxio)
setCategory(NotificationCompat.CATEGORY_SERVICE) setCategory(NotificationCompat.CATEGORY_SERVICE)
setShowWhen(false) 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_SKIP_NEXT, R.drawable.ic_skip_next))
addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit)) 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 fun renotify() {
// notification to not be updated by PlaybackStateManager before PlaybackService pushes notificationManager.notify(IntegerTable.NOTIFICATION_CODE, build())
// the notification, resulting in invalid metadata.
} }
// --- STATE FUNCTIONS --- // --- STATE FUNCTIONS ---
@ -71,13 +85,15 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
* Set the metadata of the notification using [song]. * Set the metadata of the notification using [song].
* @param onDone What to do when the loading of the album art is finished * @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)) setContentTitle(song.resolveName(context))
setContentText(song.resolveIndividualArtistName(context)) setContentText(song.resolveIndividualArtistName(context))
// On older versions of android [API <24], show the song's album on the subtext instead of // Starting in API 24, the subtext field changed semantics from being below the content
// the current mode, as that makes more sense for the old style of media notifications. // text to being above the title.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { 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)) setSubText(song.resolveName(context))
} }
@ -90,28 +106,20 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
} }
/** Set the playing icon on the notification */ /** Set the playing icon on the notification */
fun setPlaying(isPlaying: Boolean) { fun updatePlaying(isPlaying: Boolean) {
mActions[2] = buildPlayPauseAction(context, isPlaying) mActions[2] = buildPlayPauseAction(context, isPlaying)
} }
/** Update the first action to reflect the [repeatMode] given. */ /** Update the first action to reflect the [repeatMode] given. */
fun setRepeatMode(repeatMode: RepeatMode) { fun updateRepeatMode(repeatMode: RepeatMode) {
mActions[0] = buildRepeatAction(context, repeatMode) mActions[0] = buildRepeatAction(context, repeatMode)
} }
/** Update the first action to reflect whether the queue is shuffled or not */ /** 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) 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 --- // --- NOTIFICATION ACTION BUILDERS ---
private fun buildPlayPauseAction( private fun buildPlayPauseAction(
@ -161,24 +169,5 @@ private constructor(private val context: Context, mediaToken: MediaSessionCompat
companion object { companion object {
const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK" 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)
}
} }
} }

View file

@ -17,19 +17,13 @@
package org.oxycblt.auxio.playback.system package org.oxycblt.auxio.playback.system
import android.app.NotificationManager
import android.app.Service import android.app.Service
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.media.AudioManager import android.media.AudioManager
import android.os.Build
import android.os.IBinder 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.C
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem 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.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetController import org.oxycblt.auxio.widgets.WidgetController
import org.oxycblt.auxio.widgets.WidgetProvider 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 * This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so
* therefore there's no need to bind to it to deliver commands. * therefore there's no need to bind to it to deliver commands.
* @author OxygenCobalt * @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 : class PlaybackService :
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback { Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
// Player components // Player components
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var mediaSession: MediaSessionCompat
private lateinit var connector: PlaybackSessionConnector
private val replayGainProcessor = ReplayGainAudioProcessor() private val replayGainProcessor = ReplayGainAudioProcessor()
// Notification components
private lateinit var notification: PlaybackNotification
private lateinit var notificationManager: NotificationManager
// System backend components // System backend components
private lateinit var notificationComponent: NotificationComponent
private lateinit var mediaSessionComponent: MediaSessionComponent
private lateinit var widgets: WidgetController private lateinit var widgets: WidgetController
private val systemReceiver = PlaybackReceiver() private val systemReceiver = PlaybackReceiver()
@ -126,8 +110,8 @@ class PlaybackService :
// --- SYSTEM SETUP --- // --- SYSTEM SETUP ---
widgets = WidgetController(this) widgets = WidgetController(this)
mediaSession = MediaSessionCompat(this, packageName).apply { isActive = true } mediaSessionComponent = MediaSessionComponent(this, player)
connector = PlaybackSessionConnector(this, player, mediaSession) notificationComponent = NotificationComponent(this, mediaSessionComponent.token)
// Then the notification/headset callbacks // Then the notification/headset callbacks
IntentFilter().apply { IntentFilter().apply {
@ -145,11 +129,6 @@ class PlaybackService :
registerReceiver(systemReceiver, this) registerReceiver(systemReceiver, this)
} }
// --- NOTIFICATION SETUP ---
notificationManager = getSystemServiceSafe(NotificationManager::class)
notification = PlaybackNotification.from(this, notificationManager, mediaSession)
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.addCallback(this) playbackManager.addCallback(this)
@ -168,7 +147,7 @@ class PlaybackService :
if (intent.action == Intent.ACTION_MEDIA_BUTTON) { if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
// Workaround to get GadgetBridge and other apps that blindly query for // Workaround to get GadgetBridge and other apps that blindly query for
// ACTION_MEDIA_BUTTON working. // ACTION_MEDIA_BUTTON working.
MediaButtonReceiver.handleIntent(mediaSession, intent) mediaSessionComponent.handleMediaButtonIntent(intent)
} }
return START_NOT_STICKY return START_NOT_STICKY
@ -188,8 +167,7 @@ class PlaybackService :
serviceJob.cancel() serviceJob.cancel()
player.release() player.release()
connector.release() mediaSessionComponent.release()
mediaSession.release()
widgets.release() widgets.release()
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
@ -273,7 +251,8 @@ class PlaybackService :
logD("Setting player to ${song.rawName}") logD("Setting player to ${song.rawName}")
player.setMediaItem(MediaItem.fromUri(song.uri)) player.setMediaItem(MediaItem.fromUri(song.uri))
player.prepare() player.prepare()
notification.setMetadata(song, ::startForegroundOrNotify) notificationComponent.updateMetadata(
song, playbackManager.parent, ::startForegroundOrNotify)
return return
} }
@ -285,20 +264,20 @@ class PlaybackService :
override fun onPlayingChanged(isPlaying: Boolean) { override fun onPlayingChanged(isPlaying: Boolean) {
player.playWhenReady = isPlaying player.playWhenReady = isPlaying
notification.setPlaying(isPlaying) notificationComponent.updatePlaying(isPlaying)
startForegroundOrNotify() startForegroundOrNotify()
} }
override fun onRepeatChanged(repeatMode: RepeatMode) { override fun onRepeatChanged(repeatMode: RepeatMode) {
if (!settingsManager.useAltNotifAction) { if (!settingsManager.useAltNotifAction) {
notification.setRepeatMode(repeatMode) notificationComponent.updateRepeatMode(repeatMode)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
override fun onShuffledChanged(isShuffled: Boolean) { override fun onShuffledChanged(isShuffled: Boolean) {
if (settingsManager.useAltNotifAction) { if (settingsManager.useAltNotifAction) {
notification.setShuffled(isShuffled) notificationComponent.updateShuffled(isShuffled)
startForegroundOrNotify() startForegroundOrNotify()
} }
} }
@ -309,18 +288,11 @@ class PlaybackService :
// --- SETTINGSMANAGER OVERRIDES --- // --- SETTINGSMANAGER OVERRIDES ---
override fun onColorizeNotifUpdate(doColorize: Boolean) {
playbackManager.song?.let { song ->
connector.onSongChanged(song)
notification.setMetadata(song, ::startForegroundOrNotify)
}
}
override fun onNotifActionUpdate(useAltAction: Boolean) { override fun onNotifActionUpdate(useAltAction: Boolean) {
if (useAltAction) { if (useAltAction) {
notification.setShuffled(playbackManager.isShuffled) notificationComponent.updateShuffled(playbackManager.isShuffled)
} else { } else {
notification.setRepeatMode(playbackManager.repeatMode) notificationComponent.updateRepeatMode(playbackManager.repeatMode)
} }
startForegroundOrNotify() startForegroundOrNotify()
@ -328,14 +300,15 @@ class PlaybackService :
override fun onShowCoverUpdate(showCovers: Boolean) { override fun onShowCoverUpdate(showCovers: Boolean) {
playbackManager.song?.let { song -> playbackManager.song?.let { song ->
connector.onSongChanged(song) notificationComponent.updateMetadata(
notification.setMetadata(song, ::startForegroundOrNotify) song, playbackManager.parent, ::startForegroundOrNotify)
} }
} }
override fun onQualityCoverUpdate(doQualityCovers: Boolean) { override fun onQualityCoverUpdate(doQualityCovers: Boolean) {
playbackManager.song?.let { song -> playbackManager.song?.let { song ->
notification.setMetadata(song, ::startForegroundOrNotify) notificationComponent.updateMetadata(
song, playbackManager.parent, ::startForegroundOrNotify)
} }
} }
@ -398,20 +371,12 @@ class PlaybackService :
logD("Starting foreground/notifying") logD("Starting foreground/notifying")
if (!isForeground) { if (!isForeground) {
// Specify that this is a media service, if supported. startForeground(IntegerTable.NOTIFICATION_CODE, notificationComponent.build())
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())
}
isForeground = true isForeground = true
} else { } else {
// If we are already in foreground just update the notification // 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) } 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. */ /** A [BroadcastReceiver] for receiving general playback events from the system. */
private inner class PlaybackReceiver : BroadcastReceiver() { private inner class PlaybackReceiver : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false private var initialHeadsetPlugEventHandled = false

View file

@ -66,10 +66,6 @@ class WidgetController(private val context: Context) :
widget.update(context, playbackManager) 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?) { override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
widget.update(context, playbackManager) widget.update(context, playbackManager)
} }