queue: rework internal queue system
Rework the queue internally to decouple the queue from playback and better respond to reshuffling. This is being implemented under the assumption that I will be implementing the sliding queue eventually.
This commit is contained in:
parent
496b72ca78
commit
6c59a03042
13 changed files with 178 additions and 91 deletions
|
@ -28,10 +28,10 @@ import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||||
import org.oxycblt.auxio.ui.recycler.AsyncBackingData
|
import org.oxycblt.auxio.ui.recycler.AsyncBackingData
|
||||||
import org.oxycblt.auxio.ui.recycler.BindingViewHolder
|
import org.oxycblt.auxio.ui.recycler.BindingViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
|
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.MultiAdapter
|
import org.oxycblt.auxio.ui.recycler.MultiAdapter
|
||||||
import org.oxycblt.auxio.ui.recycler.NewHeaderViewHolder
|
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
@ -70,14 +70,14 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
|
|
||||||
override fun getCreatorFromItem(item: Item) =
|
override fun getCreatorFromItem(item: Item) =
|
||||||
when (item) {
|
when (item) {
|
||||||
is Header -> NewHeaderViewHolder.CREATOR
|
is Header -> HeaderViewHolder.CREATOR
|
||||||
is SortHeader -> SortHeaderViewHolder.CREATOR
|
is SortHeader -> SortHeaderViewHolder.CREATOR
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCreatorFromViewType(viewType: Int) =
|
override fun getCreatorFromViewType(viewType: Int) =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
NewHeaderViewHolder.CREATOR.viewType -> NewHeaderViewHolder.CREATOR
|
HeaderViewHolder.CREATOR.viewType -> HeaderViewHolder.CREATOR
|
||||||
SortHeaderViewHolder.CREATOR.viewType -> SortHeaderViewHolder.CREATOR
|
SortHeaderViewHolder.CREATOR.viewType -> SortHeaderViewHolder.CREATOR
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@ -90,7 +90,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
) {
|
) {
|
||||||
if (payload.isEmpty()) {
|
if (payload.isEmpty()) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
|
is Header -> (viewHolder as HeaderViewHolder).bind(item, Unit)
|
||||||
is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener)
|
is SortHeader -> (viewHolder as SortHeaderViewHolder).bind(item, listener)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||||
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
|
||||||
return when {
|
return when {
|
||||||
oldItem is Header && newItem is Header ->
|
oldItem is Header && newItem is Header ->
|
||||||
NewHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
oldItem is SortHeader && newItem is SortHeader ->
|
oldItem is SortHeader && newItem is SortHeader ->
|
||||||
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
SortHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
else -> false
|
else -> false
|
||||||
|
|
|
@ -114,8 +114,6 @@ class PlaybackPanelFragment :
|
||||||
collectImmediately(playbackModel.repeatMode, ::updateRepeat)
|
collectImmediately(playbackModel.repeatMode, ::updateRepeat)
|
||||||
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
|
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
|
||||||
collectImmediately(playbackModel.isShuffled, ::updateShuffled)
|
collectImmediately(playbackModel.isShuffled, ::updateShuffled)
|
||||||
collectImmediately(playbackModel.nextUp, ::updateNextUp)
|
|
||||||
|
|
||||||
logD("Fragment Created")
|
logD("Fragment Created")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,8 +193,4 @@ class PlaybackPanelFragment :
|
||||||
private fun updateShuffled(isShuffled: Boolean) {
|
private fun updateShuffled(isShuffled: Boolean) {
|
||||||
requireBinding().playbackShuffle.isActivated = isShuffled
|
requireBinding().playbackShuffle.isActivated = isShuffled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNextUp(nextUp: List<Song>) {
|
|
||||||
queueItem.isEnabled = nextUp.isNotEmpty()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||||
import org.oxycblt.auxio.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.application
|
import org.oxycblt.auxio.util.application
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,11 +76,6 @@ class PlaybackViewModel(application: Application) :
|
||||||
val isShuffled: StateFlow<Boolean>
|
val isShuffled: StateFlow<Boolean>
|
||||||
get() = _isShuffled
|
get() = _isShuffled
|
||||||
|
|
||||||
private val _nextUp = MutableStateFlow(listOf<Song>())
|
|
||||||
/** The queue, without the previous items. */
|
|
||||||
val nextUp: StateFlow<List<Song>>
|
|
||||||
get() = _nextUp
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicStore.addCallback(this)
|
musicStore.addCallback(this)
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
@ -196,39 +190,6 @@ class PlaybackViewModel(application: Application) :
|
||||||
playbackManager.prev()
|
playbackManager.prev()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to an item in the queue using it's recyclerview adapter index. No-ops if out of bounds.
|
|
||||||
*/
|
|
||||||
fun goto(adapterIndex: Int) {
|
|
||||||
val index = adapterIndex + (playbackManager.queue.size - _nextUp.value.size)
|
|
||||||
logD(adapterIndex)
|
|
||||||
logD(playbackManager.queue.size - _nextUp.value.size)
|
|
||||||
|
|
||||||
if (index in playbackManager.queue.indices) {
|
|
||||||
playbackManager.goto(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Remove a queue item using it's recyclerview adapter index. */
|
|
||||||
fun removeQueueDataItem(adapterIndex: Int) {
|
|
||||||
val index = adapterIndex + (playbackManager.queue.size - _nextUp.value.size)
|
|
||||||
if (index in playbackManager.queue.indices) {
|
|
||||||
playbackManager.removeQueueItem(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/** Move queue items using their recyclerview adapter indices. */
|
|
||||||
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean {
|
|
||||||
val delta = (playbackManager.queue.size - _nextUp.value.size)
|
|
||||||
val from = adapterFrom + delta
|
|
||||||
val to = adapterTo + delta
|
|
||||||
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
|
|
||||||
playbackManager.moveQueueItem(from, to)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Add a [Song] to the top of the queue. */
|
/** Add a [Song] to the top of the queue. */
|
||||||
fun playNext(song: Song) {
|
fun playNext(song: Song) {
|
||||||
playbackManager.playNext(song)
|
playbackManager.playNext(song)
|
||||||
|
@ -334,17 +295,11 @@ class PlaybackViewModel(application: Application) :
|
||||||
|
|
||||||
override fun onIndexMoved(index: Int) {
|
override fun onIndexMoved(index: Int) {
|
||||||
_song.value = playbackManager.song
|
_song.value = playbackManager.song
|
||||||
_nextUp.value = playbackManager.queue.slice(index + 1 until playbackManager.queue.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
|
||||||
_nextUp.value = queue.slice(index + 1 until queue.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||||
_song.value = playbackManager.song
|
_song.value = playbackManager.song
|
||||||
_parent.value = playbackManager.parent
|
_parent.value = playbackManager.parent
|
||||||
_nextUp.value = queue.slice(index + 1 until queue.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPositionChanged(positionMs: Long) {
|
override fun onPositionChanged(positionMs: Long) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.ui.recycler.*
|
import org.oxycblt.auxio.ui.recycler.*
|
||||||
import org.oxycblt.auxio.util.*
|
import org.oxycblt.auxio.util.*
|
||||||
|
|
||||||
class QueueAdapter(private val listener: QueueItemListener) :
|
class QueueAdapter(listener: QueueItemListener) :
|
||||||
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
|
MonoAdapter<Song, QueueItemListener, QueueSongViewHolder>(listener) {
|
||||||
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER)
|
override val data = SyncBackingData(this, QueueSongViewHolder.DIFFER)
|
||||||
override val creator = QueueSongViewHolder.CREATOR
|
override val creator = QueueSongViewHolder.CREATOR
|
||||||
|
|
|
@ -27,7 +27,6 @@ import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.sign
|
import kotlin.math.sign
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
import org.oxycblt.auxio.util.getDimenSafe
|
import org.oxycblt.auxio.util.getDimenSafe
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
@ -37,7 +36,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
* hot garbage. This shouldn't have *too many* UI bugs. I hope.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() {
|
class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() {
|
||||||
private var shouldLift = true
|
private var shouldLift = true
|
||||||
|
|
||||||
override fun getMovementFlags(
|
override fun getMovementFlags(
|
||||||
|
|
|
@ -20,14 +20,12 @@ package org.oxycblt.auxio.playback.queue
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
|
||||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,10 +33,10 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemListener {
|
||||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
private val queueModel: QueueViewModel by activityViewModels()
|
||||||
private val queueAdapter = QueueAdapter(this)
|
private val queueAdapter = QueueAdapter(this)
|
||||||
private val touchHelper: ItemTouchHelper by lifecycleObject {
|
private val touchHelper: ItemTouchHelper by lifecycleObject {
|
||||||
ItemTouchHelper(QueueDragCallback(playbackModel))
|
ItemTouchHelper(QueueDragCallback(queueModel))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater)
|
||||||
|
@ -53,7 +51,7 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ----
|
// --- VIEWMODEL SETUP ----
|
||||||
|
|
||||||
collectImmediately(playbackModel.nextUp, ::updateQueue)
|
collectImmediately(queueModel.queue, ::updateQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
override fun onDestroyBinding(binding: FragmentQueueBinding) {
|
||||||
|
@ -62,19 +60,20 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(viewHolder: RecyclerView.ViewHolder) {
|
override fun onClick(viewHolder: RecyclerView.ViewHolder) {
|
||||||
playbackModel.goto(viewHolder.bindingAdapterPosition)
|
queueModel.goto(viewHolder.bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
override fun onPickUp(viewHolder: RecyclerView.ViewHolder) {
|
||||||
touchHelper.startDrag(viewHolder)
|
touchHelper.startDrag(viewHolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateQueue(queue: List<Song>) {
|
private fun updateQueue(queue: QueueViewModel.QueueData) {
|
||||||
if (queue.isEmpty()) {
|
if (queue.nonTrivial) {
|
||||||
findNavController().navigateUp()
|
// nonTrivial implies that using a synced submitList would be slow, replace the list
|
||||||
return
|
// instead.
|
||||||
|
queueAdapter.data.replaceList(queue.queue)
|
||||||
|
} else {
|
||||||
|
queueAdapter.data.submitList(queue.queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
queueAdapter.data.submitList(queue.toMutableList())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.playback.queue
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
class QueueViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
|
data class QueueData(val queue: List<Song>, val nonTrivial: Boolean)
|
||||||
|
|
||||||
|
private val _queue = MutableStateFlow(QueueData(listOf(), false))
|
||||||
|
val queue: StateFlow<QueueData> = _queue
|
||||||
|
|
||||||
|
init {
|
||||||
|
playbackManager.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to an item in the queue using it's recyclerview adapter index. No-ops if out of bounds.
|
||||||
|
*/
|
||||||
|
fun goto(adapterIndex: Int) {
|
||||||
|
val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size)
|
||||||
|
logD(adapterIndex)
|
||||||
|
logD(playbackManager.queue.size - _queue.value.queue.size)
|
||||||
|
|
||||||
|
if (index in playbackManager.queue.indices) {
|
||||||
|
playbackManager.goto(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a queue item using it's recyclerview adapter index. */
|
||||||
|
fun removeQueueDataItem(adapterIndex: Int) {
|
||||||
|
val index = adapterIndex + (playbackManager.queue.size - _queue.value.queue.size)
|
||||||
|
if (index in playbackManager.queue.indices) {
|
||||||
|
playbackManager.removeQueueItem(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** Move queue items using their recyclerview adapter indices. */
|
||||||
|
fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean {
|
||||||
|
val delta = (playbackManager.queue.size - _queue.value.queue.size)
|
||||||
|
val from = adapterFrom + delta
|
||||||
|
val to = adapterTo + delta
|
||||||
|
if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) {
|
||||||
|
playbackManager.moveQueueItem(from, to)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIndexMoved(index: Int) {
|
||||||
|
_queue.value = QueueData(generateQueue(index, playbackManager.queue), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueueChanged(queue: List<Song>) {
|
||||||
|
_queue.value = QueueData(generateQueue(playbackManager.index, queue), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueueReworked(index: Int, queue: List<Song>) {
|
||||||
|
_queue.value = QueueData(generateQueue(index, queue), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||||
|
_queue.value = QueueData(generateQueue(index, queue), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateQueue(index: Int, queue: List<Song>) =
|
||||||
|
queue.slice(index + 1..playbackManager.queue.lastIndex)
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
playbackManager.removeCallback(this)
|
||||||
|
}
|
||||||
|
}
|
|
@ -280,7 +280,7 @@ class PlaybackStateManager private constructor() {
|
||||||
val library = musicStore.library ?: return
|
val library = musicStore.library ?: return
|
||||||
val song = song ?: return
|
val song = song ?: return
|
||||||
applyNewQueue(library, settings, shuffled, song)
|
applyNewQueue(library, settings, shuffled, song)
|
||||||
notifyQueueChanged()
|
notifyQueueReworked()
|
||||||
notifyShuffledChanged()
|
notifyShuffledChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,7 +464,13 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
private fun notifyQueueChanged() {
|
private fun notifyQueueChanged() {
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onQueueChanged(index, queue)
|
callback.onQueueChanged(queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyQueueReworked() {
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onQueueReworked(index, queue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -529,8 +535,11 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Called when the index is moved, but the queue does not change. This changes the song. */
|
/** Called when the index is moved, but the queue does not change. This changes the song. */
|
||||||
fun onIndexMoved(index: Int) {}
|
fun onIndexMoved(index: Int) {}
|
||||||
|
|
||||||
/** Called when the queue and/or index changed, but the song has not. */
|
/** Called when the queue has changed in a way that does not change the index or song. */
|
||||||
fun onQueueChanged(index: Int, queue: List<Song>) {}
|
fun onQueueChanged(queue: List<Song>) {}
|
||||||
|
|
||||||
|
/** Called when the queue and index has changed, but the song has not changed.. */
|
||||||
|
fun onQueueReworked(index: Int, queue: List<Song>) {}
|
||||||
|
|
||||||
/** Called when playback is changed completely, with a new index, queue, and parent. */
|
/** Called when playback is changed completely, with a new index, queue, and parent. */
|
||||||
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
||||||
|
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.playback.system
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
@ -101,10 +103,15 @@ class MediaSessionComponent(
|
||||||
invalidateSessionState()
|
invalidateSessionState()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueueChanged(index: Int, queue: List<Song>) {
|
override fun onQueueChanged(queue: List<Song>) {
|
||||||
updateQueue(queue)
|
updateQueue(queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onQueueReworked(index: Int, queue: List<Song>) {
|
||||||
|
updateQueue(queue)
|
||||||
|
invalidateSessionState()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
||||||
updateMediaMetadata(playbackManager.song, parent)
|
updateMediaMetadata(playbackManager.song, parent)
|
||||||
updateQueue(queue)
|
updateQueue(queue)
|
||||||
|
@ -118,8 +125,8 @@ class MediaSessionComponent(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We would leave the artist field null if it didn't exist and let downstream consumers
|
// Note: We would leave the artist field null if it didn't exist and let downstream
|
||||||
// handle it, but that would break the notification display.
|
// consumers handle it, but that would break the notification display.
|
||||||
val title = song.resolveName(context)
|
val title = song.resolveName(context)
|
||||||
val artist = song.resolveIndividualArtistName(context)
|
val artist = song.resolveIndividualArtistName(context)
|
||||||
val builder =
|
val builder =
|
||||||
|
@ -180,11 +187,11 @@ class MediaSessionComponent(
|
||||||
private fun updateQueue(queue: List<Song>) {
|
private fun updateQueue(queue: List<Song>) {
|
||||||
val queueItems =
|
val queueItems =
|
||||||
queue.mapIndexed { i, song ->
|
queue.mapIndexed { i, song ->
|
||||||
// Since we usually have to load many songs into the queue, use the Cover URI
|
// Since we usually have to load many songs into the queue, use the MediaStore URI
|
||||||
// instead of loading a bitmap.
|
// instead of loading a bitmap.
|
||||||
val description =
|
val description =
|
||||||
MediaDescriptionCompat.Builder()
|
MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(song.id.toString())
|
.setMediaId("Song:${song.id}")
|
||||||
.setTitle(song.resolveName(context))
|
.setTitle(song.resolveName(context))
|
||||||
.setSubtitle(song.resolveIndividualArtistName(context))
|
.setSubtitle(song.resolveIndividualArtistName(context))
|
||||||
.setIconUri(song.album.coverUri)
|
.setIconUri(song.album.coverUri)
|
||||||
|
@ -245,8 +252,8 @@ class MediaSessionComponent(
|
||||||
invalidateSessionState()
|
invalidateSessionState()
|
||||||
|
|
||||||
if (!playbackManager.isPlaying) {
|
if (!playbackManager.isPlaying) {
|
||||||
// Hack around issue where the position won't update after a seek (but only when it's
|
// Hack around issue where the position won't update after a seek when paused.
|
||||||
// paused). Apparently this can be fixed by re-posting the notification, but not always
|
// Apparently this can be fixed by re-posting the notification, but not always
|
||||||
// when we invalidate the state (that will cause us to be rate-limited), and also not
|
// when we invalidate the state (that will cause us to be rate-limited), and also not
|
||||||
// always when we seek (that will also cause us to be rate-limited). Someone looked at
|
// always when we seek (that will also cause us to be rate-limited). Someone looked at
|
||||||
// this system and said it was well-designed.
|
// this system and said it was well-designed.
|
||||||
|
@ -256,6 +263,31 @@ class MediaSessionComponent(
|
||||||
|
|
||||||
// --- MEDIASESSION CALLBACKS ---
|
// --- MEDIASESSION CALLBACKS ---
|
||||||
|
|
||||||
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
|
super.onPlayFromMediaId(mediaId, extras)
|
||||||
|
// STUB: Unimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromUri(uri: Uri?, extras: Bundle?) {
|
||||||
|
super.onPlayFromUri(uri, extras)
|
||||||
|
// STUB: Unimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
|
super.onPlayFromSearch(query, extras)
|
||||||
|
// STUB: Unimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddQueueItem(description: MediaDescriptionCompat?) {
|
||||||
|
super.onAddQueueItem(description)
|
||||||
|
// STUB: Unimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveQueueItem(description: MediaDescriptionCompat?) {
|
||||||
|
super.onRemoveQueueItem(description)
|
||||||
|
// STUB: Unimplemented
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
playbackManager.isPlaying = true
|
playbackManager.isPlaying = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,10 @@ import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.AsyncBackingData
|
import org.oxycblt.auxio.ui.recycler.AsyncBackingData
|
||||||
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
import org.oxycblt.auxio.ui.recycler.Header
|
||||||
|
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
|
||||||
import org.oxycblt.auxio.ui.recycler.Item
|
import org.oxycblt.auxio.ui.recycler.Item
|
||||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||||
import org.oxycblt.auxio.ui.recycler.MultiAdapter
|
import org.oxycblt.auxio.ui.recycler.MultiAdapter
|
||||||
import org.oxycblt.auxio.ui.recycler.NewHeaderViewHolder
|
|
||||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>
|
||||||
is Album -> AlbumViewHolder.CREATOR
|
is Album -> AlbumViewHolder.CREATOR
|
||||||
is Artist -> ArtistViewHolder.CREATOR
|
is Artist -> ArtistViewHolder.CREATOR
|
||||||
is Genre -> GenreViewHolder.CREATOR
|
is Genre -> GenreViewHolder.CREATOR
|
||||||
is Header -> NewHeaderViewHolder.CREATOR
|
is Header -> HeaderViewHolder.CREATOR
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>
|
||||||
AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR
|
AlbumViewHolder.CREATOR.viewType -> AlbumViewHolder.CREATOR
|
||||||
ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR
|
ArtistViewHolder.CREATOR.viewType -> ArtistViewHolder.CREATOR
|
||||||
GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR
|
GenreViewHolder.CREATOR.viewType -> GenreViewHolder.CREATOR
|
||||||
NewHeaderViewHolder.CREATOR.viewType -> NewHeaderViewHolder.CREATOR
|
HeaderViewHolder.CREATOR.viewType -> HeaderViewHolder.CREATOR
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>
|
||||||
is Album -> (viewHolder as AlbumViewHolder).bind(item, listener)
|
is Album -> (viewHolder as AlbumViewHolder).bind(item, listener)
|
||||||
is Artist -> (viewHolder as ArtistViewHolder).bind(item, listener)
|
is Artist -> (viewHolder as ArtistViewHolder).bind(item, listener)
|
||||||
is Genre -> (viewHolder as GenreViewHolder).bind(item, listener)
|
is Genre -> (viewHolder as GenreViewHolder).bind(item, listener)
|
||||||
is Header -> (viewHolder as NewHeaderViewHolder).bind(item, Unit)
|
is Header -> (viewHolder as HeaderViewHolder).bind(item, Unit)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ class SearchAdapter(listener: MenuItemListener) : MultiAdapter<MenuItemListener>
|
||||||
oldItem is Genre && newItem is Genre ->
|
oldItem is Genre && newItem is Genre ->
|
||||||
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
GenreViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
oldItem is Header && newItem is Header ->
|
oldItem is Header && newItem is Header ->
|
||||||
NewHeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
HeaderViewHolder.DIFFER.areItemsTheSame(oldItem, newItem)
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,7 +201,7 @@ private constructor(
|
||||||
* The Shared ViewHolder for a [Header].
|
* The Shared ViewHolder for a [Header].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
|
||||||
BindingViewHolder<Header, Unit>(binding.root) {
|
BindingViewHolder<Header, Unit>(binding.root) {
|
||||||
|
|
||||||
override fun bind(item: Header, listener: Unit) {
|
override fun bind(item: Header, listener: Unit) {
|
||||||
|
@ -210,12 +210,12 @@ class NewHeaderViewHolder private constructor(private val binding: ItemHeaderBin
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CREATOR =
|
val CREATOR =
|
||||||
object : Creator<NewHeaderViewHolder> {
|
object : Creator<HeaderViewHolder> {
|
||||||
override val viewType: Int
|
override val viewType: Int
|
||||||
get() = IntegerTable.ITEM_TYPE_HEADER
|
get() = IntegerTable.ITEM_TYPE_HEADER
|
||||||
|
|
||||||
override fun create(context: Context) =
|
override fun create(context: Context) =
|
||||||
NewHeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater))
|
||||||
}
|
}
|
||||||
|
|
||||||
val DIFFER =
|
val DIFFER =
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/interact_body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground">
|
android:background="?attr/selectableItemBackground">
|
||||||
|
|
|
@ -81,6 +81,8 @@
|
||||||
<string name="lbl_shuffle">Shuffle</string>
|
<string name="lbl_shuffle">Shuffle</string>
|
||||||
|
|
||||||
<string name="lbl_queue">Queue</string>
|
<string name="lbl_queue">Queue</string>
|
||||||
|
<string name="lbl_next_up">Next up</string>
|
||||||
|
<string name="lbl_later">Later</string>
|
||||||
<string name="lbl_play_next">Play next</string>
|
<string name="lbl_play_next">Play next</string>
|
||||||
<string name="lbl_queue_add">Add to queue</string>
|
<string name="lbl_queue_add">Add to queue</string>
|
||||||
<string name="lbl_queue_added">Added to queue</string>
|
<string name="lbl_queue_added">Added to queue</string>
|
||||||
|
|
Loading…
Reference in a new issue