diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index fd9818278..e57bd5da1 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -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 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index ff83df09d..9e3ce034d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -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 -> diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index be68bb4b3..ffdc38736 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -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 } 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 21aeef7bd..694f3bd0b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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 get() = _positionSecs + private val _repeatMode = MutableStateFlow(RepeatMode.NONE) /** The current repeat mode, see [RepeatMode] for more information */ val repeatMode: StateFlow @@ -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) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt new file mode 100644 index 000000000..dfd3b6099 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt @@ -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 . + */ + +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() + } +} 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 5a1fc5b37..2de039461 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,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() - 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. */ 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 6e055033a..28d39bbf9 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 @@ -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" diff --git a/build.gradle b/build.gradle index 3973b3533..4699d5121 100644 --- a/build.gradle +++ b/build.gradle @@ -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"