diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 429a26644..1dc796744 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -15,6 +15,7 @@ import androidx.navigation.fragment.findNavController import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBinding import org.oxycblt.auxio.playback.queue.QueueFragment +import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.theme.accent import org.oxycblt.auxio.theme.disable import org.oxycblt.auxio.theme.enable @@ -123,6 +124,23 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { } } + playbackModel.loopMode.observe(viewLifecycleOwner) { + when (it) { + LoopMode.NONE -> { + binding.playbackLoop.imageTintList = controlColor + binding.playbackLoop.setImageResource(R.drawable.ic_loop) + } + LoopMode.ONCE -> { + binding.playbackLoop.imageTintList = accentColor + binding.playbackLoop.setImageResource(R.drawable.ic_loop_one) + } + LoopMode.INFINITE -> { + binding.playbackLoop.imageTintList = accentColor + binding.playbackLoop.setImageResource(R.drawable.ic_loop) + } + } + } + playbackModel.isSeeking.observe(viewLifecycleOwner) { // Highlight the current duration if the user is seeking, and revert it if not. if (it) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt index abc257d8f..84d6f9444 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toURI +import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackStateCallback import org.oxycblt.auxio.playback.state.PlaybackStateManager @@ -131,6 +132,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { } } + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + // If the song loops while in the LOOP_ONCE mode, then stop looping after that. + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT && + playbackManager.loopMode == LoopMode.ONCE + ) { + playbackManager.setLoopMode(LoopMode.NONE) + } + } + // --- PLAYBACK STATE CALLBACK OVERRIDES --- override fun onSongUpdate(song: Song?) { @@ -160,6 +170,17 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { } } + override fun onLoopUpdate(mode: LoopMode) { + when (mode) { + LoopMode.NONE -> { + player.repeatMode = Player.REPEAT_MODE_OFF + } + else -> { + player.repeatMode = Player.REPEAT_MODE_ONE + } + } + } + override fun onSeekConfirm(position: Long) { player.seekTo(position * 1000) } @@ -253,8 +274,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateCallback { return notif } - // Broadcast Receiver for receiving system events [E.G Headphones connecte/disconnected - inner class SystemEventReceiver : BroadcastReceiver() { + // BroadcastReceiver for receiving system events [E.G Headphones connected/disconnected] + private inner class SystemEventReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val action = intent.action 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 2b0488add..680e337fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -10,6 +10,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.toDuration +import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackStateCallback import org.oxycblt.auxio.playback.state.PlaybackStateManager @@ -40,6 +41,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback { private val mIsShuffling = MutableLiveData(false) val isShuffling: LiveData get() = mIsShuffling + private val mLoopMode = MutableLiveData(LoopMode.NONE) + val loopMode: LiveData get() = mLoopMode + // Other private val mIsSeeking = MutableLiveData(false) val isSeeking: LiveData get() = mIsSeeking @@ -176,6 +180,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback { playbackManager.setShuffleStatus(!playbackManager.isShuffling) } + fun incrementLoopStatus() { + playbackManager.setLoopMode(playbackManager.loopMode.increment()) + } + // --- OTHER FUNCTIONS --- fun setSeekingStatus(value: Boolean) { @@ -218,6 +226,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback { mIsShuffling.value = isShuffling } + override fun onLoopUpdate(mode: LoopMode) { + mLoopMode.value = mode + } + private fun restorePlaybackState() { Log.d(this::class.simpleName, "Attempting to restore playback state.") @@ -227,5 +239,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateCallback { mIndex.value = playbackManager.index mIsPlaying.value = playbackManager.isPlaying mIsShuffling.value = playbackManager.isShuffling + mLoopMode.value = playbackManager.loopMode } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt new file mode 100644 index 000000000..983b4f548 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt @@ -0,0 +1,13 @@ +package org.oxycblt.auxio.playback.state + +enum class LoopMode { + NONE, ONCE, INFINITE; + + fun increment(): LoopMode { + return when (this) { + NONE -> ONCE + ONCE -> INFINITE + INFINITE -> NONE + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt index 9fdb2f2fd..353396140 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateCallback.kt @@ -6,9 +6,10 @@ interface PlaybackStateCallback { fun onSongUpdate(song: Song?) {} fun onPositionUpdate(position: Long) {} fun onQueueUpdate(queue: MutableList) {} + fun onIndexUpdate(index: Int) {} fun onPlayingUpdate(isPlaying: Boolean) {} fun onShuffleUpdate(isShuffling: Boolean) {} - fun onIndexUpdate(index: Int) {} + fun onLoopUpdate(mode: LoopMode) {} // Service callbacks fun onSeekConfirm(position: Long) {} 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 a830745cf..7bc275a0d 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 @@ -53,6 +53,11 @@ class PlaybackStateManager { callbacks.forEach { it.onShuffleUpdate(value) } } private var mShuffleSeed = -1L + private var mLoopMode = LoopMode.NONE + set(value) { + field = value + callbacks.forEach { it.onLoopUpdate(value) } + } val song: Song? get() = mSong val position: Long get() = mPosition @@ -60,6 +65,7 @@ class PlaybackStateManager { val index: Int get() = mIndex val isPlaying: Boolean get() = mIsPlaying val isShuffling: Boolean get() = mIsShuffling + val loopMode: LoopMode get() = mLoopMode // --- CALLBACKS --- @@ -90,6 +96,7 @@ class PlaybackStateManager { mMode = mode + resetLoopMode() updatePlayback(song) mQueue = when (mode) { @@ -144,6 +151,8 @@ class PlaybackStateManager { else -> error("what") } + resetLoopMode() + updatePlayback(mQueue[0]) mIndex = 0 @@ -185,10 +194,12 @@ class PlaybackStateManager { // --- QUEUE FUNCTIONS --- fun next() { + resetLoopMode() + if (mIndex < mQueue.lastIndex) { mIndex = mIndex.inc() } else { - // TODO: Implement option so that the playlist loops instead of stops + // TODO: Implement option to make the playlist loop instead of stop mQueue = mutableListOf() mSong = null @@ -205,6 +216,8 @@ class PlaybackStateManager { mIndex = mIndex.dec() } + resetLoopMode() + updatePlayback(mQueue[mIndex]) forceQueueUpdate() @@ -318,6 +331,17 @@ class PlaybackStateManager { } } + fun setLoopMode(mode: LoopMode) { + mLoopMode = mode + } + + private fun resetLoopMode() { + // Reset the loop mode froM ONCE if needed. + if (mLoopMode == LoopMode.ONCE) { + mLoopMode = LoopMode.NONE + } + } + // --- ORDERING FUNCTIONS --- private fun orderSongsInAlbum(album: Album): MutableList { @@ -365,5 +389,9 @@ class PlaybackStateManager { return newInstance } } + + const val LOOP_NONE = 0 + const val LOOP_ONCE = 1 + const val LOOP_ENDLESS = 2 } } diff --git a/app/src/main/res/drawable/ic_loop.xml b/app/src/main/res/drawable/ic_loop.xml new file mode 100644 index 000000000..52e22f421 --- /dev/null +++ b/app/src/main/res/drawable/ic_loop.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_loop_one.xml b/app/src/main/res/drawable/ic_loop_one.xml new file mode 100644 index 000000000..d9f548be3 --- /dev/null +++ b/app/src/main/res/drawable/ic_loop_one.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_playback.xml b/app/src/main/res/layout/fragment_playback.xml index d714d11fe..7c98de237 100644 --- a/app/src/main/res/layout/fragment_playback.xml +++ b/app/src/main/res/layout/fragment_playback.xml @@ -201,5 +201,19 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/playback_play_pause" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eaa2ed087..b34610bc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,6 +50,7 @@ Skip to last song Turn shuffle on Turn shuffle off + Change Loop Mode Unknown Genre