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:
Alexander Capehart 2022-08-29 09:13:37 -06:00
parent 8a15868ba1
commit 4d02dfb578
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 120 additions and 101 deletions

View file

@ -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
}

View file

@ -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 ->

View file

@ -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
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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. */

View file

@ -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"

View file

@ -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"