Add looping

Add the ability to loop songs either once or infinitely.
This commit is contained in:
OxygenCobalt 2020-10-30 14:51:49 -06:00
parent ce96dd6e94
commit 535fc95f71
10 changed files with 135 additions and 4 deletions

View file

@ -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) {

View file

@ -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

View file

@ -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<Boolean> get() = mIsShuffling
private val mLoopMode = MutableLiveData(LoopMode.NONE)
val loopMode: LiveData<LoopMode> get() = mLoopMode
// Other
private val mIsSeeking = MutableLiveData(false)
val isSeeking: LiveData<Boolean> 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
}
}

View file

@ -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
}
}
}

View file

@ -6,9 +6,10 @@ interface PlaybackStateCallback {
fun onSongUpdate(song: Song?) {}
fun onPositionUpdate(position: Long) {}
fun onQueueUpdate(queue: MutableList<Song>) {}
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) {}

View file

@ -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<Song> {
@ -365,5 +389,9 @@ class PlaybackStateManager {
return newInstance
}
}
const val LOOP_NONE = 0
const val LOOP_ONCE = 1
const val LOOP_ENDLESS = 2
}
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?android:attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4zM13,15L13,9h-1l-2,1v1h1.5v4L13,15z"/>
</vector>

View file

@ -201,5 +201,19 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause" />
<ImageButton
android:id="@+id/playback_loop"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="@dimen/size_play_pause_compact"
android:layout_height="@dimen/size_play_pause_compact"
android:background="@drawable/ui_unbounded_ripple"
android:src="@drawable/ic_loop"
android:layout_marginStart="@dimen/margin_mid_large"
android:onClick="@{() -> playbackModel.incrementLoopStatus()}"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/playback_play_pause"
android:contentDescription="@string/description_loop"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -50,6 +50,7 @@
<string name="description_skip_prev">Skip to last song</string>
<string name="description_shuffle_on">Turn shuffle on</string>
<string name="description_shuffle_off">Turn shuffle off</string>
<string name="description_loop">Change Loop Mode</string>
<!-- Placeholder Namespace | Placeholder values -->
<string name="placeholder_genre">Unknown Genre</string>