playback: fix service state issues
Fix a bunch of miscellanious state issues with the playback fragment.
This commit is contained in:
parent
8e849feb7d
commit
6adc5f8715
5 changed files with 69 additions and 39 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ---
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue