diff --git a/app/build.gradle b/app/build.gradle index ad870864a..5e0ac84e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' apply plugin: "androidx.navigation.safeargs" diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 50cd1f0fa..de8694ee5 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -11,6 +11,7 @@ import androidx.navigation.fragment.findNavController * A Base [Fragment] implementing a [OnBackPressedCallback] so that Auxio will navigate upwards * instead of out of the app if a Detail Fragment is currently open. Also carries the * multi-navigation fix. + * // TODO: Merge headers with recyclerview [if possible] * @author OxygenCobalt */ abstract class DetailFragment : Fragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt index e7f74e199..714863700 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/DetailAlbumAdapter.kt @@ -12,20 +12,20 @@ import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder class DetailAlbumAdapter( private val doOnClick: (data: Album) -> Unit, private val doOnLongClick: (data: Album, view: View) -> Unit -) : ListAdapter(DiffCallback()) { +) : ListAdapter(DiffCallback()) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder( + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlbumViewHolder { + return AlbumViewHolder( ItemArtistAlbumBinding.inflate(LayoutInflater.from(parent.context)) ) } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { + override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) { holder.bind(getItem(position)) } // Generic ViewHolder for a detail album - inner class ViewHolder( + inner class AlbumViewHolder( private val binding: ItemArtistAlbumBinding, ) : BaseViewHolder(binding, doOnClick, doOnLongClick) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index 5e70f52cf..0926bbb95 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -120,5 +120,6 @@ data class Genre( */ data class Header( override val id: Long = -1, - override var name: String = "" + override var name: String = "", + val isAction: Boolean = false ) : BaseModel() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt index 63a067e70..a100e18e0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -14,6 +14,7 @@ import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.ui.createToast /** * A [Fragment] that displays the currently played song at a glance, with some basic controls. @@ -57,6 +58,7 @@ class CompactPlaybackFragment : Fragment() { binding.root.setOnLongClickListener { playbackModel.save(requireContext()) + getString(R.string.debug_state_saved).createToast(requireContext()) true } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt index 499ec4461..8796e62a1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/NotificationUtils.kt @@ -105,17 +105,28 @@ fun NotificationCompat.Builder.setMetadata(song: Song, context: Context, onDone: } } -// I have no idea how to update a specific action on the fly so I have to use these restricted APIs +/** + * Update the playing button on the media notification. + * @param context The context required to refresh the action + */ @SuppressLint("RestrictedApi") fun NotificationCompat.Builder.updatePlaying(context: Context) { mActions[2] = newAction(NotificationUtils.ACTION_PLAY_PAUSE, context) } +/** + * Update the loop button on the media notification + * @param context The context required to refresh the action + */ @SuppressLint("RestrictedApi") fun NotificationCompat.Builder.updateLoop(context: Context) { mActions[0] = newAction(NotificationUtils.ACTION_LOOP, context) } +/** + * Update the subtext of the media notification to reflect the current mode. + * @param context The context required to get the strings required to show certain modes + */ fun NotificationCompat.Builder.updateMode(context: Context) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { val playbackManager = PlaybackStateManager.getInstance() @@ -164,12 +175,10 @@ private fun newAction(action: String, context: Context): NotificationCompat.Acti } return NotificationCompat.Action.Builder( - drawable, action, newPlaybackIntent(action, context) + drawable, action, + PendingIntent.getBroadcast( + context, NotificationUtils.REQUEST_CODE, + Intent(action), PendingIntent.FLAG_UPDATE_CURRENT + ) ).build() } - -private fun newPlaybackIntent(action: String, context: Context): PendingIntent { - return PendingIntent.getBroadcast( - context, NotificationUtils.REQUEST_CODE, Intent(action), PendingIntent.FLAG_UPDATE_CURRENT - ) -} 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 a6d451f3b..735b5c5d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -263,8 +263,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } } - // TODO: Do testing where service is destroyed after restore [Possible edge case] - override fun onLoopUpdate(mode: LoopMode) { changeIsFromAudioFocus = false @@ -296,6 +294,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca player.prepare() player.seekTo(playbackManager.position) } + + when (playbackManager.loopMode) { + LoopMode.NONE -> { + player.repeatMode = Player.REPEAT_MODE_OFF + } + else -> { + player.repeatMode = Player.REPEAT_MODE_ONE + } + } } private fun restoreNotification() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index 0afc4eba6..a76848e03 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -8,6 +8,8 @@ import android.view.ViewGroup import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemActionHeaderBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Header @@ -25,7 +27,8 @@ import org.oxycblt.auxio.recycler.viewholders.HeaderViewHolder * @author OxygenCobalt */ class QueueAdapter( - private val touchHelper: ItemTouchHelper + private val touchHelper: ItemTouchHelper, + private val onHeaderAction: () -> Unit ) : RecyclerView.Adapter() { private var data = mutableListOf() private var listDiffer = AsyncListDiffer(this, DiffCallback()) @@ -36,7 +39,10 @@ class QueueAdapter( val item = data[position] return if (item is Header) - HeaderViewHolder.ITEM_TYPE + if (item.isAction) + USER_QUEUE_HEADER_ITEM_tYPE + else + HeaderViewHolder.ITEM_TYPE else QUEUE_ITEM_TYPE } @@ -47,6 +53,9 @@ class QueueAdapter( QUEUE_ITEM_TYPE -> QueueSongViewHolder( ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)) ) + USER_QUEUE_HEADER_ITEM_tYPE -> UserQueueHeaderViewHolder( + ItemActionHeaderBinding.inflate(LayoutInflater.from(parent.context)) + ) else -> error("Someone messed with the ViewHolder item types.") } } @@ -54,7 +63,12 @@ class QueueAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (val item = data[position]) { is Song -> (holder as QueueSongViewHolder).bind(item) - is Header -> (holder as HeaderViewHolder).bind(item) + is Header -> + if (item.isAction) { + (holder as UserQueueHeaderViewHolder).bind(item) + } else { + (holder as HeaderViewHolder).bind(item) + } else -> { Log.e(this::class.simpleName, "Bad data fed to QueueAdapter.") @@ -131,7 +145,24 @@ class QueueAdapter( } } + inner class UserQueueHeaderViewHolder( + private val binding: ItemActionHeaderBinding + ) : BaseViewHolder
(binding, null, null) { + override fun onBind(data: Header) { + binding.header = data + binding.headerButton.apply { + setImageResource(R.drawable.ic_clear) + + setOnClickListener { + clearUserQueue() + onHeaderAction() + } + } + } + } + companion object { const val QUEUE_ITEM_TYPE = 0xA015 + const val USER_QUEUE_HEADER_ITEM_tYPE = 0xA016 } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index b913d9511..fe28165a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -34,25 +34,16 @@ class QueueFragment : Fragment() { val callback = QueueDragCallback(playbackModel) val helper = ItemTouchHelper(callback) - val queueAdapter = QueueAdapter(helper) - callback.addQueueAdapter(queueAdapter) + val queueAdapter = QueueAdapter(helper) { + playbackModel.clearUserQueue() + } - val queueClearItem = binding.queueToolbar.menu.findItem(R.id.action_clear_user_queue) + callback.addQueueAdapter(queueAdapter) // --- UI SETUP --- - binding.queueToolbar.apply { - setNavigationOnClickListener { - findNavController().navigateUp() - } - - setOnMenuItemClickListener { - if (it.itemId == R.id.action_clear_user_queue) { - queueAdapter.clearUserQueue() - playbackModel.clearUserQueue() - true - } else false - } + binding.queueToolbar.setNavigationOnClickListener { + findNavController().navigateUp() } binding.queueRecycler.apply { @@ -65,14 +56,10 @@ class QueueFragment : Fragment() { // --- VIEWMODEL SETUP --- playbackModel.userQueue.observe(viewLifecycleOwner) { - if (it.isEmpty()) { - queueClearItem.isEnabled = false + if (it.isEmpty() && playbackModel.nextItemsInQueue.value!!.isEmpty()) { + findNavController().navigateUp() - if (playbackModel.nextItemsInQueue.value!!.isEmpty()) { - findNavController().navigateUp() - - return@observe - } + return@observe } queueAdapter.submitList(createQueueData()) @@ -94,7 +81,7 @@ class QueueFragment : Fragment() { if (playbackModel.userQueue.value!!.isNotEmpty()) { queue.add( - Header(name = getString(R.string.label_next_user_queue)) + Header(name = getString(R.string.label_next_user_queue), isAction = true) ) queue.addAll(playbackModel.userQueue.value!!) } @@ -108,7 +95,8 @@ class QueueFragment : Fragment() { getString(R.string.label_all_songs) else playbackModel.parent.value!!.name - ) + ), + isAction = false ) ) queue.addAll(playbackModel.nextItemsInQueue.value!!) diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt index 8a316d45a..dc47b4edd 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt @@ -3,6 +3,8 @@ package org.oxycblt.auxio.recycler.viewholders import android.content.Context import android.view.LayoutInflater import android.view.View +import androidx.annotation.DrawableRes +import org.oxycblt.auxio.databinding.ItemActionHeaderBinding import org.oxycblt.auxio.databinding.ItemAlbumBinding import org.oxycblt.auxio.databinding.ItemArtistBinding import org.oxycblt.auxio.databinding.ItemGenreBinding @@ -129,7 +131,7 @@ class SongViewHolder private constructor( } } -open class HeaderViewHolder( +class HeaderViewHolder( private val binding: ItemHeaderBinding ) : BaseViewHolder
(binding, null, null) { @@ -147,3 +149,20 @@ open class HeaderViewHolder( } } } + +abstract class ActionHeaderViewHolder( + protected val binding: ItemActionHeaderBinding, + @DrawableRes private val iconRes: Int +) : BaseViewHolder
(binding, null, null) { + override fun onBind(data: Header) { + binding.header = data + binding.headerButton.apply { + setImageResource(iconRes) + setOnClickListener { + onActionClick() + } + } + } + + abstract fun onActionClick() +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index 1dff0c81b..ab065a8e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -9,6 +9,7 @@ import android.view.MenuItem import android.widget.ImageButton import android.widget.Toast import androidx.annotation.ColorInt +import androidx.annotation.MenuRes import androidx.appcompat.widget.PopupMenu import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.RecyclerView @@ -63,7 +64,6 @@ fun RecyclerView.applyDivider() { } fun PopupMenu.setupSongActions(song: Song, context: Context, playbackModel: PlaybackViewModel) { - inflate(R.menu.menu_song_actions) setOnMenuItemClickListener { when (it.itemId) { R.id.action_queue_add -> { @@ -85,7 +85,7 @@ fun PopupMenu.setupSongActions(song: Song, context: Context, playbackModel: Play else -> false } } - show() + inflateAndShow(R.menu.menu_song_actions) } fun PopupMenu.setupAlbumSongActions( @@ -94,7 +94,6 @@ fun PopupMenu.setupAlbumSongActions( detailViewModel: DetailViewModel, playbackModel: PlaybackViewModel ) { - inflate(R.menu.menu_album_song_actions) setOnMenuItemClickListener { when (it.itemId) { R.id.action_queue_add -> { @@ -117,7 +116,7 @@ fun PopupMenu.setupAlbumSongActions( else -> false } } - show() + inflateAndShow(R.menu.menu_album_song_actions) } fun PopupMenu.setupAlbumActions( @@ -125,7 +124,6 @@ fun PopupMenu.setupAlbumActions( context: Context, playbackModel: PlaybackViewModel ) { - inflate(R.menu.menu_album_actions) setOnMenuItemClickListener { when (it.itemId) { R.id.action_queue_add -> { @@ -148,7 +146,7 @@ fun PopupMenu.setupAlbumActions( else -> false } } - show() + inflateAndShow(R.menu.menu_album_actions) } fun PopupMenu.setupArtistActions( @@ -156,7 +154,6 @@ fun PopupMenu.setupArtistActions( context: Context, playbackModel: PlaybackViewModel ) { - inflate(R.menu.menu_detail) setOnMenuItemClickListener { when (it.itemId) { R.id.action_queue_add -> { @@ -179,7 +176,7 @@ fun PopupMenu.setupArtistActions( else -> false } } - show() + inflateAndShow(R.menu.menu_detail) } fun PopupMenu.setupGenreActions( @@ -187,7 +184,6 @@ fun PopupMenu.setupGenreActions( context: Context, playbackModel: PlaybackViewModel ) { - inflate(R.menu.menu_detail) setOnMenuItemClickListener { when (it.itemId) { R.id.action_queue_add -> { @@ -210,5 +206,10 @@ fun PopupMenu.setupGenreActions( else -> false } } + inflateAndShow(R.menu.menu_detail) +} + +private fun PopupMenu.inflateAndShow(@MenuRes menuRes: Int) { + inflate(menuRes) show() } diff --git a/app/src/main/res/layout/fragment_queue.xml b/app/src/main/res/layout/fragment_queue.xml index 5ed4fbf39..d3f0b9021 100644 --- a/app/src/main/res/layout/fragment_queue.xml +++ b/app/src/main/res/layout/fragment_queue.xml @@ -21,7 +21,6 @@ android:focusable="true" android:elevation="@dimen/elevation_normal" app:popupTheme="@style/Widget.CustomPopup" - app:menu="@menu/menu_queue" app:navigationIcon="@drawable/ic_down" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/item_action_header.xml b/app/src/main/res/layout/item_action_header.xml new file mode 100644 index 000000000..997501cc8 --- /dev/null +++ b/app/src/main/res/layout/item_action_header.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_album.xml b/app/src/main/res/layout/item_artist_album.xml index c14f7a216..e1d9d00a3 100644 --- a/app/src/main/res/layout/item_artist_album.xml +++ b/app/src/main/res/layout/item_artist_album.xml @@ -2,7 +2,7 @@ + tools:context=".detail.adapters.DetailAlbumAdapter.AlbumViewHolder"> diff --git a/app/src/main/res/menu/menu_queue.xml b/app/src/main/res/menu/menu_queue.xml deleted file mode 100644 index db57952ae..000000000 --- a/app/src/main/res/menu/menu_queue.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ 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 71f2dc7ba..579b2c56b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,9 @@ Music Playback The music playback service for Auxio. + + State saved + No music found. Music loading failed. diff --git a/build.gradle b/build.gradle index b6795aad6..764ebecb2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.10" + ext.kotlin_version = "1.4.20" repositories { google()