From 33da09a08ae2472cf7a66cd40980011261803253 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 13 Mar 2022 17:32:28 -0600 Subject: [PATCH] all: switch to spotless Switch to the spotless linter with ktfmt used as a backend instead of ktlint. This switch was done for two reasons: 1. ktfmt is more thorough than ktlint 2. License headers can be added more effectively with spotless than the default Android Studio behavior. Dump all of the changes now so I don't have to deal with it over a long period of time. I don't care. --- CHANGELOG.md | 3 + app/NOTICE | 17 + app/build.gradle | 30 +- .../main/java/org/oxycblt/auxio/AuxioApp.kt | 5 +- .../java/org/oxycblt/auxio/MainActivity.kt | 37 +- .../java/org/oxycblt/auxio/MainFragment.kt | 49 +- .../java/org/oxycblt/auxio/accent/Accent.kt | 174 ++--- .../org/oxycblt/auxio/accent/AccentAdapter.kt | 31 +- .../auxio/accent/AccentCustomizeDialog.kt | 12 +- .../auxio/accent/AccentGridLayoutManager.kt | 9 +- .../org/oxycblt/auxio/coil/BaseFetcher.kt | 88 ++- .../java/org/oxycblt/auxio/coil/CoilUtils.kt | 36 +- .../oxycblt/auxio/coil/CrossfadeFactory.kt | 21 +- .../java/org/oxycblt/auxio/coil/Fetchers.kt | 44 +- .../java/org/oxycblt/auxio/coil/MusicKeyer.kt | 21 +- .../oxycblt/auxio/coil/RoundableImageView.kt | 41 +- .../auxio/coil/SquareFrameTransform.kt | 28 +- .../auxio/detail/AlbumDetailFragment.kt | 64 +- .../auxio/detail/ArtistDetailFragment.kt | 57 +- .../auxio/detail/DetailAppBarLayout.kt | 79 +- .../oxycblt/auxio/detail/DetailFragment.kt | 27 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 54 +- .../auxio/detail/GenreDetailFragment.kt | 42 +- .../detail/recycler/AlbumDetailAdapter.kt | 52 +- .../detail/recycler/ArtistDetailAdapter.kt | 64 +- .../detail/recycler/GenreDetailAdapter.kt | 40 +- .../auxio/detail/recycler/Highlightable.kt | 7 +- .../oxycblt/auxio/home/AdaptiveTabStrategy.kt | 31 +- .../oxycblt/auxio/home/EdgeFabContainer.kt | 12 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 136 ++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 40 +- .../fastscroll/FastScrollPopupDrawable.kt | 55 +- .../home/fastscroll/FastScrollRecyclerView.kt | 292 ++++---- .../auxio/home/list/AlbumListFragment.kt | 26 +- .../auxio/home/list/ArtistListFragment.kt | 20 +- .../auxio/home/list/GenreListFragment.kt | 20 +- .../auxio/home/list/HomeListFragment.kt | 18 +- .../auxio/home/list/SongListFragment.kt | 22 +- .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 60 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 17 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 46 +- .../auxio/home/tabs/TabDragCallback.kt | 20 +- .../java/org/oxycblt/auxio/music/Models.kt | 147 ++-- .../org/oxycblt/auxio/music/MusicLoader.kt | 703 +++++++++++------- .../org/oxycblt/auxio/music/MusicStore.kt | 121 ++- .../org/oxycblt/auxio/music/MusicUtils.kt | 33 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 7 +- .../auxio/music/excluded/ExcludedDatabase.kt | 31 +- .../auxio/music/excluded/ExcludedDialog.kt | 26 +- .../music/excluded/ExcludedEntryAdapter.kt | 23 +- .../auxio/music/excluded/ExcludedViewModel.kt | 40 +- .../oxycblt/auxio/playback/PlaybackBarView.kt | 64 +- .../oxycblt/auxio/playback/PlaybackButton.kt | 76 +- .../auxio/playback/PlaybackFragment.kt | 34 +- .../oxycblt/auxio/playback/PlaybackLayout.kt | 331 +++++---- .../oxycblt/auxio/playback/PlaybackSeekBar.kt | 32 +- .../auxio/playback/PlaybackViewModel.kt | 120 ++- .../auxio/playback/queue/QueueAdapter.kt | 47 +- .../auxio/playback/queue/QueueDragCallback.kt | 52 +- .../auxio/playback/queue/QueueFragment.kt | 7 +- .../oxycblt/auxio/playback/state/LoopMode.kt | 16 +- .../auxio/playback/state/PlaybackMode.kt | 3 +- .../playback/state/PlaybackStateDatabase.kt | 119 ++- .../playback/state/PlaybackStateManager.kt | 200 ++--- .../auxio/playback/system/AudioReactor.kt | 124 ++- .../playback/system/MediaButtonReceiver.kt | 33 +- .../playback/system/PlaybackNotification.kt | 75 +- .../auxio/playback/system/PlaybackService.kt | 120 ++- .../system/PlaybackSessionConnector.kt | 99 ++- .../auxio/playback/system/ReplayGainMode.kt | 29 +- .../org/oxycblt/auxio/search/SearchAdapter.kt | 28 +- .../oxycblt/auxio/search/SearchFragment.kt | 80 +- .../oxycblt/auxio/search/SearchViewModel.kt | 59 +- .../oxycblt/auxio/settings/AboutFragment.kt | 36 +- .../oxycblt/auxio/settings/SettingsCompat.kt | 7 +- .../auxio/settings/SettingsFragment.kt | 7 +- .../auxio/settings/SettingsListFragment.kt | 111 ++- .../oxycblt/auxio/settings/SettingsManager.kt | 109 ++- .../auxio/settings/pref/IntListPrefDialog.kt | 19 +- .../auxio/settings/pref/IntListPreference.kt | 31 +- .../auxio/settings/pref/M3SwitchPreference.kt | 25 +- .../java/org/oxycblt/auxio/ui/ActionMenu.kt | 54 +- .../java/org/oxycblt/auxio/ui/DiffCallback.kt | 7 +- .../java/org/oxycblt/auxio/ui/DisplayMode.kt | 45 +- .../org/oxycblt/auxio/ui/EdgeAppBarLayout.kt | 40 +- .../oxycblt/auxio/ui/EdgeCoordinatorLayout.kt | 16 +- .../org/oxycblt/auxio/ui/EdgeRecyclerView.kt | 16 +- .../org/oxycblt/auxio/ui/LifecycleDialog.kt | 7 +- .../main/java/org/oxycblt/auxio/ui/Sort.kt | 73 +- .../java/org/oxycblt/auxio/ui/ViewHolders.kt | 113 +-- .../org/oxycblt/auxio/util/ContextUtil.kt | 86 +-- .../java/org/oxycblt/auxio/util/DbUtil.kt | 11 +- .../java/org/oxycblt/auxio/util/LogUtil.kt | 29 +- .../java/org/oxycblt/auxio/util/ViewUtil.kt | 78 +- .../java/org/oxycblt/auxio/widgets/Forms.kt | 91 +-- .../oxycblt/auxio/widgets/WidgetController.kt | 10 +- .../oxycblt/auxio/widgets/WidgetProvider.kt | 72 +- .../org/oxycblt/auxio/widgets/WidgetState.kt | 3 +- build.gradle | 1 + 99 files changed, 2829 insertions(+), 3094 deletions(-) create mode 100644 app/NOTICE diff --git a/CHANGELOG.md b/CHANGELOG.md index cf657f5de..febae9e63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## dev [v2.2.3, v2.3.0, or v3.0.0] +#### Dev/Meta +- Switched to spotless and ktfmt instead of ktlint + ## v2.2.2 #### What's New - New spanish translations and metadata [courtesy of n-berenice] diff --git a/app/NOTICE b/app/NOTICE new file mode 100644 index 000000000..dc9b86ca7 --- /dev/null +++ b/app/NOTICE @@ -0,0 +1,17 @@ +/* + * Copyright (c) $today.year 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 . + */ + diff --git a/app/build.gradle b/app/build.gradle index 378a4bf6e..a5ba391eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ apply plugin: "com.android.application" apply plugin: "kotlin-android" apply plugin: "kotlin-kapt" apply plugin: "androidx.navigation.safeargs.kotlin" +apply plugin: "com.diffplug.spotless" android { compileSdkVersion 32 @@ -45,12 +46,8 @@ android { } } -configurations { - ktlint -} - afterEvaluate { - preDebugBuild.dependsOn ktlintFormat + preDebugBuild.dependsOn spotlessApply } dependencies { @@ -104,24 +101,13 @@ dependencies { // Material implementation "com.google.android.material:material:1.6.0-alpha03" - - // --- DEBUG --- - - // Lint - ktlint "com.pinterest:ktlint:0.44.0" } -task ktlint(type: JavaExec, group: "verification") { - description = "Check Kotlin code style." - mainClass.set("com.pinterest.ktlint.Main") - classpath = configurations.ktlint - args "src/**/*.kt" -} -check.dependsOn ktlint +spotless { + kotlin { + target "src/**/*.kt" -task ktlintFormat(type: JavaExec, group: "formatting") { - description = "Fix Kotlin code style deviations." - mainClass.set("com.pinterest.ktlint.Main") - classpath = configurations.ktlint - args "-F", "src/**/*.kt" + ktfmt('0.30').dropboxStyle() + licenseHeaderFile("NOTICE") + } } diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index b12f32a4f..e69342276 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AuxioApp.kt is part of Auxio. * * 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 @@ -15,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 import android.app.Application @@ -31,10 +30,12 @@ import org.oxycblt.auxio.settings.SettingsManager /** * TODO: Plan for a general UI rework + * ``` * - Refactor fragment class * - Remove databinding and dedup layouts * - Rework RecyclerView management and item dragging * - Rework sealed classes to minimize whens and maximize overrides + * ``` */ @Suppress("UNUSED") class AuxioApp : Application(), ImageLoaderFactory { diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index a0d47383b..9c294b41d 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MainActivity.kt is part of Auxio. * * 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 @@ -15,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 import android.content.Intent @@ -39,10 +38,8 @@ import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * The single [AppCompatActivity] for Auxio. - * TODO: Add a new view for crashes with a stack trace - * TODO: Custom language support - * TODO: Rework menus [perhaps add multi-select] + * The single [AppCompatActivity] for Auxio. TODO: Add a new view for crashes with a stack trace + * TODO: Custom language support TODO: Rework menus [perhaps add multi-select] */ class MainActivity : AppCompatActivity() { private val playbackModel: PlaybackViewModel by viewModels() @@ -52,9 +49,8 @@ class MainActivity : AppCompatActivity() { setupTheme() - val binding = DataBindingUtil.setContentView( - this, R.layout.activity_main - ) + val binding = + DataBindingUtil.setContentView(this, R.layout.activity_main) applyEdgeToEdgeWindow(binding) @@ -82,9 +78,7 @@ class MainActivity : AppCompatActivity() { if (action == Intent.ACTION_VIEW && !isConsumed) { // Mark the intent as used so this does not fire again intent.putExtra(KEY_INTENT_USED, true) - intent.data?.let { fileUri -> - playbackModel.playWithUri(fileUri, this) - } + intent.data?.let { fileUri -> playbackModel.playWithUri(fileUri, this) } } } } @@ -129,12 +123,10 @@ class MainActivity : AppCompatActivity() { WindowInsets.Builder() .setInsets( WindowInsets.Type.systemBars(), - insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()) - ) + insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())) .setInsets( WindowInsets.Type.systemGestures(), - insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures()) - ) + insets.getInsetsIgnoringVisibility(WindowInsets.Type.systemGestures())) .build() .applyLeftRightInsets(binding) } @@ -144,12 +136,10 @@ class MainActivity : AppCompatActivity() { @Suppress("DEPRECATION") binding.root.apply { - systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE + systemUiVisibility = + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_STABLE - setOnApplyWindowInsetsListener { _, insets -> - insets.applyLeftRightInsets(binding) - } + setOnApplyWindowInsetsListener { _, insets -> insets.applyLeftRightInsets(binding) } } } } @@ -157,10 +147,7 @@ class MainActivity : AppCompatActivity() { private fun WindowInsets.applyLeftRightInsets(binding: ViewBinding): WindowInsets { val bars = systemBarInsetsCompat - binding.root.updatePadding( - left = bars.left, - right = bars.right - ) + binding.root.updatePadding(left = bars.left, right = bars.right) return replaceSystemBarInsetsCompat(0, bars.top, 0, bars.bottom) } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index ac0657cf3..1594a94c5 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MainFragment.kt is part of Auxio. * * 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 @@ -15,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 import android.Manifest @@ -39,10 +38,10 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW /** - * A wrapper around the home fragment that shows the playback fragment and controls - * the more high-level navigation features. - * @author OxygenCobalt - * TODO: Add a new view with a stack trace whenever the music loading process fails. + * A wrapper around the home fragment that shows the playback fragment and controls the more + * high-level navigation features. + * @author OxygenCobalt TODO: Add a new view with a stack trace whenever the music loading process + * fails. */ class MainFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() @@ -58,22 +57,18 @@ class MainFragment : Fragment() { val binding = FragmentMainBinding.inflate(inflater) // Build the permission launcher here as you can only do it in onCreateView/onCreate - val permLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { - musicModel.reloadMusic(requireContext()) - } + val permLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + musicModel.reloadMusic(requireContext()) + } // --- UI SETUP --- binding.lifecycleOwner = viewLifecycleOwner - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - Callback(binding).also { - callback = it - } - ) + requireActivity() + .onBackPressedDispatcher + .addCallback(viewLifecycleOwner, Callback(binding).also { callback = it }) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // Auxio's layout completely breaks down when it's window is resized too small, @@ -110,15 +105,15 @@ class MainFragment : Fragment() { is MusicStore.Response.Err -> { logW("Received Error") - val errorRes = when (response.kind) { - MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music - MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms - MusicStore.ErrorKind.FAILED -> R.string.err_load_failed - } + val errorRes = + when (response.kind) { + MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music + MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms + MusicStore.ErrorKind.FAILED -> R.string.err_load_failed + } - val snackbar = Snackbar.make( - binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE - ) + val snackbar = + Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE) when (response.kind) { MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> { @@ -126,7 +121,6 @@ class MainFragment : Fragment() { musicModel.reloadMusic(requireContext()) } } - MusicStore.ErrorKind.NO_PERMS -> { snackbar.setAction(R.string.lbl_grant) { permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) @@ -164,7 +158,8 @@ class MainFragment : Fragment() { if (!binding.playbackLayout.collapse()) { val navController = binding.exploreNavHost.findNavController() - if (navController.currentDestination?.id == navController.graph.startDestinationId) { + if (navController.currentDestination?.id == + navController.graph.startDestinationId) { isEnabled = false requireActivity().onBackPressed() isEnabled = true diff --git a/app/src/main/java/org/oxycblt/auxio/accent/Accent.kt b/app/src/main/java/org/oxycblt/auxio/accent/Accent.kt index e496f8193..20ecf8149 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/Accent.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/Accent.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Accent.kt is part of Auxio. * * 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 @@ -15,103 +14,112 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.accent import org.oxycblt.auxio.R -val ACCENT_COUNT: Int get() = ACCENT_NAMES.size +val ACCENT_COUNT: Int + get() = ACCENT_NAMES.size -private val ACCENT_NAMES = arrayOf( - R.string.clr_red, - R.string.clr_pink, - R.string.clr_purple, - R.string.clr_deep_purple, - R.string.clr_indigo, - R.string.clr_blue, - R.string.clr_deep_blue, - R.string.clr_cyan, - R.string.clr_teal, - R.string.clr_green, - R.string.clr_deep_green, - R.string.clr_lime, - R.string.clr_yellow, - R.string.clr_orange, - R.string.clr_brown, - R.string.clr_grey, -) +private val ACCENT_NAMES = + arrayOf( + R.string.clr_red, + R.string.clr_pink, + R.string.clr_purple, + R.string.clr_deep_purple, + R.string.clr_indigo, + R.string.clr_blue, + R.string.clr_deep_blue, + R.string.clr_cyan, + R.string.clr_teal, + R.string.clr_green, + R.string.clr_deep_green, + R.string.clr_lime, + R.string.clr_yellow, + R.string.clr_orange, + R.string.clr_brown, + R.string.clr_grey, + ) -private val ACCENT_THEMES = arrayOf( - R.style.Theme_Auxio_Red, - R.style.Theme_Auxio_Pink, - R.style.Theme_Auxio_Purple, - R.style.Theme_Auxio_DeepPurple, - R.style.Theme_Auxio_Indigo, - R.style.Theme_Auxio_Blue, - R.style.Theme_Auxio_DeepBlue, - R.style.Theme_Auxio_Cyan, - R.style.Theme_Auxio_Teal, - R.style.Theme_Auxio_Green, - R.style.Theme_Auxio_DeepGreen, - R.style.Theme_Auxio_Lime, - R.style.Theme_Auxio_Yellow, - R.style.Theme_Auxio_Orange, - R.style.Theme_Auxio_Brown, - R.style.Theme_Auxio_Grey, -) +private val ACCENT_THEMES = + arrayOf( + R.style.Theme_Auxio_Red, + R.style.Theme_Auxio_Pink, + R.style.Theme_Auxio_Purple, + R.style.Theme_Auxio_DeepPurple, + R.style.Theme_Auxio_Indigo, + R.style.Theme_Auxio_Blue, + R.style.Theme_Auxio_DeepBlue, + R.style.Theme_Auxio_Cyan, + R.style.Theme_Auxio_Teal, + R.style.Theme_Auxio_Green, + R.style.Theme_Auxio_DeepGreen, + R.style.Theme_Auxio_Lime, + R.style.Theme_Auxio_Yellow, + R.style.Theme_Auxio_Orange, + R.style.Theme_Auxio_Brown, + R.style.Theme_Auxio_Grey, + ) -private val ACCENT_BLACK_THEMES = arrayOf( - R.style.Theme_Auxio_Black_Red, - R.style.Theme_Auxio_Black_Pink, - R.style.Theme_Auxio_Black_Purple, - R.style.Theme_Auxio_Black_DeepPurple, - R.style.Theme_Auxio_Black_Indigo, - R.style.Theme_Auxio_Black_Blue, - R.style.Theme_Auxio_Black_DeepBlue, - R.style.Theme_Auxio_Black_Cyan, - R.style.Theme_Auxio_Black_Teal, - R.style.Theme_Auxio_Black_Green, - R.style.Theme_Auxio_Black_DeepGreen, - R.style.Theme_Auxio_Black_Lime, - R.style.Theme_Auxio_Black_Yellow, - R.style.Theme_Auxio_Black_Orange, - R.style.Theme_Auxio_Black_Brown, - R.style.Theme_Auxio_Black_Grey, -) +private val ACCENT_BLACK_THEMES = + arrayOf( + R.style.Theme_Auxio_Black_Red, + R.style.Theme_Auxio_Black_Pink, + R.style.Theme_Auxio_Black_Purple, + R.style.Theme_Auxio_Black_DeepPurple, + R.style.Theme_Auxio_Black_Indigo, + R.style.Theme_Auxio_Black_Blue, + R.style.Theme_Auxio_Black_DeepBlue, + R.style.Theme_Auxio_Black_Cyan, + R.style.Theme_Auxio_Black_Teal, + R.style.Theme_Auxio_Black_Green, + R.style.Theme_Auxio_Black_DeepGreen, + R.style.Theme_Auxio_Black_Lime, + R.style.Theme_Auxio_Black_Yellow, + R.style.Theme_Auxio_Black_Orange, + R.style.Theme_Auxio_Black_Brown, + R.style.Theme_Auxio_Black_Grey, + ) -private val ACCENT_PRIMARY_COLORS = arrayOf( - R.color.red_primary, - R.color.pink_primary, - R.color.purple_primary, - R.color.deep_purple_primary, - R.color.indigo_primary, - R.color.blue_primary, - R.color.deep_blue_primary, - R.color.cyan_primary, - R.color.teal_primary, - R.color.green_primary, - R.color.deep_green_primary, - R.color.lime_primary, - R.color.yellow_primary, - R.color.orange_primary, - R.color.brown_primary, - R.color.grey_primary, -) +private val ACCENT_PRIMARY_COLORS = + arrayOf( + R.color.red_primary, + R.color.pink_primary, + R.color.purple_primary, + R.color.deep_purple_primary, + R.color.indigo_primary, + R.color.blue_primary, + R.color.deep_blue_primary, + R.color.cyan_primary, + R.color.teal_primary, + R.color.green_primary, + R.color.deep_green_primary, + R.color.lime_primary, + R.color.yellow_primary, + R.color.orange_primary, + R.color.brown_primary, + R.color.grey_primary, + ) /** - * The data object for an accent. In the UI this is known as a "Color Scheme." - * This can be nominally used to gleam some attributes about a given color scheme, but this - * is not recommended. Attributes are the better option in nearly all cases. + * The data object for an accent. In the UI this is known as a "Color Scheme." This can be nominally + * used to gleam some attributes about a given color scheme, but this is not recommended. Attributes + * are the better option in nearly all cases. * - * @property name The name of this accent + * @property name The name of this accent * @property theme The theme resource for this accent * @property blackTheme The black theme resource for this accent * @property primary The primary color resource for this accent * @author OxygenCobalt */ data class Accent(val index: Int) { - val name: Int get() = ACCENT_NAMES[index] - val theme: Int get() = ACCENT_THEMES[index] - val blackTheme: Int get() = ACCENT_BLACK_THEMES[index] - val primary: Int get() = ACCENT_PRIMARY_COLORS[index] + val name: Int + get() = ACCENT_NAMES[index] + val theme: Int + get() = ACCENT_THEMES[index] + val blackTheme: Int + get() = ACCENT_BLACK_THEMES[index] + val primary: Int + get() = ACCENT_PRIMARY_COLORS[index] } diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt index 0eaa56242..18a6fb486 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentAdapter.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AccentAdapter.kt is part of Auxio. * * 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 @@ -15,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.accent import android.view.ViewGroup @@ -33,10 +32,8 @@ import org.oxycblt.auxio.util.stateList * @author OxygenCobalt * @param onSelect What to do when an accent is selected. */ -class AccentAdapter( - private var curAccent: Accent, - private val onSelect: (accent: Accent) -> Unit -) : RecyclerView.Adapter() { +class AccentAdapter(private var curAccent: Accent, private val onSelect: (accent: Accent) -> Unit) : + RecyclerView.Adapter() { private var selectedViewHolder: ViewHolder? = null override fun getItemCount(): Int = ACCENT_COUNT @@ -54,9 +51,8 @@ class AccentAdapter( onSelect(accent) } - inner class ViewHolder( - private val binding: ItemAccentBinding - ) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(private val binding: ItemAccentBinding) : + RecyclerView.ViewHolder(binding.root) { fun bind(accent: Accent) { setSelected(accent == curAccent) @@ -77,14 +73,15 @@ class AccentAdapter( val context = binding.accent.context binding.accent.isEnabled = !isSelected - binding.accent.imageTintList = if (isSelected) { - // Switch out the currently selected ViewHolder with this one. - selectedViewHolder?.setSelected(false) - selectedViewHolder = this - context.getAttrColorSafe(R.attr.colorSurface).stateList - } else { - context.getColorSafe(android.R.color.transparent).stateList - } + binding.accent.imageTintList = + if (isSelected) { + // Switch out the currently selected ViewHolder with this one. + selectedViewHolder?.setSelected(false) + selectedViewHolder = this + context.getAttrColorSafe(R.attr.colorSurface).stateList + } else { + context.getColorSafe(android.R.color.transparent).stateList + } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt index 6c2321e5a..ed15a5bb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentCustomizeDialog.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AccentDialog.kt is part of Auxio. * * 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 @@ -15,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.accent import android.os.Bundle @@ -52,10 +51,11 @@ class AccentCustomizeDialog : LifecycleDialog() { // --- UI SETUP --- binding.accentRecycler.apply { - adapter = AccentAdapter(pendingAccent) { accent -> - logD("Switching selected accent to $accent") - pendingAccent = accent - } + adapter = + AccentAdapter(pendingAccent) { accent -> + logD("Switching selected accent to $accent") + pendingAccent = accent + } } logD("Dialog created") diff --git a/app/src/main/java/org/oxycblt/auxio/accent/AccentGridLayoutManager.kt b/app/src/main/java/org/oxycblt/auxio/accent/AccentGridLayoutManager.kt index 48ed253ee..ad9c91db0 100644 --- a/app/src/main/java/org/oxycblt/auxio/accent/AccentGridLayoutManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/accent/AccentGridLayoutManager.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AutoGridLayoutManager.kt is part of Auxio. * * 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 @@ -15,20 +14,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.accent import android.content.Context import android.util.AttributeSet import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.util.pxOfDp import kotlin.math.max +import org.oxycblt.auxio.util.pxOfDp /** * A sub-class of [GridLayoutManager] that automatically sets the spans so that they fit the width - * of the RecyclerView. - * Adapted from this StackOverflow answer: https://stackoverflow.com/a/30256880/14143986 + * of the RecyclerView. Adapted from this StackOverflow answer: + * https://stackoverflow.com/a/30256880/14143986 */ class AccentGridLayoutManager( context: Context, diff --git a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt index 71801b6d1..1aa9ddc8b 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.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.coil import android.content.Context @@ -5,6 +22,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.media.MediaMetadataRetriever +import android.util.Size as AndroidSize import androidx.core.graphics.drawable.toDrawable import coil.decode.DataSource import coil.decode.ImageSource @@ -20,6 +38,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 okio.buffer @@ -28,22 +48,17 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -import java.io.ByteArrayInputStream -import java.io.InputStream -import android.util.Size as AndroidSize /** * The base implementation for all image fetchers in Auxio. - * @author OxygenCobalt - * TODO: Artist images + * @author OxygenCobalt TODO: Artist images */ abstract class BaseFetcher : Fetcher { private val settingsManager = SettingsManager.getInstance() /** - * Fetch the artwork of an [album]. - * This call respects user configuration and has proper redundancy in the case that - * an API fails to load. + * Fetch the artwork of an [album]. This call respects user configuration and has proper + * redundancy in the case that an API fails to load. */ protected suspend fun fetchArt(context: Context, album: Album): InputStream? { if (!settingsManager.showCovers) { @@ -67,9 +82,7 @@ abstract class BaseFetcher : Fetcher { val uri = data.albumCoverUri // Eliminate any chance that this blocking call might mess up the cancellation process - return withContext(Dispatchers.IO) { - context.contentResolver.openInputStream(uri) - } + return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } } private suspend fun fetchQualityCovers(context: Context, album: Album): InputStream? { @@ -115,17 +128,13 @@ abstract class BaseFetcher : Fetcher { // Get the embedded picture from MediaMetadataRetriever, which will return a full // ByteArray of the cover without any compression artifacts. // If its null [i.e there is no embedded cover], than just ignore it and move on - return ext.embeddedPicture?.let { coverBytes -> - ByteArrayInputStream(coverBytes) - } + return ext.embeddedPicture?.let { coverBytes -> ByteArrayInputStream(coverBytes) } } } private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? { val uri = album.songs[0].uri - val future = MetadataRetriever.retrieveMetadata( - context, MediaItem.fromUri(uri) - ) + val future = MetadataRetriever.retrieveMetadata(context, MediaItem.fromUri(uri)) // future.get is a blocking call that makes us spin until the future is done. // This is bad for a co-routine, as it prevents cancellation and by extension @@ -133,13 +142,14 @@ abstract class BaseFetcher : Fetcher { // To fix this we wrap this around in a withContext call to make it suspend and make // sure that the runner can do other coroutines. @Suppress("BlockingMethodInNonBlockingContext") - val tracks = withContext(Dispatchers.IO) { - try { - future.get() - } catch (e: Exception) { - null + val tracks = + withContext(Dispatchers.IO) { + try { + future.get() + } catch (e: Exception) { + null + } } - } if (tracks == null || tracks.isEmpty) { // Unrecognized format. This is expected, as ExoPlayer only supports a @@ -201,14 +211,17 @@ abstract class BaseFetcher : Fetcher { * Create a mosaic image from multiple streams of image data, Code adapted from Phonograph * https://github.com/kabouzeid/Phonograph */ - protected suspend fun createMosaic(context: Context, streams: List, size: Size): FetchResult? { + protected suspend fun createMosaic( + context: Context, + streams: List, + size: Size + ): FetchResult? { if (streams.size < 4) { return streams.firstOrNull()?.let { stream -> return SourceResult( source = ImageSource(stream.source().buffer(), context), mimeType = null, - dataSource = DataSource.DISK - ) + dataSource = DataSource.DISK) } } @@ -216,15 +229,11 @@ abstract class BaseFetcher : Fetcher { // get a symmetrical mosaic [and to prevent bugs]. If there is no size, default to a // 512x512 mosaic. val mosaicSize = AndroidSize(size.width.mosaicSize(), size.height.mosaicSize()) - val mosaicFrameSize = Size( - Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2) - ) + val mosaicFrameSize = + Size(Dimension(mosaicSize.width / 2), Dimension(mosaicSize.height / 2)) - val mosaicBitmap = Bitmap.createBitmap( - mosaicSize.width, - mosaicSize.height, - Bitmap.Config.ARGB_8888 - ) + val mosaicBitmap = + Bitmap.createBitmap(mosaicSize.width, mosaicSize.height, Bitmap.Config.ARGB_8888) val canvas = Canvas(mosaicBitmap) var x = 0 @@ -239,11 +248,9 @@ abstract class BaseFetcher : Fetcher { // Run the bitmap through a transform to make sure it's a square of the desired // resolution. - val bitmap = SquareFrameTransform.INSTANCE - .transform( - BitmapFactory.decodeStream(stream), - mosaicFrameSize - ) + val bitmap = + SquareFrameTransform.INSTANCE.transform( + BitmapFactory.decodeStream(stream), mosaicFrameSize) canvas.drawBitmap(bitmap, x.toFloat(), y.toFloat(), null) @@ -261,8 +268,7 @@ abstract class BaseFetcher : Fetcher { return DrawableResult( drawable = mosaicBitmap.toDrawable(context.resources), isSampled = true, - dataSource = DataSource.DISK - ) + dataSource = DataSource.DISK) } private fun Dimension.mosaicSize(): Int { diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt index 256ff621f..33aa4bda3 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CoilUtils.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * CoilUtils.kt is part of Auxio. * * 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 @@ -15,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.coil import android.content.Context @@ -38,27 +37,19 @@ import org.oxycblt.auxio.music.Song // --- BINDING ADAPTERS --- -/** - * Bind the album art for a [song]. - */ +/** Bind the album art for a [song]. */ @BindingAdapter("albumArt") fun ImageView.bindAlbumArt(song: Song?) = load(song, R.drawable.ic_album) -/** - * Bind the album art for an [album]. - */ +/** Bind the album art for an [album]. */ @BindingAdapter("albumArt") fun ImageView.bindAlbumArt(album: Album?) = load(album, R.drawable.ic_album) -/** - * Bind the image for an [artist] - */ +/** Bind the image for an [artist] */ @BindingAdapter("artistImage") fun ImageView.bindArtistImage(artist: Artist?) = load(artist, R.drawable.ic_artist) -/** - * Bind the image for a [genre] - */ +/** Bind the image for a [genre] */ @BindingAdapter("genreImage") fun ImageView.bindGenreImage(genre: Genre?) = load(genre, R.drawable.ic_genre) @@ -74,23 +65,14 @@ fun ImageView.load(music: T?, @DrawableRes error: Int) { /** * Get a bitmap for a [song]. [onDone] will be called with the loaded bitmap, or null if loading - * failed/shouldn't occur. - * **This not meant for UIs, instead use the Binding Adapters.** + * failed/shouldn't occur. **This not meant for UIs, instead use the Binding Adapters.** */ -fun loadBitmap( - context: Context, - song: Song, - onDone: (Bitmap?) -> Unit -) { +fun loadBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) { context.imageLoader.enqueue( ImageRequest.Builder(context) .data(song.album) .size(Size.ORIGINAL) .transformations(SquareFrameTransform()) - .target( - onError = { onDone(null) }, - onSuccess = { onDone(it.toBitmap()) } - ) - .build() - ) + .target(onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) }) + .build()) } diff --git a/app/src/main/java/org/oxycblt/auxio/coil/CrossfadeFactory.kt b/app/src/main/java/org/oxycblt/auxio/coil/CrossfadeFactory.kt index 8b2f03716..a5b1e730c 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/CrossfadeFactory.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/CrossfadeFactory.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.coil import coil.decode.DataSource @@ -9,8 +26,8 @@ import coil.transition.Transition import coil.transition.TransitionTarget /** - * A copy of [CrossfadeTransition.Factory] that applies a transition to error results. - * You know. Like they used to. + * A copy of [CrossfadeTransition.Factory] that applies a transition to error results. You know. + * Like they used to. * @author Coil Team */ class CrossfadeFactory : Transition.Factory { diff --git a/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt b/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt index 7e56c4ddf..387ac275c 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/Fetchers.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Fetchers.kt is part of Auxio. * * 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 @@ -15,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.coil import android.content.Context @@ -27,6 +26,7 @@ import coil.fetch.Fetcher import coil.fetch.SourceResult import coil.request.Options import coil.size.Size +import kotlin.math.min import okio.buffer import okio.source import org.oxycblt.auxio.music.Album @@ -34,23 +34,19 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.Sort -import kotlin.math.min /** * Fetcher that returns the album art for a given [Album] or [Song], depending on the factory used. * @author OxygenCobalt */ -class AlbumArtFetcher private constructor( - private val context: Context, - private val album: Album -) : BaseFetcher() { +class AlbumArtFetcher private constructor(private val context: Context, private val album: Album) : + BaseFetcher() { override suspend fun fetch(): FetchResult? { return fetchArt(context, album)?.let { stream -> SourceResult( source = ImageSource(stream.source().buffer(), context), mimeType = null, - dataSource = DataSource.DISK - ) + dataSource = DataSource.DISK) } } @@ -71,17 +67,15 @@ class AlbumArtFetcher private constructor( * Fetcher that fetches the image for an [Artist] * @author OxygenCobalt */ -class ArtistImageFetcher private constructor( +class ArtistImageFetcher +private constructor( private val context: Context, private val size: Size, private val artist: Artist, ) : BaseFetcher() { override suspend fun fetch(): FetchResult? { - val albums = Sort.ByName(true) - .sortAlbums(artist.albums) - val results = albums.mapAtMost(4) { album -> - fetchArt(context, album) - } + val albums = Sort.ByName(true).sortAlbums(artist.albums) + val results = albums.mapAtMost(4) { album -> fetchArt(context, album) } return createMosaic(context, results, size) } @@ -97,7 +91,8 @@ class ArtistImageFetcher private constructor( * Fetcher that fetches the image for a [Genre] * @author OxygenCobalt */ -class GenreImageFetcher private constructor( +class GenreImageFetcher +private constructor( private val context: Context, private val size: Size, private val genre: Genre, @@ -105,9 +100,7 @@ class GenreImageFetcher private constructor( override suspend fun fetch(): FetchResult? { // We don't need to sort here, as the way we val albums = genre.songs.groupBy { it.album }.keys - val results = albums.mapAtMost(4) { album -> - fetchArt(context, album) - } + val results = albums.mapAtMost(4) { album -> fetchArt(context, album) } return createMosaic(context, results, size) } @@ -120,10 +113,13 @@ class GenreImageFetcher private constructor( } /** - * Map at most [n] items from a collection. [transform] is called for each item that is eligible. - * If null is returned, then that item will be skipped. + * Map at most [n] items from a collection. [transform] is called for each item that is eligible. If + * null is returned, then that item will be skipped. */ -private inline fun Collection.mapAtMost(n: Int, transform: (T) -> R?): List { +private inline fun Collection.mapAtMost( + n: Int, + transform: (T) -> R? +): List { val until = min(size, n) val out = mutableListOf() @@ -132,9 +128,7 @@ private inline fun Collection.mapAtMost(n: Int, transform: break } - transform(item)?.let { - out.add(it) - } + transform(item)?.let { out.add(it) } } return out diff --git a/app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.kt b/app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.kt index bcc7e1902..0b9c58904 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/MusicKeyer.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.coil import coil.key.Keyer @@ -5,9 +22,7 @@ import coil.request.Options import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -/** - * A basic keyer for music data. - */ +/** A basic keyer for music data. */ class MusicKeyer : Keyer { override fun key(data: Music, options: Options): String { return if (data is Song) { diff --git a/app/src/main/java/org/oxycblt/auxio/coil/RoundableImageView.kt b/app/src/main/java/org/oxycblt/auxio/coil/RoundableImageView.kt index 49ff7f46f..32d291179 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/RoundableImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/RoundableImageView.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.coil import android.content.Context @@ -11,24 +28,24 @@ import org.oxycblt.auxio.util.getColorSafe import org.oxycblt.auxio.util.stateList /** - * An [AppCompatImageView] that applies the specified cornerRadius attribute if the user - * has enabled the "Round album covers" option. We don't round album covers by default as - * it desecrates album artwork, but if the user desires it we do have an option to enable it. + * An [AppCompatImageView] that applies the specified cornerRadius attribute if the user has enabled + * the "Round album covers" option. We don't round album covers by default as it desecrates album + * artwork, but if the user desires it we do have an option to enable it. */ -class RoundableImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : AppCompatImageView(context, attrs, defStyleAttr) { +class RoundableImageView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + AppCompatImageView(context, attrs, defStyleAttr) { init { val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.RoundableImageView) val cornerRadius = styledAttrs.getDimension(R.styleable.RoundableImageView_cornerRadius, 0f) styledAttrs.recycle() - background = MaterialShapeDrawable().apply { - setCornerSize(cornerRadius) - fillColor = context.getColorSafe(android.R.color.transparent).stateList - } + background = + MaterialShapeDrawable().apply { + setCornerSize(cornerRadius) + fillColor = context.getColorSafe(android.R.color.transparent).stateList + } } override fun onAttachedToWindow() { diff --git a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt index 1ef87f18c..c7294d1a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/SquareFrameTransform.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.coil import android.graphics.Bitmap @@ -7,8 +24,8 @@ import coil.transform.Transformation import kotlin.math.min /** - * A transformation that performs a center crop-style transformation on an image, however unlike - * the actual ScaleType, this isn't affected by any hacks we do with ImageView itself. + * A transformation that performs a center crop-style transformation on an image, however unlike the + * actual ScaleType, this isn't affected by any hacks we do with ImageView itself. * @author OxygenCobalt */ class SquareFrameTransform : Transformation { @@ -29,12 +46,7 @@ class SquareFrameTransform : Transformation { if (dstSize != wantedWidth || dstSize != wantedHeight) { // Desired size differs from the cropped size, resize the bitmap. - return Bitmap.createScaledBitmap( - dst, - wantedWidth, - wantedHeight, - true - ) + return Bitmap.createScaledBitmap(dst, wantedWidth, wantedHeight, true) } return dst 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 44d940065..c08de53c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumDetailFragment.kt is part of Auxio. * * 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 @@ -15,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.detail import android.content.Context @@ -58,11 +57,12 @@ class AlbumDetailFragment : DetailFragment() { detailModel.setAlbum(args.albumId) val binding = FragmentDetailBinding.inflate(layoutInflater) - val detailAdapter = AlbumDetailAdapter( - playbackModel, detailModel, - doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) }, - doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) } - ) + val detailAdapter = + AlbumDetailAdapter( + playbackModel, + detailModel, + doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) }, + doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ALBUM) }) // --- UI SETUP --- @@ -75,13 +75,11 @@ class AlbumDetailFragment : DetailFragment() { requireContext().showToast(R.string.lbl_queue_added) true } - R.id.action_queue_add -> { playbackModel.addToQueue(detailModel.curAlbum.value!!) requireContext().showToast(R.string.lbl_queue_added) true } - else -> false } } @@ -95,15 +93,11 @@ class AlbumDetailFragment : DetailFragment() { // -- DETAILVIEWMODEL SETUP --- - detailModel.albumData.observe(viewLifecycleOwner) { data -> - detailAdapter.submitList(data) - } + detailModel.albumData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) } detailModel.showMenu.observe(viewLifecycleOwner) { config -> if (config != null) { - showMenu(config) { id -> - id == R.id.option_sort_asc - } + showMenu(config) { id -> id == R.id.option_sort_asc } } } @@ -118,9 +112,8 @@ class AlbumDetailFragment : DetailFragment() { detailModel.finishNavToItem() } else { logD("Navigating to another album") - findNavController().navigate( - AlbumDetailFragmentDirections.actionShowAlbum(item.album.id) - ) + findNavController() + .navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.album.id)) } } @@ -133,20 +126,17 @@ class AlbumDetailFragment : DetailFragment() { detailModel.finishNavToItem() } else { logD("Navigating to another album") - findNavController().navigate( - AlbumDetailFragmentDirections.actionShowAlbum(item.id) - ) + findNavController() + .navigate(AlbumDetailFragmentDirections.actionShowAlbum(item.id)) } } // Always launch a new ArtistDetailFragment. is Artist -> { logD("Navigating to another artist") - findNavController().navigate( - AlbumDetailFragmentDirections.actionShowArtist(item.id) - ) + findNavController() + .navigate(AlbumDetailFragmentDirections.actionShowArtist(item.id)) } - null -> {} else -> logW("Unsupported navigation item ${item::class.java}") } @@ -158,8 +148,7 @@ class AlbumDetailFragment : DetailFragment() { updateQueueActions(song, binding) if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM && - playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id - ) { + playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id) { detailAdapter.highlightSong(song, binding.detailRecycler) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS @@ -172,9 +161,7 @@ class AlbumDetailFragment : DetailFragment() { return binding.root } - /** - * Updates the queue actions when - */ + /** Updates the queue actions when */ private fun updateQueueActions(song: Song?, binding: FragmentDetailBinding) { for (item in binding.detailToolbar.menu.children) { if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { @@ -183,9 +170,7 @@ class AlbumDetailFragment : DetailFragment() { } } - /** - * Scroll to an song using its [id]. - */ + /** Scroll to an song using its [id]. */ private fun scrollToItem( id: Long, binding: FragmentDetailBinding, @@ -198,8 +183,7 @@ class AlbumDetailFragment : DetailFragment() { binding.detailRecycler.post { // Make sure to increment the position to make up for the detail header binding.detailRecycler.layoutManager?.startSmoothScroll( - CenterSmoothScroller(requireContext(), pos) - ) + CenterSmoothScroller(requireContext(), pos)) // If the recyclerview can scroll, its certain that it will have to scroll to // correctly center the playing item, so make sure that the Toolbar is lifted in @@ -210,13 +194,11 @@ class AlbumDetailFragment : DetailFragment() { } /** - * [LinearSmoothScroller] subclass that centers the item on the screen instead of - * snapping to the top or bottom. + * [LinearSmoothScroller] subclass that centers the item on the screen instead of snapping to + * the top or bottom. */ - private class CenterSmoothScroller( - context: Context, - target: Int - ) : LinearSmoothScroller(context) { + private class CenterSmoothScroller(context: Context, target: Int) : + LinearSmoothScroller(context) { init { targetPosition = target } 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 f908d2fe5..778cefeef 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * ArtistDetailFragment.kt is part of Auxio. * * 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 @@ -15,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.detail import android.os.Bundle @@ -53,24 +52,19 @@ class ArtistDetailFragment : DetailFragment() { detailModel.setArtist(args.artistId) val binding = FragmentDetailBinding.inflate(layoutInflater) - val detailAdapter = ArtistDetailAdapter( - playbackModel, - doOnClick = { data -> - if (!detailModel.isNavigating) { - detailModel.setNavigating(true) + val detailAdapter = + ArtistDetailAdapter( + playbackModel, + doOnClick = { data -> + if (!detailModel.isNavigating) { + detailModel.setNavigating(true) - findNavController().navigate( - ArtistDetailFragmentDirections.actionShowAlbum(data.id) - ) - } - }, - doOnSongClick = { data -> - playbackModel.playSong(data, PlaybackMode.IN_ARTIST) - }, - doOnLongClick = { view, data -> - newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) - } - ) + findNavController() + .navigate(ArtistDetailFragmentDirections.actionShowAlbum(data.id)) + } + }, + doOnSongClick = { data -> playbackModel.playSong(data, PlaybackMode.IN_ARTIST) }, + doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_ARTIST) }) // --- UI SETUP --- @@ -91,9 +85,7 @@ class ArtistDetailFragment : DetailFragment() { detailModel.showMenu.observe(viewLifecycleOwner) { config -> if (config != null) { - showMenu(config) { id -> - id != R.id.option_sort_artist - } + showMenu(config) { id -> id != R.id.option_sort_artist } } } @@ -106,26 +98,20 @@ class ArtistDetailFragment : DetailFragment() { detailModel.finishNavToItem() } else { logD("Navigating to another artist") - findNavController().navigate( - ArtistDetailFragmentDirections.actionShowArtist(item.id) - ) + findNavController() + .navigate(ArtistDetailFragmentDirections.actionShowArtist(item.id)) } } - is Album -> { logD("Navigating to another album") - findNavController().navigate( - ArtistDetailFragmentDirections.actionShowAlbum(item.id) - ) + findNavController() + .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id)) } - is Song -> { logD("Navigating to another album") - findNavController().navigate( - ArtistDetailFragmentDirections.actionShowAlbum(item.album.id) - ) + findNavController() + .navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.album.id)) } - null -> {} else -> logW("Unsupported navigation item ${item::class.java}") } @@ -143,8 +129,7 @@ class ArtistDetailFragment : DetailFragment() { // Highlight songs if they are being played playbackModel.song.observe(viewLifecycleOwner) { song -> if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST && - playbackModel.parent.value?.id == detailModel.curArtist.value?.id - ) { + playbackModel.parent.value?.id == detailModel.curArtist.value?.id) { detailAdapter.highlightSong(song, binding.detailRecycler) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS 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 9aaf16b6f..b89548f56 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.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 android.animation.ValueAnimator @@ -12,24 +29,23 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.AppBarLayout +import java.lang.Exception import org.oxycblt.auxio.R import org.oxycblt.auxio.ui.EdgeAppBarLayout import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logTraceOrThrow -import java.lang.Exception /** * An [EdgeAppBarLayout] variant that also shows the name of the toolbar whenever the detail * recyclerview is scrolled beyond it's first item (a.k.a the header). This is used instead of - * CollapsingToolbarLayout since that thing is a mess with crippling bugs and state issues. - * This just works. + * CollapsingToolbarLayout since that thing is a mess with crippling bugs and state issues. This + * just works. * @author OxygenCobalt */ -class DetailAppBarLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : EdgeAppBarLayout(context, attrs, defStyleAttr) { +class DetailAppBarLayout +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + EdgeAppBarLayout(context, attrs, defStyleAttr) { private var mTitleView: AppCompatTextView? = null private var mRecycler: RecyclerView? = null @@ -50,16 +66,17 @@ class DetailAppBarLayout @JvmOverloads constructor( val toolbar = findViewById(R.id.detail_toolbar) // Reflect to get the actual title view to do transformations on - val newTitleView = try { - Toolbar::class.java.getDeclaredField("mTitleTextView").run { - isAccessible = true - get(toolbar) as AppCompatTextView + val newTitleView = + try { + Toolbar::class.java.getDeclaredField("mTitleTextView").run { + isAccessible = true + get(toolbar) as AppCompatTextView + } + } catch (e: Exception) { + logE("Could not get toolbar title view (likely an internal code change)") + e.logTraceOrThrow() + return null } - } catch (e: Exception) { - logE("Could not get toolbar title view (likely an internal code change)") - e.logTraceOrThrow() - return null - } newTitleView.alpha = 0f mTitleView = newTitleView @@ -103,21 +120,21 @@ class DetailAppBarLayout @JvmOverloads constructor( if (titleView?.alpha == to) return - mTitleAnimator = ValueAnimator.ofFloat(from, to).apply { - addUpdateListener { - titleView?.alpha = it.animatedValue as Float + mTitleAnimator = + ValueAnimator.ofFloat(from, to).apply { + addUpdateListener { titleView?.alpha = it.animatedValue as Float } + + duration = + resources.getInteger(R.integer.detail_app_bar_title_anim_duration).toLong() + + start() } - - duration = resources.getInteger(R.integer.detail_app_bar_title_anim_duration).toLong() - - start() - } } - class Behavior @JvmOverloads constructor( - context: Context? = null, - attrs: AttributeSet? = null - ) : AppBarLayout.Behavior(context, attrs) { + class Behavior + @JvmOverloads + constructor(context: Context? = null, attrs: AttributeSet? = null) : + AppBarLayout.Behavior(context, attrs) { override fun onNestedPreScroll( coordinatorLayout: CoordinatorLayout, child: AppBarLayout, @@ -132,8 +149,8 @@ class DetailAppBarLayout @JvmOverloads constructor( val appBar = child as DetailAppBarLayout val recycler = appBar.findRecyclerView() - val showTitle = (recycler.layoutManager as LinearLayoutManager) - .findFirstVisibleItemPosition() > 0 + val showTitle = + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() > 0 appBar.setTitleVisibility(showTitle) } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index a3a5c643b..bc66bca2a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * DetailFragment.kt is part of Auxio. * * 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 @@ -15,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.detail import androidx.annotation.MenuRes @@ -70,21 +69,15 @@ abstract class DetailFragment : Fragment() { inflateMenu(menuId) } - setNavigationOnClickListener { - findNavController().navigateUp() - } + setNavigationOnClickListener { findNavController().navigateUp() } onMenuClick?.let { onClick -> - setOnMenuItemClickListener { item -> - onClick(item.itemId) - } + setOnMenuItemClickListener { item -> onClick(item.itemId) } } } } - /** - * Shortcut method for recyclerview setup - */ + /** Shortcut method for recyclerview setup */ protected fun setupRecycler( binding: FragmentDetailBinding, detailAdapter: RecyclerView.Adapter, @@ -99,10 +92,14 @@ abstract class DetailFragment : Fragment() { /** * Shortcut method for spinning up the sorting [PopupMenu] - * @param config The initial configuration to apply to the menu. This is provided by [DetailViewModel.showMenu]. + * @param config The initial configuration to apply to the menu. This is provided by + * [DetailViewModel.showMenu]. * @param showItem Which menu items to keep */ - protected fun showMenu(config: DetailViewModel.MenuConfig, showItem: ((Int) -> Boolean)? = null) { + protected fun showMenu( + config: DetailViewModel.MenuConfig, + showItem: ((Int) -> Boolean)? = null + ) { logD("Launching menu [$config]") PopupMenu(config.anchor.context, config.anchor).apply { @@ -120,9 +117,7 @@ abstract class DetailFragment : Fragment() { true } - setOnDismissListener { - detailModel.finishShowMenu(null) - } + setOnDismissListener { detailModel.finishShowMenu(null) } if (showItem != null) { for (item in menu.children) { 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 31a41dc57..f46afc5f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * DetailViewModel.kt is part of Auxio. * * 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 @@ -15,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.detail import android.view.View @@ -47,22 +46,26 @@ class DetailViewModel : ViewModel() { // --- CURRENT VALUES --- private val mCurGenre = MutableLiveData() - val curGenre: LiveData get() = mCurGenre + val curGenre: LiveData + get() = mCurGenre private val mGenreData = MutableLiveData(listOf()) val genreData: LiveData> = mGenreData private val mCurArtist = MutableLiveData() - val curArtist: LiveData get() = mCurArtist + val curArtist: LiveData + get() = mCurArtist private val mArtistData = MutableLiveData(listOf()) val artistData: LiveData> = mArtistData private val mCurAlbum = MutableLiveData() - val curAlbum: LiveData get() = mCurAlbum + val curAlbum: LiveData + get() = mCurAlbum private val mAlbumData = MutableLiveData(listOf()) - val albumData: LiveData> get() = mAlbumData + val albumData: LiveData> + get() = mAlbumData data class MenuConfig(val anchor: View, val sortMode: Sort) @@ -72,7 +75,8 @@ class DetailViewModel : ViewModel() { private val mNavToItem = MutableLiveData() /** Flag for unified navigation. Observe this to coordinate navigation to an item's UI. */ - val navToItem: LiveData get() = mNavToItem + val navToItem: LiveData + get() = mNavToItem var isNavigating = false private set @@ -101,10 +105,7 @@ class DetailViewModel : ViewModel() { refreshAlbumData() } - /** - * Mark that the menu process is done with the new [Sort]. - * Pass null if there was no change. - */ + /** Mark that the menu process is done with the new [Sort]. Pass null if there was no change. */ fun finishShowMenu(newMode: Sort?) { mShowMenu.value = null @@ -130,23 +131,17 @@ class DetailViewModel : ViewModel() { currentMenuContext = null } - /** - * Navigate to an item, whether a song/album/artist - */ + /** Navigate to an item, whether a song/album/artist */ fun navToItem(item: Item) { mNavToItem.value = item } - /** - * Mark that the navigation process is done. - */ + /** Mark that the navigation process is done. */ fun finishNavToItem() { mNavToItem.value = null } - /** - * Update the current navigation status to [isNavigating] - */ + /** Update the current navigation status to [isNavigating] */ fun setNavigating(navigating: Boolean) { isNavigating = navigating } @@ -165,9 +160,7 @@ class DetailViewModel : ViewModel() { onClick = { view -> currentMenuContext = DisplayMode.SHOW_GENRES mShowMenu.value = MenuConfig(view, settingsManager.detailGenreSort) - } - ) - ) + })) data.addAll(settingsManager.detailGenreSort.sortGenre(curGenre.value!!)) @@ -179,12 +172,7 @@ class DetailViewModel : ViewModel() { val artist = requireNotNull(curArtist.value) val data = mutableListOf(artist) - data.add( - Header( - id = -2, - string = R.string.lbl_albums - ) - ) + data.add(Header(id = -2, string = R.string.lbl_albums)) data.addAll(Sort.ByYear(false).sortAlbums(artist.albums)) @@ -197,9 +185,7 @@ class DetailViewModel : ViewModel() { onClick = { view -> currentMenuContext = DisplayMode.SHOW_ARTISTS mShowMenu.value = MenuConfig(view, settingsManager.detailArtistSort) - } - ) - ) + })) data.addAll(settingsManager.detailArtistSort.sortArtist(artist)) @@ -220,9 +206,7 @@ class DetailViewModel : ViewModel() { onClick = { view -> currentMenuContext = DisplayMode.SHOW_ALBUMS mShowMenu.value = MenuConfig(view, settingsManager.detailAlbumSort) - } - ) - ) + })) data.addAll(settingsManager.detailAlbumSort.sortAlbum(curAlbum.value!!)) 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 96bc663ac..583341066 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * GenreDetailFragment.kt is part of Auxio. * * 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 @@ -15,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.detail import android.os.Bundle @@ -53,15 +52,11 @@ class GenreDetailFragment : DetailFragment() { detailModel.setGenre(args.genreId) val binding = FragmentDetailBinding.inflate(inflater) - val detailAdapter = GenreDetailAdapter( - playbackModel, - doOnClick = { song -> - playbackModel.playSong(song, PlaybackMode.IN_GENRE) - }, - doOnLongClick = { view, data -> - newMenu(view, data, ActionMenu.FLAG_IN_GENRE) - } - ) + val detailAdapter = + GenreDetailAdapter( + playbackModel, + doOnClick = { song -> playbackModel.playSong(song, PlaybackMode.IN_GENRE) }, + doOnLongClick = { view, data -> newMenu(view, data, ActionMenu.FLAG_IN_GENRE) }) // --- UI SETUP --- @@ -75,34 +70,26 @@ class GenreDetailFragment : DetailFragment() { // --- DETAILVIEWMODEL SETUP --- - detailModel.genreData.observe(viewLifecycleOwner) { data -> - detailAdapter.submitList(data) - } + detailModel.genreData.observe(viewLifecycleOwner) { data -> detailAdapter.submitList(data) } detailModel.navToItem.observe(viewLifecycleOwner) { item -> when (item) { // All items will launch new detail fragments. is Artist -> { logD("Navigating to another artist") - findNavController().navigate( - GenreDetailFragmentDirections.actionShowArtist(item.id) - ) + findNavController() + .navigate(GenreDetailFragmentDirections.actionShowArtist(item.id)) } - is Album -> { logD("Navigating to another album") - findNavController().navigate( - GenreDetailFragmentDirections.actionShowAlbum(item.id) - ) + findNavController() + .navigate(GenreDetailFragmentDirections.actionShowAlbum(item.id)) } - is Song -> { logD("Navigating to another song") - findNavController().navigate( - GenreDetailFragmentDirections.actionShowAlbum(item.album.id) - ) + findNavController() + .navigate(GenreDetailFragmentDirections.actionShowAlbum(item.album.id)) } - null -> {} else -> logW("Unsupported navigation command ${item::class.java}") } @@ -112,8 +99,7 @@ class GenreDetailFragment : DetailFragment() { playbackModel.song.observe(viewLifecycleOwner) { song -> if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE && - playbackModel.parent.value?.id == detailModel.curGenre.value!!.id - ) { + playbackModel.parent.value?.id == detailModel.curGenre.value!!.id) { detailAdapter.highlightSong(song, binding.detailRecycler) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS 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 f58733277..004cb456c 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumDetailAdapter.kt is part of Auxio. * * 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 @@ -15,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.detail.recycler import android.view.View @@ -63,16 +62,11 @@ class AlbumDetailAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - ALBUM_DETAIL_ITEM_TYPE -> AlbumDetailViewHolder( - ItemDetailBinding.inflate(parent.context.inflater) - ) - - ALBUM_SONG_ITEM_TYPE -> AlbumSongViewHolder( - ItemAlbumSongBinding.inflate(parent.context.inflater) - ) - + ALBUM_DETAIL_ITEM_TYPE -> + AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) + ALBUM_SONG_ITEM_TYPE -> + AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater)) ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) - else -> error("Invalid ViewHolder item type $viewType") } } @@ -84,8 +78,7 @@ class AlbumDetailAdapter( is Album -> (holder as AlbumDetailViewHolder).bind(item) is Song -> (holder as AlbumSongViewHolder).bind(item) is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item) - else -> { - } + else -> {} } if (holder is Highlightable) { @@ -114,9 +107,7 @@ class AlbumDetailAdapter( if (song != null) { // Use existing data instead of having to re-sort it. - val pos = currentList.indexOfFirst { item -> - item.id == song.id && item is Song - } + val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song } // Check if the ViewHolder for this song is visible, if it is then highlight it. // If the ViewHolder is not visible, then the adapter should take care of it if @@ -130,9 +121,8 @@ class AlbumDetailAdapter( } } - inner class AlbumDetailViewHolder( - private val binding: ItemDetailBinding - ) : BaseViewHolder(binding) { + inner class AlbumDetailViewHolder(private val binding: ItemDetailBinding) : + BaseViewHolder(binding) { override fun onBind(data: Album) { binding.detailCover.apply { @@ -144,27 +134,21 @@ class AlbumDetailAdapter( binding.detailSubhead.apply { text = data.artist.resolvedName - setOnClickListener { - detailModel.navToItem(data.artist) - } + setOnClickListener { detailModel.navToItem(data.artist) } } binding.detailInfo.apply { - text = context.getString( - R.string.fmt_three, - data.year?.toString() ?: context.getString(R.string.def_date), - context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size), - data.totalDuration - ) + text = + context.getString( + R.string.fmt_three, + data.year?.toString() ?: context.getString(R.string.def_date), + context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size), + data.totalDuration) } - binding.detailPlayButton.setOnClickListener { - playbackModel.playAlbum(data, false) - } + binding.detailPlayButton.setOnClickListener { playbackModel.playAlbum(data, false) } - binding.detailShuffleButton.setOnClickListener { - playbackModel.playAlbum(data, true) - } + binding.detailShuffleButton.setOnClickListener { playbackModel.playAlbum(data, true) } } } 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 e80eb2ce2..499114da3 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * ArtistDetailAdapter.kt is part of Auxio. * * 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 @@ -15,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.detail.recycler import android.view.View @@ -70,22 +69,14 @@ class ArtistDetailAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - ARTIST_DETAIL_ITEM_TYPE -> ArtistDetailViewHolder( - ItemDetailBinding.inflate(parent.context.inflater) - ) - - ARTIST_ALBUM_ITEM_TYPE -> ArtistAlbumViewHolder( - ItemArtistAlbumBinding.inflate(parent.context.inflater) - ) - - ARTIST_SONG_ITEM_TYPE -> ArtistSongViewHolder( - ItemArtistSongBinding.inflate(parent.context.inflater) - ) - + ARTIST_DETAIL_ITEM_TYPE -> + ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) + ARTIST_ALBUM_ITEM_TYPE -> + ArtistAlbumViewHolder(ItemArtistAlbumBinding.inflate(parent.context.inflater)) + ARTIST_SONG_ITEM_TYPE -> + ArtistSongViewHolder(ItemArtistSongBinding.inflate(parent.context.inflater)) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) - ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) - else -> error("Invalid ViewHolder item type $viewType") } } @@ -99,8 +90,7 @@ class ArtistDetailAdapter( is Song -> (holder as ArtistSongViewHolder).bind(item) is Header -> (holder as HeaderViewHolder).bind(item) is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item) - else -> { - } + else -> {} } if (holder is Highlightable) { @@ -133,9 +123,7 @@ class ArtistDetailAdapter( if (album != null) { // Use existing data instead of having to re-sort it. - val pos = currentList.indexOfFirst { item -> - item.id == album.id && item is Album - } + val pos = currentList.indexOfFirst { item -> item.id == album.id && item is Album } // Check if the ViewHolder if this album is visible, and highlight it if so. recycler.layoutManager?.findViewByPosition(pos)?.let { child -> @@ -163,9 +151,7 @@ class ArtistDetailAdapter( if (song != null) { // Use existing data instead of having to re-sort it. // We also have to account for the album count when searching for the ViewHolder. - val pos = currentList.indexOfFirst { item -> - item.id == song.id && item is Song - } + val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song } // Check if the ViewHolder for this song is visible, if it is then highlight it. // If the ViewHolder is not visible, then the adapter should take care of it if @@ -179,39 +165,35 @@ class ArtistDetailAdapter( } } - inner class ArtistDetailViewHolder( - private val binding: ItemDetailBinding - ) : BaseViewHolder(binding) { + inner class ArtistDetailViewHolder(private val binding: ItemDetailBinding) : + BaseViewHolder(binding) { override fun onBind(data: Artist) { val context = binding.root.context binding.detailCover.apply { bindArtistImage(data) - contentDescription = context.getString( - R.string.desc_artist_image, - data.resolvedName - ) + contentDescription = + context.getString(R.string.desc_artist_image, data.resolvedName) } binding.detailName.text = data.resolvedName // Get the genre that corresponds to the most songs in this artist, which would be // the most "Prominent" genre. - binding.detailSubhead.text = data.songs - .groupBy { it.genre.resolvedName } - .entries.maxByOrNull { it.value.size } - ?.key ?: context.getString(R.string.def_genre) + binding.detailSubhead.text = + data.songs + .groupBy { it.genre.resolvedName } + .entries + .maxByOrNull { it.value.size } + ?.key + ?: context.getString(R.string.def_genre) binding.detailInfo.bindArtistInfo(data) - binding.detailPlayButton.setOnClickListener { - playbackModel.playArtist(data, false) - } + binding.detailPlayButton.setOnClickListener { playbackModel.playArtist(data, false) } - binding.detailShuffleButton.setOnClickListener { - playbackModel.playArtist(data, true) - } + binding.detailShuffleButton.setOnClickListener { playbackModel.playArtist(data, true) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 11b2affbf..8704bf4c8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * GenreDetailAdapter.kt is part of Auxio. * * 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 @@ -15,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.detail.recycler import android.view.View @@ -60,16 +59,13 @@ class GenreDetailAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - GENRE_DETAIL_ITEM_TYPE -> GenreDetailViewHolder( - ItemDetailBinding.inflate(parent.context.inflater) - ) - - GENRE_SONG_ITEM_TYPE -> GenreSongViewHolder( - ItemGenreSongBinding.inflate(parent.context.inflater), - ) - + GENRE_DETAIL_ITEM_TYPE -> + GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) + GENRE_SONG_ITEM_TYPE -> + GenreSongViewHolder( + ItemGenreSongBinding.inflate(parent.context.inflater), + ) ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) - else -> error("Bad ViewHolder item type $viewType") } } @@ -110,9 +106,7 @@ class GenreDetailAdapter( if (song != null) { // Use existing data instead of having to re-sort it. - val pos = currentList.indexOfFirst { item -> - item.id == song.id && item is Song - } + val pos = currentList.indexOfFirst { item -> item.id == song.id && item is Song } // Check if the ViewHolder for this song is visible, if it is then highlight it. // If the ViewHolder is not visible, then the adapter should take care of it if @@ -126,31 +120,23 @@ class GenreDetailAdapter( } } - inner class GenreDetailViewHolder( - private val binding: ItemDetailBinding - ) : BaseViewHolder(binding) { + inner class GenreDetailViewHolder(private val binding: ItemDetailBinding) : + BaseViewHolder(binding) { override fun onBind(data: Genre) { val context = binding.root.context binding.detailCover.apply { bindGenreImage(data) - contentDescription = context.getString( - R.string.desc_genre_image, - data.resolvedName - ) + contentDescription = context.getString(R.string.desc_genre_image, data.resolvedName) } binding.detailName.text = data.resolvedName binding.detailSubhead.bindGenreInfo(data) binding.detailInfo.text = data.totalDuration - binding.detailPlayButton.setOnClickListener { - playbackModel.playGenre(data, false) - } + binding.detailPlayButton.setOnClickListener { playbackModel.playGenre(data, false) } - binding.detailShuffleButton.setOnClickListener { - playbackModel.playGenre(data, true) - } + binding.detailShuffleButton.setOnClickListener { playbackModel.playGenre(data, true) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/Highlightable.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/Highlightable.kt index 532d6040d..d5b3aec2f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/Highlightable.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/Highlightable.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Highlightable.kt is part of Auxio. * * 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 @@ -15,12 +14,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.detail.recycler -/** - * Interface that allows the highlighting of certain ViewHolders - */ +/** Interface that allows the highlighting of certain ViewHolders */ interface Highlightable { fun setHighlighted(isHighlighted: Boolean) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/AdaptiveTabStrategy.kt b/app/src/main/java/org/oxycblt/auxio/home/AdaptiveTabStrategy.kt index e76f5941a..071a32518 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/AdaptiveTabStrategy.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/AdaptiveTabStrategy.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.home import android.content.Context @@ -11,10 +28,8 @@ import org.oxycblt.auxio.util.logD * - On medium screens, use only text * - On large screens, use text and an icon */ -class AdaptiveTabStrategy( - context: Context, - private val homeModel: HomeViewModel -) : TabLayoutMediator.TabConfigurationStrategy { +class AdaptiveTabStrategy(context: Context, private val homeModel: HomeViewModel) : + TabLayoutMediator.TabConfigurationStrategy { private val width = context.resources.configuration.smallestScreenWidthDp override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { @@ -23,19 +38,15 @@ class AdaptiveTabStrategy( when { width < 370 -> { logD("Using icon-only configuration") - tab.setIcon(tabMode.icon) - .setContentDescription(tabMode.string) + tab.setIcon(tabMode.icon).setContentDescription(tabMode.string) } - width < 640 -> { logD("Using text-only configuration") tab.setText(tabMode.string) } - else -> { logD("Using icon-and-text configuration") - tab.setIcon(tabMode.icon) - .setText(tabMode.string) + tab.setIcon(tabMode.icon).setText(tabMode.string) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/EdgeFabContainer.kt b/app/src/main/java/org/oxycblt/auxio/home/EdgeFabContainer.kt index 8828c62f6..6cd476431 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/EdgeFabContainer.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/EdgeFabContainer.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * EdgeFloatingActionButton.kt is part of Auxio. * * 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 @@ -15,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.home import android.content.Context @@ -30,11 +29,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * A container for a FloatingActionButton that enables edge-to-edge support. * @author OxygenCobalt */ -class EdgeFabContainer @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr) { +class EdgeFabContainer +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + FrameLayout(context, attrs, defStyleAttr) { init { clipToPadding = false } 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 8a9d6af25..049d46892 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MainFragment.kt is part of Auxio. * * 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 @@ -15,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.home import android.os.Bundle @@ -52,11 +51,10 @@ import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logTraceOrThrow /** - * The main "Launching Point" fragment of Auxio, allowing navigation to the detail - * views for each respective item. - * @author OxygenCobalt - * TODO: Make tabs invisible when there is only one - * TODO: Add duration and song count sorts + * The main "Launching Point" fragment of Auxio, allowing navigation to the detail views for each + * respective item. + * @author OxygenCobalt TODO: Make tabs invisible when there is only one TODO: Add duration and song + * count sorts */ class HomeFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() @@ -83,35 +81,37 @@ class HomeFragment : Fragment() { logD("Navigating to search") findNavController().navigate(HomeFragmentDirections.actionShowSearch()) } - R.id.action_settings -> { logD("Navigating to settings") - parentFragment?.parentFragment?.findNavController()?.navigate( - MainFragmentDirections.actionShowSettings() - ) + parentFragment + ?.parentFragment + ?.findNavController() + ?.navigate(MainFragmentDirections.actionShowSettings()) } - R.id.action_about -> { logD("Navigating to about") - parentFragment?.parentFragment?.findNavController()?.navigate( - MainFragmentDirections.actionShowAbout() - ) + parentFragment + ?.parentFragment + ?.findNavController() + ?.navigate(MainFragmentDirections.actionShowAbout()) } - - R.id.submenu_sorting -> { } - + R.id.submenu_sorting -> {} R.id.option_sort_asc -> { item.isChecked = !item.isChecked - val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) - .ascending(item.isChecked) + val new = + homeModel + .getSortForDisplay(homeModel.curTab.value!!) + .ascending(item.isChecked) homeModel.updateCurrentSort(new) } // Sorting option was selected, mark it as selected and update the mode else -> { item.isChecked = true - val new = homeModel.getSortForDisplay(homeModel.curTab.value!!) - .assignId(item.itemId) + val new = + homeModel + .getSortForDisplay(homeModel.curTab.value!!) + .assignId(item.itemId) homeModel.updateCurrentSort(requireNotNull(new)) } } @@ -129,12 +129,14 @@ class HomeFragment : Fragment() { // scroll events being registered as horizontal scroll events. Reflect into the // internal recyclerview and change the touch slope so that touch actions will // act more as a scroll than as a swipe. - // Derived from: https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414 + // Derived from: + // https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414 try { - val recycler = ViewPager2::class.java.getDeclaredField("mRecyclerView").run { - isAccessible = true - get(binding.homePager) - } + val recycler = + ViewPager2::class.java.getDeclaredField("mRecyclerView").run { + isAccessible = true + get(binding.homePager) + } RecyclerView::class.java.getDeclaredField("mTouchSlop").apply { isAccessible = true @@ -152,18 +154,17 @@ class HomeFragment : Fragment() { // page transitions. offscreenPageLimit = homeModel.tabs.size - registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) = homeModel.updateCurrentTab(position) - }) + registerOnPageChangeCallback( + object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) = + homeModel.updateCurrentTab(position) + }) - TabLayoutMediator( - binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel) - ).attach() + TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel)) + .attach() } - binding.homeFab.setOnClickListener { - playbackModel.shuffleAll() - } + binding.homeFab.setOnClickListener { playbackModel.shuffleAll() } // --- VIEWMODEL SETUP --- @@ -213,18 +214,12 @@ class HomeFragment : Fragment() { // the tab changes. when (tab) { DisplayMode.SHOW_SONGS -> updateSortMenu(sortItem, tab) - - DisplayMode.SHOW_ALBUMS -> updateSortMenu(sortItem, tab) { id -> - id != R.id.option_sort_album - } - - DisplayMode.SHOW_ARTISTS -> updateSortMenu(sortItem, tab) { id -> - id == R.id.option_sort_asc - } - - DisplayMode.SHOW_GENRES -> updateSortMenu(sortItem, tab) { id -> - id == R.id.option_sort_asc - } + DisplayMode.SHOW_ALBUMS -> + updateSortMenu(sortItem, tab) { id -> id != R.id.option_sort_album } + DisplayMode.SHOW_ARTISTS -> + updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc } + DisplayMode.SHOW_GENRES -> + updateSortMenu(sortItem, tab) { id -> id == R.id.option_sort_asc } } binding.homeAppbar.liftOnScrollTargetViewId = tab.viewId @@ -236,24 +231,19 @@ class HomeFragment : Fragment() { // This is only here just in case a collapsing toolbar is re-added. binding.homeAppbar.post { when (item) { - is Song -> findNavController().navigate( - HomeFragmentDirections.actionShowAlbum(item.album.id) - ) - - is Album -> findNavController().navigate( - HomeFragmentDirections.actionShowAlbum(item.id) - ) - - is Artist -> findNavController().navigate( - HomeFragmentDirections.actionShowArtist(item.id) - ) - - is Genre -> findNavController().navigate( - HomeFragmentDirections.actionShowGenre(item.id) - ) - - else -> { - } + is Song -> + findNavController() + .navigate(HomeFragmentDirections.actionShowAlbum(item.album.id)) + is Album -> + findNavController() + .navigate(HomeFragmentDirections.actionShowAlbum(item.id)) + is Artist -> + findNavController() + .navigate(HomeFragmentDirections.actionShowArtist(item.id)) + is Genre -> + findNavController() + .navigate(HomeFragmentDirections.actionShowGenre(item.id)) + else -> {} } } } @@ -283,12 +273,14 @@ class HomeFragment : Fragment() { } } - private val DisplayMode.viewId: Int get() = when (this) { - DisplayMode.SHOW_SONGS -> R.id.home_song_list - DisplayMode.SHOW_ALBUMS -> R.id.home_album_list - DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list - DisplayMode.SHOW_GENRES -> R.id.home_genre_list - } + private val DisplayMode.viewId: Int + get() = + when (this) { + DisplayMode.SHOW_SONGS -> R.id.home_song_list + DisplayMode.SHOW_ALBUMS -> R.id.home_album_list + DisplayMode.SHOW_ARTISTS -> R.id.home_artist_list + DisplayMode.SHOW_GENRES -> R.id.home_genre_list + } private inner class HomePagerAdapter : FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { 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 9089b23f4..f8af2e412 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * HomeViewModel.kt is part of Auxio. * * 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 @@ -15,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.home import androidx.lifecycle.LiveData @@ -42,31 +41,34 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { private val settingsManager = SettingsManager.getInstance() private val mSongs = MutableLiveData(listOf()) - val songs: LiveData> get() = mSongs + val songs: LiveData> + get() = mSongs private val mAlbums = MutableLiveData(listOf()) - val albums: LiveData> get() = mAlbums + val albums: LiveData> + get() = mAlbums private val mArtists = MutableLiveData(listOf()) - val artists: LiveData> get() = mArtists + val artists: LiveData> + get() = mArtists private val mGenres = MutableLiveData(listOf()) - val genres: LiveData> get() = mGenres + val genres: LiveData> + get() = mGenres var tabs: List = visibleTabs private set /** Internal getter for getting the visible library tabs */ - private val visibleTabs: List get() = settingsManager.libTabs - .filterIsInstance().map { it.mode } + private val visibleTabs: List + get() = settingsManager.libTabs.filterIsInstance().map { it.mode } private val mCurTab = MutableLiveData(tabs[0]) val curTab: LiveData = mCurTab /** - * Marker to recreate all library tabs, usually initiated by a settings change. - * When this flag is set, all tabs (and their respective viewpager fragments) will be - * recreated from scratch. + * Marker to recreate all library tabs, usually initiated by a settings change. When this flag + * is set, all tabs (and their respective viewpager fragments) will be recreated from scratch. */ private val mRecreateTabs = MutableLiveData(false) val recreateTabs: LiveData = mRecreateTabs @@ -86,9 +88,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { } } - /** - * Update the current tab based off of the new ViewPager position. - */ + /** Update the current tab based off of the new ViewPager position. */ fun updateCurrentTab(pos: Int) { logD("Updating current tab to ${tabs[pos]}") mCurTab.value = tabs[pos] @@ -107,9 +107,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { } } - /** - * Update the currently displayed item's [Sort]. - */ + /** Update the currently displayed item's [Sort]. */ fun updateCurrentSort(sort: Sort) { logD("Updating ${mCurTab.value} sort to $sort") when (mCurTab.value) { @@ -117,29 +115,25 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback { settingsManager.libSongSort = sort mSongs.value = sort.sortSongs(mSongs.value!!) } - DisplayMode.SHOW_ALBUMS -> { settingsManager.libAlbumSort = sort mAlbums.value = sort.sortAlbums(mAlbums.value!!) } - DisplayMode.SHOW_ARTISTS -> { settingsManager.libArtistSort = sort mArtists.value = sort.sortParents(mArtists.value!!) } - DisplayMode.SHOW_GENRES -> { settingsManager.libGenreSort = sort mGenres.value = sort.sortParents(mGenres.value!!) } - else -> {} } } /** - * Update the fast scroll state. This is used to control the FAB visibility whenever - * the user begins to fast scroll. + * Update the fast scroll state. This is used to control the FAB visibility whenever the user + * begins to fast scroll. */ fun updateFastScrolling(scrolling: Boolean) { mFastScrolling.value = scrolling diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt index 1f08b1468..4780775bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupDrawable.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Md2PopupBackground.java is part of Auxio. * * 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 @@ -15,6 +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.home.fastscroll import android.content.Context @@ -30,19 +30,18 @@ import android.graphics.drawable.Drawable import android.os.Build import android.view.View import androidx.core.graphics.drawable.DrawableCompat +import kotlin.math.sqrt import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getAttrColorSafe import org.oxycblt.auxio.util.getDimenOffsetSafe -import kotlin.math.sqrt /** - * The custom drawable used as FastScrollRecyclerView's popup background. - * This is an adaptation from AndroidFastScroll's MD2 theme. + * The custom drawable used as FastScrollRecyclerView's popup background. This is an adaptation from + * AndroidFastScroll's MD2 theme. * - * Attributions as per the Apache 2.0 license: - * ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai] - * PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll] - * MODIFIER: OxygenCobalt [https://github.com/] + * Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang + * [https://github.com/zhanghai] PROJECT: Android Fast Scroll + * [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/] * * !!! MODIFICATIONS !!!: * - Use modified Auxio resources instead of AFS resources @@ -53,11 +52,12 @@ import kotlin.math.sqrt * @author Hai Zhang, OxygenCobalt */ class FastScrollPopupDrawable(context: Context) : Drawable() { - private val paint: Paint = Paint().apply { - isAntiAlias = true - color = context.getAttrColorSafe(R.attr.colorSecondary) - style = Paint.Style.FILL - } + private val paint: Paint = + Paint().apply { + isAntiAlias = true + color = context.getAttrColorSafe(R.attr.colorSecondary) + style = Paint.Style.FILL + } private val path = Path() private val matrix = Matrix() @@ -86,13 +86,14 @@ class FastScrollPopupDrawable(context: Context) : Drawable() { // Paths don't need to be convex on android Q, but the API was mislabeled and so // we still have to use this method. Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> outline.setConvexPath(path) - - else -> if (!path.isConvex) { - // The outline path must be convex before Q, but we may run into floating point - // errors caused by calculations involving sqrt(2) or OEM implementation differences, - // so in this case we just omit the shadow instead of crashing. - super.getOutline(outline) - } + else -> + if (!path.isConvex) { + // The outline path must be convex before Q, but we may run into floating point + // errors caused by calculations involving sqrt(2) or OEM implementation + // differences, + // so in this case we just omit the shadow instead of crashing. + super.getOutline(outline) + } } } @@ -153,11 +154,15 @@ class FastScrollPopupDrawable(context: Context) : Drawable() { sweepAngle: Float ) { path.arcTo( - centerX - radius, centerY - radius, centerX + radius, centerY + radius, - startAngle, sweepAngle, false - ) + centerX - radius, + centerY - radius, + centerX + radius, + centerY + radius, + startAngle, + sweepAngle, + false) } - private val isRtl: Boolean get() = - DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL + private val isRtl: Boolean + get() = DrawableCompat.getLayoutDirection(this) == View.LAYOUT_DIRECTION_RTL } 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 bd08de937..9a6002348 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * FastScrollRecyclerView.kt is part of Auxio. * * 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 @@ -15,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.home.fastscroll import android.annotation.SuppressLint @@ -41,6 +40,7 @@ import androidx.core.widget.TextViewCompat import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.util.canScroll import org.oxycblt.auxio.util.getAttrColorSafe @@ -48,20 +48,18 @@ import org.oxycblt.auxio.util.getDimenOffsetSafe import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.systemBarInsetsCompat -import kotlin.math.abs /** * A [RecyclerView] that enables better fast-scrolling. This is fundamentally a implementation of * Hai Zhang's AndroidFastScroll but slimmed down for Auxio and with a couple of enhancements. * - * Attributions as per the Apache 2.0 license: - * ORIGINAL AUTHOR: Hai Zhang [https://github.com/zhanghai] - * PROJECT: Android Fast Scroll [https://github.com/zhanghai/AndroidFastScroll] - * MODIFIER: OxygenCobalt [https://github.com/] + * Attributions as per the Apache 2.0 license: ORIGINAL AUTHOR: Hai Zhang + * [https://github.com/zhanghai] PROJECT: Android Fast Scroll + * [https://github.com/zhanghai/AndroidFastScroll] MODIFIER: OxygenCobalt [https://github.com/] * * !!! MODIFICATIONS !!!: - * - Scroller will no longer show itself on startup or relayouts, which looked unpleasant - * with multiple views + * - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with + * multiple views * - DefaultAnimationHelper and RecyclerViewHelper were merged into the class * - FastScroller overlay was merged into RecyclerView instance * - Removed FastScrollerBuilder @@ -75,17 +73,16 @@ import kotlin.math.abs * * @author Hai Zhang, OxygenCobalt */ -class FastScrollRecyclerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : RecyclerView(context, attrs, defStyleAttr) { +class FastScrollRecyclerView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + RecyclerView(context, attrs, defStyleAttr) { /** Callback to provide a string to be shown on the popup when an item is passed */ var popupProvider: ((Int) -> String)? = null /** - * A listener for when a drag event occurs. The value will be true if a drag has begun, - * and false if a drag ended. + * A listener for when a drag event occurs. The value will be true if a drag has begun, and + * false if a drag ended. */ var onDragListener: ((Boolean) -> Unit)? = null @@ -128,35 +125,37 @@ class FastScrollRecyclerView @JvmOverloads constructor( val thumbDrawable = context.getDrawableSafe(R.drawable.ui_scroll_thumb) trackView = View(context) - thumbView = View(context).apply { - alpha = 0f - background = thumbDrawable - } - - popupView = AppCompatTextView(context).apply { - alpha = 0f - layoutParams = FrameLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - - minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width) - minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large) - - (layoutParams as FrameLayout.LayoutParams).apply { - gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP - marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small) + thumbView = + View(context).apply { + alpha = 0f + background = thumbDrawable } - TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) - setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary)) + popupView = + AppCompatTextView(context).apply { + alpha = 0f + layoutParams = + FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - background = FastScrollPopupDrawable(context) - elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat() - ellipsize = TextUtils.TruncateAt.MIDDLE - gravity = Gravity.CENTER - includeFontPadding = false - isSingleLine = true - } + minimumWidth = context.getDimenSizeSafe(R.dimen.popup_min_width) + minimumHeight = context.getDimenSizeSafe(R.dimen.size_btn_large) + + (layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.CENTER_HORIZONTAL or Gravity.TOP + marginEnd = context.getDimenOffsetSafe(R.dimen.spacing_small) + } + + TextViewCompat.setTextAppearance(this, R.style.TextAppearance_Auxio_HeadlineLarge) + setTextColor(context.getAttrColorSafe(R.attr.colorOnSecondary)) + + background = FastScrollPopupDrawable(context) + elevation = context.getDimenSizeSafe(R.dimen.elevation_normal).toFloat() + ellipsize = TextUtils.TruncateAt.MIDDLE + gravity = Gravity.CENTER + includeFontPadding = false + isSingleLine = true + } thumbWidth = thumbDrawable.intrinsicWidth thumbHeight = thumbDrawable.intrinsicHeight @@ -168,33 +167,28 @@ class FastScrollRecyclerView @JvmOverloads constructor( overlay.add(thumbView) overlay.add(popupView) - addItemDecoration(object : ItemDecoration() { - override fun onDraw( - canvas: Canvas, - parent: RecyclerView, - state: State - ) { - onPreDraw() - } - }) + addItemDecoration( + object : ItemDecoration() { + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: State) { + onPreDraw() + } + }) // We use a listener instead of overriding onTouchEvent so that we don't conflict with // RecyclerView touch events. - addOnItemTouchListener(object : SimpleOnItemTouchListener() { - override fun onTouchEvent( - recyclerView: RecyclerView, - event: MotionEvent - ) { - onItemTouch(event) - } + addOnItemTouchListener( + object : SimpleOnItemTouchListener() { + override fun onTouchEvent(recyclerView: RecyclerView, event: MotionEvent) { + onItemTouch(event) + } - override fun onInterceptTouchEvent( - recyclerView: RecyclerView, - event: MotionEvent - ): Boolean { - return onItemTouch(event) - } - }) + override fun onInterceptTouchEvent( + recyclerView: RecyclerView, + event: MotionEvent + ): Boolean { + return onItemTouch(event) + } + }) } // --- RECYCLERVIEW EVENT MANAGEMENT --- @@ -206,33 +200,34 @@ class FastScrollRecyclerView @JvmOverloads constructor( thumbView.layoutDirection = layoutDirection popupView.layoutDirection = layoutDirection - val trackLeft = if (isRtl) { - scrollerPadding.left - } else { - width - scrollerPadding.right - thumbWidth - } + val trackLeft = + if (isRtl) { + scrollerPadding.left + } else { + width - scrollerPadding.right - thumbWidth + } trackView.layout( - trackLeft, scrollerPadding.top, trackLeft + thumbWidth, - height - scrollerPadding.bottom - ) + trackLeft, scrollerPadding.top, trackLeft + thumbWidth, height - scrollerPadding.bottom) - val thumbLeft = if (isRtl) { - scrollerPadding.left - } else { - width - scrollerPadding.right - thumbWidth - } + val thumbLeft = + if (isRtl) { + scrollerPadding.left + } else { + width - scrollerPadding.right - thumbWidth + } val thumbTop = scrollerPadding.top + thumbOffset thumbView.layout(thumbLeft, thumbTop, thumbLeft + thumbWidth, thumbTop + thumbHeight) val firstPos = firstAdapterPos - val popupText = if (firstPos != NO_POSITION) { - popupProvider?.invoke(firstPos) ?: "" - } else { - "" - } + val popupText = + if (firstPos != NO_POSITION) { + popupProvider?.invoke(firstPos) ?: "" + } else { + "" + } popupView.isInvisible = popupText.isEmpty() @@ -242,58 +237,67 @@ class FastScrollRecyclerView @JvmOverloads constructor( if (popupView.text != popupText) { popupView.text = popupText - val widthMeasureSpec = ViewGroup.getChildMeasureSpec( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - scrollerPadding.left + scrollerPadding.right + thumbWidth + - popupLayoutParams.leftMargin + popupLayoutParams.rightMargin, - popupLayoutParams.width - ) + val widthMeasureSpec = + ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + scrollerPadding.left + + scrollerPadding.right + + thumbWidth + + popupLayoutParams.leftMargin + + popupLayoutParams.rightMargin, + popupLayoutParams.width) - val heightMeasureSpec = ViewGroup.getChildMeasureSpec( - MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), - scrollerPadding.top + scrollerPadding.bottom + popupLayoutParams.topMargin + - popupLayoutParams.bottomMargin, - popupLayoutParams.height - ) + val heightMeasureSpec = + ViewGroup.getChildMeasureSpec( + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), + scrollerPadding.top + + scrollerPadding.bottom + + popupLayoutParams.topMargin + + popupLayoutParams.bottomMargin, + popupLayoutParams.height) popupView.measure(widthMeasureSpec, heightMeasureSpec) } val popupWidth = popupView.measuredWidth val popupHeight = popupView.measuredHeight - val popupLeft = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { - scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin - } else { - width - scrollerPadding.right - thumbWidth - popupLayoutParams.rightMargin - popupWidth - } + val popupLeft = + if (layoutDirection == View.LAYOUT_DIRECTION_RTL) { + scrollerPadding.left + thumbWidth + popupLayoutParams.leftMargin + } else { + width - + scrollerPadding.right - + thumbWidth - + popupLayoutParams.rightMargin - + popupWidth + } // We handle RTL separately, so it's okay if Gravity.RIGHT is used here @SuppressLint("RtlHardcoded") - val popupAnchorY = when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) { - Gravity.CENTER_HORIZONTAL -> popupHeight / 2 - Gravity.RIGHT -> popupHeight - else -> 0 - } - - val thumbAnchorY = when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) { - Gravity.CENTER_VERTICAL -> { - thumbView.paddingTop + ( - thumbHeight - thumbView.paddingTop - thumbView.paddingBottom - ) / 2 + val popupAnchorY = + when (popupLayoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK) { + Gravity.CENTER_HORIZONTAL -> popupHeight / 2 + Gravity.RIGHT -> popupHeight + else -> 0 } - Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom - else -> thumbView.paddingTop - } - val popupTop = MathUtils.clamp( - thumbTop + thumbAnchorY - popupAnchorY, - scrollerPadding.top + popupLayoutParams.topMargin, - height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight - ) + val thumbAnchorY = + when (popupLayoutParams.gravity and Gravity.VERTICAL_GRAVITY_MASK) { + Gravity.CENTER_VERTICAL -> { + thumbView.paddingTop + + (thumbHeight - thumbView.paddingTop - thumbView.paddingBottom) / 2 + } + Gravity.BOTTOM -> thumbHeight - thumbView.paddingBottom + else -> thumbView.paddingTop + } - popupView.layout( - popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight - ) + val popupTop = + MathUtils.clamp( + thumbTop + thumbAnchorY - popupAnchorY, + scrollerPadding.top + popupLayoutParams.topMargin, + height - scrollerPadding.bottom - popupLayoutParams.bottomMargin - popupHeight) + + popupView.layout(popupLeft, popupTop, popupLeft + popupWidth, popupTop + popupHeight) } } @@ -315,9 +319,10 @@ class FastScrollRecyclerView @JvmOverloads constructor( val bars = insets.systemBarInsetsCompat updatePadding( - initialPadding.left, initialPadding.top, initialPadding.right, - initialPadding.bottom + bars.bottom - ) + initialPadding.left, + initialPadding.top, + initialPadding.right, + initialPadding.bottom + bars.bottom) scrollerPadding.bottom = bars.bottom @@ -358,24 +363,25 @@ class FastScrollRecyclerView @JvmOverloads constructor( if (isInViewTouchTarget(thumbView, eventX, eventY)) { dragStartThumbOffset = thumbOffset } else { - dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt() + dragStartThumbOffset = + (eventY - scrollerPadding.top - thumbHeight / 2f).toInt() scrollToThumbOffset(dragStartThumbOffset) } setDragging(true) } } - MotionEvent.ACTION_MOVE -> { - if (!dragging && isInViewTouchTarget(trackView, downX, downY) && - abs(eventY - downY) > touchSlop - ) { + if (!dragging && + isInViewTouchTarget(trackView, downX, downY) && + abs(eventY - downY) > touchSlop) { if (isInViewTouchTarget(thumbView, downX, downY)) { dragStartY = lastY dragStartThumbOffset = thumbOffset } else { dragStartY = eventY - dragStartThumbOffset = (eventY - scrollerPadding.top - thumbHeight / 2f).toInt() + dragStartThumbOffset = + (eventY - scrollerPadding.top - thumbHeight / 2f).toInt() scrollToThumbOffset(dragStartThumbOffset) } setDragging(true) @@ -386,7 +392,6 @@ class FastScrollRecyclerView @JvmOverloads constructor( scrollToThumbOffset(thumbOffset) } } - MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> setDragging(false) } @@ -433,9 +438,9 @@ class FastScrollRecyclerView @JvmOverloads constructor( private fun scrollToThumbOffset(thumbOffset: Int) { val clampedThumbOffset = MathUtils.clamp(thumbOffset, 0, thumbOffsetRange) - val scrollOffset = ( - scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange - ).toInt() - paddingTop + val scrollOffset = + (scrollOffsetRange.toLong() * clampedThumbOffset / thumbOffsetRange).toInt() - + paddingTop scrollTo(scrollOffset) } @@ -461,7 +466,6 @@ class FastScrollRecyclerView @JvmOverloads constructor( targetPosition *= mgr.spanCount mgr.scrollToPositionWithOffset(targetPosition, trueOffset) } - is LinearLayoutManager -> { mgr.scrollToPositionWithOffset(targetPosition, trueOffset) } @@ -538,10 +542,7 @@ class FastScrollRecyclerView @JvmOverloads constructor( } private fun animateView(view: View, alpha: Float) { - view.animate() - .alpha(alpha) - .setDuration(ANIM_MILLIS) - .start() + view.animate().alpha(alpha).setDuration(ANIM_MILLIS).start() } // --- LAYOUT STATE --- @@ -601,11 +602,12 @@ class FastScrollRecyclerView @JvmOverloads constructor( } private val itemCount: Int - get() = when (val mgr = layoutManager) { - is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1 - is LinearLayoutManager -> mgr.itemCount - else -> 0 - } + get() = + when (val mgr = layoutManager) { + is GridLayoutManager -> (mgr.itemCount - 1) / mgr.spanCount + 1 + is LinearLayoutManager -> mgr.itemCount + else -> 0 + } companion object { private const val ANIM_MILLIS = 150L 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 271283e22..e90872c90 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumListFragment.kt is part of Auxio. * * 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 @@ -15,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.home.list import android.os.Bundle @@ -49,14 +48,12 @@ class AlbumListFragment : HomeListFragment() { binding.lifecycleOwner = viewLifecycleOwner - val adapter = AlbumAdapter( - doOnClick = { album -> - findNavController().navigate( - HomeFragmentDirections.actionShowAlbum(album.id) - ) - }, - ::newMenu - ) + val adapter = + AlbumAdapter( + doOnClick = { album -> + findNavController().navigate(HomeFragmentDirections.actionShowAlbum(album.id)) + }, + ::newMenu) setupRecycler(R.id.home_album_list, binding, adapter, homeModel.albums) @@ -70,16 +67,13 @@ class AlbumListFragment : HomeListFragment() { // Change how we display the popup depending on the mode. when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { // By Name -> Use Name - is Sort.ByName -> album.name.sliceArticle() - .first().uppercase() + is Sort.ByName -> album.name.sliceArticle().first().uppercase() // By Artist -> Use Artist Name - is Sort.ByArtist -> album.artist.resolvedName.sliceArticle() - .first().uppercase() + is Sort.ByArtist -> album.artist.resolvedName.sliceArticle().first().uppercase() // Year -> Use Full Year - is Sort.ByYear -> album.year?.toString() - ?: getString(R.string.def_date) + is Sort.ByYear -> album.year?.toString() ?: getString(R.string.def_date) // Unsupported sort, error gracefully else -> "" 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 eb3d96f19..9a3b6620f 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumListFragment.kt is part of Auxio. * * 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 @@ -15,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.home.list import android.os.Bundle @@ -47,14 +46,12 @@ class ArtistListFragment : HomeListFragment() { binding.lifecycleOwner = viewLifecycleOwner - val adapter = ArtistAdapter( - doOnClick = { artist -> - findNavController().navigate( - HomeFragmentDirections.actionShowArtist(artist.id) - ) - }, - ::newMenu - ) + val adapter = + ArtistAdapter( + doOnClick = { artist -> + findNavController().navigate(HomeFragmentDirections.actionShowArtist(artist.id)) + }, + ::newMenu) setupRecycler(R.id.home_artist_list, binding, adapter, homeModel.artists) @@ -63,8 +60,7 @@ class ArtistListFragment : HomeListFragment() { override val listPopupProvider: (Int) -> String get() = { idx -> - homeModel.artists.value!![idx].resolvedName - .sliceArticle().first().uppercase() + homeModel.artists.value!![idx].resolvedName.sliceArticle().first().uppercase() } class ArtistAdapter( 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 93482d032..70be379ce 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AlbumListFragment.kt is part of Auxio. * * 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 @@ -15,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.home.list import android.os.Bundle @@ -47,14 +46,12 @@ class GenreListFragment : HomeListFragment() { binding.lifecycleOwner = viewLifecycleOwner - val adapter = GenreAdapter( - doOnClick = { Genre -> - findNavController().navigate( - HomeFragmentDirections.actionShowGenre(Genre.id) - ) - }, - ::newMenu - ) + val adapter = + GenreAdapter( + doOnClick = { Genre -> + findNavController().navigate(HomeFragmentDirections.actionShowGenre(Genre.id)) + }, + ::newMenu) setupRecycler(R.id.home_genre_list, binding, adapter, homeModel.genres) @@ -63,8 +60,7 @@ class GenreListFragment : HomeListFragment() { override val listPopupProvider: (Int) -> String get() = { idx -> - homeModel.genres.value!![idx].resolvedName - .sliceArticle().first().uppercase() + homeModel.genres.value!![idx].resolvedName.sliceArticle().first().uppercase() } class GenreAdapter( diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index 9bca96f19..52749d9bf 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * HomeListFragment.kt is part of Auxio. * * 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 @@ -15,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.home.list import android.annotation.SuppressLint @@ -38,9 +37,7 @@ abstract class HomeListFragment : Fragment() { protected val homeModel: HomeViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() - /** - * The popup provider to use for the fast scroller view. - */ + /** The popup provider to use for the fast scroller view. */ abstract val listPopupProvider: (Int) -> String protected fun setupRecycler( @@ -56,18 +53,15 @@ abstract class HomeListFragment : Fragment() { applySpans() popupProvider = listPopupProvider - onDragListener = { dragging -> - homeModel.updateFastScrolling(dragging) - } + onDragListener = { dragging -> homeModel.updateFastScrolling(dragging) } } // Make sure that this RecyclerView has data before startup - homeData.observe(viewLifecycleOwner) { data -> - homeAdapter.updateData(data) - } + homeData.observe(viewLifecycleOwner) { data -> homeAdapter.updateData(data) } } - abstract class HomeAdapter : RecyclerView.Adapter() { + abstract class HomeAdapter : + RecyclerView.Adapter() { protected var data = listOf() @SuppressLint("NotifyDataSetChanged") 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 139d88809..f0068aa15 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SongListFragment.kt is part of Auxio. * * 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 @@ -15,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.home.list import android.os.Bundle @@ -47,12 +46,7 @@ class SongListFragment : HomeListFragment() { binding.lifecycleOwner = viewLifecycleOwner - val adapter = SongsAdapter( - doOnClick = { song -> - playbackModel.playSong(song) - }, - ::newMenu - ) + val adapter = SongsAdapter(doOnClick = { song -> playbackModel.playSong(song) }, ::newMenu) setupRecycler(R.id.home_song_list, binding, adapter, homeModel.songs) @@ -68,21 +62,17 @@ class SongListFragment : HomeListFragment() { // based off the names of the parent objects and not the child objects. when (homeModel.getSortForDisplay(DisplayMode.SHOW_SONGS)) { // Name -> Use name - is Sort.ByName -> song.name.sliceArticle() - .first().uppercase() + is Sort.ByName -> song.name.sliceArticle().first().uppercase() // Artist -> Use Artist Name is Sort.ByArtist -> - song.album.artist.resolvedName - .sliceArticle().first().uppercase() + song.album.artist.resolvedName.sliceArticle().first().uppercase() // Album -> Use Album Name - is Sort.ByAlbum -> song.album.name.sliceArticle() - .first().uppercase() + is Sort.ByAlbum -> song.album.name.sliceArticle().first().uppercase() // Year -> Use Full Year - is Sort.ByYear -> song.album.year?.toString() - ?: getString(R.string.def_date) + is Sort.ByYear -> song.album.year?.toString() ?: getString(R.string.def_date) } } 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 3f8a30686..8c234f5a0 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Tab.kt is part of Auxio. * * 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 @@ -15,24 +14,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.home.tabs import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.util.logE /** - * A data representation of a library tab. - * A tab can come in two moves, [Visible] or [Invisible]. Invisibility means that the tab - * will still be present in the customization menu, but will not be shown on the home UI. + * A data representation of a library tab. A tab can come in two moves, [Visible] or [Invisible]. + * Invisibility means that the tab will still be present in the customization menu, but will not be + * shown on the home UI. * * Like other IO-bound datatypes in Auxio, tabs are stored in a binary format. However, tabs cannot * be serialized on their own. Instead, they are saved as a sequence of tabs as shown below: * * 0bTAB1_TAB2_TAB3_TAB4_TAB5 * - * Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists. - * Each chunk in a sequence is represented as: + * Where TABN is a chunk representing a tab at position N. TAB5 is reserved for playlists. Each + * chunk in a sequence is represented as: * * VTTT * @@ -49,14 +48,12 @@ sealed class Tab(open val mode: DisplayMode) { data class Invisible(override val mode: DisplayMode) : Tab(mode) companion object { - /** The length a well-formed tab sequence should be **/ + /** The length a well-formed tab sequence should be */ const val SEQUENCE_LEN = 4 - /** The default tab sequence, represented in integer form **/ + /** The default tab sequence, represented in integer form */ const val SEQUENCE_DEFAULT = 0b1000_1001_1010_1011_0100 - /** - * Convert an array [tabs] into a sequence of tabs. - */ + /** Convert an array [tabs] into a sequence of tabs. */ fun toSequence(tabs: Array): Int { // Like when deserializing, make sure there are no duplicate tabs for whatever reason. val distinct = tabs.distinctBy { it.mode } @@ -65,10 +62,11 @@ sealed class Tab(open val mode: DisplayMode) { var shift = SEQUENCE_LEN * 4 for (tab in distinct) { - val bin = when (tab) { - is Visible -> 1.shl(3) or tab.mode.ordinal - is Invisible -> tab.mode.ordinal - } + val bin = + when (tab) { + is Visible -> 1.shl(3) or tab.mode.ordinal + is Invisible -> tab.mode.ordinal + } sequence = sequence or bin.shl(shift) shift -= 4 @@ -77,9 +75,7 @@ sealed class Tab(open val mode: DisplayMode) { return sequence } - /** - * Convert a [sequence] into an array of tabs. - */ + /** Convert a [sequence] into an array of tabs. */ fun fromSequence(sequence: Int): Array? { val tabs = mutableListOf() @@ -88,20 +84,22 @@ sealed class Tab(open val mode: DisplayMode) { for (shift in (0..4 * SEQUENCE_LEN).reversed() step 4) { val chunk = sequence.shr(shift) and 0b1111 - val mode = when (chunk and 7) { - 0 -> DisplayMode.SHOW_SONGS - 1 -> DisplayMode.SHOW_ALBUMS - 2 -> DisplayMode.SHOW_ARTISTS - 3 -> DisplayMode.SHOW_GENRES - else -> continue - } + val mode = + when (chunk and 7) { + 0 -> DisplayMode.SHOW_SONGS + 1 -> DisplayMode.SHOW_ALBUMS + 2 -> DisplayMode.SHOW_ARTISTS + 3 -> DisplayMode.SHOW_GENRES + else -> continue + } // Figure out the visibility - tabs += if (chunk and 1.shl(3) != 0) { - Visible(mode) - } else { - Invisible(mode) - } + tabs += + if (chunk and 1.shl(3) != 0) { + Visible(mode) + } else { + Invisible(mode) + } } // Make sure there are no duplicate tabs 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 401663c45..1a6dfbe31 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * TabAdapter.kt is part of Auxio. * * 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 @@ -15,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.home.tabs import android.annotation.SuppressLint @@ -31,7 +30,8 @@ class TabAdapter( private val getTabs: () -> Array, private val onTabSwitch: (Tab) -> Unit, ) : RecyclerView.Adapter() { - private val tabs: Array get() = getTabs() + private val tabs: Array + get() = getTabs() override fun getItemCount(): Int = Tab.SEQUENCE_LEN @@ -43,13 +43,12 @@ class TabAdapter( holder.bind(tabs[position]) } - inner class TabViewHolder( - private val binding: ItemTabBinding - ) : RecyclerView.ViewHolder(binding.root) { + inner class TabViewHolder(private val binding: ItemTabBinding) : + RecyclerView.ViewHolder(binding.root) { init { - binding.root.layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT - ) + binding.root.layoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) } @SuppressLint("ClickableViewAccessibility") 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 93b478f7d..b303c307b 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * CustomizeListDialog.kt is part of Auxio. * * 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 @@ -15,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.home.tabs import android.os.Bundle @@ -32,8 +31,8 @@ import org.oxycblt.auxio.ui.LifecycleDialog import org.oxycblt.auxio.util.logD /** - * The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel - * and serializes it's state instead of + * The dialog for customizing library tabs. This dialog does not rely on any specific ViewModel and + * serializes it's state instead of * @author OxygenCobalt */ class TabCustomizeDialog : LifecycleDialog() { @@ -58,27 +57,28 @@ class TabCustomizeDialog : LifecycleDialog() { // Set up adapter & drag callback val callback = TabDragCallback { pendingTabs } val helper = ItemTouchHelper(callback) - val tabAdapter = TabAdapter( - helper, - getTabs = { pendingTabs }, - onTabSwitch = { tab -> - // Don't find the specific tab [Which might be outdated due to the nature - // of how ViewHolders are bound], but instead simply look for the mode in - // the list of pending tabs and update that instead. - val index = pendingTabs.indexOfFirst { it.mode == tab.mode } - if (index != -1) { - val curTab = pendingTabs[index] - logD("Updating tab $curTab to $tab") - pendingTabs[index] = when (curTab) { - is Tab.Visible -> Tab.Invisible(curTab.mode) - is Tab.Invisible -> Tab.Visible(curTab.mode) + val tabAdapter = + TabAdapter( + helper, + getTabs = { pendingTabs }, + onTabSwitch = { tab -> + // Don't find the specific tab [Which might be outdated due to the nature + // of how ViewHolders are bound], but instead simply look for the mode in + // the list of pending tabs and update that instead. + val index = pendingTabs.indexOfFirst { it.mode == tab.mode } + if (index != -1) { + val curTab = pendingTabs[index] + logD("Updating tab $curTab to $tab") + pendingTabs[index] = + when (curTab) { + is Tab.Visible -> Tab.Invisible(curTab.mode) + is Tab.Invisible -> Tab.Visible(curTab.mode) + } } - } - (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = - pendingTabs.filterIsInstance().isNotEmpty() - } - ) + (requireDialog() as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE) + .isEnabled = pendingTabs.filterIsInstance().isNotEmpty() + }) callback.addTabAdapter(tabAdapter) 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 f9de5ac79..e0e071d08 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * QueueDragCallback.kt is part of Auxio. * * 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 @@ -15,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.home.tabs import android.graphics.Canvas @@ -24,21 +23,18 @@ import androidx.recyclerview.widget.RecyclerView /** * A simple [ItemTouchHelper.Callback] that handles dragging items in the tab customization menu. - * Unlike QueueAdapter's ItemTouchHelper, this one is bare and simple. - * TODO: Consider unifying the shared behavior between this and QueueDragCallback into a single - * class. + * Unlike QueueAdapter's ItemTouchHelper, this one is bare and simple. TODO: Consider unifying the + * shared behavior between this and QueueDragCallback into a single class. */ class TabDragCallback(private val getTabs: () -> Array) : ItemTouchHelper.Callback() { - private val tabs: Array get() = getTabs() + private val tabs: Array + get() = getTabs() private lateinit var tabAdapter: TabAdapter override fun getMovementFlags( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder - ): Int = makeFlag( - ItemTouchHelper.ACTION_STATE_DRAG, - ItemTouchHelper.UP or ItemTouchHelper.DOWN - ) + ): Int = makeFlag(ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) override fun onChildDraw( c: Canvas, @@ -76,8 +72,8 @@ class TabDragCallback(private val getTabs: () -> Array) : ItemTouchHelper.C override fun isLongPressDragEnabled(): Boolean = false /** - * Add the tab adapter to this callback. - * Done because there's a circular dependency between the two objects + * Add the tab adapter to this callback. Done because there's a circular dependency between the + * two objects */ fun addTabAdapter(adapter: TabAdapter) { tabAdapter = adapter diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index 13504b1b3..a1ebae9c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Models.kt is part of Auxio. * * 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 @@ -15,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.music import android.content.ContentUris @@ -27,18 +26,15 @@ import androidx.annotation.StringRes // --- MUSIC MODELS --- -/** - * The base for all items in Auxio. - */ +/** The base for all items in Auxio. */ sealed class Item { /** A unique ID for this item. ***THIS IS NOT A MEDIASTORE ID!** */ abstract val id: Long } /** - * [Item] variant that represents a music item. - * TODO: Make name the actual display name and move raw names (including file names) to a new - * field called rawName. + * [Item] variant that represents a music item. TODO: Make name the actual display name and move raw + * names (including file names) to a new field called rawName. */ sealed class Music : Item() { /** The raw name of this item. */ @@ -46,21 +42,19 @@ sealed class Music : Item() { } /** - * [Music] variant that denotes that this object is a parent of other data objects, such - * as an [Album] or [Artist] + * [Music] variant that denotes that this object is a parent of other data objects, such as an + * [Album] or [Artist] * @property resolvedName */ sealed class MusicParent : Music() { /** - * A name resolved from it's raw form to a form suitable to be shown in a ui. - * Ex. "unknown" would become Unknown Artist, (124) would become its proper genre name, etc. + * A name resolved from it's raw form to a form suitable to be shown in a ui. Ex. "unknown" + * would become Unknown Artist, (124) would become its proper genre name, etc. */ abstract val resolvedName: String } -/** - * The data object for a song. - */ +/** The data object for a song. */ data class Song( override val name: String, /** The file name of this song, excluding the full path. */ @@ -82,57 +76,69 @@ data class Song( /** Internal field. Do not use. */ val internalMediaStoreAlbumArtistName: String?, ) : Music() { - override val id: Long get() { - var result = name.hashCode().toLong() - result = 31 * result + album.name.hashCode() - result = 31 * result + album.artist.name.hashCode() - result = 31 * result + (track ?: 0) - result = 31 * result + duration.hashCode() - return result - } + override val id: Long + get() { + var result = name.hashCode().toLong() + result = 31 * result + album.name.hashCode() + result = 31 * result + album.artist.name.hashCode() + result = 31 * result + (track ?: 0) + result = 31 * result + duration.hashCode() + return result + } /** The URI for this song. */ - val uri: Uri get() = ContentUris.withAppendedId( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId - ) + val uri: Uri + get() = + ContentUris.withAppendedId( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, internalMediaStoreId) /** The duration of this song, in seconds (rounded down) */ - val seconds: Long get() = duration / 1000 + val seconds: Long + get() = duration / 1000 /** The seconds of this song, but as a duration. */ - val formattedDuration: String get() = seconds.toDuration(false) + val formattedDuration: String + get() = seconds.toDuration(false) private var mAlbum: Album? = null /** The album of this song. */ - val album: Album get() = requireNotNull(mAlbum) + val album: Album + get() = requireNotNull(mAlbum) private var mGenre: Genre? = null /** The genre of this song. Will be an "unknown genre" if the song does not have any. */ - val genre: Genre get() = requireNotNull(mGenre) + val genre: Genre + get() = requireNotNull(mGenre) /** An album name resolved to this song in particular. */ - val resolvedAlbumName: String get() = - album.resolvedName + val resolvedAlbumName: String + get() = album.resolvedName /** An artist name resolved to this song in particular. */ - val resolvedArtistName: String get() = - internalMediaStoreArtistName ?: album.artist.resolvedName + val resolvedArtistName: String + get() = internalMediaStoreArtistName ?: album.artist.resolvedName /** Internal field. Do not use. */ - val internalAlbumGroupingId: Long get() { - var result = internalGroupingArtistName.lowercase().hashCode().toLong() - result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode() - return result - } + val internalAlbumGroupingId: Long + get() { + var result = internalGroupingArtistName.lowercase().hashCode().toLong() + result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode() + return result + } /** Internal field. Do not use. */ - val internalGroupingArtistName: String get() = internalMediaStoreAlbumArtistName - ?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING + val internalGroupingArtistName: String + get() = + internalMediaStoreAlbumArtistName + ?: internalMediaStoreArtistName ?: MediaStore.UNKNOWN_STRING /** Internal field. Do not use. */ - val internalIsMissingAlbum: Boolean get() = mAlbum == null + val internalIsMissingAlbum: Boolean + get() = mAlbum == null /** Internal field. Do not use. */ - val internalIsMissingArtist: Boolean get() = mAlbum?.internalIsMissingArtist ?: true - /** Internal field. Do not use. **/ - val internalIsMissingGenre: Boolean get() = mGenre == null + val internalIsMissingArtist: Boolean + get() = mAlbum?.internalIsMissingArtist ?: true + /** Internal field. Do not use. */ + val internalIsMissingGenre: Boolean + get() = mGenre == null /** Internal method. Do not use. */ fun internalLinkAlbum(album: Album) { @@ -145,9 +151,7 @@ data class Song( } } -/** - * The data object for an album. - */ +/** The data object for an album. */ data class Album( override val name: String, /** The latest year of the songs in this album. Null if none of the songs had metadata. */ @@ -165,34 +169,37 @@ data class Album( } } - override val id: Long get() { - var result = name.hashCode().toLong() - result = 31 * result + artist.name.hashCode() - result = 31 * result + (year ?: 0) - return result - } + override val id: Long + get() { + var result = name.hashCode().toLong() + result = 31 * result + artist.name.hashCode() + result = 31 * result + (year ?: 0) + return result + } override val resolvedName: String get() = name /** The formatted total duration of this album */ - val totalDuration: String get() = - songs.sumOf { it.seconds }.toDuration(false) + val totalDuration: String + get() = songs.sumOf { it.seconds }.toDuration(false) private var mArtist: Artist? = null /** The parent artist of this album. */ - val artist: Artist get() = requireNotNull(mArtist) + val artist: Artist + get() = requireNotNull(mArtist) /** The artist name, resolved to this album in particular. */ - val resolvedArtistName: String get() = - artist.resolvedName + val resolvedArtistName: String + get() = artist.resolvedName /** Internal field. Do not use. */ - val internalArtistGroupingId: Long get() = - internalGroupingArtistName.lowercase().hashCode().toLong() + val internalArtistGroupingId: Long + get() = internalGroupingArtistName.lowercase().hashCode().toLong() /** Internal field. Do not use. */ - val internalIsMissingArtist: Boolean get() = mArtist == null + val internalIsMissingArtist: Boolean + get() = mArtist == null /** Internal method. Do not use. */ fun internalLinkArtist(artist: Artist) { @@ -201,8 +208,8 @@ data class Album( } /** - * The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) - * album artist or artist field, not the individual performers of an artist. + * The [MusicParent] for an *album* artist. This reflects a group of songs with the same(ish) album + * artist or artist field, not the individual performers of an artist. */ data class Artist( override val name: String, @@ -222,9 +229,7 @@ data class Artist( val songs = albums.flatMap { it.songs } } -/** - * The data object for a genre. - */ +/** The data object for a genre. */ data class Genre( override val name: String, override val resolvedName: String, @@ -239,13 +244,11 @@ data class Genre( override val id = name.hashCode().toLong() /** The formatted total duration of this genre */ - val totalDuration: String get() = - songs.sumOf { it.seconds }.toDuration(false) + val totalDuration: String + get() = songs.sumOf { it.seconds }.toDuration(false) } -/** - * A data object used solely for the "Header" UI element. - */ +/** A data object used solely for the "Header" UI element. */ data class Header( override val id: Long, /** The string resource used for the header. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index 5f4debfa2..a7eb4da02 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.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 import android.content.ContentResolver @@ -15,53 +32,52 @@ import org.oxycblt.auxio.util.logD /** * This class acts as the base for most the black magic required to get a remotely sensible music * indexing system while still optimizing for time. I would recommend you leave this module now - * before you lose your sanity trying to understand the hoops I had to jump through for this - * system, but if you really want to stay, here's a debrief on why this code is so awful. + * before you lose your sanity trying to understand the hoops I had to jump through for this system, + * but if you really want to stay, here's a debrief on why this code is so awful. * * MediaStore is not a good API. It is not even a bad API. Calling it a bad API is an insult to - * other bad android APIs, like CoordinatorLayout or InputMethodManager. No. MediaStore is a - * crime against humanity and probably a way to summon Zalgo if you look at it the wrong way. + * other bad android APIs, like CoordinatorLayout or InputMethodManager. No. MediaStore is a crime + * against humanity and probably a way to summon Zalgo if you look at it the wrong way. * - * You think that if you wanted to query a song's genre from a media database, you could just - * put "genre" in the query and it would return it, right? But not with MediaStore! No, that's - * too straightforward for this contract that was dropped on it's head as a baby. So instead, you - * have to query for each genre, query all the songs in each genre, and then iterate through those - * songs to link every song with their genre. This is not documented anywhere, and the - * O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's - * loading times. At no point have the devs considered that this system is absolutely insane, and - * instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their - * own Google Play Music, and of course every Google Play Music user knew how great that turned - * out! + * You think that if you wanted to query a song's genre from a media database, you could just put + * "genre" in the query and it would return it, right? But not with MediaStore! No, that's too + * straightforward for this contract that was dropped on it's head as a baby. So instead, you have + * to query for each genre, query all the songs in each genre, and then iterate through those songs + * to link every song with their genre. This is not documented anywhere, and the O(mom im scared) + * algorithm you have to run to get it working single-handedly DOUBLES Auxio's loading times. At no + * point have the devs considered that this system is absolutely insane, and instead focused on + * adding infuriat- I mean nice proprietary extensions to MediaStore for their own Google Play + * Music, and of course every Google Play Music user knew how great that turned out! * * It's not even ergonomics that makes this API bad. It's base implementation is completely borked - * as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files? - * I sure didn't, until I decided to upgrade my music collection to ID3v2.4 and FLAC only to see - * that the metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or - * DATE tag. Once again, this is because internally android uses an ancient in-house metadata - * parser to get everything indexed, and so far they have not bothered to modernize this parser - * or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 has - * been around for *21 years.* *It can drink now.* All of my what. + * as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files? I + * sure didn't, until I decided to upgrade my music collection to ID3v2.4 and FLAC only to see that + * the metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or DATE tag. + * Once again, this is because internally android uses an ancient in-house metadata parser to get + * everything indexed, and so far they have not bothered to modernize this parser or even switch it + * to something more powerful like Taglib, not even in Android 12. ID3v2.4 has been around for *21 + * years.* *It can drink now.* All of my what. * * Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums - * table, so we have to go for the less efficient "make a big query on all the songs lol" method - * so that songs don't end up fragmented across artists. Pretty much every OEM has added some - * extension or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH) - * crippling the normal tables so that you're railroaded into their music app. The way I do - * blacklisting relies on a semi-deprecated method, and the supposedly "modern" method is SLOWER and - * causes even more problems since I have to manage databases across version boundaries. Sometimes - * music will have a deformed clone that I can't filter out, sometimes Genres will just break for - * no reason, and sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to - * Latin-1 to *Shift JIS* WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY + * table, so we have to go for the less efficient "make a big query on all the songs lol" method so + * that songs don't end up fragmented across artists. Pretty much every OEM has added some extension + * or quirk to MediaStore that I cannot reproduce, with some OEMs (COUGHSAMSUNGCOUGH) crippling the + * normal tables so that you're railroaded into their music app. The way I do blacklisting relies on + * a semi-deprecated method, and the supposedly "modern" method is SLOWER and causes even more + * problems since I have to manage databases across version boundaries. Sometimes music will have a + * deformed clone that I can't filter out, sometimes Genres will just break for no reason, and + * sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to Latin-1 to *Shift + * JIS* WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY * - * Is there anything we can do about it? No. Google has routinely shut down issues that begged google - * to fix glaring issues with MediaStore or to just take the API behind the woodshed and shoot it. - * Largely because they have zero incentive to improve it given how "obscure" local music listening - * is. As a result, some players like Vanilla and VLC just hack their own pseudo-MediaStore - * implementation from their own (better) parsers, but this is both infeasible for Auxio due to how - * incredibly slow it is to get a file handle from the android sandbox AND how much harder it is to - * manage a database of your own media that mirrors the filesystem perfectly. And even if I set - * aside those crippling issues and changed my indexer to that, it would face the even larger - * problem of how google keeps trying to kill the filesystem and force you into their + * Is there anything we can do about it? No. Google has routinely shut down issues that begged + * google to fix glaring issues with MediaStore or to just take the API behind the woodshed and + * shoot it. Largely because they have zero incentive to improve it given how "obscure" local music + * listening is. As a result, some players like Vanilla and VLC just hack their own + * pseudo-MediaStore implementation from their own (better) parsers, but this is both infeasible for + * Auxio due to how incredibly slow it is to get a file handle from the android sandbox AND how much + * harder it is to manage a database of your own media that mirrors the filesystem perfectly. And + * even if I set aside those crippling issues and changed my indexer to that, it would face the even + * larger problem of how google keeps trying to kill the filesystem and force you into their * ContentResolver API. In the future MediaStore could be the only system we have, which is also the * day that greenland melts and birthdays stop happening forever. * @@ -94,38 +110,30 @@ class MusicLoader { for (song in songs) { if (song.internalIsMissingAlbum || song.internalIsMissingArtist || - song.internalIsMissingGenre - ) { + song.internalIsMissingGenre) { throw IllegalStateException( "Found malformed song: ${song.name} [" + "album: ${!song.internalIsMissingAlbum} " + "artist: ${!song.internalIsMissingArtist} " + - "genre: ${!song.internalIsMissingGenre}]" - ) + "genre: ${!song.internalIsMissingGenre}]") } } - return Library( - genres, - artists, - albums, - songs - ) + return Library(genres, artists, albums, songs) } /** - * Gets a content resolver in a way that does not mangle metadata on - * certain OEM skins. See https://github.com/OxygenCobalt/Auxio/issues/50 - * for more info. + * Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See + * https://github.com/OxygenCobalt/Auxio/issues/50 for more info. */ - private val Context.contentResolverSafe: ContentResolver get() = - applicationContext.contentResolver + private val Context.contentResolverSafe: ContentResolver + get() = applicationContext.contentResolver /** - * Does the initial query over the song database, including excluded directory - * checks. The songs returned by this function are **not** well-formed. The - * companion [buildAlbums], [buildArtists], and [readGenres] functions must be - * called with the returned list so that all songs are properly linked up. + * Does the initial query over the song database, including excluded directory checks. The songs + * returned by this function are **not** well-formed. The companion [buildAlbums], + * [buildArtists], and [readGenres] functions must be called with the returned list so that all + * songs are properly linked up. */ private fun loadSongs(context: Context): List { val blacklistDatabase = ExcludedDatabase.getInstance(context) @@ -146,89 +154,100 @@ class MusicLoader { var songs = mutableListOf() context.contentResolverSafe.query( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - arrayOf( - MediaStore.Audio.AudioColumns._ID, - MediaStore.Audio.AudioColumns.TITLE, - MediaStore.Audio.AudioColumns.DISPLAY_NAME, - MediaStore.Audio.AudioColumns.TRACK, - MediaStore.Audio.AudioColumns.DURATION, - MediaStore.Audio.AudioColumns.YEAR, - MediaStore.Audio.AudioColumns.ALBUM, - MediaStore.Audio.AudioColumns.ALBUM_ID, - MediaStore.Audio.AudioColumns.ARTIST, - AUDIO_COLUMN_ALBUM_ARTIST - ), - selector, args.toTypedArray(), null - )?.use { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) - val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) - val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) - val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) - val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) - val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) - val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) - val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) - val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) - val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + arrayOf( + MediaStore.Audio.AudioColumns._ID, + MediaStore.Audio.AudioColumns.TITLE, + MediaStore.Audio.AudioColumns.DISPLAY_NAME, + MediaStore.Audio.AudioColumns.TRACK, + MediaStore.Audio.AudioColumns.DURATION, + MediaStore.Audio.AudioColumns.YEAR, + MediaStore.Audio.AudioColumns.ALBUM, + MediaStore.Audio.AudioColumns.ALBUM_ID, + MediaStore.Audio.AudioColumns.ARTIST, + AUDIO_COLUMN_ALBUM_ARTIST), + selector, + args.toTypedArray(), + null) + ?.use { cursor -> + val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) + val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) + val fileIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) + val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) + val durationIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) + val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) + val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) + val albumIdIndex = + cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) + val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) + val albumArtistIndex = cursor.getColumnIndexOrThrow(AUDIO_COLUMN_ALBUM_ARTIST) - while (cursor.moveToNext()) { - val id = cursor.getLong(idIndex) - val title = cursor.getString(titleIndex) - val fileName = cursor.getString(fileIndex) + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + val title = cursor.getString(titleIndex) + val fileName = cursor.getString(fileIndex) - // The TRACK field is for some reason formatted as DTTT, where D is the disk - // and T is the track. This is dumb and insane and forces me to mangle track - // numbers above 1000, but there is nothing we can do that won't break the app - // below API 30. - // TODO: Disk number support? - val track = cursor.getIntOrNull(trackIndex)?.mod(1000) + // The TRACK field is for some reason formatted as DTTT, where D is the disk + // and T is the track. This is dumb and insane and forces me to mangle track + // numbers above 1000, but there is nothing we can do that won't break the app + // below API 30. + // TODO: Disk number support? + val track = cursor.getIntOrNull(trackIndex)?.mod(1000) - val duration = cursor.getLong(durationIndex) - val year = cursor.getIntOrNull(yearIndex) + val duration = cursor.getLong(durationIndex) + val year = cursor.getIntOrNull(yearIndex) - val album = cursor.getString(albumIndex) - val albumId = cursor.getLong(albumIdIndex) + val album = cursor.getString(albumIndex) + val albumId = cursor.getLong(albumIdIndex) - // If the artist field is , make it null. This makes handling the - // insanity of the artist field easier later on. - val artist = cursor.getStringOrNull(artistIndex)?.run { - if (this == MediaStore.UNKNOWN_STRING) { - null - } else { - this - } + // If the artist field is , make it null. This makes handling the + // insanity of the artist field easier later on. + val artist = + cursor.getStringOrNull(artistIndex)?.run { + if (this == MediaStore.UNKNOWN_STRING) { + null + } else { + this + } + } + + val albumArtist = cursor.getStringOrNull(albumArtistIndex) + + // Note: Directory parsing is currently disabled until artist images are added. + // val dirs = cursor.getStringOrNull(dataIndex)?.run { + // substringBeforeLast("/", "").ifEmpty { null } + // } + + songs.add( + Song( + title, + fileName, + duration, + track, + id, + year, + album, + albumId, + artist, + albumArtist, + )) } - - val albumArtist = cursor.getStringOrNull(albumArtistIndex) - - // Note: Directory parsing is currently disabled until artist images are added. - // val dirs = cursor.getStringOrNull(dataIndex)?.run { - // substringBeforeLast("/", "").ifEmpty { null } - // } - - songs.add( - Song( - title, - fileName, - duration, - track, - id, - year, - album, - albumId, - artist, - albumArtist, - ) - ) } - } // Deduplicate songs to prevent (most) deformed music clones - songs = songs.distinctBy { - it.name to it.internalMediaStoreAlbumName to it.internalMediaStoreArtistName to - it.internalMediaStoreAlbumArtistName to it.track to it.duration - }.toMutableList() + songs = + songs + .distinctBy { + it.name to + it.internalMediaStoreAlbumName to + it.internalMediaStoreArtistName to + it.internalMediaStoreAlbumArtistName to + it.track to + it.duration + } + .toMutableList() logD("Successfully loaded ${songs.size} songs") @@ -236,17 +255,17 @@ class MusicLoader { } /** - * Group songs up into their respective albums. Instead of using the unreliable album or - * artist databases, we instead group up songs by their *lowercase* artist and album name - * to create albums. This serves two purposes: - * 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN". - * This makes sure both of those are resolved into a single artist called "Rammstein" - * 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This - * ensures that all songs are unified under a single album. + * Group songs up into their respective albums. Instead of using the unreliable album or artist + * databases, we instead group up songs by their *lowercase* artist and album name to create + * albums. This serves two purposes: + * 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN". This + * makes sure both of those are resolved into a single artist called "Rammstein" + * 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This ensures + * that all songs are unified under a single album. * - * This does come with some costs, it's far slower than using the album ID itself, and - * it may result in an unrelated album art being selected depending on the song chosen - * as the template, but it seems to work pretty well. + * This does come with some costs, it's far slower than using the album ID itself, and it may + * result in an unrelated album art being selected depending on the song chosen as the template, + * but it seems to work pretty well. */ private fun buildAlbums(songs: List): List { val albums = mutableListOf() @@ -259,15 +278,14 @@ class MusicLoader { // This allows us to replicate the LAST_YEAR field, which is useful as it means that // weird years like "0" wont show up if there are alternatives. // TODO: Weigh songs with null years lower than songs with zero years - val templateSong = requireNotNull( - albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 } - ) + val templateSong = + requireNotNull(albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 }) val albumName = templateSong.internalMediaStoreAlbumName val albumYear = templateSong.internalMediaStoreYear - val albumCoverUri = ContentUris.withAppendedId( - Uri.parse("content://media/external/audio/albumart"), - templateSong.internalMediaStoreAlbumId - ) + val albumCoverUri = + ContentUris.withAppendedId( + Uri.parse("content://media/external/audio/albumart"), + templateSong.internalMediaStoreAlbumId) val artistName = templateSong.internalGroupingArtistName albums.add( @@ -277,8 +295,7 @@ class MusicLoader { albumCoverUri, albumSongs, artistName, - ) - ) + )) } logD("Successfully built ${albums.size} albums") @@ -287,8 +304,8 @@ class MusicLoader { } /** - * Group up albums into artists. This also requires a de-duplication step due to some - * edge cases where [buildAlbums] could not detect duplicates. + * Group up albums into artists. This also requires a de-duplication step due to some edge cases + * where [buildAlbums] could not detect duplicates. */ private fun buildArtists(context: Context, albums: List): List { val artists = mutableListOf() @@ -297,19 +314,14 @@ class MusicLoader { for (entry in albumsByArtist) { val templateAlbum = entry.value[0] val artistName = templateAlbum.internalGroupingArtistName - val resolvedName = when (templateAlbum.internalGroupingArtistName) { - MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist) - else -> artistName - } + val resolvedName = + when (templateAlbum.internalGroupingArtistName) { + MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist) + else -> artistName + } val artistAlbums = entry.value - artists.add( - Artist( - artistName, - resolvedName, - artistAlbums - ) - ) + artists.add(Artist(artistName, resolvedName, artistAlbums)) } logD("Successfully built ${artists.size} artists") @@ -318,50 +330,45 @@ class MusicLoader { } /** - * Read all genres and link them up to the given songs. This is the code that - * requires me to make dozens of useless queries just to link genres up. + * Read all genres and link them up to the given songs. This is the code that requires me to + * make dozens of useless queries just to link genres up. */ private fun readGenres(context: Context, songs: List): List { val genres = mutableListOf() context.contentResolverSafe.query( - MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, - arrayOf( - MediaStore.Audio.Genres._ID, - MediaStore.Audio.Genres.NAME - ), - null, null, null - )?.use { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) - val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) + MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, + arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME), + null, + null, + null) + ?.use { cursor -> + val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) + val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) - while (cursor.moveToNext()) { - // Genre names can be a normal name, an ID3v2 constant, or null. Normal names are - // resolved as usual, but null values don't make sense and are often junk anyway, - // so we skip genres that have them. - val id = cursor.getLong(idIndex) - val name = cursor.getStringOrNull(nameIndex) ?: continue - val resolvedName = name.genreNameCompat ?: name - val genreSongs = queryGenreSongs(context, id, songs) ?: continue + while (cursor.moveToNext()) { + // Genre names can be a normal name, an ID3v2 constant, or null. Normal names + // are + // resolved as usual, but null values don't make sense and are often junk + // anyway, + // so we skip genres that have them. + val id = cursor.getLong(idIndex) + val name = cursor.getStringOrNull(nameIndex) ?: continue + val resolvedName = name.genreNameCompat ?: name + val genreSongs = queryGenreSongs(context, id, songs) ?: continue - genres.add( - Genre( - name, - resolvedName, - genreSongs - ) - ) + genres.add(Genre(name, resolvedName, genreSongs)) + } } - } val songsWithoutGenres = songs.filter { it.internalIsMissingGenre } if (songsWithoutGenres.isNotEmpty()) { // Songs that don't have a genre will be thrown into an unknown genre. - val unknownGenre = Genre( - name = MediaStore.UNKNOWN_STRING, - resolvedName = context.getString(R.string.def_genre), - songsWithoutGenres - ) + val unknownGenre = + Genre( + name = MediaStore.UNKNOWN_STRING, + resolvedName = context.getString(R.string.def_genre), + songsWithoutGenres) genres.add(unknownGenre) } @@ -372,60 +379,63 @@ class MusicLoader { } /** - * Decodes the genre name from an ID3(v2) constant. See [genreConstantTable] for the - * genre constant map that Auxio uses. + * Decodes the genre name from an ID3(v2) constant. See [genreConstantTable] for the genre + * constant map that Auxio uses. */ - private val String.genreNameCompat: String? get() { - if (isDigitsOnly()) { - // ID3v1, just parse as an integer - return genreConstantTable.getOrNull(toInt()) - } - - if (startsWith('(') && endsWith(')')) { - // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer - // Any genres formatted as "(CHARS)" will be ignored. - val genreInt = substring(1 until lastIndex).toIntOrNull() - if (genreInt != null) { - return genreConstantTable.getOrNull(genreInt) + private val String.genreNameCompat: String? + get() { + if (isDigitsOnly()) { + // ID3v1, just parse as an integer + return genreConstantTable.getOrNull(toInt()) } - } - // Current name is fine. - return null - } + if (startsWith('(') && endsWith(')')) { + // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer + // Any genres formatted as "(CHARS)" will be ignored. + val genreInt = substring(1 until lastIndex).toIntOrNull() + if (genreInt != null) { + return genreConstantTable.getOrNull(genreInt) + } + } + + // Current name is fine. + return null + } /** - * Queries the genre songs for [genreId]. Some genres are insane and don't contain songs - * for some reason, so if that's the case then this function will return null. + * Queries the genre songs for [genreId]. Some genres are insane and don't contain songs for + * some reason, so if that's the case then this function will return null. */ private fun queryGenreSongs(context: Context, genreId: Long, songs: List): List? { val genreSongs = mutableListOf() // Don't even bother blacklisting here as useless iterations are less expensive than IO context.contentResolverSafe.query( - MediaStore.Audio.Genres.Members.getContentUri("external", genreId), - arrayOf(MediaStore.Audio.Genres.Members._ID), - null, null, null - )?.use { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) + MediaStore.Audio.Genres.Members.getContentUri("external", genreId), + arrayOf(MediaStore.Audio.Genres.Members._ID), + null, + null, + null) + ?.use { cursor -> + val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) - while (cursor.moveToNext()) { - val id = cursor.getLong(idIndex) - songs.find { it.internalMediaStoreId == id }?.let { song -> - genreSongs.add(song) + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + songs.find { it.internalMediaStoreId == id }?.let { song -> + genreSongs.add(song) + } } } - } return genreSongs.ifEmpty { null } } companion object { /** - * The album_artist MediaStore field has existed since at least API 21, but until API - * 30 it was a proprietary extension for Google Play Music and was not documented. - * Since this field probably works on all versions Auxio supports, we suppress the - * warning about using a possibly-unsupported constant. + * The album_artist MediaStore field has existed since at least API 21, but until API 30 it + * was a proprietary extension for Google Play Music and was not documented. Since this + * field probably works on all versions Auxio supports, we suppress the warning about using + * a possibly-unsupported constant. */ @Suppress("InlinedApi") private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST @@ -434,42 +444,205 @@ class MusicLoader { * A complete table of all the constant genre values for ID3(v2), including non-standard * extensions. */ - private val genreConstantTable = arrayOf( - // ID3 Standard - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", - "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", - "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", - "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", - "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", - "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", - "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", - "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", - "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", - "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", + private val genreConstantTable = + arrayOf( + // ID3 Standard + "Blues", + "Classic Rock", + "Country", + "Dance", + "Disco", + "Funk", + "Grunge", + "Hip-Hop", + "Jazz", + "Metal", + "New Age", + "Oldies", + "Other", + "Pop", + "R&B", + "Rap", + "Reggae", + "Rock", + "Techno", + "Industrial", + "Alternative", + "Ska", + "Death Metal", + "Pranks", + "Soundtrack", + "Euro-Techno", + "Ambient", + "Trip-Hop", + "Vocal", + "Jazz+Funk", + "Fusion", + "Trance", + "Classical", + "Instrumental", + "Acid", + "House", + "Game", + "Sound Clip", + "Gospel", + "Noise", + "AlternRock", + "Bass", + "Soul", + "Punk", + "Space", + "Meditative", + "Instrumental Pop", + "Instrumental Rock", + "Ethnic", + "Gothic", + "Darkwave", + "Techno-Industrial", + "Electronic", + "Pop-Folk", + "Eurodance", + "Dream", + "Southern Rock", + "Comedy", + "Cult", + "Gangsta", + "Top 40", + "Christian Rap", + "Pop/Funk", + "Jungle", + "Native American", + "Cabaret", + "New Wave", + "Psychadelic", + "Rave", + "Showtunes", + "Trailer", + "Lo-Fi", + "Tribal", + "Acid Punk", + "Acid Jazz", + "Polka", + "Retro", + "Musical", + "Rock & Roll", + "Hard Rock", - // Winamp extensions, more or less a de-facto standard - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", - "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", - "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", - "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", - "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", - "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", - "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", - "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "Britpop", - "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal", - "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", - "Thrash Metal", "Anime", "JPop", "Synthpop", + // Winamp extensions, more or less a de-facto standard + "Folk", + "Folk-Rock", + "National Folk", + "Swing", + "Fast Fusion", + "Bebob", + "Latin", + "Revival", + "Celtic", + "Bluegrass", + "Avantgarde", + "Gothic Rock", + "Progressive Rock", + "Psychedelic Rock", + "Symphonic Rock", + "Slow Rock", + "Big Band", + "Chorus", + "Easy Listening", + "Acoustic", + "Humour", + "Speech", + "Chanson", + "Opera", + "Chamber Music", + "Sonata", + "Symphony", + "Booty Bass", + "Primus", + "Porn Groove", + "Satire", + "Slow Jam", + "Club", + "Tango", + "Samba", + "Folklore", + "Ballad", + "Power Ballad", + "Rhythmic Soul", + "Freestyle", + "Duet", + "Punk Rock", + "Drum Solo", + "A capella", + "Euro-House", + "Dance Hall", + "Goa", + "Drum & Bass", + "Club-House", + "Hardcore", + "Terror", + "Indie", + "Britpop", + "Negerpunk", + "Polsk Punk", + "Beat", + "Christian Gangsta", + "Heavy Metal", + "Black Metal", + "Crossover", + "Contemporary Christian", + "Christian Rock", + "Merengue", + "Salsa", + "Thrash Metal", + "Anime", + "JPop", + "Synthpop", - // Winamp 5.6+ extensions, also used by EasyTAG. - // I only include this because post-rock is a based genre and deserves a slot. - "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", - "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", - "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", - "Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", - "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music", - "Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast", - "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient" - ) + // Winamp 5.6+ extensions, also used by EasyTAG. + // I only include this because post-rock is a based genre and deserves a slot. + "Abstract", + "Art Rock", + "Baroque", + "Bhangra", + "Big Beat", + "Breakbeat", + "Chillout", + "Downtempo", + "Dub", + "EBM", + "Eclectic", + "Electro", + "Electroclash", + "Emo", + "Experimental", + "Garage", + "Global", + "IDM", + "Illbient", + "Industro-Goth", + "Jam Band", + "Krautrock", + "Leftfield", + "Lounge", + "Math Rock", + "New Romantic", + "Nu-Breakz", + "Post-Punk", + "Post-Rock", + "Psytrance", + "Shoegaze", + "Space Rock", + "Trop Rock", + "World Music", + "Neoclassical", + "Audiobook", + "Audio Theatre", + "Neue Deutsche Welle", + "Podcast", + "Indie Rock", + "G-Funk", + "Dubstep", + "Garage Rock", + "Psybient") } } 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 95cf81e9a..5d927b4ce 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MusicStore.kt is part of Auxio. * * 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 @@ -15,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.music import android.Manifest @@ -25,41 +24,42 @@ import android.content.pm.PackageManager import android.net.Uri import android.provider.OpenableColumns import androidx.core.content.ContextCompat +import java.lang.Exception import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE -import java.lang.Exception /** - * The main storage for music items. - * Getting an instance of this object is more complicated as it loads asynchronously. - * See the companion object for more. - * TODO: Add automatic rescanning [major change] + * The main storage for music items. Getting an instance of this object is more complicated as it + * loads asynchronously. See the companion object for more. TODO: Add automatic rescanning [major + * change] * @author OxygenCobalt */ class MusicStore private constructor() { private var mGenres = listOf() - val genres: List get() = mGenres + val genres: List + get() = mGenres private var mArtists = listOf() - val artists: List get() = mArtists + val artists: List + get() = mArtists private var mAlbums = listOf() - val albums: List get() = mAlbums + val albums: List + get() = mAlbums private var mSongs = listOf() - val songs: List get() = mSongs + val songs: List + get() = mSongs - /** - * Load/Sort the entire music library. Should always be ran on a coroutine. - */ + /** Load/Sort the entire music library. Should always be ran on a coroutine. */ private fun load(context: Context): Response { logD("Starting initial music load") - val notGranted = ContextCompat.checkSelfPermission( - context, Manifest.permission.READ_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_DENIED + val notGranted = + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == + PackageManager.PERMISSION_DENIED if (notGranted) { return Response.Err(ErrorKind.NO_PERMS) @@ -69,8 +69,7 @@ class MusicStore private constructor() { val start = System.currentTimeMillis() val loader = MusicLoader() - val library = loader.load(context) - ?: return Response.Err(ErrorKind.NO_MUSIC) + val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC) mSongs = library.songs mAlbums = library.albums @@ -87,27 +86,22 @@ class MusicStore private constructor() { return Response.Ok(this) } - /** - * Find a song in a faster manner using an ID for its album as well. - */ + /** Find a song in a faster manner using an ID for its album as well. */ fun findSongFast(songId: Long, albumId: Long): Song? { return albums.find { it.id == albumId }?.songs?.find { it.id == songId } } /** - * Find a song for a [uri], this is similar to [findSongFast], but with some kind of content uri. + * Find a song for a [uri], this is similar to [findSongFast], but with some kind of content + * uri. * @return The corresponding [Song] for this [uri], null if there isn't one. */ fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? { - resolver.query( - uri, - arrayOf(OpenableColumns.DISPLAY_NAME), - null, null, null - )?.use { cursor -> + resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor + -> cursor.moveToFirst() - val fileName = cursor.getString( - cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) - ) + val fileName = + cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) return songs.find { it.fileName == fileName } } @@ -116,9 +110,8 @@ class MusicStore private constructor() { } /** - * A response that [MusicStore] returns when loading music. - * And before you ask, yes, I do like rust. - * TODO: Add the exception to the "FAILED" ErrorKind + * A response that [MusicStore] returns when loading music. And before you ask, yes, I do like + * rust. TODO: Add the exception to the "FAILED" ErrorKind */ sealed class Response { class Ok(val musicStore: MusicStore) : Response() @@ -126,12 +119,13 @@ class MusicStore private constructor() { } enum class ErrorKind { - NO_PERMS, NO_MUSIC, FAILED + NO_PERMS, + NO_MUSIC, + FAILED } companion object { - @Volatile - private var RESPONSE: Response? = null + @Volatile private var RESPONSE: Response? = null /** * Initialize the loading process for this instance. This must be ran on a background @@ -145,41 +139,41 @@ class MusicStore private constructor() { return currentInstance } - val response = withContext(Dispatchers.IO) { - val response = MusicStore().load(context) - synchronized(this) { - RESPONSE = response + val response = + withContext(Dispatchers.IO) { + val response = MusicStore().load(context) + synchronized(this) { RESPONSE = response } + response } - response - } return response } /** - * Await the successful creation of a [MusicStore] instance. The co-routine calling - * this will block until the successful creation of a [MusicStore], in which it will - * then be returned. + * Await the successful creation of a [MusicStore] instance. The co-routine calling this + * will block until the successful creation of a [MusicStore], in which it will then be + * returned. */ - suspend fun awaitInstance() = withContext(Dispatchers.Default) { - // We have to do a withContext call so we don't block the JVM thread - val musicStore: MusicStore + suspend fun awaitInstance() = + withContext(Dispatchers.Default) { + // We have to do a withContext call so we don't block the JVM thread + val musicStore: MusicStore - while (true) { - val response = RESPONSE + while (true) { + val response = RESPONSE - if (response is Response.Ok) { - musicStore = response.musicStore - break + if (response is Response.Ok) { + musicStore = response.musicStore + break + } } + + musicStore } - musicStore - } - /** - * Maybe get a MusicStore instance. This is useful if you are running code while the - * loading process may still be going on. + * Maybe get a MusicStore instance. This is useful if you are running code while the loading + * process may still be going on. * * @return null if the music store instance is still loading or if the loading process has * encountered an error. An instance is returned otherwise. @@ -195,9 +189,8 @@ class MusicStore private constructor() { } /** - * Require a MusicStore instance. This function is dangerous and should only be used if - * it's guaranteed that the caller's code will only be called after the initial loading - * process. + * Require a MusicStore instance. This function is dangerous and should only be used if it's + * guaranteed that the caller's code will only be called after the initial loading process. */ fun requireInstance(): MusicStore { return requireNotNull(maybeGetInstance()) { @@ -205,9 +198,7 @@ class MusicStore private constructor() { } } - /** - * Check if this instance has successfully loaded or not. - */ + /** Check if this instance has successfully loaded or not. */ fun loaded(): Boolean { return maybeGetInstance() != null } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index 5c03a5e95..ac7631505 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MusicUtils.kt is part of Auxio. * * 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 @@ -15,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.music import android.text.format.DateUtils @@ -30,8 +29,8 @@ import org.oxycblt.auxio.util.logW /** * Convert a [Long] of seconds into a string duration. - * @param isElapsed Whether this duration is represents elapsed time. If this is false, then - * --:-- will be returned if the second value is 0. + * @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:-- + * will be returned if the second value is 0. */ fun Long.toDuration(isElapsed: Boolean): String { if (!isElapsed && this == 0L) { @@ -58,11 +57,7 @@ fun TextView.bindSongInfo(song: Song?) { return } - text = context.getString( - R.string.fmt_two, - song.resolvedArtistName, - song.resolvedAlbumName - ) + text = context.getString(R.string.fmt_two, song.resolvedArtistName, song.resolvedAlbumName) } @BindingAdapter("albumInfo") @@ -72,11 +67,11 @@ fun TextView.bindAlbumInfo(album: Album?) { return } - text = context.getString( - R.string.fmt_two, - album.resolvedArtistName, - context.getPluralSafe(R.plurals.fmt_song_count, album.songs.size) - ) + text = + context.getString( + R.string.fmt_two, + album.resolvedArtistName, + context.getPluralSafe(R.plurals.fmt_song_count, album.songs.size)) } @BindingAdapter("artistInfo") @@ -86,11 +81,11 @@ fun TextView.bindArtistInfo(artist: Artist?) { return } - text = context.getString( - R.string.fmt_two, - context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size), - context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size) - ) + text = + context.getString( + R.string.fmt_two, + context.getPluralSafe(R.plurals.fmt_album_count, artist.albums.size), + context.getPluralSafe(R.plurals.fmt_song_count, artist.songs.size)) } @BindingAdapter("genreInfo") 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 b91adb12f..ae13ee2a2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * MusicViewModel.kt is part of Auxio. * * 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 @@ -15,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.music import android.content.Context @@ -33,8 +32,8 @@ class MusicViewModel : ViewModel() { private var isBusy = false /** - * Initiate the loading process. This is done here since HomeFragment will be the first - * fragment navigated to and because SnackBars will have the best UX here. + * Initiate the loading process. This is done here since HomeFragment will be the first fragment + * navigated to and because SnackBars will have the best UX here. */ fun loadMusic(context: Context) { if (mLoaderResponse.value != null || isBusy) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt index 3267b986d..875ce90d5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * BlacklistDatabase.kt is part of Auxio. * * 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 @@ -15,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.music.excluded import android.content.ContentValues @@ -28,9 +27,9 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.queryAll /** - * Database for storing excluded directories. - * Note that the paths stored here will not work with MediaStore unless you append a "%" at the end. - * Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs. + * Database for storing excluded directories. Note that the paths stored here will not work with + * MediaStore unless you append a "%" at the end. Yes. I know Room exists. But that would needlessly + * bloat my app and has crippling bugs. * @author OxygenCobalt */ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { @@ -47,9 +46,7 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu onUpgrade(db, newVersion, oldVersion) } - /** - * Write a list of [paths] to the database. - */ + /** Write a list of [paths] to the database. */ fun writePaths(paths: List) { assertBackgroundThread() @@ -58,21 +55,14 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu logD("Deleted paths db") for (path in paths) { - insert( - TABLE_NAME, null, - ContentValues(1).apply { - put(COLUMN_PATH, path) - } - ) + insert(TABLE_NAME, null, ContentValues(1).apply { put(COLUMN_PATH, path) }) } logD("Successfully wrote ${paths.size} paths to db") } } - /** - * Get the current list of paths from the database. - */ + /** Get the current list of paths from the database. */ fun readPaths(): List { assertBackgroundThread() @@ -97,12 +87,9 @@ class ExcludedDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu const val TABLE_NAME = "blacklist_dirs_table" const val COLUMN_PATH = "COLUMN_PATH" - @Volatile - private var INSTANCE: ExcludedDatabase? = null + @Volatile private var INSTANCE: ExcludedDatabase? = null - /** - * Get/Instantiate the single instance of [ExcludedDatabase]. - */ + /** Get/Instantiate the single instance of [ExcludedDatabase]. */ fun getInstance(context: Context): ExcludedDatabase { val currentInstance = INSTANCE diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt index b45aa4ed5..6296ba728 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * BlacklistDialog.kt is part of Auxio. * * 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 @@ -15,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.music.excluded import android.net.Uri @@ -57,13 +56,10 @@ class ExcludedDialog : LifecycleDialog() { ): View { val binding = DialogExcludedBinding.inflate(inflater) - val adapter = ExcludedEntryAdapter { path -> - excludedModel.removePath(path) - } + val adapter = ExcludedEntryAdapter { path -> excludedModel.removePath(path) } - val launcher = registerForActivityResult( - ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath - ) + val launcher = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath) // --- UI SETUP --- @@ -131,9 +127,9 @@ class ExcludedDialog : LifecycleDialog() { private fun parseDocTreePath(uri: Uri): String? { // Turn the raw URI into a document tree URI - val docUri = DocumentsContract.buildDocumentUriUsingTree( - uri, DocumentsContract.getTreeDocumentId(uri) - ) + val docUri = + DocumentsContract.buildDocumentUriUsingTree( + uri, DocumentsContract.getTreeDocumentId(uri)) // Turn it into a semi-usable path val typeAndPath = DocumentsContract.getTreeDocumentId(docUri).split(":") @@ -153,15 +149,11 @@ class ExcludedDialog : LifecycleDialog() { private fun saveAndRestart() { excludedModel.save { - playbackModel.savePlaybackState(requireContext()) { - requireContext().hardRestart() - } + playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() } } } - /** - * Get *just* the root path, nothing else is really needed. - */ + /** Get *just* the root path, nothing else is really needed. */ private fun getRootPath(): String { return Environment.getExternalStorageDirectory().absolutePath } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt index e5763439e..66ea7bfff 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * BlacklistEntryAdapter.kt is part of Auxio. * * 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 @@ -15,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.music.excluded import android.annotation.SuppressLint @@ -28,9 +27,8 @@ import org.oxycblt.auxio.util.inflater * Adapter that shows the excluded directories and their "Clear" button. * @author OxygenCobalt */ -class ExcludedEntryAdapter( - private val onClear: (String) -> Unit -) : RecyclerView.Adapter() { +class ExcludedEntryAdapter(private val onClear: (String) -> Unit) : + RecyclerView.Adapter() { private var paths = mutableListOf() override fun getItemCount() = paths.size @@ -49,21 +47,18 @@ class ExcludedEntryAdapter( notifyDataSetChanged() } - inner class ViewHolder( - private val binding: ItemExcludedDirBinding - ) : RecyclerView.ViewHolder(binding.root) { + inner class ViewHolder(private val binding: ItemExcludedDirBinding) : + RecyclerView.ViewHolder(binding.root) { init { - binding.root.layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT - ) + binding.root.layoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) } fun bind(path: String) { binding.excludedPath.text = path binding.excludedPath.requestLayout() - binding.excludedClear.setOnClickListener { - onClear(path) - } + binding.excludedClear.setOnClickListener { onClear(path) } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt index 41dda89d8..48104a6fd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * BlacklistViewModel.kt is part of Auxio. * * 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 @@ -15,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.music.excluded import android.content.Context @@ -30,29 +29,28 @@ import kotlinx.coroutines.withContext import org.oxycblt.auxio.util.logD /** - * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal - * of paths. Use [Factory] to instantiate this. - * TODO: Unify with MusicViewModel + * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal of + * paths. Use [Factory] to instantiate this. TODO: Unify with MusicViewModel * @author OxygenCobalt */ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() { private val mPaths = MutableLiveData(mutableListOf()) - val paths: LiveData> get() = mPaths + val paths: LiveData> + get() = mPaths private var dbPaths = listOf() - /** - * Check if changes have been made to the ViewModel's paths. - */ - val isModified: Boolean get() = dbPaths != paths.value + /** Check if changes have been made to the ViewModel's paths. */ + val isModified: Boolean + get() = dbPaths != paths.value init { loadDatabasePaths() } /** - * Add a path to this ViewModel. It will not write the path to the database unless - * [save] is called. + * Add a path to this ViewModel. It will not write the path to the database unless [save] is + * called. */ fun addPath(path: String) { if (!mPaths.value!!.contains(path)) { @@ -70,9 +68,7 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo mPaths.value = mPaths.value } - /** - * Save the pending paths to the database. [onDone] will be called on completion. - */ + /** Save the pending paths to the database. [onDone] will be called on completion. */ fun save(onDone: () -> Unit) { viewModelScope.launch(Dispatchers.IO) { val start = System.currentTimeMillis() @@ -80,24 +76,18 @@ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewMo dbPaths = mPaths.value!! onDone() this@ExcludedViewModel.logD( - "Path save completed successfully in ${System.currentTimeMillis() - start}ms" - ) + "Path save completed successfully in ${System.currentTimeMillis() - start}ms") } } - /** - * Load the paths stored in the database to this ViewModel, will erase any pending changes. - */ + /** Load the paths stored in the database to this ViewModel, will erase any pending changes. */ private fun loadDatabasePaths() { viewModelScope.launch(Dispatchers.IO) { val start = System.currentTimeMillis() dbPaths = excludedDatabase.readPaths() - withContext(Dispatchers.Main) { - mPaths.value = dbPaths.toMutableList() - } + withContext(Dispatchers.Main) { mPaths.value = dbPaths.toMutableList() } this@ExcludedViewModel.logD( - "Path load completed successfully in ${System.currentTimeMillis() - start}ms" - ) + "Path load completed successfully in ${System.currentTimeMillis() - start}ms") } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarView.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarView.kt index caf863c05..10059b68c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarView.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarView.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * CompactPlaybackView.kt is part of Auxio. * * 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 @@ -15,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.playback import android.content.Context @@ -35,14 +34,13 @@ import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * A view displaying the playback state in a compact manner. This is only meant to be used - * by [PlaybackLayout]. + * A view displaying the playback state in a compact manner. This is only meant to be used by + * [PlaybackLayout]. */ -class PlaybackBarView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : ConstraintLayout(context, attrs, defStyleAttr) { +class PlaybackBarView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + ConstraintLayout(context, attrs, defStyleAttr) { private val binding = ViewPlaybackBarBinding.inflate(context.inflater, this, true) init { @@ -52,35 +50,29 @@ class PlaybackBarView @JvmOverloads constructor( // we use colorSecondary instead of colorSurfaceVariant. This is because // colorSurfaceVariant is used with the assumption that the view that is using it is // not elevated and is therefore not colored. This view is elevated. - binding.playbackProgressBar.trackColor = MaterialColors.compositeARGBWithAlpha( - context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt() - ) + binding.playbackProgressBar.trackColor = + MaterialColors.compositeARGBWithAlpha( + context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()) } override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { // Since we swipe up this view, we need to make sure it does not collide with // any gesture events. So, apply the system gesture insets if present and then // only default to the system bar insets when there are no other options. - val gesturePadding = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - insets.getInsets(WindowInsets.Type.systemGestures()).bottom + val gesturePadding = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + insets.getInsets(WindowInsets.Type.systemGestures()).bottom + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + @Suppress("DEPRECATION") insets.systemGestureInsets.bottom + } + else -> 0 } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { - @Suppress("DEPRECATION") - insets.systemGestureInsets.bottom - } - - else -> 0 - } - updatePadding( bottom = - if (gesturePadding != 0) - gesturePadding - else - insets.systemBarInsetsCompat.bottom - ) + if (gesturePadding != 0) gesturePadding else insets.systemBarInsetsCompat.bottom) return insets } @@ -91,23 +83,15 @@ class PlaybackBarView @JvmOverloads constructor( viewLifecycleOwner: LifecycleOwner ) { setOnLongClickListener { - playbackModel.song.value?.let { song -> - detailModel.navToItem(song) - } + playbackModel.song.value?.let { song -> detailModel.navToItem(song) } true } - binding.playbackSkipPrev?.setOnClickListener { - playbackModel.skipPrev() - } + binding.playbackSkipPrev?.setOnClickListener { playbackModel.skipPrev() } - binding.playbackPlayPause.setOnClickListener { - playbackModel.invertPlayingStatus() - } + binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlayingStatus() } - binding.playbackSkipNext?.setOnClickListener { - playbackModel.skipNext() - } + binding.playbackSkipNext?.setOnClickListener { playbackModel.skipNext() } binding.playbackPlayPause.isActivated = playbackModel.isPlaying.value!! diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt index 4aea26ac2..97ce3dc20 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackButton.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.playback import android.content.Context @@ -14,20 +31,19 @@ import org.oxycblt.auxio.util.getDrawableSafe /** * An [AppCompatImageButton] designed for the buttons used in the playback display. * - * Auxio's playback buttons have never followed the typical 24dp icon size that all - * other UI elements do, mostly because those icons just look bad at that size with - * all the gobs of whitespace surrounding them. So, this view resizes the icons to a - * fixed 32dp in a way that doesn't require a whole new icon set. + * Auxio's playback buttons have never followed the typical 24dp icon size that all other UI + * elements do, mostly because those icons just look bad at that size with all the gobs of + * whitespace surrounding them. So, this view resizes the icons to a fixed 32dp in a way that + * doesn't require a whole new icon set. * - * This view also enables use of an "indicator", which is a dot that can denote when a - * button is active. This is useful for the shuffle/loop buttons, as at times highlighting - * them is not enough to differentiate them. + * This view also enables use of an "indicator", which is a dot that can denote when a button is + * active. This is useful for the shuffle/loop buttons, as at times highlighting them is not enough + * to differentiate them. */ -class PlaybackButton @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : AppCompatImageButton(context, attrs, defStyleAttr) { +class PlaybackButton +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + AppCompatImageButton(context, attrs, defStyleAttr) { private val iconSize = context.getDimenSizeSafe(R.dimen.size_playback_icon) private val centerMatrix = Matrix() private val matrixSrc = RectF() @@ -55,31 +71,35 @@ class PlaybackButton @JvmOverloads constructor( override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) - imageMatrix = centerMatrix.apply { - reset() - drawable?.let { drawable -> - // Android is too good to allow us to set a fixed image size, so we instead need - // to define a matrix to scale an image directly. + imageMatrix = + centerMatrix.apply { + reset() + drawable?.let { drawable -> + // Android is too good to allow us to set a fixed image size, so we instead need + // to define a matrix to scale an image directly. - // First scale the icon up to the desired size. - matrixSrc.set(0f, 0f, drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat()) - matrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat()) - centerMatrix.setRectToRect(matrixSrc, matrixDst, Matrix.ScaleToFit.CENTER) + // First scale the icon up to the desired size. + matrixSrc.set( + 0f, + 0f, + drawable.intrinsicWidth.toFloat(), + drawable.intrinsicHeight.toFloat()) + matrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat()) + centerMatrix.setRectToRect(matrixSrc, matrixDst, Matrix.ScaleToFit.CENTER) - // Then actually center it into the icon, which the previous call does not actually do. - centerMatrix.postTranslate( - (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f - ) + // Then actually center it into the icon, which the previous call does not + // actually do. + centerMatrix.postTranslate( + (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f) + } } - } // Put the indicator right below the icon. val x = (measuredWidth - indicatorDrawable.intrinsicWidth) / 2 val y = ((measuredHeight - iconSize) / 2) + iconSize indicatorDrawable.bounds.set( - x, y, x + indicatorDrawable.intrinsicWidth, y + indicatorDrawable.intrinsicHeight - ) + x, y, x + indicatorDrawable.intrinsicWidth, y + indicatorDrawable.intrinsicHeight) } override fun onDraw(canvas: Canvas) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 5e665e8f1..48296ac77 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackFragment.kt is part of Auxio. * * 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 @@ -15,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.playback import android.os.Bundle @@ -38,8 +37,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat /** * A [Fragment] that displays more information about the song, along with more media controls. * Instantiation is done by the navigation component, **do not instantiate this fragment manually.** - * @author OxygenCobalt - * TODO: Handle RTL correctly in the playback buttons + * @author OxygenCobalt TODO: Handle RTL correctly in the playback buttons */ class PlaybackFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() @@ -66,18 +64,13 @@ class PlaybackFragment : Fragment() { binding.root.setOnApplyWindowInsetsListener { _, insets -> val bars = insets.systemBarInsetsCompat - binding.root.updatePadding( - top = bars.top, - bottom = bars.bottom - ) + binding.root.updatePadding(top = bars.top, bottom = bars.bottom) insets } binding.playbackToolbar.apply { - setNavigationOnClickListener { - navigateUp() - } + setNavigationOnClickListener { navigateUp() } setOnMenuItemClickListener { item -> if (item.itemId == R.id.action_queue) { @@ -96,9 +89,7 @@ class PlaybackFragment : Fragment() { binding.playbackSeekBar.onConfirmListener = playbackModel::setPosition // Abuse the play/pause FAB (see style definition for more info) - binding.playbackPlayPause.post { - binding.playbackPlayPause.stateListAnimator = null - } + binding.playbackPlayPause.post { binding.playbackPlayPause.stateListAnimator = null } // --- VIEWMODEL SETUP -- @@ -114,8 +105,8 @@ class PlaybackFragment : Fragment() { } playbackModel.parent.observe(viewLifecycleOwner) { parent -> - binding.playbackToolbar.subtitle = parent?.resolvedName - ?: getString(R.string.lbl_all_songs) + binding.playbackToolbar.subtitle = + parent?.resolvedName ?: getString(R.string.lbl_all_songs) } playbackModel.isShuffling.observe(viewLifecycleOwner) { isShuffling -> @@ -123,11 +114,12 @@ class PlaybackFragment : Fragment() { } playbackModel.loopMode.observe(viewLifecycleOwner) { loopMode -> - val resId = when (loopMode) { - LoopMode.NONE, null -> R.drawable.ic_loop - LoopMode.ALL -> R.drawable.ic_loop_on - LoopMode.TRACK -> R.drawable.ic_loop_one - } + val resId = + when (loopMode) { + LoopMode.NONE, null -> R.drawable.ic_loop + LoopMode.ALL -> R.drawable.ic_loop_on + LoopMode.TRACK -> R.drawable.ic_loop_one + } binding.playbackLoop.apply { isActivated = loopMode != LoopMode.NONE diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt index b458553ac..fd4ccb88d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackLayout.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.playback import android.content.Context @@ -19,6 +36,9 @@ import androidx.core.view.isInvisible import androidx.customview.widget.ViewDragHelper import androidx.lifecycle.LifecycleOwner import com.google.android.material.shape.MaterialShapeDrawable +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.detail.DetailViewModel @@ -32,31 +52,30 @@ import org.oxycblt.auxio.util.pxOfDp import org.oxycblt.auxio.util.replaceSystemBarInsetsCompat import org.oxycblt.auxio.util.stateList import org.oxycblt.auxio.util.systemBarInsetsCompat -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min /** - * This layout handles pretty much every aspect of the playback UI flow, notably the playback - * bar and it's ability to slide up into the playback view. It's a blend of Hai Zhang's + * This layout handles pretty much every aspect of the playback UI flow, notably the playback bar + * and it's ability to slide up into the playback view. It's a blend of Hai Zhang's * PersistentBarLayout and Umano's SlidingUpPanelLayout, albeit heavily minified to remove * extraneous use cases and updated to support the latest SDK level and androidx tools. * * **Note:** If you want to adapt this layout into your own app. Good luck. This layout has been - * reduced to Auxio's use case in particular and is really hard to understand since it has a ton - * of state and view magic. I tried my best to document it, but it's probably not the most friendly - * or extendable. You have been warned. + * reduced to Auxio's use case in particular and is really hard to understand since it has a ton of + * state and view magic. I tried my best to document it, but it's probably not the most friendly or + * extendable. You have been warned. * - * @author OxygenCobalt (With help from Umano and Hai Zhang) - * TODO: Find a better way to handle PlaybackFragment in general (navigation, creation) + * @author OxygenCobalt (With help from Umano and Hai Zhang) TODO: Find a better way to handle + * PlaybackFragment in general (navigation, creation) */ -class PlaybackLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : ViewGroup(context, attrs, defStyle) { +class PlaybackLayout +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : + ViewGroup(context, attrs, defStyle) { private enum class PanelState { - EXPANDED, COLLAPSED, HIDDEN, DRAGGING + EXPANDED, + COLLAPSED, + HIDDEN, + DRAGGING } private lateinit var contentView: View @@ -67,20 +86,19 @@ class PlaybackLayout @JvmOverloads constructor( private val playbackContainerBg: MaterialShapeDrawable private val playbackFragment = PlaybackFragment() - /** - * The drag helper that animates and dispatches drag events to the panels. - */ - private val dragHelper = ViewDragHelper.create(this, DragHelperCallback()).apply { - minVelocity = MIN_FLING_VEL * resources.displayMetrics.density - } + /** The drag helper that animates and dispatches drag events to the panels. */ + private val dragHelper = + ViewDragHelper.create(this, DragHelperCallback()).apply { + minVelocity = MIN_FLING_VEL * resources.displayMetrics.density + } /** - * The current window insets. - * Important since this layout must play a long with Auxio's edge-to-edge functionality. + * The current window insets. Important since this layout must play a long with Auxio's + * edge-to-edge functionality. */ private var lastInsets: WindowInsets? = null - /** The current panel state. Can be [PanelState.DRAGGING]*/ + /** The current panel state. Can be [PanelState.DRAGGING] */ private var panelState = INIT_PANEL_STATE /** The last panel state before a drag event began. */ @@ -90,10 +108,8 @@ class PlaybackLayout @JvmOverloads constructor( private var panelRange = 0 /** - * The relative offset of this panel as a percentage of [panelRange]. - * A value of 1 means a fully expanded panel. - * A value of 0 means a collapsed panel. - * A value below 0 means a hidden panel. + * The relative offset of this panel as a percentage of [panelRange]. A value of 1 means a fully + * expanded panel. A value of 0 means a collapsed panel. A value below 0 means a hidden panel. */ private var panelOffset = 0f @@ -105,88 +121,96 @@ class PlaybackLayout @JvmOverloads constructor( private val elevationNormal = context.getDimenSafe(R.dimen.elevation_normal) /** See [isDragging] */ - private val dragStateField = ViewDragHelper::class.java.getDeclaredField("mDragState").apply { - isAccessible = true - } + private val dragStateField = + ViewDragHelper::class.java.getDeclaredField("mDragState").apply { isAccessible = true } init { setWillNotDraw(false) // Set up our playback views. Doing this allows us to abstract away the implementation // of these views from the user of this layout [MainFragment]. - playbackContainerView = FrameLayout(context).apply { - id = R.id.playback_container + playbackContainerView = + FrameLayout(context).apply { + id = R.id.playback_container - isClickable = true - isFocusable = false - isFocusableInTouchMode = false + isClickable = true + isFocusable = false + isFocusableInTouchMode = false - playbackContainerBg = MaterialShapeDrawable.createWithElevationOverlay(context).apply { - fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList - elevation = context.pxOfDp(elevationNormal).toFloat() + playbackContainerBg = + MaterialShapeDrawable.createWithElevationOverlay(context).apply { + fillColor = context.getAttrColorSafe(R.attr.colorSurface).stateList + elevation = context.pxOfDp(elevationNormal).toFloat() + } + + // The way we fade out the elevation overlay is not by actually reducing the + // elevation + // but by fading out the background drawable itself. To be safe, we apply this + // background drawable to a layer list with another colorSurface shape drawable, + // just + // in case weird things happen if background drawable is completely transparent. + background = + (context.getDrawableSafe(R.drawable.ui_panel_bg) as LayerDrawable).apply { + setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg) + } + + disableDropShadowCompat() } - // The way we fade out the elevation overlay is not by actually reducing the elevation - // but by fading out the background drawable itself. To be safe, we apply this - // background drawable to a layer list with another colorSurface shape drawable, just - // in case weird things happen if background drawable is completely transparent. - background = (context.getDrawableSafe(R.drawable.ui_panel_bg) as LayerDrawable).apply { - setDrawableByLayerId(R.id.panel_overlay, playbackContainerBg) - } + playbackBarView = + PlaybackBarView(context).apply { + id = R.id.playback_bar - disableDropShadowCompat() - } + playbackContainerView.addView(this) - playbackBarView = PlaybackBarView(context).apply { - id = R.id.playback_bar + (layoutParams as FrameLayout.LayoutParams).apply { + width = LayoutParams.MATCH_PARENT + height = LayoutParams.WRAP_CONTENT + gravity = Gravity.TOP + } - playbackContainerView.addView(this) - - (layoutParams as FrameLayout.LayoutParams).apply { - width = LayoutParams.MATCH_PARENT - height = LayoutParams.WRAP_CONTENT - gravity = Gravity.TOP - } - - // The bar view if clicked will expand into the full panel - setOnClickListener { - if (canSlide && panelState != PanelState.EXPANDED) { - applyState(PanelState.EXPANDED) + // The bar view if clicked will expand into the full panel + setOnClickListener { + if (canSlide && panelState != PanelState.EXPANDED) { + applyState(PanelState.EXPANDED) + } } } - } - playbackPanelView = FrameLayout(context).apply { - playbackContainerView.addView(this) + playbackPanelView = + FrameLayout(context).apply { + playbackContainerView.addView(this) - (layoutParams as FrameLayout.LayoutParams).apply { - width = LayoutParams.MATCH_PARENT - height = LayoutParams.MATCH_PARENT - gravity = Gravity.CENTER + (layoutParams as FrameLayout.LayoutParams).apply { + width = LayoutParams.MATCH_PARENT + height = LayoutParams.MATCH_PARENT + gravity = Gravity.CENTER + } + + id = R.id.playback_panel + + // Make sure we add our fragment to this view. This is actually a replace operation + // since we don't want to stack fragments but we can't ensure that this view doesn't + // already have a fragment attached. + try { + (context as AppCompatActivity) + .supportFragmentManager + .beginTransaction() + .replace(R.id.playback_panel, playbackFragment) + .commit() + } catch (e: Exception) { + // Band-aid to stop the app crashing if we have to swap out the content view + // without warning (which we have to do sometimes because android is the worst + // thing ever) + } } - - id = R.id.playback_panel - - // Make sure we add our fragment to this view. This is actually a replace operation - // since we don't want to stack fragments but we can't ensure that this view doesn't - // already have a fragment attached. - try { - (context as AppCompatActivity).supportFragmentManager.beginTransaction() - .replace(R.id.playback_panel, playbackFragment) - .commit() - } catch (e: Exception) { - // Band-aid to stop the app crashing if we have to swap out the content view - // without warning (which we have to do sometimes because android is the worst - // thing ever) - } - } } // / --- CONTROL METHODS --- /** - * Update the song that this layout is showing. This will be reflected in the compact view - * at the bottom of the screen. + * Update the song that this layout is showing. This will be reflected in the compact view at + * the bottom of the screen. */ fun setup( playbackModel: PlaybackViewModel, @@ -195,9 +219,7 @@ class PlaybackLayout @JvmOverloads constructor( ) { setSong(playbackModel.song.value) - playbackModel.song.observe(viewLifecycleOwner) { song -> - setSong(song) - } + playbackModel.song.observe(viewLifecycleOwner) { song -> setSong(song) } playbackBarView.setup(playbackModel, detailModel, viewLifecycleOwner) } @@ -243,7 +265,8 @@ class PlaybackLayout @JvmOverloads constructor( } if (!isLaidOut) { - // Not laid out, just apply the state and let the measure + layout steps apply it for us. + // Not laid out, just apply the state and let the measure + layout steps apply it for + // us. setPanelStateInternal(state) } else { // We are laid out. In this case we actually animate to our desired target. @@ -293,11 +316,12 @@ class PlaybackLayout @JvmOverloads constructor( if (!isLaidOut) { // This is our first layout, so make sure we know what offset we should work with // before we measure our content - panelOffset = when (panelState) { - PanelState.EXPANDED -> 1.0f - PanelState.HIDDEN -> computePanelOffset(measuredHeight) - else -> 0f - } + panelOffset = + when (panelState) { + PanelState.EXPANDED -> 1.0f + PanelState.HIDDEN -> computePanelOffset(measuredHeight) + else -> 0f + } updatePanelTransition() } @@ -315,9 +339,8 @@ class PlaybackLayout @JvmOverloads constructor( // Note that these views will always be a fixed MATCH_PARENT. This is intentional, // as it reduces the logic we have to deal with regarding WRAP_CONTENT views. val contentWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY) - val contentHeightSpec = MeasureSpec.makeMeasureSpec( - measuredHeight - barHeightAdjusted, MeasureSpec.EXACTLY - ) + val contentHeightSpec = + MeasureSpec.makeMeasureSpec(measuredHeight - barHeightAdjusted, MeasureSpec.EXACTLY) contentView.measure(contentWidthSpec, contentHeightSpec) } @@ -330,8 +353,7 @@ class PlaybackLayout @JvmOverloads constructor( 0, panelTop, playbackContainerView.measuredWidth, - playbackContainerView.measuredHeight + panelTop - ) + playbackContainerView.measuredHeight + panelTop) layoutContent() } @@ -352,9 +374,7 @@ class PlaybackLayout @JvmOverloads constructor( canvas.clipRect(tRect) } - return super.drawChild(canvas, child, drawingTime).also { - canvas.restoreToCount(save) - } + return super.drawChild(canvas, child, drawingTime).also { canvas.restoreToCount(save) } } override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { @@ -369,8 +389,8 @@ class PlaybackLayout @JvmOverloads constructor( } /** - * Apply window insets to the content views in this layouts. This is done separately as at - * times we want to re-inset the content views but not re-inset the bar view. + * Apply window insets to the content views in this layouts. This is done separately as at times + * we want to re-inset the content views but not re-inset the bar view. */ private fun applyContentWindowInsets() { val insets = lastInsets @@ -379,9 +399,7 @@ class PlaybackLayout @JvmOverloads constructor( } } - /** - * Adjust window insets to line up with the panel - */ + /** Adjust window insets to line up with the panel */ private fun adjustInsets(insets: WindowInsets): WindowInsets { // We kind to do a reverse-measure to figure out how we should inset this view. // Find how much space is lost by the panel and then combine that with the @@ -390,21 +408,20 @@ class PlaybackLayout @JvmOverloads constructor( val consumedByPanel = computePanelTopPosition(panelOffset) - measuredHeight val adjustedBottomInset = (consumedByPanel + bars.bottom).coerceAtLeast(0) return insets.replaceSystemBarInsetsCompat( - bars.left, bars.top, bars.right, adjustedBottomInset - ) + bars.left, bars.top, bars.right, adjustedBottomInset) } - override fun onSaveInstanceState(): Parcelable = Bundle().apply { - putParcelable("superState", super.onSaveInstanceState()) - putSerializable( - KEY_PANEL_STATE, - if (panelState != PanelState.DRAGGING) { - panelState - } else { - lastIdlePanelState - } - ) - } + override fun onSaveInstanceState(): Parcelable = + Bundle().apply { + putParcelable("superState", super.onSaveInstanceState()) + putSerializable( + KEY_PANEL_STATE, + if (panelState != PanelState.DRAGGING) { + panelState + } else { + lastIdlePanelState + }) + } override fun onRestoreInstanceState(state: Parcelable) { if (state is Bundle) { @@ -425,13 +442,14 @@ class PlaybackLayout @JvmOverloads constructor( return if (!canSlide) { super.onTouchEvent(ev) - } else try { - dragHelper.processTouchEvent(ev) - true - } catch (ex: Exception) { - // Ignore the pointer out of range exception - false - } + } else + try { + dragHelper.processTouchEvent(ev) + true + } catch (ex: Exception) { + // Ignore the pointer out of range exception + false + } } override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { @@ -454,10 +472,10 @@ class PlaybackLayout @JvmOverloads constructor( return false } } - MotionEvent.ACTION_MOVE -> { val pointerUnder = playbackContainerView.isUnder(ev.x.toInt(), ev.y.toInt()) - val motionUnder = playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt()) + val motionUnder = + playbackContainerView.isUnder(initMotionX.toInt(), initMotionY.toInt()) if (!(pointerUnder || motionUnder) || ady > dragSlop && adx > ady) { // Pointer has moved beyond our control, do not intercept this event @@ -465,7 +483,6 @@ class PlaybackLayout @JvmOverloads constructor( return false } } - MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> if (dragHelper.isDragging) { // Stopped pressing while we were dragging, let the drag helper handle it @@ -504,11 +521,12 @@ class PlaybackLayout @JvmOverloads constructor( get() { // We can't grab the drag state outside of a callback, but that's stupid and I don't // want to vendor ViewDragHelper so I just do reflection instead. - val state = try { - dragStateField.get(this) - } catch (e: Exception) { - ViewDragHelper.STATE_IDLE - } + val state = + try { + dragStateField.get(this) + } catch (e: Exception) { + ViewDragHelper.STATE_IDLE + } return state == ViewDragHelper.STATE_DRAGGING } @@ -524,9 +542,9 @@ class PlaybackLayout @JvmOverloads constructor( } /** - * Do the nice view animations that occur whenever we slide up the playback panel. - * The way I transition is largely inspired by Android 12's notification panel, with the - * compact view fading out completely before the panel view fades in. + * Do the nice view animations that occur whenever we slide up the playback panel. The way I + * transition is largely inspired by Android 12's notification panel, with the compact view + * fading out completely before the panel view fades in. */ private fun updatePanelTransition() { val ratio = max(panelOffset, 0f) @@ -566,8 +584,7 @@ class PlaybackLayout @JvmOverloads constructor( params.leftMargin, (bars.top * halfOutRatio).toInt(), params.rightMargin, - params.bottomMargin - ) + params.bottomMargin) // Poke the layout only when we changed something if (params.topMargin != oldTopMargin) { @@ -592,9 +609,9 @@ class PlaybackLayout @JvmOverloads constructor( private fun smoothSlideTo(offset: Float) { logD("Smooth sliding to $offset") - val okay = dragHelper.smoothSlideViewTo( - playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset) - ) + val okay = + dragHelper.smoothSlideViewTo( + playbackContainerView, playbackContainerView.left, computePanelTopPosition(offset)) if (okay) { postInvalidateOnAnimation() @@ -621,7 +638,6 @@ class PlaybackLayout @JvmOverloads constructor( setPanelStateInternal(PanelState.HIDDEN) playbackContainerView.visibility = INVISIBLE } - else -> setPanelStateInternal(PanelState.EXPANDED) } } @@ -658,16 +674,17 @@ class PlaybackLayout @JvmOverloads constructor( } override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) { - val newOffset = when { - // Swipe Up -> Expand to top - yvel < 0 -> 1f - // Swipe down -> Collapse to bottom - yvel > 0 -> 0f - // No velocity, far enough from middle to expand to top - panelOffset >= 0.5f -> 1f - // Collapse to bottom - else -> 0f - } + val newOffset = + when { + // Swipe Up -> Expand to top + yvel < 0 -> 1f + // Swipe down -> Collapse to bottom + yvel > 0 -> 0f + // No velocity, far enough from middle to expand to top + panelOffset >= 0.5f -> 1f + // Collapse to bottom + else -> 0f + } dragHelper.settleCapturedViewAt(releasedChild.left, computePanelTopPosition(newOffset)) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt index 6b7002141..cca6bff89 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSeekBar.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackSeeker.kt is part of Auxio. * * 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 @@ -15,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.playback import android.annotation.SuppressLint @@ -33,20 +32,22 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.stateList /** - * A custom view that bundles together a seekbar with a current duration and a total duration. - * The sub-views are specifically laid out so that the seekbar has an adequate touch height while - * still not having gobs of whitespace everywhere. - * TODO: Add smooth seeking [i.e seeking in sub-second values] + * A custom view that bundles together a seekbar with a current duration and a total duration. The + * sub-views are specifically laid out so that the seekbar has an adequate touch height while still + * not having gobs of whitespace everywhere. TODO: Add smooth seeking [i.e seeking in sub-second + * values] * @author OxygenCobalt */ @SuppressLint("RestrictedApi") -class PlaybackSeekBar @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleRes: Int = 0 -) : ConstraintLayout(context, attrs, defStyleRes), Slider.OnChangeListener, Slider.OnSliderTouchListener { +class PlaybackSeekBar +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) : + ConstraintLayout(context, attrs, defStyleRes), + Slider.OnChangeListener, + Slider.OnSliderTouchListener { private val binding = ViewSeekBarBinding.inflate(context.inflater, this, true) - private val isSeeking: Boolean get() = binding.playbackDurationCurrent.isActivated + private val isSeeking: Boolean + get() = binding.playbackDurationCurrent.isActivated var onConfirmListener: ((Long) -> Unit)? = null @@ -55,9 +56,10 @@ class PlaybackSeekBar @JvmOverloads constructor( binding.seekBar.addOnSliderTouchListener(this) // Override the inactive color so that it lines up with the playback progress bar. - binding.seekBar.trackInactiveTintList = MaterialColors.compositeARGBWithAlpha( - context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt() - ).stateList + binding.seekBar.trackInactiveTintList = + MaterialColors.compositeARGBWithAlpha( + context.getAttrColorSafe(R.attr.colorSecondary), (255 * 0.2).toInt()) + .stateList } fun setProgress(seconds: Long) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 93fd20cac..14f30667e 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackViewModel.kt is part of Auxio. * * 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 @@ -15,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.playback import android.content.Context @@ -41,13 +40,14 @@ import org.oxycblt.auxio.util.logE /** * The ViewModel that provides a UI frontend for [PlaybackStateManager]. * - * **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this provides - * an interface that properly sanitizes input and abstracts functions unlike the master class.** + * **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this + * provides an interface that properly sanitizes input and abstracts functions unlike the master + * class.** * @author OxygenCobalt * - * TODO: Completely rework this module to support the new music rescan system, - * proper android auto and external exposing, and so on. - * - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS. + * TODO: Completely rework this module to support the new music rescan system, proper android auto + * and external exposing, and so on. + * - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS. */ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // Playback @@ -68,21 +68,29 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { private var mIntentUri: Uri? = null /** The current song. */ - val song: LiveData get() = mSong + val song: LiveData + get() = mSong /** The current model that is being played from, such as an [Album] or [Artist] */ - val parent: LiveData get() = mParent + val parent: LiveData + get() = mParent - val isPlaying: LiveData get() = mIsPlaying - val isShuffling: LiveData get() = mIsShuffling + val isPlaying: LiveData + get() = mIsPlaying + val isShuffling: LiveData + get() = mIsShuffling /** The current repeat mode, see [LoopMode] for more information */ - val loopMode: LiveData get() = mLoopMode + val loopMode: LiveData + get() = mLoopMode /** The current playback position, in seconds */ - val position: LiveData get() = mPosition + val position: LiveData + get() = mPosition /** The queue, without the previous items. */ - val nextUp: LiveData> get() = mNextUp + val nextUp: LiveData> + get() = mNextUp /** The current [PlaybackMode] that also determines the queue */ - val playbackMode: LiveData get() = mMode + val playbackMode: LiveData + get() = mMode private val playbackManager = PlaybackStateManager.getInstance() private val settingsManager = SettingsManager.getInstance() @@ -102,8 +110,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- PLAYING FUNCTIONS --- /** - * Play a [song] with the [mode] specified. [mode] will default to the preferred song - * playback mode of the user if not specified. + * Play a [song] with the [mode] specified. [mode] will default to the preferred song playback + * mode of the user if not specified. */ fun playSong(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) { playbackManager.playSong(song, mode) @@ -152,8 +160,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } /** - * Play using a file [uri]. - * This will not play instantly during the initial startup sequence. + * Play using a file [uri]. This will not play instantly during the initial startup sequence. */ fun playWithUri(uri: Uri, context: Context) { // Check if everything is already running to run the URI play @@ -166,54 +173,41 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } } - /** - * Play with a file URI. - * This is called after [playWithUri] once its deemed safe to do so. - */ + /** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */ private fun playWithUriInternal(uri: Uri, context: Context) { logD("Playing with uri $uri") val musicStore = MusicStore.maybeGetInstance() ?: return - musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> - playSong(song) - } + musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) } } - /** - * Shuffle all songs - */ + /** Shuffle all songs */ fun shuffleAll() { playbackManager.shuffleAll() } // --- POSITION FUNCTIONS --- - /** - * Update the position and push it to [PlaybackStateManager] - */ + /** Update the position and push it to [PlaybackStateManager] */ fun setPosition(progress: Long) { playbackManager.seekTo((progress * 1000)) } // --- QUEUE FUNCTIONS --- - /** - * Skip to the next song. - */ + /** Skip to the next song. */ fun skipNext() { playbackManager.next() } - /** - * Skip to the previous song. - */ + /** Skip to the previous song. */ fun skipPrev() { playbackManager.prev() } /** - * Remove a queue item using it's recyclerview adapter index. If the indices are valid, - * [apply] is called just before the change is committed so that the adapter can be updated. + * Remove a queue item using it's recyclerview adapter index. If the indices are valid, [apply] + * is called just before the change is committed so that the adapter can be updated. */ fun removeQueueDataItem(adapterIndex: Int, apply: () -> Unit) { val index = adapterIndex + (playbackManager.queue.size - mNextUp.value!!.size) @@ -223,8 +217,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { } } /** - * Move queue items using their recyclerview adapter indices. If the indices are valid, - * [apply] is called just before the change is committed so that the adapter can be updated. + * Move queue items using their recyclerview adapter indices. If the indices are valid, [apply] + * is called just before the change is committed so that the adapter can be updated. */ fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int, apply: () -> Unit): Boolean { val delta = (playbackManager.queue.size - mNextUp.value!!.size) @@ -239,53 +233,39 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { return false } - /** - * Add a [Song] to the top of the queue. - */ + /** Add a [Song] to the top of the queue. */ fun playNext(song: Song) { playbackManager.playNext(song) } - /** - * Add an [Album] to the top of the queue. - */ + /** Add an [Album] to the top of the queue. */ fun playNext(album: Album) { playbackManager.playNext(settingsManager.detailAlbumSort.sortAlbum(album)) } -/** - * Add a [Song] to the end of the queue. - */ + /** Add a [Song] to the end of the queue. */ fun addToQueue(song: Song) { playbackManager.addToQueue(song) } - /** - * Add an [Album] to the end of the queue. - */ + /** Add an [Album] to the end of the queue. */ fun addToQueue(album: Album) { playbackManager.addToQueue(settingsManager.detailAlbumSort.sortAlbum(album)) } -// --- STATUS FUNCTIONS --- + // --- STATUS FUNCTIONS --- - /** - * Flip the playing status, e.g from playing to paused - */ + /** Flip the playing status, e.g from playing to paused */ fun invertPlayingStatus() { playbackManager.setPlaying(!playbackManager.isPlaying) } - /** - * Flip the shuffle status, e.g from on to off. Will keep song by default. - */ + /** Flip the shuffle status, e.g from on to off. Will keep song by default. */ fun invertShuffleStatus() { playbackManager.setShuffling(!playbackManager.isShuffling, true) } - /** - * Increment the loop status, e.g from off to loop once - */ + /** Increment the loop status, e.g from off to loop once */ fun incrementLoopStatus() { playbackManager.setLoopMode(playbackManager.loopMode.increment()) } @@ -293,8 +273,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // --- SAVE/RESTORE FUNCTIONS --- /** - * Force save the current [PlaybackStateManager] state to the database. - * Called by SettingsListFragment. + * Force save the current [PlaybackStateManager] state to the database. Called by + * SettingsListFragment. */ fun savePlaybackState(context: Context, onDone: () -> Unit) { viewModelScope.launch { @@ -320,15 +300,13 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { playbackManager.markRestored() } else if (!playbackManager.isRestored) { // Otherwise just restore - viewModelScope.launch { - playbackManager.restoreFromDatabase(context) - } + viewModelScope.launch { playbackManager.restoreFromDatabase(context) } } } /** - * Attempt to restore the current playback state from an existing - * [PlaybackStateManager] instance. + * Attempt to restore the current playback state from an existing [PlaybackStateManager] + * instance. */ private fun restorePlaybackState() { logD("Attempting to restore playback state") 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 7c362c5fb..6e9e5a8fd 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * QueueAdapter.kt is part of Auxio. * * 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 @@ -15,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.playback.queue import android.annotation.SuppressLint @@ -47,9 +46,8 @@ import org.oxycblt.auxio.util.stateList * @param touchHelper The [ItemTouchHelper] ***containing*** [QueueDragCallback] to be used * @author OxygenCobalt */ -class QueueAdapter( - private val touchHelper: ItemTouchHelper -) : RecyclerView.Adapter() { +class QueueAdapter(private val touchHelper: ItemTouchHelper) : + RecyclerView.Adapter() { private var data = mutableListOf() private var listDiffer = AsyncListDiffer(this, DiffCallback()) @@ -60,16 +58,14 @@ class QueueAdapter( is Song -> QUEUE_SONG_ITEM_TYPE is Header -> HeaderViewHolder.ITEM_TYPE is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE - else -> -1 } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder( - ItemQueueSongBinding.inflate(parent.context.inflater) - ) + QUEUE_SONG_ITEM_TYPE -> + QueueSongViewHolder(ItemQueueSongBinding.inflate(parent.context.inflater)) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) else -> error("Invalid ViewHolder item type $viewType") @@ -86,8 +82,8 @@ class QueueAdapter( } /** - * Submit data using [AsyncListDiffer]. - * **Only use this if you have no idea what changes occurred to the data** + * Submit data using [AsyncListDiffer]. **Only use this if you have no idea what changes + * occurred to the data** */ fun submitList(newData: MutableList) { if (data != newData) { @@ -96,39 +92,32 @@ class QueueAdapter( } } - /** - * Move Items. - * Used since [submitList] will cause QueueAdapter to freak out. - */ + /** Move Items. Used since [submitList] will cause QueueAdapter to freak out. */ fun moveItems(adapterFrom: Int, adapterTo: Int) { data.add(adapterTo, data.removeAt(adapterFrom)) notifyItemMoved(adapterFrom, adapterTo) } - /** - * Remove an item. - * Used since [submitList] will cause QueueAdapter to freak out. - */ + /** Remove an item. Used since [submitList] will cause QueueAdapter to freak out. */ fun removeItem(adapterIndex: Int) { data.removeAt(adapterIndex) notifyItemRemoved(adapterIndex) } - /** - * Generic ViewHolder for a queue song - */ + /** Generic ViewHolder for a queue song */ inner class QueueSongViewHolder( private val binding: ItemQueueSongBinding, ) : BaseViewHolder(binding) { - val bodyView: View get() = binding.body - val backgroundView: View get() = binding.background + val bodyView: View + get() = binding.body + val backgroundView: View + get() = binding.background init { - binding.body.background = MaterialShapeDrawable.createWithElevationOverlay( - binding.root.context - ).apply { - fillColor = (binding.body.background as ColorDrawable).color.stateList - } + binding.body.background = + MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { + fillColor = (binding.body.background as ColorDrawable).color.stateList + } binding.root.disableDropShadowCompat() } 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 a010fadf8..3096b2ab8 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * QueueDragCallback.kt is part of Auxio. * * 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 @@ -15,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.playback.queue import android.graphics.Canvas @@ -24,19 +23,19 @@ import androidx.core.view.isInvisible import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable -import org.oxycblt.auxio.R -import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.util.getDimenSafe -import org.oxycblt.auxio.util.logD import kotlin.math.abs import kotlin.math.max import kotlin.math.min import kotlin.math.sign +import org.oxycblt.auxio.R +import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.util.getDimenSafe +import org.oxycblt.auxio.util.logD /** * A highly customized [ItemTouchHelper.Callback] that handles the queue system while basically - * rebuilding most the "Material-y" aspects of an editable list because Google's implementations - * are hot garbage. This shouldn't have *too many* UI bugs. I hope. + * rebuilding most the "Material-y" aspects of an editable list because Google's implementations are + * hot garbage. This shouldn't have *too many* UI bugs. I hope. * @author OxygenCobalt */ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouchHelper.Callback() { @@ -59,17 +58,14 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc ): Int { // Fix to make QueueFragment scroll slower when an item is scrolled out of bounds. // Adapted from NewPipe: https://github.com/TeamNewPipe/NewPipe - val standardSpeed = super.interpolateOutOfBoundsScroll( - recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll - ) + val standardSpeed = + super.interpolateOutOfBoundsScroll( + recyclerView, viewSize, viewSizeOutOfBounds, totalSize, msSinceStartScroll) - val clampedAbsVelocity = max( - MINIMUM_INITIAL_DRAG_VELOCITY, - min( - abs(standardSpeed), - MAXIMUM_INITIAL_DRAG_VELOCITY - ) - ) + val clampedAbsVelocity = + max( + MINIMUM_INITIAL_DRAG_VELOCITY, + min(abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY)) return clampedAbsVelocity * sign(viewSizeOutOfBounds.toDouble()).toInt() } @@ -94,12 +90,12 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc val bg = holder.bodyView.background as MaterialShapeDrawable val elevation = recyclerView.context.getDimenSafe(R.dimen.elevation_small) - holder.itemView.animate() + holder + .itemView + .animate() .translationZ(elevation) .setDuration(100) - .setUpdateListener { - bg.elevation = holder.itemView.translationZ - } + .setUpdateListener { bg.elevation = holder.itemView.translationZ } .setInterpolator(AccelerateDecelerateInterpolator()) .start() @@ -132,7 +128,9 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc logD("Dropping queue item") val bg = holder.bodyView.background as MaterialShapeDrawable - holder.itemView.animate() + holder + .itemView + .animate() .translationZ(0.0f) .setDuration(100) .setUpdateListener { bg.elevation = holder.itemView.translationZ } @@ -154,9 +152,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc val from = viewHolder.bindingAdapterPosition val to = target.bindingAdapterPosition - return playbackModel.moveQueueDataItems(from, to) { - queueAdapter.moveItems(from, to) - } + return playbackModel.moveQueueDataItems(from, to) { queueAdapter.moveItems(from, to) } } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { @@ -168,8 +164,8 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc override fun isLongPressDragEnabled(): Boolean = false /** - * Add the queue adapter to this callback. - * Done because there's a circular dependency between the two objects + * Add the queue adapter to this callback. Done because there's a circular dependency between + * the two objects */ fun addQueueAdapter(adapter: QueueAdapter) { queueAdapter = adapter 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 0337c54c7..bf29fcf7e 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * QueueFragment.kt is part of Auxio. * * 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 @@ -15,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.playback.queue import android.os.Bundle @@ -54,9 +53,7 @@ class QueueFragment : Fragment() { binding.lifecycleOwner = viewLifecycleOwner - binding.queueToolbar.setNavigationOnClickListener { - findNavController().navigateUp() - } + binding.queueToolbar.setNavigationOnClickListener { findNavController().navigateUp() } binding.queueRecycler.apply { setHasFixedSize(true) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt index b9e336d75..a7ca40828 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/LoopMode.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * LoopMode.kt is part of Auxio. * * 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 @@ -15,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.playback.state /** @@ -23,11 +22,11 @@ package org.oxycblt.auxio.playback.state * @author OxygenCobalt */ enum class LoopMode { - NONE, ALL, TRACK; + NONE, + ALL, + TRACK; - /** - * Increment the LoopMode, e.g from [NONE] to [ALL] - */ + /** Increment the LoopMode, e.g from [NONE] to [ALL] */ fun increment(): LoopMode { return when (this) { NONE -> ALL @@ -53,15 +52,12 @@ enum class LoopMode { private const val INT_ALL = 0xA101 private const val INT_TRACK = 0xA102 - /** - * Convert an int [constant] into a LoopMode, or null if it isn't valid. - */ + /** Convert an int [constant] into a LoopMode, or null if it isn't valid. */ fun fromInt(constant: Int): LoopMode? { return when (constant) { INT_NONE -> NONE INT_ALL -> ALL INT_TRACK -> TRACK - else -> null } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt index 48924d241..2c49d25de 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackMode.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackMode.kt is part of Auxio. * * 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 @@ -15,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.playback.state /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index 3450096f3..c899c8be3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackStateDatabase.kt is part of Auxio. * * 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 @@ -15,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.playback.state import android.content.ContentValues @@ -32,8 +31,8 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.queryAll /** - * A SQLite database for managing the persistent playback state and queue. - * Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs. + * A SQLite database for managing the persistent playback state and queue. Yes. I know Room exists. + * But that would needlessly bloat my app and has crippling bugs. * @author OxygenCobalt */ class PlaybackStateDatabase(context: Context) : @@ -58,9 +57,7 @@ class PlaybackStateDatabase(context: Context) : // --- DATABASE CONSTRUCTION FUNCTIONS --- - /** - * Create a table for this database. - */ + /** Create a table for this database. */ private fun createTable(database: SQLiteDatabase, tableName: String) { val command = StringBuilder() command.append("CREATE TABLE IF NOT EXISTS $tableName(") @@ -74,11 +71,10 @@ class PlaybackStateDatabase(context: Context) : database.execSQL(command.toString()) } - /** - * Construct a [StateColumns] table - */ + /** Construct a [StateColumns] table */ private fun constructStateTable(command: StringBuilder): StringBuilder { - command.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,") + command + .append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,") .append("${StateColumns.COLUMN_SONG_HASH} LONG,") .append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,") .append("${StateColumns.COLUMN_PARENT_HASH} LONG,") @@ -90,11 +86,10 @@ class PlaybackStateDatabase(context: Context) : return command } - /** - * Construct a [QueueColumns] table - */ + /** Construct a [QueueColumns] table */ private fun constructQueueTable(command: StringBuilder): StringBuilder { - command.append("${QueueColumns.ID} LONG PRIMARY KEY,") + command + .append("${QueueColumns.ID} LONG PRIMARY KEY,") .append("${QueueColumns.SONG_HASH} INTEGER NOT NULL,") .append("${QueueColumns.ALBUM_HASH} INTEGER NOT NULL)") @@ -126,30 +121,31 @@ class PlaybackStateDatabase(context: Context) : cursor.moveToFirst() - val song = cursor.getLongOrNull(songIndex)?.let { id -> - musicStore.songs.find { it.id == id } - } + val song = + cursor.getLongOrNull(songIndex)?.let { id -> musicStore.songs.find { it.id == id } } val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS - val parent = cursor.getLongOrNull(parentIndex)?.let { id -> - when (mode) { - PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id } - PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id } - PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id } - PlaybackMode.ALL_SONGS -> null + val parent = + cursor.getLongOrNull(parentIndex)?.let { id -> + when (mode) { + PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id } + PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id } + PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id } + PlaybackMode.ALL_SONGS -> null + } } - } - state = SavedState( - song = song, - position = cursor.getLong(posIndex), - parent = parent, - queueIndex = cursor.getInt(indexIndex), - playbackMode = mode, - isShuffling = cursor.getInt(shuffleIndex) == 1, - loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE, - ) + state = + SavedState( + song = song, + position = cursor.getLong(posIndex), + parent = parent, + queueIndex = cursor.getInt(indexIndex), + playbackMode = mode, + isShuffling = cursor.getInt(shuffleIndex) == 1, + loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE, + ) logD("Successfully read playback state: $state") } @@ -157,9 +153,7 @@ class PlaybackStateDatabase(context: Context) : return state } - /** - * Clear the previously written [SavedState] and write a new one. - */ + /** Clear the previously written [SavedState] and write a new one. */ fun writeState(state: SavedState) { assertBackgroundThread() @@ -168,16 +162,17 @@ class PlaybackStateDatabase(context: Context) : this@PlaybackStateDatabase.logD("Wiped state db") - val stateData = ContentValues(10).apply { - put(StateColumns.COLUMN_ID, 0) - put(StateColumns.COLUMN_SONG_HASH, state.song?.id) - put(StateColumns.COLUMN_POSITION, state.position) - put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id) - put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex) - put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt()) - put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling) - put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt()) - } + val stateData = + ContentValues(10).apply { + put(StateColumns.COLUMN_ID, 0) + put(StateColumns.COLUMN_SONG_HASH, state.song?.id) + put(StateColumns.COLUMN_POSITION, state.position) + put(StateColumns.COLUMN_PARENT_HASH, state.parent?.id) + put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex) + put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt()) + put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling) + put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt()) + } insert(TABLE_NAME_STATE, null, stateData) } @@ -202,9 +197,7 @@ class PlaybackStateDatabase(context: Context) : while (cursor.moveToNext()) { musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex)) - ?.let { song -> - queue.add(song) - } + ?.let { song -> queue.add(song) } } } @@ -213,16 +206,12 @@ class PlaybackStateDatabase(context: Context) : return queue } - /** - * Write a queue to the database. - */ + /** Write a queue to the database. */ fun writeQueue(queue: MutableList) { assertBackgroundThread() val database = writableDatabase - database.transaction { - delete(TABLE_NAME_QUEUE, null, null) - } + database.transaction { delete(TABLE_NAME_QUEUE, null, null) } logD("Wiped queue db") @@ -243,11 +232,12 @@ class PlaybackStateDatabase(context: Context) : val song = queue[i] i++ - val itemData = ContentValues(4).apply { - put(QueueColumns.ID, idStart + i) - put(QueueColumns.SONG_HASH, song.id) - put(QueueColumns.ALBUM_HASH, song.album.id) - } + val itemData = + ContentValues(4).apply { + put(QueueColumns.ID, idStart + i) + put(QueueColumns.SONG_HASH, song.id) + put(QueueColumns.ALBUM_HASH, song.album.id) + } insert(TABLE_NAME_QUEUE, null, itemData) } @@ -295,12 +285,9 @@ class PlaybackStateDatabase(context: Context) : const val TABLE_NAME_STATE = "playback_state_table" const val TABLE_NAME_QUEUE = "queue_table" - @Volatile - private var INSTANCE: PlaybackStateDatabase? = null + @Volatile private var INSTANCE: PlaybackStateDatabase? = null - /** - * Get/Instantiate the single instance of [PlaybackStateDatabase]. - */ + /** Get/Instantiate the single instance of [PlaybackStateDatabase]. */ fun getInstance(context: Context): PlaybackStateDatabase { val currentInstance = INSTANCE diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 348bb2a81..f1c07d8fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackStateManager.kt is part of Auxio. * * 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 @@ -15,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.playback.state import android.content.Context @@ -35,8 +34,10 @@ import org.oxycblt.auxio.util.logE * Master class (and possible god object) for the playback state. * * This should ***NOT*** be used outside of the playback module. - * - If you want to use the playback state in the UI, use [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs. - * - If you want to use the playback state with the ExoPlayer instance or system-side things, use [org.oxycblt.auxio.playback.system.PlaybackService]. + * - If you want to use the playback state in the UI, use + * [org.oxycblt.auxio.playback.PlaybackViewModel] as it can withstand volatile UIs. + * - If you want to use the playback state with the ExoPlayer instance or system-side things, use + * [org.oxycblt.auxio.playback.system.PlaybackService]. * * All access should be done with [PlaybackStateManager.getInstance]. * @author OxygenCobalt @@ -90,27 +91,38 @@ class PlaybackStateManager private constructor() { private var mHasPlayed = false /** The currently playing song. Null if there isn't one */ - val song: Song? get() = mSong + val song: Song? + get() = mSong /** The parent the queue is based on, null if all_songs */ - val parent: MusicParent? get() = mParent + val parent: MusicParent? + get() = mParent /** The current playback progress */ - val position: Long get() = mPosition + val position: Long + get() = mPosition /** The current queue determined by [parent] and [playbackMode] */ - val queue: List get() = mQueue + val queue: List + get() = mQueue /** The current position in the queue */ - val index: Int get() = mIndex + val index: Int + get() = mIndex /** The current [PlaybackMode] */ - val playbackMode: PlaybackMode get() = mPlaybackMode + val playbackMode: PlaybackMode + get() = mPlaybackMode /** Whether playback is paused or not */ - val isPlaying: Boolean get() = mIsPlaying + val isPlaying: Boolean + get() = mIsPlaying /** Whether the queue is shuffled */ - val isShuffling: Boolean get() = mIsShuffling + val isShuffling: Boolean + get() = mIsShuffling /** The current [LoopMode] */ - val loopMode: LoopMode get() = mLoopMode + val loopMode: LoopMode + get() = mLoopMode /** Whether this instance has already been restored */ - val isRestored: Boolean get() = mIsRestored + val isRestored: Boolean + get() = mIsRestored /** Whether playback has begun in this instance during **PlaybackService's Lifecycle.** */ - val hasPlayed: Boolean get() = mHasPlayed + val hasPlayed: Boolean + get() = mHasPlayed private val settingsManager = SettingsManager.getInstance() @@ -119,16 +131,14 @@ class PlaybackStateManager private constructor() { private val callbacks = mutableListOf() /** - * Add a [PlaybackStateManager.Callback] to this instance. - * Make sure to remove the callback with [removeCallback] when done. + * Add a [PlaybackStateManager.Callback] to this instance. Make sure to remove the callback with + * [removeCallback] when done. */ fun addCallback(callback: Callback) { callbacks.add(callback) } - /** - * Remove a [PlaybackStateManager.Callback] bound to this instance. - */ + /** Remove a [PlaybackStateManager.Callback] bound to this instance. */ fun removeCallback(callback: Callback) { callbacks.remove(callback) } @@ -149,17 +159,14 @@ class PlaybackStateManager private constructor() { mParent = null mQueue = musicStore.songs.toMutableList() } - PlaybackMode.IN_GENRE -> { mParent = song.genre mQueue = song.genre.songs.toMutableList() } - PlaybackMode.IN_ARTIST -> { mParent = song.album.artist mQueue = song.album.artist.songs.toMutableList() } - PlaybackMode.IN_ALBUM -> { mParent = song.album mQueue = song.album.songs.toMutableList() @@ -188,12 +195,10 @@ class PlaybackStateManager private constructor() { mQueue = parent.songs.toMutableList() mPlaybackMode = PlaybackMode.IN_ALBUM } - is Artist -> { mQueue = parent.songs.toMutableList() mPlaybackMode = PlaybackMode.IN_ARTIST } - is Genre -> { mQueue = parent.songs.toMutableList() mPlaybackMode = PlaybackMode.IN_GENRE @@ -204,9 +209,7 @@ class PlaybackStateManager private constructor() { updatePlayback(mQueue[0]) } - /** - * Shuffle all songs. - */ + /** Shuffle all songs. */ fun shuffleAll() { val musicStore = MusicStore.maybeGetInstance() ?: return @@ -218,9 +221,7 @@ class PlaybackStateManager private constructor() { updatePlayback(mQueue[0]) } - /** - * Update the playback to a new [song], doing all the required logic. - */ + /** Update the playback to a new [song], doing all the required logic. */ private fun updatePlayback(song: Song, shouldPlay: Boolean = true) { mSong = song mPosition = 0 @@ -229,9 +230,7 @@ class PlaybackStateManager private constructor() { // --- QUEUE FUNCTIONS --- - /** - * Go to the next song, along with doing all the checks that entails. - */ + /** Go to the next song, along with doing all the checks that entails. */ fun next() { // Increment the index, if it cannot be incremented any further, then // loop and pause/resume playback depending on the setting @@ -246,9 +245,7 @@ class PlaybackStateManager private constructor() { pushQueueUpdate() } - /** - * Go to the previous song, doing any checks that are needed. - */ + /** Go to the previous song, doing any checks that are needed. */ fun prev() { // If enabled, rewind before skipping back if the position is past 3 seconds [3000ms] if (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) { @@ -266,9 +263,7 @@ class PlaybackStateManager private constructor() { // --- QUEUE EDITING FUNCTIONS --- - /** - * Remove a queue item at [index]. Will ignore invalid indexes. - */ + /** Remove a queue item at [index]. Will ignore invalid indexes. */ fun removeQueueItem(index: Int): Boolean { if (index > mQueue.size || index < 0) { logE("Index is out of bounds, did not remove queue item") @@ -281,9 +276,7 @@ class PlaybackStateManager private constructor() { return true } - /** - * Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. - */ + /** Move a queue item at [from] to a position at [to]. Will ignore invalid indexes. */ fun moveQueueItems(from: Int, to: Int): Boolean { if (from > mQueue.size || from < 0 || to > mQueue.size || to < 0) { logE("Indices were out of bounds, did not move queue item") @@ -296,9 +289,7 @@ class PlaybackStateManager private constructor() { return true } - /** - * Add a [song] to the top of the queue. - */ + /** Add a [song] to the top of the queue. */ fun playNext(song: Song) { if (mQueue.isEmpty()) { return @@ -308,9 +299,7 @@ class PlaybackStateManager private constructor() { pushQueueUpdate() } - /** - * Add a list of [songs] to the top of the queue. - */ + /** Add a list of [songs] to the top of the queue. */ fun playNext(songs: List) { if (mQueue.isEmpty()) { return @@ -320,29 +309,21 @@ class PlaybackStateManager private constructor() { pushQueueUpdate() } - /** - * Add a [song] to the end of the queue. - */ + /** Add a [song] to the end of the queue. */ fun addToQueue(song: Song) { mQueue.add(song) pushQueueUpdate() } - /** - * Add a list of [songs] to the end of the queue. - */ + /** Add a list of [songs] to the end of the queue. */ fun addToQueue(songs: List) { mQueue.addAll(songs) pushQueueUpdate() } - /** - * Force any callbacks to receive a queue update. - */ + /** Force any callbacks to receive a queue update. */ private fun pushQueueUpdate() { - callbacks.forEach { - it.onQueueUpdate(mQueue, mIndex) - } + callbacks.forEach { it.onQueueUpdate(mQueue, mIndex) } } // --- SHUFFLE FUNCTIONS --- @@ -392,16 +373,17 @@ class PlaybackStateManager private constructor() { val musicStore = MusicStore.maybeGetInstance() ?: return val lastSong = mSong - mQueue = when (mPlaybackMode) { - PlaybackMode.ALL_SONGS -> - settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList() - PlaybackMode.IN_ALBUM -> - settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList() - PlaybackMode.IN_ARTIST -> - settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList() - PlaybackMode.IN_GENRE -> - settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList() - } + mQueue = + when (mPlaybackMode) { + PlaybackMode.ALL_SONGS -> + settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList() + PlaybackMode.IN_ALBUM -> + settingsManager.detailAlbumSort.sortAlbum(mParent as Album).toMutableList() + PlaybackMode.IN_ARTIST -> + settingsManager.detailArtistSort.sortArtist(mParent as Artist).toMutableList() + PlaybackMode.IN_GENRE -> + settingsManager.detailGenreSort.sortGenre(mParent as Genre).toMutableList() + } if (keepSong) { mIndex = mQueue.indexOf(lastSong) @@ -412,9 +394,7 @@ class PlaybackStateManager private constructor() { // --- STATE FUNCTIONS --- - /** - * Set whether this instance is currently [playing]. - */ + /** Set whether this instance is currently [playing]. */ fun setPlaying(playing: Boolean) { if (mIsPlaying != playing) { if (playing) { @@ -449,39 +429,29 @@ class PlaybackStateManager private constructor() { callbacks.forEach { it.onSeek(position) } } - /** - * Rewind to the beginning of a song. - */ + /** Rewind to the beginning of a song. */ fun rewind() { seekTo(0) setPlaying(true) } - /** - * Loop playback around to the beginning. - */ + /** Loop playback around to the beginning. */ fun loop() { seekTo(0) setPlaying(!settingsManager.pauseOnLoop) } - /** - * Set the [LoopMode] to [mode]. - */ + /** Set the [LoopMode] to [mode]. */ fun setLoopMode(mode: LoopMode) { mLoopMode = mode } - /** - * Mark whether this instance has played or not - */ + /** Mark whether this instance has played or not */ fun setHasPlayed(hasPlayed: Boolean) { mHasPlayed = hasPlayed } - /** - * Mark this instance as restored. - */ + /** Mark this instance as restored. */ fun markRestored() { mIsRestored = true } @@ -503,16 +473,19 @@ class PlaybackStateManager private constructor() { database.writeState( PlaybackStateDatabase.SavedState( - mSong, mPosition, mParent, mIndex, - mPlaybackMode, mIsShuffling, mLoopMode, - ) - ) + mSong, + mPosition, + mParent, + mIndex, + mPlaybackMode, + mIsShuffling, + mLoopMode, + )) database.writeQueue(mQueue) this@PlaybackStateManager.logD( - "State save completed successfully in ${System.currentTimeMillis() - start}ms" - ) + "State save completed successfully in ${System.currentTimeMillis() - start}ms") } } @@ -549,9 +522,7 @@ class PlaybackStateManager private constructor() { markRestored() } - /** - * Unpack a [playbackState] into this instance. - */ + /** Unpack a [playbackState] into this instance. */ private fun unpackFromPlaybackState(playbackState: PlaybackStateDatabase.SavedState) { // Turn the simplified information from PlaybackState into usable data. @@ -573,26 +544,23 @@ class PlaybackStateManager private constructor() { pushQueueUpdate() } - /** - * Do a sanity check to make sure the parent was not lost in the restore process. - */ + /** Do a sanity check to make sure the parent was not lost in the restore process. */ private fun doParentSanityCheck() { // Check if the parent was lost while in the DB. if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) { logD("Parent lost, attempting restore") - mParent = when (mPlaybackMode) { - PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album - PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist - PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre - PlaybackMode.ALL_SONGS -> null - } + mParent = + when (mPlaybackMode) { + PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album + PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist + PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre + PlaybackMode.ALL_SONGS -> null + } } } - /** - * Do a sanity check to make sure that the index lines up with the current song. - */ + /** Do a sanity check to make sure that the index lines up with the current song. */ private fun doIndexSanityCheck() { // Be careful with how we handle the queue since a possible index de-sync // could easily result in an OOB crash. @@ -639,9 +607,8 @@ class PlaybackStateManager private constructor() { } /** - * The interface for receiving updates from [PlaybackStateManager]. - * Add the callback to [PlaybackStateManager] using [addCallback], - * remove them on destruction with [removeCallback]. + * The interface for receiving updates from [PlaybackStateManager]. Add the callback to + * [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback]. */ interface Callback { fun onSongUpdate(song: Song?) {} @@ -658,12 +625,9 @@ class PlaybackStateManager private constructor() { companion object { private const val REWIND_THRESHOLD = 3000L - @Volatile - private var INSTANCE: PlaybackStateManager? = null + @Volatile private var INSTANCE: PlaybackStateManager? = null - /** - * Get/Instantiate the single instance of [PlaybackStateManager]. - */ + /** Get/Instantiate the single instance of [PlaybackStateManager]. */ fun getInstance(): PlaybackStateManager { val currentInstance = INSTANCE diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt index 4380eaba8..f772843a6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/AudioReactor.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AudioReactor.kt is part of Auxio. * * 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 @@ -15,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.playback.system import android.content.Context @@ -28,22 +27,20 @@ import androidx.media.AudioManagerCompat import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.vorbis.VorbisComment +import kotlin.math.pow import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -import kotlin.math.pow /** * Manages the current volume and playback state across ReplayGain and AudioFocus events. * @author OxygenCobalt */ -class AudioReactor( - context: Context, - private val callback: (Float) -> Unit -) : AudioManager.OnAudioFocusChangeListener, SettingsManager.Callback { +class AudioReactor(context: Context, private val callback: (Float) -> Unit) : + AudioManager.OnAudioFocusChangeListener, SettingsManager.Callback { private data class Gain(val track: Float, val album: Float) private data class GainTag(val key: String, val value: Float) @@ -51,16 +48,16 @@ class AudioReactor( private val settingsManager = SettingsManager.getInstance() private val audioManager = context.getSystemServiceSafe(AudioManager::class) - private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) - .setWillPauseWhenDucked(false) - .setAudioAttributes( - AudioAttributesCompat.Builder() - .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .build() - ) - .setOnAudioFocusChangeListener(this) - .build() + private val request = + AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setWillPauseWhenDucked(false) + .setAudioAttributes( + AudioAttributesCompat.Builder() + .setContentType(AudioAttributesCompat.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributesCompat.USAGE_MEDIA) + .build()) + .setOnAudioFocusChangeListener(this) + .build() private var pauseWasTransient = false @@ -82,19 +79,16 @@ class AudioReactor( settingsManager.addCallback(this) } - /** - * Request the android system for audio focus - */ + /** Request the android system for audio focus */ fun requestFocus() { logD("Requesting audio focus") AudioManagerCompat.requestAudioFocus(audioManager, request) } /** - * Updates the rough volume adjustment for [Metadata] with ReplayGain tags. - * This is based off Vanilla Music's implementation. - * TODO: Add ReplayGain pre-amp - * TODO: Add positive ReplayGain values + * Updates the rough volume adjustment for [Metadata] with ReplayGain tags. This is based off + * Vanilla Music's implementation. TODO: Add ReplayGain pre-amp TODO: Add positive ReplayGain + * values */ fun applyReplayGain(metadata: Metadata?) { if (metadata == null) { @@ -104,49 +98,44 @@ class AudioReactor( } // ReplayGain is configurable, so determine what to do based off of the mode. - val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) { - ReplayGainMode.OFF -> { - logD("ReplayGain is off") - volume = 1f - return + val useAlbumGain: (Gain) -> Boolean = + when (settingsManager.replayGainMode) { + ReplayGainMode.OFF -> { + logD("ReplayGain is off") + volume = 1f + return + } + + // User wants track gain to be preferred. Default to album gain only if there + // is no track gain. + ReplayGainMode.TRACK -> { gain -> gain.track == 0f } + + // User wants album gain to be preferred. Default to track gain only if there + // is no album gain. + ReplayGainMode.ALBUM -> { gain -> gain.album != 0f } + + // User wants album gain to be used when in an album, track gain otherwise. + ReplayGainMode.DYNAMIC -> { _ -> + playbackManager.parent is Album && + playbackManager.song?.album == playbackManager.parent + } } - // User wants track gain to be preferred. Default to album gain only if there - // is no track gain. - ReplayGainMode.TRACK -> - { gain -> - gain.track == 0f - } - - // User wants album gain to be preferred. Default to track gain only if there - // is no album gain. - ReplayGainMode.ALBUM -> - { gain -> - gain.album != 0f - } - - // User wants album gain to be used when in an album, track gain otherwise. - ReplayGainMode.DYNAMIC -> - { _ -> - playbackManager.parent is Album && - playbackManager.song?.album == playbackManager.parent - } - } - val gain = parseReplayGain(metadata) - val adjust = if (gain != null) { - if (useAlbumGain(gain)) { - logD("Using album gain") - gain.album + val adjust = + if (gain != null) { + if (useAlbumGain(gain)) { + logD("Using album gain") + gain.album + } else { + logD("Using track gain") + gain.track + } } else { - logD("Using track gain") - gain.track + // No gain tags were present + 0f } - } else { - // No gain tags were present - 0f - } // Final adjustment along the volume curve. // Ensure this is clamped to 0 or 1 so that it can be used as a volume. @@ -171,12 +160,10 @@ class AudioReactor( key = entry.description?.uppercase() value = entry.value } - is VorbisComment -> { key = entry.key value = entry.value } - else -> continue } @@ -226,9 +213,7 @@ class AudioReactor( } } - /** - * Abandon the current focus request and any callbacks - */ + /** Abandon the current focus request and any callbacks */ fun release() { AudioManagerCompat.abandonAudioFocusRequest(audioManager, request) settingsManager.removeCallback(this) @@ -302,11 +287,6 @@ class AudioReactor( const val R128_TRACK = "R128_TRACK_GAIN" const val R128_ALBUM = "R128_ALBUM_GAIN" - val REPLAY_GAIN_TAGS = arrayOf( - RG_TRACK, - RG_ALBUM, - R128_ALBUM, - R128_TRACK - ) + val REPLAY_GAIN_TAGS = arrayOf(RG_TRACK, RG_ALBUM, R128_ALBUM, R128_TRACK) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt index 8cde108b2..4d16ae095 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaButtonReceiver.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.playback.system import android.content.BroadcastReceiver @@ -8,14 +25,14 @@ import androidx.core.content.ContextCompat import org.oxycblt.auxio.util.logD /** - * Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON - * intent to determine the media apps on a system. *Auxio does not expose this.* Auxio exposes - * a MediaSession that an app should control instead through the much better MediaController API. - * But who cares about that, we need to make sure the 3% of barely functioning TouchWiz devices - * running KitKat don't break! To prevent Auxio from not showing up at all in these apps, we - * declare a BroadcastReceiver that deliberately handles this event. This also means that Auxio - * will start without warning if you use the media buttons while the app exists, because I guess - * we just have to deal with this. + * Some apps like to party like it's 2011 and just blindly query for the ACTION_MEDIA_BUTTON intent + * to determine the media apps on a system. *Auxio does not expose this.* Auxio exposes a + * MediaSession that an app should control instead through the much better MediaController API. But + * who cares about that, we need to make sure the 3% of barely functioning TouchWiz devices running + * KitKat don't break! To prevent Auxio from not showing up at all in these apps, we declare a + * BroadcastReceiver that deliberately handles this event. This also means that Auxio will start + * without warning if you use the media buttons while the app exists, because I guess we just have + * to deal with this. * @author OxygenCobalt */ class MediaButtonReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt index d0fca446f..a88e4fe99 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackNotification.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackNotification.kt is part of Auxio. * * 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 @@ -15,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.playback.system import android.annotation.SuppressLint @@ -37,15 +36,14 @@ import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newMainIntent /** - * The unified notification for [PlaybackService]. This is not self-sufficient, updates have - * to be delivered manually. + * The unified notification for [PlaybackService]. This is not self-sufficient, updates have to be + * delivered manually. * @author OxygenCobalt */ @SuppressLint("RestrictedApi") -class PlaybackNotification private constructor( - private val context: Context, - mediaToken: MediaSessionCompat.Token -) : NotificationCompat.Builder(context, CHANNEL_ID) { +class PlaybackNotification +private constructor(private val context: Context, mediaToken: MediaSessionCompat.Token) : + NotificationCompat.Builder(context, CHANNEL_ID) { init { setSmallIcon(R.drawable.ic_auxio) setCategory(NotificationCompat.CATEGORY_SERVICE) @@ -61,11 +59,7 @@ class PlaybackNotification private constructor( addAction(buildAction(context, PlaybackService.ACTION_SKIP_NEXT, R.drawable.ic_skip_next)) addAction(buildAction(context, PlaybackService.ACTION_EXIT, R.drawable.ic_exit)) - setStyle( - MediaStyle() - .setMediaSession(mediaToken) - .setShowActionsInCompactView(1, 2, 3) - ) + setStyle(MediaStyle().setMediaSession(mediaToken).setShowActionsInCompactView(1, 2, 3)) // Don't connect to PlaybackStateManager here. This is because it's possible for this // notification to not be updated by PlaybackStateManager before PlaybackService pushes @@ -96,30 +90,22 @@ class PlaybackNotification private constructor( } } - /** - * Set the playing icon on the notification - */ + /** Set the playing icon on the notification */ fun setPlaying(isPlaying: Boolean) { mActions[2] = buildPlayPauseAction(context, isPlaying) } - /** - * Update the first action to reflect the [loopMode] given. - */ + /** Update the first action to reflect the [loopMode] given. */ fun setLoop(loopMode: LoopMode) { mActions[0] = buildLoopAction(context, loopMode) } - /** - * Update the first action to reflect whether the queue is shuffled or not - */ + /** Update the first action to reflect whether the queue is shuffled or not */ fun setShuffle(isShuffling: Boolean) { mActions[0] = buildShuffleAction(context, isShuffling) } - /** - * Apply the current [parent] to the header of the notification. - */ + /** Apply the current [parent] to the header of the notification. */ fun setParent(parent: MusicParent?) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return @@ -138,15 +124,13 @@ class PlaybackNotification private constructor( return buildAction(context, PlaybackService.ACTION_PLAY_PAUSE, drawableRes) } - private fun buildLoopAction( - context: Context, - loopMode: LoopMode - ): NotificationCompat.Action { - val drawableRes = when (loopMode) { - LoopMode.NONE -> R.drawable.ic_remote_loop_off - LoopMode.ALL -> R.drawable.ic_loop - LoopMode.TRACK -> R.drawable.ic_loop_one - } + private fun buildLoopAction(context: Context, loopMode: LoopMode): NotificationCompat.Action { + val drawableRes = + when (loopMode) { + LoopMode.NONE -> R.drawable.ic_remote_loop_off + LoopMode.ALL -> R.drawable.ic_loop + LoopMode.TRACK -> R.drawable.ic_loop_one + } return buildAction(context, PlaybackService.ACTION_LOOP, drawableRes) } @@ -155,7 +139,8 @@ class PlaybackNotification private constructor( context: Context, isShuffled: Boolean ): NotificationCompat.Action { - val drawableRes = if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_remote_shuffle_off + val drawableRes = + if (isShuffled) R.drawable.ic_shuffle else R.drawable.ic_remote_shuffle_off return buildAction(context, PlaybackService.ACTION_SHUFFLE, drawableRes) } @@ -165,10 +150,9 @@ class PlaybackNotification private constructor( actionName: String, @DrawableRes iconRes: Int ): NotificationCompat.Action { - val action = NotificationCompat.Action.Builder( - iconRes, actionName, - context.newBroadcastIntent(actionName) - ) + val action = + NotificationCompat.Action.Builder( + iconRes, actionName, context.newBroadcastIntent(actionName)) return action.build() } @@ -177,19 +161,18 @@ class PlaybackNotification private constructor( const val CHANNEL_ID = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK" const val NOTIFICATION_ID = 0xA0A0 - /** - * Build a new instance of [PlaybackNotification]. - */ + /** Build a new instance of [PlaybackNotification]. */ fun from( context: Context, notificationManager: NotificationManager, mediaSession: MediaSessionCompat ): PlaybackNotification { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, context.getString(R.string.info_channel_name), - NotificationManager.IMPORTANCE_DEFAULT - ) + val channel = + NotificationChannel( + CHANNEL_ID, + context.getString(R.string.info_channel_name), + NotificationManager.IMPORTANCE_DEFAULT) notificationManager.createNotificationChannel(channel) } 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 c8e0d8ccc..485859d23 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 @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackService.kt is part of Auxio. * * 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 @@ -15,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.playback.system import android.app.NotificationManager @@ -69,11 +68,12 @@ import org.oxycblt.auxio.widgets.WidgetProvider * - Headset management * - Widgets * - * This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], - * so therefore there's no need to bind to it to deliver commands. + * This service relies on [PlaybackStateManager.Callback] and [SettingsManager.Callback], so + * therefore there's no need to bind to it to deliver commands. * @author OxygenCobalt */ -class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback { +class PlaybackService : + Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback { // Player components private lateinit var player: ExoPlayer private lateinit var mediaSession: MediaSessionCompat @@ -126,22 +126,20 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_MUSIC) .build(), - false - ) + false) - audioReactor = AudioReactor(this) { volume -> - logD("Updating player volume to $volume") - player.volume = volume - } + audioReactor = + AudioReactor(this) { volume -> + logD("Updating player volume to $volume") + player.volume = volume + } // --- SYSTEM SETUP --- widgets = WidgetController(this) // Set up the media button callbacks - mediaSession = MediaSessionCompat(this, packageName).apply { - isActive = true - } + mediaSession = MediaSessionCompat(this, packageName).apply { isActive = true } connector = PlaybackSessionConnector(this, player, mediaSession) @@ -215,7 +213,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac override fun onPlaybackStateChanged(state: Int) { when (state) { Player.STATE_READY -> startPolling() - Player.STATE_ENDED -> { if (playbackManager.loopMode == LoopMode.TRACK) { playbackManager.loop() @@ -223,7 +220,6 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac playbackManager.next() } } - else -> {} } } @@ -347,17 +343,14 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac // --- OTHER FUNCTIONS --- - /** - * Create the [ExoPlayer] instance. - */ + /** Create the [ExoPlayer] instance. */ private fun newPlayer(): ExoPlayer { // Since Auxio is a music player, only specify an audio renderer to save // battery/apk size/cache size val audioRenderer = RenderersFactory { handler, _, audioListener, _, _ -> arrayOf( MediaCodecAudioRenderer(this, MediaCodecSelector.DEFAULT, handler, audioListener), - LibflacAudioRenderer(handler, audioListener) - ) + LibflacAudioRenderer(handler, audioListener)) } // Enable constant bitrate seeking so that certain MP3s/AACs are seekable @@ -369,9 +362,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac .build() } - /** - * Fully restore the notification and playback state - */ + /** Fully restore the notification and playback state */ private fun restore() { logD("Restoring the service state") @@ -387,16 +378,16 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac widgets.update() } - /** - * Start polling the position on a coroutine. - */ + /** Start polling the position on a coroutine. */ private fun startPolling() { - val pollFlow = flow { - while (true) { - emit(player.currentPosition) - delay(POS_POLL_INTERVAL) - } - }.conflate() + val pollFlow = + flow { + while (true) { + emit(player.currentPosition) + delay(POS_POLL_INTERVAL) + } + } + .conflate() serviceScope.launch { pollFlow.takeWhile { player.isPlaying }.collect { pos -> @@ -416,9 +407,9 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac // Specify that this is a media service, if supported. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { startForeground( - PlaybackNotification.NOTIFICATION_ID, notification.build(), - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK - ) + PlaybackNotification.NOTIFICATION_ID, + notification.build(), + ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK) } else { startForeground(PlaybackNotification.NOTIFICATION_ID, notification.build()) } @@ -427,24 +418,19 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac } else { // If we are already in foreground just update the notification notificationManager.notify( - PlaybackNotification.NOTIFICATION_ID, notification.build() - ) + PlaybackNotification.NOTIFICATION_ID, notification.build()) } } } - /** - * Stop the foreground state and hide the notification - */ + /** Stop the foreground state and hide the notification */ private fun stopForegroundAndNotification() { stopForeground(true) notificationManager.cancel(PlaybackNotification.NOTIFICATION_ID) isForeground = false } - /** - * A [BroadcastReceiver] for receiving general playback events from the system. - */ + /** A [BroadcastReceiver] for receiving general playback events from the system. */ private inner class PlaybackReceiver : BroadcastReceiver() { private var initialHeadsetPlugEventHandled = false @@ -477,56 +463,44 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac AudioManager.ACTION_AUDIO_BECOMING_NOISY -> pauseFromPlug() // --- AUXIO EVENTS --- - ACTION_PLAY_PAUSE -> playbackManager.setPlaying( - !playbackManager.isPlaying - ) - - ACTION_LOOP -> playbackManager.setLoopMode( - playbackManager.loopMode.increment() - ) - - ACTION_SHUFFLE -> playbackManager.setShuffling( - !playbackManager.isShuffling, keepSong = true - ) - + ACTION_PLAY_PAUSE -> playbackManager.setPlaying(!playbackManager.isPlaying) + ACTION_LOOP -> playbackManager.setLoopMode(playbackManager.loopMode.increment()) + ACTION_SHUFFLE -> + playbackManager.setShuffling(!playbackManager.isShuffling, keepSong = true) ACTION_SKIP_PREV -> playbackManager.prev() ACTION_SKIP_NEXT -> playbackManager.next() - ACTION_EXIT -> { playbackManager.setPlaying(false) stopForegroundAndNotification() } - WidgetProvider.ACTION_WIDGET_UPDATE -> widgets.update() } } /** - * Resume from a headset plug event in the case that the quirk is enabled. - * This functionality remains a quirk for two reasons: - * 1. Automatically resuming more or less overrides all other audio streams, which - * is not that friendly - * 2. There is a bug where playback will always start when this service starts, mostly - * due to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but - * I fear that it may not work on OEM skins that for whatever reason don't make this - * action fire. - * TODO: Figure out how players like Retro are able to get autoplay working with - * bluetooth headsets + * Resume from a headset plug event in the case that the quirk is enabled. This + * functionality remains a quirk for two reasons: + * 1. Automatically resuming more or less overrides all other audio streams, which is not + * that friendly + * 2. There is a bug where playback will always start when this service starts, mostly due + * to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but I fear + * that it may not work on OEM skins that for whatever reason don't make this action fire. + * TODO: Figure out how players like Retro are able to get autoplay working with bluetooth + * headsets */ private fun maybeResumeFromPlug() { if (playbackManager.song != null && settingsManager.headsetAutoplay && - initialHeadsetPlugEventHandled - ) { + initialHeadsetPlugEventHandled) { logD("Device connected, resuming") playbackManager.setPlaying(true) } } /** - * Pause from a headset plug. - * TODO: Find a way to centralize this stuff into a single BroadcastReciever instead - * of the weird disjointed arrangement between MediaSession and this. + * Pause from a headset plug. TODO: Find a way to centralize this stuff into a single + * BroadcastReciever instead of the weird disjointed arrangement between MediaSession and + * this. */ private fun pauseFromPlug() { if (playbackManager.song != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt index 0c211c683..afb9c999c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackSessionConnector.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * PlaybackSessionConnector.kt is part of Auxio. * * 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 @@ -15,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.playback.system import android.content.Context @@ -32,8 +31,8 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD /** - * Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], - * and [PlaybackStateManager]. + * Nightmarish class that coordinates communication between [MediaSessionCompat], [Player], and + * [PlaybackStateManager]. */ class PlaybackSessionConnector( private val context: Context, @@ -84,12 +83,13 @@ class PlaybackSessionConnector( } override fun onSetRepeatMode(repeatMode: Int) { - val mode = when (repeatMode) { - PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL - PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL - PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK - else -> LoopMode.NONE - } + val mode = + when (repeatMode) { + PlaybackStateCompat.REPEAT_MODE_ALL -> LoopMode.ALL + PlaybackStateCompat.REPEAT_MODE_GROUP -> LoopMode.ALL + PlaybackStateCompat.REPEAT_MODE_ONE -> LoopMode.TRACK + else -> LoopMode.NONE + } playbackManager.setLoopMode(mode) } @@ -98,8 +98,7 @@ class PlaybackSessionConnector( playbackManager.setShuffling( shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, - true - ) + true) } override fun onStop() { @@ -117,15 +116,16 @@ class PlaybackSessionConnector( val artistName = song.resolvedArtistName - val builder = MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name) - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName) - .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName) - .putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.resolvedAlbumName) - .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) + val builder = + MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, song.name) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, song.name) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_COMPOSER, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, artistName) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, song.resolvedAlbumName) + .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, song.duration) // Load the cover asynchronously. This is the entire reason I don't use a plain // MediaSessionConnector, which AFAIK makes it impossible to load this the way I do @@ -144,14 +144,12 @@ class PlaybackSessionConnector( override fun onEvents(player: Player, events: Player.Events) { if (events.containsAny( - Player.EVENT_POSITION_DISCONTINUITY, - Player.EVENT_PLAYBACK_STATE_CHANGED, - Player.EVENT_PLAY_WHEN_READY_CHANGED, - Player.EVENT_IS_PLAYING_CHANGED, - Player.EVENT_REPEAT_MODE_CHANGED, - Player.EVENT_PLAYBACK_PARAMETERS_CHANGED - ) - ) { + Player.EVENT_POSITION_DISCONTINUITY, + Player.EVENT_PLAYBACK_STATE_CHANGED, + Player.EVENT_PLAY_WHEN_READY_CHANGED, + Player.EVENT_IS_PLAYING_CHANGED, + Player.EVENT_REPEAT_MODE_CHANGED, + Player.EVENT_PLAYBACK_PARAMETERS_CHANGED)) { invalidateSessionState() } } @@ -162,24 +160,20 @@ class PlaybackSessionConnector( logD("Updating media session state") // Position updates arrive faster when you upload STATE_PAUSED for some insane reason. - val state = PlaybackStateCompat.Builder() - .setActions(ACTIONS) - .setBufferedPosition(player.bufferedPosition) - .setState( - PlaybackStateCompat.STATE_PAUSED, - player.currentPosition, - 1.0f, - SystemClock.elapsedRealtime() - ) + val state = + PlaybackStateCompat.Builder() + .setActions(ACTIONS) + .setBufferedPosition(player.bufferedPosition) + .setState( + PlaybackStateCompat.STATE_PAUSED, + player.currentPosition, + 1.0f, + SystemClock.elapsedRealtime()) mediaSession.setPlaybackState(state.build()) state.setState( - getPlayerState(), - player.currentPosition, - 1.0f, - SystemClock.elapsedRealtime() - ) + getPlayerState(), player.currentPosition, 1.0f, SystemClock.elapsedRealtime()) mediaSession.setPlaybackState(state.build()) } @@ -199,14 +193,15 @@ class PlaybackSessionConnector( } companion object { - const val ACTIONS = PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_SET_REPEAT_MODE or - PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_SEEK_TO or - PlaybackStateCompat.ACTION_STOP + const val ACTIONS = + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SET_REPEAT_MODE or + PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SEEK_TO or + PlaybackStateCompat.ACTION_STOP } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGainMode.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGainMode.kt index 023671efc..fba56c0f9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGainMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/ReplayGainMode.kt @@ -1,8 +1,23 @@ +/* + * 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.playback.system -/** - * Represents the current setting for ReplayGain. - */ +/** Represents the current setting for ReplayGain. */ enum class ReplayGainMode { /** Do not apply ReplayGain. */ OFF, @@ -13,9 +28,7 @@ enum class ReplayGainMode { /** Apply the album gain only when playing from an album, defaulting to track gain otherwise. */ DYNAMIC; - /** - * Converts this type to an integer constant. - */ + /** Converts this type to an integer constant. */ fun toInt(): Int { return when (this) { OFF -> INT_OFF @@ -31,9 +44,7 @@ enum class ReplayGainMode { private const val INT_ALBUM = 0xA112 private const val INT_DYNAMIC = 0xA113 - /** - * Converts an integer constant to this type. - */ + /** Converts an integer constant to this type. */ fun fromInt(value: Int): ReplayGainMode? { return when (value) { INT_OFF -> OFF 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 3a7059520..4b3d6f7ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SearchAdapter.kt is part of Auxio. * * 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 @@ -15,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.search import android.view.View @@ -58,24 +57,15 @@ class SearchAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { - GenreViewHolder.ITEM_TYPE -> GenreViewHolder.from( - parent.context, doOnClick, doOnLongClick - ) - - ArtistViewHolder.ITEM_TYPE -> ArtistViewHolder.from( - parent.context, doOnClick, doOnLongClick - ) - - AlbumViewHolder.ITEM_TYPE -> AlbumViewHolder.from( - parent.context, doOnClick, doOnLongClick - ) - - SongViewHolder.ITEM_TYPE -> SongViewHolder.from( - parent.context, doOnClick, doOnLongClick - ) - + GenreViewHolder.ITEM_TYPE -> + GenreViewHolder.from(parent.context, doOnClick, doOnLongClick) + ArtistViewHolder.ITEM_TYPE -> + ArtistViewHolder.from(parent.context, doOnClick, doOnLongClick) + AlbumViewHolder.ITEM_TYPE -> + AlbumViewHolder.from(parent.context, doOnClick, doOnLongClick) + SongViewHolder.ITEM_TYPE -> + SongViewHolder.from(parent.context, doOnClick, doOnLongClick) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) - else -> error("Invalid ViewHolder item type") } } 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 f83e3b566..ae2ac2011 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SearchFragment.kt is part of Auxio. * * 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 @@ -15,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.search import android.os.Bundle @@ -67,24 +66,21 @@ class SearchFragment : Fragment() { val imm = requireContext().getSystemServiceSafe(InputMethodManager::class) - val searchAdapter = SearchAdapter( - doOnClick = { item -> - onItemSelection(item, imm) - }, - ::newMenu - ) + val searchAdapter = + SearchAdapter(doOnClick = { item -> onItemSelection(item, imm) }, ::newMenu) // --- UI SETUP -- binding.lifecycleOwner = viewLifecycleOwner binding.searchToolbar.apply { - val itemId = when (searchModel.filterMode) { - DisplayMode.SHOW_SONGS -> R.id.option_filter_songs - DisplayMode.SHOW_ALBUMS -> R.id.option_filter_albums - DisplayMode.SHOW_ARTISTS -> R.id.option_filter_artists - DisplayMode.SHOW_GENRES -> R.id.option_filter_genres - null -> R.id.option_filter_all - } + val itemId = + when (searchModel.filterMode) { + DisplayMode.SHOW_SONGS -> R.id.option_filter_songs + DisplayMode.SHOW_ALBUMS -> R.id.option_filter_albums + DisplayMode.SHOW_ARTISTS -> R.id.option_filter_artists + DisplayMode.SHOW_GENRES -> R.id.option_filter_genres + null -> R.id.option_filter_all + } menu.findItem(itemId).isChecked = true @@ -114,9 +110,7 @@ class SearchFragment : Fragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown requestFocus() - postDelayed(200) { - imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) - } + postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) } launchedKeyboard = true } @@ -125,9 +119,7 @@ class SearchFragment : Fragment() { binding.searchRecycler.apply { adapter = searchAdapter - applySpans { pos -> - searchAdapter.currentList[pos] is Header - } + applySpans { pos -> searchAdapter.currentList[pos] is Header } } // --- VIEWMODEL SETUP --- @@ -148,15 +140,14 @@ class SearchFragment : Fragment() { } detailModel.navToItem.observe(viewLifecycleOwner) { item -> - findNavController().navigate( - when (item) { - is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id) - is Album -> SearchFragmentDirections.actionShowAlbum(item.id) - is Artist -> SearchFragmentDirections.actionShowArtist(item.id) - - else -> return@observe - } - ) + findNavController() + .navigate( + when (item) { + is Song -> SearchFragmentDirections.actionShowAlbum(item.album.id) + is Album -> SearchFragmentDirections.actionShowAlbum(item.id) + is Artist -> SearchFragmentDirections.actionShowArtist(item.id) + else -> return@observe + }) imm.hide() } @@ -177,8 +168,7 @@ class SearchFragment : Fragment() { } /** - * Function that handles when an [item] is selected. - * Handles all datatypes that are selectable. + * Function that handles when an [item] is selected. Handles all datatypes that are selectable. */ private fun onItemSelection(item: Music, imm: InputMethodManager) { if (item is Song) { @@ -192,20 +182,20 @@ class SearchFragment : Fragment() { logD("Navigating to the detail fragment for ${item.name}") - findNavController().navigate( - when (item) { - is Genre -> SearchFragmentDirections.actionShowGenre(item.id) - is Artist -> SearchFragmentDirections.actionShowArtist(item.id) - is Album -> SearchFragmentDirections.actionShowAlbum(item.id) + findNavController() + .navigate( + when (item) { + is Genre -> SearchFragmentDirections.actionShowGenre(item.id) + is Artist -> SearchFragmentDirections.actionShowArtist(item.id) + is Album -> SearchFragmentDirections.actionShowAlbum(item.id) - // If given model wasn't valid, then reset the navigation status - // and abort the navigation. - else -> { - searchModel.setNavigating(false) - return - } - } - ) + // If given model wasn't valid, then reset the navigation status + // and abort the navigation. + else -> { + searchModel.setNavigating(false) + return + } + }) imm.hide() } 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 2f5445651..2cccf928e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SearchViewModel.kt is part of Auxio. * * 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 @@ -15,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.search import androidx.annotation.IdRes @@ -23,6 +22,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import java.text.Normalizer import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Header @@ -34,7 +34,6 @@ import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.util.logD -import java.text.Normalizer /** * The [ViewModel] for the search functionality @@ -47,9 +46,12 @@ class SearchViewModel : ViewModel() { private var mLastQuery = "" /** Current search results from the last [search] call. */ - val searchResults: LiveData> get() = mSearchResults - val isNavigating: Boolean get() = mIsNavigating - val filterMode: DisplayMode? get() = mFilterMode + val searchResults: LiveData> + get() = mSearchResults + val isNavigating: Boolean + get() = mIsNavigating + val filterMode: DisplayMode? + get() = mFilterMode private val settingsManager = SettingsManager.getInstance() @@ -63,8 +65,7 @@ class SearchViewModel : ViewModel() { } /** - * Use [query] to perform a search of the music library. - * Will push results to [searchResults]. + * Use [query] to perform a search of the music library. Will push results to [searchResults]. */ fun search(query: String) { val musicStore = MusicStore.maybeGetInstance() @@ -118,18 +119,17 @@ class SearchViewModel : ViewModel() { } /** - * Update the current filter mode with a menu [id]. - * New value will be pushed to [filterMode]. + * Update the current filter mode with a menu [id]. New value will be pushed to [filterMode]. */ fun updateFilterModeWithId(@IdRes id: Int) { - mFilterMode = when (id) { - R.id.option_filter_songs -> DisplayMode.SHOW_SONGS - R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS - R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS - R.id.option_filter_genres -> DisplayMode.SHOW_GENRES - - else -> null - } + mFilterMode = + when (id) { + R.id.option_filter_songs -> DisplayMode.SHOW_SONGS + R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS + R.id.option_filter_artists -> DisplayMode.SHOW_ARTISTS + R.id.option_filter_genres -> DisplayMode.SHOW_GENRES + else -> null + } logD("Updating filter mode to $mFilterMode") @@ -139,17 +139,18 @@ class SearchViewModel : ViewModel() { } /** - * Shortcut that will run a ignoreCase filter on a list and only return - * a value if the resulting list is empty. + * Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting + * list is empty. */ private fun List.filterByOrNull(value: String): List? { val filtered = filter { // Ensure the name we match with is correct. - val name = if (it is MusicParent) { - it.resolvedName - } else { - it.name - } + val name = + if (it is MusicParent) { + it.resolvedName + } else { + it.name + } // First see if the normal item name will work. If that fails, try the "normalized" // [e.g all accented/unicode chars become latin chars] instead. Hopefully this @@ -182,8 +183,8 @@ class SearchViewModel : ViewModel() { when (Character.getType(cp)) { // Character.NON_SPACING_MARK and Character.COMBINING_SPACING_MARK were added // by normalizer - 6, 8 -> continue - + 6, + 8 -> continue else -> sb.appendCodePoint(cp) } } @@ -191,9 +192,7 @@ class SearchViewModel : ViewModel() { return sb.toString() } - /** - * Update the current navigation status to [isNavigating] - */ + /** Update the current navigation status to [isNavigating] */ fun setNavigating(isNavigating: Boolean) { mIsNavigating = isNavigating } 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 f24110e70..cc95de2ee 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * AboutFragment.kt is part of Auxio. * * 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 @@ -15,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.settings import android.content.ActivityNotFoundException @@ -59,9 +58,7 @@ class AboutFragment : Fragment() { insets } - binding.aboutToolbar.setNavigationOnClickListener { - findNavController().navigateUp() - } + binding.aboutToolbar.setNavigationOnClickListener { findNavController().navigateUp() } binding.aboutVersion.text = BuildConfig.VERSION_NAME binding.aboutCode.setOnClickListener { openLinkInBrowser(LINK_CODEBASE) } @@ -69,9 +66,7 @@ class AboutFragment : Fragment() { binding.aboutLicenses.setOnClickListener { openLinkInBrowser(LINK_LICENSES) } homeModel.songs.observe(viewLifecycleOwner) { songs -> - binding.aboutSongCount.text = getString( - R.string.fmt_songs_loaded, songs.size - ) + binding.aboutSongCount.text = getString(R.string.fmt_songs_loaded, songs.size) } logD("Dialog created") @@ -79,15 +74,12 @@ class AboutFragment : Fragment() { return binding.root } - /** - * Go through the process of opening a [link] in a browser. - */ + /** Go through the process of opening a [link] in a browser. */ private fun openLinkInBrowser(link: String) { logD("Opening $link") - val browserIntent = Intent(Intent.ACTION_VIEW, link.toUri()).setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - ) + val browserIntent = + Intent(Intent.ACTION_VIEW, link.toUri()).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Android 11 seems to now handle the app chooser situations on its own now @@ -104,9 +96,12 @@ class AboutFragment : Fragment() { // not work in all cases, especially when no default app was set. If that is the // case, we will try to manually handle these cases before we try to launch the // browser. - val pkgName = requireContext().packageManager.resolveActivity( - browserIntent, PackageManager.MATCH_DEFAULT_ONLY - )?.activityInfo?.packageName + val pkgName = + requireContext() + .packageManager + .resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) + ?.activityInfo + ?.packageName if (pkgName != null) { if (pkgName == "android") { @@ -130,9 +125,10 @@ class AboutFragment : Fragment() { } private fun openAppChooser(intent: Intent) { - val chooserIntent = Intent(Intent.ACTION_CHOOSER) - .putExtra(Intent.EXTRA_INTENT, intent) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + val chooserIntent = + Intent(Intent.ACTION_CHOOSER) + .putExtra(Intent.EXTRA_INTENT, intent) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(chooserIntent) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt index 8ee87c17e..7e13e07fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SettingsCompat.kt is part of Auxio. * * 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 @@ -15,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.settings import android.content.SharedPreferences @@ -53,9 +52,7 @@ fun handleAccentCompat(prefs: SharedPreferences): Accent { return Accent(prefs.getInt(SettingsManager.KEY_ACCENT, 5)) } -/** - * Cache of the old keys used in Auxio. - */ +/** Cache of the old keys used in Auxio. */ private object OldKeys { const val KEY_ACCENT2 = "KEY_ACCENT2" } 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 4b0ef7dfc..f6dd15587 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SettingsFragment.kt is part of Auxio. * * 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 @@ -15,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.settings import android.os.Bundle @@ -39,9 +38,7 @@ class SettingsFragment : Fragment() { val binding = FragmentSettingsBinding.inflate(inflater) binding.settingsToolbar.apply { - setNavigationOnClickListener { - findNavController().navigateUp() - } + setNavigationOnClickListener { findNavController().navigateUp() } } binding.settingsAppbar.liftOnScrollTargetViewId = androidx.preference.R.id.recycler_view diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 3b8e2a278..4ec9768a5 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SettingsListFragment.kt is part of Auxio. * * 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 @@ -15,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.settings import android.os.Bundle @@ -32,8 +31,8 @@ import androidx.recyclerview.widget.RecyclerView import coil.Coil import org.oxycblt.auxio.R import org.oxycblt.auxio.accent.AccentCustomizeDialog -import org.oxycblt.auxio.music.excluded.ExcludedDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog +import org.oxycblt.auxio.music.excluded.ExcludedDialog import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.pref.IntListPrefDialog import org.oxycblt.auxio.settings.pref.IntListPreference @@ -83,9 +82,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { } } - /** - * Recursively handle a preference, doing any specific actions on it. - */ + /** Recursively handle a preference, doing any specific actions on it. */ private fun recursivelyHandlePreference(preference: Preference) { if (!preference.isVisible) return @@ -100,82 +97,80 @@ class SettingsListFragment : PreferenceFragmentCompat() { SettingsManager.KEY_THEME -> { setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon()) - onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value -> - AppCompatDelegate.setDefaultNightMode(value as Int) - setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon()) - true - } - } - - SettingsManager.KEY_BLACK_THEME -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - if (requireContext().isNight) { - requireActivity().recreate() + onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, value -> + AppCompatDelegate.setDefaultNightMode(value as Int) + setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon()) + true } - - true - } } + SettingsManager.KEY_BLACK_THEME -> { + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + if (requireContext().isNight) { + requireActivity().recreate() + } + true + } + } SettingsManager.KEY_ACCENT -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - AccentCustomizeDialog().show(childFragmentManager, AccentCustomizeDialog.TAG) - true - } + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + AccentCustomizeDialog() + .show(childFragmentManager, AccentCustomizeDialog.TAG) + true + } summary = context.getString(settingsManager.accent.name) } - SettingsManager.KEY_LIB_TABS -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG) - true - } + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG) + true + } } - SettingsManager.KEY_SHOW_COVERS, SettingsManager.KEY_QUALITY_COVERS -> { - onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> - Coil.imageLoader(requireContext()).apply { - this.memoryCache?.clear() + onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, _ -> + Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() } + + true } - - true - } } - SettingsManager.KEY_SAVE_STATE -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - playbackModel.savePlaybackState(requireContext()) { - requireContext().showToast(R.string.lbl_state_saved) + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + playbackModel.savePlaybackState(requireContext()) { + requireContext().showToast(R.string.lbl_state_saved) + } + + true } - - true - } } - SettingsManager.KEY_RELOAD -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - playbackModel.savePlaybackState(requireContext()) { - requireContext().hardRestart() + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + playbackModel.savePlaybackState(requireContext()) { + requireContext().hardRestart() + } + + true } - - true - } } - SettingsManager.KEY_EXCLUDED -> { - onPreferenceClickListener = Preference.OnPreferenceClickListener { - ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG) - true - } + onPreferenceClickListener = + Preference.OnPreferenceClickListener { + ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG) + true + } } } } } - /** - * Convert an theme integer into an icon that can be used. - */ + /** Convert an theme integer into an icon that can be used. */ @DrawableRes private fun Int.toThemeIcon(): Int { return when (this) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index 47678c1b6..70267a1df 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SettingsManager.kt is part of Auxio. * * 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 @@ -15,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.settings import android.content.Context @@ -64,16 +63,17 @@ class SettingsManager private constructor(context: Context) : } /** - * Whether to display the LoopMode or the shuffle status on the notification. - * False if loop, true if shuffle. + * Whether to display the LoopMode or the shuffle status on the notification. False if loop, + * true if shuffle. */ val useAltNotifAction: Boolean get() = prefs.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false) /** The current library tabs preferred by the user. */ var libTabs: Array - get() = Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) - ?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!! + get() = + Tab.fromSequence(prefs.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) + ?: Tab.fromSequence(Tab.SEQUENCE_DEFAULT)!! set(value) { prefs.edit { putInt(KEY_LIB_TABS, Tab.toSequence(value)) @@ -103,13 +103,15 @@ class SettingsManager private constructor(context: Context) : /** The current ReplayGain configuration */ val replayGainMode: ReplayGainMode - get() = ReplayGainMode.fromInt(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE)) - ?: ReplayGainMode.OFF + get() = + ReplayGainMode.fromInt(prefs.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE)) + ?: ReplayGainMode.OFF /** What queue to create when a song is selected (ex. From All Songs or Search) */ val songPlaybackMode: PlaybackMode - get() = PlaybackMode.fromInt(prefs.getInt(KEY_SONG_PLAYBACK_MODE, Int.MIN_VALUE)) - ?: PlaybackMode.ALL_SONGS + get() = + PlaybackMode.fromInt(prefs.getInt(KEY_SONG_PLAYBACK_MODE, Int.MIN_VALUE)) + ?: PlaybackMode.ALL_SONGS /** Whether shuffle should stay on when a new song is selected. */ val keepShuffle: Boolean @@ -119,7 +121,9 @@ class SettingsManager private constructor(context: Context) : val rewindWithPrev: Boolean get() = prefs.getBoolean(KEY_PREV_REWIND, true) - /** Whether [org.oxycblt.auxio.playback.state.LoopMode.TRACK] should pause when the track repeats */ + /** + * Whether [org.oxycblt.auxio.playback.state.LoopMode.TRACK] should pause when the track repeats + */ val pauseOnLoop: Boolean get() = prefs.getBoolean(KEY_LOOP_PAUSE, false) @@ -133,10 +137,9 @@ class SettingsManager private constructor(context: Context) : } } - /** The song sort mode on HomeFragment **/ + /** The song sort mode on HomeFragment */ var libSongSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = Sort.fromInt(prefs.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_LIB_SONGS_SORT, value.toInt()) @@ -144,10 +147,9 @@ class SettingsManager private constructor(context: Context) : } } - /** The album sort mode on HomeFragment **/ + /** The album sort mode on HomeFragment */ var libAlbumSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = Sort.fromInt(prefs.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_LIB_ALBUMS_SORT, value.toInt()) @@ -155,10 +157,9 @@ class SettingsManager private constructor(context: Context) : } } - /** The artist sort mode on HomeFragment **/ + /** The artist sort mode on HomeFragment */ var libArtistSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = Sort.fromInt(prefs.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_LIB_ARTISTS_SORT, value.toInt()) @@ -166,10 +167,9 @@ class SettingsManager private constructor(context: Context) : } } - /** The genre sort mode on HomeFragment **/ + /** The genre sort mode on HomeFragment */ var libGenreSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = Sort.fromInt(prefs.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_LIB_GENRES_SORT, value.toInt()) @@ -177,10 +177,10 @@ class SettingsManager private constructor(context: Context) : } } - /** The detail album sort mode **/ + /** The detail album sort mode */ var detailAlbumSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = + Sort.fromInt(prefs.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_DETAIL_ALBUM_SORT, value.toInt()) @@ -188,10 +188,10 @@ class SettingsManager private constructor(context: Context) : } } - /** The detail artist sort mode **/ + /** The detail artist sort mode */ var detailArtistSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) - ?: Sort.ByYear(false) + get() = + Sort.fromInt(prefs.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) ?: Sort.ByYear(false) set(value) { prefs.edit { putInt(KEY_DETAIL_ARTIST_SORT, value.toInt()) @@ -199,10 +199,10 @@ class SettingsManager private constructor(context: Context) : } } - /** The detail genre sort mode **/ + /** The detail genre sort mode */ var detailGenreSort: Sort - get() = Sort.fromInt(prefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) + get() = + Sort.fromInt(prefs.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) set(value) { prefs.edit { putInt(KEY_DETAIL_GENRE_SORT, value.toInt()) @@ -226,29 +226,13 @@ class SettingsManager private constructor(context: Context) : override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { - KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { - it.onNotifActionUpdate(useAltNotifAction) - } - - KEY_SHOW_COVERS -> callbacks.forEach { - it.onShowCoverUpdate(showCovers) - } - - KEY_QUALITY_COVERS -> callbacks.forEach { - it.onQualityCoverUpdate(useQualityCovers) - } - - KEY_LIB_TABS -> callbacks.forEach { - it.onLibTabsUpdate(libTabs) - } - - KEY_AUDIO_FOCUS -> callbacks.forEach { - it.onAudioFocusUpdate(doAudioFocus) - } - - KEY_REPLAY_GAIN -> callbacks.forEach { - it.onReplayGainUpdate(replayGainMode) - } + KEY_USE_ALT_NOTIFICATION_ACTION -> + callbacks.forEach { it.onNotifActionUpdate(useAltNotifAction) } + KEY_SHOW_COVERS -> callbacks.forEach { it.onShowCoverUpdate(showCovers) } + KEY_QUALITY_COVERS -> callbacks.forEach { it.onQualityCoverUpdate(useQualityCovers) } + KEY_LIB_TABS -> callbacks.forEach { it.onLibTabsUpdate(libTabs) } + KEY_AUDIO_FOCUS -> callbacks.forEach { it.onAudioFocusUpdate(doAudioFocus) } + KEY_REPLAY_GAIN -> callbacks.forEach { it.onReplayGainUpdate(replayGainMode) } } } @@ -304,26 +288,21 @@ class SettingsManager private constructor(context: Context) : const val KEY_DETAIL_ARTIST_SORT = "auxio_artist_sort" const val KEY_DETAIL_GENRE_SORT = "auxio_genre_sort" - @Volatile - private var INSTANCE: SettingsManager? = null + @Volatile private var INSTANCE: SettingsManager? = null /** - * Init the single instance of [SettingsManager]. Done so that every object - * can have access to it regardless of if it has a context. + * Init the single instance of [SettingsManager]. Done so that every object can have access + * to it regardless of if it has a context. */ fun init(context: Context): SettingsManager { if (INSTANCE == null) { - synchronized(this) { - INSTANCE = SettingsManager(context) - } + synchronized(this) { INSTANCE = SettingsManager(context) } } return getInstance() } - /** - * Get the single instance of [SettingsManager]. - */ + /** Get the single instance of [SettingsManager]. */ fun getInstance(): SettingsManager { val instance = INSTANCE diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt index 93bbd141e..abfa10492 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * IntListPrefDialog.kt is part of Auxio. * * 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 @@ -15,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.settings.pref import android.os.Bundle @@ -24,17 +23,15 @@ import androidx.preference.PreferenceFragmentCompat import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.ui.LifecycleDialog -/** - * The dialog shown whenever an [IntListPreference] is shown. - */ +/** The dialog shown whenever an [IntListPreference] is shown. */ class IntListPrefDialog : LifecycleDialog() { override fun onConfigDialog(builder: AlertDialog.Builder) { // Since we have to store the preference key as an argument, we have to find the // preference we need to use manually. - val pref = requireNotNull( - (parentFragment as PreferenceFragmentCompat).preferenceManager - .findPreference(requireArguments().getString(ARG_KEY, null)) - ) + val pref = + requireNotNull( + (parentFragment as PreferenceFragmentCompat).preferenceManager.findPreference< + IntListPreference>(requireArguments().getString(ARG_KEY, null))) builder.setTitle(pref.title) @@ -52,9 +49,7 @@ class IntListPrefDialog : LifecycleDialog() { fun from(pref: IntListPreference): IntListPrefDialog { return IntListPrefDialog().apply { - arguments = Bundle().apply { - putString(ARG_KEY, pref.key) - } + arguments = Bundle().apply { putString(ARG_KEY, pref.key) } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt index 78bdc08b4..66d820968 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * IntListPreference.kt is part of Auxio. * * 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 @@ -15,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.settings.pref import android.content.Context @@ -25,32 +24,34 @@ import androidx.preference.DialogPreference import androidx.preference.Preference import org.oxycblt.auxio.R -class IntListPreference @JvmOverloads constructor( +class IntListPreference +@JvmOverloads +constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle, defStyleRes: Int = 0 ) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) { // Reflect into Preference to get the (normally inaccessible) default value. - private val defValueField = Preference::class.java.getDeclaredField("mDefaultValue").apply { - isAccessible = true - } + private val defValueField = + Preference::class.java.getDeclaredField("mDefaultValue").apply { isAccessible = true } val entries: Array val values: IntArray private var currentValue: Int? = null - private val defValue: Int get() = defValueField.get(this) as Int + private val defValue: Int + get() = defValueField.get(this) as Int init { - val prefAttrs = context.obtainStyledAttributes( - attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes - ) + val prefAttrs = + context.obtainStyledAttributes( + attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes) entries = prefAttrs.getTextArray(R.styleable.IntListPreference_entries) - values = context.resources.getIntArray( - prefAttrs.getResourceId(R.styleable.IntListPreference_entryValues, -1) - ) + values = + context.resources.getIntArray( + prefAttrs.getResourceId(R.styleable.IntListPreference_entryValues, -1)) prefAttrs.recycle() @@ -80,9 +81,7 @@ class IntListPreference @JvmOverloads constructor( return -1 } - /** - * Set a value using the index of it in [values] - */ + /** Set a value using the index of it in [values] */ fun setValueIndex(index: Int) { setValue(values[index]) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt index db4204fee..d8170d753 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.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.settings.pref import android.content.Context @@ -11,10 +28,12 @@ import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getDrawableSafe /** - * A [SwitchPreferenceCompat] that emulates the M3 switches until the design team - * actually bothers to add them to MDC. + * A [SwitchPreferenceCompat] that emulates the M3 switches until the design team actually bothers + * to add them to MDC. */ -class M3SwitchPreference @JvmOverloads constructor( +class M3SwitchPreference +@JvmOverloads +constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = R.attr.switchPreferenceCompatStyle, diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt index 7f8892f43..966f2cfd5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * ActionMenu.kt is part of Auxio. * * 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 @@ -15,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.ui import android.view.View @@ -52,12 +51,11 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE) * @param activity [AppCompatActivity] required as both a context and ViewModelStore owner. * @param anchor [View] This should be centered around * @param data [Item] this menu corresponds to - * @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details. + * @param flag Any extra flags to accompany the data. See [FLAG_NONE], [FLAG_IN_ALBUM], + * [FLAG_IN_ARTIST], [FLAG_IN_GENRE] for more details. * @throws IllegalStateException When there is no menu for this specific datatype/flag - * @author OxygenCobalt - * TODO: Stop scrolling when a menu is open - * TODO: Prevent duplicate menus from showing up - * TODO: Maybe replace this with a bottom sheet? + * @author OxygenCobalt TODO: Stop scrolling when a menu is open TODO: Prevent duplicate menus from + * showing up TODO: Maybe replace this with a bottom sheet? */ class ActionMenu( activity: AppCompatActivity, @@ -98,9 +96,7 @@ class ActionMenu( } } - /** - * Figure out what menu to use here, based on the data & flags - */ + /** Figure out what menu to use here, based on the data & flags */ @MenuRes private fun determineMenu(): Int { return when (data) { @@ -109,31 +105,23 @@ class ActionMenu( FLAG_NONE, FLAG_IN_GENRE -> R.menu.menu_song_actions FLAG_IN_ALBUM -> R.menu.menu_album_song_actions FLAG_IN_ARTIST -> R.menu.menu_artist_song_actions - else -> -1 } } - is Album -> { when (flag) { FLAG_NONE -> R.menu.menu_album_actions FLAG_IN_ARTIST -> R.menu.menu_artist_album_actions - else -> -1 } } - is Artist -> R.menu.menu_artist_actions - is Genre -> R.menu.menu_genre_actions - else -> -1 } } - /** - * Determine what to do when a MenuItem is clicked. - */ + /** Determine what to do when a MenuItem is clicked. */ private fun onMenuClick(@IdRes id: Int) { when (id) { R.id.action_play -> { @@ -141,59 +129,48 @@ class ActionMenu( is Album -> playbackModel.playAlbum(data, false) is Artist -> playbackModel.playArtist(data, false) is Genre -> playbackModel.playGenre(data, false) - else -> {} } } - R.id.action_shuffle -> { when (data) { is Album -> playbackModel.playAlbum(data, true) is Artist -> playbackModel.playArtist(data, true) is Genre -> playbackModel.playGenre(data, true) - else -> {} } } - R.id.action_play_next -> { when (data) { is Song -> { playbackModel.playNext(data) context.showToast(R.string.lbl_queue_added) } - is Album -> { playbackModel.playNext(data) context.showToast(R.string.lbl_queue_added) } - else -> {} } } - R.id.action_queue_add -> { when (data) { is Song -> { playbackModel.addToQueue(data) context.showToast(R.string.lbl_queue_added) } - is Album -> { playbackModel.addToQueue(data) context.showToast(R.string.lbl_queue_added) } - else -> {} } } - R.id.action_go_album -> { if (data is Song) { detailModel.navToItem(data.album) } } - R.id.action_go_artist -> { if (data is Song) { detailModel.navToItem(data.album.artist) @@ -205,13 +182,22 @@ class ActionMenu( } companion object { - /** No Flags **/ + /** No Flags */ const val FLAG_NONE = -1 - /** Flag for when a menu is opened from an artist (See [org.oxycblt.auxio.detail.ArtistDetailFragment]) **/ + /** + * Flag for when a menu is opened from an artist (See + * [org.oxycblt.auxio.detail.ArtistDetailFragment]) + */ const val FLAG_IN_ARTIST = 0 - /** Flag for when a menu is opened from an album (See [org.oxycblt.auxio.detail.AlbumDetailFragment]) **/ + /** + * Flag for when a menu is opened from an album (See + * [org.oxycblt.auxio.detail.AlbumDetailFragment]) + */ const val FLAG_IN_ALBUM = 1 - /** Flag for when a menu is opened from a genre (See [org.oxycblt.auxio.detail.GenreDetailFragment]) **/ + /** + * Flag for when a menu is opened from a genre (See + * [org.oxycblt.auxio.detail.GenreDetailFragment]) + */ const val FLAG_IN_GENRE = 2 } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt b/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt index 7aa7ccfda..dccde380f 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * DiffCallback.kt is part of Auxio. * * 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 @@ -15,15 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.ui import androidx.recyclerview.widget.DiffUtil import org.oxycblt.auxio.music.Item /** - * A re-usable diff callback for all [Item] implementations. - * **Use this instead of creating a DiffCallback for each adapter.** + * A re-usable diff callback for all [Item] implementations. **Use this instead of creating a + * DiffCallback for each adapter.** * @author OxygenCobalt */ class DiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt b/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt index 92ed829c3..58fb9b8df 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/DisplayMode.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * DisplayMode.kt is part of Auxio. * * 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 @@ -15,15 +14,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.ui import org.oxycblt.auxio.R /** - * An enum for determining what items to show in a given list. - * Note: **DO NOT RE-ARRANGE THE ENUM**. The ordinals are used to store library tabs, so doing - * changing them would also change the meaning of tab instances. + * An enum for determining what items to show in a given list. Note: **DO NOT RE-ARRANGE THE ENUM**. + * The ordinals are used to store library tabs, so doing changing them would also change the meaning + * of tab instances. * @author OxygenCobalt */ enum class DisplayMode { @@ -32,19 +31,23 @@ enum class DisplayMode { SHOW_ARTISTS, SHOW_GENRES; - val string: Int get() = when (this) { - SHOW_SONGS -> R.string.lbl_songs - SHOW_ALBUMS -> R.string.lbl_albums - SHOW_ARTISTS -> R.string.lbl_artists - SHOW_GENRES -> R.string.lbl_genres - } + val string: Int + get() = + when (this) { + SHOW_SONGS -> R.string.lbl_songs + SHOW_ALBUMS -> R.string.lbl_albums + SHOW_ARTISTS -> R.string.lbl_artists + SHOW_GENRES -> R.string.lbl_genres + } - val icon: Int get() = when (this) { - SHOW_SONGS -> R.drawable.ic_song - SHOW_ALBUMS -> R.drawable.ic_album - SHOW_ARTISTS -> R.drawable.ic_artist - SHOW_GENRES -> R.drawable.ic_genre - } + val icon: Int + get() = + when (this) { + SHOW_SONGS -> R.drawable.ic_song + SHOW_ALBUMS -> R.drawable.ic_album + SHOW_ARTISTS -> R.drawable.ic_artist + SHOW_GENRES -> R.drawable.ic_genre + } companion object { private const val INT_NULL = 0xA107 @@ -54,8 +57,8 @@ enum class DisplayMode { private const val INT_SHOW_SONGS = 0xA10B /** - * Convert this enum into an integer for filtering. - * In this context, a null value means to filter nothing. + * Convert this enum into an integer for filtering. In this context, a null value means to + * filter nothing. * @return An integer constant for that display mode, or a constant for a null [DisplayMode] */ fun toFilterInt(value: DisplayMode?): Int { @@ -69,8 +72,8 @@ enum class DisplayMode { } /** - * Convert a filtering integer to a [DisplayMode]. - * In this context, a null value means to filter nothing. + * Convert a filtering integer to a [DisplayMode]. In this context, a null value means to + * filter nothing. * @return A [DisplayMode] for this constant (including null) */ fun fromFilterInt(value: Int): DisplayMode? { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/EdgeAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/EdgeAppBarLayout.kt index f30302ca0..30decb333 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/EdgeAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/EdgeAppBarLayout.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * LiftAppBarLayout.kt is part of Auxio. * * 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 @@ -15,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.ui import android.content.Context @@ -33,32 +32,31 @@ import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.systemBarInsetsCompat /** - * An [AppBarLayout] that fixes a bug with the default implementation where the lifted state - * will not properly respond to RecyclerView events. - * **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. + * An [AppBarLayout] that fixes a bug with the default implementation where the lifted state will + * not properly respond to RecyclerView events. **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. */ -open class EdgeAppBarLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : AppBarLayout(context, attrs, defStyleAttr) { +open class EdgeAppBarLayout +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + AppBarLayout(context, attrs, defStyleAttr) { private var scrollingChild: View? = null private val tConsumed = IntArray(2) - private val onPreDraw = ViewTreeObserver.OnPreDrawListener { - val child = findScrollingChild() + private val onPreDraw = + ViewTreeObserver.OnPreDrawListener { + val child = findScrollingChild() - if (child != null) { - val coordinator = parent as CoordinatorLayout - (layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll( - coordinator, this, coordinator, 0, 0, tConsumed, 0 - ) + if (child != null) { + val coordinator = parent as CoordinatorLayout + (layoutParams as CoordinatorLayout.LayoutParams).behavior?.onNestedPreScroll( + coordinator, this, coordinator, 0, 0, tConsumed, 0) + } + + true } - true - } - init { viewTreeObserver.addOnPreDrawListener(onPreDraw) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/EdgeCoordinatorLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/EdgeCoordinatorLayout.kt index 84341f7be..87b8996ff 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/EdgeCoordinatorLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/EdgeCoordinatorLayout.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * FuckedCoordinatorLayout.kt is part of Auxio. * * 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 @@ -15,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.ui import android.content.Context @@ -26,16 +25,15 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.children /** - * Class that fixes an issue where [CoordinatorLayout] will override [onApplyWindowInsets] - * and delegate the job to ***LAYOUT BEHAVIOR INSTANCES*** instead of the actual views. + * Class that fixes an issue where [CoordinatorLayout] will override [onApplyWindowInsets] and + * delegate the job to ***LAYOUT BEHAVIOR INSTANCES*** instead of the actual views. * * I can't believe I have to do this. */ -class EdgeCoordinatorLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : CoordinatorLayout(context, attrs, defStyleAttr) { +class EdgeCoordinatorLayout +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + CoordinatorLayout(context, attrs, defStyleAttr) { override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { for (child in children) { child.onApplyWindowInsets(insets) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt b/app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt index 3a273a55b..fbda7331e 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/EdgeRecyclerView.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * EdgeRecyclerView.kt is part of Auxio. * * 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 @@ -15,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.ui import android.content.Context @@ -26,14 +25,11 @@ import androidx.core.view.updatePadding import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.util.systemBarInsetsCompat -/** - * A [RecyclerView] that automatically applies insets to itself. - */ -class EdgeRecyclerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - @AttrRes defStyleAttr: Int = 0 -) : RecyclerView(context, attrs, defStyleAttr) { +/** A [RecyclerView] that automatically applies insets to itself. */ +class EdgeRecyclerView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + RecyclerView(context, attrs, defStyleAttr) { override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { updatePadding(bottom = insets.systemBarInsetsCompat.bottom) return insets diff --git a/app/src/main/java/org/oxycblt/auxio/ui/LifecycleDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/LifecycleDialog.kt index 519bfbd46..c05c5e491 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/LifecycleDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/LifecycleDialog.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * LifecycleDialog.kt is part of Auxio. * * 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 @@ -15,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.ui import android.app.Dialog @@ -27,8 +26,8 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder /** - * A wrapper around [DialogFragment] that allows the usage of the standard Auxio lifecycle - * override [onCreateView] and [onDestroyView], but with a proper dialog being created. + * A wrapper around [DialogFragment] that allows the usage of the standard Auxio lifecycle override + * [onCreateView] and [onDestroyView], but with a proper dialog being created. */ abstract class LifecycleDialog : AppCompatDialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index d92075c10..d82026d36 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Sort.kt is part of Auxio. * * 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 @@ -15,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.ui import androidx.annotation.IdRes @@ -53,15 +52,15 @@ sealed class Sort(open val isAscending: Boolean) { /** Sort by the year of an item, only supported by [Album] and [Song] */ class ByYear(override val isAscending: Boolean) : Sort(isAscending) - /** - * Get the corresponding item id for this sort. - */ - val itemId: Int get() = when (this) { - is ByName -> R.id.option_sort_name - is ByArtist -> R.id.option_sort_artist - is ByAlbum -> R.id.option_sort_album - is ByYear -> R.id.option_sort_year - } + /** Get the corresponding item id for this sort. */ + val itemId: Int + get() = + when (this) { + is ByName -> R.id.option_sort_name + is ByArtist -> R.id.option_sort_artist + is ByAlbum -> R.id.option_sort_album + is ByYear -> R.id.option_sort_year + } /** * Apply [ascending] to the status of this sort. @@ -100,10 +99,10 @@ sealed class Sort(open val isAscending: Boolean) { fun sortSongs(songs: Collection): List { return when (this) { is ByName -> songs.stringSort { it.name } - - else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album -> - album.songs.intSort(true) { it.track ?: 0 } - } + else -> + sortAlbums(songs.groupBy { it.album }.keys).flatMap { album -> + album.songs.intSort(true) { it.track ?: 0 } + } } } @@ -117,10 +116,10 @@ sealed class Sort(open val isAscending: Boolean) { fun sortAlbums(albums: Collection): List { return when (this) { is ByName, is ByAlbum -> albums.stringSort { it.resolvedName } - - is ByArtist -> sortParents(albums.groupBy { it.artist }.keys) - .flatMap { ByYear(false).sortAlbums(it.albums) } - + is ByArtist -> + sortParents(albums.groupBy { it.artist }.keys).flatMap { + ByYear(false).sortAlbums(it.albums) + } is ByYear -> albums.intSort { it.year ?: 0 } } } @@ -158,9 +157,7 @@ sealed class Sort(open val isAscending: Boolean) { return sortSongs(genre.songs) } - /** - * Convert this sort to it's integer representation. - */ + /** Convert this sort to it's integer representation. */ fun toInt(): Int { return when (this) { is ByName -> INT_NAME @@ -175,15 +172,14 @@ sealed class Sort(open val isAscending: Boolean) { selector: (T) -> String ): List { // Chain whatever item call with sliceArticle for correctness - val chained: (T) -> String = { - selector(it).sliceArticle() - } + val chained: (T) -> String = { selector(it).sliceArticle() } - val comparator = if (asc) { - compareBy(String.CASE_INSENSITIVE_ORDER, chained) - } else { - compareByDescending(String.CASE_INSENSITIVE_ORDER, chained) - } + val comparator = + if (asc) { + compareBy(String.CASE_INSENSITIVE_ORDER, chained) + } else { + compareByDescending(String.CASE_INSENSITIVE_ORDER, chained) + } return sortedWith(comparator) } @@ -192,11 +188,12 @@ sealed class Sort(open val isAscending: Boolean) { asc: Boolean = isAscending, selector: (T) -> Int, ): List { - val comparator = if (asc) { - compareBy(selector) - } else { - compareByDescending(selector) - } + val comparator = + if (asc) { + compareBy(selector) + } else { + compareByDescending(selector) + } return sortedWith(comparator) } @@ -227,9 +224,9 @@ sealed class Sort(open val isAscending: Boolean) { } /** - * Slice a string so that any preceding articles like The/A(n) are truncated. - * This is hilariously anglo-centric, but its mostly for MediaStore compat and hopefully - * shouldn't run with other languages. + * Slice a string so that any preceding articles like The/A(n) are truncated. This is hilariously + * anglo-centric, but its mostly for MediaStore compat and hopefully shouldn't run with other + * languages. */ fun String.sliceArticle(): String { if (length > 5 && startsWith("the ", true)) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt index 975c23ca1..d36e9983d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewHolders.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * SortHeaderViewHolder.kt is part of Auxio. * * 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 @@ -15,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.ui import android.content.Context @@ -53,22 +52,18 @@ abstract class BaseViewHolder( ) : RecyclerView.ViewHolder(binding.root) { init { // Force the layout to *actually* be the screen width - binding.root.layoutParams = RecyclerView.LayoutParams( - RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT - ) + binding.root.layoutParams = + RecyclerView.LayoutParams( + RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) } /** - * Bind the viewholder with whatever [Item] instance that has been specified. - * Will call [onBind] on the inheriting ViewHolder. + * Bind the viewholder with whatever [Item] instance that has been specified. Will call [onBind] + * on the inheriting ViewHolder. * @param data Data that the viewholder should be bound with */ fun bind(data: T) { - doOnClick?.let { onClick -> - binding.root.setOnClickListener { - onClick(data) - } - } + doOnClick?.let { onClick -> binding.root.setOnClickListener { onClick(data) } } doOnLongClick?.let { onLongClick -> binding.root.setOnLongClickListener { view -> @@ -84,16 +79,15 @@ abstract class BaseViewHolder( } /** - * Function that performs binding operations unique to the inheriting viewholder. - * Add any specialized code to an override of this instead of [BaseViewHolder] itself. + * Function that performs binding operations unique to the inheriting viewholder. Add any + * specialized code to an override of this instead of [BaseViewHolder] itself. */ protected abstract fun onBind(data: T) } -/** - * The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. - */ -class SongViewHolder private constructor( +/** The Shared ViewHolder for a [Song]. Instantiation should be done with [from]. */ +class SongViewHolder +private constructor( private val binding: ItemSongBinding, doOnClick: (data: Song) -> Unit, doOnLongClick: (view: View, data: Song) -> Unit @@ -109,26 +103,21 @@ class SongViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA000 - /** - * Create an instance of [SongViewHolder] - */ + /** Create an instance of [SongViewHolder] */ fun from( context: Context, doOnClick: (data: Song) -> Unit, doOnLongClick: (view: View, data: Song) -> Unit ): SongViewHolder { return SongViewHolder( - ItemSongBinding.inflate(context.inflater), - doOnClick, doOnLongClick - ) + ItemSongBinding.inflate(context.inflater), doOnClick, doOnLongClick) } } } -/** - * The Shared ViewHolder for a [Album]. Instantiation should be done with [from]. - */ -class AlbumViewHolder private constructor( +/** The Shared ViewHolder for a [Album]. Instantiation should be done with [from]. */ +class AlbumViewHolder +private constructor( private val binding: ItemAlbumBinding, doOnClick: (data: Album) -> Unit, doOnLongClick: (view: View, data: Album) -> Unit @@ -142,26 +131,21 @@ class AlbumViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA001 - /** - * Create an instance of [AlbumViewHolder] - */ + /** Create an instance of [AlbumViewHolder] */ fun from( context: Context, doOnClick: (data: Album) -> Unit, doOnLongClick: (view: View, data: Album) -> Unit ): AlbumViewHolder { return AlbumViewHolder( - ItemAlbumBinding.inflate(context.inflater), - doOnClick, doOnLongClick - ) + ItemAlbumBinding.inflate(context.inflater), doOnClick, doOnLongClick) } } } -/** - * The Shared ViewHolder for a [Artist]. Instantiation should be done with [from]. - */ -class ArtistViewHolder private constructor( +/** The Shared ViewHolder for a [Artist]. Instantiation should be done with [from]. */ +class ArtistViewHolder +private constructor( private val binding: ItemArtistBinding, doOnClick: (Artist) -> Unit, doOnLongClick: (view: View, data: Artist) -> Unit @@ -175,26 +159,21 @@ class ArtistViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA002 - /** - * Create an instance of [ArtistViewHolder] - */ + /** Create an instance of [ArtistViewHolder] */ fun from( context: Context, doOnClick: (Artist) -> Unit, doOnLongClick: (view: View, data: Artist) -> Unit ): ArtistViewHolder { return ArtistViewHolder( - ItemArtistBinding.inflate(context.inflater), - doOnClick, doOnLongClick - ) + ItemArtistBinding.inflate(context.inflater), doOnClick, doOnLongClick) } } } -/** - * The Shared ViewHolder for a [Genre]. Instantiation should be done with [from]. - */ -class GenreViewHolder private constructor( +/** The Shared ViewHolder for a [Genre]. Instantiation should be done with [from]. */ +class GenreViewHolder +private constructor( private val binding: ItemGenreBinding, doOnClick: (Genre) -> Unit, doOnLongClick: (view: View, data: Genre) -> Unit @@ -208,28 +187,21 @@ class GenreViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA003 - /** - * Create an instance of [GenreViewHolder] - */ + /** Create an instance of [GenreViewHolder] */ fun from( context: Context, doOnClick: (Genre) -> Unit, doOnLongClick: (view: View, data: Genre) -> Unit ): GenreViewHolder { return GenreViewHolder( - ItemGenreBinding.inflate(context.inflater), - doOnClick, doOnLongClick - ) + ItemGenreBinding.inflate(context.inflater), doOnClick, doOnLongClick) } } } -/** - * The Shared ViewHolder for a [Header]. Instantiation should be done with [from] - */ -class HeaderViewHolder private constructor( - private val binding: ItemHeaderBinding -) : BaseViewHolder
(binding) { +/** The Shared ViewHolder for a [Header]. Instantiation should be done with [from] */ +class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : + BaseViewHolder
(binding) { override fun onBind(data: Header) { binding.header = data @@ -238,23 +210,16 @@ class HeaderViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA004 - /** - * Create an instance of [HeaderViewHolder] - */ + /** Create an instance of [HeaderViewHolder] */ fun from(context: Context): HeaderViewHolder { - return HeaderViewHolder( - ItemHeaderBinding.inflate(context.inflater) - ) + return HeaderViewHolder(ItemHeaderBinding.inflate(context.inflater)) } } } -/** - * The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] - */ -class ActionHeaderViewHolder private constructor( - private val binding: ItemActionHeaderBinding -) : BaseViewHolder(binding) { +/** The Shared ViewHolder for an [ActionHeader]. Instantiation should be done with [from] */ +class ActionHeaderViewHolder private constructor(private val binding: ItemActionHeaderBinding) : + BaseViewHolder(binding) { override fun onBind(data: ActionHeader) { binding.header = data @@ -271,9 +236,7 @@ class ActionHeaderViewHolder private constructor( companion object { const val ITEM_TYPE = 0xA005 - /** - * Create an instance of [ActionHeaderViewHolder] - */ + /** Create an instance of [ActionHeaderViewHolder] */ fun from(context: Context): ActionHeaderViewHolder { return ActionHeaderViewHolder(ItemActionHeaderBinding.inflate(context.inflater)) } 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 e075552a8..043d2fd63 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * ContextUtil.kt is part of Auxio. * * 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 @@ -15,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.app.PendingIntent @@ -39,30 +38,28 @@ import androidx.annotation.PluralsRes import androidx.annotation.Px import androidx.annotation.StringRes import androidx.core.content.ContextCompat -import org.oxycblt.auxio.MainActivity import kotlin.reflect.KClass import kotlin.system.exitProcess +import org.oxycblt.auxio.MainActivity const val INTENT_REQUEST_CODE = 0xA0A0 -/** - * Shortcut to get a [LayoutInflater] from a [Context] - */ -val Context.inflater: LayoutInflater get() = LayoutInflater.from(this) +/** Shortcut to get a [LayoutInflater] from a [Context] */ +val Context.inflater: LayoutInflater + get() = LayoutInflater.from(this) /** - * Returns whether the current UI is in night mode or not. This will work if the theme is - * automatic as well. + * Returns whether the current UI is in night mode or not. This will work if the theme is automatic + * as well. */ -val Context.isNight: Boolean get() = - resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES +val Context.isNight: Boolean + get() = + resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == + Configuration.UI_MODE_NIGHT_YES -/** - * Returns if this device is in landscape. - */ -val Context.isLandscape get() = - resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +/** Returns if this device is in landscape. */ +val Context.isLandscape + get() = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE /** * Convenience method for getting a plural. @@ -117,11 +114,12 @@ fun Context.getAttrColorSafe(@AttrRes attr: Int): Int { theme.resolveAttribute(attr, resolvedAttr, true) // Then convert it to a proper color - val color = if (resolvedAttr.resourceId != 0) { - resolvedAttr.resourceId - } else { - resolvedAttr.data - } + val color = + if (resolvedAttr.resourceId != 0) { + resolvedAttr.resourceId + } else { + resolvedAttr.data + } return getColorSafe(color) } @@ -183,9 +181,8 @@ fun Context.getDimenOffsetSafe(@DimenRes dimen: Int): Int { @Px fun Context.pxOfDp(@Dimension dp: Float): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics - ).toInt() + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics) + .toInt() } private fun Context.handleResourceFailure(e: Exception, what: String, default: T): T { @@ -207,46 +204,37 @@ fun Context.getSystemServiceSafe(serviceClass: KClass): T { } } -/** - * Create a toast using the provided string resource. - */ +/** Create a toast using the provided string resource. */ fun Context.showToast(@StringRes str: Int) { Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show() } -/** - * Create a [PendingIntent] that leads to Auxio's [MainActivity] - */ +/** Create a [PendingIntent] that leads to Auxio's [MainActivity] */ fun Context.newMainIntent(): PendingIntent { return PendingIntent.getActivity( - this, INTENT_REQUEST_CODE, Intent(this, MainActivity::class.java), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - PendingIntent.FLAG_IMMUTABLE - else 0 - ) + this, + INTENT_REQUEST_CODE, + Intent(this, MainActivity::class.java), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) } -/** - * Create a broadcast [PendingIntent] - */ +/** Create a broadcast [PendingIntent] */ fun Context.newBroadcastIntent(what: String): PendingIntent { return PendingIntent.getBroadcast( - this, INTENT_REQUEST_CODE, Intent(what), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - PendingIntent.FLAG_IMMUTABLE - else 0 - ) + this, + INTENT_REQUEST_CODE, + Intent(what), + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0) } -/** - * Hard-restarts the app. Useful for forcing the app to reload music. - */ +/** Hard-restarts the app. Useful for forcing the app to reload music. */ fun Context.hardRestart() { // Instead of having to do a ton of cleanup and horrible code changes // to restart this application non-destructively, I just restart the UI task [There is only // one, after all] and then kill the application using exitProcess. Works well enough. - val intent = Intent(applicationContext, MainActivity::class.java) - .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + val intent = + Intent(applicationContext, MainActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) startActivity(intent) exitProcess(0) } diff --git a/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt index f47c88d32..4361722f3 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * DbUtil.kt is part of Auxio. * * 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 @@ -15,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.database.Cursor @@ -23,15 +22,13 @@ import android.database.sqlite.SQLiteDatabase import android.os.Looper /** - * Shortcut for querying all items in a database and running [block] with the cursor returned. - * Will not run if the cursor is null. + * Shortcut for querying all items in a database and running [block] with the cursor returned. Will + * not run if the cursor is null. */ fun SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = query(tableName, null, null, null, null, null, null)?.use(block) -/** - * Assert that we are on a background thread. - */ +/** Assert that we are on a background thread. */ fun assertBackgroundThread() { check(Looper.myLooper() != Looper.getMainLooper()) { "This operation must be ran on a background thread" 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 500b65df9..2727cb9cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LogUtil.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * LogUtil.kt is part of Auxio. * * 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 @@ -15,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.util.Log @@ -25,14 +24,16 @@ import org.oxycblt.auxio.BuildConfig // Yes, I know timber exists but this does what I need. /** - * Shortcut method for logging a non-string [obj] to debug. Should only be used for debug preferably. + * Shortcut method for logging a non-string [obj] to debug. Should only be used for debug + * preferably. */ fun Any.logD(obj: Any) { logD(obj.toString()) } /** - * Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous objects + * Shortcut method for logging [msg] to the debug console. Handles debug builds and anonymous + * objects */ fun Any.logD(msg: String) { if (BuildConfig.DEBUG) { @@ -41,16 +42,12 @@ fun Any.logD(msg: String) { } } -/** - * Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects - */ +/** Shortcut method for logging [msg] as a warning to the console. Handles anonymous objects */ fun Any.logW(msg: String) { Log.w(getName(), msg) } -/** - * Shortcut method for logging [msg] as an error to the console. Handles anonymous objects - */ +/** Shortcut method for logging [msg] as an error to the console. Handles anonymous objects */ fun Any.logE(msg: String) { Log.e(getName(), msg) } @@ -77,18 +74,16 @@ private fun Any.getName(): String = "Auxio.${this::class.simpleName ?: "Anonymou * I know that this will not stop you, but consider what you are doing with your life, plagiarizers. * Do you want to live a fulfilling existence on this planet? Or do you want to spend your life * taking work others did and making it objectively worse so you could arbitrage a fraction of a - * penny on every AdMob impression you get? You could do so many great things if you simply had - * the courage to come up with an idea of your own. If you still want to go on, I guess the only - * thing I can say is this: JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件 + * penny on every AdMob impression you get? You could do so many great things if you simply had the + * courage to come up with an idea of your own. If you still want to go on, I guess the only thing I + * can say is this: JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件 */ private fun basedCopyleftNotice() { if (BuildConfig.APPLICATION_ID != "org.oxycblt.auxio" && - BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug" - ) { + BuildConfig.APPLICATION_ID != "org.oxycblt.auxio.debug") { Log.d( "Auxio Project", "Friendly reminder: Auxio is licensed under the " + - "GPLv3 and all modifications must be made open source!" - ) + "GPLv3 and all modifications must be made open source!") } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt index 7f1c9773e..ea7fae6c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ViewUtil.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * ViewUtil.kt is part of Auxio. * * 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 @@ -15,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.res.ColorStateList @@ -29,10 +28,9 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R -/** - * Converts this color to a single-color [ColorStateList]. - */ -val @receiver:ColorRes Int.stateList get() = ColorStateList.valueOf(this) +/** Converts this color to a single-color [ColorStateList]. */ +val @receiver:ColorRes Int.stateList + get() = ColorStateList.valueOf(this) /** * Apply the recommended spans for a [RecyclerView]. @@ -47,25 +45,24 @@ fun RecyclerView.applySpans(shouldBeFullWidth: ((Int) -> Boolean)? = null) { val mgr = GridLayoutManager(context, spans) if (shouldBeFullWidth != null) { - mgr.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return if (shouldBeFullWidth(position)) spans else 1 + mgr.spanSizeLookup = + object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (shouldBeFullWidth(position)) spans else 1 + } } - } } layoutManager = mgr } } -/** - * Returns whether a recyclerview can scroll. - */ +/** Returns whether a recyclerview can scroll. */ fun RecyclerView.canScroll(): Boolean = computeVerticalScrollRange() > height /** - * Disables drop shadows on a view programmatically in a version-compatible manner. - * This only works on Android 9 and above. Below that version, shadows will remain visible. + * Disables drop shadows on a view programmatically in a version-compatible manner. This only works + * on Android 9 and above. Below that version, shadows will remain visible. */ fun View.disableDropShadowCompat() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -77,49 +74,44 @@ fun View.disableDropShadowCompat() { } /** - * Resolve system bar insets in a version-aware manner. This can be used to apply padding to - * a view that properly follows all the frustrating changes that were made between 8-11. + * Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view + * that properly follows all the frustrating changes that were made between 8-11. */ -val WindowInsets.systemBarInsetsCompat: Rect get() { - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - getInsets(WindowInsets.Type.systemBars()).run { - Rect(left, top, right, bottom) +val WindowInsets.systemBarInsetsCompat: Rect + get() { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + getInsets(WindowInsets.Type.systemBars()).run { Rect(left, top, right, bottom) } + } + else -> { + @Suppress("DEPRECATION") + Rect( + systemWindowInsetLeft, + systemWindowInsetTop, + systemWindowInsetRight, + systemWindowInsetBottom) } } - - else -> { - @Suppress("DEPRECATION") - Rect( - systemWindowInsetLeft, - systemWindowInsetTop, - systemWindowInsetRight, - systemWindowInsetBottom - ) - } } -} /** * Replaces the system bar insets in a version-aware manner. This can be used to modify the insets * for child views in a way that follows all of the frustrating changes that were made between 8-11. */ -fun WindowInsets.replaceSystemBarInsetsCompat(left: Int, top: Int, right: Int, bottom: Int): WindowInsets { +fun WindowInsets.replaceSystemBarInsetsCompat( + left: Int, + top: Int, + right: Int, + bottom: Int +): WindowInsets { return when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { WindowInsets.Builder(this) - .setInsets( - WindowInsets.Type.systemBars(), - Insets.of(left, top, right, bottom) - ) + .setInsets(WindowInsets.Type.systemBars(), Insets.of(left, top, right, bottom)) .build() } - else -> { - @Suppress("DEPRECATION") - replaceSystemWindowInsets( - left, top, right, bottom - ) + @Suppress("DEPRECATION") replaceSystemWindowInsets(left, top, right, bottom) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt index feb146d7a..9bb15a4e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/Forms.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * Forms.kt is part of Auxio. * * 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 @@ -15,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.widgets import android.content.Context @@ -28,8 +27,8 @@ import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newMainIntent /** - * The default widget is displayed whenever there is no music playing. It just shows the - * message "No music playing". + * The default widget is displayed whenever there is no music playing. It just shows the message "No + * music playing". */ fun createDefaultWidget(context: Context): RemoteViews { return createViews(context, R.layout.widget_default) @@ -46,9 +45,9 @@ fun createTinyWidget(context: Context, state: WidgetState): RemoteViews { } /** - * The small widget is for 2x2 widgets and just shows the cover art and playback controls. - * This is generally because a Medium widget is too large for this widget size and a text-only - * widget is too small for this widget size. + * The small widget is for 2x2 widgets and just shows the cover art and playback controls. This is + * generally because a Medium widget is too large for this widget size and a text-only widget is too + * small for this widget size. */ fun createSmallWidget(context: Context, state: WidgetState): RemoteViews { return createViews(context, R.layout.widget_small) @@ -57,8 +56,8 @@ fun createSmallWidget(context: Context, state: WidgetState): RemoteViews { } /** - * The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three - * controls. This is the default widget configuration. + * The medium widget is for 2x3 widgets and shows the cover art, title/artist, and three controls. + * This is the default widget configuration. */ fun createMediumWidget(context: Context, state: WidgetState): RemoteViews { return createViews(context, R.layout.widget_medium) @@ -66,34 +65,24 @@ fun createMediumWidget(context: Context, state: WidgetState): RemoteViews { .applyBasicControls(context, state) } -/** - * The wide widget is for Nx2 widgets and is like the small widget but with more controls. - */ +/** The wide widget is for Nx2 widgets and is like the small widget but with more controls. */ fun createWideWidget(context: Context, state: WidgetState): RemoteViews { return createViews(context, R.layout.widget_wide) .applyCover(context, state) .applyFullControls(context, state) } -/** - * The large widget is for 3x4 widgets and shows all metadata and controls. - */ +/** The large widget is for 3x4 widgets and shows all metadata and controls. */ fun createLargeWidget(context: Context, state: WidgetState): RemoteViews { return createViews(context, R.layout.widget_large) .applyMeta(context, state) .applyFullControls(context, state) } -private fun createViews( - context: Context, - @LayoutRes layout: Int -): RemoteViews { +private fun createViews(context: Context, @LayoutRes layout: Int): RemoteViews { val views = RemoteViews(context.packageName, layout) - views.setOnClickPendingIntent( - android.R.id.background, - context.newMainIntent() - ) + views.setOnClickPendingIntent(android.R.id.background, context.newMainIntent()) return views } @@ -112,8 +101,7 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote setImageViewBitmap(R.id.widget_cover, state.albumArt) setContentDescription( R.id.widget_cover, - context.getString(R.string.desc_album_cover, state.song.resolvedAlbumName) - ) + context.getString(R.string.desc_album_cover, state.song.resolvedAlbumName)) } else { setImageViewResource(R.id.widget_cover, R.drawable.ic_widget_album) setContentDescription(R.id.widget_cover, context.getString(R.string.desc_no_cover)) @@ -124,11 +112,7 @@ private fun RemoteViews.applyCover(context: Context, state: WidgetState): Remote private fun RemoteViews.applyPlayControls(context: Context, state: WidgetState): RemoteViews { setOnClickPendingIntent( - R.id.widget_play_pause, - context.newBroadcastIntent( - PlaybackService.ACTION_PLAY_PAUSE - ) - ) + R.id.widget_play_pause, context.newBroadcastIntent(PlaybackService.ACTION_PLAY_PAUSE)) setImageViewResource( R.id.widget_play_pause, @@ -136,8 +120,7 @@ private fun RemoteViews.applyPlayControls(context: Context, state: WidgetState): R.drawable.ic_pause } else { R.drawable.ic_play - } - ) + }) return this } @@ -146,18 +129,10 @@ private fun RemoteViews.applyBasicControls(context: Context, state: WidgetState) applyPlayControls(context, state) setOnClickPendingIntent( - R.id.widget_skip_prev, - context.newBroadcastIntent( - PlaybackService.ACTION_SKIP_PREV - ) - ) + R.id.widget_skip_prev, context.newBroadcastIntent(PlaybackService.ACTION_SKIP_PREV)) setOnClickPendingIntent( - R.id.widget_skip_next, - context.newBroadcastIntent( - PlaybackService.ACTION_SKIP_NEXT - ) - ) + R.id.widget_skip_next, context.newBroadcastIntent(PlaybackService.ACTION_SKIP_NEXT)) return this } @@ -166,31 +141,25 @@ private fun RemoteViews.applyFullControls(context: Context, state: WidgetState): applyBasicControls(context, state) setOnClickPendingIntent( - R.id.widget_loop, - context.newBroadcastIntent( - PlaybackService.ACTION_LOOP - ) - ) + R.id.widget_loop, context.newBroadcastIntent(PlaybackService.ACTION_LOOP)) setOnClickPendingIntent( - R.id.widget_shuffle, - context.newBroadcastIntent( - PlaybackService.ACTION_SHUFFLE - ) - ) + R.id.widget_shuffle, context.newBroadcastIntent(PlaybackService.ACTION_SHUFFLE)) // Like notifications, use the remote variants of icons since we really don't want to hack // indicators. - val shuffleRes = when { - state.isShuffled -> R.drawable.ic_remote_shuffle_on - else -> R.drawable.ic_remote_shuffle_off - } + val shuffleRes = + when { + state.isShuffled -> R.drawable.ic_remote_shuffle_on + else -> R.drawable.ic_remote_shuffle_off + } - val loopRes = when (state.loopMode) { - LoopMode.NONE -> R.drawable.ic_remote_loop_off - LoopMode.ALL -> R.drawable.ic_loop_on - LoopMode.TRACK -> R.drawable.ic_loop_one - } + val loopRes = + when (state.loopMode) { + LoopMode.NONE -> R.drawable.ic_remote_loop_off + LoopMode.ALL -> R.drawable.ic_loop_on + LoopMode.TRACK -> R.drawable.ic_loop_one + } setImageViewResource(R.id.widget_shuffle, shuffleRes) setImageViewResource(R.id.widget_loop, loopRes) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt index 10bd5026c..7f48a1daf 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetController.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * WidgetController.kt is part of Auxio. * * 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 @@ -15,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.widgets import android.content.Context @@ -28,12 +27,11 @@ import org.oxycblt.auxio.util.logD /** * A wrapper around each [WidgetProvider] that plugs into the main Auxio process and updates the * widget state based off of that. This cannot be rolled into [WidgetProvider] directly, as it may - * result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound - * to without being released. + * result in memory leaks if [PlaybackStateManager]/[SettingsManager] gets created and bound to + * without being released. */ class WidgetController(private val context: Context) : - PlaybackStateManager.Callback, - SettingsManager.Callback { + PlaybackStateManager.Callback, SettingsManager.Callback { private val playbackManager = PlaybackStateManager.getInstance() private val settingsManager = SettingsManager.getInstance() private val widget = WidgetProvider() 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 96dbf54ed..486911fc0 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * WidgetProvider.kt is part of Auxio. * * 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 @@ -15,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.widgets import android.appwidget.AppWidgetHostView @@ -33,6 +32,7 @@ import androidx.core.graphics.drawable.toBitmap import coil.imageLoader import coil.request.ImageRequest import coil.transform.RoundedCornersTransformation +import kotlin.math.min import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.coil.SquareFrameTransform import org.oxycblt.auxio.music.Song @@ -41,7 +41,6 @@ import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -import kotlin.math.min /** * Auxio's one and only appwidget. This widget follows a more unorthodox approach, effectively @@ -67,38 +66,36 @@ class WidgetProvider : AppWidgetProvider() { } loadWidgetBitmap(context, song) { bitmap -> - val state = WidgetState( - song, - bitmap, - playbackManager.isPlaying, - playbackManager.isShuffling, - playbackManager.loopMode - ) + val state = + WidgetState( + song, + bitmap, + playbackManager.isPlaying, + playbackManager.isShuffling, + playbackManager.loopMode) // Map each widget form to the cells where it would look at least okay. - val views = mapOf( - SizeF(180f, 100f) to createTinyWidget(context, state), - SizeF(180f, 152f) to createSmallWidget(context, state), - SizeF(272f, 152f) to createWideWidget(context, state), - SizeF(180f, 270f) to createMediumWidget(context, state), - SizeF(272f, 270f) to createLargeWidget(context, state) - ) + val views = + mapOf( + SizeF(180f, 100f) to createTinyWidget(context, state), + SizeF(180f, 152f) to createSmallWidget(context, state), + SizeF(272f, 152f) to createWideWidget(context, state), + SizeF(180f, 270f) to createMediumWidget(context, state), + SizeF(272f, 270f) to createLargeWidget(context, state)) appWidgetManager.applyViewsCompat(context, views) } } /** - * Custom function for loading bitmaps to the widget in a way that works with the - * widget ImageView instances. + * Custom function for loading bitmaps to the widget in a way that works with the widget + * ImageView instances. */ private fun loadWidgetBitmap(context: Context, song: Song, onDone: (Bitmap?) -> Unit) { - val coverRequest = ImageRequest.Builder(context) - .data(song.album) - .target( - onError = { onDone(null) }, - onSuccess = { onDone(it.toBitmap()) } - ) + val coverRequest = + ImageRequest.Builder(context) + .data(song.album) + .target(onError = { onDone(null) }, onSuccess = { onDone(it.toBitmap()) }) // The widget has two distinct styles that we must transform the album art to accommodate: // - Before Android 12, the widget has hard edges, so we don't need to round out the album @@ -109,15 +106,17 @@ class WidgetProvider : AppWidgetProvider() { // Use RoundedCornersTransformation. This is because our hack to get a 1:1 aspect // ratio on widget ImageViews doesn't actually result in a square ImageView, so // clipToOutline won't work. - val transform = RoundedCornersTransformation( - context.getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius) - .toFloat() - ) + val transform = + RoundedCornersTransformation( + context + .getDimenSizeSafe(android.R.dimen.system_app_widget_inner_radius) + .toFloat()) // The output of RoundedCornersTransformation is dimension-dependent, so scale up the // image to the screen size to ensure consistent radii. val metrics = context.resources.displayMetrics - coverRequest.transformations(SquareFrameTransform(), transform) + coverRequest + .transformations(SquareFrameTransform(), transform) .size(min(metrics.widthPixels, metrics.heightPixels)) } else { coverRequest.transformations(SquareFrameTransform()) @@ -132,9 +131,8 @@ class WidgetProvider : AppWidgetProvider() { fun reset(context: Context) { logD("Resetting widget") - AppWidgetManager.getInstance(context).updateAppWidget( - ComponentName(context, this::class.java), createDefaultWidget(context) - ) + AppWidgetManager.getInstance(context) + .updateAppWidget(ComponentName(context, this::class.java), createDefaultWidget(context)) } // --- OVERRIDES --- @@ -170,8 +168,7 @@ class WidgetProvider : AppWidgetProvider() { private fun requestUpdate(context: Context) { logD("Sending update intent to PlaybackService") - val intent = Intent(ACTION_WIDGET_UPDATE) - .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) + val intent = Intent(ACTION_WIDGET_UPDATE).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY) context.sendBroadcast(intent) } @@ -243,9 +240,8 @@ class WidgetProvider : AppWidgetProvider() { // Default to the smallest view if no layout fits logW("No good widget layout found") - val minimum = requireNotNull( - views.minByOrNull { it.key.width * it.key.height }?.value - ) + val minimum = + requireNotNull(views.minByOrNull { it.key.width * it.key.height }?.value) updateAppWidget(id, minimum) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt index 5ef139531..64a0dbceb 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetState.kt @@ -1,6 +1,5 @@ /* * Copyright (c) 2021 Auxio Project - * WidgetState.kt is part of Auxio. * * 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 @@ -15,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.widgets import android.graphics.Bitmap diff --git a/build.gradle b/build.gradle index 773a522aa..cc6d5fc5f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version" + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.3.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files