diff --git a/app/build.gradle b/app/build.gradle index 281bc881e..f5dcee567 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -83,14 +83,14 @@ dependencies { // Material implementation 'com.google.android.material:material:1.3.0-alpha03' - // Lint - ktlint "com.pinterest:ktlint:0.37.2" - // ExoPlayer def exoplayer_version = "2.12.1" implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version" implementation "com.google.android.exoplayer:extension-mediasession:$exoplayer_version" + // Lint + ktlint "com.pinterest:ktlint:0.37.2" + // Memory Leak checking debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 56850aa02..a3855f4bc 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -59,9 +59,9 @@ class MainFragment : Fragment() { ) val navController = ( - childFragmentManager.findFragmentById(R.id.explore_nav_host) - as NavHostFragment? - )?.findNavController() + childFragmentManager.findFragmentById(R.id.explore_nav_host) + as NavHostFragment? + )?.findNavController() // --- UI SETUP --- @@ -93,17 +93,30 @@ class MainFragment : Fragment() { } } - playbackModel.navToSong.observe(viewLifecycleOwner) { + playbackModel.navToPlayingSong.observe(viewLifecycleOwner) { if (it) { if (binding.navBar.selectedItemId != R.id.library_fragment || - ( - navController!!.currentDestination?.id == R.id.album_detail_fragment && - detailModel.currentAlbum.value == null || - detailModel.currentAlbum.value?.id - != playbackModel.song.value!!.album.id - ) || - navController.currentDestination?.id == R.id.artist_detail_fragment || - navController.currentDestination?.id == R.id.genre_detail_fragment + shouldGoToAlbum(navController!!) + ) { + binding.navBar.selectedItemId = R.id.library_fragment + } + } + } + + playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) { + if (it) { + if (binding.navBar.selectedItemId != R.id.library_fragment || + shouldGoToAlbum(navController!!) + ) { + binding.navBar.selectedItemId = R.id.library_fragment + } + } + } + + playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) { + if (it) { + if (binding.navBar.selectedItemId != R.id.library_fragment || + shouldGoToArtist(navController!!) ) { binding.navBar.selectedItemId = R.id.library_fragment } @@ -117,6 +130,25 @@ class MainFragment : Fragment() { return binding.root } + // I have no idea what these things even do + private fun shouldGoToAlbum(controller: NavController): Boolean { + return ( + controller.currentDestination!!.id == R.id.album_detail_fragment && + detailModel.currentAlbum.value?.id != playbackModel.song.value!!.album.id + ) || + controller.currentDestination!!.id == R.id.artist_detail_fragment || + controller.currentDestination!!.id == R.id.genre_detail_fragment + } + + private fun shouldGoToArtist(controller: NavController): Boolean { + return ( + controller.currentDestination!!.id == R.id.artist_detail_fragment && + detailModel.currentArtist.value?.id != playbackModel.song.value!!.album.artist.id + ) || + controller.currentDestination!!.id == R.id.album_detail_fragment || + controller.currentDestination!!.id == R.id.genre_detail_fragment + } + private fun navigateWithItem(navController: NavController, item: MenuItem): Boolean { if (navController.currentDestination!!.id != item.itemId) { // Create custom NavOptions myself so that animations work diff --git a/app/src/main/java/org/oxycblt/auxio/database/PlaybackState.kt b/app/src/main/java/org/oxycblt/auxio/database/PlaybackState.kt index 4b3ce9456..8eaec9abb 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/PlaybackState.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/PlaybackState.kt @@ -1,5 +1,17 @@ package org.oxycblt.auxio.database +/** + * A database entity that stores a compressed variant of the current playback state. + * @property id - The database key for this state + * @property songId - The song that is currently playing + * @property parentId - The parent that is being played from [-1 if none] + * @property index - The current index in the queue. + * @property mode - The integer form of the current [org.oxycblt.auxio.playback.state.PlaybackMode] + * @property isShuffling - A bool for if the queue was shuffled + * @property shuffleSeed - A long for the seed used to shuffle the queue [Used for quick-restore] + * @property loopMode - The integer form of the current [org.oxycblt.auxio.playback.state.LoopMode] + * @property inUserQueue - A bool for if the state was currently playing from the user queue. + */ data class PlaybackState( val id: Long = 0L, val songId: Long = -1L, diff --git a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt index 65a0269f5..db48cefba 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt @@ -188,6 +188,7 @@ class PlaybackStateDatabase(context: Context) : var position = 0 + // Try to write out the entirety of the queue, any failed inserts will be skipped. while (position < queue.size) { database.beginTransaction() var i = position diff --git a/app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt b/app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt index 790028c0e..bfbf4f9d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt @@ -1,5 +1,12 @@ package org.oxycblt.auxio.database +/** + * A database entity that stores a simplified representation of a song in a queue. + * @property id The database entity's id + * @property songId The song id for this queue item + * @property albumId The album id for this queue item, used to make searching quicker + * @property isUserQueue A bool for if this queue item is a user queue item or not + */ data class QueueItem( var id: Long = 0L, val songId: Long = Long.MIN_VALUE, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 12301aee9..44afc2f3c 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -122,18 +122,20 @@ class AlbumDetailFragment : DetailFragment() { } } - playbackModel.navToSong.observe(viewLifecycleOwner) { + playbackModel.navToPlayingSong.observe(viewLifecycleOwner) { if (it) { // Calculate where the item for the currently played song is, and navigate to there. - val pos = detailModel.currentAlbum.value!!.songs.indexOf(playbackModel.song.value) + val pos = detailModel.albumSortMode.value!!.getSortedSongList( + detailModel.currentAlbum.value!!.songs + ).indexOf(playbackModel.song.value) if (pos != -1) { binding.albumSongRecycler.post { // Only scroll after UI creation val y = binding.albumSongRecycler.y + - binding.albumSongRecycler.getChildAt(pos).y + binding.albumSongRecycler.getChildAt(pos).y - binding.nestedScroll.smoothScrollBy(0, y.toInt()) + binding.nestedScroll.smoothScrollTo(0, y.toInt()) } playbackModel.doneWithNavToPlayingSong() @@ -141,6 +143,12 @@ class AlbumDetailFragment : DetailFragment() { } } + playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) { + if (it) { + playbackModel.doneWithNavToPlayingAlbum() + } + } + Log.d(this::class.simpleName, "Fragment created.") return binding.root diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index c8428494f..0b8725ccf 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -108,6 +108,12 @@ class ArtistDetailFragment : DetailFragment() { ) } + playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) { + if (it) { + playbackModel.doneWithNavToPlayingArtist() + } + } + Log.d(this::class.simpleName, "Fragment created.") return binding.root diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index fb57cf0ec..b7f6ee036 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -151,7 +151,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { } } - playbackModel.navToSong.observe(viewLifecycleOwner) { + playbackModel.navToPlayingSong.observe(viewLifecycleOwner) { if (it) { libraryModel.updateNavigationStatus(false) @@ -159,6 +159,22 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { } } + playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) { + if (it) { + libraryModel.updateNavigationStatus(false) + + navToItem(playbackModel.song.value!!.album) + } + } + + playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) { + if (it) { + libraryModel.updateNavigationStatus(false) + + navToItem(playbackModel.song.value!!.album.artist) + } + } + Log.d(this::class.simpleName, "Fragment created.") return binding.root 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 0926bbb95..704837b9d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -69,6 +69,9 @@ data class Album( /** * The data object for an artist. Inherits [BaseModel] + * @property albums The list of all [Album]s in this artist + * @property genres The list of all parent [Genre]s in this artist, sorted by relevance + * @property songs The list of all [Song]s in this artist * @author OxygenCobalt */ data class Artist( @@ -89,7 +92,8 @@ data class Artist( /** * The data object for a genre. Inherits [BaseModel] - * @property artists The list of all [Artist]s in this genre + * @property artists The list of all [Artist]s in this genre. + * @property albums The list of all [Album]s in this genre. * @property songs The list of all [Song]s in this genre. * @author OxygenCobalt */ @@ -117,6 +121,8 @@ data class Genre( /** * A data object used solely for the "Header" UI element. Inherits [BaseModel]. + * @property isAction Value that marks whether this header should have an action attached to it. + * @author OxygenCobalt */ data class Header( override val id: Long = -1, diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index 32e8cc26d..fe378eed1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -9,7 +9,7 @@ import org.oxycblt.auxio.music.processing.MusicSorter import org.oxycblt.auxio.recycler.ShowMode /** - * The main storage for music items. Use [MusicStore.from()] to get the instance. + * The main storage for music items. Use [MusicStore.getInstance] to get the instance. */ class MusicStore private constructor() { private var mGenres = listOf() 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 c1c779158..c13ebcbd0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/CompactPlaybackFragment.kt @@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController +import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding @@ -54,10 +55,14 @@ class CompactPlaybackFragment : Fragment() { true } - binding.playbackControls.setOnLongClickListener { - playbackModel.save(requireContext()) - getString(R.string.debug_state_saved).createToast(requireContext()) - true + // Enable the ability to force-save the state in debug builds, in order to check + // for persistence issues without waiting for PlaybackService to be killed. + if (BuildConfig.DEBUG) { + binding.playbackControls.setOnLongClickListener { + playbackModel.save(requireContext()) + getString(R.string.debug_state_saved).createToast(requireContext()) + true + } } // --- VIEWMODEL SETUP --- 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 2ae6965f3..b382c9a3a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -187,6 +187,24 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener { } } + playbackModel.navToPlayingSong.observe(viewLifecycleOwner) { + if (it) { + findNavController().navigateUp() + } + } + + playbackModel.navToPlayingAlbum.observe(viewLifecycleOwner) { + if (it) { + findNavController().navigateUp() + } + } + + playbackModel.navToPlayingArtist.observe(viewLifecycleOwner) { + if (it) { + findNavController().navigateUp() + } + } + Log.d(this::class.simpleName, "Fragment Created.") return binding.root 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 a5d394692..72f383ab6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -62,8 +62,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { private val mIsSeeking = MutableLiveData(false) val isSeeking: LiveData get() = mIsSeeking - private val mNavToSong = MutableLiveData(false) - val navToSong: LiveData get() = mNavToSong + private val mNavToPlayingSong = MutableLiveData(false) + val navToPlayingSong: LiveData get() = mNavToPlayingSong + + private val mNavToPlayingAlbum = MutableLiveData(false) + val navToPlayingAlbum: LiveData get() = mNavToPlayingAlbum + + private val mNavToPlayingArtist = MutableLiveData(false) + val navToPlayingArtist: LiveData get() = mNavToPlayingArtist private var mCanAnimate = false val canAnimate: Boolean get() = mCanAnimate @@ -283,11 +289,27 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } fun navToPlayingSong() { - mNavToSong.value = true + mNavToPlayingSong.value = true } fun doneWithNavToPlayingSong() { - mNavToSong.value = false + mNavToPlayingSong.value = false + } + + fun navToPlayingAlbum() { + mNavToPlayingAlbum.value = true + } + + fun doneWithNavToPlayingAlbum() { + mNavToPlayingAlbum.value = false + } + + fun navToPlayingArtist() { + mNavToPlayingArtist.value = true + } + + fun doneWithNavToPlayingArtist() { + mNavToPlayingArtist.value = false } fun enableAnimation() { 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 6278454df..2cfc83697 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 @@ -22,7 +22,7 @@ import kotlin.random.Random * - If you want to use the playback state with the ExoPlayer instance or system-side things, * use [org.oxycblt.auxio.playback.PlaybackService]. * - * All instantiation should be done with [PlaybackStateManager.from()]. + * All instantiation should be done with [PlaybackStateManager.getInstance]. * @author OxygenCobalt */ class PlaybackStateManager private constructor() { @@ -560,12 +560,12 @@ class PlaybackStateManager private constructor() { // Traverse albums and then album songs instead of just the songs, as its faster. musicStore.albums.find { it.id == item.albumId } ?.songs?.find { it.id == item.songId }?.let { - if (item.isUserQueue) { - mUserQueue.add(it) - } else { - mQueue.add(it) - } + if (item.isUserQueue) { + mUserQueue.add(it) + } else { + mQueue.add(it) } + } } // Get a more accurate index [At least if were not in the user queue] diff --git a/app/src/main/res/drawable/ui_header_dividers.xml b/app/src/main/res/drawable/ui_header_dividers.xml index d5470056f..8a036f39b 100644 --- a/app/src/main/res/drawable/ui_header_dividers.xml +++ b/app/src/main/res/drawable/ui_header_dividers.xml @@ -9,7 +9,7 @@ https://stackoverflow.com/a/61157571/14143986 android:top="-2dp"> diff --git a/app/src/main/res/layout/fragment_playback.xml b/app/src/main/res/layout/fragment_playback.xml index 47d750044..9fdec4d0d 100644 --- a/app/src/main/res/layout/fragment_playback.xml +++ b/app/src/main/res/layout/fragment_playback.xml @@ -67,6 +67,7 @@ android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large" android:text="@{song.name}" + android:onClick="@{() -> playbackModel.navToPlayingSong()}" android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6" app:layout_constraintBottom_toTopOf="@+id/playback_artist" app:layout_constraintEnd_toEndOf="parent" @@ -83,6 +84,7 @@ android:layout_marginStart="@dimen/margin_mid_large" android:layout_marginEnd="@dimen/margin_mid_large" android:text="@{song.album.artist.name}" + android:onClick="@{() -> playbackModel.navToPlayingArtist()}" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:textColor="?android:attr/textColorSecondary" app:layout_constraintBottom_toTopOf="@+id/playback_album" @@ -100,6 +102,7 @@ android:ellipsize="end" android:singleLine="true" android:text="@{song.album.name}" + android:onClick="@{() -> playbackModel.navToPlayingAlbum()}" android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" android:textColor="?android:attr/textColorSecondary" app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"