playback: fix service state issues

Fix a bunch of miscellanious state issues with the playback fragment.
This commit is contained in:
OxygenCobalt 2022-04-29 16:17:26 -06:00
parent 8e849feb7d
commit 6adc5f8715
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
5 changed files with 69 additions and 39 deletions

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
import org.oxycblt.auxio.util.clamp
import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getAttrColorSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.stateList
@ -177,8 +178,9 @@ class PlaybackPanelFragment :
val seconds = song.seconds val seconds = song.seconds
binding.playbackDuration.textSafe = seconds.toDuration(false) binding.playbackDuration.textSafe = seconds.toDuration(false)
binding.playbackSeekBar.apply { binding.playbackSeekBar.apply {
valueTo = max(seconds, 1L).toFloat()
isEnabled = seconds > 0L isEnabled = seconds > 0L
valueTo = max(seconds, 1L).toFloat()
value = seconds.clamp(0, valueTo.toLong()).toFloat()
} }
} }

View file

@ -100,7 +100,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// around & the fact that we are in the init function], then attempt to restore the // around & the fact that we are in the init function], then attempt to restore the
// ViewModel state. If it isn't, then wait for MainFragment to give the command to restore // ViewModel state. If it isn't, then wait for MainFragment to give the command to restore
// PlaybackStateManager. // PlaybackStateManager.
if (playbackManager.isRestored) { if (playbackManager.isInitialized) {
restorePlaybackState() restorePlaybackState()
} }
} }
@ -162,7 +162,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/ */
fun playWithUri(uri: Uri, context: Context) { fun playWithUri(uri: Uri, context: Context) {
// Check if everything is already running to run the URI play // Check if everything is already running to run the URI play
if (playbackManager.isRestored && musicStore.library != null) { if (playbackManager.isInitialized && musicStore.library != null) {
playWithUriInternal(uri, context) playWithUriInternal(uri, context)
} else { } else {
logD("Cant play this URI right now, waiting") logD("Cant play this URI right now, waiting")
@ -296,7 +296,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Were not going to be restoring playbackManager after this, so mark it as such. // Were not going to be restoring playbackManager after this, so mark it as such.
playbackManager.markRestored() playbackManager.markRestored()
} else if (!playbackManager.isRestored) { } else if (!playbackManager.isInitialized) {
// Otherwise just restore // Otherwise just restore
viewModelScope.launch { playbackManager.restoreFromDatabase(context) } viewModelScope.launch { playbackManager.restoreFromDatabase(context) }
} }

View file

@ -81,8 +81,8 @@ class PlaybackStateManager private constructor() {
var isShuffled = false var isShuffled = false
private set private set
/** Whether this instance has already been restored */ /** Whether this instance has been initialized */
var isRestored = false var isInitialized = false
private set private set
// --- CALLBACKS --- // --- CALLBACKS ---
@ -122,7 +122,9 @@ class PlaybackStateManager private constructor() {
applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song, true) applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song, true)
notifyNewPlayback() notifyNewPlayback()
notifyShuffledChanged() notifyShuffledChanged()
seekTo(0)
isPlaying = true isPlaying = true
isInitialized = true
} }
/** /**
@ -135,7 +137,9 @@ class PlaybackStateManager private constructor() {
applyNewQueue(library, shuffled, null, true) applyNewQueue(library, shuffled, null, true)
notifyNewPlayback() notifyNewPlayback()
notifyShuffledChanged() notifyShuffledChanged()
seekTo(0)
isPlaying = true isPlaying = true
isInitialized = true
} }
/** Shuffle all songs. */ /** Shuffle all songs. */
@ -145,7 +149,9 @@ class PlaybackStateManager private constructor() {
applyNewQueue(library, true, null, true) applyNewQueue(library, true, null, true)
notifyNewPlayback() notifyNewPlayback()
notifyShuffledChanged() notifyShuffledChanged()
seekTo(0)
isPlaying = true isPlaying = true
isInitialized = true
} }
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
@ -175,6 +181,7 @@ class PlaybackStateManager private constructor() {
private fun goto(idx: Int, play: Boolean) { private fun goto(idx: Int, play: Boolean) {
index = idx index = idx
notifyIndexMoved() notifyIndexMoved()
seekTo(0)
isPlaying = play isPlaying = play
} }
@ -304,7 +311,7 @@ class PlaybackStateManager private constructor() {
/** Repeat the current song (in line with the user configuration). */ /** Repeat the current song (in line with the user configuration). */
fun repeat() { fun repeat() {
seekTo(0) rewind()
if (settingsManager.pauseOnRepeat) { if (settingsManager.pauseOnRepeat) {
isPlaying = false isPlaying = false
} }
@ -314,7 +321,7 @@ class PlaybackStateManager private constructor() {
/** Mark this instance as restored. */ /** Mark this instance as restored. */
fun markRestored() { fun markRestored() {
isRestored = true isInitialized = true
} }
// --- PERSISTENCE FUNCTIONS --- // --- PERSISTENCE FUNCTIONS ---

View file

@ -24,6 +24,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.media.AudioManager import android.media.AudioManager
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
@ -52,8 +53,8 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.getSystemServiceSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -98,27 +99,15 @@ class PlaybackService :
// State // State
private var isForeground = false private var isForeground = false
private var hasPlayed = false
// 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 saveScope = CoroutineScope(serviceJob + Dispatchers.Main) private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main)
// --- SERVICE OVERRIDES --- // --- SERVICE OVERRIDES ---
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
// Workaround to get GadgetBridge and other apps that blindly query for
// ACTION_MEDIA_BUTTON working.
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
return START_NOT_STICKY
}
// No binding, service is headless
// Communicate using PlaybackStateManager, SettingsManager, or Broadcasts instead.
override fun onBind(intent: Intent): IBinder? = null
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -142,7 +131,6 @@ class PlaybackService :
// Then the notification/headset callbacks // Then the notification/headset callbacks
IntentFilter().apply { IntentFilter().apply {
addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)
addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
addAction(AudioManager.ACTION_HEADSET_PLUG) addAction(AudioManager.ACTION_HEADSET_PLUG)
@ -165,7 +153,7 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP --- // --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.addCallback(this) playbackManager.addCallback(this)
if (playbackManager.song != null || playbackManager.isRestored) { if (playbackManager.isInitialized) {
restore() restore()
} }
@ -176,12 +164,29 @@ class PlaybackService :
logD("Service created") logD("Service created")
} }
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
// Workaround to get GadgetBridge and other apps that blindly query for
// ACTION_MEDIA_BUTTON working.
MediaButtonReceiver.handleIntent(mediaSession, intent)
}
return START_NOT_STICKY
}
// No binding, service is headless
// Communicate using PlaybackStateManager, SettingsManager, or Broadcasts instead.
override fun onBind(intent: Intent): IBinder? = null
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopForegroundAndNotification() stopForeground(true)
isForeground = false
unregisterReceiver(systemReceiver) unregisterReceiver(systemReceiver)
serviceJob.cancel()
player.release() player.release()
connector.release() connector.release()
mediaSession.release() mediaSession.release()
@ -193,13 +198,6 @@ class PlaybackService :
// Pause just in case this destruction was unexpected. // Pause just in case this destruction was unexpected.
playbackManager.isPlaying = false playbackManager.isPlaying = false
// The service coroutines last job is to save the state to the DB, before terminating itself
// FIXME: This is a terrible idea, move this to when the user closes the notification
saveScope.launch {
playbackManager.saveStateToDatabase(this@PlaybackService)
serviceJob.cancel()
}
logD("Service destroyed") logD("Service destroyed")
} }
@ -207,6 +205,11 @@ class PlaybackService :
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason) super.onPlayWhenReadyChanged(playWhenReady, reason)
if (playWhenReady) {
hasPlayed = true
}
if (playbackManager.isPlaying != playWhenReady) { if (playbackManager.isPlaying != playWhenReady) {
playbackManager.isPlaying = playWhenReady playbackManager.isPlaying = playWhenReady
} }
@ -277,7 +280,7 @@ class PlaybackService :
// Clear if there's nothing to play. // Clear if there's nothing to play.
logD("Nothing playing, stopping playback") logD("Nothing playing, stopping playback")
player.stop() player.stop()
stopForegroundAndNotification() stopAndSave()
} }
override fun onPlayingChanged(isPlaying: Boolean) { override fun onPlayingChanged(isPlaying: Boolean) {
@ -379,6 +382,7 @@ class PlaybackService :
onSongChanged(playbackManager.song) onSongChanged(playbackManager.song)
onSeek(playbackManager.positionMs) onSeek(playbackManager.positionMs)
onPlayingChanged(playbackManager.isPlaying)
onShuffledChanged(playbackManager.isShuffled) onShuffledChanged(playbackManager.isShuffled)
onRepeatChanged(playbackManager.repeatMode) onRepeatChanged(playbackManager.repeatMode)
@ -390,7 +394,7 @@ class PlaybackService :
* Bring the service into the foreground and show the notification, or refresh the notification. * Bring the service into the foreground and show the notification, or refresh the notification.
*/ */
private fun startForegroundOrNotify() { private fun startForegroundOrNotify() {
if (/*playbackManager.hasPlayed &&*/ playbackManager.song != null) { if (hasPlayed && playbackManager.song != null) {
logD("Starting foreground/notifying") logD("Starting foreground/notifying")
if (!isForeground) { if (!isForeground) {
@ -413,12 +417,25 @@ class PlaybackService :
} }
/** Stop the foreground state and hide the notification */ /** Stop the foreground state and hide the notification */
private fun stopForegroundAndNotification() { private fun stopAndSave() {
stopForeground(true) stopForeground(true)
notificationManager.cancel(IntegerTable.NOTIFICATION_CODE)
isForeground = false isForeground = false
saveScope.launch { playbackManager.saveStateToDatabase(this@PlaybackService) }
} }
data class Metadata(
val title: String,
val album: String,
val artist: String,
val album_artist: String,
val genre: String,
val parent: String,
val year: Int,
val track: Int?,
val albumCover: Bitmap
)
/** A [BroadcastReceiver] for receiving general playback events from the system. */ /** A [BroadcastReceiver] for receiving general playback events from the system. */
private inner class PlaybackReceiver : BroadcastReceiver() { private inner class PlaybackReceiver : BroadcastReceiver() {
private var initialHeadsetPlugEventHandled = false private var initialHeadsetPlugEventHandled = false
@ -450,13 +467,14 @@ class PlaybackService :
// --- AUXIO EVENTS --- // --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying
ACTION_INC_REPEAT_MODE -> playbackManager.repeatMode = playbackManager.repeatMode.increment() ACTION_INC_REPEAT_MODE ->
playbackManager.repeatMode = playbackManager.repeatMode.increment()
ACTION_INVERT_SHUFFLE -> playbackManager.reshuffle(!playbackManager.isShuffled) ACTION_INVERT_SHUFFLE -> playbackManager.reshuffle(!playbackManager.isShuffled)
ACTION_SKIP_PREV -> playbackManager.prev() ACTION_SKIP_PREV -> playbackManager.prev()
ACTION_SKIP_NEXT -> playbackManager.next() ACTION_SKIP_NEXT -> playbackManager.next()
ACTION_EXIT -> { ACTION_EXIT -> {
playbackManager.isPlaying = false playbackManager.isPlaying = false
stopForegroundAndNotification() stopAndSave()
} }
WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update() WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update()
} }

View file

@ -42,3 +42,6 @@ fun <T> unlikelyToBeNull(value: T?): T {
/** Shortcut to clamp an integer between [min] and [max] */ /** Shortcut to clamp an integer between [min] and [max] */
fun Int.clamp(min: Int, max: Int): Int = MathUtils.clamp(this, min, max) fun Int.clamp(min: Int, max: Int): Int = MathUtils.clamp(this, min, max)
/** Shortcut to clamp an integer between [min] and [max] */
fun Long.clamp(min: Long, max: Long): Long = MathUtils.clamp(this, min, max)