playback: rework controller into InternalPlayer
Rework the Controller interface into a standalone interface called InternalPlayer. This is mostly preparation for further changes.
This commit is contained in:
parent
8a15868ba1
commit
4d02dfb578
8 changed files with 120 additions and 101 deletions
|
@ -28,7 +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.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.system.PlaybackService
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.androidViewModels
|
||||
|
@ -70,17 +70,17 @@ class MainActivity : AppCompatActivity() {
|
|||
startService(Intent(this, IndexerService::class.java))
|
||||
startService(Intent(this, PlaybackService::class.java))
|
||||
|
||||
if (!startIntentDelayedAction(intent)) {
|
||||
playbackModel.startAction(PlaybackStateManager.ControllerAction.RestoreState)
|
||||
if (!startIntentAction(intent)) {
|
||||
playbackModel.startAction(InternalPlayer.Action.RestoreState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
startIntentDelayedAction(intent)
|
||||
startIntentAction(intent)
|
||||
}
|
||||
|
||||
private fun startIntentDelayedAction(intent: Intent?): Boolean {
|
||||
private fun startIntentAction(intent: Intent?): Boolean {
|
||||
if (intent == null) {
|
||||
return false
|
||||
}
|
||||
|
@ -97,10 +97,9 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
val action =
|
||||
when (intent.action) {
|
||||
Intent.ACTION_VIEW ->
|
||||
PlaybackStateManager.ControllerAction.Open(intent.data ?: return false)
|
||||
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
|
||||
AuxioApp.INTENT_KEY_SHORTCUT_SHUFFLE -> {
|
||||
PlaybackStateManager.ControllerAction.ShuffleAll
|
||||
InternalPlayer.Action.ShuffleAll
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
|
|
|
@ -104,11 +104,7 @@ class MusicStore private constructor() {
|
|||
/** Sanitize an old item to find the corresponding item in a new library. */
|
||||
fun sanitize(genre: Genre) = findGenreById(genre.id)
|
||||
|
||||
/**
|
||||
* Find a song for a [uri], this is similar to [findSong], but with some kind of content
|
||||
* uri.
|
||||
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
||||
*/
|
||||
/** Find a song for a [uri]. */
|
||||
fun findSongForUri(context: Context, uri: Uri) =
|
||||
context.contentResolverSafe.useQuery(uri, arrayOf(OpenableColumns.DISPLAY_NAME)) {
|
||||
cursor ->
|
||||
|
|
|
@ -47,7 +47,6 @@ class MusicViewModel : ViewModel(), Indexer.Callback {
|
|||
|
||||
override fun onIndexerStateChanged(state: Indexer.State?) {
|
||||
_indexerState.value = state
|
||||
|
||||
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
|
||||
_libraryExists.value = true
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.state.PlaybackMode
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
|
@ -63,6 +64,7 @@ class PlaybackViewModel(application: Application) :
|
|||
/** The current playback position, in seconds */
|
||||
val positionSecs: StateFlow<Long>
|
||||
get() = _positionSecs
|
||||
|
||||
private val _repeatMode = MutableStateFlow(RepeatMode.NONE)
|
||||
/** The current repeat mode, see [RepeatMode] for more information */
|
||||
val repeatMode: StateFlow<RepeatMode>
|
||||
|
@ -130,15 +132,15 @@ class PlaybackViewModel(application: Application) :
|
|||
}
|
||||
|
||||
/**
|
||||
* Perform the given [PlaybackStateManager.ControllerAction].
|
||||
* Perform the given [InternalPlayer.Action].
|
||||
*
|
||||
* A "controller action" is a class of playback actions that must have music present to
|
||||
* function, usually alongside a context too. Examples include:
|
||||
* These are 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
|
||||
*/
|
||||
fun startAction(action: PlaybackStateManager.ControllerAction) {
|
||||
fun startAction(action: InternalPlayer.Action) {
|
||||
playbackManager.startAction(action)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
import android.net.Uri
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
/** Represents a class capable of managing the internal player. */
|
||||
interface InternalPlayer {
|
||||
/** The audio session ID of the player instance. */
|
||||
val audioSessionId: Int
|
||||
|
||||
/** Whether the player should rewind instead of going to the previous song. */
|
||||
val shouldRewindWithPrev: Boolean
|
||||
|
||||
/** Called when a new song should be loaded into the player. */
|
||||
fun loadSong(song: Song?)
|
||||
|
||||
/** Seek to [positionMs] in the player. */
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
/** Called when the playing state is changed. */
|
||||
fun onPlayingChanged(isPlaying: Boolean)
|
||||
|
||||
/**
|
||||
* Called when [PlaybackStateManager] desires some [Action] to be completed. Returns true if the
|
||||
* action was consumed, false otherwise.
|
||||
*/
|
||||
fun onAction(action: Action): Boolean
|
||||
|
||||
sealed class Action {
|
||||
object RestoreState : Action()
|
||||
object ShuffleAll : Action()
|
||||
data class Open(val uri: Uri) : Action()
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package org.oxycblt.auxio.playback.state
|
||||
|
||||
import android.net.Uri
|
||||
import kotlin.math.max
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -46,7 +45,7 @@ import org.oxycblt.auxio.util.logW
|
|||
* [org.oxycblt.auxio.playback.system.PlaybackService].
|
||||
*
|
||||
* Internal consumers should usually use [Callback], however the component that manages the player
|
||||
* itself should instead operate as a [Controller].
|
||||
* itself should instead operate as a [InternalPlayer].
|
||||
*
|
||||
* All access should be done with [PlaybackStateManager.getInstance].
|
||||
* @author OxygenCobalt
|
||||
|
@ -90,17 +89,17 @@ class PlaybackStateManager private constructor() {
|
|||
var isInitialized = false
|
||||
private set
|
||||
|
||||
/** The current audio session ID of the controller. Null if no controller present. */
|
||||
/** The current audio session ID of the internal player. Null if no internal player present. */
|
||||
val currentAudioSessionId: Int?
|
||||
get() = controller?.audioSessionId
|
||||
get() = internalPlayer?.audioSessionId
|
||||
|
||||
/** An action that is awaiting the controller instance to consume it. */
|
||||
var pendingAction: ControllerAction? = null
|
||||
/** An action that is awaiting the internal player instance to consume it. */
|
||||
var pendingAction: InternalPlayer.Action? = null
|
||||
|
||||
// --- CALLBACKS ---
|
||||
|
||||
private val callbacks = mutableListOf<Callback>()
|
||||
private var controller: Controller? = null
|
||||
private var internalPlayer: InternalPlayer? = null
|
||||
|
||||
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||
@Synchronized
|
||||
|
@ -122,33 +121,33 @@ class PlaybackStateManager private constructor() {
|
|||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
/** Register a [Controller] with this instance. */
|
||||
/** Register a [InternalPlayer] with this instance. */
|
||||
@Synchronized
|
||||
fun registerController(controller: Controller) {
|
||||
if (BuildConfig.DEBUG && this.controller != null) {
|
||||
logW("Controller is already registered")
|
||||
fun registerInternalPlayer(internalPlayer: InternalPlayer) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer != null) {
|
||||
logW("Internal player is already registered")
|
||||
return
|
||||
}
|
||||
|
||||
if (isInitialized) {
|
||||
controller.loadSong(song)
|
||||
controller.seekTo(positionMs)
|
||||
controller.onPlayingChanged(isPlaying)
|
||||
requestAction(controller)
|
||||
internalPlayer.loadSong(song)
|
||||
internalPlayer.seekTo(positionMs)
|
||||
internalPlayer.onPlayingChanged(isPlaying)
|
||||
requestAction(internalPlayer)
|
||||
}
|
||||
|
||||
this.controller = controller
|
||||
this.internalPlayer = internalPlayer
|
||||
}
|
||||
|
||||
/** Unregister a [Controller] with this instance. */
|
||||
/** Unregister a [InternalPlayer] with this instance. */
|
||||
@Synchronized
|
||||
fun unregisterController(controller: Controller) {
|
||||
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||
logW("Given controller did not match current controller")
|
||||
fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
this.controller = null
|
||||
this.internalPlayer = null
|
||||
}
|
||||
|
||||
// --- PLAYING FUNCTIONS ---
|
||||
|
@ -215,7 +214,7 @@ class PlaybackStateManager private constructor() {
|
|||
@Synchronized
|
||||
fun prev() {
|
||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
||||
if (controller?.shouldPrevRewind() == true) {
|
||||
if (internalPlayer?.shouldRewindWithPrev == true) {
|
||||
rewind()
|
||||
isPlaying = true
|
||||
} else {
|
||||
|
@ -326,13 +325,13 @@ class PlaybackStateManager private constructor() {
|
|||
isShuffled = shuffled
|
||||
}
|
||||
|
||||
// --- CONTROLLER FUNCTIONS ---
|
||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
||||
|
||||
/** Update the current [positionMs]. Only meant for use by [Controller] */
|
||||
/** Update the current [positionMs]. Only meant for use by [InternalPlayer] */
|
||||
@Synchronized
|
||||
fun synchronizePosition(controller: Controller, positionMs: Long) {
|
||||
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||
logW("Given controller did not match current controller")
|
||||
fun synchronizePosition(internalPlayer: InternalPlayer, positionMs: Long) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -345,23 +344,23 @@ 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.")
|
||||
fun startAction(action: InternalPlayer.Action) {
|
||||
val internalPlayer = internalPlayer
|
||||
if (internalPlayer == null || !internalPlayer.onAction(action)) {
|
||||
logD("Internal player not present or did not consume action, ignoring")
|
||||
pendingAction = action
|
||||
}
|
||||
}
|
||||
|
||||
/** Request the stored [Controller.Action] */
|
||||
/** Request the stored [InternalPlayer.Action] */
|
||||
@Synchronized
|
||||
fun requestAction(controller: Controller) {
|
||||
if (BuildConfig.DEBUG && this.controller !== controller) {
|
||||
logW("Given controller did not match current controller")
|
||||
fun requestAction(internalPlayer: InternalPlayer) {
|
||||
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
if (pendingAction?.let(controller::onAction) == true) {
|
||||
if (pendingAction?.let(internalPlayer::onAction) == true) {
|
||||
logD("Pending action consumed")
|
||||
pendingAction = null
|
||||
}
|
||||
|
@ -374,7 +373,7 @@ class PlaybackStateManager private constructor() {
|
|||
@Synchronized
|
||||
fun seekTo(positionMs: Long) {
|
||||
this.positionMs = positionMs
|
||||
controller?.seekTo(positionMs)
|
||||
internalPlayer?.seekTo(positionMs)
|
||||
notifyPositionChanged()
|
||||
}
|
||||
|
||||
|
@ -467,7 +466,7 @@ class PlaybackStateManager private constructor() {
|
|||
notifyNewPlayback()
|
||||
|
||||
if (index > -1) {
|
||||
// Controller may have reloaded the media item, re-seek to the previous position
|
||||
// Internal player may have reloaded the media item, re-seek to the previous position
|
||||
seekTo(oldPosition)
|
||||
}
|
||||
}
|
||||
|
@ -484,7 +483,7 @@ class PlaybackStateManager private constructor() {
|
|||
// --- CALLBACKS ---
|
||||
|
||||
private fun notifyIndexMoved() {
|
||||
controller?.loadSong(song)
|
||||
internalPlayer?.loadSong(song)
|
||||
for (callback in callbacks) {
|
||||
callback.onIndexMoved(index)
|
||||
}
|
||||
|
@ -503,14 +502,14 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
private fun notifyNewPlayback() {
|
||||
controller?.loadSong(song)
|
||||
internalPlayer?.loadSong(song)
|
||||
for (callback in callbacks) {
|
||||
callback.onNewPlayback(index, queue, parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyPlayingChanged() {
|
||||
controller?.onPlayingChanged(isPlaying)
|
||||
internalPlayer?.onPlayingChanged(isPlaying)
|
||||
for (callback in callbacks) {
|
||||
callback.onPlayingChanged(isPlaying)
|
||||
}
|
||||
|
@ -534,35 +533,6 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Represents a class capable of managing the internal player. */
|
||||
interface Controller {
|
||||
val audioSessionId: Int
|
||||
|
||||
/** Called when a new song should be loaded into the player. */
|
||||
fun loadSong(song: Song?)
|
||||
|
||||
/** Seek to [positionMs] in the player. */
|
||||
fun seekTo(positionMs: Long)
|
||||
|
||||
/** Called when the class wants to determine whether it should rewind or skip back. */
|
||||
fun shouldPrevRewind(): Boolean
|
||||
|
||||
/** Called when the playing state is changed. */
|
||||
fun onPlayingChanged(isPlaying: 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()
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for receiving updates from [PlaybackStateManager]. Add the callback to
|
||||
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
|
||||
|
@ -583,7 +553,7 @@ class PlaybackStateManager private constructor() {
|
|||
/** Called when the playing state is changed. */
|
||||
fun onPlayingChanged(isPlaying: Boolean) {}
|
||||
|
||||
/** Called when the position is re-synchronized by the controller. */
|
||||
/** Called when the position is re-synchronized by the internal player. */
|
||||
fun onPositionChanged(positionMs: Long) {}
|
||||
|
||||
/** Called when the repeat mode is changed. */
|
||||
|
|
|
@ -49,6 +49,7 @@ 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.InternalPlayer
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
|
@ -76,7 +77,7 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
|||
class PlaybackService :
|
||||
Service(),
|
||||
Player.Listener,
|
||||
PlaybackStateManager.Controller,
|
||||
InternalPlayer,
|
||||
MediaSessionComponent.Callback,
|
||||
Settings.Callback,
|
||||
MusicStore.Callback {
|
||||
|
@ -146,7 +147,7 @@ class PlaybackService :
|
|||
settings = Settings(this, this)
|
||||
foregroundManager = ForegroundManager(this)
|
||||
|
||||
playbackManager.registerController(this)
|
||||
playbackManager.registerInternalPlayer(this)
|
||||
musicStore.addCallback(this)
|
||||
positionScope.launch {
|
||||
while (true) {
|
||||
|
@ -198,7 +199,7 @@ class PlaybackService :
|
|||
// Pause just in case this destruction was unexpected.
|
||||
playbackManager.isPlaying = false
|
||||
|
||||
playbackManager.unregisterController(this)
|
||||
playbackManager.unregisterInternalPlayer(this)
|
||||
settings.release()
|
||||
unregisterReceiver(systemReceiver)
|
||||
serviceJob.cancel()
|
||||
|
@ -279,6 +280,9 @@ class PlaybackService :
|
|||
override val audioSessionId: Int
|
||||
get() = player.audioSessionId
|
||||
|
||||
override val shouldRewindWithPrev: Boolean
|
||||
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||
|
||||
override fun loadSong(song: Song?) {
|
||||
if (song == null) {
|
||||
// Stop the foreground state if there's nothing to play.
|
||||
|
@ -332,29 +336,26 @@ class PlaybackService :
|
|||
player.seekTo(positionMs)
|
||||
}
|
||||
|
||||
override fun shouldPrevRewind() =
|
||||
settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
|
||||
|
||||
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||
player.playWhenReady = isPlaying
|
||||
}
|
||||
|
||||
override fun onAction(action: PlaybackStateManager.ControllerAction): Boolean {
|
||||
override fun onAction(action: InternalPlayer.Action): Boolean {
|
||||
val library = musicStore.library
|
||||
if (library != null) {
|
||||
logD("Performing action: $action")
|
||||
|
||||
when (action) {
|
||||
is PlaybackStateManager.ControllerAction.RestoreState -> {
|
||||
is InternalPlayer.Action.RestoreState -> {
|
||||
restoreScope.launch {
|
||||
playbackManager.restoreState(
|
||||
PlaybackStateDatabase.getInstance(this@PlaybackService), false)
|
||||
}
|
||||
}
|
||||
is PlaybackStateManager.ControllerAction.ShuffleAll -> {
|
||||
is InternalPlayer.Action.ShuffleAll -> {
|
||||
playbackManager.shuffleAll(settings)
|
||||
}
|
||||
is PlaybackStateManager.ControllerAction.Open -> {
|
||||
is InternalPlayer.Action.Open -> {
|
||||
library.findSongForUri(application, action.uri)?.let { song ->
|
||||
playbackManager.play(song, settings.libPlaybackMode, settings)
|
||||
}
|
||||
|
@ -389,6 +390,7 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
// --- MUSICSTORE OVERRIDES ---
|
||||
|
||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||
if (library != null) {
|
||||
playbackManager.requestAction(this)
|
||||
|
@ -477,7 +479,7 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
companion object {
|
||||
private const val POS_POLL_INTERVAL = 1000L
|
||||
private const val POS_POLL_INTERVAL = 100L
|
||||
private const val REWIND_THRESHOLD = 3000L
|
||||
|
||||
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||
|
|
|
@ -9,7 +9,7 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.0-alpha09'
|
||||
classpath 'com.android.tools.build:gradle:7.4.0-alpha10'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"
|
||||
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.6.1"
|
||||
|
|
Loading…
Reference in a new issue