From 9d283fc6e498137b41242a73379de8fa7d397358 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 25 Dec 2022 19:31:27 -0700 Subject: [PATCH] accent: move back to ui Move the accent module back into the ui module, where it's more consistent. --- .../main/java/org/oxycblt/auxio/AuxioApp.kt | 8 +- .../java/org/oxycblt/auxio/IntegerTable.kt | 4 +- .../java/org/oxycblt/auxio/MainActivity.kt | 7 +- .../java/org/oxycblt/auxio/MainFragment.kt | 24 +- .../auxio/detail/AlbumDetailFragment.kt | 1 - .../auxio/detail/ArtistDetailFragment.kt | 3 +- .../java/org/oxycblt/auxio/detail/Detail.kt | 19 +- .../auxio/detail/DetailAppBarLayout.kt | 18 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 18 +- .../auxio/detail/GenreDetailFragment.kt | 3 +- .../oxycblt/auxio/detail/ReadOnlyTextInput.kt | 4 +- .../oxycblt/auxio/detail/SongDetailDialog.kt | 2 +- .../detail/recycler/AlbumDetailAdapter.kt | 2 +- .../detail/recycler/ArtistDetailAdapter.kt | 2 +- .../auxio/detail/recycler/DetailAdapter.kt | 2 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 68 +-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 44 +- .../home/fastscroll/FastScrollRecyclerView.kt | 13 +- .../auxio/home/list/AlbumListFragment.kt | 5 +- .../auxio/home/list/ArtistListFragment.kt | 5 +- .../auxio/home/list/GenreListFragment.kt | 5 +- .../auxio/home/list/SongListFragment.kt | 5 +- .../auxio/home/tabs/AdaptiveTabStrategy.kt | 1 - .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 8 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 4 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 2 +- .../auxio/home/tabs/TabDragCallback.kt | 2 +- .../org/oxycblt/auxio/image/BitmapProvider.kt | 72 ++-- .../org/oxycblt/auxio/image/ImageGroup.kt | 16 +- .../auxio/image/PlaybackIndicatorView.kt | 12 +- .../oxycblt/auxio/image/StyledImageView.kt | 12 +- .../auxio/image/extractor/Components.kt | 10 +- .../oxycblt/auxio/image/extractor/Covers.kt | 29 +- .../oxycblt/auxio/image/extractor/Images.kt | 25 +- .../main/java/org/oxycblt/auxio/list/Data.kt | 21 +- .../org/oxycblt/auxio/list/ListFragment.kt | 16 +- .../java/org/oxycblt/auxio/list/Listeners.kt | 23 +- .../auxio/list/recycler/AuxioRecyclerView.kt | 4 +- .../auxio/list/recycler/DialogRecyclerView.kt | 9 +- .../list/recycler/PlayingIndicatorAdapter.kt | 12 +- .../recycler/SelectionIndicatorAdapter.kt | 4 +- .../auxio/list/recycler/SimpleItemCallback.kt | 5 +- .../auxio/list/recycler/SyncListDiffer.kt | 12 +- .../auxio/list/recycler/ViewHolders.kt | 2 +- .../auxio/list/selection/SelectionFragment.kt | 12 +- .../list/selection/SelectionToolbarOverlay.kt | 10 +- .../list/selection/SelectionViewModel.kt | 28 +- .../java/org/oxycblt/auxio/music/Music.kt | 399 ++++++++---------- .../org/oxycblt/auxio/music/MusicStore.kt | 30 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 1 - .../main/java/org/oxycblt/auxio/music/Sort.kt | 39 +- .../auxio/music/extractor/CacheExtractor.kt | 55 +-- .../auxio/music/extractor/ExtractionResult.kt | 31 +- .../music/extractor/MediaStoreExtractor.kt | 70 +-- .../music/extractor/MetadataExtractor.kt | 43 +- .../auxio/music/extractor/ParsingUtil.kt | 60 ++- .../auxio/music/extractor/SeparatorsDialog.kt | 7 +- .../auxio/music/picker/ArtistChoiceAdapter.kt | 4 +- .../picker/ArtistNavigationPickerDialog.kt | 2 +- .../auxio/music/picker/ArtistPickerDialog.kt | 11 +- .../auxio/music/picker/PickerViewModel.kt | 11 +- .../auxio/music/storage/DirectoryAdapter.kt | 3 +- .../auxio/music/storage/MusicDirsDialog.kt | 16 +- .../oxycblt/auxio/music/storage/Storage.kt | 73 ++-- .../auxio/music/storage/StorageUtil.kt | 60 ++- .../org/oxycblt/auxio/music/system/Indexer.kt | 130 +++--- .../music/system/IndexerNotifications.kt | 9 +- .../auxio/music/system/IndexerService.kt | 34 +- .../auxio/playback/PlaybackBarFragment.kt | 6 +- .../auxio/playback/PlaybackPanelFragment.kt | 6 +- .../auxio/playback/queue/QueueAdapter.kt | 39 +- .../auxio/playback/queue/QueueDragCallback.kt | 10 +- .../auxio/playback/queue/QueueFragment.kt | 2 +- .../auxio/playback/queue/QueueViewModel.kt | 18 +- .../replaygain/PreAmpCustomizeDialog.kt | 2 +- .../playback/system/NotificationComponent.kt | 2 +- .../auxio/playback/system/PlaybackService.kt | 2 +- .../playback/ui/BaseBottomSheetBehavior.kt | 4 +- .../org/oxycblt/auxio/search/SearchAdapter.kt | 6 +- .../oxycblt/auxio/search/SearchFragment.kt | 4 +- .../oxycblt/auxio/search/SearchViewModel.kt | 71 ++-- .../{shared => service}/ForegroundManager.kt | 10 +- .../ForegroundServiceNotification.kt | 9 +- .../oxycblt/auxio/settings/AboutFragment.kt | 2 +- .../org/oxycblt/auxio/settings/Settings.kt | 40 +- .../auxio/settings/SettingsFragment.kt | 2 +- .../auxio/settings/prefs/IntListPreference.kt | 8 +- .../settings/prefs/PreferenceFragment.kt | 8 +- .../settings/prefs/WrappedDialogPreference.kt | 4 +- .../auxio/{shared => ui}/AuxioAppBarLayout.kt | 24 +- .../{shared => ui}/NavigationViewModel.kt | 32 +- .../ViewBindingDialogFragment.kt | 10 +- .../{shared => ui}/ViewBindingFragment.kt | 10 +- .../auxio/{settings => ui}/accent/Accent.kt | 19 +- .../{settings => ui}/accent/AccentAdapter.kt | 2 +- .../accent/AccentCustomizeDialog.kt | 7 +- .../accent/AccentGridLayoutManager.kt | 6 +- .../org/oxycblt/auxio/util/ContextUtil.kt | 12 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 82 ++-- .../java/org/oxycblt/auxio/util/LangUtil.kt | 14 +- .../java/org/oxycblt/auxio/util/LogUtil.kt | 8 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 13 +- .../oxycblt/auxio/widgets/WidgetProvider.kt | 71 ++-- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 41 +- app/src/main/res/layout/dialog_accent.xml | 2 +- app/src/main/res/layout/fragment_about.xml | 4 +- app/src/main/res/layout/fragment_home.xml | 4 +- app/src/main/res/layout/fragment_search.xml | 4 +- app/src/main/res/layout/fragment_settings.xml | 4 +- app/src/main/res/navigation/nav_main.xml | 2 +- 110 files changed, 1159 insertions(+), 1168 deletions(-) rename app/src/main/java/org/oxycblt/auxio/{shared => service}/ForegroundManager.kt (94%) rename app/src/main/java/org/oxycblt/auxio/{shared => service}/ForegroundServiceNotification.kt (92%) rename app/src/main/java/org/oxycblt/auxio/{shared => ui}/AuxioAppBarLayout.kt (88%) rename app/src/main/java/org/oxycblt/auxio/{shared => ui}/NavigationViewModel.kt (85%) rename app/src/main/java/org/oxycblt/auxio/{shared => ui}/ViewBindingDialogFragment.kt (96%) rename app/src/main/java/org/oxycblt/auxio/{shared => ui}/ViewBindingFragment.kt (96%) rename app/src/main/java/org/oxycblt/auxio/{settings => ui}/accent/Accent.kt (94%) rename app/src/main/java/org/oxycblt/auxio/{settings => ui}/accent/AccentAdapter.kt (99%) rename app/src/main/java/org/oxycblt/auxio/{settings => ui}/accent/AccentCustomizeDialog.kt (94%) rename app/src/main/java/org/oxycblt/auxio/{settings => ui}/accent/AccentGridLayoutManager.kt (92%) diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index 202494d77..6b4202953 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -74,14 +74,10 @@ class AuxioApp : Application(), ImageLoaderFactory { .build() companion object { - /** - * The ID of the "Shuffle All" shortcut. - */ + /** The ID of the "Shuffle All" shortcut. */ const val SHORTCUT_SHUFFLE_ID = "shortcut_shuffle" - /** - * The [Intent] name for the "Shuffle All" shortcut. - */ + /** The [Intent] name for the "Shuffle All" shortcut. */ const val INTENT_KEY_SHORTCUT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE_ALL" } } diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 65b921d82..e7a16b502 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -18,8 +18,8 @@ package org.oxycblt.auxio /** - * A table containing all of the magic integer codes that the codebase has currently reserved. - * May be non-contiguous. + * A table containing all of the magic integer codes that the codebase has currently reserved. May + * be non-contiguous. * @author Alexander Capehart (OxygenCobalt) */ object IntegerTable { diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index ffe500111..abc20ea6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -113,10 +113,9 @@ class MainActivity : AppCompatActivity() { } /** - * Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] - * that can be used in the playback system. - * @param intent The (new) [Intent] given to this [MainActivity], or null if there - * is no intent. + * Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used + * in the playback system. + * @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent. * @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started, * false otherwise. */ diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index aebd69e53..c1b332d56 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -40,9 +40,9 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior -import org.oxycblt.auxio.shared.MainNavigationAction -import org.oxycblt.auxio.shared.NavigationViewModel -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.MainNavigationAction +import org.oxycblt.auxio.ui.NavigationViewModel +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.* /** @@ -348,8 +348,8 @@ class MainFragment : } /** - * A [OnBackPressedCallback] that overrides the back button to first navigate out of - * internal app components, such as the Bottom Sheets or Explore Navigation. + * A [OnBackPressedCallback] that overrides the back button to first navigate out of internal + * app components, such as the Bottom Sheets or Explore Navigation. */ private inner class DynamicBackPressedCallback : OnBackPressedCallback(false) { override fun handleOnBackPressed() { @@ -379,13 +379,13 @@ class MainFragment : } /** - * Force this instance to update whether it's enabled or not. If there are no app - * components that the back button should close first, the instance is disabled and - * back navigation is delegated to the system. + * Force this instance to update whether it's enabled or not. If there are no app components + * that the back button should close first, the instance is disabled and back navigation is + * delegated to the system. * - * Normally, this callback would have just called the [MainActivity.onBackPressed] - * if there were no components to close, but that prevents adaptive back navigation - * from working on Android 14+, so we must do it this way. + * Normally, this callback would have just called the [MainActivity.onBackPressed] if there + * were no components to close, but that prevents adaptive back navigation from working on + * Android 14+, so we must do it this way. */ fun invalidateEnabled() { val binding = requireBinding() @@ -397,7 +397,7 @@ class MainFragment : isEnabled = queueSheetBehavior?.state == NeoBottomSheetBehavior.STATE_EXPANDED || - playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED || + playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED || exploreNavController.currentDestination?.id != exploreNavController.graph.startDestinationId } 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 bd2bf8859..3ea4dc802 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -174,7 +174,6 @@ class AlbumDetailFragment : ListFragment(), AlbumDetailAd navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists) } - private fun updateAlbum(album: Album?) { if (album == null) { // Album we were showing no longer exists. 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 f4cc64c39..28cb2ef1b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -54,8 +54,7 @@ class ArtistDetailFragment : ListFragment(), DetailAdapte // Information about what artist to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an artist. private val args: ArtistDetailFragmentArgs by navArgs() - private val detailAdapter = - ArtistDetailAdapter(this) + private val detailAdapter = ArtistDetailAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt index 47d2f132e..639687f45 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.detail import androidx.annotation.StringRes @@ -35,4 +52,4 @@ data class DetailSong(val song: Song, val properties: Properties?) { val sampleRateHz: Int?, val resolvedMimeType: MimeType ) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt index 7584ae05d..8e4a4ed6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout import java.lang.reflect.Field import org.oxycblt.auxio.R -import org.oxycblt.auxio.shared.AuxioAppBarLayout +import org.oxycblt.auxio.ui.AuxioAppBarLayout import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.lazyReflectedField @@ -75,12 +75,13 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // The Toolbar's title view is actually hidden. To avoid having to create our own // title view, we just reflect into Toolbar and grab the hidden field. - val newTitleView = (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply { - // We can never properly initialize the title view's state before draw time, - // so we just set it's alpha to 0f to produce a less jarring initialization - // animation.. - alpha = 0f - } + val newTitleView = + (TOOLBAR_TITLE_TEXT_FIELD.get(toolbar) as TextView).apply { + // We can never properly initialize the title view's state before draw time, + // so we just set it's alpha to 0f to produce a less jarring initialization + // animation.. + alpha = 0f + } this.titleView = newTitleView return newTitleView @@ -161,8 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Title should be visible if we are no longer showing the top item // (i.e the header) appBarLayout.setTitleVisibility( - (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0 - ) + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0) } } 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 a25d8f7b0..4a94e6bb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -44,9 +44,9 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.* /** - * [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. - * Keeps track of the current item they are showing, sub-data to display, and configuration. - * Since this ViewModel requires a context, it must be instantiated [AndroidViewModel]'s Factory. + * [AndroidViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of + * the current item they are showing, sub-data to display, and configuration. Since this ViewModel + * requires a context, it must be instantiated [AndroidViewModel]'s Factory. * @param application [Application] context required to initialize certain information. * @author Alexander Capehart (OxygenCobalt) */ @@ -61,8 +61,8 @@ class DetailViewModel(application: Application) : private val _currentSong = MutableStateFlow(null) /** - * The current [DetailSong] to display. Null if there is nothing to show. - * TODO: De-couple Song and Properties? + * The current [DetailSong] to display. Null if there is nothing to show. TODO: De-couple Song + * and Properties? */ val currentSong: StateFlow get() = _currentSong @@ -224,7 +224,7 @@ class DetailViewModel(application: Application) : * @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid. */ fun setGenreUid(uid: Music.UID) { - if (_currentGenre.value?.uid == uid) { + if (_currentGenre.value?.uid == uid) { // Nothing to do. return } @@ -232,7 +232,7 @@ class DetailViewModel(application: Application) : _currentGenre.value = requireMusic(uid).also { refreshGenreList(it) } } - private fun requireMusic(uid: Music.UID): T = + private fun requireMusic(uid: Music.UID): T = requireNotNull(unlikelyToBeNull(musicStore.library).find(uid)) { "Invalid id provided" } /** @@ -392,8 +392,8 @@ class DetailViewModel(application: Application) : /** * A simpler mapping of [Album.Type] used for grouping and sorting songs. - * @param headerTitleRes The title string resource to use for a header created - * out of an instance of this enum. + * @param headerTitleRes The title string resource to use for a header created out of an + * instance of this enum. */ private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) { ALBUMS(R.string.lbl_albums), 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 4c0ee9090..bb266d1cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -55,8 +55,7 @@ class GenreDetailFragment : ListFragment(), DetailAdapter // Information about what genre to display is initially within the navigation arguments // as a UID, as that is the only safe way to parcel an genre. private val args: GenreDetailFragmentArgs by navArgs() - private val detailAdapter = - GenreDetailAdapter(this) + private val detailAdapter = GenreDetailAdapter(this) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt index ea9f53c42..327038255 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ReadOnlyTextInput.kt @@ -25,8 +25,8 @@ import com.google.android.material.textfield.TextInputEditText import org.oxycblt.auxio.R /** - * A [TextInputEditText] that deliberately restricts all input except for selection. This will - * work just like a normal block of selectable/copyable text, but with nicer aesthetics. + * A [TextInputEditText] that deliberately restricts all input except for selection. This will work + * just like a normal block of selectable/copyable text, but with nicer aesthetics. * * Adapted from Material Files: https://github.com/zhanghai/MaterialFiles * diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index 7fe0792cb..fb51a4593 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.collectImmediately diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index e89f7688f..8d1eb25ec 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.detail.DiscHeader -import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.music.Album diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index 44830fd53..12b9b2fd4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -26,8 +26,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding -import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.music.Album diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 051e5c45d..20d18d578 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.detail.SortHeader -import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 05efb148c..e2320e1c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -58,8 +58,8 @@ import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.system.Indexer -import org.oxycblt.auxio.shared.MainNavigationAction -import org.oxycblt.auxio.shared.NavigationViewModel +import org.oxycblt.auxio.ui.MainNavigationAction +import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.util.* /** @@ -100,8 +100,7 @@ class HomeFragment : override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater) - override fun getSelectionToolbar(binding: FragmentHomeBinding) = - binding.homeSelectionToolbar + override fun getSelectionToolbar(binding: FragmentHomeBinding) = binding.homeSelectionToolbar override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) @@ -239,7 +238,8 @@ class HomeFragment : } private fun setupPager(binding: FragmentHomeBinding) { - binding.homePager.adapter = HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) + binding.homePager.adapter = + HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner) val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams if (homeModel.currentTabModes.size == 1) { @@ -256,32 +256,36 @@ class HomeFragment : } // Set up the mapping between the ViewPager and TabLayout. - TabLayoutMediator(binding.homeTabs, binding.homePager, - AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)).attach() + TabLayoutMediator( + binding.homeTabs, + binding.homePager, + AdaptiveTabStrategy(requireContext(), homeModel.currentTabModes)) + .attach() } private fun updateCurrentTab(tabMode: MusicMode) { // Update the sort options to align with those allowed by the tab - val isVisible: (Int) -> Boolean = when (tabMode) { - // Disallow sorting by count for songs - MusicMode.SONGS -> { id -> id != R.id.option_sort_count } - // Disallow sorting by album for albums - MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album } - // Only allow sorting by name, count, and duration for artists - MusicMode.ARTISTS -> { id -> - id == R.id.option_sort_asc || - id == R.id.option_sort_name || - id == R.id.option_sort_count || - id == R.id.option_sort_duration + val isVisible: (Int) -> Boolean = + when (tabMode) { + // Disallow sorting by count for songs + MusicMode.SONGS -> { id -> id != R.id.option_sort_count } + // Disallow sorting by album for albums + MusicMode.ALBUMS -> { id -> id != R.id.option_sort_album } + // Only allow sorting by name, count, and duration for artists + MusicMode.ARTISTS -> { id -> + id == R.id.option_sort_asc || + id == R.id.option_sort_name || + id == R.id.option_sort_count || + id == R.id.option_sort_duration + } + // Only allow sorting by name, count, and duration for genres + MusicMode.GENRES -> { id -> + id == R.id.option_sort_asc || + id == R.id.option_sort_name || + id == R.id.option_sort_count || + id == R.id.option_sort_duration + } } - // Only allow sorting by name, count, and duration for genres - MusicMode.GENRES -> { id -> - id == R.id.option_sort_asc || - id == R.id.option_sort_name || - id == R.id.option_sort_count || - id == R.id.option_sort_duration - } - } val sortMenu = requireNotNull(sortItem.subMenu) val toHighlight = homeModel.getSortForTab(tabMode) @@ -289,8 +293,9 @@ class HomeFragment : for (option in sortMenu) { // Check the ascending option and corresponding sort option to align with // the current sort of the tab. - option.isChecked = option.itemId == toHighlight.mode.itemId - || (option.itemId == R.id.option_sort_asc && toHighlight.isAscending) + option.isChecked = + option.itemId == toHighlight.mode.itemId || + (option.itemId == R.id.option_sort_asc && toHighlight.isAscending) // Disable options that are not allowed by the isVisible lambda option.isVisible = isVisible(option.itemId) @@ -454,8 +459,8 @@ class HomeFragment : } /** - * Get the ID of the RecyclerView contained by [ViewPager2] tab represented with - * the given [MusicMode]. + * Get the ID of the RecyclerView contained by [ViewPager2] tab represented with the given + * [MusicMode]. * @param tabMode The [MusicMode] of the tab. * @return The ID of the RecyclerView contained by the given tab. */ @@ -478,8 +483,7 @@ class HomeFragment : private val tabs: List, fragmentManager: FragmentManager, lifecycleOwner: LifecycleOwner - ) : - FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle ) { + ) : FragmentStateAdapter(fragmentManager, lifecycleOwner.lifecycle) { override fun getItemCount() = tabs.size diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index eb353778c..d6be75fd4 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -44,60 +44,50 @@ class HomeViewModel(application: Application) : private val settings = Settings(application, this) private val _songsList = MutableStateFlow(listOf()) - /** - * A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. - */ + /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ val songLists: StateFlow> get() = _songsList private val _albumsLists = MutableStateFlow(listOf()) - /** - * A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. - */ + /** A list of [Album]s, sorted by the preferred [Sort], to be shown in the home view. */ val albumsList: StateFlow> get() = _albumsLists private val _artistsList = MutableStateFlow(listOf()) /** - * A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. - * Note that if "Hide collaborators" is on, this list will not include [Artist]s - * where [Artist.isCollaborator] is true. + * A list of [Artist]s, sorted by the preferred [Sort], to be shown in the home view. Note that + * if "Hide collaborators" is on, this list will not include [Artist]s where + * [Artist.isCollaborator] is true. */ val artistsList: MutableStateFlow> get() = _artistsList private val _genresList = MutableStateFlow(listOf()) - /** - * A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. - */ + /** A list of [Genre]s, sorted by the preferred [Sort], to be shown in the home view. */ val genresList: StateFlow> get() = _genresList /** - * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding - * invisible [Tab]s. + * A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible + * [Tab]s. */ var currentTabModes: List = makeTabModes() private set private val _currentTabMode = MutableStateFlow(currentTabModes[0]) - /** - * The [MusicMode] of the currently shown [Tab]. - */ + /** The [MusicMode] of the currently shown [Tab]. */ val currentTabMode: StateFlow = _currentTabMode private val _shouldRecreate = MutableStateFlow(false) /** - * A marker to re-create all library tabs, usually initiated by a settings change. - * When this flag is true, all tabs (and their respective ViewPager2 fragments) will be - * re-created from scratch. + * A marker to re-create all library tabs, usually initiated by a settings change. When this + * flag is true, all tabs (and their respective ViewPager2 fragments) will be re-created from + * scratch. */ val shouldRecreate: StateFlow = _shouldRecreate private val _isFastScrolling = MutableStateFlow(false) - /** - * A marker for whether the user is fast-scrolling in the home view or not. - */ + /** A marker for whether the user is fast-scrolling in the home view or not. */ val isFastScrolling: StateFlow = _isFastScrolling init { @@ -136,7 +126,6 @@ class HomeViewModel(application: Application) : currentTabModes = makeTabModes() _shouldRecreate.value = true } - context.getString(R.string.set_key_hide_collaborators) -> { // Changes in the hide collaborator setting will change the artist contents // of the library, consider it a library update. @@ -213,9 +202,8 @@ class HomeViewModel(application: Application) : /** * Create a list of [MusicMode]s representing a simpler version of the [Tab] configuration. - * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, - * ordered in the same way as the configuration. + * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in + * the same way as the configuration. */ - private fun makeTabModes() = - settings.libTabs.filterIsInstance().map { it.mode } + private fun makeTabModes() = settings.libTabs.filterIsInstance().map { it.mode } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt index ab59bee7d..4add92475 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollRecyclerView.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.home.fastscroll import android.content.Context import android.graphics.Canvas -import android.graphics.PointF import android.graphics.Rect import android.util.AttributeSet import android.view.Gravity @@ -72,22 +71,18 @@ class FastScrollRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : AuxioRecyclerView(context, attrs, defStyleAttr) { - /** - * An interface to provide text to use in the popup when fast-scrolling. - */ + /** An interface to provide text to use in the popup when fast-scrolling. */ interface PopupProvider { /** * Get text to use in the popup at the specified position. * @param pos The position in the list. - * @return A [String] to use in the popup. Null if there is no applicable text for - * the popup at [pos]. + * @return A [String] to use in the popup. Null if there is no applicable text for the popup + * at [pos]. */ fun getPopup(pos: Int): String? } - /** - * A listener for fast scroller interactions. - */ + /** A listener for fast scroller interactions. */ interface Listener { /** * Called when the fast scrolling state changes. diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index 2b69905df..f37309f84 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -46,7 +46,10 @@ import org.oxycblt.auxio.util.collectImmediately * A [ListFragment] that shows a list of [Album]s. * @author Alexander Capehart (OxygenCobalt) */ -class AlbumListFragment : ListFragment(), FastScrollRecyclerView.Listener, FastScrollRecyclerView.PopupProvider { +class AlbumListFragment : + ListFragment(), + FastScrollRecyclerView.Listener, + FastScrollRecyclerView.PopupProvider { private val homeModel: HomeViewModel by activityViewModels() private val albumAdapter = AlbumAdapter(this) // Save memory by re-using the same formatter and string builder when creating popup text diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 09db6c8eb..29fecfd17 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -44,7 +44,10 @@ import org.oxycblt.auxio.util.nonZeroOrNull * A [ListFragment] that shows a list of [Artist]s. * @author Alexander Capehart (OxygenCobalt) */ -class ArtistListFragment : ListFragment(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { +class ArtistListFragment : + ListFragment(), + FastScrollRecyclerView.PopupProvider, + FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = ArtistAdapter(this) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 9cbb5dd52..1019a85eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -43,7 +43,10 @@ import org.oxycblt.auxio.util.collectImmediately * A [ListFragment] that shows a list of [Genre]s. * @author Alexander Capehart (OxygenCobalt) */ -class GenreListFragment : ListFragment(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener{ +class GenreListFragment : + ListFragment(), + FastScrollRecyclerView.PopupProvider, + FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = GenreAdapter(this) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index b9f50a428..6fc1a257c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -47,7 +47,10 @@ import org.oxycblt.auxio.util.collectImmediately * A [ListFragment] that shows a list of [Song]s. * @author Alexander Capehart (OxygenCobalt) */ -class SongListFragment : ListFragment(), FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.Listener { +class SongListFragment : + ListFragment(), + FastScrollRecyclerView.PopupProvider, + FastScrollRecyclerView.Listener { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = SongAdapter(this) // Save memory by re-using the same formatter and string builder when creating popup text diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt index 276a52a40..d1a97ba58 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/AdaptiveTabStrategy.kt @@ -21,7 +21,6 @@ import android.content.Context import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import org.oxycblt.auxio.R -import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt index f22f68f8e..6675e8967 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/Tab.kt @@ -33,8 +33,7 @@ sealed class Tab(open val mode: MusicMode) { data class Visible(override val mode: MusicMode) : Tab(mode) /** - * A visible tab. This will be visible in the tab configuration view, but not in the - * home view. + * A visible tab. This will be visible in the tab configuration view, but not in the home view. * @param mode The type of list in the home view this instance corresponds to. */ data class Invisible(override val mode: MusicMode) : Tab(mode) @@ -58,9 +57,8 @@ sealed class Tab(open val mode: MusicMode) { private const val SEQUENCE_LEN = 4 /** - * The default tab sequence, in integer form. - * This represents a set of four visible tabs ordered as "Song", "Album", "Artist", and - * "Genre". + * The default tab sequence, in integer form. This represents a set of four visible tabs + * ordered as "Song", "Album", "Artist", and "Genre". */ const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index bef65495d..10a459d5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -78,8 +78,8 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter Invisible and vice versa). + * Called when a tab is clicked, requesting that the visibility should be inverted (i.e + * Visible -> Invisible and vice versa). * @param tabMode The [MusicMode] of the tab clicked. */ fun onToggleVisibility(tabMode: MusicMode) diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 3379c1120..0bb712767 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -27,7 +27,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt index fd70d4be1..0eeb29b1e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabDragCallback.kt @@ -30,7 +30,7 @@ class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callbac recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ) = // Allow dragging up and down only - makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) + makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) override fun onChildDraw( c: Canvas, diff --git a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt index 76aaaf39f..4159c1158 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BitmapProvider.kt @@ -31,25 +31,26 @@ import org.oxycblt.auxio.music.Song * A utility to provide bitmaps in a race-less manner. * * When it comes to components that load images manually as [Bitmap] instances, queued - * [ImageRequest]s may cause a race condition that results in the incorrect image being - * drawn. This utility resolves this by keeping track of the current request, and disposing - * it as soon as a new request is queued or if another, competing request is newer. + * [ImageRequest]s may cause a race condition that results in the incorrect image being drawn. This + * utility resolves this by keeping track of the current request, and disposing it as soon as a new + * request is queued or if another, competing request is newer. * * @param context [Context] required to load images. * @author Alexander Capehart (OxygenCobalt) */ class BitmapProvider(private val context: Context) { - /** An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to. */ + /** + * An extension of [Disposable] with an additional [Target] to deliver the final [Bitmap] to. + */ private data class Request(val disposable: Disposable, val callback: Target) /** The target that will receive the requested [Bitmap]. */ interface Target { /** * Configure the [ImageRequest.Builder] to enable [Target]-specific configuration. - * @param builder The [ImageRequest.Builder] that will be used to request the - * desired [Bitmap]. - * @return The same [ImageRequest.Builder] in order to easily chain configuration - * methods. + * @param builder The [ImageRequest.Builder] that will be used to request the desired + * [Bitmap]. + * @return The same [ImageRequest.Builder] in order to easily chain configuration methods. */ fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder @@ -80,39 +81,38 @@ class BitmapProvider(private val context: Context) { currentRequest = null val imageRequest = - target.onConfigRequest( - ImageRequest.Builder(context) - .data(song) - // Use ORIGINAL sizing, as we are not loading into any View-like component. - .size(Size.ORIGINAL) - .transformations(SquareFrameTransform.INSTANCE)) - // Override the target in order to deliver the bitmap to the given - // callback. - .target( - onSuccess = { - synchronized(this) { - if (currentHandle == handle) { - // Has not been superceded by a new request, can deliver - // this result. - target.onCompleted(it.toBitmap()) - } + target + .onConfigRequest( + ImageRequest.Builder(context) + .data(song) + // Use ORIGINAL sizing, as we are not loading into any View-like component. + .size(Size.ORIGINAL) + .transformations(SquareFrameTransform.INSTANCE)) + // Override the target in order to deliver the bitmap to the given + // callback. + .target( + onSuccess = { + synchronized(this) { + if (currentHandle == handle) { + // Has not been superceded by a new request, can deliver + // this result. + target.onCompleted(it.toBitmap()) } - }, - onError = { - synchronized(this) { - if (currentHandle == handle) { - // Has not been superceded by a new request, can deliver - // this result. - target.onCompleted(null) - } + } + }, + onError = { + synchronized(this) { + if (currentHandle == handle) { + // Has not been superceded by a new request, can deliver + // this result. + target.onCompleted(null) } - }) + } + }) currentRequest = Request(context.imageLoader.enqueue(imageRequest.build()), target) } - /** - * Release this instance, cancelling any currently running operations. - */ + /** Release this instance, cancelling any currently running operations. */ @Synchronized fun release() { ++currentHandle diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 388ec7c0f..5b9e814a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -39,8 +39,8 @@ import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.getInteger /** - * A super-charged [StyledImageView]. This class enables the following features in addition - * to [StyledImageView]: + * A super-charged [StyledImageView]. This class enables the following features in addition to + * [StyledImageView]: * - A selection indicator * - An activation (playback) indicator * - Support for ONE custom view @@ -174,9 +174,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Whether this view should be indicated to have ongoing playback or not. See - * PlaybackIndicatorView for more information on what occurs here. - * Note: It's expected for this view to already be marked as playing with setSelected - * (not the same thing) before this is set to true. + * PlaybackIndicatorView for more information on what occurs here. Note: It's expected for this + * view to already be marked as playing with setSelected (not the same thing) before this is set + * to true. */ var isPlaying: Boolean get() = playbackIndicatorView.isPlaying @@ -214,13 +214,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (isActivated) { // View is "activated" (i.e marked as selected), so show the selection indicator. targetAlpha = 1f - targetDuration = - context.getInteger(R.integer.anim_fade_enter_duration).toLong() + targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() } else { // View is not "activated", hide the selection indicator. targetAlpha = 0f - targetDuration = - context.getInteger(R.integer.anim_fade_exit_duration).toLong() + targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong() } if (selectionIndicatorView.alpha == targetAlpha) { diff --git a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt index 7760961c3..f5df8f9bb 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt @@ -33,8 +33,8 @@ import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDrawableCompat /** - * A view that displays an activation (i.e playback) indicator, with an accented styling and - * an animated equalizer icon. + * A view that displays an activation (i.e playback) indicator, with an accented styling and an + * animated equalizer icon. * * This is only meant for use with [ImageGroup]. Due to limitations with [AnimationDrawable] * instances within custom views, this cannot be merged with [ImageGroup]. @@ -55,8 +55,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val settings = Settings(context) /** - * The corner radius of this view. This allows the outer ImageGroup to apply it's - * corner radius to this view without any attribute hacks. + * The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius + * to this view without any attribute hacks. */ var cornerRadius = 0f set(value) { @@ -71,8 +71,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } /** - * Whether this view should be indicated to have ongoing playback or not. If true, - * the animated playing icon will be shown. If false, the static paused icon will be shown. + * Whether this view should be indicated to have ongoing playback or not. If true, the animated + * playing icon will be shown. If false, the static paused icon will be shown. */ var isPlaying: Boolean get() = drawable == playingIndicatorDrawable diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index a137e7f3e..590404a6a 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -48,8 +48,8 @@ import org.oxycblt.auxio.util.getDrawableCompat * * - Tonal background * - Rounded corners based on user preferences - * - Built-in support for binding image data or using a static icon with the same - * styling as placeholder drawables. + * - Built-in support for binding image data or using a static icon with the same styling as + * placeholder drawables. * * @author Alexander Capehart (OxygenCobalt) */ @@ -116,8 +116,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr * Internally bind a [Music]'s image to this view. * @param music The music to find. * @param errorRes The error drawable resource to use if the music cannot be loaded. - * @param descRes The content description string resource to use. The resource must have - * one field for the name of the [Music]. + * @param descRes The content description string resource to use. The resource must have one + * field for the name of the [Music]. */ private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) { // Dispose of any previous image request and load a new image. @@ -132,8 +132,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } /** - * A [Drawable] wrapper that re-styles the drawable to better align with the style - * of [StyledImageView]. + * A [Drawable] wrapper that re-styles the drawable to better align with the style of + * [StyledImageView]. * @param context [Context] required for initialization. * @param inner The [Drawable] to wrap. */ diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 50c202046..8c9acd412 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -52,8 +52,8 @@ class MusicKeyer : Keyer { } /** - * Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. - * Use [SongFactory] or [AlbumFactory] for instantiation. + * Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or + * [AlbumFactory] for instantiation. * @author Alexander Capehart (OxygenCobalt) */ class AlbumCoverFetcher @@ -66,7 +66,7 @@ private constructor(private val context: Context, private val album: Album) : Fe dataSource = DataSource.DISK) } - /** A [Fetcher.Factory] implementation that works with [Song]s.*/ + /** A [Fetcher.Factory] implementation that works with [Song]s. */ class SongFactory : Fetcher.Factory { override fun create(data: Song, options: Options, imageLoader: ImageLoader) = AlbumCoverFetcher(options.context, data.album) @@ -129,8 +129,8 @@ private constructor( * Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be * transformed into [R]. * @param n The maximum amount of items to map. - * @param transform The function that transforms data [T] from the original list into - * data [R] in the new list. Can return null if the [T] cannot be transformed into an [R]. + * @param transform The function that transforms data [T] from the original list into data [R] in + * the new list. Can return null if the [T] cannot be transformed into an [R]. * @return A new list of at most N non-null [R] items. */ private inline fun Collection.mapAtMostNotNull( diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt index 9167ffda4..a3b30a4a7 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.image.extractor import android.content.Context @@ -7,6 +24,8 @@ import com.google.android.exoplayer2.MediaMetadata import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.metadata.flac.PictureFrame import com.google.android.exoplayer2.metadata.id3.ApicFrame +import java.io.ByteArrayInputStream +import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.image.CoverMode @@ -14,8 +33,6 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -import java.io.ByteArrayInputStream -import java.io.InputStream /** * Internal utilities for loading album covers. @@ -26,8 +43,8 @@ object Covers { * Fetch an album cover, respecting the current cover configuration. * @param context [Context] required to load the image. * @param album [Album] to load the cover from. - * @return An [InputStream] of image data if the cover loading was successful, null if the - * cover loading failed or should not occur. + * @return An [InputStream] of image data if the cover loading was successful, null if the cover + * loading failed or should not occur. */ suspend fun fetch(context: Context, album: Album): InputStream? { val settings = Settings(context) @@ -45,8 +62,8 @@ object Covers { } /** - * Load an [Album] cover directly from one of it's Song files. This attempts - * the following in order: + * Load an [Album] cover directly from one of it's Song files. This attempts the following in + * order: * - [MediaMetadataRetriever], as it has the best support and speed. * - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken * [MediaMetadataRetriever] implementations. diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt index 543ebd55c..30a7125d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Images.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.image.extractor import android.content.Context @@ -14,9 +31,9 @@ import coil.fetch.SourceResult import coil.size.Dimension import coil.size.Size import coil.size.pxOrElse +import java.io.InputStream import okio.buffer import okio.source -import java.io.InputStream /** * Utilities for constructing Artist and Genre images. @@ -24,8 +41,8 @@ import java.io.InputStream */ object Images { /** - * Create a mosaic image from the given image [InputStream]s. - * Derived from phonograph: https://github.com/kabouzeid/Phonograph + * Create a mosaic image from the given image [InputStream]s. Derived from phonograph: + * https://github.com/kabouzeid/Phonograph * @param context [Context] required to generate the mosaic. * @param streams [InputStream]s of image data to create the mosaic out of. * @param size [Size] of the Mosaic to generate. @@ -94,4 +111,4 @@ object Images { val size = pxOrElse { 512 } return if (size.mod(2) > 0) size + 1 else size } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/list/Data.kt b/app/src/main/java/org/oxycblt/auxio/list/Data.kt index c5710bcbd..878a6a9d3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Data.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Data.kt @@ -1,10 +1,25 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.list import androidx.annotation.StringRes -/** - * A marker for something that is a RecyclerView item. Has no functionality on it's own. - */ +/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */ interface Item /** diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 41c1450a3..79c2db68f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.shared.MainNavigationAction -import org.oxycblt.auxio.shared.NavigationViewModel +import org.oxycblt.auxio.ui.MainNavigationAction +import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast @@ -47,8 +47,8 @@ abstract class ListFragment : SelectionFragment(), Selecta } /** - * Called when [onClick] is called, but does not result in the item being selected. This - * more or less corresponds to an [onClick] implementation in a non-[ListFragment]. + * Called when [onClick] is called, but does not result in the item being selected. This more or + * less corresponds to an [onClick] implementation in a non-[ListFragment]. * @param music The [Music] item that was clicked. */ abstract fun onRealClick(music: Music) @@ -70,8 +70,8 @@ abstract class ListFragment : SelectionFragment(), Selecta } /** - * Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and - * closed when the view is destroyed. If a menu is already opened, this call is ignored. + * Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and closed + * when the view is destroyed. If a menu is already opened, this call is ignored. * @param anchor The [View] to anchor the menu to. * @param menuRes The resource of the menu to load. * @param song The [Song] to create the menu for. @@ -223,8 +223,8 @@ abstract class ListFragment : SelectionFragment(), Selecta } /** - * Open a menu. This menu will be managed by the Fragment and closed when the view is - * destroyed. If a menu is already opened, this call is ignored. + * Open a menu. This menu will be managed by the Fragment and closed when the view is destroyed. + * If a menu is already opened, this call is ignored. * @param anchor The [View] to anchor the menu to. * @param menuRes The resource of the menu to load. * @param block A block that is ran within [PopupMenu] that allows further configuration. diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index 29437c259..2945d3c33 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -1,14 +1,29 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.list -import android.view.MotionEvent import android.view.View import android.widget.Button -import android.widget.ImageView import androidx.recyclerview.widget.RecyclerView /** - * A basic listener for list interactions. - * TODO: Supply a ViewHolder on clicks (allows editable lists to be standardized into a listener.) + * A basic listener for list interactions. TODO: Supply a ViewHolder on clicks (allows editable + * lists to be standardized into a listener.) * @author Alexander Capehart (OxygenCobalt) */ interface ClickableListListener { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt index e265ce0b7..b29e8cb7f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/AuxioRecyclerView.kt @@ -18,7 +18,6 @@ package org.oxycblt.auxio.list.recycler import android.content.Context -import android.graphics.Rect import android.util.AttributeSet import android.view.WindowInsets import androidx.annotation.AttrRes @@ -56,8 +55,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { // Update the RecyclerView's padding such that the bottom insets are applied // while still preserving bottom padding. - updatePadding( - bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom) + updatePadding(bottom = initialPaddingBottom + insets.systemBarInsetsCompat.bottom) return insets } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt index 644f29b2f..6984f1c93 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/DialogRecyclerView.kt @@ -100,17 +100,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr lmm.findLastCompletelyVisibleItemPosition() == (lmm.itemCount - 1) } - /** - * A [RecyclerView.ViewHolder] that implements dialog-specific fixes. - */ + /** A [RecyclerView.ViewHolder] that implements dialog-specific fixes. */ abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { init { // ViewHolders are not automatically full-width in dialogs, manually resize // them to be as such. - root.layoutParams = - LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + root.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) } } } - diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt index a23ff1e14..17b016575 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt @@ -21,7 +21,6 @@ import android.view.View import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW /** * A [RecyclerView.Adapter] that supports indicating the playback status of a particular item. @@ -36,8 +35,7 @@ abstract class PlayingIndicatorAdapter : RecyclerV private var isPlaying = false /** - * The current list of the adapter. This is used to update items if the indicator - * state changes. + * The current list of the adapter. This is used to update items if the indicator state changes. */ abstract val currentList: List @@ -106,15 +104,13 @@ abstract class PlayingIndicatorAdapter : RecyclerV } } - /** - * A [RecyclerView.ViewHolder] that can display a playing indicator. - */ + /** A [RecyclerView.ViewHolder] that can display a playing indicator. */ abstract class ViewHolder(root: View) : RecyclerView.ViewHolder(root) { /** * Update the playing indicator within this [RecyclerView.ViewHolder]. * @param isActive True if this item is playing, false otherwise. - * @param isPlaying True if playback is ongoing, false if paused. If this - * is true, [isActive] will also be true. + * @param isPlaying True if playback is ongoing, false if paused. If this is true, + * [isActive] will also be true. */ abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt index a9c565c1f..29ecc2582 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt @@ -68,9 +68,7 @@ abstract class SelectionIndicatorAdapter : } } - /** - * A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator. - */ + /** A [PlayingIndicatorAdapter.ViewHolder] that can display a selection indicator. */ abstract class ViewHolder(root: View) : PlayingIndicatorAdapter.ViewHolder(root) { /** * Update the selection indicator within this [PlayingIndicatorAdapter.ViewHolder]. diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt index a5a127173..9a289fc88 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SimpleItemCallback.kt @@ -21,9 +21,8 @@ import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.list.Item /** - * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. - * Use this whenever creating [DiffUtil.ItemCallback] implementations with an [Item] - * subclass. + * A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this + * whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass. * @author Alexander Capehart (OxygenCobalt) */ abstract class SimpleItemCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt index ef9842501..4b47c22c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SyncListDiffer.kt @@ -23,8 +23,8 @@ import androidx.recyclerview.widget.RecyclerView /** * A list differ that operates synchronously. This can help resolve some shortcomings with - * AsyncListDiffer, at the cost of performance. - * Derived from Material Files: https://github.com/zhanghai/MaterialFiles + * AsyncListDiffer, at the cost of performance. Derived from Material Files: + * https://github.com/zhanghai/MaterialFiles * @author Hai Zhang, Alexander Capehart (OxygenCobalt) */ class SyncListDiffer( @@ -111,8 +111,8 @@ class SyncListDiffer( } /** - * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only - * use it if the changes are trivial. + * Submit a list like AsyncListDiffer. This is exceedingly slow for large diffs, so only use it + * if the changes are trivial. * @param newList The list to update to. */ fun submitList(newList: List) { @@ -125,8 +125,8 @@ class SyncListDiffer( } /** - * Replace this list with a new list. This is good for large diffs that are too slow to - * update synchronously, but too chaotic to update asynchronously. + * Replace this list with a new list. This is good for large diffs that are too slow to update + * synchronously, but too chaotic to update asynchronously. * @param newList The list to update to. */ fun replaceList(newList: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index e616512b7..9f3d07805 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -24,8 +24,8 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemHeaderBinding import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding -import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.list.Header +import org.oxycblt.auxio.list.SelectableListListener import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt index f95598643..a5d762c42 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionFragment.kt @@ -18,16 +18,14 @@ package org.oxycblt.auxio.list.selection import android.os.Bundle -import android.view.LayoutInflater import android.view.MenuItem import androidx.appcompat.widget.Toolbar import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.androidActivityViewModels -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast /** @@ -40,10 +38,10 @@ abstract class SelectionFragment : protected val playbackModel: PlaybackViewModel by androidActivityViewModels() /** - * Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed - * by [SelectionFragment]. - * @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or - * null if there is not one. + * Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by + * [SelectionFragment]. + * @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if + * there is not one. */ open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt index 88b72f8f3..106244edd 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt @@ -115,13 +115,11 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr if (selectionVisible) { targetInnerAlpha = 0f targetSelectionAlpha = 1f - targetDuration = - context.getInteger(R.integer.anim_fade_enter_duration).toLong() + targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong() } else { targetInnerAlpha = 1f targetSelectionAlpha = 0f - targetDuration = - context.getInteger(R.integer.anim_fade_exit_duration).toLong() + targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong() } if (innerToolbar.alpha == targetInnerAlpha && @@ -154,8 +152,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Update the alpha of the inner and selection [MaterialToolbar]s. - * @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the - * inverse opacity of the selection [MaterialToolbar]. + * @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse + * opacity of the selection [MaterialToolbar]. */ private fun setToolbarsAlpha(innerAlpha: Float) { innerToolbar.apply { diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 13d09f5dd..ae6b0bb60 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.util.logD /** * A [ViewModel] that manages the current selection. @@ -31,10 +30,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback { private val musicStore = MusicStore.getInstance() private val _selected = MutableStateFlow(listOf()) - /** - * the currently selected items. These are ordered in earliest selected - * and latest selected. - */ + /** the currently selected items. These are ordered in earliest selected and latest selected. */ val selected: StateFlow> get() = _selected @@ -49,14 +45,15 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback { // Sanitize the selection to remove items that no longer exist and thus // won't appear in any list. - _selected.value = _selected.value.mapNotNull { - when (it) { - is Song -> library.sanitize(it) - is Album -> library.sanitize(it) - is Artist -> library.sanitize(it) - is Genre -> library.sanitize(it) + _selected.value = + _selected.value.mapNotNull { + when (it) { + is Song -> library.sanitize(it) + is Album -> library.sanitize(it) + is Artist -> library.sanitize(it) + is Genre -> library.sanitize(it) + } } - } } override fun onCleared() { @@ -65,8 +62,8 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback { } /** - * Select a new [Music] item. If this item is already within the selected items, the item will be - * removed. Otherwise, it will be added. + * Select a new [Music] item. If this item is already within the selected items, the item will + * be removed. Otherwise, it will be added. * @param music The [Music] item to select. */ fun select(music: Music) { @@ -81,6 +78,5 @@ class SelectionViewModel : ViewModel(), MusicStore.Callback { * Consume the current selection. This will clear any items that were selected prior. * @return The list of selected items before it was cleared. */ - fun consume() = - _selected.value.also { _selected.value = listOf() } + fun consume() = _selected.value.also { _selected.value = listOf() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 0151a4e66..fc6079e8f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -14,8 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - -// We use a special naming convention for internal fields, disable the lints that check for that. + @file:Suppress("PropertyName", "FunctionName") package org.oxycblt.auxio.music @@ -25,6 +24,8 @@ import android.os.Parcelable import java.security.MessageDigest import java.text.CollationKey import java.text.Collator +import java.text.ParseException +import java.text.SimpleDateFormat import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel @@ -39,14 +40,12 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.unlikelyToBeNull -import java.text.ParseException -import java.text.SimpleDateFormat // --- MUSIC MODELS --- /** - * Abstract music data. This contains universal information about all concrete music implementations, - * such as identification information and names. + * Abstract music data. This contains universal information about all concrete music + * implementations, such as identification information and names. * @author Alexander Capehart (OxygenCobalt) */ sealed class Music : Item { @@ -66,34 +65,33 @@ sealed class Music : Item { * Returns a name suitable for use in the app UI. This should be favored over [rawName] in * nearly all cases. * @param context [Context] required to obtain placeholder text or formatting information. - * @return A human-readable string representing the name of this music. In the case that - * the item does not have a name, an analogous "Unknown X" name is returned. + * @return A human-readable string representing the name of this music. In the case that the + * item does not have a name, an analogous "Unknown X" name is returned. */ abstract fun resolveName(context: Context): String /** - * The raw sort name of this item as it was extracted from the file-system. This can be used - * not only when sorting music, but also trying to locate music based on a fuzzy search by - * the user. Will be null if the item has no known sort name. + * The raw sort name of this item as it was extracted from the file-system. This can be used not + * only when sorting music, but also trying to locate music based on a fuzzy search by the user. + * Will be null if the item has no known sort name. */ abstract val rawSortName: String? /** - * A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items - * in a semantically-correct manner. Will be null if the item has no name. + * A [CollationKey] derived from [rawName] and [rawSortName] that can be used to sort items in a + * semantically-correct manner. Will be null if the item has no name. * * The key will have the following attributes: - * - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] - * is used. + * - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used. * - If the string begins with an article, such as "the", it will be stripped, as is usually * convention for sorting media. This is not internationalized. */ abstract val collationKey: CollationKey? /** - * Finalize this item once the music library has been fully constructed. This is where - * any final ordering or sanity checking should occur. - * **This function is internal to the music package. Do not use it elsewhere.** + * Finalize this item once the music library has been fully constructed. This is where any final + * ordering or sanity checking should occur. **This function is internal to the music package. + * Do not use it elsewhere.** */ abstract fun _finalize() @@ -128,20 +126,20 @@ sealed class Music : Item { * A unique identifier for a piece of music. * * [UID] enables a much cheaper and more reliable form of differentiating music, derived from - * either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables - * several improvements to music management in this app, including: + * either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several + * improvements to music management in this app, including: * - * - Proper differentiation of identical music. It's common for large, well-tagged libraries - * to have functionally duplicate items that are differentiated with MusicBrainz IDs, and so - * [UID] allows us to properly differentiate between these in the app. + * - Proper differentiation of identical music. It's common for large, well-tagged libraries to + * have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID] + * allows us to properly differentiate between these in the app. * - Better music persistence between restarts. Whereas directly storing song names would be * prone to collisions, and storing MediaStore IDs would drift rapidly as the music library * changes, [UID] enables a much stronger form of persistence given it's unique link to a - * specific files metadata configuration, which is unlikely to collide with another item - * or drift as the music library changes. + * specific files metadata configuration, which is unlikely to collide with another item or + * drift as the music library changes. * - * Note: Generally try to use [UID] as a black box that can only be read, written, and - * compared. It will not be fun if you try to manipulate it in any other manner. + * Note: Generally try to use [UID] as a black box that can only be read, written, and compared. + * It will not be fun if you try to manipulate it in any other manner. * * @author Alexander Capehart (OxygenCobalt) */ @@ -180,11 +178,11 @@ sealed class Music : Item { companion object { /** - * Creates an auxio-style [UID] with a [UUID] composed of a hash of the - * non-subjective, unlikely-to-change metadata of the music. + * Creates an auxio-style [UID] with a [UUID] composed of a hash of the non-subjective, + * unlikely-to-change metadata of the music. * @param mode The analogous [MusicMode] of the item that created this [UID]. - * @param updates Block to update the [MessageDigest] hash with the metadata of - * the item. Make sure the metadata hashed semantically aligns with the format + * @param updates Block to update the [MessageDigest] hash with the metadata of the + * item. Make sure the metadata hashed semantically aligns with the format * specification. * @return A new auxio-style [UID]. */ @@ -192,10 +190,11 @@ sealed class Music : Item { // Auxio hashes consist of the MD5 hash of the non-subjective, consistent // tags in a music item. For easier use with MusicBrainz IDs, we transform // this into a UUID too. - val uuid = MessageDigest.getInstance("MD5").run { - updates() - digest().toUuid() - } + val uuid = + MessageDigest.getInstance("MD5").run { + updates() + digest().toUuid() + } return UID(Format.AUXIO, mode, uuid) } @@ -235,7 +234,8 @@ sealed class Music : Item { return null } - val mode = MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null + val mode = + MusicMode.fromIntCode(ids[0].toIntOrNull(16) ?: return null) ?: return null val uuid = ids[1].toUuidOrNull() ?: return null return UID(format, mode, uuid) @@ -254,9 +254,7 @@ sealed class Music : Item { * @author Alexander Capehart (OxygenCobalt) */ sealed class MusicParent : Music() { - /** - * The [Song]s in this this group. - */ + /** The [Song]s in this this group. */ abstract val songs: List // Note: Append song contents to MusicParent equality so that Groups with @@ -279,8 +277,8 @@ sealed class MusicParent : Music() { */ class Song constructor(raw: Raw, settings: Settings) : Music() { override val uid = - // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } ?: UID.auxio(MusicMode.SONGS) { // Song UIDs are based on the raw data without parsing so that they remain // consistent across music setting changes. Parents are not held up to the @@ -310,14 +308,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { val date = raw.date /** - * The URI to the audio file that this instance was created from. This can be used to - * access the audio file in a way that is scoped-storage-safe. + * The URI to the audio file that this instance was created from. This can be used to access the + * audio file in a way that is scoped-storage-safe. */ val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri() /** - * The [Path] to this audio file. This is only intended for display, [uri] should be - * favored instead for accessing the audio file. + * The [Path] to this audio file. This is only intended for display, [uri] should be favored + * instead for accessing the audio file. */ val path = Path( @@ -341,8 +339,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { private var _album: Album? = null /** - * The parent [Album]. If the metadata did not specify an album, it's parent directory is - * used instead. + * The parent [Album]. If the metadata did not specify an album, it's parent directory is used + * instead. */ val album: Album get() = unlikelyToBeNull(_album) @@ -371,23 +369,23 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { private val _artists = mutableListOf() /** - * The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more - * than one [Artist] name was specified in the metadata. Unliked [Album], artists are - * prioritized for this field. + * The parent [Artist]s of this [Song]. Is often one, but there can be multiple if more than one + * [Artist] name was specified in the metadata. Unliked [Album], artists are prioritized for + * this field. */ val artists: List get() = _artists /** * Resolves one or more [Artist]s into a single piece of human-readable names. - * @param context [Context] required for [resolveName]. - * TODO Internationalize the list formatter. + * @param context [Context] required for [resolveName]. TODO Internationalize the list + * formatter. */ fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) } /** - * Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This - * will only compare surface-level names, and not [Music.UID]s. + * Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only + * compare surface-level names, and not [Music.UID]s. * @param other The [Song] to compare to. * @return True if the [Artist] displays are equal, false otherwise */ @@ -405,8 +403,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { private val _genres = mutableListOf() /** - * The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more - * than one [Genre] name was specified in the metadata. + * The parent [Genre]s of this [Song]. Is often one, but there can be multiple if more than one + * [Genre] name was specified in the metadata. */ val genres: List get() = _genres @@ -420,9 +418,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { // --- INTERNAL FIELDS --- /** - * The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into - * an [Album]. - * **This is only meant for use within the music package.** + * The [Album.Raw] instances collated by the [Song]. This can be used to group [Song]s into an + * [Album]. **This is only meant for use within the music package.** */ val _rawAlbum = Album.Raw( @@ -435,19 +432,17 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) /** - * The [Artist.Raw] instances collated by the [Song]. The artists of the song take - * priority, followed by the album artists. If there are no artists, this field will - * be a single "unknown" [Artist.Raw]. This can be used to group up [Song]s into - * an [Artist]. - * **This is only meant for use within the music package.** + * The [Artist.Raw] instances collated by the [Song]. The artists of the song take priority, + * followed by the album artists. If there are no artists, this field will be a single "unknown" + * [Artist.Raw]. This can be used to group up [Song]s into an [Artist]. **This is only meant for + * use within the music package.** */ val _rawArtists = rawArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(Artist.Raw()) } /** - * The [Genre.Raw] instances collated by the [Song]. This can be used to group up - * [Song]s into a [Genre]. ID3v2 Genre names are automatically converted to their - * resolved names. - * **This is only meant for use within the music package.** + * The [Genre.Raw] instances collated by the [Song]. This can be used to group up [Song]s into a + * [Genre]. ID3v2 Genre names are automatically converted to their resolved names. **This is + * only meant for use within the music package.** */ val _rawGenres = raw.genreNames @@ -457,8 +452,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { /** * Links this [Song] with a parent [Album]. - * @param album The parent [Album] to link to. - * **This is only meant for use within the music package.** + * @param album The parent [Album] to link to. **This is only meant for use within the music + * package.** */ fun _link(album: Album) { _album = album @@ -466,8 +461,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { /** * Links this [Song] with a parent [Artist]. - * @param artist The parent [Artist] to link to. - * **This is only meant for use within the music package.** + * @param artist The parent [Artist] to link to. **This is only meant for use within the music + * package.** */ fun _link(artist: Artist) { _artists.add(artist) @@ -475,8 +470,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { /** * Links this [Song] with a parent [Genre]. - * @param genre The parent [Genre] to link to. - * **This is only meant for use within the music package.** + * @param genre The parent [Genre] to link to. **This is only meant for use within the music + * package.** */ fun _link(genre: Genre) { _genres.add(genre) @@ -508,14 +503,14 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { } /** - * Raw information about a [Song] obtained from the filesystem/Extractor instances. - * **This is only meant for use within the music package.** + * Raw information about a [Song] obtained from the filesystem/Extractor instances. **This is + * only meant for use within the music package.** */ class Raw constructor( /** - * The ID of the [Song]'s audio file, obtained from MediaStore. Note that this - * ID is highly unstable and should only be used for accessing the audio file. + * The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly + * unstable and should only be used for accessing the audio file. */ var mediaStoreId: Long? = null, /** @see Song.dateAdded */ @@ -583,8 +578,8 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { */ class Album constructor(raw: Raw, override val songs: List) : MusicParent() { override val uid = - // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) } + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ALBUMS, it) } ?: UID.auxio(MusicMode.ALBUMS) { // Hash based on only names despite the presence of a date to increase stability. // I don't know if there is any situation where an artist will have two albums with @@ -598,23 +593,20 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( override fun resolveName(context: Context) = rawName /** - * The earliest [Date] this album was released. - * Will be null if no valid date was present in the metadata of any [Song]. - * TODO: Date ranges? + * The earliest [Date] this album was released. Will be null if no valid date was present in the + * metadata of any [Song]. TODO: Date ranges? */ val date: Date? /** - * The [Type] of this album, signifying the type of release it actually is. - * Defaults to [Type.Album]. + * The [Type] of this album, signifying the type of release it actually is. Defaults to + * [Type.Album]. */ - val type = raw.type ?: Type.Album(null) /** - * The URI to a MediaStore-provided album cover. These images will be fast to load, but - * at the cost of image quality. + * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the + * cost of image quality. */ - val coverUri = raw.mediaStoreId.toCoverUri() /** The duration of all songs in the album, in milliseconds. */ @@ -655,9 +647,9 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( private val _artists = mutableListOf() /** - * The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more - * than one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], - * album artists are prioritized for this field. + * The parent [Artist]s of this [Album]. Is often one, but there can be multiple if more than + * one [Artist] name was specified in the metadata of the [Song]'s. Unlike [Song], album artists + * are prioritized for this field. */ val artists: List get() = _artists @@ -669,8 +661,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) } /** - * Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This - * will only compare surface-level names, and not [Music.UID]s. + * Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This will + * only compare surface-level names, and not [Music.UID]s. * @param other The [Album] to compare to. * @return True if the [Artist] displays are equal, false otherwise */ @@ -690,17 +682,16 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( /** * The [Artist.Raw] instances collated by the [Album]. The album artists of the song take - * priority, followed by the artists. If there are no artists, this field will - * be a single "unknown" [Artist.Raw]. This can be used to group up [Album]s into - * an [Artist]. - * **This is only meant for use within the music package.** + * priority, followed by the artists. If there are no artists, this field will be a single + * "unknown" [Artist.Raw]. This can be used to group up [Album]s into an [Artist]. **This is + * only meant for use within the music package.** */ val _rawArtists = raw.rawArtists /** * Links this [Album] with a parent [Artist]. - * @param artist The parent [Artist] to link to. - * **This is only meant for use within the music package.** + * @param artist The parent [Artist] to link to. **This is only meant for use within the music + * package.** */ fun _link(artist: Artist) { _artists.add(artist) @@ -722,8 +713,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( /** * The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc. * - * This class is derived from the MusicBrainz Release Group Type specification. It can - * be found at: https://musicbrainz.org/doc/Release_Group/Type + * This class is derived from the MusicBrainz Release Group Type specification. It can be found + * at: https://musicbrainz.org/doc/Release_Group/Type * @author Alexander Capehart (OxygenCobalt) */ sealed class Type { @@ -732,10 +723,10 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( * considered "Plain". */ abstract val refinement: Refinement? - + /** The string resource corresponding to the name of this release type to show in the UI. */ abstract val stringRes: Int - + /** * A plain album. * @param refinement A specification of what kind of performance this release is. If null, @@ -751,7 +742,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( Refinement.REMIX -> R.string.lbl_album_remix } } - + /** * A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs. * @param refinement A specification of what kind of performance this release is. If null, @@ -767,7 +758,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( Refinement.REMIX -> R.string.lbl_ep_remix } } - + /** * A single. Usually a release consisting of 1-2 songs. * @param refinement A specification of what kind of performance this release is. If null, @@ -783,7 +774,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( Refinement.REMIX -> R.string.lbl_single_remix } } - + /** * A compilation. Usually consists of many songs from a variety of artists. * @param refinement A specification of what kind of performance this release is. If null, @@ -799,7 +790,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( Refinement.REMIX -> R.string.lbl_compilation_remix } } - + /** * A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually * visual) media. @@ -807,11 +798,11 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( object Soundtrack : Type() { override val refinement: Refinement? get() = null - + override val stringRes: Int get() = R.string.lbl_soundtrack } - + /** * A (DJ) Mix. These are usually one large track consisting of the artist playing several * sub-tracks with smooth transitions between them. @@ -819,45 +810,38 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( object Mix : Type() { override val refinement: Refinement? get() = null - + override val stringRes: Int get() = R.string.lbl_mix } - + /** - * A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] - * or a future release. + * A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or + * a future release. */ object Mixtape : Type() { override val refinement: Refinement? get() = null - + override val stringRes: Int get() = R.string.lbl_mixtape } - - /** - * A specification of what kind of performance a particular release is. - */ + + /** A specification of what kind of performance a particular release is. */ enum class Refinement { - /** - * A release consisting of a live performance - */ + /** A release consisting of a live performance */ LIVE, - - /** - * A release consisting of another [Artist]s remix of a prior performance. - */ + + /** A release consisting of another [Artist]s remix of a prior performance. */ REMIX } - + companion object { /** * Parse a [Type] from a string formatted with the MusicBrainz Release Group Type * specification. * @param types A list of values consisting of valid release type values. - * @return A [Type] consisting of the given types, or null if the types - * were not valid. + * @return A [Type] consisting of the given types, or null if the types were not valid. */ fun parse(types: List): Type? { val primary = types.getOrNull(0) ?: return null @@ -872,14 +856,14 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( else -> types.parseSecondaryTypes(0) { Album(it) } } } - + /** * Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted * with the MusicBrainz Release Group Type specification. * @param index The index of the release type to parse. - * @param convertRefinement Code to convert a [Refinement] into a [Type] - * corresponding to the callee's context. This is used in order to handle secondary - * times that are actually [Refinement]s. + * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding + * to the callee's context. This is used in order to handle secondary times that are + * actually [Refinement]s. * @return A [Type] corresponding to the secondary type found at that index. */ private inline fun List.parseSecondaryTypes( @@ -896,14 +880,14 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( parseSecondaryTypeImpl(secondary, convertRefinement) } } - + /** - * Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond - * to any child values. + * Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to + * any child values. * @param type The release type value to parse. - * @param convertRefinement Code to convert a [Refinement] into a [Type] - * corresponding to the callee's context. This is used in order to handle secondary - * times that are actually [Refinement]s. + * @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding + * to the callee's context. This is used in order to handle secondary times that are + * actually [Refinement]s. */ private inline fun parseSecondaryTypeImpl( type: String?, @@ -922,14 +906,13 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( } /** - * Raw information about an [Album] obtained from the component [Song] instances. - * **This is only meant for use within the music package.** + * Raw information about an [Album] obtained from the component [Song] instances. **This is only + * meant for use within the music package.** */ class Raw( /** - * The ID of the [Album]'s grouping, obtained from MediaStore. Note that this - * ID is highly unstable and should only be used for accessing the system-provided - * cover art. + * The ID of the [Album]'s grouping, obtained from MediaStore. Note that this ID is highly + * unstable and should only be used for accessing the system-provided cover art. */ val mediaStoreId: Long, /** @see Music.uid */ @@ -970,18 +953,18 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( } /** - * An abstract artist. These are actually a combination of the artist and album artist tags - * from within the library, derived from [Song]s and [Album]s respectively. + * An abstract artist. These are actually a combination of the artist and album artist tags from + * within the library, derived from [Song]s and [Album]s respectively. * @param raw The [Artist.Raw] to derive the member data from. - * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], - * either through artist or album artist tags. Providing [Song]s to the artist is optional. - * These instances will be linked to this [Artist]. + * @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist], either + * through artist or album artist tags. Providing [Song]s to the artist is optional. These instances + * will be linked to this [Artist]. * @author Alexander Capehart (OxygenCobalt) */ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicParent() { override val uid = - // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. - raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } + // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. + raw.musicBrainzId?.let { UID.musicBrainz(MusicMode.ARTISTS, it) } ?: UID.auxio(MusicMode.ARTISTS) { update(raw.name) } override val rawName = raw.name override val rawSortName = raw.sortName @@ -990,21 +973,21 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP override val songs: List /** - * All of the [Album]s this artist is credited to. Note that any [Song] credited to this - * artist will have it's [Album] considered to be "indirectly" linked to this [Artist], and - * thus included in this list. + * All of the [Album]s this artist is credited to. Note that any [Song] credited to this artist + * will have it's [Album] considered to be "indirectly" linked to this [Artist], and thus + * included in this list. */ val albums: List /** - * The duration of all [Song]s in the artist, in milliseconds. - * Will be null if there are no songs. + * The duration of all [Song]s in the artist, in milliseconds. Will be null if there are no + * songs. */ val durationMs: Long? /** - * Whether this artist is considered a "collaborator", i.e it is not directly credited on - * any [Album]. + * Whether this artist is considered a "collaborator", i.e it is not directly credited on any + * [Album]. */ val isCollaborator: Boolean @@ -1045,8 +1028,8 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } /** - * Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This - * will only compare surface-level names, and not [Music.UID]s. + * Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This will + * only compare surface-level names, and not [Music.UID]s. * @param other The [Artist] to compare to. * @return True if the [Genre] displays are equal, false otherwise */ @@ -1066,12 +1049,12 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP /** * Returns the original position of this [Artist]'s [Artist.Raw] within the given [Artist.Raw] - * list. This can be used to create a consistent ordering within child [Artist] lists - * based on the original tag order. + * list. This can be used to create a consistent ordering within child [Artist] lists based on + * the original tag order. * @param rawArtists The [Artist.Raw] instances to check. It is assumed that this [Artist]'s * [Artist.Raw] will be within the list. - * @return The index of the [Artist]'s [Artist.Raw] within the list. - * **This is only meant for use within the music package.** + * @return The index of the [Artist]'s [Artist.Raw] within the list. **This is only meant for + * use within the music package.** */ fun _getOriginalPositionIn(rawArtists: List) = rawArtists.indexOf(raw) @@ -1138,19 +1121,13 @@ class Genre constructor(private val raw: Raw, override val songs: List) : override val collationKey = makeCollationKeyImpl() override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre) - /** - * The albums indirectly linked to by the [Song]s of this [Genre]. - */ + /** The albums indirectly linked to by the [Song]s of this [Genre]. */ val albums: List - /** - * The artists indirectly linked to by the [Artist]s of this [Genre]. - */ + /** The artists indirectly linked to by the [Artist]s of this [Genre]. */ val artists: List - /** - * The total duration of the songs in this genre, in milliseconds. - */ + /** The total duration of the songs in this genre, in milliseconds. */ val durationMs: Long init { @@ -1177,12 +1154,12 @@ class Genre constructor(private val raw: Raw, override val songs: List) : /** * Returns the original position of this [Genre]'s [Genre.Raw] within the given [Genre.Raw] - * list. This can be used to create a consistent ordering within child [Genre] lists - * based on the original tag order. + * list. This can be used to create a consistent ordering within child [Genre] lists based on + * the original tag order. * @param rawGenres The [Genre.Raw] instances to check. It is assumed that this [Genre]'s * [Genre.Raw] will be within the list. - * @return The index of the [Genre]'s [Genre.Raw] within the list. - * **This is only meant for use within the music package.** + * @return The index of the [Genre]'s [Genre.Raw] within the list. **This is only meant for use + * within the music package.** */ fun _getOriginalPositionIn(rawGenres: List) = rawGenres.indexOf(raw) @@ -1191,13 +1168,11 @@ class Genre constructor(private val raw: Raw, override val songs: List) : } /** - * Raw information about a [Genre] obtained from the component [Song] instances. - * **This is only meant for use within the music package.** + * Raw information about a [Genre] obtained from the component [Song] instances. **This is only + * meant for use within the music package.** */ class Raw( - /** - * @see Music.rawName - */ + /** @see Music.rawName */ val name: String? = null ) { // Only group by the lowercase genre name. This allows Genre grouping to be @@ -1219,13 +1194,11 @@ class Genre constructor(private val raw: Raw, override val songs: List) : } } - /** * An ISO-8601/RFC 3339 Date. * - * This class only encodes the timestamp spec and it's conversion to a human-readable date, - * without any other time management or validation. In general, this should only be used for - * display. + * This class only encodes the timestamp spec and it's conversion to a human-readable date, without + * any other time management or validation. In general, this should only be used for display. * * @author Alexander Capehart (OxygenCobalt) */ @@ -1240,20 +1213,21 @@ class Date private constructor(private val tokens: List) : Comparable /** * Resolve this instance into a human-readable date. * @param context [Context] required to get human-readable names. - * @return If the [Date] has a valid month and year value, a more fine-grained date - * (ex. "Jan 2020") will be returned. Otherwise, a plain year value (ex. "2020") is - * returned. Dates will be properly localized. + * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan + * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will + * be properly localized. */ fun resolveDate(context: Context): String { if (month != null) { // Parse a date format from an ISO-ish format val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) format.applyPattern("yyyy-MM") - val date = try { - format.parse("$year-$month") - } catch (e: ParseException) { - null - } + val date = + try { + format.parse("$year-$month") + } catch (e: ParseException) { + null + } if (date != null) { // Reformat as a readable month and year @@ -1307,8 +1281,8 @@ class Date private constructor(private val tokens: List) : Comparable companion object { /** - * A [Regex] that can parse a variable-precision ISO-8601 timestamp. - * Derived from https://github.com/quodlibet/mutagen + * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from + * https://github.com/quodlibet/mutagen */ private val ISO8601_REGEX = Regex( @@ -1326,9 +1300,8 @@ class Date private constructor(private val tokens: List) : Comparable * @param year The year component. * @param month The month component. * @param day The day component. - * @return A new [Date] consisting of the given components. May have reduced precision - * if the components were partially invalid, and will be null if all components are - * invalid. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. */ fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day)) @@ -1338,9 +1311,8 @@ class Date private constructor(private val tokens: List) : Comparable * @param month The month component. * @param day The day component. * @param hour The hour component - * @return A new [Date] consisting of the given components. May have reduced precision - * if the components were partially invalid, and will be null if all components are - * invalid. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. */ fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) = fromTokens(listOf(year, month, day, hour, minute)) @@ -1348,14 +1320,14 @@ class Date private constructor(private val tokens: List) : Comparable /** * Create a [Date] from a [String] timestamp. * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision. - * @return A new [Date] consisting of the given components. May have reduced precision - * if the components were partially invalid, and will be null if all components are - * invalid or if the timestamp is invalid. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid or + * if the timestamp is invalid. */ fun from(timestamp: String): Date? { val tokens = - // Match the input with the timestamp regex - (ISO8601_REGEX.matchEntire(timestamp) ?: return null) + // Match the input with the timestamp regex + (ISO8601_REGEX.matchEntire(timestamp) ?: return null) .groupValues // Filter to the specific tokens we want and convert them to integer tokens. .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null } @@ -1365,9 +1337,8 @@ class Date private constructor(private val tokens: List) : Comparable /** * Create a [Date] from the given non-validated tokens. * @param tokens The tokens to use for each date component, in order of precision. - * @return A new [Date] consisting of the given components. May have reduced precision - * if the components were partially invalid, and will be null if all components are - * invalid. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. */ private fun fromTokens(tokens: List): Date? { val validated = mutableListOf() @@ -1380,8 +1351,8 @@ class Date private constructor(private val tokens: List) : Comparable } /** - * Validate a list of tokens provided by [src], and add the valid ones to [dst]. - * Will stop as soon as an invalid token is found. + * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop + * as soon as an invalid token is found. * @param src The input tokens to validate. * @param dst The destination list to add valid tokens to. */ @@ -1440,8 +1411,8 @@ private fun MessageDigest.update(n: Int?) { /** * Convert a [ByteArray] to a [UUID]. Assumes that the [ByteArray] has a length of 16. - * @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s - * in the [UUID] will be little-endian. + * @return A [UUID] derived from the [ByteArray]'s contents. Internally, the two [Long]s in the + * [UUID] will be little-endian. */ fun ByteArray.toUuid(): UUID { check(size == 16) 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 71857bd22..609c84674 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -20,15 +20,15 @@ package org.oxycblt.auxio.music import android.content.Context import android.net.Uri import android.provider.OpenableColumns -import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.contentResolverSafe +import org.oxycblt.auxio.music.storage.useQuery /** * A repository granting access to the music library.. * - * This can be used to obtain certain music items, or await changes to the music library. - * It is generally recommended to use this over Indexer to keep track of the library state, - * as the interface will be less volatile. + * This can be used to obtain certain music items, or await changes to the music library. It is + * generally recommended to use this over Indexer to keep track of the library state, as the + * interface will be less volatile. * * @author Alexander Capehart (OxygenCobalt) */ @@ -36,9 +36,9 @@ class MusicStore private constructor() { private val callbacks = mutableListOf() /** - * The current [Library]. May be null if a [Library] has not been successfully loaded yet. - * This can change, so it's highly recommended to not access this directly and instead - * rely on [Callback]. + * The current [Library]. May be null if a [Library] has not been successfully loaded yet. This + * can change, so it's highly recommended to not access this directly and instead rely on + * [Callback]. */ var library: Library? = null set(value) { @@ -49,9 +49,8 @@ class MusicStore private constructor() { } /** - * Add a [Callback] to this instance. This can be used to receive changes in the music - * library. Will invoke all [Callback] methods to initialize the instance with the - * current state. + * Add a [Callback] to this instance. This can be used to receive changes in the music library. + * Will invoke all [Callback] methods to initialize the instance with the current state. * @param callback The [Callback] to add. * @see Callback */ @@ -62,10 +61,9 @@ class MusicStore private constructor() { } /** - * Remove a [Callback] from this instance, preventing it from recieving any further - * updates. - * @param callback The [Callback] to remove. Does nothing if the [Callback] was never - * added in the first place. + * Remove a [Callback] from this instance, preventing it from recieving any further updates. + * @param callback The [Callback] to remove. Does nothing if the [Callback] was never added in + * the first place. * @see Callback */ @Synchronized @@ -116,8 +114,8 @@ class MusicStore private constructor() { /** * Finds a [Music] item [T] in the library by it's [Music.UID]. * @param uid The [Music.UID] to search for. - * @return The [T] corresponding to the given [Music.UID], or null if nothing could be - * found or the [Music.UID] did not correspond to a [T]. + * @return The [T] corresponding to the given [Music.UID], or null if nothing could be found + * or the [Music.UID] did not correspond to a [T]. */ @Suppress("UNCHECKED_CAST") fun find(uid: Music.UID) = uidMap[uid] as? T diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index e9684aa22..c115d080b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.system.Indexer -import org.oxycblt.auxio.util.logD /** * A [ViewModel] providing data specific to the music loading process. diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt index 878d4f64a..7ce2248bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt @@ -149,7 +149,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } /** - * Get a [Comparator] that sorts [Album]s according to this [Mode]. + * Get a [Comparator] that sorts [Album]s according to this [Mode]. * @param isAscending Whether to sort in ascending or descending order. * @return A [Comparator] that can be used to sort a [Album] list according to this [Mode]. */ @@ -281,11 +281,13 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getSongComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.SONG)) + compareByDynamic(isAscending) { it.durationMs }, + compareBy(BasicComparator.SONG)) override fun getAlbumComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.ALBUM)) + compareByDynamic(isAscending) { it.durationMs }, + compareBy(BasicComparator.ALBUM)) override fun getArtistComparator(isAscending: Boolean): Comparator = MultiComparator( @@ -294,7 +296,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getGenreComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending) { it.durationMs }, compareBy(BasicComparator.GENRE)) + compareByDynamic(isAscending) { it.durationMs }, + compareBy(BasicComparator.GENRE)) } /** @@ -310,7 +313,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getAlbumComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.ALBUM)) + compareByDynamic(isAscending) { it.songs.size }, + compareBy(BasicComparator.ALBUM)) override fun getArtistComparator(isAscending: Boolean): Comparator = MultiComparator( @@ -319,7 +323,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getGenreComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending) { it.songs.size }, compareBy(BasicComparator.GENRE)) + compareByDynamic(isAscending) { it.songs.size }, + compareBy(BasicComparator.GENRE)) } /** @@ -430,8 +435,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } /** - * Utility function to create a [Comparator] that sorts in ascending order based on - * the given [Comparator], with a selector based on the item itself. + * Utility function to create a [Comparator] that sorts in ascending order based on the + * given [Comparator], with a selector based on the item itself. * @param comparator The [Comparator] to wrap. * @return A new [Comparator] with the specified configuration. * @see compareBy @@ -440,11 +445,9 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { compareBy(comparator) { it } /** - * A [Comparator] that chains several other [Comparator]s together to form one - * comparison. - * @param comparators The [Comparator]s to chain. These will be iterated through - * in order during a comparison, with the first non-equal result becoming the - * result. + * A [Comparator] that chains several other [Comparator]s together to form one comparison. + * @param comparators The [Comparator]s to chain. These will be iterated through in order + * during a comparison, with the first non-equal result becoming the result. */ private class MultiComparator(vararg comparators: Comparator) : Comparator { private val _comparators = comparators @@ -493,8 +496,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } /** - * A [Comparator] that compares abstract [Music] values. Internally, this is similar - * to [NullableComparator], however comparing [Music.collationKey] instead of [Comparable]. + * A [Comparator] that compares abstract [Music] values. Internally, this is similar to + * [NullableComparator], however comparing [Music.collationKey] instead of [Comparable]. * @see NullableComparator * @see Music.collationKey */ @@ -511,7 +514,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } companion object { - /** A re-usable instance configured for [Song]s. */ + /** A re-usable instance configured for [Song]s. */ val SONG: Comparator = BasicComparator() /** A re-usable instance configured for [Album]s. */ val ALBUM: Comparator = BasicComparator() @@ -523,8 +526,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { } /** - * A [Comparator] that compares two possibly null values. Values will be considered - * lesser if they are null, and greater if they are non-null. + * A [Comparator] that compares two possibly null values. Values will be considered lesser + * if they are null, and greater if they are non-null. */ private class NullableComparator> private constructor() : Comparator { override fun compare(a: T?, b: T?) = diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt index 55a78ef58..28c232f0d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -38,22 +38,20 @@ import org.oxycblt.auxio.util.requireBackgroundThread * @author Alexander Capehart (OxygenCobalt) */ interface CacheExtractor { - /** - * Initialize the Extractor by reading the cache data into memory. - */ + /** Initialize the Extractor by reading the cache data into memory. */ fun init() /** - * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, - * alongside freeing up memory. + * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside + * freeing up memory. * @param rawSongs The songs to write into the cache. */ fun finalize(rawSongs: List) /** * Use the cache to populate the given [Song.Raw]. - * @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will - * only contain the bare minimum information required to load a cache entry. + * @param rawSong The [Song.Raw] to attempt to populate. Note that this [Song.Raw] will only + * contain the bare minimum information required to load a cache entry. * @return An [ExtractionResult] representing the result of the operation. * [ExtractionResult.PARSED] is not returned. */ @@ -61,8 +59,8 @@ interface CacheExtractor { } /** - * A [CacheExtractor] only capable of writing to the cache. This can be used to load music - * with without the cache if the user desires. + * A [CacheExtractor] only capable of writing to the cache. This can be used to load music with + * without the cache if the user desires. * @param context [Context] required to read the cache database. * @see CacheExtractor * @author Alexander Capehart (OxygenCobalt) @@ -120,9 +118,10 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr } override fun populate(rawSong: Song.Raw): ExtractionResult { - val map = requireNotNull(cacheMap) { - "Must initialize this extractor before populating a raw song." - } + val map = + requireNotNull(cacheMap) { + "Must initialize this extractor before populating a raw song." + } // For a cached raw song to be used, it must exist within the cache and have matching // addition and modification timestamps. Technically the addition timestamp doesn't @@ -228,8 +227,8 @@ private class CacheDatabase(context: Context) : /** * Read out this database into memory. * @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing - * the cacheable data for the entry. Note that any filesystem-dependent information - * (excluding IDs and timestamps) is not cached. + * the cacheable data for the entry. Note that any filesystem-dependent information (excluding + * IDs and timestamps) is not cached. */ fun read(): Map { requireBackgroundThread() @@ -323,7 +322,9 @@ private class CacheDatabase(context: Context) : raw.albumArtistSortNames = it.parseSQLMultiValue() } - cursor.getStringOrNull(genresIndex)?.let { raw.genreNames = it.parseSQLMultiValue() } + cursor.getStringOrNull(genresIndex)?.let { + raw.genreNames = it.parseSQLMultiValue() + } map[id] = raw } @@ -376,20 +377,22 @@ private class CacheDatabase(context: Context) : put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId) put(Columns.ALBUM_NAME, rawSong.albumName) put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName) - put( - Columns.ALBUM_TYPES, - rawSong.albumTypes.toSQLMultiValue()) + put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue()) put( Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toSQLMultiValue()) put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue()) - put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toSQLMultiValue()) + put( + Columns.ARTIST_SORT_NAMES, + rawSong.artistSortNames.toSQLMultiValue()) put( Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, rawSong.albumArtistMusicBrainzIds.toSQLMultiValue()) - put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toSQLMultiValue()) + put( + Columns.ALBUM_ARTIST_NAMES, + rawSong.albumArtistNames.toSQLMultiValue()) put( Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toSQLMultiValue()) @@ -416,8 +419,8 @@ private class CacheDatabase(context: Context) : /** * Transforms the multi-string list into a SQL-safe multi-string value. - * @return A single string containing all values within the multi-string list, delimited - * by a ";". Pre-existing ";" characters will be escaped. + * @return A single string containing all values within the multi-string list, delimited by a + * ";". Pre-existing ";" characters will be escaped. */ private fun List.toSQLMultiValue() = if (isNotEmpty()) { @@ -428,14 +431,12 @@ private class CacheDatabase(context: Context) : /** * Transforms the SQL-safe multi-string value into a multi-string list. - * @return A list of strings corresponding to the delimited values present within the - * original string. Escaped delimiters are converted back into their normal forms. + * @return A list of strings corresponding to the delimited values present within the original + * string. Escaped delimiters are converted back into their normal forms. */ private fun String.parseSQLMultiValue() = splitEscaped { it == ';' } - /** - * Defines the columns used in this database. - */ + /** Defines the columns used in this database. */ private object Columns { /** @see Song.Raw.mediaStoreId */ const val MEDIA_STORE_ID = "msid" diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ExtractionResult.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ExtractionResult.kt index e14a05526..6b56f8c70 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ExtractionResult.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ExtractionResult.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.extractor /** @@ -5,18 +22,12 @@ package org.oxycblt.auxio.music.extractor * @author Alexander Capehart (OxygenCobalt) */ enum class ExtractionResult { - /** - * A raw song was successfully extracted from the cache. - */ + /** A raw song was successfully extracted from the cache. */ CACHED, - /** - * A raw song was successfully extracted from parsing it's file. - */ + /** A raw song was successfully extracted from parsing it's file. */ PARSED, - /** - * A raw song could not be parsed. - */ + /** A raw song could not be parsed. */ NONE -} \ No newline at end of file +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index bf9e37072..5bc92acd8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -29,21 +29,21 @@ import androidx.core.database.getStringOrNull import java.io.File import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.storage.Directory +import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.directoryCompat import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.storage.safeQuery import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD /** * The layer that loads music from the [MediaStore] database. This is an intermediate step in the - * music extraction process and primarily intended for redundancy for files not natively - * supported by [MetadataExtractor]. Solely relying on this is not recommended, as it often - * produces bad metadata. + * music extraction process and primarily intended for redundancy for files not natively supported + * by [MetadataExtractor]. Solely relying on this is not recommended, as it often produces bad + * metadata. * @param context [Context] required to query the media database. * @param cacheExtractor [CacheExtractor] implementation for cache optimizations. * @author Alexander Capehart (OxygenCobalt) @@ -69,15 +69,15 @@ abstract class MediaStoreExtractor( private val genreNamesMap = mutableMapOf() /** - * The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform - * path information from the database into volume-aware paths. + * The [StorageVolume]s currently scanned by [MediaStore]. This should be used to transform path + * information from the database into volume-aware paths. */ protected var volumes = listOf() private set /** - * Initialize this instance. This involves setting up the required sub-extractors and - * querying the media database for music files. + * Initialize this instance. This involves setting up the required sub-extractors and querying + * the media database for music files. * @return A [Cursor] of the music data returned from the database. */ open fun init(): Cursor { @@ -124,11 +124,14 @@ abstract class MediaStoreExtractor( // Now we can actually query MediaStore. logD("Starting song query [proj: ${projection.toList()}, selector: $selector, args: $args]") - val cursor = context.contentResolverSafe.safeQuery( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - projection, - selector, - args.toTypedArray()).also { cursor = it } + val cursor = + context.contentResolverSafe + .safeQuery( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + projection, + selector, + args.toTypedArray()) + .also { cursor = it } logD("Song query succeeded [Projected total: ${cursor.count}]") // Set up cursor indices for later use. @@ -184,8 +187,8 @@ abstract class MediaStoreExtractor( } /** - * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, - * alongside freeing up memory. + * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside + * freeing up memory. * @param rawSongs The songs to write into the cache. */ fun finalize(rawSongs: List) { @@ -222,8 +225,8 @@ abstract class MediaStoreExtractor( } /** - * The database columns available to all android versions supported by Auxio. - * Concrete implementations can extend this projection to add version-specific columns. + * The database columns available to all android versions supported by Auxio. Concrete + * implementations can extend this projection to add version-specific columns. */ protected open val projection: Array get() = @@ -244,8 +247,8 @@ abstract class MediaStoreExtractor( AUDIO_COLUMN_ALBUM_ARTIST) /** - * The companion template to add to the projection's selector whenever arguments are added - * by [addDirToSelector]. + * The companion template to add to the projection's selector whenever arguments are added by + * [addDirToSelector]. * @see addDirToSelector */ protected abstract val dirSelectorTemplate: String @@ -253,8 +256,8 @@ abstract class MediaStoreExtractor( /** * Add a [Directory] to the given list of projection selector arguments. * @param dir The [Directory] to add. - * @param args The destination list to append selector arguments to that are analogous - * to the given [Directory]. + * @param args The destination list to append selector arguments to that are analogous to the + * given [Directory]. * @return true if the [Directory] was added, false otherwise. * @see dirSelectorTemplate */ @@ -263,8 +266,8 @@ abstract class MediaStoreExtractor( /** * Populate a [Song.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is the * data that cannot be cached. This includes any information not intrinsic to the file and - * instead dependent on the file-system, which could change without invalidating the cache - * due to volume additions or removals. + * instead dependent on the file-system, which could change without invalidating the cache due + * to volume additions or removals. * @param cursor The [Cursor] to read from. * @param raw The [Song.Raw] to populate. * @see populateMetadata @@ -281,9 +284,9 @@ abstract class MediaStoreExtractor( } /** - * Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the - * data about a [Song.Raw] that can be cached. This includes any information intrinsic to - * the file or it's file format, such as music tags. + * Populate a [Song.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the data + * about a [Song.Raw] that can be cached. This includes any information intrinsic to the file or + * it's file format, such as music tags. * @param cursor The [Cursor] to read from. * @param raw The [Song.Raw] to populate. * @see populateFileData @@ -334,8 +337,8 @@ abstract class MediaStoreExtractor( private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST /** - * The external volume. This naming has existed since API 21, but no constant existed - * for it until API 29. This will work on all versions that Auxio supports. + * The external volume. This naming has existed since API 21, but no constant existed for it + * until API 29. This will work on all versions that Auxio supports. */ @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL } @@ -367,7 +370,8 @@ class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) override val projection: Array get() = super.projection + - arrayOf(MediaStore.Audio.AudioColumns.TRACK, + arrayOf( + MediaStore.Audio.AudioColumns.TRACK, // Below API 29, we are restricted to the absolute path (Called DATA by // MedaStore) when working with audio files. MediaStore.Audio.AudioColumns.DATA) @@ -486,8 +490,8 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheE } /** - * A [MediaStoreExtractor] that completes the music loading process in a way compatible with at - * API 29. + * A [MediaStoreExtractor] that completes the music loading process in a way compatible with at API + * 29. * @param context [Context] required to query the media database. * @param cacheExtractor [CacheExtractor] implementation for cache functionality. * @author Alexander Capehart (OxygenCobalt) @@ -521,8 +525,8 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra } /** - * A [MediaStoreExtractor] that completes the music loading process in a way compatible from - * API 30 onwards. + * A [MediaStoreExtractor] that completes the music loading process in a way compatible from API 30 + * onwards. * @param context [Context] required to query the media database. * @param cacheExtractor [CacheExtractor] implementation for cache optimizations. * @author Alexander Capehart (OxygenCobalt) diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 9a9797c09..f5529093d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -32,8 +32,8 @@ import org.oxycblt.auxio.util.logW /** * The extractor that leverages ExoPlayer's [MetadataRetriever] API to parse metadata. This is the - * last step in the music extraction process and is mostly responsible for papering over the - * bad metadata that [MediaStoreExtractor] produces. + * last step in the music extraction process and is mostly responsible for papering over the bad + * metadata that [MediaStoreExtractor] produces. * * @param context [Context] required for reading audio files. * @param mediaStoreExtractor [MediaStoreExtractor] implementation for cache optimizations and @@ -56,17 +56,17 @@ class MetadataExtractor( fun init() = mediaStoreExtractor.init().count /** - * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, - * alongside freeing up memory. + * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside + * freeing up memory. * @param rawSongs The songs to write into the cache. */ fun finalize(rawSongs: List) = mediaStoreExtractor.finalize(rawSongs) /** - * Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate - * to the sub-extractors before parsing the metadata itself. - * @param emit A callback that will be invoked with every new [Song.Raw] instance when - * they are successfully loaded. + * Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate to the + * sub-extractors before parsing the metadata itself. + * @param emit A callback that will be invoked with every new [Song.Raw] instance when they are + * successfully loaded. */ suspend fun parse(emit: suspend (Song.Raw) -> Unit) { while (true) { @@ -122,8 +122,8 @@ class MetadataExtractor( } /** - * Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. - * TODO: Re-unify with MetadataExtractor. + * Wraps a [MetadataExtractor] future and processes it into a [Song.Raw] when completed. TODO: + * Re-unify with MetadataExtractor. * @param context [Context] required to open the audio file. * @param raw [Song.Raw] to process. * @author Alexander Capehart (OxygenCobalt) @@ -135,7 +135,8 @@ class Task(context: Context, private val raw: Song.Raw) { private val future = MetadataRetriever.retrieveMetadata( context, - MediaItem.fromUri(requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri())) + MediaItem.fromUri( + requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri())) /** * Try to get a completed song from this [Task], if it has finished processing. @@ -246,14 +247,17 @@ class Task(context: Context, private val raw: Song.Raw) { // 5. ID3v2.3 Release Year, as it is the most common date type (textFrames["TDOR"]?.run { get(0).parseTimestamp() } ?: textFrames["TDRC"]?.run { get(0).parseTimestamp() } - ?: textFrames["TDRL"]?.run { get(0).parseTimestamp() } ?: parseId3v23Date(textFrames)) + ?: textFrames["TDRL"]?.run { get(0).parseTimestamp() } + ?: parseId3v23Date(textFrames)) ?.let { raw.date = it } // Album textFrames["TXXX:MusicBrainz Album Id"]?.let { raw.albumMusicBrainzId = it[0] } textFrames["TALB"]?.let { raw.albumName = it[0] } textFrames["TSOA"]?.let { raw.albumSortName = it[0] } - (textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let { raw.albumTypes = it } + (textFrames["TXXX:MusicBrainz Album Type"] ?: textFrames["GRP1"])?.let { + raw.albumTypes = it + } // Artist textFrames["TXXX:MusicBrainz Artist Id"]?.let { raw.artistMusicBrainzIds = it } @@ -274,9 +278,9 @@ class Task(context: Context, private val raw: Song.Raw) { * Frames. * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more * values. - * @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, - * and a hour/minute value from TIME. No second value is included. The latter two fields may - * not be included in they cannot be parsed. Will be null if a year value could not be parsed. + * @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a + * hour/minute value from TIME. No second value is included. The latter two fields may not be + * included in they cannot be parsed. Will be null if a year value could not be parsed. */ private fun parseId3v23Date(textFrames: Map>): Date? { // Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY @@ -313,8 +317,7 @@ class Task(context: Context, private val raw: Song.Raw) { /** * Complete this instance's [Song.Raw] with Vorbis comments. - * @param comments A mapping between vorbis comment names and one or more vorbis comment - * values. + * @param comments A mapping between vorbis comment names and one or more vorbis comment values. */ private fun populateWithVorbis(comments: Map>) { // Song @@ -363,8 +366,8 @@ class Task(context: Context, private val raw: Song.Raw) { /** * Copies and sanitizes a possibly native/non-UTF-8 string. - * @return A new string allocated in a memory-safe manner with any UTF-8 errors - * replaced with the Unicode replacement byte sequence. + * @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with + * the Unicode replacement byte sequence. */ private fun String.sanitize() = String(encodeToByteArray()) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index f400184eb..0f9c096a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -24,27 +24,25 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.nonZeroOrNull /** - * Unpack the track number from a combined track + disc [Int] field. - * These fields appear within MediaStore's TRACK column, and combine the track and disc value - * into a single field where the disc number is the 4th+ digit. - * @return The track number extracted from the combined integer value, or null if the value - * was zero. + * Unpack the track number from a combined track + disc [Int] field. These fields appear within + * MediaStore's TRACK column, and combine the track and disc value into a single field where the + * disc number is the 4th+ digit. + * @return The track number extracted from the combined integer value, or null if the value was + * zero. */ fun Int.unpackTrackNo() = mod(1000).nonZeroOrNull() /** - * Unpack the disc number from a combined track + disc [Int] field. - * These fields appear within MediaStore's TRACK column, and combine the track and disc value - * into a single field where the disc number is the 4th+ digit. - * @return The disc number extracted from the combined integer field, or null if the value - * was zero. + * Unpack the disc number from a combined track + disc [Int] field. These fields appear within + * MediaStore's TRACK column, and combine the track and disc value into a single field where the + * disc number is the 4th+ digit. + * @return The disc number extracted from the combined integer field, or null if the value was zero. */ fun Int.unpackDiscNo() = div(1000).nonZeroOrNull() /** - * Parse the number out of a combined number + total position [String] field. - * These fields often appear in ID3v2 files, and consist of a number and an (optional) total - * value delimited by a /. + * Parse the number out of a combined number + total position [String] field. These fields often + * appear in ID3v2 files, and consist of a number and an (optional) total value delimited by a /. * @return The number value extracted from the string field, or null if the value could not be * parsed or if the value was zero. */ @@ -59,24 +57,23 @@ fun Int.toDate() = Date.from(this) /** * Parse an integer year field from a [String] and transform it into a [Date]. - * @return A [Date] consisting of the year value, or null if the value could not - * be parsed or if the value was zero. + * @return A [Date] consisting of the year value, or null if the value could not be parsed or if the + * value was zero. * @see Date.from */ fun String.parseYear() = toIntOrNull()?.toDate() /** * Parse an ISO-8601 timestamp [String] into a [Date]. - * @return A [Date] consisting of the year value plus one or more refinement values - * (ex. month, day), or null if the timestamp was not valid. + * @return A [Date] consisting of the year value plus one or more refinement values (ex. month, + * day), or null if the timestamp was not valid. */ fun String.parseTimestamp() = Date.from(this) /** - * Split a [String] by the given selector, automatically handling escaped characters - * that satisfy the selector. - * @param selector A block that determines if the string should be split at a given - * character. + * Split a [String] by the given selector, automatically handling escaped characters that satisfy + * the selector. + * @param selector A block that determines if the string should be split at a given character. * @return One or more [String]s split by the selector. */ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList { @@ -118,9 +115,9 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): MutableList } /** - * Parse a multi-value tag based on the user configuration. If the value is already composed of - * more than one value, nothing is done. Otherwise, this function will attempt to split it based - * on the user's separator preferences. + * Parse a multi-value tag based on the user configuration. If the value is already composed of more + * than one value, nothing is done. Otherwise, this function will attempt to split it based on the + * user's separator preferences. * @param settings [Settings] required to obtain user separator configuration. * @return A new list of one or more [String]s. */ @@ -157,8 +154,8 @@ fun String.toUuidOrNull(): UUID? = /** * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer - * representations of genre fields into their named counterparts, and split up singular - * ID3v2-style integer genre fields into one or more genres. + * representations of genre fields into their named counterparts, and split up singular ID3v2-style + * integer genre fields into one or more genres. * @param settings [Settings] required to obtain user separator configuration. * @return A list of one or more genre names.. */ @@ -197,16 +194,15 @@ private fun String.parseId3v1Genre(): String? = } /** - * A [Regex] that implements parsing for ID3v2's genre format. - * Derived from mutagen: https://github.com/quodlibet/mutagen + * A [Regex] that implements parsing for ID3v2's genre format. Derived from mutagen: + * https://github.com/quodlibet/mutagen */ private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") /** - * Parse an ID3v2 integer genre field, which has support for multiple genre values and - * combined named/integer genres. - * @return A list of one or more genres, or null if the field is not a valid ID3v2 - * integer genre. + * Parse an ID3v2 integer genre field, which has support for multiple genre values and combined + * named/integer genres. + * @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre. */ private fun String.parseId3v2Genre(): List? { val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index 2c8b37785..e060a76aa 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -25,13 +25,12 @@ import com.google.android.material.checkbox.MaterialCheckBox import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context /** - * A [ViewBindingDialogFragment] that allows the user to configure the separator characters - * used to split tags with multiple values. - * TODO: Add saved state for pending configurations. + * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to + * split tags with multiple values. TODO: Add saved state for pending configurations. * @author Alexander Capehart (OxygenCobalt) */ class SeparatorsDialog : ViewBindingDialogFragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt index 8d23750a3..73065eabc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt @@ -57,8 +57,8 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) : } /** - * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical - * [Artist] item, for use with [ArtistChoiceAdapter]. Use [new] to create an instance. + * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for + * use with [ArtistChoiceAdapter]. Use [new] to create an instance. */ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogRecyclerView.ViewHolder(binding.root) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt index 9b87c0e08..86f9af08b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt @@ -23,7 +23,7 @@ import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.shared.NavigationViewModel +import org.oxycblt.auxio.ui.NavigationViewModel /** * An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous. diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt index c5c3380df..7c59364db 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt @@ -27,16 +27,17 @@ import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.collectImmediately /** - * The base class for dialogs that implements common behavior across all [Artist] pickers. - * These are shown whenever what to do with an item's [Artist] is ambiguous, as there are - * multiple [Artist]'s to choose from. + * The base class for dialogs that implements common behavior across all [Artist] pickers. These are + * shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s + * to choose from. * @author Alexander Capehart (OxygenCobalt) */ -abstract class ArtistPickerDialog : ViewBindingDialogFragment(), ClickableListListener { +abstract class ArtistPickerDialog : + ViewBindingDialogFragment(), ClickableListListener { protected val pickerModel: PickerViewModel by viewModels() // Okay to leak this since the Listener will not be called until after initialization. private val artistAdapter = ArtistChoiceAdapter(@Suppress("LeakingThis") this) diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index 8247cfdd9..b2cb7877b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -27,10 +27,9 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.unlikelyToBeNull /** - * a [ViewModel] that manages the current music picker state. - * TODO: This really shouldn't exist. Make it so that the dialogs just contain the music - * themselves and then exit if the library changes. - * TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of + * a [ViewModel] that manages the current music picker state. TODO: This really shouldn't exist. + * Make it so that the dialogs just contain the music themselves and then exit if the library + * changes. TODO: While we are at it, let's go and add ClickableSpan too to reduce the extent of * this dialog. * @author Alexander Capehart (OxygenCobalt) */ @@ -46,7 +45,8 @@ class PickerViewModel : ViewModel(), MusicStore.Callback { private val _currentArtists = MutableStateFlow?>(null) /** - * The current [Artist] whose choices are being shown in the picker. Null/Empty if there is none. + * The current [Artist] whose choices are being shown in the picker. Null/Empty if there is + * none. */ val currentArtists: StateFlow?> get() = _currentArtists @@ -90,5 +90,4 @@ class PickerViewModel : ViewModel(), MusicStore.Callback { // Map the UIDs to artist instances and filter out the ones that can't be found. _currentArtists.value = uids.mapNotNull { library.find(it) }.ifEmpty { null } } - } diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt index d34eb2f91..f0474c364 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt @@ -30,7 +30,8 @@ import org.oxycblt.auxio.util.inflater * @param listener A [DirectoryAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter() { +class DirectoryAdapter(private val listener: Listener) : + RecyclerView.Adapter() { private val _dirs = mutableListOf() /** * The current list of [Directory]s, may not line up with [MusicDirectories] due to removals. diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt index 6b800eb25..90507e915 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD @@ -58,8 +58,7 @@ class MusicDirsDialog : .setNegativeButton(R.string.lbl_cancel, null) .setPositiveButton(R.string.lbl_save) { _, _ -> val dirs = settings.getMusicDirs(storageManager) - val newDirs = - MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) + val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) if (dirs != newDirs) { logD("Committing changes") settings.setMusicDirs(newDirs) @@ -69,7 +68,8 @@ class MusicDirsDialog : override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { val launcher = - registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) + registerForActivityResult( + ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) // Now that the dialog exists, we get the view manually when the dialog is shown // and override its click listener so that the dialog does not auto-dismiss when we @@ -95,7 +95,9 @@ class MusicDirsDialog : if (pendingDirs != null) { dirs = MusicDirectories( - pendingDirs.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }, + pendingDirs.mapNotNull { + Directory.fromDocumentTreeUri(storageManager, it) + }, savedInstanceState.getBoolean(KEY_PENDING_MODE)) } } @@ -170,9 +172,7 @@ class MusicDirsDialog : } } - /** - * Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true. - */ + /** Get if the UI has currently configured [MusicDirectories.shouldInclude] to be true. */ private fun isUiModeInclude(binding: DialogMusicDirsBinding) = binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt index d20f87d92..00d33959b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.music.storage import android.content.Context @@ -5,13 +22,12 @@ import android.os.storage.StorageManager import android.os.storage.StorageVolume import android.webkit.MimeTypeMap import com.google.android.exoplayer2.util.MimeTypes -import org.oxycblt.auxio.R import java.io.File - +import org.oxycblt.auxio.R /** - * A full absolute path to a file. Only intended for display purposes. For accessing files, - * URIs are preferred in all cases due to scoped storage limitations. + * A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are + * preferred in all cases due to scoped storage limitations. * @param name The name of the file. * @param parent The parent [Directory] of the file. * @author Alexander Capehart (OxygenCobalt) @@ -35,12 +51,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath) /** - * Converts this [Directory] instance into an opaque document tree path. - * This is a huge violation of the document tree URI contract, but it's also the only - * one can sensibly work with these uris in the UI, and it doesn't exactly matter since - * we never write or read directory. - * @return A URI [String] abiding by the document tree specification, or null - * if the [Directory] is not valid. + * Converts this [Directory] instance into an opaque document tree path. This is a huge + * violation of the document tree URI contract, but it's also the only one can sensibly work + * with these uris in the UI, and it doesn't exactly matter since we never write or read + * directory. + * @return A URI [String] abiding by the document tree specification, or null if the [Directory] + * is not valid. */ fun toDocumentTreeUri() = // Document tree URIs consist of a prefixed volume name followed by a relative path. @@ -63,9 +79,7 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: other is Directory && other.volume == volume && other.relativePath == relativePath companion object { - /** - * The name given to the internal volume when in a document tree URI. - */ + /** The name given to the internal volume when in a document tree URI. */ private const val DOCUMENT_URI_PRIMARY_NAME = "primary" /** @@ -80,10 +94,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator)) /** - * Create a new directory from a document tree URI. - * This is a huge violation of the document tree URI contract, but it's also the only - * one can sensibly work with these uris in the UI, and it doesn't exactly matter since - * we never write or read directory. + * Create a new directory from a document tree URI. This is a huge violation of the document + * tree URI contract, but it's also the only one can sensibly work with these uris in the + * UI, and it doesn't exactly matter since we never write or read directory. * @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified * in the given URI. * @param uri The URI string to parse into a [Directory]. @@ -109,12 +122,12 @@ class Directory private constructor(val volume: StorageVolume, val relativePath: } /** - * Represents the configuration for specific directories to filter to/from when loading music. - * TODO: Migrate to a combined "Include + Exclude" system that is more sensible. - * @param dirs A list of [Directory] instances. How these are interpreted depends on - * [shouldInclude]. - * @param shouldInclude True if the library should only load from the [Directory] instances, - * false if the library should not load from the [Directory] instances. + * Represents the configuration for specific directories to filter to/from when loading music. TODO: + * Migrate to a combined "Include + Exclude" system that is more sensible. + * @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude] + * . + * @param shouldInclude True if the library should only load from the [Directory] instances, false + * if the library should not load from the [Directory] instances. * @author Alexander Capehart (OxygenCobalt) */ data class MusicDirectories(val dirs: List, val shouldInclude: Boolean) @@ -122,17 +135,17 @@ data class MusicDirectories(val dirs: List, val shouldInclude: Boolea /** * A mime type of a file. Only intended for display. * @param fromExtension The mime type obtained by analyzing the file extension. - * @param fromFormat The mime type obtained by analyzing the file format. Null if could - * not be obtained. + * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be + * obtained. * @author Alexander Capehart (OxygenCobalt) */ data class MimeType(val fromExtension: String, val fromFormat: String?) { /** * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis". * @param context [Context] required to obtain human-readable strings. - * @return A human-readable name for this mime type. Will first try [fromFormat], - * then falling back to [fromExtension], then falling back to the extension name, - * and then finally a placeholder "No Format" string. + * @return A human-readable name for this mime type. Will first try [fromFormat], then falling + * back to [fromExtension], then falling back to the extension name, and then finally a + * placeholder "No Format" string. */ fun resolveName(context: Context): String { // We try our best to produce a more readable name for the common audio formats. @@ -193,8 +206,8 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { } else { // Fall back to the extension if we can't find a special name for this format. MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase() - // Fall back to a placeholder if even that fails. - ?: context.getString(R.string.def_codec) + // Fall back to a placeholder if even that fails. + ?: context.getString(R.string.def_codec) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt index 97c398ec3..60bc797e9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/StorageUtil.kt @@ -34,8 +34,8 @@ import org.oxycblt.auxio.util.lazyReflectedMethod // --- MEDIASTORE UTILITIES --- /** - * Get a content resolver that will not mangle MediaStore queries on certain devices. - * See https://github.com/OxygenCobalt/Auxio/issues/50 for more info. + * Get a content resolver that will not mangle MediaStore queries on certain devices. See + * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. */ val Context.contentResolverSafe: ContentResolver get() = applicationContext.contentResolver @@ -44,8 +44,8 @@ val Context.contentResolverSafe: ContentResolver * A shortcut for querying the [ContentResolver] database. * @param uri The [Uri] of content to retrieve. * @param projection A list of SQL columns to query from the database. - * @param selector A SQL selection statement to filter results. Spaces where - * arguments should be filled in are represented with a "?". + * @param selector A SQL selection statement to filter results. Spaces where arguments should be + * filled in are represented with a "?". * @param args The arguments used for the selector. * @return A [Cursor] of the queried values, organized by the column projection. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. @@ -56,20 +56,18 @@ fun ContentResolver.safeQuery( projection: Array, selector: String? = null, args: Array? = null -) = requireNotNull(query(uri, projection, selector, args, null)) { - "ContentResolver query failed" -} +) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" } /** - * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s - * resources when no longer used. + * A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources + * when no longer used. * @param uri The [Uri] of content to retrieve. * @param projection A list of SQL columns to query from the database. - * @param selector A SQL selection statement to filter results. Spaces where - * arguments should be filled in are represented with a "?". + * @param selector A SQL selection statement to filter results. Spaces where arguments should be + * filled in are represented with a "?". * @param args The arguments used for the selector. - * @param block The block of code to run with the queried [Cursor]. Will not be ran if the - * [Cursor] is empty. + * @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor] + * is empty. * @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor]. * @see ContentResolver.query */ @@ -81,9 +79,7 @@ inline fun ContentResolver.useQuery( block: (Cursor) -> R ) = safeQuery(uri, projection, selector, args).use(block) -/** - * Album art [MediaStore] database is not a built-in constant, have to define it ourselves. - */ +/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albumart") /** @@ -92,11 +88,12 @@ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albu * @see ContentUris.withAppendedId * @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI */ -fun Long.toAudioUri() = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this) +fun Long.toAudioUri() = + ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this) /** - * Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover - * will be fast to load, but will be lower quality. + * Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will + * be fast to load, but will be lower quality. * @return An external storage image [Uri]. May not exist. * @see ContentUris.withAppendedId */ @@ -114,10 +111,9 @@ fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this) private val SM_API21_GET_VOLUME_LIST_METHOD: Method by lazyReflectedMethod(StorageManager::class, "getVolumeList") - /** - * Provides the analogous method to [StorageVolume.getDirectory] method that is usable from - * API 21 to API 23, in which the [StorageVolume] API was hidden and differed greatly. + * Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21 + * to API 23, in which the [StorageVolume] API was hidden and differed greatly. * @see StorageVolume.getDirectory */ @Suppress("NewApi") @@ -175,8 +171,8 @@ val StorageVolume.directoryCompat: String? fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context) /** - * If this [StorageVolume] is considered the "Primary" volume where the Android System is - * kept. May still be a removable volume. + * If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May + * still be a removable volume. * @see StorageVolume.isPrimary */ val StorageVolume.isPrimaryCompat: Boolean @@ -191,8 +187,8 @@ val StorageVolume.isEmulatedCompat: Boolean @SuppressLint("NewApi") get() = isEmulated /** - * If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as - * "primary" to [MediaStore] and Document [Uri]s, obtained in a version compatible manner. + * If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary" + * to [MediaStore] and Document [Uri]s, obtained in a version compatible manner. */ val StorageVolume.isInternalCompat: Boolean // Must contain the android system AND be an emulated drive, as non-emulated system @@ -200,24 +196,24 @@ val StorageVolume.isInternalCompat: Boolean get() = isPrimaryCompat && isEmulatedCompat /** - * The unique identifier for this [StorageVolume], obtained in a version compatible manner - * Can be null. + * The unique identifier for this [StorageVolume], obtained in a version compatible manner Can be + * null. * @see StorageVolume.getUuid */ val StorageVolume.uuidCompat: String? @SuppressLint("NewApi") get() = uuid /** - * The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in - * a version compatible manner. + * The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a + * version compatible manner. * @see StorageVolume.getState */ val StorageVolume.stateCompat: String @SuppressLint("NewApi") get() = state /** - * Returns the name of this volume that can be used to interact with [MediaStore], in - * a version compatible manner. Will be null if the volume is not scanned by [MediaStore]. + * Returns the name of this volume that can be used to interact with [MediaStore], in a version + * compatible manner. Will be null if the volume is not scanned by [MediaStore]. * @see StorageVolume.getMediaStoreVolumeName */ val StorageVolume.mediaStoreVolumeNameCompat: String? diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 323a62f4a..d3a3d58f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -43,10 +43,10 @@ import org.oxycblt.auxio.util.logW /** * Core music loading state class. * - * This class provides low-level access into the exact state of the music loading process. - * **This class should not be used in most cases.** It is highly volatile and provides far - * more information than is usually needed. Use [MusicStore] instead if you do not need to - * work with the exact music loading state. + * This class provides low-level access into the exact state of the music loading process. **This + * class should not be used in most cases.** It is highly volatile and provides far more information + * than is usually needed. Use [MusicStore] instead if you do not need to work with the exact music + * loading state. * * @author Alexander Capehart (OxygenCobalt) */ @@ -61,9 +61,9 @@ class Indexer private constructor() { get() = indexingState != null /** - * Whether this instance has not completed a loading process and is not currently - * loading music. This often occurs early in an app's lifecycle, and consumers should - * try to avoid showing any state when this flag is true. + * Whether this instance has not completed a loading process and is not currently loading music. + * This often occurs early in an app's lifecycle, and consumers should try to avoid showing any + * state when this flag is true. */ val isIndeterminate: Boolean get() = lastResponse == null && indexingState == null @@ -105,9 +105,9 @@ class Indexer private constructor() { } /** - * Register the [Callback] for this instance. This can be used to receive rapid-fire updates - * to the current music loading state. There can be only one [Callback] at a time. - * Will invoke all [Callback] methods to initialize the instance with the current state. + * Register the [Callback] for this instance. This can be used to receive rapid-fire updates to + * the current music loading state. There can be only one [Callback] at a time. Will invoke all + * [Callback] methods to initialize the instance with the current state. * @param callback The [Callback] to add. */ @Synchronized @@ -125,10 +125,9 @@ class Indexer private constructor() { } /** - * Unregister a [Callback] from this instance, preventing it from recieving any further - * updates. - * @param callback The [Callback] to unregister. Must be the current [Callback]. Does - * nothing if invoked by another [Callback] implementation. + * Unregister a [Callback] from this instance, preventing it from recieving any further updates. + * @param callback The [Callback] to unregister. Must be the current [Callback]. Does nothing if + * invoked by another [Callback] implementation. * @see Callback */ @Synchronized @@ -145,12 +144,12 @@ class Indexer private constructor() { * Start the indexing process. This should be done from in the background from [Controller]'s * context after a command has been received to start the process. * @param context [Context] required to load music. - * @param withCache Whether to use the cache or not when loading. If false, the cache will - * still be written, but no cache entries will be loaded into the new library. + * @param withCache Whether to use the cache or not when loading. If false, the cache will still + * be written, but no cache entries will be loaded into the new library. */ suspend fun index(context: Context, withCache: Boolean) { if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == - PackageManager.PERMISSION_DENIED) { + PackageManager.PERMISSION_DENIED) { // No permissions, signal that we can't do anything. emitCompletion(Response.NoPerms) return @@ -186,9 +185,9 @@ class Indexer private constructor() { } /** - * Request that the music library should be reloaded. This should be used by components that - * do not manage the indexing process in order to signal that the [Controller] should call - * [index] eventually. + * Request that the music library should be reloaded. This should be used by components that do + * not manage the indexing process in order to signal that the [Controller] should call [index] + * eventually. * @param withCache Whether to use the cache when loading music. Does nothing if there is no * [Controller]. */ @@ -199,8 +198,8 @@ class Indexer private constructor() { } /** - * Reset the current loading state to signal that the instance is not loading. This should - * be called by [Controller] after it's indexing co-routine was cancelled. + * Reset the current loading state to signal that the instance is not loading. This should be + * called by [Controller] after it's indexing co-routine was cancelled. */ @Synchronized fun reset() { @@ -211,19 +210,20 @@ class Indexer private constructor() { /** * Internal implementation of the music loading process. * @param context [Context] required to load music. - * @param withCache Whether to use the cache or not when loading. If false, the cache will - * still be written, but no cache entries will be loaded into the new library. + * @param withCache Whether to use the cache or not when loading. If false, the cache will still + * be written, but no cache entries will be loaded into the new library. * @return A newly-loaded [MusicStore.Library], or null if nothing was loaded. */ private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? { // Create the chain of extractors. Each extractor builds on the previous and // enables version-specific features in order to create the best possible music // experience. - val cacheDatabase = if (withCache) { - ReadWriteCacheExtractor(context) - } else { - WriteOnlyCacheExtractor(context) - } + val cacheDatabase = + if (withCache) { + ReadWriteCacheExtractor(context) + } else { + WriteOnlyCacheExtractor(context) + } val mediaStoreExtractor = when { @@ -255,11 +255,11 @@ class Indexer private constructor() { /** * Load a list of [Song]s from the device. - * @param metadataExtractor The completed [MetadataExtractor] instance to use to load - * [Song.Raw] instances. + * @param metadataExtractor The completed [MetadataExtractor] instance to use to load [Song.Raw] + * instances. * @param settings [Settings] required to create [Song] instances. - * @return A possibly empty list of [Song]s. These [Song]s will be incomplete and - * must be linked with parent [Album], [Artist], and [Genre] items in order to be usable. + * @return A possibly empty list of [Song]s. These [Song]s will be incomplete and must be linked + * with parent [Album], [Artist], and [Genre] items in order to be usable. */ private suspend fun buildSongs( metadataExtractor: MetadataExtractor, @@ -301,10 +301,10 @@ class Indexer private constructor() { /** * Build a list of [Album]s from the given [Song]s. - * @param songs The [Song]s to build [Album]s from. These will be linked with their - * respective [Album]s when created. - * @return A non-empty list of [Album]s. These [Album]s will be incomplete and - * must be linked with parent [Artist] instances in order to be usable. + * @param songs The [Song]s to build [Album]s from. These will be linked with their respective + * [Album]s when created. + * @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked + * with parent [Artist] instances in order to be usable. */ private fun buildAlbums(songs: List): List { // Group songs by their singular raw album, then map the raw instances and their @@ -316,17 +316,17 @@ class Indexer private constructor() { } /** - * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required - * as they group into [Artist] instances much differently, with [Song]s being grouped - * primarily by artist names, and [Album]s being grouped primarily by album artist names. - * @param songs The [Song]s to build [Artist]s from. One [Song] can result in - * the creation of one or more [Artist] instances. These will be linked with their - * respective [Artist]s when created. - * @param albums The [Album]s to build [Artist]s from. One [Album] can result in - * the creation of one or more [Artist] instances. These will be linked with their - * respective [Artist]s when created. - * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined - * groupings of [Song]s and [Album]s. + * Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as + * they group into [Artist] instances much differently, with [Song]s being grouped primarily by + * artist names, and [Album]s being grouped primarily by album artist names. + * @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of + * one or more [Artist] instances. These will be linked with their respective [Artist]s when + * created. + * @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of + * one or more [Artist] instances. These will be linked with their respective [Artist]s when + * created. + * @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings + * of [Song]s and [Album]s. */ private fun buildArtists(songs: List, albums: List): List { // Add every raw artist credited to each Song/Album to the grouping. This way, @@ -353,9 +353,9 @@ class Indexer private constructor() { /** * Group up [Song]s into [Genre] instances. - * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in - * the creation of one or more [Genre] instances. These will be linked with their - * respective [Genre]s when created. + * @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of + * one or more [Genre] instances. These will be linked with their respective [Genre]s when + * created. * @return A non-empty list of [Genre]s. */ private fun buildGenres(songs: List): List { @@ -376,8 +376,8 @@ class Indexer private constructor() { /** * Emit a new [State.Indexing] state. This can be used to signal the current state of the music - * loading process to external code. Assumes that the callee has already checked if they - * have not been canceled and thus have the ability to emit a new state. + * loading process to external code. Assumes that the callee has already checked if they have + * not been canceled and thus have the ability to emit a new state. * @param indexing The new [Indexing] state to emit, or null if no loading process is occurring. */ @Synchronized @@ -393,8 +393,8 @@ class Indexer private constructor() { /** * Emit a new [State.Complete] state. This can be used to signal the completion of the music - * loading process to external code. Will check if the callee has not been canceled and thus - * has the ability to emit a new state + * loading process to external code. Will check if the callee has not been canceled and thus has + * the ability to emit a new state * @param response The new [Response] to emit, representing the outcome of the music loading * process. */ @@ -439,8 +439,7 @@ class Indexer private constructor() { */ sealed class Indexing { /** - * Music loading is occurring, but no definite estimate can be put on the current - * progress. + * Music loading is occurring, but no definite estimate can be put on the current progress. */ object Indeterminate : Indexing() @@ -477,8 +476,8 @@ class Indexer private constructor() { * A callback for rapid-fire changes in the music loading state. * * This is only useful for code that absolutely must show the current loading process. - * Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only - * consisting of the [MusicStore.Library]. + * Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only consisting of + * the [MusicStore.Library]. */ interface Callback { /** @@ -493,13 +492,13 @@ class Indexer private constructor() { } /** - * Context that runs the music loading process. Implementations should be capable of - * running the background for long periods of time without android killing the process. + * Context that runs the music loading process. Implementations should be capable of running the + * background for long periods of time without android killing the process. */ interface Controller : Callback { /** - * Called when a new music loading process was requested. Implementations should - * forward this to [index]. + * Called when a new music loading process was requested. Implementations should forward + * this to [index]. * @param withCache Whether to use the cache or not when loading. If false, the cache should * still be written, but no cache entries will be loaded into the new library. * @see index @@ -511,9 +510,8 @@ class Indexer private constructor() { @Volatile private var INSTANCE: Indexer? = null /** - * A version-compatible identifier for the read external storage permission required - * by the system to load audio. - * TODO: Move elsewhere. + * A version-compatible identifier for the read external storage permission required by the + * system to load audio. TODO: Move elsewhere. */ val PERMISSION_READ_AUDIO = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt index accb30a6c..d9d34fa02 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt @@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R -import org.oxycblt.auxio.shared.ForegroundServiceNotification +import org.oxycblt.auxio.service.ForegroundServiceNotification import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent @@ -89,11 +89,12 @@ class IndexingNotification(private val context: Context) : } /** - * A static [ForegroundServiceNotification] that signals to the user that the app is currently monitoring - * the music library for changes. + * A static [ForegroundServiceNotification] that signals to the user that the app is currently + * monitoring the music library for changes. * @author Alexander Capehart (OxygenCobalt) */ -class ObservingNotification(context: Context) : ForegroundServiceNotification(context, INDEXER_CHANNEL) { +class ObservingNotification(context: Context) : + ForegroundServiceNotification(context, INDEXER_CHANNEL) { init { setSmallIcon(R.drawable.ic_indexer_24) setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 3ee3bd4c3..975e910c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -35,20 +35,20 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.playback.state.PlaybackStateManager +import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ForegroundManager import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD /** * A [Service] that manages the background music loading process. * - * Loading music is a time-consuming process that would likely be killed by the system before - * it could complete if ran anywhere else. So, this [Service] manages the music loading process - * as an instance of [Indexer.Controller]. + * Loading music is a time-consuming process that would likely be killed by the system before it + * could complete if ran anywhere else. So, this [Service] manages the music loading process as an + * instance of [Indexer.Controller]. * - * This [Service] also handles automatic rescanning, as that is a similarly long-running - * background operation that would be unsuitable elsewhere in the app. + * This [Service] also handles automatic rescanning, as that is a similarly long-running background + * operation that would be unsuitable elsewhere in the app. * * TODO: Unify with PlaybackService as part of the service independence project * @@ -121,8 +121,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { indexer.reset() } // Start a new music loading job on a co-routine. - currentIndexJob = indexScope.launch { - indexer.index(this@IndexerService, withCache) } + currentIndexJob = indexScope.launch { indexer.index(this@IndexerService, withCache) } } override fun onIndexerStateChanged(state: Indexer.State?) { @@ -165,8 +164,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { // --- INTERNAL --- /** - * Update the current state to "Active", in which the service signals that music - * loading is on-going. + * Update the current state to "Active", in which the service signals that music loading is + * on-going. * @param state The current music loading state. */ private fun updateActiveSession(state: Indexer.Indexing) { @@ -184,8 +183,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { } /** - * Update the current state to "Idle", in which it either does nothing or signals - * that it's currently monitoring the music library for changes. + * Update the current state to "Idle", in which it either does nothing or signals that it's + * currently monitoring the music library for changes. */ private fun updateIdleSession() { if (settings.shouldBeObserving) { @@ -208,9 +207,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { wakeLock.releaseSafe() } - /** - * Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. - */ + /** Utility to safely acquire a [PowerManager.WakeLock] without crashes/inefficiency. */ private fun PowerManager.WakeLock.acquireSafe() { // Avoid unnecessary acquire calls. if (!wakeLock.isHeld) { @@ -222,9 +219,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { } } - /** - * Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. - */ + /** Utility to safely release a [PowerManager.WakeLock] without crashes/inefficiency. */ private fun PowerManager.WakeLock.releaseSafe() { // Avoid unnecessary release calls. if (wakeLock.isHeld) { @@ -259,7 +254,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { * known to the user as automatic rescanning. The active (and not passive) nature of observing * the database is what requires [IndexerService] to stay foreground when this is enabled. */ - private inner class SystemContentObserver : ContentObserver(Handler(Looper.getMainLooper())), Runnable { + private inner class SystemContentObserver : + ContentObserver(Handler(Looper.getMainLooper())), Runnable { private val handler = Handler(Looper.getMainLooper()) init { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index b9931919b..7e419d26f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -25,9 +25,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.MainNavigationAction -import org.oxycblt.auxio.shared.NavigationViewModel -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.MainNavigationAction +import org.oxycblt.auxio.ui.NavigationViewModel +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.getAttrColorCompat diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 9f166db7e..fbccba0fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -33,9 +33,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.shared.MainNavigationAction -import org.oxycblt.auxio.shared.NavigationViewModel -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.MainNavigationAction +import org.oxycblt.auxio.ui.NavigationViewModel +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.showToast 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 1b406d205..102fa38a2 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 @@ -41,8 +41,7 @@ import org.oxycblt.auxio.util.inflater * @param listener A [Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class QueueAdapter(private val listener: Listener) : - RecyclerView.Adapter() { +class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter() { private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK) // Since PlayingIndicator adapter relies on an item value, we cannot use it for this // adapter, as one item can appear at several points in the UI. Use a similar implementation @@ -72,8 +71,8 @@ class QueueAdapter(private val listener: Listener) : } /** - * Synchronously update the list with new items. This is exceedingly slow for large diffs, - * so only use it for trivial updates. + * Synchronously update the list with new items. This is exceedingly slow for large diffs, so + * only use it for trivial updates. * @param newList The new [Song]s for the adapter to display. */ fun submitList(newList: List) { @@ -81,8 +80,8 @@ class QueueAdapter(private val listener: Listener) : } /** - * Replace the list with a new list. This is exceedingly slow for large diffs, - * so only use it for trivial updates. + * Replace the list with a new list. This is exceedingly slow for large diffs, so only use it + * for trivial updates. * @param newList The new [Song]s for the adapter to display. */ fun replaceList(newList: List) { @@ -90,8 +89,8 @@ class QueueAdapter(private val listener: Listener) : } /** - * Set the position of the currently playing item in the queue. This will mark the item - * as playing and any previous items as played. + * Set the position of the currently playing item in the queue. This will mark the item as + * playing and any previous items as played. * @param index The position of the currently playing item in the queue. * @param isPlaying Whether playback is ongoing or paused. */ @@ -122,9 +121,7 @@ class QueueAdapter(private val listener: Listener) : } } - /** - * A listener for queue list events. - */ + /** A listener for queue list events. */ interface Listener { /** * Called when a [RecyclerView.ViewHolder] in the list as clicked. @@ -152,21 +149,15 @@ class QueueAdapter(private val listener: Listener) : */ class QueueSongViewHolder private constructor(private val binding: ItemQueueSongBinding) : PlayingIndicatorAdapter.ViewHolder(binding.root) { - /** - * The "body" view of this [QueueSongViewHolder] that shows the [Song] information. - */ + /** The "body" view of this [QueueSongViewHolder] that shows the [Song] information. */ val bodyView: View get() = binding.body - /** - * The background view of this [QueueSongViewHolder] that shows the delete icon. - */ + /** The background view of this [QueueSongViewHolder] that shows the delete icon. */ val backgroundView: View get() = binding.background - /** - * The actual background drawable of this [QueueSongViewHolder] that can be manipulated. - */ + /** The actual background drawable of this [QueueSongViewHolder] that can be manipulated. */ val backgroundDrawable = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) @@ -174,9 +165,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong alpha = 0 } - /** - * If this queue item is considered "in the future" (i.e has not played yet). - */ + /** If this queue item is considered "in the future" (i.e has not played yet). */ var isFuture: Boolean get() = binding.songAlbumCover.isEnabled set(value) { @@ -205,9 +194,7 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong */ @SuppressLint("ClickableViewAccessibility") fun bind(song: Song, listener: QueueAdapter.Listener) { - binding.body.setOnClickListener { - listener.onClick(this) - } + binding.body.setOnClickListener { listener.onClick(this) } binding.songAlbumCover.bind(song) binding.songName.text = song.resolveName(binding.context) 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 a35c96a1b..1fb220c9b 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 @@ -28,8 +28,8 @@ import org.oxycblt.auxio.util.getInteger import org.oxycblt.auxio.util.logD /** - * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue - * UI, such as an animation when lifting items. + * A highly customized [ItemTouchHelper.Callback] that enables some extra eye candy in the queue UI, + * such as an animation when lifting items. * @author Alexander Capehart (OxygenCobalt) */ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHelper.Callback() { @@ -73,7 +73,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe holder.itemView .animate() .translationZ(elevation) - .setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) + .setDuration( + recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) .setUpdateListener { bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() } @@ -114,7 +115,8 @@ class QueueDragCallback(private val playbackModel: QueueViewModel) : ItemTouchHe holder.itemView .animate() .translationZ(0f) - .setDuration(recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) + .setDuration( + recyclerView.context.getInteger(R.integer.anim_fade_exit_duration).toLong()) .setUpdateListener { bg.alpha = ((holder.itemView.translationZ / elevation) * 255).toInt() } 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 db4d5cebf..d1271d761 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 @@ -26,7 +26,7 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 6afe6d5f3..6502bef8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -37,7 +37,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { val queue: StateFlow> = _queue private val _index = MutableStateFlow(playbackManager.index) - /** The index of the currently playing song in the queue. */ + /** The index of the currently playing song in the queue. */ val index: StateFlow get() = _index @@ -52,8 +52,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { /** * Start playing the the queue item at the given index. - * @param adapterIndex The index of the queue item to play. Does nothing if the index is out - * of range. + * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of + * range. */ fun goto(adapterIndex: Int) { if (adapterIndex !in playbackManager.queue.indices) { @@ -65,8 +65,8 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { /** * Remove a queue item at the given index. - * @param adapterIndex The index of the queue item to play. Does nothing if the index is - * out of range. + * @param adapterIndex The index of the queue item to play. Does nothing if the index is out of + * range. */ fun removeQueueDataItem(adapterIndex: Int) { if (adapterIndex <= playbackManager.index || @@ -93,16 +93,12 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { return true } - /** - * Finish a replace flag specified by [replaceQueue]. - */ + /** Finish a replace flag specified by [replaceQueue]. */ fun finishReplace() { replaceQueue = null } - /** - * Finish a scroll operation started by [scrollTo]. - */ + /** Finish a scroll operation started by [scrollTo]. */ fun finishScrollTo() { scrollTo = null } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 8b9cd83ed..18861025d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -25,7 +25,7 @@ import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index d61599f62..0cb314f6c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.shared.ForegroundServiceNotification +import org.oxycblt.auxio.service.ForegroundServiceNotification import org.oxycblt.auxio.util.newBroadcastPendingIntent import org.oxycblt.auxio.util.newMainPendingIntent 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 48e5d1154..3bea27d56 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 @@ -54,8 +54,8 @@ import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode +import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ForegroundManager import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider diff --git a/app/src/main/java/org/oxycblt/auxio/playback/ui/BaseBottomSheetBehavior.kt b/app/src/main/java/org/oxycblt/auxio/playback/ui/BaseBottomSheetBehavior.kt index a9b30d1d9..d185bc253 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/ui/BaseBottomSheetBehavior.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/ui/BaseBottomSheetBehavior.kt @@ -51,8 +51,8 @@ abstract class BaseBottomSheetBehavior(context: Context, attributeSet: abstract fun createBackground(context: Context): Drawable /** - * Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] - * is linked to. + * Called when window insets are being applied to the [View] this [BaseBottomSheetBehavior] is + * linked to. * @param child The child view recieving the [WindowInsets]. * @param insets The [WindowInsets] to apply. * @return The (possibly modified) [WindowInsets]. diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 0a1242272..3f9cca499 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -60,7 +60,7 @@ class SearchAdapter(private val listener: SelectableListListener) : } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (val item = differ.currentList[position]) { + when (val item = differ.currentList[position]) { is Song -> (holder as SongViewHolder).bind(item, listener) is Album -> (holder as AlbumViewHolder).bind(item, listener) is Artist -> (holder as ArtistViewHolder).bind(item, listener) @@ -72,8 +72,8 @@ class SearchAdapter(private val listener: SelectableListListener) : override fun isItemFullWidth(position: Int) = differ.currentList[position] is Header /** - * Asynchronously update the list with new items. Assumes that the list only contains - * supported data.. + * Asynchronously update the list with new items. Assumes that the list only contains supported + * data.. * @param newList The new [Item]s for the adapter to display. * @param callback A block called when the asynchronous update is completed. */ 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 3f3f49d18..7a586b538 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -208,9 +208,7 @@ class SearchFragment : ListFragment() { } } - /** - * Safely hide the keyboard from this view. - */ + /** Safely hide the keyboard from this view. */ private fun InputMethodManager.hide() { hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } 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 4aaf87e9c..a088e32e7 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -30,13 +30,9 @@ import kotlinx.coroutines.yield import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.context @@ -129,10 +125,12 @@ class SearchViewModel(application: Application) : } if (filterMode == null || filterMode == MusicMode.SONGS) { - library.songs.searchListImpl(query) { q, song -> song.path.name.contains(q) }?.let { - results.add(Header(R.string.lbl_songs)) - results.addAll(sort.songs(it)) - } + library.songs + .searchListImpl(query) { q, song -> song.path.name.contains(q) } + ?.let { + results.add(Header(R.string.lbl_songs)) + results.addAll(sort.songs(it)) + } } // Handle if we were canceled while searching. @@ -141,41 +139,43 @@ class SearchViewModel(application: Application) : /** * Search a given [Music] list. - * @param query The query to search for. The routine will compare this query to the names - * of each object in the list and + * @param query The query to search for. The routine will compare this query to the names of + * each object in the list and * @param fallback Additional comparison code to run if the item does not match the query - * initially. This can be used to compare against additional attributes to improve search - * result quality. + * initially. This can be used to compare against additional attributes to improve search result + * quality. */ private inline fun List.searchListImpl( query: String, fallback: (String, T) -> Boolean = { _, _ -> false } - ) = filter { - // See if the plain resolved name matches the query. This works for most situations. - val name = it.resolveName(context) - if (name.contains(query, ignoreCase = true)) { - return@filter true - } + ) = + filter { + // See if the plain resolved name matches the query. This works for most situations. + val name = it.resolveName(context) + if (name.contains(query, ignoreCase = true)) { + return@filter true + } - // See if the sort name matches. This can sometimes be helpful as certain libraries - // will tag sort names to have a alphabetized version of the title. - val sortName = it.rawSortName - if (sortName != null && sortName.contains(query, ignoreCase = true)) { - return@filter true - } + // See if the sort name matches. This can sometimes be helpful as certain libraries + // will tag sort names to have a alphabetized version of the title. + val sortName = it.rawSortName + if (sortName != null && sortName.contains(query, ignoreCase = true)) { + return@filter true + } - // As a last-ditch effort, see if the normalized name matches. This will replace - // any non-alphabetical characters with their alphabetical representations, which - // could make it match the query. - val normalizedName = NORMALIZATION_SANITIZE_REGEX.replace( - Normalizer.normalize(name, Normalizer.Form.NFKD), "") - if (normalizedName.contains(query, ignoreCase = true)) { - return@filter true - } + // As a last-ditch effort, see if the normalized name matches. This will replace + // any non-alphabetical characters with their alphabetical representations, which + // could make it match the query. + val normalizedName = + NORMALIZATION_SANITIZE_REGEX.replace( + Normalizer.normalize(name, Normalizer.Form.NFKD), "") + if (normalizedName.contains(query, ignoreCase = true)) { + return@filter true + } - fallback(query, it) - } - .ifEmpty { null } + fallback(query, it) + } + .ifEmpty { null } /** * Returns the ID of the filter option to currently highlight. @@ -212,7 +212,6 @@ class SearchViewModel(application: Application) : search(lastQuery) } - companion object { /** * Converts the output of [Normalizer] to remove any junk characters added by it's diff --git a/app/src/main/java/org/oxycblt/auxio/shared/ForegroundManager.kt b/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/shared/ForegroundManager.kt rename to app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt index aeb9544d5..cebb96001 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/ForegroundManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/service/ForegroundManager.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.service import android.app.Service import androidx.core.app.ServiceCompat @@ -31,17 +31,15 @@ import org.oxycblt.auxio.util.logD class ForegroundManager(private val service: Service) { private var isForeground = false - /** - * Release this instance. - */ + /** Release this instance. */ fun release() { tryStopForeground() } /** * Try to enter a foreground state. - * @param notification The [ForegroundServiceNotification] to show in order to signal the foreground - * state. + * @param notification The [ForegroundServiceNotification] to show in order to signal the + * foreground state. * @return true if the state was changed, false otherwise * @see Service.startForeground */ diff --git a/app/src/main/java/org/oxycblt/auxio/shared/ForegroundServiceNotification.kt b/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt similarity index 92% rename from app/src/main/java/org/oxycblt/auxio/shared/ForegroundServiceNotification.kt rename to app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt index 282fd22a8..c42d126a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/ForegroundServiceNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/service/ForegroundServiceNotification.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.service import android.content.Context import androidx.annotation.StringRes @@ -51,14 +51,11 @@ abstract class ForegroundServiceNotification(context: Context, info: ChannelInfo */ abstract val code: Int - /** - * Post this notification using [NotificationManagerCompat]. - */ + /** Post this notification using [NotificationManagerCompat]. */ fun post() { // This is safe to call without the POST_NOTIFICATIONS permission, as it's a foreground // notification. - @Suppress("MissingPermission") - notificationManager.notify(code, build()) + @Suppress("MissingPermission") notificationManager.notify(code, build()) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index d8a80a096..829cf74d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentAboutBinding import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.formatDurationMs -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index a818ce3b5..aac9bded4 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -35,13 +35,13 @@ import org.oxycblt.auxio.music.storage.MusicDirectories import org.oxycblt.auxio.playback.ActionMode import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp -import org.oxycblt.auxio.settings.accent.Accent +import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** - * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. - * Object mutability + * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object + * mutability * @author Alexander Capehart (OxygenCobalt) */ class Settings(private val context: Context, private val callback: Callback? = null) : @@ -55,8 +55,8 @@ class Settings(private val context: Context, private val callback: Callback? = n } /** - * Migrate any settings from an old version into their modern counterparts. This can cause - * data loss depending on the feasibility of a migration. + * Migrate any settings from an old version into their modern counterparts. This can cause data + * loss depending on the feasibility of a migration. */ fun migrate() { if (inner.contains(OldKeys.KEY_ACCENT3)) { @@ -153,8 +153,8 @@ class Settings(private val context: Context, private val callback: Callback? = n } /** - * Release this instance and any callbacks held by it. This is not needed if no [Callback] - * was originally attached. + * Release this instance and any callbacks held by it. This is not needed if no [Callback] was + * originally attached. */ fun release() { inner.unregisterOnSharedPreferenceChangeListener(this) @@ -164,9 +164,7 @@ class Settings(private val context: Context, private val callback: Callback? = n unlikelyToBeNull(callback).onSettingChanged(key) } - /** - * TODO: Remove this - */ + /** TODO: Remove this */ interface Callback { fun onSettingChanged(key: String) } @@ -264,8 +262,8 @@ class Settings(private val context: Context, private val callback: Callback? = n ?: MusicMode.SONGS /** - * What MusicParent item to play from when a Song is played from the detail view. - * Will be null if configured to play from the currently shown item. + * What MusicParent item to play from when a Song is played from the detail view. Will be null + * if configured to play from the currently shown item. */ val detailPlaybackMode: MusicMode? get() = @@ -329,8 +327,8 @@ class Settings(private val context: Context, private val callback: Callback? = n } /** - * A string of characters representing the desired separator characters to denote - * multi-value tags. + * A string of characters representing the desired separator characters to denote multi-value + * tags. */ var musicSeparators: String? // Differ from convention and store a string of separator characters instead of an int @@ -358,7 +356,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The Song [Sort] mode used in the Home UI. */ + /** The Song [Sort] mode used in the Home UI. */ var libSongSort: Sort get() = Sort.fromIntCode( @@ -371,7 +369,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The Album [Sort] mode used in the Home UI. */ + /** The Album [Sort] mode used in the Home UI. */ var libAlbumSort: Sort get() = Sort.fromIntCode( @@ -384,7 +382,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The Artist [Sort] mode used in the Home UI. */ + /** The Artist [Sort] mode used in the Home UI. */ var libArtistSort: Sort get() = Sort.fromIntCode( @@ -397,7 +395,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The Genre [Sort] mode used in the Home UI. */ + /** The Genre [Sort] mode used in the Home UI. */ var libGenreSort: Sort get() = Sort.fromIntCode( @@ -410,7 +408,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The [Sort] mode used in the Album Detail UI. */ + /** The [Sort] mode used in the Album Detail UI. */ var detailAlbumSort: Sort get() { var sort = @@ -433,7 +431,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The [Sort] mode used in the Artist Detail UI. */ + /** The [Sort] mode used in the Artist Detail UI. */ var detailArtistSort: Sort get() = Sort.fromIntCode( @@ -446,7 +444,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } } - /** The [Sort] mode used in the Genre Detail UI. */ + /** The [Sort] mode used in the Genre Detail UI. */ var detailGenreSort: Sort get() = Sort.fromIntCode( diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt index c0574f646..03ccaa199 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt @@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.google.android.material.transition.MaterialFadeThrough import org.oxycblt.auxio.databinding.FragmentSettingsBinding -import org.oxycblt.auxio.shared.ViewBindingFragment +import org.oxycblt.auxio.ui.ViewBindingFragment /** * A [Fragment] wrapper containing the preference fragment and a companion Toolbar. diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt index 9b33a2738..a25d7a718 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt @@ -114,8 +114,8 @@ constructor( /** * Get the index of the current value. - * @return The index of the current value within [values], or -1 if the [IntListPreference] - * is not set. + * @return The index of the current value within [values], or -1 if the [IntListPreference] is + * not set. */ fun getValueIndex(): Int { val curValue = currentValue @@ -148,9 +148,7 @@ constructor( } } - /** - * Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference]. - */ + /** Copy of ListPreference's [Preference.SummaryProvider] for this [IntListPreference]. */ private inner class IntListSummaryProvider : SummaryProvider { override fun provideSummary(preference: IntListPreference): CharSequence { val index = getValueIndex() diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt index ec95eea08..da944e309 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt @@ -124,8 +124,8 @@ class PreferenceFragment : PreferenceFragmentCompat() { context.getString(R.string.set_key_wipe_state) -> { playbackModel.wipePlaybackState { wiped -> if (wiped) { - // Use the nullable context, as we could try to show a toast when this - // fragment is no longer attached. + // Use the nullable context, as we could try to show a toast when this + // fragment is no longer attached. this.context?.showToast(R.string.lbl_state_wiped) } else { this.context?.showToast(R.string.err_did_not_wipe) @@ -135,8 +135,8 @@ class PreferenceFragment : PreferenceFragmentCompat() { context.getString(R.string.set_key_restore_state) -> playbackModel.tryRestorePlaybackState { restored -> if (restored) { - // Use the nullable context, as we could try to show a toast when this - // fragment is no longer attached. + // Use the nullable context, as we could try to show a toast when this + // fragment is no longer attached. this.context?.showToast(R.string.lbl_state_restored) } else { this.context?.showToast(R.string.err_did_not_restore) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt index 3b799222c..429364a94 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/WrappedDialogPreference.kt @@ -22,8 +22,8 @@ import android.util.AttributeSet import androidx.preference.DialogPreference /** - * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that - * custom dialog preferences are handled. + * Wraps a [DialogPreference] to be instantiatable. This has no purpose other to ensure that custom + * dialog preferences are handled. * @author Alexander Capehart (OxygenCobalt) */ class WrappedDialogPreference diff --git a/app/src/main/java/org/oxycblt/auxio/shared/AuxioAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/shared/AuxioAppBarLayout.kt rename to app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt index effc64de4..f758a1dc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/AuxioAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.ui import android.content.Context import android.util.AttributeSet @@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.coordinatorLayoutBehavior * 1. Lift state failing to update when list data changes. * 2. Expansion causing jumping in [RecyclerView] instances. * - * Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what - * scrolling view to use. Failure to specify this will result in the layout not working. + * Note: This layout relies on [AppBarLayout.liftOnScrollTargetViewId] to figure out what scrolling + * view to use. Failure to specify this will result in the layout not working. * * Derived from Material Files: https://github.com/zhanghai/MaterialFiles * @@ -70,8 +70,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * Expand this [AppBarLayout] with respect to the given [RecyclerView], preventing it from * jumping around. - * @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. - * TODO: Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument? + * @param recycler [RecyclerView] to expand with, or null if one is currently unavailable. TODO: + * Is it possible to use liftOnScrollTargetViewId to avoid the [RecyclerView] argument? */ fun expandWithRecycler(recycler: RecyclerView?) { setExpanded(true) @@ -108,13 +108,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr /** * An [AppBarLayout.OnOffsetChangedListener] that will automatically move the given - * [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view - * is expanding. Will be removed automatically. + * [RecyclerView] as the [AppBarLayout] expands. Should be added right when the view is + * expanding. Will be removed automatically. * @param recycler [RecyclerView] to scroll with the [AppBarLayout]. */ private class ExpansionHackListener(private val recycler: RecyclerView) : OnOffsetChangedListener { - private val offsetAnimationMaxEndTime = (AnimationUtils.currentAnimationTimeMillis() + + private val offsetAnimationMaxEndTime = + (AnimationUtils.currentAnimationTimeMillis() + APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION) private var currentVerticalOffset: Int? = null @@ -123,8 +124,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr AnimationUtils.currentAnimationTimeMillis() > offsetAnimationMaxEndTime) { // AppBarLayout crashes with IndexOutOfBoundsException when a non-last listener // removes itself, so we have to do the removal asynchronously. - appBarLayout.postOnAnimation { - appBarLayout.removeOnOffsetChangedListener(this) } + appBarLayout.postOnAnimation { appBarLayout.removeOnOffsetChangedListener(this) } } // If possible, scroll by the offset delta between this update and the last update. @@ -137,9 +137,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } companion object { - /** - * @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION - */ + /** @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION */ private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600 } } diff --git a/app/src/main/java/org/oxycblt/auxio/shared/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt similarity index 85% rename from app/src/main/java/org/oxycblt/auxio/shared/NavigationViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index 376257fc8..6a460ce9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.ui import androidx.lifecycle.ViewModel import androidx.navigation.NavDirections @@ -25,14 +25,11 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.util.logD -/** - * A [ViewModel] that handles complicated navigation functionality. - */ +/** A [ViewModel] that handles complicated navigation functionality. */ class NavigationViewModel : ViewModel() { private val _mainNavigationAction = MutableStateFlow(null) /** - * Flag for navigation within the main navigation graph. Only intended for use by - * MainFragment. + * Flag for navigation within the main navigation graph. Only intended for use by MainFragment. */ val mainNavigationAction: StateFlow get() = _mainNavigationAction @@ -47,17 +44,17 @@ class NavigationViewModel : ViewModel() { private val _exploreNavigationArtists = MutableStateFlow?>(null) /** - * Variation of [exploreNavigationItem] for situations where the choice of [Artist] - * to navigate to is ambiguous. Only intended for use by MainFragment, as the resolved - * choice will eventually be assigned to [exploreNavigationItem]. + * Variation of [exploreNavigationItem] for situations where the choice of [Artist] to navigate + * to is ambiguous. Only intended for use by MainFragment, as the resolved choice will + * eventually be assigned to [exploreNavigationItem]. */ val exploreNavigationArtists: StateFlow?> get() = _exploreNavigationArtists /** * Navigate to something in the main navigation graph. This can be used by UIs in the explore - * navigation graph to trigger navigation in the higher-level main navigation graph. - * Will do nothing if already navigating. + * navigation graph to trigger navigation in the higher-level main navigation graph. Will do + * nothing if already navigating. * @param action The [MainNavigationAction] to perform. */ fun mainNavigateTo(action: MainNavigationAction) { @@ -81,8 +78,7 @@ class NavigationViewModel : ViewModel() { /** * Navigate to a given [Music] item. Will do nothing if already navigating. - * @param item The [Music] to navigate to. - * TODO: Extend to song properties??? + * @param item The [Music] to navigate to. TODO: Extend to song properties??? */ fun exploreNavigateTo(item: Music) { if (_exploreNavigationItem.value != null) { @@ -96,8 +92,8 @@ class NavigationViewModel : ViewModel() { /** * Navigate to an [Artist] out of a list of [Artist]s, like [exploreNavigateTo]. - * @param artists The [Artist]s to navigate to. In the case of multiple artists, the - * user will be prompted with a choice on which [Artist] to navigate to. + * @param artists The [Artist]s to navigate to. In the case of multiple artists, the user will + * be prompted with a choice on which [Artist] to navigate to. */ fun exploreNavigateTo(artists: List) { if (_exploreNavigationArtists.value != null) { @@ -114,7 +110,7 @@ class NavigationViewModel : ViewModel() { } /** - * Mark that the navigation process within the explore navigation graph (initiated by + * Mark that the navigation process within the explore navigation graph (initiated by * [exploreNavigateTo]) was completed. */ fun finishExploreNavigation() { @@ -126,8 +122,8 @@ class NavigationViewModel : ViewModel() { /** * Represents the possible actions within the main navigation graph. This can be used with - * [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere - * in the app, including outside the main navigation graph. + * [NavigationViewModel] to initiate navigation in the main navigation graph from anywhere in the + * app, including outside the main navigation graph. * @author Alexander Capehart (OxygenCobalt) */ sealed class MainNavigationAction { diff --git a/app/src/main/java/org/oxycblt/auxio/shared/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/shared/ViewBindingDialogFragment.kt rename to app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index f23a3c541..f110330d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.ui import android.os.Bundle import android.view.LayoutInflater @@ -86,8 +86,8 @@ abstract class ViewBindingDialogFragment : DialogFragment() { } /** - * Delegate to automatically create and destroy an object derived from the [ViewBinding]. - * TODO: Phase this out, it's really dumb + * Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO: + * Phase this out, it's really dumb * @param create Block to create the object from the [ViewBinding]. */ fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { @@ -140,9 +140,7 @@ abstract class ViewBindingDialogFragment : DialogFragment() { logD("Fragment destroyed") } - /** - * Internal implementation of [lifecycleObject]. - */ + /** Internal implementation of [lifecycleObject]. */ private data class LifecycleObject(var data: T?, val create: (VB) -> T) { fun populate(binding: VB) { data = create(binding) diff --git a/app/src/main/java/org/oxycblt/auxio/shared/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/shared/ViewBindingFragment.kt rename to app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index 1140d960c..1bc4ae058 100644 --- a/app/src/main/java/org/oxycblt/auxio/shared/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.shared +package org.oxycblt.auxio.ui import android.os.Bundle import android.view.LayoutInflater @@ -76,8 +76,8 @@ abstract class ViewBindingFragment : Fragment() { } /** - * Delegate to automatically create and destroy an object derived from the [ViewBinding]. - * TODO: Phase this out, it's really dumb + * Delegate to automatically create and destroy an object derived from the [ViewBinding]. TODO: + * Phase this out, it's really dumb * @param create Block to create the object from the [ViewBinding]. */ fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { @@ -121,9 +121,7 @@ abstract class ViewBindingFragment : Fragment() { logD("Fragment destroyed") } - /** - * Internal implementation of [lifecycleObject]. - */ + /** Internal implementation of [lifecycleObject]. */ private data class LifecycleObject(var data: T?, val create: (VB) -> T) { fun populate(binding: VB) { data = create(binding) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt rename to app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt index 019c4d537..1be63d5df 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/Accent.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.accent +package org.oxycblt.auxio.ui.accent import android.os.Build import org.oxycblt.auxio.R @@ -120,8 +120,7 @@ class Accent private constructor(val index: Int) : Item { val theme: Int get() = ACCENT_THEMES[index] /** - * The black theme resource for this accent. Identical to [theme], but with a black - * background. + * The black theme resource for this accent. Identical to [theme], but with a black background. */ val blackTheme: Int get() = ACCENT_BLACK_THEMES[index] @@ -137,20 +136,18 @@ class Accent private constructor(val index: Int) : Item { /** * Create a new instance. * @param index The unique number for this particular accent. - * @return A new [Accent] with the specified [index]. If [index] is not within the - * range of valid accents, [index] will be [DEFAULT] instead. + * @return A new [Accent] with the specified [index]. If [index] is not within the range of + * valid accents, [index] will be [DEFAULT] instead. */ fun from(index: Int): Accent { - if (index !in 0 until MAX ) { + if (index !in 0 until MAX) { logW("Accent is out of bounds [idx: $index]") return Accent(DEFAULT) } return Accent(index) } - /** - * The default accent. - */ + /** The default accent. */ val DEFAULT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Use dynamic coloring on devices that support it. @@ -160,9 +157,7 @@ class Accent private constructor(val index: Int) : Item { 5 } - /** - * The amount of valid accents. - */ + /** The amount of valid accents. */ val MAX = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { ACCENT_THEMES.size diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index 803bd1de4..790e9ca54 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.accent +package org.oxycblt.auxio.ui.accent import android.view.View import android.view.ViewGroup diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt similarity index 94% rename from app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt rename to app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index 789ddd2ba..a90ce9720 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.accent +package org.oxycblt.auxio.ui.accent import android.os.Bundle import android.view.LayoutInflater @@ -26,7 +26,7 @@ import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.shared.ViewBindingDialogFragment +import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -35,7 +35,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * A [ViewBindingDialogFragment] that allows the user to configure the current [Accent]. * @author Alexander Capehart (OxygenCobalt) */ -class AccentCustomizeDialog : ViewBindingDialogFragment(), ClickableListListener { +class AccentCustomizeDialog : + ViewBindingDialogFragment(), ClickableListListener { private var accentAdapter = AccentAdapter(this) private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt similarity index 92% rename from app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt rename to app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt index c27a9f583..6b075aaf2 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentGridLayoutManager.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.accent +package org.oxycblt.auxio.ui.accent import android.content.Context import android.util.AttributeSet @@ -27,8 +27,8 @@ import org.oxycblt.auxio.util.getDimenPixels /** * A [GridLayoutManager] that automatically sets the span size in order to use the most possible - * space in the [RecyclerView]. - * Derived from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986 + * space in the [RecyclerView]. Derived from this StackOverflow answer: + * https://stackoverflow.com/a/30256880/14143986 */ class AccentGridLayoutManager( context: Context, diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index af34af6b1..de624a85d 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -47,17 +47,13 @@ import org.oxycblt.auxio.MainActivity val Context.inflater: LayoutInflater get() = LayoutInflater.from(this) -/** - * Whether the device is in night mode or not. - */ +/** Whether the device is in night mode or not. */ val Context.isNight get() = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES -/** - * Whether the device is in landscape mode or not. - */ +/** Whether the device is in landscape mode or not. */ val Context.isLandscape get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE @@ -152,9 +148,7 @@ fun Context.showToast(@StringRes stringRes: Int) { Toast.makeText(applicationContext, getString(stringRes), Toast.LENGTH_SHORT).show() } -/** - * Create a [PendingIntent] that will launch the app activity when launched. - */ +/** Create a [PendingIntent] that will launch the app activity when launched. */ fun Context.newMainPendingIntent(): PendingIntent = PendingIntent.getActivity( this, diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 5611f6b61..1fd65f0bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.util import android.content.Context @@ -50,11 +50,10 @@ import kotlinx.coroutines.launch * Get if this [View] contains the given [PointF], with optional leeway. * @param x The x value of the point to check. * @param y The y value of the point to check. - * @param minTouchTargetSize A minimum size to use when checking the value. - * This can be used to extend the range where a point is considered "contained" - * by the [View] beyond it's actual size. - * @return true if the [PointF] is contained by the view, false otherwise. - * Adapted from AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll + * @param minTouchTargetSize A minimum size to use when checking the value. This can be used to + * extend the range where a point is considered "contained" by the [View] beyond it's actual size. + * @return true if the [PointF] is contained by the view, false otherwise. Adapted from + * AndroidFastScroll: https://github.com/zhanghai/AndroidFastScroll */ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) = isUnderImpl(x, left, right, (parent as View).width, minTouchTargetSize) && @@ -66,8 +65,7 @@ fun View.isUnder(x: Float, y: Float, minTouchTargetSize: Int = 0) = * @param viewStart The start of the view bounds, on the same axis as [position]. * @param viewEnd The end of the view bounds, on the same axis as [position] * @param parentEnd The end of the parent bounds, on the same axis as [position]. - * @param minTouchTargetSize The minimum size to use when checking if the value is - * in range. + * @param minTouchTargetSize The minimum size to use when checking if the value is in range. */ private fun isUnderImpl( position: Float, @@ -98,27 +96,21 @@ private fun isUnderImpl( return position >= touchTargetStart && position < touchTargetEnd } -/** - * Whether this [View] is using an RTL layout direction. - */ +/** Whether this [View] is using an RTL layout direction. */ val View.isRtl: Boolean get() = layoutDirection == View.LAYOUT_DIRECTION_RTL -/** - * Whether this [Drawable] is using an RTL layout direction. - */ +/** Whether this [Drawable] is using an RTL layout direction. */ val Drawable.isRtl: Boolean get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL -/** - * Get a [Context] from a [ViewBinding]'s root [View]. - */ +/** Get a [Context] from a [ViewBinding]'s root [View]. */ val ViewBinding.context: Context get() = root.context /** - * Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on - * one screen. + * Compute if this [RecyclerView] can scroll through their items, or if the items can all fit on one + * screen. */ fun RecyclerView.canScroll() = computeVerticalScrollRange() > height @@ -131,8 +123,8 @@ val View.coordinatorLayoutBehavior: CoordinatorLayout.Behavior? /** * Collect a [StateFlow] into [block] in a lifecycle-aware manner *eventually.* Due to co-routine - * launching, the initializing call will occur ~100ms after draw time. If this is not desirable, - * use [collectImmediately]. + * launching, the initializing call will occur ~100ms after draw time. If this is not desirable, use + * [collectImmediately]. * @param stateFlow The [StateFlow] to collect. * @param block The code to run when the [StateFlow] updates. */ @@ -142,8 +134,8 @@ fun Fragment.collect(stateFlow: StateFlow, block: (T) -> Unit) { /** * Collect a [StateFlow] into a [block] in a lifecycle-aware manner *immediately.* This will - * immediately run an initializing call to ensure the UI is set up before draw-time. Note - * that this will result in two initializing calls. + * immediately run an initializing call to ensure the UI is set up before draw-time. Note that this + * will result in two initializing calls. * @param stateFlow The [StateFlow] to collect. * @param block The code to run when the [StateFlow] updates. */ @@ -153,8 +145,8 @@ fun Fragment.collectImmediately(stateFlow: StateFlow, block: (T) -> Unit) } /** - * Like [collectImmediately], but with two [StateFlow] instances that are collected - * with the same block. + * Like [collectImmediately], but with two [StateFlow] instances that are collected with the same + * block. * @param a The first [StateFlow] to collect. * @param b The second [StateFlow] to collect. * @param block The code to run when either [StateFlow] updates. @@ -173,8 +165,8 @@ fun Fragment.collectImmediately( } /** - * Like [collectImmediately], but with three [StateFlow] instances that are collected - * with the same block. + * Like [collectImmediately], but with three [StateFlow] instances that are collected with the same + * block. * @param a The first [StateFlow] to collect. * @param b The second [StateFlow] to collect. * @param c The third [StateFlow] to collect. @@ -192,9 +184,9 @@ fun Fragment.collectImmediately( } /** - * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. - * This should always been used when launching [Fragment] co-routines was it will not result - * in unexpected behavior. + * Launch a [Fragment] co-routine whenever the [Lifecycle] hits the given [Lifecycle.State]. This + * should always been used when launching [Fragment] co-routines was it will not result in + * unexpected behavior. * @param state The [Lifecycle.State] to launch the co-routine in. * @param block The block to run in the co-routine. * @see repeatOnLifecycle @@ -208,40 +200,36 @@ private fun Fragment.launch( /** * An extension to [viewModels] that automatically provides an - * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] - * is used. + * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. */ inline fun Fragment.androidViewModels() = viewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } /** * An extension to [viewModels] that automatically provides an - * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] - * is used. Note that this implementation is for an [AppCompatActivity], and thus - * makes this functionally equivalent in scope to [androidActivityViewModels]. + * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. Note + * that this implementation is for an [AppCompatActivity], and thus makes this functionally + * equivalent in scope to [androidActivityViewModels]. */ inline fun AppCompatActivity.androidViewModels() = viewModels { ViewModelProvider.AndroidViewModelFactory(application) } /** * An extension to [activityViewModels] that automatically provides an - * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] - * is used. + * [ViewModelProvider.AndroidViewModelFactory]. Use whenever an [AndroidViewModel] is used. */ inline fun Fragment.androidActivityViewModels() = activityViewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } -/** - * The [Context] provided to an [AndroidViewModel]. - */ +/** The [Context] provided to an [AndroidViewModel]. */ inline val AndroidViewModel.context: Context get() = getApplication() /** - * Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] - * is loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor] + * Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is + * loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor] * resources. * @param tableName The name of the table to query all columns in. * @param block The code block to run with the loaded [Cursor]. @@ -250,8 +238,8 @@ inline fun SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) query(tableName, null, null, null, null, null, null)?.use(block) /** - * Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner - * This can be used to prevent [View] elements from intersecting with the navigation bars. + * Get the "System Bar" [Insets] in this [WindowInsets] instance in a version-compatible manner This + * can be used to prevent [View] elements from intersecting with the navigation bars. */ val WindowInsets.systemBarInsetsCompat: Insets get() = @@ -266,9 +254,9 @@ val WindowInsets.systemBarInsetsCompat: Insets /** * Get the "System Gesture" [Insets] in this [WindowInsets] instance in a version-compatible manner - * This can be used to prevent [View] elements from intersecting with the navigation bars and - * their extended gesture hit-boxes. Note that "System Bar" insets will be used if the system - * does not provide gesture insets. + * This can be used to prevent [View] elements from intersecting with the navigation bars and their + * extended gesture hit-boxes. Note that "System Bar" insets will be used if the system does not + * provide gesture insets. */ val WindowInsets.systemGestureInsetsCompat: Insets get() = diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 53f6f8ccf..6e2ecf982 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -53,8 +53,8 @@ fun Long.nonZeroOrNull() = if (this > 0) this else null fun Int.inRangeOrNull(range: IntRange) = if (range.contains(this)) this else null /** - * Lazily set up a reflected field. Automatically handles visibility changes. - * Adapted from Material Files: https://github.com/zhanghai/MaterialFiles + * Lazily set up a reflected field. Automatically handles visibility changes. Adapted from Material + * Files: https://github.com/zhanghai/MaterialFiles * @param clazz The [KClass] to reflect into. * @param field The name of the field to obtain. */ @@ -62,8 +62,8 @@ fun lazyReflectedField(clazz: KClass<*>, field: String) = lazy { clazz.java.getDeclaredField(field).also { it.isAccessible = true } } /** - * Lazily set up a reflected method. Automatically handles visibility changes. - * Adapted from Material Files: https://github.com/zhanghai/MaterialFiles + * Lazily set up a reflected method. Automatically handles visibility changes. Adapted from Material + * Files: https://github.com/zhanghai/MaterialFiles * @param clazz The [KClass] to reflect into. * @param field The name of the method to obtain. */ @@ -72,9 +72,9 @@ fun lazyReflectedMethod(clazz: KClass<*>, method: String) = lazy { } /** - * Assert that the execution is currently on a background thread. This is helpful for - * functions that don't necessarily require suspend, but still want to ensure that they - * are being called with a co-routine. + * Assert that the execution is currently on a background thread. This is helpful for functions that + * don't necessarily require suspend, but still want to ensure that they are being called with a + * co-routine. * @throws IllegalStateException If the execution is not on a background thread. */ fun requireBackgroundThread() { diff --git a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt index 846cc2390..ee447580d 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -52,15 +52,15 @@ fun Any.logW(msg: String) = Log.w(autoTag, msg) fun Any.logE(msg: String) = Log.e(autoTag, msg) /** - * The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" - * if the object does not exist. + * The LogCat-suitable tag for this string. Consists of the object's name, or "Anonymous Object" if + * the object does not exist. */ private val Any.autoTag: String get() = "Auxio.${this::class.simpleName ?: "Anonymous Object"}" /** - * Please don't plagiarize Auxio! - * You are free to remove this as long as you continue to keep your source open. + * Please don't plagiarize Auxio! You are free to remove this as long as you continue to keep your + * source open. */ @Suppress("KotlinConstantConditions") private fun copyleftNotice(): Boolean { diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index d2558edbd..d51a34530 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -35,9 +35,8 @@ import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.logD /** - * A component that manages the "Now Playing" state. - * This is kept separate from the [WidgetProvider] itself to prevent possible memory - * leaks and enable extension to more widgets in the future. + * A component that manages the "Now Playing" state. This is kept separate from the [WidgetProvider] + * itself to prevent possible memory leaks and enable extension to more widgets in the future. * @param context [Context] required to manage AppWidgetProviders. * @author Alexander Capehart (OxygenCobalt) */ @@ -52,9 +51,7 @@ class WidgetComponent(private val context: Context) : playbackManager.addCallback(this) } - /** - * Update [WidgetProvider] with the current playback state. - */ + /** Update [WidgetProvider] with the current playback state. */ fun update() { val song = playbackManager.song if (song == null) { @@ -104,9 +101,7 @@ class WidgetComponent(private val context: Context) : }) } - /** - * Release this instance, preventing any further events from updating the widget instances. - */ + /** Release this instance, preventing any further events from updating the widget instances. */ fun release() { provider.release() settings.release() diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 9c449e404..d37d46648 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -35,8 +35,8 @@ import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.* /** - * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current - * playback state alongside actions to control it. + * The [AppWidgetProvider] for the "Now Playing" widget. This widget shows the current playback + * state alongside actions to control it. * @author Alexander Capehart (OxygenCobalt) */ class WidgetProvider : AppWidgetProvider() { @@ -69,8 +69,7 @@ class WidgetProvider : AppWidgetProvider() { /** * Update the currently shown layout based on the given [WidgetComponent.PlaybackState] * @param context [Context] required to update the widget layout. - * @param state [WidgetComponent.PlaybackState] to show, or null if no playback is going - * on. + * @param state [WidgetComponent.PlaybackState] to show, or null if no playback is going on. */ fun update(context: Context, state: WidgetComponent.PlaybackState?) { if (state == null) { @@ -135,8 +134,8 @@ class WidgetProvider : AppWidgetProvider() { newRemoteViews(context, R.layout.widget_default) /** - * Create and configure a [RemoteViews] for [R.layout.widget_thin], intended for extremely - * small grid sizes on phones in landscape mode. + * Create and configure a [RemoteViews] for [R.layout.widget_thin], intended for extremely small + * grid sizes on phones in landscape mode. * @param context [Context] required to create the [RemoteViews]. */ private fun newThinLayout(context: Context, state: WidgetComponent.PlaybackState) = @@ -157,8 +156,8 @@ class WidgetProvider : AppWidgetProvider() { .setupTimelineControls(context, state) /** - * Create and configure a [RemoteViews] for [R.layout.widget_medium], intended to be - * a taller widget that shows more information about the currently playing song. + * Create and configure a [RemoteViews] for [R.layout.widget_medium], intended to be a taller + * widget that shows more information about the currently playing song. * @param context [Context] required to create the [RemoteViews]. */ private fun newMediumLayout(context: Context, state: WidgetComponent.PlaybackState) = @@ -168,8 +167,8 @@ class WidgetProvider : AppWidgetProvider() { .setupTimelineControls(context, state) /** - * Create and configure a [RemoteViews] for [R.layout.widget_wide], intended to be - * a wider version of [R.layout.widget_small] that shows additional controls. + * Create and configure a [RemoteViews] for [R.layout.widget_wide], intended to be a wider + * version of [R.layout.widget_small] that shows additional controls. * @param context [Context] required to create the [RemoteViews]. */ private fun newWideLayout(context: Context, state: WidgetComponent.PlaybackState) = @@ -179,8 +178,8 @@ class WidgetProvider : AppWidgetProvider() { .setupFullControls(context, state) /** - * Create and configure a [RemoteViews] for [R.layout.widget_large], intended to be - * a wider version of [R.layout.widget_medium] that shows additional controls. + * Create and configure a [RemoteViews] for [R.layout.widget_large], intended to be a wider + * version of [R.layout.widget_medium] that shows additional controls. * @param context [Context] required to create the [RemoteViews]. */ private fun newLargeLayout(context: Context, state: WidgetComponent.PlaybackState) = @@ -197,12 +196,12 @@ class WidgetProvider : AppWidgetProvider() { private fun RemoteViews.setupBar(context: Context): RemoteViews { // Below API 31, enable a rounded bar only if round mode is enabled. // On API 31+, the bar should always be round in order to fit in with other widgets. - val background = if (Settings(context).roundMode && - Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - R.drawable.ui_widget_bar_round - } else { - R.drawable.ui_widget_bar_system - } + val background = + if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + R.drawable.ui_widget_bar_round + } else { + R.drawable.ui_widget_bar_system + } setBackgroundResource(R.id.widget_controls, background) return this } @@ -216,12 +215,12 @@ class WidgetProvider : AppWidgetProvider() { // Below API 31, enable a rounded background only if round mode is enabled. // On API 31+, the background should always be round in order to fit in with other // widgets. - val background = if (Settings(context).roundMode && - Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - R.drawable.ui_widget_bar_round - } else { - R.drawable.ui_widget_bar_system - } + val background = + if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + R.drawable.ui_widget_bar_round + } else { + R.drawable.ui_widget_bar_system + } setBackgroundResource(android.R.id.background, background) return this } @@ -252,8 +251,8 @@ class WidgetProvider : AppWidgetProvider() { } /** - * Set up the album cover, song title, and artist name in a [RemoteViews] layout that - * contains them. + * Set up the album cover, song title, and artist name in a [RemoteViews] layout that contains + * them. * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -303,8 +302,8 @@ class WidgetProvider : AppWidgetProvider() { } /** - * Set up the play/pause and skip previous/next button in a [RemoteViews] layout that - * contains them. + * Set up the play/pause and skip previous/next button in a [RemoteViews] layout that contains + * them. * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -319,15 +318,17 @@ class WidgetProvider : AppWidgetProvider() { // Hook the skip buttons to the respective broadcasts that can be recognized // by PlaybackService. setOnClickPendingIntent( - R.id.widget_skip_prev, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)) + R.id.widget_skip_prev, + context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_PREV)) setOnClickPendingIntent( - R.id.widget_skip_next, context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)) + R.id.widget_skip_next, + context.newBroadcastPendingIntent(PlaybackService.ACTION_SKIP_NEXT)) return this } /** - * Set up the play/pause, skip previous/next, and repeat/shuffle buttons in a [RemoteViews] - * that contains them. + * Set up the play/pause, skip previous/next, and repeat/shuffle buttons in a [RemoteViews] that + * contains them. * @param context [Context] required to set up the view. * @param state Current [WidgetComponent.PlaybackState] to display. */ @@ -370,9 +371,9 @@ class WidgetProvider : AppWidgetProvider() { companion object { /** - * Broadcast when [WidgetProvider] desires to update it's widget with new - * information. Responsible background tasks should intercept this and relay - * the message to [WidgetComponent]. + * Broadcast when [WidgetProvider] desires to update it's widget with new information. + * Responsible background tasks should intercept this and relay the message to + * [WidgetComponent]. */ const val ACTION_WIDGET_UPDATE = BuildConfig.APPLICATION_ID + ".action.WIDGET_UPDATE" } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index ec1b73e13..2baeebbb4 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.oxycblt.auxio.widgets import android.appwidget.AppWidgetManager @@ -9,14 +26,14 @@ import android.widget.RemoteViews import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes +import kotlin.math.sqrt import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent -import kotlin.math.sqrt /** - * Create a [RemoteViews] instance with the specified layout and an automatic click handler - * to open the Auxio activity. + * Create a [RemoteViews] instance with the specified layout and an automatic click handler to open + * the Auxio activity. * @param context [Context] required to create [RemoteViews]. * @param layoutRes Resource ID of the layout to use. Must be compatible with [RemoteViews]. * @return A new [RemoteViews] instance with the specified configuration. @@ -28,8 +45,8 @@ fun newRemoteViews(context: Context, @LayoutRes layoutRes: Int): RemoteViews { } /** - * Get an image size guaranteed to not exceed the [RemoteViews] bitmap memory limit, assuming - * that there is only one image. + * Get an image size guaranteed to not exceed the [RemoteViews] bitmap memory limit, assuming that + * there is only one image. * @param context [Context] required to perform calculation. * @param reduce Optional multiplier to reduce the image size. Recommended value is 2 to avoid * device-specific variations in memory limit. @@ -63,8 +80,8 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) { } /** - * Update the app widget layouts corresponding to the given [AppWidgetProvider] [ComponentName] - * with an adaptive layout, in a version-compatible manner. + * Update the app widget layouts corresponding to the given [AppWidgetProvider] [ComponentName] with + * an adaptive layout, in a version-compatible manner. * @param context [Context] required to backport adaptive layout behavior. * @param component [ComponentName] of the app widget layout to update. * @param views Mapping between different size classes and [RemoteViews] instances. @@ -104,12 +121,14 @@ fun AppWidgetManager.updateAppWidgetCompat( // Find the layout with the greatest area that fits entirely within // the app widget. This is what we will use. Fall back to the smallest layout // otherwise. - val layout = views.keys.filter { it.width <= width && it.height <= height } - .maxByOrNull { it.height * it.width } ?: - views.minBy { it.key.width * it.key.height }.key + val layout = + views.keys + .filter { it.width <= width && it.height <= height } + .maxByOrNull { it.height * it.width } + ?: views.minBy { it.key.width * it.key.height }.key logD("Using layout $layout ${views.contains(layout)}") updateAppWidget(id, views[layout]) } } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/dialog_accent.xml b/app/src/main/res/layout/dialog_accent.xml index 78c46970d..cfbd49d13 100644 --- a/app/src/main/res/layout/dialog_accent.xml +++ b/app/src/main/res/layout/dialog_accent.xml @@ -7,6 +7,6 @@ android:layout_height="wrap_content" android:paddingStart="@dimen/spacing_medium" android:paddingEnd="@dimen/spacing_medium" - app:layoutManager="org.oxycblt.auxio.settings.accent.AccentGridLayoutManager" + app:layoutManager=".ui.accent.AccentGridLayoutManager" tools:itemCount="16" tools:listitem="@layout/item_accent" /> diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index 54d61dc6b..6c99a4f20 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -9,7 +9,7 @@ android:transitionGroup="true" tools:context=".settings.AboutFragment"> - @@ -21,7 +21,7 @@ app:navigationIcon="@drawable/ic_back_24" app:title="@string/lbl_about" /> - + - @@ -37,7 +37,7 @@ app:tabGravity="start" app:tabMode="scrollable" /> - + - @@ -51,7 +51,7 @@ - + - - +