diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 7a25701e6..2cda9fed0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -36,6 +36,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.clamp import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.stateList @@ -177,8 +178,9 @@ class PlaybackPanelFragment : val seconds = song.seconds binding.playbackDuration.textSafe = seconds.toDuration(false) binding.playbackSeekBar.apply { - valueTo = max(seconds, 1L).toFloat() isEnabled = seconds > 0L + valueTo = max(seconds, 1L).toFloat() + value = seconds.clamp(0, valueTo.toLong()).toFloat() } } 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 6f7fc8bf9..c79854bd8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -100,7 +100,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // 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 // PlaybackStateManager. - if (playbackManager.isRestored) { + if (playbackManager.isInitialized) { restorePlaybackState() } } @@ -162,7 +162,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { */ fun playWithUri(uri: Uri, context: Context) { // 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) } else { 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. playbackManager.markRestored() - } else if (!playbackManager.isRestored) { + } else if (!playbackManager.isInitialized) { // Otherwise just restore viewModelScope.launch { playbackManager.restoreFromDatabase(context) } } 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 2fd983b20..0119b45a4 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 @@ -81,8 +81,8 @@ class PlaybackStateManager private constructor() { var isShuffled = false private set - /** Whether this instance has already been restored */ - var isRestored = false + /** Whether this instance has been initialized */ + var isInitialized = false private set // --- CALLBACKS --- @@ -122,7 +122,9 @@ class PlaybackStateManager private constructor() { applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song, true) notifyNewPlayback() notifyShuffledChanged() + seekTo(0) isPlaying = true + isInitialized = true } /** @@ -135,7 +137,9 @@ class PlaybackStateManager private constructor() { applyNewQueue(library, shuffled, null, true) notifyNewPlayback() notifyShuffledChanged() + seekTo(0) isPlaying = true + isInitialized = true } /** Shuffle all songs. */ @@ -145,7 +149,9 @@ class PlaybackStateManager private constructor() { applyNewQueue(library, true, null, true) notifyNewPlayback() notifyShuffledChanged() + seekTo(0) isPlaying = true + isInitialized = true } // --- QUEUE FUNCTIONS --- @@ -175,6 +181,7 @@ class PlaybackStateManager private constructor() { private fun goto(idx: Int, play: Boolean) { index = idx notifyIndexMoved() + seekTo(0) isPlaying = play } @@ -304,7 +311,7 @@ class PlaybackStateManager private constructor() { /** Repeat the current song (in line with the user configuration). */ fun repeat() { - seekTo(0) + rewind() if (settingsManager.pauseOnRepeat) { isPlaying = false } @@ -314,7 +321,7 @@ class PlaybackStateManager private constructor() { /** Mark this instance as restored. */ fun markRestored() { - isRestored = true + isInitialized = true } // --- PERSISTENCE FUNCTIONS --- 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 0519101ed..057d08431 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 @@ -24,6 +24,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.ServiceInfo +import android.graphics.Bitmap import android.media.AudioManager import android.os.Build import android.os.IBinder @@ -52,8 +53,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.music.MusicParent 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.RepeatMode import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.logD @@ -98,27 +99,15 @@ class PlaybackService : // State private var isForeground = false + private var hasPlayed = false + // Coroutines private val serviceJob = Job() private val positionScope = CoroutineScope(serviceJob + Dispatchers.Main) private val saveScope = CoroutineScope(serviceJob + Dispatchers.Main) // --- 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() { super.onCreate() @@ -142,7 +131,6 @@ class PlaybackService : // Then the notification/headset callbacks IntentFilter().apply { - addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY) addAction(AudioManager.ACTION_HEADSET_PLUG) @@ -165,7 +153,7 @@ class PlaybackService : // --- PLAYBACKSTATEMANAGER SETUP --- playbackManager.addCallback(this) - if (playbackManager.song != null || playbackManager.isRestored) { + if (playbackManager.isInitialized) { restore() } @@ -176,12 +164,29 @@ class PlaybackService : 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() { super.onDestroy() - stopForegroundAndNotification() + stopForeground(true) + isForeground = false + unregisterReceiver(systemReceiver) + serviceJob.cancel() player.release() connector.release() mediaSession.release() @@ -193,13 +198,6 @@ class PlaybackService : // Pause just in case this destruction was unexpected. 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") } @@ -207,6 +205,11 @@ class PlaybackService : override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { super.onPlayWhenReadyChanged(playWhenReady, reason) + + if (playWhenReady) { + hasPlayed = true + } + if (playbackManager.isPlaying != playWhenReady) { playbackManager.isPlaying = playWhenReady } @@ -277,7 +280,7 @@ class PlaybackService : // Clear if there's nothing to play. logD("Nothing playing, stopping playback") player.stop() - stopForegroundAndNotification() + stopAndSave() } override fun onPlayingChanged(isPlaying: Boolean) { @@ -379,6 +382,7 @@ class PlaybackService : onSongChanged(playbackManager.song) onSeek(playbackManager.positionMs) + onPlayingChanged(playbackManager.isPlaying) onShuffledChanged(playbackManager.isShuffled) onRepeatChanged(playbackManager.repeatMode) @@ -390,7 +394,7 @@ class PlaybackService : * Bring the service into the foreground and show the notification, or refresh the notification. */ private fun startForegroundOrNotify() { - if (/*playbackManager.hasPlayed &&*/ playbackManager.song != null) { + if (hasPlayed && playbackManager.song != null) { logD("Starting foreground/notifying") if (!isForeground) { @@ -413,12 +417,25 @@ class PlaybackService : } /** Stop the foreground state and hide the notification */ - private fun stopForegroundAndNotification() { + private fun stopAndSave() { stopForeground(true) - notificationManager.cancel(IntegerTable.NOTIFICATION_CODE) 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. */ private inner class PlaybackReceiver : BroadcastReceiver() { private var initialHeadsetPlugEventHandled = false @@ -450,13 +467,14 @@ class PlaybackService : // --- AUXIO EVENTS --- 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_SKIP_PREV -> playbackManager.prev() ACTION_SKIP_NEXT -> playbackManager.next() ACTION_EXIT -> { playbackManager.isPlaying = false - stopForegroundAndNotification() + stopAndSave() } WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update() } diff --git a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt index 281f2fcca..11e71826e 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/PrimitiveUtil.kt @@ -42,3 +42,6 @@ fun unlikelyToBeNull(value: T?): T { /** Shortcut to clamp an integer between [min] and [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)