From b2085e440e17226fdf843bea333627db6453ba38 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 28 Aug 2022 19:06:24 -0600 Subject: [PATCH] playback: move delayed actions to service Make PlaybackService handle delayed actions. I wanted to do this before, but technical limitations always stopped me from doing so. Turns out all I needed was a dash of global mutable state to make it all work. This is actually really good, as it separates concerns better and paves the way for future improvements to the service. --- .../java/org/oxycblt/auxio/MainActivity.kt | 9 +-- .../auxio/playback/PlaybackViewModel.kt | 62 ++----------------- .../playback/state/PlaybackStateManager.kt | 47 +++++++++++--- .../auxio/playback/system/PlaybackService.kt | 50 +++++++++++++-- 4 files changed, 97 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 6eb998892..fd9818278 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -28,6 +28,7 @@ import androidx.core.view.updatePadding import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.music.system.IndexerService import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.androidViewModels @@ -70,7 +71,7 @@ class MainActivity : AppCompatActivity() { startService(Intent(this, PlaybackService::class.java)) if (!startIntentDelayedAction(intent)) { - playbackModel.startDelayedAction(PlaybackViewModel.DelayedAction.RestoreState) + playbackModel.startAction(PlaybackStateManager.ControllerAction.RestoreState) } } @@ -97,14 +98,14 @@ class MainActivity : AppCompatActivity() { val action = when (intent.action) { Intent.ACTION_VIEW -> - PlaybackViewModel.DelayedAction.Open(intent.data ?: return false) + PlaybackStateManager.ControllerAction.Open(intent.data ?: return false) AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> { - PlaybackViewModel.DelayedAction.ShuffleAll + PlaybackStateManager.ControllerAction.ShuffleAll } else -> return false } - playbackModel.startDelayedAction(action) + playbackModel.startAction(action) return true } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 51408df39..21aeef7bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.playback import android.app.Application -import android.net.Uri import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -28,7 +27,6 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicParent -import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackStateDatabase @@ -47,13 +45,10 @@ import org.oxycblt.auxio.util.logE * @author OxygenCobalt */ class PlaybackViewModel(application: Application) : - AndroidViewModel(application), PlaybackStateManager.Callback, MusicStore.Callback { - private val musicStore = MusicStore.getInstance() + AndroidViewModel(application), PlaybackStateManager.Callback { private val settings = Settings(application) private val playbackManager = PlaybackStateManager.getInstance() - private var pendingDelayedAction: DelayedAction? = null - private val _song = MutableStateFlow(null) /** The current song. */ val song: StateFlow @@ -80,7 +75,6 @@ class PlaybackViewModel(application: Application) : get() = playbackManager.currentAudioSessionId init { - musicStore.addCallback(this) playbackManager.addCallback(this) } @@ -136,41 +130,16 @@ class PlaybackViewModel(application: Application) : } /** - * Perform the given [DelayedAction]. + * Perform the given [PlaybackStateManager.ControllerAction]. * - * A "delayed action" is a class of playback actions that must have music present to function, - * usually alongside a context too. Examples include: + * A "controller action" is a class of playback actions that must have music present to + * function, usually alongside a context too. Examples include: * - Opening files * - Restoring the playback state * - App shortcuts - * - * We would normally want to put this kind of functionality into PlaybackService, but it's - * lifecycle makes that more or less impossible. */ - fun startDelayedAction(action: DelayedAction) { - val library = musicStore.library - if (library != null) { - performActionImpl(action, library) - } else { - pendingDelayedAction = action - } - } - - private fun performActionImpl(action: DelayedAction, library: MusicStore.Library) { - when (action) { - is DelayedAction.RestoreState -> { - viewModelScope.launch { - playbackManager.restoreState( - PlaybackStateDatabase.getInstance(application), false) - } - } - is DelayedAction.ShuffleAll -> shuffleAll() - is DelayedAction.Open -> { - library.findSongForUri(application, action.uri)?.let { song -> - play(song, settings.libPlaybackMode) - } - } - } + fun startAction(action: PlaybackStateManager.ControllerAction) { + playbackManager.startAction(action) } // --- PLAYER FUNCTIONS --- @@ -280,19 +249,10 @@ class PlaybackViewModel(application: Application) : } } - /** An action delayed until the complete load of the music library. */ - sealed class DelayedAction { - object RestoreState : DelayedAction() - object ShuffleAll : DelayedAction() - data class Open(val uri: Uri) : DelayedAction() - } - // --- OVERRIDES --- override fun onCleared() { - musicStore.removeCallback(this) playbackManager.removeCallback(this) - pendingDelayedAction = null } override fun onIndexMoved(index: Int) { @@ -319,14 +279,4 @@ class PlaybackViewModel(application: Application) : override fun onRepeatChanged(repeatMode: RepeatMode) { _repeatMode.value = repeatMode } - - override fun onLibraryChanged(library: MusicStore.Library?) { - if (library != null) { - val action = pendingDelayedAction - if (action != null) { - performActionImpl(action, library) - pendingDelayedAction = null - } - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 16d6bad27..5a1fc5b37 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -17,6 +17,7 @@ package org.oxycblt.auxio.playback.state +import android.net.Uri import kotlin.math.max import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -93,6 +94,9 @@ class PlaybackStateManager private constructor() { val currentAudioSessionId: Int? get() = controller?.audioSessionId + /** An action that is awaiting the controller instance to consume it. */ + var pendingAction: ControllerAction? = null + // --- CALLBACKS --- private val callbacks = mutableListOf() @@ -130,7 +134,7 @@ class PlaybackStateManager private constructor() { controller.loadSong(song) controller.seekTo(positionMs) controller.onPlayingChanged(isPlaying) - controller.onPlayingChanged(isPlaying) + requestAction(controller) } this.controller = controller @@ -322,7 +326,7 @@ class PlaybackStateManager private constructor() { isShuffled = shuffled } - // --- STATE FUNCTIONS --- + // --- CONTROLLER FUNCTIONS --- /** Update the current [positionMs]. Only meant for use by [Controller] */ @Synchronized @@ -340,6 +344,29 @@ class PlaybackStateManager private constructor() { } } + @Synchronized + fun startAction(action: ControllerAction) { + val controller = controller + if (controller == null || !controller.onAction(action)) { + logD("Controller not present or did not consume action, ignoring.") + pendingAction = action + } + } + + /** Request the stored [Controller.Action] */ + @Synchronized + fun requestAction(controller: Controller) { + if (BuildConfig.DEBUG && this.controller !== controller) { + logW("Given controller did not match current controller") + return + } + + if (pendingAction?.let(controller::onAction) == true) { + logD("Pending action consumed") + pendingAction = null + } + } + /** * **Seek** to a [positionMs]. * @param positionMs The position to seek to in millis. @@ -523,11 +550,17 @@ class PlaybackStateManager private constructor() { /** Called when the playing state is changed. */ fun onPlayingChanged(isPlaying: Boolean) - // /** Called when the repeat mode is changed. */ - // fun onRepeatChanged(repeatMode: RepeatMode) - // - // /** Called when the shuffled state is changed. */ - // fun onShuffledChanged(isShuffled: Boolean) + /** + * Called when [PlaybackStateManager] desires some [ControllerAction] to be completed. + * Returns true if the action was consumed, false otherwise. + */ + fun onAction(action: ControllerAction): Boolean + } + + sealed class ControllerAction { + object RestoreState : ControllerAction() + object ShuffleAll : ControllerAction() + data class Open(val uri: Uri) : ControllerAction() } /** 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 16ec17465..6e055033a 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 @@ -46,6 +46,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.PlaybackStateDatabase @@ -77,7 +78,8 @@ class PlaybackService : Player.Listener, PlaybackStateManager.Controller, MediaSessionComponent.Callback, - Settings.Callback { + Settings.Callback, + MusicStore.Callback { // Player components private lateinit var player: ExoPlayer private lateinit var replayGainProcessor: ReplayGainAudioProcessor @@ -89,6 +91,7 @@ class PlaybackService : // Managers private val playbackManager = PlaybackStateManager.getInstance() + private val musicStore = MusicStore.getInstance() private lateinit var settings: Settings // State @@ -99,6 +102,7 @@ class PlaybackService : // Coroutines private val serviceJob = Job() private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main) + private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main) private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main) // --- SERVICE OVERRIDES --- @@ -139,7 +143,11 @@ class PlaybackService : player.addListener(this) + settings = Settings(this, this) + foregroundManager = ForegroundManager(this) + playbackManager.registerController(this) + musicStore.addCallback(this) positionScope.launch { while (true) { playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition) @@ -147,9 +155,6 @@ class PlaybackService : } } - settings = Settings(this, this) - foregroundManager = ForegroundManager(this) - widgetComponent = WidgetComponent(this) mediaSessionComponent = MediaSessionComponent(this, player, this) @@ -334,6 +339,36 @@ class PlaybackService : player.playWhenReady = isPlaying } + override fun onAction(action: PlaybackStateManager.ControllerAction): Boolean { + val library = musicStore.library + if (library != null) { + logD("Performing action: $action") + + when (action) { + is PlaybackStateManager.ControllerAction.RestoreState -> { + restoreScope.launch { + playbackManager.restoreState( + PlaybackStateDatabase.getInstance(this@PlaybackService), false) + } + } + is PlaybackStateManager.ControllerAction.ShuffleAll -> { + playbackManager.shuffleAll(settings) + } + is PlaybackStateManager.ControllerAction.Open -> { + library.findSongForUri(application, action.uri)?.let { song -> + playbackManager.play(song, settings.libPlaybackMode, settings) + } + } + } + + return true + } + + return false + } + + // --- MEDIASESSIONCOMPONENT OVERRIDES --- + override fun onPostNotification( notification: NotificationComponent?, reason: MediaSessionComponent.PostingReason @@ -353,6 +388,13 @@ class PlaybackService : } } + // --- MUSICSTORE OVERRIDES --- + override fun onLibraryChanged(library: MusicStore.Library?) { + if (library != null) { + playbackManager.requestAction(this) + } + } + // --- SETTINGSMANAGER OVERRIDES --- override fun onSettingChanged(key: String) {