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.
This commit is contained in:
Alexander Capehart 2022-08-28 19:06:24 -06:00
parent 9fae621f7e
commit b2085e440e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
4 changed files with 97 additions and 71 deletions

View file

@ -28,6 +28,7 @@ import androidx.core.view.updatePadding
import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.music.system.IndexerService import org.oxycblt.auxio.music.system.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.androidViewModels
@ -70,7 +71,7 @@ class MainActivity : AppCompatActivity() {
startService(Intent(this, PlaybackService::class.java)) startService(Intent(this, PlaybackService::class.java))
if (!startIntentDelayedAction(intent)) { if (!startIntentDelayedAction(intent)) {
playbackModel.startDelayedAction(PlaybackViewModel.DelayedAction.RestoreState) playbackModel.startAction(PlaybackStateManager.ControllerAction.RestoreState)
} }
} }
@ -97,14 +98,14 @@ class MainActivity : AppCompatActivity() {
val action = val action =
when (intent.action) { when (intent.action) {
Intent.ACTION_VIEW -> Intent.ACTION_VIEW ->
PlaybackViewModel.DelayedAction.Open(intent.data ?: return false) PlaybackStateManager.ControllerAction.Open(intent.data ?: return false)
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> { AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> {
PlaybackViewModel.DelayedAction.ShuffleAll PlaybackStateManager.ControllerAction.ShuffleAll
} }
else -> return false else -> return false
} }
playbackModel.startDelayedAction(action) playbackModel.startAction(action)
return true return true
} }

View file

