From d57f9801485d705603f6f81614fa348db6b54fc4 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Fri, 29 Apr 2022 19:53:58 -0600 Subject: [PATCH] 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. --- ...nConnector.kt => MediaSessionComponent.kt} | 80 ++++++++++++----- ...tification.kt => NotificationComponent.kt} | 73 +++++++--------- .../auxio/playback/system/PlaybackService.kt | 85 +++++-------------- .../oxycblt/auxio/widgets/WidgetController.kt | 4 - 4 files changed, 107 insertions(+), 135 deletions(-) rename app/src/main/java/org/oxycblt/auxio/playback/system/{PlaybackSessionConnector.kt => MediaSessionComponent.kt} (77%) rename app/src/main/java/org/oxycblt/auxio/playback/system/{PlaybackNotification.kt => NotificationComponent.kt} (70%) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt similarity index 77% rename from app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt rename to app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 8a2ec8128..ce0c618a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -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) { - onSongChanged(playbackManager.song) - } - override fun onNewPlayback(index: Int, queue: List, 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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt similarity index 70% rename from app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt rename to app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index 5b2f30b05..5de3fb177 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -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) - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 057d08431..777781e00 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt index 1bb57464a..7bc06ce65 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt @@ -66,10 +66,6 @@ class WidgetController(private val context: Context) : widget.update(context, playbackManager) } - override fun onQueueChanged(index: Int, queue: List) { - widget.update(context, playbackManager) - } - override fun onNewPlayback(index: Int, queue: List, parent: MusicParent?) { widget.update(context, playbackManager) }