diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 5790589be..ef52b9bde 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -60,11 +60,11 @@ class MainActivity : AppCompatActivity() { // to PlaybackViewModel to be used later. if (intent != null) { val action = intent.action - val isConsumed = intent.getBooleanExtra(KEY_INTENT_CONSUMED, false) + val isConsumed = intent.getBooleanExtra(KEY_INTENT_USED, false) if (action == Intent.ACTION_VIEW && !isConsumed) { // Mark the intent as used so this does not fire again - intent.putExtra(KEY_INTENT_CONSUMED, true) + intent.putExtra(KEY_INTENT_USED, true) intent.data?.let { fileUri -> playbackModel.playWithUri(fileUri, this) @@ -100,6 +100,6 @@ class MainActivity : AppCompatActivity() { } companion object { - private const val KEY_INTENT_CONSUMED = "KEY_FILE_INTENT_USED" + private const val KEY_INTENT_USED = "KEY_FILE_INTENT_USED" } } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt index 0d2a34d7f..7e4019822 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/AlbumArtFetcher.kt @@ -20,6 +20,7 @@ import java.io.InputStream /** * Fetcher that returns the album art for a given [Album]. Handles settings on whether to use * quality covers or not. + * @author OxygenCobalt */ class AlbumArtFetcher(private val context: Context) : Fetcher { private val settingsManager = SettingsManager.getInstance() diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index d08cc91b5..556a9cda4 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -19,7 +19,7 @@ import org.oxycblt.auxio.settings.SettingsManager // --- BINDING ADAPTERS --- /** - * Bind the album art for a [Song]. + * Bind the album art for a [song]. */ @BindingAdapter("albumArt") fun ImageView.bindAlbumArt(song: Song) { @@ -27,7 +27,7 @@ fun ImageView.bindAlbumArt(song: Song) { } /** - * Bind the album art for an [Album]. + * Bind the album art for an [album]. */ @BindingAdapter("albumArt") fun ImageView.bindAlbumArt(album: Album) { @@ -35,7 +35,7 @@ fun ImageView.bindAlbumArt(album: Album) { } /** - * Bind the image for an [Artist] + * Bind the image for an [artist] */ @BindingAdapter("artistImage") fun ImageView.bindArtistImage(artist: Artist) { @@ -43,7 +43,7 @@ fun ImageView.bindArtistImage(artist: Artist) { } /** - * Bind the image for a [Genre] + * Bind the image for a [genre] */ @BindingAdapter("genreImage") fun ImageView.bindGenreImage(genre: Genre) { 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 f0d8f5c0e..a5bd1853b 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt @@ -2,7 +2,6 @@ package org.oxycblt.auxio.database import android.content.ContentValues import android.content.Context -import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.os.Looper @@ -35,9 +34,7 @@ class PlaybackStateDatabase(context: Context) : // --- DATABASE CONSTRUCTION FUNCTIONS --- /** - * Create a table - * @param database DB to create the tables on - * @param tableName The name of the table to create. + * Create a table for this database. */ private fun createTable(database: SQLiteDatabase, tableName: String) { val command = StringBuilder() @@ -106,15 +103,17 @@ class PlaybackStateDatabase(context: Context) : val stateData = ContentValues(9) - stateData.put(PlaybackState.COLUMN_ID, state.id) - stateData.put(PlaybackState.COLUMN_SONG_NAME, state.songName) - stateData.put(PlaybackState.COLUMN_POSITION, state.position) - stateData.put(PlaybackState.COLUMN_PARENT_NAME, state.parentName) - stateData.put(PlaybackState.COLUMN_INDEX, state.index) - stateData.put(PlaybackState.COLUMN_MODE, state.mode) - stateData.put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling) - stateData.put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode) - stateData.put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue) + stateData.apply { + put(PlaybackState.COLUMN_ID, state.id) + put(PlaybackState.COLUMN_SONG_NAME, state.songName) + put(PlaybackState.COLUMN_POSITION, state.position) + put(PlaybackState.COLUMN_PARENT_NAME, state.parentName) + put(PlaybackState.COLUMN_INDEX, state.index) + put(PlaybackState.COLUMN_MODE, state.mode) + put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling) + put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode) + put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue) + } database.insert(TABLE_NAME_STATE, null, stateData) database.setTransactionSuccessful() @@ -135,10 +134,9 @@ class PlaybackStateDatabase(context: Context) : val database = writableDatabase var state: PlaybackState? = null - val stateCursor: Cursor try { - stateCursor = database.query( + val stateCursor = database.query( TABLE_NAME_STATE, null, null, null, null, null, null @@ -179,10 +177,9 @@ class PlaybackStateDatabase(context: Context) : } /** - * Write a list of [QueueItem]s to the database, clearing the previous queue present. - * @param queue The list of [QueueItem]s to be written. + * Write a list of [queueItems] to the database, clearing the previous queue present. */ - fun writeQueue(queue: List) { + fun writeQueue(queueItems: List) { assertBackgroundThread() val database = readableDatabase @@ -202,21 +199,23 @@ 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) { + while (position < queueItems.size) { database.beginTransaction() var i = position try { - while (i < queue.size) { - val item = queue[i] + while (i < queueItems.size) { + val item = queueItems[i] val itemData = ContentValues(4) i++ - itemData.put(QueueItem.COLUMN_ID, item.id) - itemData.put(QueueItem.COLUMN_SONG_NAME, item.songName) - itemData.put(QueueItem.COLUMN_ALBUM_NAME, item.albumName) - itemData.put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue) + itemData.apply { + put(QueueItem.COLUMN_ID, item.id) + put(QueueItem.COLUMN_SONG_NAME, item.songName) + put(QueueItem.COLUMN_ALBUM_NAME, item.albumName) + put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue) + } database.insert(TABLE_NAME_QUEUE, null, itemData) } @@ -242,12 +241,10 @@ class PlaybackStateDatabase(context: Context) : assertBackgroundThread() val database = readableDatabase - val queueItems = mutableListOf() - val queueCursor: Cursor try { - queueCursor = database.query( + val queueCursor = database.query( TABLE_NAME_QUEUE, null, null, null, null, null, null ) 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 1c76e0094..2edd752e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.recycler.CenterSmoothScroller import org.oxycblt.auxio.ui.ActionMenu +import org.oxycblt.auxio.ui.canScroll import org.oxycblt.auxio.ui.createToast import org.oxycblt.auxio.ui.newMenu @@ -83,44 +84,42 @@ class AlbumDetailFragment : DetailFragment() { } detailModel.navToItem.observe(viewLifecycleOwner) { - if (it != null) { - when (it) { - // Songs should be scrolled to if the album matches, or a new detail - // fragment should be launched otherwise. - is Song -> { - if (detailModel.currentAlbum.value!!.id == it.album.id) { - scrollToItem(it.id) + when (it) { + // Songs should be scrolled to if the album matches, or a new detail + // fragment should be launched otherwise. + is Song -> { + if (detailModel.currentAlbum.value!!.id == it.album.id) { + scrollToItem(it.id) - detailModel.doneWithNavToItem() - } else { - findNavController().navigate( - AlbumDetailFragmentDirections.actionShowAlbum(it.album.id) - ) - } - } - - // If the album matches, no need to do anything. Otherwise launch a new - // detail fragment. - is Album -> { - if (detailModel.currentAlbum.value!!.id == it.id) { - binding.detailRecycler.scrollToPosition(0) - detailModel.doneWithNavToItem() - } else { - findNavController().navigate( - AlbumDetailFragmentDirections.actionShowAlbum(it.id) - ) - } - } - - // Always launch a new ArtistDetailFragment. - is Artist -> { + detailModel.doneWithNavToItem() + } else { findNavController().navigate( - AlbumDetailFragmentDirections.actionShowArtist(it.id) + AlbumDetailFragmentDirections.actionShowAlbum(it.album.id) ) } - - else -> {} } + + // If the album matches, no need to do anything. Otherwise launch a new + // detail fragment. + is Album -> { + if (detailModel.currentAlbum.value!!.id == it.id) { + binding.detailRecycler.scrollToPosition(0) + detailModel.doneWithNavToItem() + } else { + findNavController().navigate( + AlbumDetailFragmentDirections.actionShowAlbum(it.id) + ) + } + } + + // Always launch a new ArtistDetailFragment. + is Artist -> { + findNavController().navigate( + AlbumDetailFragmentDirections.actionShowArtist(it.id) + ) + } + + else -> {} } } @@ -148,6 +147,9 @@ class AlbumDetailFragment : DetailFragment() { return binding.root } + /** + * Scroll to an song using its [id]. + */ private fun scrollToItem(id: Long) { // Calculate where the item for the currently played song is val pos = detailModel.albumSortMode.value!!.getSortedSongList( @@ -164,9 +166,7 @@ class AlbumDetailFragment : DetailFragment() { // If the recyclerview can scroll, its certain that it will have to scroll to // correctly center the playing item, so make sure that the Toolbar is lifted in // that case. - if (binding.detailRecycler.computeVerticalScrollRange() > binding.detailRecycler.height) { - binding.detailAppbar.isLifted = true - } + binding.detailAppbar.isLifted = binding.detailRecycler.canScroll() } } } 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 4f08aab93..829f49cc4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -51,6 +51,8 @@ abstract class DetailFragment : Fragment() { /** * Shortcut method for doing setup of the detail toolbar. + * @param menu Menu resource to use + * @param onMenuClick (Optional) a click listener for that menu */ protected fun setupToolbar( @MenuRes menu: Int = -1, diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 3905035fa..bb09f6311 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -12,40 +12,43 @@ import org.oxycblt.auxio.recycler.SortMode /** * ViewModel that stores data for the [DetailFragment]s, such as what they're showing & what * [SortMode] they are currently on. + * @author OxygenCobalt */ class DetailViewModel : ViewModel() { - private var mIsNavigating = false - val isNavigating: Boolean get() = mIsNavigating + private val mCurrentGenre = MutableLiveData() + private val mCurrentArtist = MutableLiveData() + private val mCurrentAlbum = MutableLiveData() private val mGenreSortMode = MutableLiveData(SortMode.ALPHA_DOWN) - val genreSortMode: LiveData get() = mGenreSortMode - private val mArtistSortMode = MutableLiveData(SortMode.NUMERIC_DOWN) - val artistSortMode: LiveData get() = mArtistSortMode - private val mAlbumSortMode = MutableLiveData(SortMode.NUMERIC_DOWN) - val albumSortMode: LiveData get() = mAlbumSortMode - // Current music models being shown - private val mCurrentGenre = MutableLiveData() + private val mNavToItem = MutableLiveData() + private var mIsNavigating = false + val currentGenre: LiveData get() = mCurrentGenre - - private val mCurrentArtist = MutableLiveData() val currentArtist: LiveData get() = mCurrentArtist - - private val mCurrentAlbum = MutableLiveData() val currentAlbum: LiveData get() = mCurrentAlbum - // Primary navigation flag. - private val mNavToItem = MutableLiveData() + val genreSortMode: LiveData get() = mGenreSortMode + val albumSortMode: LiveData get() = mAlbumSortMode + val artistSortMode: LiveData get() = mArtistSortMode + + val isNavigating: Boolean get() = mIsNavigating + + /** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ val navToItem: LiveData get() = mNavToItem - /** - * Update the current navigation status - * @param value Whether the current [DetailFragment] is navigating or not. - */ - fun updateNavigationStatus(value: Boolean) { - mIsNavigating = value + fun updateGenre(genre: Genre) { + mCurrentGenre.value = genre + } + + fun updateArtist(artist: Artist) { + mCurrentArtist.value = artist + } + + fun updateAlbum(album: Album) { + mCurrentAlbum.value = album } /** @@ -75,7 +78,7 @@ class DetailViewModel : ViewModel() { } /** - * Increment the sort mode of the album songs + * Increment the sort mode of the album song */ fun incrementAlbumSortMode() { mAlbumSortMode.value = when (mAlbumSortMode.value) { @@ -86,25 +89,24 @@ class DetailViewModel : ViewModel() { } } - fun updateGenre(genre: Genre) { - mCurrentGenre.value = genre - } - - fun updateArtist(artist: Artist) { - mCurrentArtist.value = artist - } - - fun updateAlbum(album: Album) { - mCurrentAlbum.value = album - } - - /** Navigate to an item, whether a song/album/artist */ + /** + * Navigate to an item, whether a song/album/artist + */ fun navToItem(item: BaseModel) { mNavToItem.value = item } - /** Mark that the navigation process is done. */ + /** + * Mark that the navigation process is done. + */ fun doneWithNavToItem() { mNavToItem.value = null } + + /** + * Update the current navigation status to [isNavigating] + */ + fun updateNavigationStatus(isNavigating: Boolean) { + mIsNavigating = isNavigating + } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 2e0bee862..41c3fb40e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -70,23 +70,21 @@ class GenreDetailFragment : DetailFragment() { } detailModel.navToItem.observe(viewLifecycleOwner) { - if (it != null) { - when (it) { - // All items will launch new detail fragments. - is Artist -> findNavController().navigate( - GenreDetailFragmentDirections.actionShowArtist(it.id) - ) + when (it) { + // All items will launch new detail fragments. + is Artist -> findNavController().navigate( + GenreDetailFragmentDirections.actionShowArtist(it.id) + ) - is Album -> findNavController().navigate( - GenreDetailFragmentDirections.actionShowAlbum(it.id) - ) + is Album -> findNavController().navigate( + GenreDetailFragmentDirections.actionShowAlbum(it.id) + ) - is Song -> findNavController().navigate( - GenreDetailFragmentDirections.actionShowAlbum(it.album.id) - ) + is Song -> findNavController().navigate( + GenreDetailFragmentDirections.actionShowAlbum(it.album.id) + ) - else -> {} - } + else -> {} } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt index 2ea3f5ce3..b04900b5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt @@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource /** * An adapter for displaying the details and [Song]s of an [Album] + * @author OxygenCobalt */ class AlbumDetailAdapter( private val detailModel: DetailViewModel, @@ -75,8 +76,8 @@ class AlbumDetailAdapter( } /** - * Update the current song that this adapter should be watching for to highlight. - * @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders + * Update the [song] that this adapter should highlight + * @param recycler The recyclerview the highlighting should act on. */ fun highlightSong(song: Song?, recycler: RecyclerView) { // Clear out the last ViewHolder as a song update usually signifies that this current diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt index 9afda4a32..00fa41fbc 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt @@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource /** * An adapter for displaying the [Album]s of an artist. + * @author OxygenCobalt */ class ArtistDetailAdapter( private val detailModel: DetailViewModel, @@ -76,8 +77,8 @@ class ArtistDetailAdapter( } /** - * Update the current album that this adapter should be watching for to highlight. - * @param album The [Album] to highlight if found, null to clear any highlighted ViewHolders + * Update the current [album] that this adapter should highlight + * @param recycler The recyclerview the highlighting should act on. */ fun setCurrentAlbum(album: Album?, recycler: RecyclerView) { // Clear out the last ViewHolder as a song update usually signifies that this current diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt index a6e43041d..e54f8ab0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt @@ -23,6 +23,7 @@ import org.oxycblt.auxio.ui.setTextColorResource /** * An adapter for displaying the [Song]s of a genre. + * @author OxygenCobalt */ class GenreDetailAdapter( private val detailModel: DetailViewModel, @@ -76,8 +77,8 @@ class GenreDetailAdapter( } /** - * Update the current song that this adapter should be watching for to highlight. - * @param song The [Song] to highlight if found, null to clear any highlighted ViewHolders + * Update the [song] that this adapter should highlight + * @param recycler The recyclerview the highlighting should act on. */ fun highlightSong(song: Song?, recycler: RecyclerView) { // Clear out the last ViewHolder as a song update usually signifies that this current diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt index 9d0b19f39..4dafdc54b 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryAdapter.kt @@ -59,8 +59,7 @@ class LibraryAdapter( } /** - * Update the data directly. [notifyDataSetChanged] will be called - * @param newData The new data to be used + * Update the data with [newData]. [notifyDataSetChanged] will be called. */ fun updateData(newData: List) { data = newData 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 f051ec044..2b6a5ef13 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -24,6 +24,7 @@ import org.oxycblt.auxio.ui.newMenu /** * A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for * search functionality. + * @author OxygenCobalt */ class LibraryFragment : Fragment() { private val libraryModel: LibraryViewModel by activityViewModels() @@ -36,7 +37,7 @@ class LibraryFragment : Fragment() { ): View { val binding = FragmentLibraryBinding.inflate(inflater) - val libraryAdapter = LibraryAdapter(::onItemSelection) { view, data -> newMenu(view, data) } + val libraryAdapter = LibraryAdapter(::navToDetail) { view, data -> newMenu(view, data) } // --- UI SETUP --- @@ -78,9 +79,9 @@ class LibraryFragment : Fragment() { libraryModel.updateNavigationStatus(false) if (it is Parent) { - onItemSelection(it) + navToDetail(it) } else if (it is Song) { - onItemSelection(it.album) + navToDetail(it.album) } } } @@ -103,10 +104,9 @@ class LibraryFragment : Fragment() { } /** - * Navigate to a parent UI - * @param parent The parent that should be navigated with + * Navigate to the detail UI for a [parent]. */ - private fun onItemSelection(parent: Parent) { + private fun navToDetail(parent: Parent) { requireView().rootView.clearFocus() if (!libraryModel.isNavigating) { diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt index 6365fa64a..c5abde99f 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryViewModel.kt @@ -20,14 +20,14 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback { private val mLibraryData = MutableLiveData(listOf()) val libraryData: LiveData> get() = mLibraryData - private var mIsNavigating = false - val isNavigating: Boolean get() = mIsNavigating - private var mSortMode = SortMode.ALPHA_DOWN val sortMode: SortMode get() = mSortMode private var mDisplayMode = DisplayMode.SHOW_ARTISTS + private var mIsNavigating = false + val isNavigating: Boolean get() = mIsNavigating + private val settingsManager = SettingsManager.getInstance() private val musicStore = MusicStore.getInstance() @@ -42,8 +42,7 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback { } /** - * Update the current [SortMode] with a menu id. - * @param itemId The id of the menu item selected. + * Update the current [SortMode] using an menu [itemId]. */ fun updateSortMode(@IdRes itemId: Int) { val mode = when (itemId) { @@ -64,28 +63,11 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback { /** * Update the current navigation status - * @param value Whether LibraryFragment is navigating or not */ - fun updateNavigationStatus(value: Boolean) { - mIsNavigating = value + fun updateNavigationStatus(isNavigating: Boolean) { + mIsNavigating = isNavigating } - // --- OVERRIDES --- - - override fun onCleared() { - super.onCleared() - - settingsManager.removeCallback(this) - } - - override fun onLibDisplayModeUpdate(displayMode: DisplayMode) { - mDisplayMode = displayMode - - updateLibraryData() - } - - // --- UTILS --- - /** * Shortcut function for updating the library data with the current [SortMode]/[DisplayMode] */ @@ -100,4 +82,18 @@ class LibraryViewModel : ViewModel(), SettingsManager.Callback { else -> error("DisplayMode $mDisplayMode is unsupported.") } } + + // --- OVERRIDES --- + + override fun onCleared() { + super.onCleared() + + settingsManager.removeCallback(this) + } + + override fun onLibDisplayModeUpdate(displayMode: DisplayMode) { + mDisplayMode = displayMode + + updateLibraryData() + } } diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt index b816f0801..3bd8125dd 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingFragment.kt @@ -15,6 +15,10 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentLoadingBinding import org.oxycblt.auxio.music.MusicStore +/** + * Fragment that handles what to display during the loading process. + * @author OxygenCobalt + */ class LoadingFragment : Fragment() { private val loadingModel: LoadingViewModel by viewModels { LoadingViewModel.Factory(requireActivity().application) @@ -85,6 +89,9 @@ class LoadingFragment : Fragment() { // --- PERMISSIONS --- + /** + * Check if Auxio has the permissions to load music + */ private fun hasNoPermissions(): Boolean { val needRationale = shouldShowRequestPermissionRationale( Manifest.permission.READ_EXTERNAL_STORAGE @@ -107,16 +114,24 @@ class LoadingFragment : Fragment() { // --- UI DISPLAY --- + /** + * Hide all error elements and return to the loading view + */ private fun showLoading(binding: FragmentLoadingBinding) { binding.apply { - loadingCircle.visibility = View.VISIBLE loadingErrorIcon.visibility = View.GONE loadingErrorText.visibility = View.GONE loadingRetryButton.visibility = View.GONE loadingGrantButton.visibility = View.GONE + loadingCircle.visibility = View.VISIBLE } } + /** + * Show an error prompt. + * @param error The [MusicStore.Response] that this error corresponds to. Ignores + * [MusicStore.Response.SUCCESS] + */ private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) { binding.loadingCircle.visibility = View.GONE binding.loadingErrorIcon.visibility = View.VISIBLE diff --git a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt index dd25e5634..70e489d6c 100644 --- a/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/loading/LoadingViewModel.kt @@ -9,17 +9,25 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.oxycblt.auxio.music.MusicStore +/** + * ViewModel responsible for the loading UI and beginning the loading process overall. + * @author OxygenCobalt + */ class LoadingViewModel(private val app: Application) : ViewModel() { private val mResponse = MutableLiveData(null) - val response: LiveData = mResponse - private val mDoGrant = MutableLiveData(false) + + /** The last response from [MusicStore]. Null if the loading process is occurring. */ + val response: LiveData = mResponse val doGrant: LiveData = mDoGrant private var isBusy = false private val musicStore = MusicStore.getInstance() + /** + * Begin the music loading process. The response is pushed to [response] + */ fun load() { // Dont start a new load if the last one hasnt finished if (isBusy) return @@ -33,14 +41,23 @@ class LoadingViewModel(private val app: Application) : ViewModel() { } } + /** + * Show the grant prompt. + */ fun grant() { mDoGrant.value = true } + /** + * Mark that the grant prompt is now shown. + */ fun doneWithGrant() { mDoGrant.value = false } + /** + * Notify this viewmodel that there are no permissions + */ fun notifyNoPermissions() { mResponse.value = MusicStore.Response.NO_PERMS } 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 ca2fa73f8..6712f332c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -8,7 +8,6 @@ import android.net.Uri * The base data object for all music. * @property id The ID that is assigned to this object * @property name The name of this object (Such as a song title) - * @author OxygenCobalt */ sealed class BaseModel { abstract val id: Long @@ -34,7 +33,6 @@ sealed class Parent : BaseModel() { * @property genre The Song's [Genre] * @property seconds The Song's duration in seconds * @property formattedDuration The Song's duration as a duration string. - * @author OxygenCobalt */ data class Song( override val id: Long = -1, @@ -74,7 +72,6 @@ data class Song( * @property artist The Album's parent [Artist]. use this instead of [artistName] * @property songs The Album's child [Song]s. * @property totalDuration The combined duration of all of the album's child songs, formatted. - * @author OxygenCobalt */ data class Album( override val id: Long = -1, @@ -108,7 +105,6 @@ data class Album( * @property albums The list of all [Album]s in this artist * @property genre The most prominent genre for this artist * @property songs The list of all [Song]s in this artist - * @author OxygenCobalt */ data class Artist( override val id: Long = -1, @@ -134,14 +130,13 @@ data class Artist( * The data object for a genre. Inherits [Parent] * @property songs The list of all [Song]s in this genre. * @property resolvedName A name that has been resolved from its int-genre form to its named form. - * @author OxygenCobalt */ data class Genre( override val id: Long = -1, override val name: String, ) : Parent() { val resolvedName: String by lazy { - if (name.contains(Regex("[0123456789)]"))) { + if (name.contains(Regex("([1-9])"))) { name.toNamedGenre() ?: name } else { name @@ -162,7 +157,6 @@ 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 a27e1c00d..9803fb881 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -38,12 +38,12 @@ class MusicStore private constructor() { } } + /** Marker for whether the music loading process has successfully completed. */ var loaded = false private set /** - * Load/Sort the entire music library. - * ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.*** + * Load/Sort the entire music library. Should always be ran on a coroutine. * @param app [Application] required to load the music. */ suspend fun load(app: Application): Response { @@ -68,8 +68,6 @@ class MusicStore private constructor() { mArtists = linker.artists.toList() mGenres = linker.genres.toList() - loaded = true - this@MusicStore.logD( "Music load completed successfully in ${System.currentTimeMillis() - start}ms." ) @@ -80,12 +78,15 @@ class MusicStore private constructor() { return@withContext Response.FAILED } + loaded = true + return@withContext Response.SUCCESS } } /** - * Get the song for a specific URI. + * Get the song for a file [uri]. + * @return The corresponding [Song] for this [uri], null if there isnt one. */ suspend fun getSongForUri(uri: Uri, resolver: ContentResolver): Song? { return withContext(Dispatchers.IO) { @@ -98,6 +99,9 @@ class MusicStore private constructor() { } } + /** + * Responses that [MusicStore] sends back when a [load] call completes. + */ enum class Response { NO_MUSIC, NO_PERMS, FAILED, SUCCESS } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index 075ed7fa8..6a2efa22a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -60,25 +60,17 @@ fun String.toNamedGenre(): String? { } /** - * Convert a song id to its URI - * @return The [Uri] for this song/ + * Convert an id to its corresponding URI */ fun Long.toURI(): Uri { - return ContentUris.withAppendedId( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - this - ) + return ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this) } /** - * Get the URI for an album's cover art. - * @return The [Uri] for the album's cover art/ + * Get the URI for an album's cover art, corresponds to MediaStore. */ fun Long.toAlbumArtURI(): Uri { - return ContentUris.withAppendedId( - Uri.parse("content://media/external/audio/albumart"), - this - ) + return ContentUris.withAppendedId(Uri.parse("content://media/external/audio/albumart"), this) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt index 9bf182cb4..869c91266 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt @@ -10,7 +10,9 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song /** - * Object that links music data to one-another, + * Object that links music data, such as grouping songs into their albums & genres and creating + * artists out of the albums. + * @author OxygenCobalt */ class MusicLinker( private val context: Context, @@ -21,6 +23,10 @@ class MusicLinker( private val resolver = context.contentResolver val artists = mutableListOf() + /** + * Begin the linking process. + * Modified models are pushed to [songs], [albums], [artists], and [genres] + */ fun link() { linkAlbums() linkArtists() diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt index 1f9e88a1e..186574caf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLoader.kt @@ -17,6 +17,7 @@ import org.oxycblt.auxio.music.toAlbumArtURI /** * Class that loads/constructs [Genre]s, [Album]s, and [Song] objects from the filesystem * Artists are constructed in [MusicLinker], as they are only really containers for [Album]s + * @author OxygenCobalt */ class MusicLoader(private val app: Application) { var genres = mutableListOf() @@ -25,6 +26,9 @@ class MusicLoader(private val app: Application) { private val resolver = app.contentResolver + /** + * Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs]. + */ fun loadMusic() { loadGenres() loadAlbums() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlayPauseButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlayPauseButton.kt index 88ac9200f..68ca95298 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlayPauseButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlayPauseButton.kt @@ -12,6 +12,7 @@ import org.oxycblt.auxio.ui.toAnimDrawable /** * Custom [AppCompatImageButton] that handles the animated play/pause icons. + * @author OxygenCobalt */ class PlayPauseButton @JvmOverloads constructor( context: Context, @@ -27,6 +28,10 @@ class PlayPauseButton @JvmOverloads constructor( } } + /** + * Update the play/pause icon to reflect [isPlaying] + * @param animate Whether the icon change should be animated or not. + */ fun setPlaying(isPlaying: Boolean, animate: Boolean) { if (isPlaying) { if (animate) { 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 86a1b709d..3f8d4d14f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -121,7 +121,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.playSong(song, mode) } - /** Play an album.*/ + /** + * Play an [album]. + * @param shuffled Whether to shuffle the new queue + */ fun playAlbum(album: Album, shuffled: Boolean) { if (album.songs.isEmpty()) { logE("Album is empty, Not playing.") @@ -132,7 +135,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.playParent(album, shuffled) } - /** Play an Artist */ + /** + * Play an [artist]. + * @param shuffled Whether to shuffle the new queue + */ fun playArtist(artist: Artist, shuffled: Boolean) { if (artist.songs.isEmpty()) { logE("Artist is empty, Not playing.") @@ -143,7 +149,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.playParent(artist, shuffled) } - /** Play a genre. */ + /** + * Play a [genre]. + * @param shuffled Whether to shuffle the new queue + */ fun playGenre(genre: Genre, shuffled: Boolean) { if (genre.songs.isEmpty()) { logE("Genre is empty, Not playing.") @@ -154,7 +163,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.playParent(genre, shuffled) } - /** Play using a file URI. This will not play instantly during the initial startup sequence.*/ + /** + * Play using a file [uri]. + * This will not play instantly during the initial startup sequence. + */ fun playWithUri(uri: Uri, context: Context) { // Check if everything is already running to run the URI play if (playbackManager.isRestored && musicStore.loaded) { @@ -164,7 +176,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } } - /** Actually play with a given URI internally. The frontend doesn't play instantly. */ + /** + * Play with a file URI. + * This is called after [playWithUri] once its deemed safe to do so. + */ private fun playWithUriInternal(uri: Uri, context: Context) { viewModelScope.launch { musicStore.getSongForUri(uri, context.contentResolver)?.let { song -> @@ -173,14 +188,18 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } } - /** Shuffle all songs */ + /** + * Shuffle all songs + */ fun shuffleAll() { playbackManager.shuffleAll() } // --- POSITION FUNCTIONS --- - /** Update the position and push it to [PlaybackStateManager] */ + /** + * Update the position and push it to [PlaybackStateManager] + */ fun setPosition(progress: Int) { playbackManager.seekTo((progress * 1000).toLong()) } @@ -196,22 +215,25 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- QUEUE FUNCTIONS --- - /** Skip to the next song. */ + /** + * Skip to the next song. + */ fun skipNext() { playbackManager.next() } - /** Skip to the previous song */ + /** + * Skip to the previous song. + */ fun skipPrev() { playbackManager.prev() } /** - * Remove a queue OR user queue item, given a QueueAdapter index. - * @param adapterIndex The [QueueAdapter] index to remove - * @param queueAdapter The [QueueAdapter] itself to push changes to when successful. + * Remove an item at [adapterIndex], being a non-header data index. + * @param queueAdapter [QueueAdapter] instance to push changes to when successful. */ - fun removeQueueAdapterItem(adapterIndex: Int, queueAdapter: QueueAdapter) { + fun removeQueueDataItem(adapterIndex: Int, queueAdapter: QueueAdapter) { var index = adapterIndex.dec() // If the item is in the user queue, then remove it from there after accounting for the header. @@ -234,12 +256,11 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } /** - * Move queue OR user queue items, given QueueAdapter indices. - * @param adapterFrom The [QueueAdapter] index that needs to be moved - * @param adapterTo The destination [QueueAdapter] index. - * @param queueAdapter the [QueueAdapter] to push changes to when successful. + * Move a queue OR user queue item from [adapterFrom] to [adapterTo], as long as both + * indices are non-header data indices. + * @param queueAdapter [QueueAdapter] instance to push changes to when successful. */ - fun moveQueueAdapterItems( + fun moveQueueDataItems( adapterFrom: Int, adapterTo: Int, queueAdapter: QueueAdapter @@ -283,38 +304,50 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { return true } - /** Add a [Song] to the user queue.*/ + /** + * Add a [Song] to the user queue. + */ fun addToUserQueue(song: Song) { playbackManager.addToUserQueue(song) } - /** Add an [Album] to the user queue */ + /** + * Add an [Album] to the user queue + */ fun addToUserQueue(album: Album) { val songs = SortMode.NUMERIC_DOWN.getSortedSongList(album.songs) playbackManager.addToUserQueue(songs) } - /** Clear the user queue entirely */ + /** + * Clear the user queue entirely + */ fun clearUserQueue() { playbackManager.clearUserQueue() } // --- STATUS FUNCTIONS --- - /** Flip the playing status, e.g from playing to paused */ + /** + * Flip the playing status, e.g from playing to paused + */ fun invertPlayingStatus() { enableAnimation() playbackManager.setPlaying(!playbackManager.isPlaying) } - /** Flip the shuffle status, e.g from on to off. Will keep song by default. */ + /** + * Flip the shuffle status, e.g from on to off. Will keep song by default. + */ fun invertShuffleStatus() { playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true) } - /** Increment the loop status, e.g from off to loop once */ + /** + * Increment the loop status, e.g from off to loop once + */ fun incrementLoopStatus() { playbackManager.setLoopMode(playbackManager.loopMode.increment()) } @@ -322,8 +355,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- SAVE/RESTORE FUNCTIONS --- /** - * Force save the current [PlaybackStateManager] state to the database. Called by SettingsListFragment. - * @param context [Context] required. + * Force save the current [PlaybackStateManager] state to the database. + * Called by SettingsListFragment. */ fun savePlaybackState(context: Context) { viewModelScope.launch { @@ -334,7 +367,9 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } /** - * Handle the file last-saved file intent, or restore playback, depending on the situation. + * Restore playback on startup. This can do one of two things: + * - Play a file intent that was given by MainActivity in [playWithUri] + * - Restore the last playback state if there is no active file intent. */ fun setupPlayback(context: Context) { val intentUri = mIntentUri @@ -347,13 +382,17 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // Remove the uri after finishing the calls so that this does not fire again. mIntentUri = null } else if (!playbackManager.isRestored) { + // Otherwise just restore viewModelScope.launch { - playbackManager.getStateFromDatabase(context) + playbackManager.restoreFromDatabase(context) } } } - /** Attempt to restore the current playback state from an existing [PlaybackStateManager] instance */ + /** + * Attempt to restore the current playback state from an existing + * [PlaybackStateManager] instance. + */ private fun restorePlaybackState() { logD("Attempting to restore playback state.") @@ -371,17 +410,23 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- OTHER FUNCTIONS --- - /** Set whether the seeking indicator should be highlighted */ - fun setSeekingStatus(value: Boolean) { - mIsSeeking.value = value + /** + * Set whether the seeking indicator should be highlighted + */ + fun setSeekingStatus(isSeeking: Boolean) { + mIsSeeking.value = isSeeking } - /** Enable animation on CompactPlaybackFragment */ + /** + * Enable animation on CompactPlaybackFragment + */ fun enableAnimation() { mCanAnimate = true } - /** Disable animation on CompactPlaybackFragment */ + /** + * Disable animation on CompactPlaybackFragment + */ fun disableAnimation() { mCanAnimate = false } 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 646fbc59a..f911f045e 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 @@ -38,13 +38,15 @@ class QueueAdapter( override fun getItemViewType(position: Int): Int { val item = data[position] - return if (item is Header) - if (item.isAction) + return if (item is Header) { + if (item.isAction) { USER_QUEUE_HEADER_ITEM_TYPE - else + } else { HeaderViewHolder.ITEM_TYPE - else + } + } else { QUEUE_SONG_ITEM_TYPE + } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -80,7 +82,8 @@ class QueueAdapter( } /** - * Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes occurred to the data** + * Submit data using [AsyncListDiffer]. + * **Only use this if you have no idea what changes occurred to the data** */ fun submitList(newData: MutableList) { if (data != newData) { @@ -91,7 +94,8 @@ class QueueAdapter( } /** - * Move Items. Used since [submitList] will cause QueueAdapter to freak-out here. + * Move Items. + * Used since [submitList] will cause QueueAdapter to freak-out here. */ fun moveItems(adapterFrom: Int, adapterTo: Int) { val item = data.removeAt(adapterFrom) @@ -101,17 +105,20 @@ class QueueAdapter( } /** - * Remove an item. Used since [submitList] will cause QueueAdapter to freak-out here. + * Remove an item. + * Used since [submitList] will cause QueueAdapter to freak-out here. */ fun removeItem(adapterIndex: Int) { data.removeAt(adapterIndex) /* - * Check for two things: - * If the data from the next queue is now entirely empty [Signified by a header at the end] - * Or if the data from the last queue is now entirely empty [Signified by there being - * 2 headers with no items in between] - * If so, remove the header and the removed item in a range. Otherwise just remove the item. + * If the data from the next queue is now entirely empty [Signified by a header at the + * end, remove the next queue header as notify as such. + * + * If the user queue is empty [Signified by there being two headers at the beginning with + * nothing in between], then remove the user queue header and notify as such. + * + * Otherwise just remove the item as usual. */ if (data[data.lastIndex] is Header) { val lastIndex = data.lastIndex diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt index 48177cafb..4393cc621 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueDragCallback.kt @@ -11,7 +11,6 @@ import kotlin.math.sign /** * The Drag callback used by the queue recyclerview. Delivers updates to [PlaybackViewModel] * and [QueueAdapter] simultaneously. - * @param playbackModel The [PlaybackViewModel] required to dispatch updates to. * @author OxygenCobalt */ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() { @@ -58,7 +57,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder ): Boolean { - return playbackModel.moveQueueAdapterItems( + return playbackModel.moveQueueDataItems( viewHolder.adapterPosition, target.adapterPosition, queueAdapter @@ -66,7 +65,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - playbackModel.removeQueueAdapterItem(viewHolder.adapterPosition, queueAdapter) + playbackModel.removeQueueDataItem(viewHolder.adapterPosition, queueAdapter) } /** 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 5479c8fc4..8f1a4a592 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 @@ -22,8 +22,6 @@ import org.oxycblt.auxio.ui.isIrregularLandscape /** * A [Fragment] that contains both the user queue and the next queue, with the ability to * edit them as well. - * - * Instantiation is done by the navigation component, **do not instantiate this fragment manually.** * @author OxygenCobalt */ class QueueFragment : Fragment() { 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 index bddfd9b6b..2217d9bad 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt @@ -2,6 +2,7 @@ package org.oxycblt.auxio.playback.state /** * Enum that determines the playback repeat mode. + * @author OxygenCobalt */ enum class LoopMode { NONE, ONCE, INFINITE; @@ -35,7 +36,7 @@ enum class LoopMode { const val CONSTANT_INFINITE = 0xA052 /** - * Convert an int constant into a LoopMode + * Convert an int [constant] into a LoopMode * @return The corresponding LoopMode. Null if it corresponds to nothing. */ fun fromInt(constant: Int): LoopMode? { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt index cfdee1494..7e3d1adf2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt @@ -1,6 +1,9 @@ package org.oxycblt.auxio.playback.state -// Enum that instructs how the queue should be constructed +/** + * Enum that indicates how the queue should be constructed. + * @author OxygenCobalt + */ enum class PlaybackMode { /** Construct the queue from the genre's songs */ IN_GENRE, @@ -31,7 +34,7 @@ enum class PlaybackMode { const val CONSTANT_ALL_SONGS = 0xA043 /** - * Get a [PlaybackMode] for an int constant + * Get a [PlaybackMode] for an int [constant] * @return The mode, null if there isnt one for this. */ fun fromInt(constant: Int): PlaybackMode? { 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 a54249f89..61999973c 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 @@ -144,8 +144,7 @@ class PlaybackStateManager private constructor() { // --- PLAYING FUNCTIONS --- /** - * Play a song. - * @param song The song to be played + * Play a [song]. * @param mode The [PlaybackMode] to construct the queue off of. */ fun playSong(song: Song, mode: PlaybackMode) { @@ -190,9 +189,8 @@ class PlaybackStateManager private constructor() { } /** - * Play a parent model, e.g an artist or an album. - * @param parent The model to use - * @param shuffled Whether to shuffle the queue or not + * Play a [parent], such as an artist or album. + * @param shuffled Whether the queue is shuffled or not */ fun playParent(parent: Parent, shuffled: Boolean) { logD("Playing ${parent.name}") @@ -221,8 +219,19 @@ class PlaybackStateManager private constructor() { } /** - * Shortcut function for updating what song is being played. ***USE THIS INSTEAD OF WRITING OUT ALL THE CODE YOURSELF!!!*** - * @param song The song to play + * Shuffle all songs. + */ + fun shuffleAll() { + mMode = PlaybackMode.ALL_SONGS + mQueue = musicStore.songs.toMutableList() + mParent = null + + setShuffling(true, keepSong = false) + updatePlayback(mQueue[0]) + } + + /** + * Update the playback to a new [song], doing all the required logic. */ private fun updatePlayback(song: Song) { mIsInUserQueue = false @@ -326,8 +335,7 @@ class PlaybackStateManager private constructor() { // --- QUEUE EDITING FUNCTIONS --- /** - * Remove a queue item at a QUEUE index. Will log an error if the index is out of bounds - * @param index The index at which the item should be removed. + * Remove a queue item at [index]. Will ignore invalid indexes. */ fun removeQueueItem(index: Int): Boolean { logD("Removing item ${mQueue[index].name}.") @@ -346,10 +354,7 @@ class PlaybackStateManager private constructor() { } /** - * Move a queue item from a QUEUE INDEX to a QUEUE INDEX. Will log an error if one of the indices - * is out of bounds. - * @param from The starting item's index - * @param to The destination index. + * Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */ fun moveQueueItems(from: Int, to: Int): Boolean { if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) { @@ -367,8 +372,7 @@ class PlaybackStateManager private constructor() { } /** - * Add a song to the user queue. - * @param song The song to add + * Add a [song] to the user queue. */ fun addToUserQueue(song: Song) { mUserQueue.add(song) @@ -377,8 +381,7 @@ class PlaybackStateManager private constructor() { } /** - * Add a list of songs to the user queue. - * @param songs The songs to add. + * Add a list of [songs] to the user queue. */ fun addToUserQueue(songs: List) { mUserQueue.addAll(songs) @@ -387,8 +390,7 @@ class PlaybackStateManager private constructor() { } /** - * Remove a USER QUEUE item at a USER QUEUE index. Will log an error if the index is out of bounds. - * @param index The index at which the item should be removed. + * Remove a USER queue item at [index]. Will ignore invalid indexes. */ fun removeUserQueueItem(index: Int) { logD("Removing item ${mUserQueue[index].name}.") @@ -405,10 +407,7 @@ class PlaybackStateManager private constructor() { } /** - * Move a USER QUEUE item from a USER QUEUE index to another USER QUEUE index. Will log an error if one of the indices - * is out of bounds. - * @param from The starting item's index - * @param to The destination index. + * Move a USER queue item at [from] to a position at [to]. Will ignore invalid indexes. */ fun moveUserQueueItems(from: Int, to: Int) { if (from > mUserQueue.size || from < 0 || to > mUserQueue.size || to < 0) { @@ -449,24 +448,11 @@ class PlaybackStateManager private constructor() { // --- SHUFFLE FUNCTIONS --- /** - * Shuffle all songs. - */ - fun shuffleAll() { - mMode = PlaybackMode.ALL_SONGS - mQueue = musicStore.songs.toMutableList() - mParent = null - - setShuffling(true, keepSong = false) - updatePlayback(mQueue[0]) - } - - /** - * Set the shuffle status. Updates the queue accordingly - * @param shuffling Whether the queue should be shuffled or not. + * Set whether this instance is [shuffled]. Updates the queue accordingly * @param keepSong Whether the current song should be kept as the queue is shuffled/unshuffled */ - fun setShuffling(shuffling: Boolean, keepSong: Boolean) { - mIsShuffling = shuffling + fun setShuffling(shuffled: Boolean, keepSong: Boolean) { + mIsShuffling = shuffled if (mIsShuffling) { genShuffle(keepSong, mIsInUserQueue) @@ -525,8 +511,7 @@ class PlaybackStateManager private constructor() { // --- STATE FUNCTIONS --- /** - * Set the current playing status - * @param playing Whether the playback should be playing or paused. + * Set whether this instance is currently [playing]. */ fun setPlaying(playing: Boolean) { if (mIsPlaying != playing) { @@ -539,7 +524,7 @@ class PlaybackStateManager private constructor() { } /** - * Update the current position. Will not notify any listeners of a seek event, that's what [seekTo] is for. + * Update the current [position]. Will not notify listeners of a seek event. * @param position The new position in millis. * @see seekTo */ @@ -553,10 +538,9 @@ class PlaybackStateManager private constructor() { } /** - * **Seek** to a position, this calls [PlaybackStateManager.Callback.onSeek] to notify + * **Seek** to a [position], this calls [PlaybackStateManager.Callback.onSeek] to notify * elements that rely on that. * @param position The position to seek to in millis. - * @see setPosition */ fun seekTo(position: Long) { mPosition = position @@ -573,15 +557,14 @@ class PlaybackStateManager private constructor() { } /** - * Set the [LoopMode] - * @param mode The [LoopMode] to be used + * Set the [LoopMode] to [mode]. */ fun setLoopMode(mode: LoopMode) { mLoopMode = mode } /** - * Reset the current [LoopMode], if needed. + * Reset the current [LoopMode] from [LoopMode.ONCE], if needed. * Use this instead of duplicating the code manually. */ fun clearLoopMode() { @@ -614,45 +597,42 @@ class PlaybackStateManager private constructor() { suspend fun saveStateToDatabase(context: Context) { logD("Saving state to DB.") - val start = System.currentTimeMillis() - + // Pack the entire state and save it to the database. withContext(Dispatchers.IO) { - val playbackState = packToPlaybackState() - val queueItems = packQueue() + val start = System.currentTimeMillis() val database = PlaybackStateDatabase.getInstance(context) - database.writeState(playbackState) - database.writeQueue(queueItems) + + database.writeState(packToPlaybackState()) + database.writeQueue(packQueue()) + + this@PlaybackStateManager.logD( + "Save finished in ${System.currentTimeMillis() - start}ms" + ) } - - val time = System.currentTimeMillis() - start - - logD("Save finished in ${time}ms") } /** * Restore the state from the database * @param context [Context] required. */ - suspend fun getStateFromDatabase(context: Context) { + suspend fun restoreFromDatabase(context: Context) { logD("Getting state from DB.") - val start: Long - + val now: Long val state: PlaybackState? + // The coroutine call is locked at queueItems so that this function does not + // go ahead until EVERYTHING is read. val queueItems = withContext(Dispatchers.IO) { - start = System.currentTimeMillis() + now = System.currentTimeMillis() val database = PlaybackStateDatabase.getInstance(context) - state = database.readState() database.readQueue() } - val loadTime = System.currentTimeMillis() - start - - logD("Load finished in ${loadTime}ms") + // Get off the IO coroutine since it will cause LiveData updates to throw an exception state?.let { logD("Valid playback state $it") @@ -663,11 +643,9 @@ class PlaybackStateManager private constructor() { doParentSanityCheck() } - val time = System.currentTimeMillis() - start + logD("Restore finished in ${System.currentTimeMillis() - now}ms") - logD("Restore finished in ${time}ms") - - mIsRestored = true + setRestored() } /** @@ -693,8 +671,7 @@ class PlaybackStateManager private constructor() { } /** - * Unpack the state from a [PlaybackState] - * @param playbackState The state to unpack. + * Unpack a [playbackState] into this instance. */ private fun unpackFromPlaybackState(playbackState: PlaybackState) { // Turn the simplified information from PlaybackState into values that can be used diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt index 39fa377d9..03d09aad2 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt @@ -14,6 +14,7 @@ import org.oxycblt.auxio.settings.SettingsManager /** * Object that manages the AudioFocus state. * Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe) + * @author OxygenCobalt */ class AudioReactor( context: Context, diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt index 62edee717..73f62edf6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt @@ -10,6 +10,7 @@ import android.os.Build import android.support.v4.media.session.MediaSessionCompat import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat +import androidx.media.app.NotificationCompat.MediaStyle import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.MainActivity import org.oxycblt.auxio.R @@ -18,7 +19,6 @@ import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.PlaybackStateManager -import androidx.media.app.NotificationCompat as MediaNotificationCompat /** * The unified notification for [PlaybackService]. This is not self-sufficient, updates have @@ -51,7 +51,7 @@ class PlaybackNotification private constructor( addAction(buildAction(context, ACTION_EXIT, R.drawable.ic_exit)) setStyle( - MediaNotificationCompat.MediaStyle() + MediaStyle() .setMediaSession(mediaToken) .setShowActionsInCompactView(1, 2, 3) ) @@ -186,7 +186,7 @@ class PlaybackNotification private constructor( const val ACTION_EXIT = "ACTION_AUXIO_EXIT_" + BuildConfig.BUILD_TYPE /** - * Build a new instance of [PlaybackNotification]. + * Build a new instance of [PlaybackNotification] from a [context] and [mediaSession] */ fun from(context: Context, mediaSession: MediaSessionCompat): PlaybackNotification { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 1e3354797..806ea9dc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -49,7 +49,6 @@ import org.oxycblt.auxio.settings.SettingsManager * - The single [SimpleExoPlayer] instance. * - The [MediaSessionCompat] * - The Media Notification - * - Audio Focus * - Headset management * * This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], @@ -485,6 +484,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca private inner class SystemEventReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { + + // --- NOTIFICATION CASES --- + PlaybackNotification.ACTION_PLAY_PAUSE -> playbackManager.setPlaying( !playbackManager.isPlaying ) diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt b/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt index 3148c9f54..a54298e7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/SortMode.kt @@ -11,7 +11,6 @@ import org.oxycblt.auxio.music.Song /** * An enum for the current sorting mode. Contains helper functions to sort lists based * off the given sorting mode. - * TODO: Improve sorting by separating UP/DOWN from what should be sorted (Names, Tracks, etc) * @property iconRes The icon for this [SortMode] * @author OxygenCobalt */ diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt index 07ab8b2a2..f3c00ca04 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/BaseViewHolder.kt @@ -9,8 +9,8 @@ import org.oxycblt.auxio.music.BaseModel * A [RecyclerView.ViewHolder] that streamlines a lot of the common things across all viewholders. * @param T The datatype, inheriting [BaseModel] for this ViewHolder. * @param binding Basic [ViewDataBinding] required to set up click listeners & sizing. - * @param doOnClick (Optional, defaults to null) Function that specifies what to do on a click. Null if nothing should be done. - * @param doOnLongClick (Optional, defaults to null) Functions that specifics what to do on a long click. Null if nothing should be done. + * @param doOnClick (Optional) Function that calls on a click. + * @param doOnLongClick (Optional) Functions that calls on a long-click. * @author OxygenCobalt */ abstract class BaseViewHolder( @@ -38,8 +38,8 @@ abstract class BaseViewHolder( } doOnLongClick?.let { onLongClick -> - binding.root.setOnLongClickListener { - onLongClick(binding.root, data) + binding.root.setOnLongClickListener { view -> + onLongClick(view, data) true } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index dff9a62d5..3c0b43ceb 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -76,6 +76,7 @@ class SearchFragment : Fragment() { } binding.searchEditText.addTextChangedListener { + // Run the search with the updated text as the query searchModel.doSearch(it?.toString() ?: "", requireContext()) } @@ -144,29 +145,29 @@ class SearchFragment : Fragment() { } /** - * Navigate to an item, or play it, depending on what the given item is. - * @param baseModel The data the action should be done with + * Function that handles when an [item] is selected. + * Handles all datatypes that are selectable. */ - private fun onItemSelection(baseModel: BaseModel) { - if (baseModel is Song) { - playbackModel.playSong(baseModel) + private fun onItemSelection(item: BaseModel) { + if (item is Song) { + playbackModel.playSong(item) return } - // Get rid of the keyboard + // Get rid of the keyboard if we are navigating requireView().rootView.clearFocus() if (!searchModel.isNavigating) { searchModel.updateNavigationStatus(true) - logD("Navigating to the detail fragment for ${baseModel.name}") + logD("Navigating to the detail fragment for ${item.name}") findNavController().navigate( - when (baseModel) { - is Genre -> SearchFragmentDirections.actionShowGenre(baseModel.id) - is Artist -> SearchFragmentDirections.actionShowArtist(baseModel.id) - is Album -> SearchFragmentDirections.actionShowAlbum(baseModel.id) + when (item) { + is Genre -> SearchFragmentDirections.actionShowGenre(item.id) + is Artist -> SearchFragmentDirections.actionShowArtist(item.id) + is Album -> SearchFragmentDirections.actionShowAlbum(item.id) // If given model wasn't valid, then reset the navigation status // and abort the navigation. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 22bd8c34e..dce995318 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -20,15 +20,14 @@ import org.oxycblt.auxio.settings.SettingsManager */ class SearchViewModel : ViewModel() { private val mSearchResults = MutableLiveData(listOf()) - val searchResults: LiveData> get() = mSearchResults - + private var mIsNavigating = false private var mFilterMode = DisplayMode.SHOW_ALL - val filterMode: DisplayMode get() = mFilterMode - private var mLastQuery = "" - private var mIsNavigating = false + /** Current search results from the last [doSearch] call. */ + val searchResults: LiveData> get() = mSearchResults val isNavigating: Boolean get() = mIsNavigating + val filterMode: DisplayMode get() = mFilterMode private val musicStore = MusicStore.getInstance() private val settingsManager = SettingsManager.getInstance() @@ -38,9 +37,8 @@ class SearchViewModel : ViewModel() { } /** - * Perform a search of the music library. Will push results to [searchResults]. - * @param query The query to use - * @param context [Context] required to create the headers + * Use [query] to perform a search of the music library. + * Will push results to [searchResults]. */ fun doSearch(query: String, context: Context) { mLastQuery = query @@ -86,6 +84,10 @@ class SearchViewModel : ViewModel() { } } + /** + * Update the current filter mode with a menu [id]. + * New value will be pushed to [filterMode]. + */ fun updateFilterModeWithId(@IdRes id: Int, context: Context) { mFilterMode = DisplayMode.fromId(id) @@ -94,6 +96,10 @@ class SearchViewModel : ViewModel() { doSearch(mLastQuery, context) } + /** + * Shortcut that will run a ignoreCase filter on a list and only return + * a value if the resulting list is empty. + */ private fun List.filterByOrNull(value: String): List? { val filtered = filter { it.name.contains(value, ignoreCase = true) } @@ -101,10 +107,9 @@ class SearchViewModel : ViewModel() { } /** - * Update the current navigation status - * @param value Whether LibraryFragment is navigating or not + * Update the current navigation status to [isNavigating] */ - fun updateNavigationStatus(value: Boolean) { - mIsNavigating = value + fun updateNavigationStatus(isNavigating: Boolean) { + mIsNavigating = isNavigating } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 5e8c054d2..277bd765d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -45,6 +45,9 @@ class SettingsListFragment : PreferenceFragmentCompat() { setPreferencesFromResource(R.xml.prefs_main, rootKey) } + /** + * Recursively call [handlePreference] on a preference. + */ private fun recursivelyHandleChildren(pref: Preference) { if (pref is PreferenceCategory) { // If this preference is a category of its own, handle its own children @@ -54,6 +57,9 @@ class SettingsListFragment : PreferenceFragmentCompat() { } } + /** + * Handle a preference, doing any specific actions on it. + */ private fun handlePreference(pref: Preference) { pref.apply { when (key) { @@ -134,6 +140,9 @@ class SettingsListFragment : PreferenceFragmentCompat() { } } + /** + * Show the accent dialog to the user + */ private fun showAccentDialog() { MaterialDialog(requireActivity()).show { title(R.string.setting_accent) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index e75c000f7..bd322e70a 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -23,15 +23,11 @@ class SettingsManager private constructor(context: Context) : // --- VALUES --- - /** - * The current theme. - */ + /** The current theme */ val theme: Int get() = sharedPrefs.getString(Keys.KEY_THEME, EntryValues.THEME_AUTO)!!.toThemeInt() - /** - * The current accent. - */ + /** The current accent. */ var accent: Accent get() { // Accent is stored as an index [to be efficient], so retrieve it when done. @@ -47,9 +43,7 @@ class SettingsManager private constructor(context: Context) : .apply() } - /** - * Whether to colorize the notification - */ + /** Whether to colorize the notification */ val colorizeNotif: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_COLORIZE_NOTIFICATION, true) @@ -60,9 +54,7 @@ class SettingsManager private constructor(context: Context) : val useAltNotifAction: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_USE_ALT_NOTIFICATION_ACTION, false) - /** - * What to display on the library. - */ + /** What to display on the library. */ val libraryDisplayMode: DisplayMode get() = DisplayMode.valueOfOrFallback( sharedPrefs.getString( @@ -78,27 +70,19 @@ class SettingsManager private constructor(context: Context) : val showCovers: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_SHOW_COVERS, true) - /** - * Whether to ignore MediaStore covers - */ + /** Whether to ignore MediaStore covers */ val useQualityCovers: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_QUALITY_COVERS, false) - /** - * Whether to do Audio focus. - */ + /** Whether to do Audio focus. */ val doAudioFocus: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_AUDIO_FOCUS, true) - /** - * Whether to resume/stop playback when a headset is connected/disconnected. - */ + /** Whether to resume/stop playback when a headset is connected/disconnected. */ val doPlugMgt: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_PLUG_MANAGEMENT, true) - /** - * What queue to create when a song is selected (ex. From All Songs or Search) - */ + /** What queue to create when a song is selected (ex. From All Songs or Search) */ val songPlaybackMode: PlaybackMode get() = PlaybackMode.valueOfOrFallback( sharedPrefs.getString( @@ -107,28 +91,20 @@ class SettingsManager private constructor(context: Context) : ) ) - /** - * What to do at the end of a playlist. - */ + /** What to do at the end of a playlist. */ val doAtEnd: String get() = sharedPrefs.getString(Keys.KEY_AT_END, EntryValues.AT_END_LOOP_PAUSE) ?: EntryValues.AT_END_LOOP_PAUSE - /** - * Whether shuffle should stay on when a new song is selected. - */ + /** Whether shuffle should stay on when a new song is selected. */ val keepShuffle: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_KEEP_SHUFFLE, true) - /** - * Whether to rewind when the back button is pressed. - */ + /** Whether to rewind when the back button is pressed. */ val rewindWithPrev: Boolean get() = sharedPrefs.getBoolean(Keys.KEY_PREV_REWIND, true) - /** - * The current [SortMode] of the library. - */ + /** The current [SortMode] of the library. */ var librarySortMode: SortMode get() = SortMode.fromInt( sharedPrefs.getInt( @@ -143,9 +119,7 @@ class SettingsManager private constructor(context: Context) : .apply() } - /** - * The current filter mode of the search tab - */ + /** The current filter mode of the search tab */ var searchFilterMode: DisplayMode get() = DisplayMode.valueOfOrFallback( sharedPrefs.getString( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/AboutDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/AboutDialog.kt index 7cf6a19a0..a8f1a010c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/AboutDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/AboutDialog.kt @@ -46,7 +46,8 @@ class AboutDialog : BottomSheetDialogFragment() { } /** - * Go through the process of opening one of the about links in a browser. + * Go through the process of opening a [link] in a browser. Only supports the links + * in [AboutDialog.Companion.LINKS]. */ private fun openLinkInBrowser(link: String) { check(link in LINKS) { "Invalid link." } @@ -83,9 +84,9 @@ class AboutDialog : BottomSheetDialogFragment() { } } catch (e: Exception) { logE("Browser intent launching failed [Probably android's fault]") - e.printStackTrace() + logE(e.stackTraceToString()) - // Sometimes people have """""Browsers""""" on their phone according to android, + // Sometimes people have """Browsers""" on their phone according to android, // but they actually don't so here's a fallback for that. getString(R.string.error_no_browser).createToast(requireContext()) } diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt index 503b1a456..6d8925e90 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsAdapter.kt @@ -11,6 +11,7 @@ import org.oxycblt.auxio.recycler.viewholders.SongViewHolder * @param data List of [Song]s to be shown * @param doOnClick What to do on a click action * @param doOnLongClick What to do on a long click action + * @author OxygenCobalt */ class SongsAdapter( private val data: List, diff --git a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt index 2ad0e22e6..ab2880789 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/SongsFragment.kt @@ -18,23 +18,25 @@ import org.oxycblt.auxio.logD import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.Accent +import org.oxycblt.auxio.ui.canScroll import org.oxycblt.auxio.ui.getSpans import org.oxycblt.auxio.ui.newMenu import kotlin.math.ceil /** - * A [Fragment] that shows a list of all songs on the device. Contains options to search/shuffle - * them. + * A [Fragment] that shows a list of all songs on the device. + * Contains options to search/shuffle them. * @author OxygenCobalt */ class SongsFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() + private val musicStore = MusicStore.getInstance() // Lazy init the text size so that it doesn't have to be calculated every time. private val indicatorTextSize: Float by lazy { TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 14F, - requireContext().resources.displayMetrics + resources.displayMetrics ) } @@ -44,8 +46,6 @@ class SongsFragment : Fragment() { savedInstanceState: Bundle? ): View { val binding = FragmentSongsBinding.inflate(inflater) - - val musicStore = MusicStore.getInstance() val songAdapter = SongsAdapter(musicStore.songs, playbackModel::playSong) { view, data -> newMenu(view, data) } @@ -54,13 +54,12 @@ class SongsFragment : Fragment() { binding.songToolbar.apply { setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_shuffle -> { - playbackModel.shuffleAll() - } + if (it.itemId == R.id.action_shuffle) { + playbackModel.shuffleAll() + true } - true + false } } @@ -76,7 +75,7 @@ class SongsFragment : Fragment() { post { // Disable fast scrolling if there is nothing to scroll - if (computeVerticalScrollRange() < height) { + if (!canScroll()) { binding.songFastScroll.visibility = View.GONE binding.songFastScrollThumb.visibility = View.GONE } @@ -101,8 +100,6 @@ class SongsFragment : Fragment() { * @param binding Binding required */ private fun setupFastScroller(binding: FragmentSongsBinding) { - val musicStore = MusicStore.getInstance() - binding.songFastScroll.apply { var concatInterval = -1 @@ -172,25 +169,27 @@ class SongsFragment : Fragment() { useDefaultScroller = false - itemIndicatorSelectedCallbacks.add( - object : FastScrollerView.ItemIndicatorSelectedCallback { - override fun onItemIndicatorSelected( - indicator: FastScrollItemIndicator, - indicatorCenterY: Int, - itemPosition: Int - ) { - binding.songRecycler.apply { - (layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - itemPosition, 0 - ) + addIndicatorCallback { pos -> + binding.songRecycler.apply { + (layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0) - stopScroll() - } - } + stopScroll() } - ) + } } binding.songFastScrollThumb.setupWithFastScroller(binding.songFastScroll) } + + private fun FastScrollerView.addIndicatorCallback(callback: (pos: Int) -> Unit) { + itemIndicatorSelectedCallbacks.add( + object : FastScrollerView.ItemIndicatorSelectedCallback { + override fun onItemIndicatorSelected( + indicator: FastScrollItemIndicator, + indicatorCenterY: Int, + itemPosition: Int + ) = callback(itemPosition) + } + ) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt index 09e3e1f31..3f4d7bc0f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Accent.kt @@ -44,6 +44,7 @@ val ACCENTS = arrayOf( * @property color The color resource for this accent * @property theme The theme resource for this accent * @property name The name of this accent + * @author OxygenCobalt */ data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes val name: Int) { /** diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt index 11126cd4f..24e823cda 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt @@ -34,7 +34,8 @@ fun Fragment.newMenu(anchor: View, data: BaseModel, flag: Int = ActionMenu.FLAG_ * @param anchor [View] This should be centered around * @param data [BaseModel] this menu corresponds to * @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details. - * @throws IllegalArgumentException When there is no menu for this specific datatype/flag + * @throws IllegalStateException When there is no menu for this specific datatype/flag + * @author OxygenCobalt */ class ActionMenu( activity: AppCompatActivity, 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 3c968092e..c3a9732fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -167,6 +167,11 @@ fun RecyclerView.getSpans(): Int { } } +/** + * Returns whether a recyclerview can scroll. + */ +fun RecyclerView.canScroll() = computeVerticalScrollRange() > height + /** * Check if we are in the "Irregular" landscape mode (e.g landscape, but nav bar is on the sides) * Used to disable most of edge-to-edge if that's the case, as I cant get it to work on this mode. diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt b/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt index e9deb1c7e..d0ddd6ea2 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MemberBinder.kt @@ -26,6 +26,7 @@ fun Fragment.memberBinding( /** * The delegate for the [memberBinding] shortcut function. * Adapted from KAHelpers (https://github.com/FunkyMuse/KAHelpers/tree/master/viewbinding) + * @author OxygenCobalt */ class MemberBinder( private val fragment: Fragment, diff --git a/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt index ae8933a39..506d31192 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/SlideLinearLayout.kt @@ -17,6 +17,7 @@ import java.lang.reflect.Field * * Adapted from this StackOverflow answer: * https://stackoverflow.com/a/35087229 + * @author OxygenCobalt */ class SlideLinearLayout @JvmOverloads constructor( context: Context,