@ -18,7 +18,6 @@
package org.oxycblt.auxio.playback package org.oxycblt.auxio.playback
import android.app.Application import android.app.Application
import android.net.Uri
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow 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.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
@ -47,13 +45,10 @@ import org.oxycblt.auxio.util.logE
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackViewModel(application: Application) : class PlaybackViewModel(application: Application) :
AndroidViewModel(application), PlaybackStateManager.Callback, MusicStore.Callback { AndroidViewModel(application), PlaybackStateManager.Callback {
private val musicStore = MusicStore.getInstance()
private val settings = Settings(application) private val settings = Settings(application)
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private var pendingDelayedAction: DelayedAction? = null
private val _song = MutableStateFlow<Song?>(null) private val _song = MutableStateFlow<Song?>(null)
/** The current song. */ /** The current song. */
val song: StateFlow<Song?> val song: StateFlow<Song?>
@ -80,7 +75,6 @@ class PlaybackViewModel(application: Application) :
get() = playbackManager.currentAudioSessionId get() = playbackManager.currentAudioSessionId
init { init {
musicStore.addCallback(this)
playbackManager.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, * A "controller action" is a class of playback actions that must have music present to
* usually alongside a context too. Examples include: * function, usually alongside a context too. Examples include:
* - Opening files * - Opening files
* - Restoring the playback state * - Restoring the playback state
* - App shortcuts * - 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) { fun startAction(action: PlaybackStateManager.ControllerAction) {
val library = musicStore.library playbackManager.startAction(action)
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)
}
}
}
} }
// --- PLAYER FUNCTIONS --- // --- 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 --- // --- OVERRIDES ---
override fun onCleared() { override fun onCleared() {
musicStore.removeCallback(this)
playbackManager.removeCallback(this) playbackManager.removeCallback(this)
pendingDelayedAction = null
} }
override fun onIndexMoved(index: Int) { override fun onIndexMoved(index: Int) {
@ -319,14 +279,4 @@ class PlaybackViewModel(application: Application) :
override fun onRepeatChanged(repeatMode: RepeatMode) { override fun onRepeatChanged(repeatMode: RepeatMode) {
_repeatMode.value = repeatMode _repeatMode.value = repeatMode
} }
override fun onLibraryChanged(library: MusicStore.Library?) {
if (library != null) {
val action = pendingDelayedAction
if (action != null) {
performActionImpl(action, library)
pendingDelayedAction = null
}
}
}
} }

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.playback.state package org.oxycblt.auxio.playback.state
import android.net.Uri
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -93,6 +94,9 @@ class PlaybackStateManager private constructor() {
val currentAudioSessionId: Int? val currentAudioSessionId: Int?
get() = controller?.audioSessionId get() = controller?.audioSessionId
/** An action that is awaiting the controller instance to consume it. */
var pendingAction: ControllerAction? = null
// --- CALLBACKS --- // --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>() private val callbacks = mutableListOf<Callback>()
@ -130,7 +134,7 @@ class PlaybackStateManager private constructor() {
controller.loadSong(song) controller.loadSong(song)
controller.seekTo(positionMs) controller.seekTo(positionMs)
controller.onPlayingChanged(isPlaying) controller.onPlayingChanged(isPlaying)
controller.onPlayingChanged(isPlaying) requestAction(controller)
} }
this.controller = controller this.controller = controller
@ -322,7 +326,7 @@ class PlaybackStateManager private constructor() {
isShuffled = shuffled isShuffled = shuffled
} }
// --- STATE FUNCTIONS --- // --- CONTROLLER FUNCTIONS ---
/** Update the current [positionMs]. Only meant for use by [Controller] */ /** Update the current [positionMs]. Only meant for use by [Controller] */
@Synchronized @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]. * **Seek** to a [positionMs].
* @param positionMs The position to seek to in millis. * @param positionMs The position to seek to in millis.
@ -523,11 +550,17 @@ class PlaybackStateManager private constructor() {
/** Called when the playing state is changed. */ /** Called when the playing state is changed. */
fun onPlayingChanged(isPlaying: Boolean) fun onPlayingChanged(isPlaying: Boolean)
// /** Called when the repeat mode is changed. */ /**
// fun onRepeatChanged(repeatMode: RepeatMode) * Called when [PlaybackStateManager] desires some [ControllerAction] to be completed.
// * Returns true if the action was consumed, false otherwise.
// /** Called when the shuffled state is changed. */ */
// fun onShuffledChanged(isShuffled: Boolean) fun onAction(action: ControllerAction): Boolean
}
sealed class ControllerAction {
object RestoreState : ControllerAction()
object ShuffleAll : ControllerAction()
data class Open(val uri: Uri) : ControllerAction()
} }
/** /**

View file

@ -46,6 +46,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
@ -77,7 +78,8 @@ class PlaybackService :
Player.Listener, Player.Listener,
PlaybackStateManager.Controller, PlaybackStateManager.Controller,
MediaSessionComponent.Callback, MediaSessionComponent.Callback,
Settings.Callback { Settings.Callback,
MusicStore.Callback {
// Player components // Player components
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var replayGainProcessor: ReplayGainAudioProcessor private lateinit var replayGainProcessor: ReplayGainAudioProcessor
@ -89,6 +91,7 @@ class PlaybackService :
// Managers // Managers
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val musicStore = MusicStore.getInstance()
private lateinit var settings: Settings private lateinit var settings: Settings
// State // State
@ -99,6 +102,7 @@ class PlaybackService :
// Coroutines // Coroutines
private val serviceJob = Job() private val serviceJob = Job()
private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main) private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.Main)
private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main) private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
// --- SERVICE OVERRIDES --- // --- SERVICE OVERRIDES ---
@ -139,7 +143,11 @@ class PlaybackService :
player.addListener(this) player.addListener(this)
settings = Settings(this, this)
foregroundManager = ForegroundManager(this)
playbackManager.registerController(this) playbackManager.registerController(this)
musicStore.addCallback(this)
positionScope.launch { positionScope.launch {
while (true) { while (true) {
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition) playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
@ -147,9 +155,6 @@ class PlaybackService :
} }
} }
settings = Settings(this, this)
foregroundManager = ForegroundManager(this)
widgetComponent = WidgetComponent(this) widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, player, this) mediaSessionComponent = MediaSessionComponent(this, player, this)
@ -334,6 +339,36 @@ class PlaybackService :
player.playWhenReady = isPlaying 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( override fun onPostNotification(
notification: NotificationComponent?, notification: NotificationComponent?,
reason: MediaSessionComponent.PostingReason 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 --- // --- SETTINGSMANAGER OVERRIDES ---
override fun onSettingChanged(key: String) { override fun onSettingChanged(key: String) {