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:
parent
9fae621f7e
commit
b2085e440e
4 changed files with 97 additions and 71 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue