From f6b7a8f448b5860f6c51ee62da5c365b7cbb34c8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 08:42:35 -0700 Subject: [PATCH 01/24] music: formalize whitespace handling Formalize how whitespace tags are handled. The checks for blank tags and removal of trailing whitespace from tags are now the same function, carefully used to prevent blank tags from setting through. More testing will need to be done in order to fully ensure this system will work as intended. --- CHANGELOG.md | 3 +++ .../auxio/music/extractor/CacheExtractor.kt | 2 +- .../auxio/music/extractor/MetadataExtractor.kt | 6 +++--- .../auxio/music/extractor/ParsingUtil.kt | 17 +++++++++++++++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc7f6db2..a94d5e349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## dev +#### What's Improved +- Formalized whitespace handling + ## 3.0.0 #### What's New diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt index e9159a0db..d5e11713a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -388,7 +388,7 @@ private class CacheDatabase(context: Context) : * string. Escaped delimiters are converted back into their normal forms. */ private fun String.parseSQLMultiValue() = - splitEscaped { it == ';' } + splitEscaped { it == ';' }.correctWhitespace() /** Defines the columns used in this database. */ private object Columns { diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 6603850e4..f50b34d59 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -187,7 +187,7 @@ class Task(context: Context, private val raw: Song.Raw) { // Map TXXX frames differently so we can specifically index by their // descriptions. val id = tag.description?.let { "TXXX:${it.sanitize()}" } ?: tag.id.sanitize() - val values = tag.values.map { it.sanitize() }.filter { it.isNotEmpty() } + val values = tag.values.map { it.sanitize() }.correctWhitespace() if (values.isNotEmpty()) { id3v2Tags[id] = values } @@ -195,8 +195,8 @@ class Task(context: Context, private val raw: Song.Raw) { is VorbisComment -> { // Vorbis comment keys can be in any case, make them uppercase for simplicity. val id = tag.key.sanitize().uppercase() - val value = tag.value.sanitize() - if (value.isNotEmpty()) { + val value = tag.value.sanitize().correctWhitespace() + if (value != null) { vorbisTags.getOrPut(id) { mutableListOf() }.add(value) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 05a2deeb7..92cbeee1d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -115,6 +115,19 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List { return split } +/** + * Fix trailing whitespace or blank contents in a [String]. + * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace + * or empty. + */ +fun String.correctWhitespace() = trim().ifBlank { null } + +/** + * Fix trailing whitespace or blank contents within a list of [String]s. + * @return A list of non-blank strings with trailing whitespace removed. + */ +fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } + /** * Parse a multi-value tag based on the user configuration. If the value is already composed of more * than one value, nothing is done. Otherwise, this function will attempt to split it based on the @@ -127,7 +140,7 @@ fun List.parseMultiValue(settings: Settings) = get(0).maybeParseSeparators(settings) } else { // Nothing to do. - this.map { it.trim() } + this } /** @@ -138,7 +151,7 @@ fun List.parseMultiValue(settings: Settings) = fun String.maybeParseSeparators(settings: Settings): List { // Get the separators the user desires. If null, there's nothing to do. val separators = settings.musicSeparators ?: return listOf(this) - return splitEscaped { separators.contains(it) }.map { it.trim() } + return splitEscaped { separators.contains(it) }.correctWhitespace() } /** From 00401430f7c7ab4e8282c790c783854afc4a3a58 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 09:13:11 -0700 Subject: [PATCH 02/24] actions: create ci workflow Create a github actions workflow that automatically produces artifacts. --- .github/workflows/android.yml | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/android.yml diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 000000000..a80d3422c --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,38 @@ +name: Android CI + +on: + push: + branches: [ "dev" ] + pull_request: + branches: [ "dev" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: '11' + distribution: 'temurin' + cache: gradle + - name: Run pre-build steps + uses: nttld/setup-ndk@v1.2.0 + id: setup-ndk + with: + ndk-version: r21e + add-to-path: false + - run: python3 prebuild.py + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build Debug APK with Gradle + run: ./gradlew buildDebug + - name: Upload a Build Artifact + uses: actions/upload-artifact@v3.1.1 + with: + name: Auxio_Canary.apk + path: ./app/build/intermediates/apk/debug/app-debug.apk From affe5c482ba506fef05853e640cb76818c431883 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 11:06:15 -0700 Subject: [PATCH 03/24] all: reformat code --- CHANGELOG.md | 2 +- app/build.gradle | 4 ++++ .../java/org/oxycblt/auxio/MainFragment.kt | 1 - .../org/oxycblt/auxio/home/HomeFragment.kt | 2 +- .../oxycblt/auxio/image/extractor/Covers.kt | 4 +++- .../auxio/music/extractor/CacheExtractor.kt | 3 +-- .../auxio/music/extractor/ParsingUtil.kt | 21 +++++++++--------- .../auxio/music/extractor/SeparatorsDialog.kt | 19 ++++++++-------- .../auxio/music/picker/GenreChoiceAdapter.kt | 17 ++++++++++++++ .../music/picker/GenrePlaybackPickerDialog.kt | 22 +++++++++++++++++-- .../auxio/music/picker/PickerViewModel.kt | 4 ++-- .../oxycblt/auxio/music/storage/Storage.kt | 2 -- .../auxio/playback/PlaybackViewModel.kt | 8 +++---- .../org/oxycblt/auxio/settings/Settings.kt | 4 +--- .../oxycblt/auxio/ui/NavigationViewModel.kt | 8 +++---- .../org/oxycblt/auxio/widgets/WidgetUtil.kt | 4 ++-- prebuild.py | 2 +- 17 files changed, 82 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a94d5e349..d4d5ba5f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Added setting to hide "collaborator" artists - Upgraded music ID management: - Added support for MusicBrainz IDs (MBIDs) - - Use the more unique MD5 hash of metadata when MBIDs can't be used + - Use a more unique hash of metadata when MBIDs can't be used - Genres now display a list of artists - Added toggle to load non-music (Such as podcasts) - Music loader now caches parsed metadata for faster load times diff --git a/app/build.gradle b/app/build.gradle index ef8f54ab5..ddeeaeb23 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,3 +121,7 @@ spotless { licenseHeaderFile("NOTICE") } } + +afterEvaluate { + preDebugBuild.dependsOn spotlessApply +} diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index e66ef86cc..b692ae477 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -37,7 +37,6 @@ import kotlin.math.max import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentMainBinding import org.oxycblt.auxio.list.selection.SelectionViewModel -import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior 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 3e6795c5a..7c9ace57b 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -292,7 +292,7 @@ class HomeFragment : // Check the ascending option and corresponding sort option to align with // the current sort of the tab. if (option.itemId == toHighlight.mode.itemId || - (option.itemId == R.id.option_sort_asc && toHighlight.isAscending)) { + (option.itemId == R.id.option_sort_asc && toHighlight.isAscending)) { option.isChecked = true } diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt index 6b703e37f..31a4bda55 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Covers.kt @@ -177,6 +177,8 @@ object Covers { @Suppress("BlockingMethodInNonBlockingContext") private suspend fun fetchMediaStoreCovers(context: Context, album: Album): InputStream? { // Eliminate any chance that this blocking call might mess up the loading process - return withContext(Dispatchers.IO) { context.contentResolver.openInputStream(album.coverUri) } + return withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(album.coverUri) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt index d5e11713a..e9b391594 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -387,8 +387,7 @@ private class CacheDatabase(context: Context) : * @return A list of strings corresponding to the delimited values present within the original * string. Escaped delimiters are converted back into their normal forms. */ - private fun String.parseSQLMultiValue() = - splitEscaped { it == ';' }.correctWhitespace() + private fun String.parseSQLMultiValue() = splitEscaped { it == ';' }.correctWhitespace() /** Defines the columns used in this database. */ private object Columns { diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt index 92cbeee1d..29b0ba624 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/ParsingUtil.kt @@ -17,11 +17,9 @@ package org.oxycblt.auxio.music.extractor -import androidx.core.text.isDigitsOnly import java.util.UUID import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull /** @@ -117,8 +115,8 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List { /** * Fix trailing whitespace or blank contents in a [String]. - * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace - * or empty. + * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or + * empty. */ fun String.correctWhitespace() = trim().ifBlank { null } @@ -197,12 +195,15 @@ private fun String.parseId3v1Genre(): String? { // ID3v1 genres are a plain integer value without formatting, so in that case // try to index the genre table with such. If this fails, then try to compare it // to some other hard-coded values. - val numeric = toIntOrNull() ?: return when (this) { - // CR and RX are not technically ID3v1, but are formatted similarly to a plain number. - "CR" -> "Cover" - "RX" -> "Remix" - else -> null - } + val numeric = + toIntOrNull() + ?: return when (this) { + // CR and RX are not technically ID3v1, but are formatted similarly to a plain + // number. + "CR" -> "Cover" + "RX" -> "Remix" + else -> null + } return GENRE_TABLE.getOrNull(numeric) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index c7e2ed55f..12d8eae5d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -61,16 +61,17 @@ class SeparatorsDialog : ViewBindingDialogFragment() { // More efficient to do one iteration through the separator list and initialize // the corresponding CheckBox for each character instead of doing an iteration // through the separator list for each CheckBox. - (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: settings.musicSeparators)?.forEach { - when (it) { - SEPARATOR_COMMA -> binding.separatorComma.isChecked = true - SEPARATOR_SEMICOLON -> binding.separatorSemicolon.isChecked = true - SEPARATOR_SLASH -> binding.separatorSlash.isChecked = true - SEPARATOR_PLUS -> binding.separatorPlus.isChecked = true - SEPARATOR_AND -> binding.separatorAnd.isChecked = true - else -> error("Unexpected separator in settings data") + (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: settings.musicSeparators) + ?.forEach { + when (it) { + SEPARATOR_COMMA -> binding.separatorComma.isChecked = true + SEPARATOR_SEMICOLON -> binding.separatorSemicolon.isChecked = true + SEPARATOR_SLASH -> binding.separatorSlash.isChecked = true + SEPARATOR_PLUS -> binding.separatorPlus.isChecked = true + SEPARATOR_AND -> binding.separatorAnd.isChecked = true + else -> error("Unexpected separator in settings data") + } } - } } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt index 57f8322ce..0d6fceae1 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.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.picker import android.view.View diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt index a5bba8d48..9ba9368fe 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.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.picker import android.os.Bundle @@ -21,7 +38,8 @@ import org.oxycblt.auxio.util.collectImmediately * A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous. * @author Alexander Capehart (OxygenCobalt) */ -class GenrePlaybackPickerDialog : ViewBindingDialogFragment(), ClickableListListener { +class GenrePlaybackPickerDialog : + ViewBindingDialogFragment(), ClickableListListener { private val pickerModel: PickerViewModel by viewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels() // Information about what Song to show choices for is initially within the navigation arguments @@ -63,4 +81,4 @@ class GenrePlaybackPickerDialog : ViewBindingDialogFragment(null) /** The current item whose artists should be shown in the picker. Null if there is no item. */ - val currentItem: StateFlow get() = _currentItem + val currentItem: StateFlow + get() = _currentItem private val _artistChoices = MutableStateFlow>(listOf()) /** The current [Artist] choices. Empty if no item is shown in the picker. */ @@ -75,5 +76,4 @@ class PickerViewModel : ViewModel(), MusicStore.Callback { else -> {} } } - } diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt index ebe8a0ae2..00a22deaf 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/Storage.kt @@ -18,12 +18,10 @@ package org.oxycblt.auxio.music.storage import android.content.Context -import android.media.MediaExtractor import android.media.MediaFormat import android.os.storage.StorageManager import android.os.storage.StorageVolume import android.webkit.MimeTypeMap -import com.google.android.exoplayer2.util.MimeTypes import java.io.File import org.oxycblt.auxio.R 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 d2e5e4181..42a37159b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -70,8 +70,8 @@ class PlaybackViewModel(application: Application) : private val _artistPlaybackPickerSong = MutableStateFlow(null) /** - * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when - * playing a [Song] from one of it's [Artist]s. + * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a + * [Song] from one of it's [Artist]s. * @see playFromArtist */ val artistPickerSong: StateFlow @@ -79,8 +79,8 @@ class PlaybackViewModel(application: Application) : private val _genrePlaybackPickerSong = MutableStateFlow(null) /** - * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing - * a [Song] from one of it's [Genre]s. + * Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a + * [Song] from one of it's [Genre]s. */ val genrePickerSong: StateFlow get() = _genrePlaybackPickerSong diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index f5711e221..50887fede 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -165,9 +165,7 @@ class Settings(private val context: Context, private val callback: Callback? = n unlikelyToBeNull(callback).onSettingChanged(key) } - /** - * Simplified callback for settings changes. - */ + /** Simplified callback for settings changes. */ interface Callback { // TODO: Refactor this lifecycle /** diff --git a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt index 905862d7d..2ea31a0c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/NavigationViewModel.kt @@ -92,8 +92,8 @@ class NavigationViewModel : ViewModel() { /** * Navigate to one of the parent [Artist]'s of the given [Song]. - * @param song The [Song] to navigate with. If there are multiple parent [Artist]s, - * a picker dialog will be shown. + * @param song The [Song] to navigate with. If there are multiple parent [Artist]s, a picker + * dialog will be shown. */ fun exploreNavigateToParentArtist(song: Song) { exploreNavigateToParentArtistImpl(song, song.artists) @@ -101,8 +101,8 @@ class NavigationViewModel : ViewModel() { /** * Navigate to one of the parent [Artist]'s of the given [Album]. - * @param album The [Album] to navigate with. If there are multiple parent [Artist]s, - * a picker dialog will be shown. + * @param album The [Album] to navigate with. If there are multiple parent [Artist]s, a picker + * dialog will be shown. */ fun exploreNavigateToParentArtist(album: Album) { exploreNavigateToParentArtistImpl(album, album.artists) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt index 2085db573..58f75b0a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -80,8 +80,8 @@ fun RemoteViews.setLayoutDirection(@IdRes viewId: Int, layoutDirection: Int) { } /** - * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with - * an adaptive layout, in a version-compatible manner. + * Update the app widget layouts corresponding to the given [WidgetProvider] [ComponentName] with an + * adaptive layout, in a version-compatible manner. * @param context [Context] required to backport adaptive layout behavior. * @param component [ComponentName] of the app widget layout to update. * @param views Mapping between different size classes and [RemoteViews] instances. diff --git a/prebuild.py b/prebuild.py index 7aeb9eb11..59ea9552b 100755 --- a/prebuild.py +++ b/prebuild.py @@ -60,7 +60,7 @@ if os.getenv("ANDROID_HOME") is None and os.getenv("ANDROID_SDK_ROOT") is None: "ANDROID_HOME/ANDROID_SDK_ROOT before continuing.") sys.exit(1) -ndk_path = os.getenv("NDK_PATH") +ndk_path = os.getenv("ANDROID_NDK_HOME") if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk-build")): # We don't have a proper path. Do some digging on the Android SDK directory # to see if we can find it. From b0f38fc5dbd9565f3893c1129a359a5a6902ddb1 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 11:29:43 -0700 Subject: [PATCH 04/24] actions: fix artifacts Fix an inccorrectly specific build command and path for creating artifacts. --- .github/workflows/android.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a80d3422c..d150e9336 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -30,9 +30,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build Debug APK with Gradle - run: ./gradlew buildDebug + run: ./gradlew app:packageDebug - name: Upload a Build Artifact uses: actions/upload-artifact@v3.1.1 with: - name: Auxio_Canary.apk - path: ./app/build/intermediates/apk/debug/app-debug.apk + name: Auxio_Canary + path: ./app/build/outputs/apk/debug/app-debug.apk From 4533251efd039415db5802e0f131b7d3dc6a444e Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 14:42:19 -0700 Subject: [PATCH 05/24] info: make bug report log field mandatory Sick and tired of people providing unhelpful bug reports without a stack trace. This should force them to provide one. --- .github/ISSUE_TEMPLATE/bug-crash-report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-crash-report.yml b/.github/ISSUE_TEMPLATE/bug-crash-report.yml index 614c45a41..d34ce4da9 100644 --- a/.github/ISSUE_TEMPLATE/bug-crash-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-crash-report.yml @@ -72,6 +72,8 @@ body: - `./adb -d logcat AndroidRuntime:E *:S` in the case of a crash 5. Copy and paste the output to this area of the issue. render: shell + validations: + required: true - type: checkboxes id: terms attributes: From cf6e7a5f0d65dba19bb041940c6d538a004da89f Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 16:07:17 -0700 Subject: [PATCH 06/24] music: add support for album date ranges Add support for albums to have a range of dates. Often compilation albums will have Songs released in different months or years, so it makes some sense to show a date range rather than just the ealiest date. The only point at which the earliest date is still shown is in the home view's popup, as maxiumum dates in a date range are not sorted by, and so showing it doesn't make sense. --- CHANGELOG.md | 3 + .../detail/recycler/AlbumDetailAdapter.kt | 4 +- .../detail/recycler/ArtistDetailAdapter.kt | 5 +- .../auxio/home/list/AlbumListFragment.kt | 4 +- .../auxio/home/list/SongListFragment.kt | 2 +- .../java/org/oxycblt/auxio/music/Music.kt | 77 ++++++++++++++----- .../main/java/org/oxycblt/auxio/music/Sort.kt | 14 ++-- app/src/main/res/values/donottranslate.xml | 1 + 8 files changed, 79 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d5ba5f4..08afaeb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## dev +#### What's New +- Added support for album date ranges (ex. 2010 - 2013) + #### What's Improved - Formalized whitespace handling 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 8d1eb25ec..a61135a61 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 @@ -142,7 +142,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite // Date, song count, and duration map to the info text binding.detailInfo.apply { // Fall back to a friendlier "No date" text if the album doesn't have date information - val date = album.date?.resolveDate(context) ?: context.getString(R.string.def_date) + val date = album.dates?.resolveDate(context) ?: context.getString(R.string.def_date) val songCount = context.getPlural(R.plurals.fmt_song_count, album.songs.size) val duration = album.durationMs.formatDurationMs(true) text = context.getString(R.string.fmt_three, date, songCount, duration) @@ -170,7 +170,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite override fun areContentsTheSame(oldItem: Album, newItem: Album) = oldItem.rawName == newItem.rawName && oldItem.areArtistContentsTheSame(newItem) && - oldItem.date == newItem.date && + oldItem.dates == newItem.dates && oldItem.songs.size == newItem.songs.size && oldItem.durationMs == newItem.durationMs && oldItem.type == newItem.type 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 12b9b2fd4..f8c213852 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 @@ -189,7 +189,8 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite binding.parentName.text = album.resolveName(binding.context) binding.parentInfo.text = // Fall back to a friendlier "No date" text if the album doesn't have date information - album.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date) + album.dates?.resolveDate(binding.context) + ?: binding.context.getString(R.string.def_date) } override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { @@ -217,7 +218,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite val DIFF_CALLBACK = object : SimpleItemCallback() { override fun areContentsTheSame(oldItem: Album, newItem: Album) = - oldItem.rawName == newItem.rawName && oldItem.date == newItem.date + oldItem.rawName == newItem.rawName && oldItem.dates == newItem.dates } } } 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 f37309f84..01d2637fa 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 @@ -94,8 +94,8 @@ class AlbumListFragment : is Sort.Mode.ByArtist -> album.artists[0].collationKey?.run { sourceString.first().uppercase() } - // Year -> Use Full Year - is Sort.Mode.ByDate -> album.date?.resolveDate(requireContext()) + // Date -> Use minimum date (Maximum dates are not sorted by, so showing them is odd) + is Sort.Mode.ByDate -> album.dates?.run { min.resolveDate(requireContext()) } // Duration -> Use formatted duration is Sort.Mode.ByDuration -> album.durationMs.formatDurationMs(false) 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 c040761fc..1a2cc40c0 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 @@ -103,7 +103,7 @@ class SongListFragment : song.album.collationKey?.run { sourceString.first().uppercase() } // Year -> Use Full Year - is Sort.Mode.ByDate -> song.album.date?.resolveDate(requireContext()) + is Sort.Mode.ByDate -> song.album.dates?.resolveDate(requireContext()) // Duration -> Use formatted duration is Sort.Mode.ByDuration -> song.durationMs.formatDurationMs(false) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index aa424003c..299e5c60b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -610,11 +610,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( override val collationKey = makeCollationKeyImpl() override fun resolveName(context: Context) = rawName - /** - * The earliest [Date] this album was released. Will be null if no valid date was present in the - * metadata of any [Song] - */ - val date: Date? // TODO: Date ranges? + /** The [DateRange] that [Song]s in the [Album] were released. */ + val dates: DateRange? = DateRange.from(songs.mapNotNull { it.date }) /** * The [Type] of this album, signifying the type of release it actually is. Defaults to @@ -634,31 +631,18 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( val dateAdded: Long init { - var earliestDate: Date? = null var totalDuration: Long = 0 var earliestDateAdded: Long = Long.MAX_VALUE // Do linking and value generation in the same loop for efficiency. for (song in songs) { song._link(this) - - if (song.date != null) { - // Since we can't really assign a maximum value for dates, we instead - // just check if the current earliest date doesn't exist and fill it - // in with the current song if that's the case. - if (earliestDate == null || song.date < earliestDate) { - earliestDate = song.date - } - } - if (song.dateAdded < earliestDateAdded) { earliestDateAdded = song.dateAdded } - totalDuration += song.durationMs } - date = earliestDate durationMs = totalDuration dateAdded = earliestDateAdded } @@ -1385,6 +1369,63 @@ class Date private constructor(private val tokens: List) : Comparable } } +/** + * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from several + * sub-items and thus can have a "range" of release dates. + * @param min The earliest [Date] in the range. + * @param max The latest [Date] in the range. May be the same as [min]. + * @author Alexander Capehart + */ +class DateRange private constructor(val min: Date, val max: Date) : Comparable { + + /** + * Resolve this instance into a human-readable date range. + * @param context [Context] required to get human-readable names. + * @return If the date has a maximum value, then a `min - max` formatted string will be returned + * with the formatted [Date]s of the minimum and maximum dates respectively. Otherwise, the + * formatted name of the minimum [Date] will be returned. + */ + fun resolveDate(context: Context) = + if (min != max) { + context.getString( + R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context)) + } else { + min.resolveDate(context) + } + + override fun compareTo(other: DateRange): Int { + return min.compareTo(other.min) + } + + companion object { + /** + * Create a [DateRange] from the given list of [Date]s. + * @param dates The [Date]s to use. + * @return A [DateRange] based on the minimum and maximum [Date]s from [dates], or a + * [DateRange] with a single minimum. If no [Date]s were given, null is returned. + */ + fun from(dates: List): DateRange? { + if (dates.isEmpty()) { + // Nothing to do. + return null + } + // Simultaneously find the minimum and maximum values in the given range. + // If this list has only one item, then that one date is the minimum and maximum. + var min = dates.first() + var max = min + for (i in 1..dates.lastIndex) { + if (dates[i] < min) { + min = dates[i] + } + if (dates[i] > max) { + max = dates[i] + } + } + return DateRange(min, max) + } + } +} + // --- MUSIC UID CREATION UTILITIES --- /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt index 7ce2248bd..4f8b9c2d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt @@ -232,7 +232,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getSongComparator(isAscending: Boolean): Comparator = MultiComparator( compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists }, - compareByDescending(NullableComparator.DATE) { it.album.date }, + compareByDescending(NullableComparator.DATE_RANGE) { it.album.dates }, compareByDescending(BasicComparator.ALBUM) { it.album }, compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.track }, @@ -241,14 +241,14 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getAlbumComparator(isAscending: Boolean): Comparator = MultiComparator( compareByDynamic(isAscending, ListComparator.ARTISTS) { it.artists }, - compareByDescending(NullableComparator.DATE) { it.date }, + compareByDescending(NullableComparator.DATE_RANGE) { it.dates }, compareBy(BasicComparator.ALBUM)) } /** * Sort by the [Date] of an item. Only available for [Song] and [Album]. * @see Song.date - * @see Album.date + * @see Album.dates */ object ByDate : Mode() { override val intCode: Int @@ -259,7 +259,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getSongComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending, NullableComparator.DATE) { it.album.date }, + compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.album.dates }, compareByDescending(BasicComparator.ALBUM) { it.album }, compareBy(NullableComparator.INT) { it.disc }, compareBy(NullableComparator.INT) { it.track }, @@ -267,7 +267,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { override fun getAlbumComparator(isAscending: Boolean): Comparator = MultiComparator( - compareByDynamic(isAscending, NullableComparator.DATE) { it.date }, + compareByDynamic(isAscending, NullableComparator.DATE_RANGE) { it.dates }, compareBy(BasicComparator.ALBUM)) } @@ -366,7 +366,7 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { /** * Sort by the date an item was added. Only supported by [Song]s and [Album]s. * @see Song.dateAdded - * @see Album.date + * @see Album.dates */ object ByDateAdded : Mode() { override val intCode: Int @@ -545,6 +545,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { val LONG = NullableComparator() /** A re-usable instance configured for [Date]s. */ val DATE = NullableComparator() + /** A re-usable instance configured for [DateRange]s. */ + val DATE_RANGE = NullableComparator() } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index aca5a0088..2535c992e 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -7,6 +7,7 @@ %1$s • %2$s %1$s • %2$s • %3$s %d + %s - %s %1$s/%2$s From 58e026e781d4f12f7aa16ec3422b4d8c34fa26e8 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 16:17:39 -0700 Subject: [PATCH 07/24] music: split off dates into separate file Split off Dates and Date Ranges off into their own file. The Music file was getting too big for it's own good, and the addition of Date ranges makes splitting it off much easier. --- .../main/java/org/oxycblt/auxio/music/Date.kt | 256 ++++++++++++++++++ .../java/org/oxycblt/auxio/music/Music.kt | 237 +--------------- .../main/java/org/oxycblt/auxio/music/Sort.kt | 6 +- 3 files changed, 260 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/Date.kt diff --git a/app/src/main/java/org/oxycblt/auxio/music/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/Date.kt new file mode 100644 index 000000000..11a2378bd --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/Date.kt @@ -0,0 +1,256 @@ +/* + * 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.Context +import java.text.ParseException +import java.text.SimpleDateFormat +import kotlin.math.max +import org.oxycblt.auxio.R +import org.oxycblt.auxio.util.inRangeOrNull +import org.oxycblt.auxio.util.nonZeroOrNull + +/** + * An ISO-8601/RFC 3339 Date. + * + * This class only encodes the timestamp spec and it's conversion to a human-readable date, without + * any other time management or validation. In general, this should only be used for display. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class Date private constructor(private val tokens: List) : Comparable { + private val year = tokens[0] + private val month = tokens.getOrNull(1) + private val day = tokens.getOrNull(2) + private val hour = tokens.getOrNull(3) + private val minute = tokens.getOrNull(4) + private val second = tokens.getOrNull(5) + + /** + * Resolve this instance into a human-readable date. + * @param context [Context] required to get human-readable names. + * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan + * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will + * be properly localized. + */ + fun resolveDate(context: Context): String { + if (month != null) { + // Parse a date format from an ISO-ish format + val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) + format.applyPattern("yyyy-MM") + val date = + try { + format.parse("$year-$month") + } catch (e: ParseException) { + null + } + + if (date != null) { + // Reformat as a readable month and year + format.applyPattern("MMM yyyy") + return format.format(date) + } + } + + // Unable to create fine-grained date, just format as a year. + return context.getString(R.string.fmt_number, year) + } + + override fun hashCode() = tokens.hashCode() + + override fun equals(other: Any?) = other is Date && tokens == other.tokens + + override fun compareTo(other: Date): Int { + for (i in 0 until max(tokens.size, other.tokens.size)) { + val ai = tokens.getOrNull(i) + val bi = other.tokens.getOrNull(i) + when { + ai != null && bi != null -> { + val result = ai.compareTo(bi) + if (result != 0) { + return result + } + } + ai == null && bi != null -> return -1 // a < b + ai == null && bi == null -> return 0 // a = b + else -> return 1 // a < b + } + } + + return 0 + } + + override fun toString() = StringBuilder().appendDate().toString() + + private fun StringBuilder.appendDate(): StringBuilder { + // Construct an ISO-8601 date, dropping precision that doesn't exist. + append(year.toStringFixed(4)) + append("-${(month ?: return this).toStringFixed(2)}") + append("-${(day ?: return this).toStringFixed(2)}") + append("T${(hour ?: return this).toStringFixed(2)}") + append(":${(minute ?: return this.append('Z')).toStringFixed(2)}") + append(":${(second ?: return this.append('Z')).toStringFixed(2)}") + return this.append('Z') + } + + private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len) + + /** + * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from + * several sub-items and thus can have a "range" of release dates. + * @param min The earliest [Date] in the range. + * @param max The latest [Date] in the range. May be the same as [min]. + * @author Alexander Capehart + */ + class Range private constructor(val min: Date, val max: Date) : Comparable { + + /** + * Resolve this instance into a human-readable date range. + * @param context [Context] required to get human-readable names. + * @return If the date has a maximum value, then a `min - max` formatted string will be + * returned with the formatted [Date]s of the minimum and maximum dates respectively. + * Otherwise, the formatted name of the minimum [Date] will be returned. + */ + fun resolveDate(context: Context) = + if (min != max) { + context.getString( + R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context)) + } else { + min.resolveDate(context) + } + + override fun compareTo(other: Range): Int { + return min.compareTo(other.min) + } + + companion object { + /** + * Create a [Range] from the given list of [Date]s. + * @param dates The [Date]s to use. + * @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s, + * null is returned. + */ + fun from(dates: List): Range? { + if (dates.isEmpty()) { + // Nothing to do. + return null + } + // Simultaneously find the minimum and maximum values in the given range. + // If this list has only one item, then that one date is the minimum and maximum. + var min = dates.first() + var max = min + for (i in 1..dates.lastIndex) { + if (dates[i] < min) { + min = dates[i] + } + if (dates[i] > max) { + max = dates[i] + } + } + return Range(min, max) + } + } + } + + companion object { + /** + * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from + * https://github.com/quodlibet/mutagen + */ + private val ISO8601_REGEX = + Regex( + """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""") + + /** + * Create a [Date] from a year component. + * @param year The year component. + * @return A new [Date] of the given component, or null if the component is invalid. + */ + fun from(year: Int) = fromTokens(listOf(year)) + + /** + * Create a [Date] from a date component. + * @param year The year component. + * @param month The month component. + * @param day The day component. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. + */ + fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day)) + + /** + * Create [Date] from a datetime component. + * @param year The year component. + * @param month The month component. + * @param day The day component. + * @param hour The hour component + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. + */ + fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) = + fromTokens(listOf(year, month, day, hour, minute)) + + /** + * Create a [Date] from a [String] timestamp. + * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid or + * if the timestamp is invalid. + */ + fun from(timestamp: String): Date? { + val tokens = + // Match the input with the timestamp regex + (ISO8601_REGEX.matchEntire(timestamp) ?: return null) + .groupValues + // Filter to the specific tokens we want and convert them to integer tokens. + .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null } + return fromTokens(tokens) + } + + /** + * Create a [Date] from the given non-validated tokens. + * @param tokens The tokens to use for each date component, in order of precision. + * @return A new [Date] consisting of the given components. May have reduced precision if + * the components were partially invalid, and will be null if all components are invalid. + */ + private fun fromTokens(tokens: List): Date? { + val validated = mutableListOf() + validateTokens(tokens, validated) + if (validated.isEmpty()) { + // No token was valid, return null. + return null + } + return Date(validated) + } + + /** + * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop + * as soon as an invalid token is found. + * @param src The input tokens to validate. + * @param dst The destination list to add valid tokens to. + */ + private fun validateTokens(src: List, dst: MutableList) { + dst.add(src.getOrNull(0)?.nonZeroOrNull() ?: return) + dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return) + dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: return) + dst.add(src.getOrNull(3)?.inRangeOrNull(0..23) ?: return) + dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return) + dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return) + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 299e5c60b..528ff3531 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -24,8 +24,6 @@ import android.os.Parcelable import java.security.MessageDigest import java.text.CollationKey import java.text.Collator -import java.text.ParseException -import java.text.SimpleDateFormat import java.util.UUID import kotlin.math.max import kotlinx.parcelize.IgnoredOnParcel @@ -37,7 +35,6 @@ import org.oxycblt.auxio.music.extractor.parseMultiValue import org.oxycblt.auxio.music.extractor.toUuidOrNull import org.oxycblt.auxio.music.storage.* import org.oxycblt.auxio.settings.Settings -import org.oxycblt.auxio.util.inRangeOrNull import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -610,8 +607,8 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( override val collationKey = makeCollationKeyImpl() override fun resolveName(context: Context) = rawName - /** The [DateRange] that [Song]s in the [Album] were released. */ - val dates: DateRange? = DateRange.from(songs.mapNotNull { it.date }) + /** The [Date.Range] that [Song]s in the [Album] were released. */ + val dates = Date.Range.from(songs.mapNotNull { it.date }) /** * The [Type] of this album, signifying the type of release it actually is. Defaults to @@ -1196,236 +1193,6 @@ class Genre constructor(private val raw: Raw, override val songs: List) : } } -/** - * An ISO-8601/RFC 3339 Date. - * - * This class only encodes the timestamp spec and it's conversion to a human-readable date, without - * any other time management or validation. In general, this should only be used for display. - * - * @author Alexander Capehart (OxygenCobalt) - */ -class Date private constructor(private val tokens: List) : Comparable { - private val year = tokens[0] - private val month = tokens.getOrNull(1) - private val day = tokens.getOrNull(2) - private val hour = tokens.getOrNull(3) - private val minute = tokens.getOrNull(4) - private val second = tokens.getOrNull(5) - - /** - * Resolve this instance into a human-readable date. - * @param context [Context] required to get human-readable names. - * @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan - * 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will - * be properly localized. - */ - fun resolveDate(context: Context): String { - if (month != null) { - // Parse a date format from an ISO-ish format - val format = (SimpleDateFormat.getDateInstance() as SimpleDateFormat) - format.applyPattern("yyyy-MM") - val date = - try { - format.parse("$year-$month") - } catch (e: ParseException) { - null - } - - if (date != null) { - // Reformat as a readable month and year - format.applyPattern("MMM yyyy") - return format.format(date) - } - } - - // Unable to create fine-grained date, just format as a year. - return context.getString(R.string.fmt_number, year) - } - - override fun hashCode() = tokens.hashCode() - - override fun equals(other: Any?) = other is Date && tokens == other.tokens - - override fun compareTo(other: Date): Int { - for (i in 0 until max(tokens.size, other.tokens.size)) { - val ai = tokens.getOrNull(i) - val bi = other.tokens.getOrNull(i) - when { - ai != null && bi != null -> { - val result = ai.compareTo(bi) - if (result != 0) { - return result - } - } - ai == null && bi != null -> return -1 // a < b - ai == null && bi == null -> return 0 // a = b - else -> return 1 // a < b - } - } - - return 0 - } - - override fun toString() = StringBuilder().appendDate().toString() - - private fun StringBuilder.appendDate(): StringBuilder { - // Construct an ISO-8601 date, dropping precision that doesn't exist. - append(year.toStringFixed(4)) - append("-${(month ?: return this).toStringFixed(2)}") - append("-${(day ?: return this).toStringFixed(2)}") - append("T${(hour ?: return this).toStringFixed(2)}") - append(":${(minute ?: return this.append('Z')).toStringFixed(2)}") - append(":${(second ?: return this.append('Z')).toStringFixed(2)}") - return this.append('Z') - } - - private fun Int.toStringFixed(len: Int) = toString().padStart(len, '0').substring(0 until len) - - companion object { - /** - * A [Regex] that can parse a variable-precision ISO-8601 timestamp. Derived from - * https://github.com/quodlibet/mutagen - */ - private val ISO8601_REGEX = - Regex( - """^(\d{4,})([-.](\d{2})([-.](\d{2})([T ](\d{2})([:.](\d{2})([:.](\d{2})(Z)?)?)?)?)?)?$""") - - /** - * Create a [Date] from a year component. - * @param year The year component. - * @return A new [Date] of the given component, or null if the component is invalid. - */ - fun from(year: Int) = fromTokens(listOf(year)) - - /** - * Create a [Date] from a date component. - * @param year The year component. - * @param month The month component. - * @param day The day component. - * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. - */ - fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day)) - - /** - * Create [Date] from a datetime component. - * @param year The year component. - * @param month The month component. - * @param day The day component. - * @param hour The hour component - * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. - */ - fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) = - fromTokens(listOf(year, month, day, hour, minute)) - - /** - * Create a [Date] from a [String] timestamp. - * @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision. - * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid or - * if the timestamp is invalid. - */ - fun from(timestamp: String): Date? { - val tokens = - // Match the input with the timestamp regex - (ISO8601_REGEX.matchEntire(timestamp) ?: return null) - .groupValues - // Filter to the specific tokens we want and convert them to integer tokens. - .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null } - return fromTokens(tokens) - } - - /** - * Create a [Date] from the given non-validated tokens. - * @param tokens The tokens to use for each date component, in order of precision. - * @return A new [Date] consisting of the given components. May have reduced precision if - * the components were partially invalid, and will be null if all components are invalid. - */ - private fun fromTokens(tokens: List): Date? { - val validated = mutableListOf() - validateTokens(tokens, validated) - if (validated.isEmpty()) { - // No token was valid, return null. - return null - } - return Date(validated) - } - - /** - * Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop - * as soon as an invalid token is found. - * @param src The input tokens to validate. - * @param dst The destination list to add valid tokens to. - */ - private fun validateTokens(src: List, dst: MutableList) { - dst.add(src.getOrNull(0)?.nonZeroOrNull() ?: return) - dst.add(src.getOrNull(1)?.inRangeOrNull(1..12) ?: return) - dst.add(src.getOrNull(2)?.inRangeOrNull(1..31) ?: return) - dst.add(src.getOrNull(3)?.inRangeOrNull(0..23) ?: return) - dst.add(src.getOrNull(4)?.inRangeOrNull(0..59) ?: return) - dst.add(src.getOrNull(5)?.inRangeOrNull(0..59) ?: return) - } - } -} - -/** - * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from several - * sub-items and thus can have a "range" of release dates. - * @param min The earliest [Date] in the range. - * @param max The latest [Date] in the range. May be the same as [min]. - * @author Alexander Capehart - */ -class DateRange private constructor(val min: Date, val max: Date) : Comparable { - - /** - * Resolve this instance into a human-readable date range. - * @param context [Context] required to get human-readable names. - * @return If the date has a maximum value, then a `min - max` formatted string will be returned - * with the formatted [Date]s of the minimum and maximum dates respectively. Otherwise, the - * formatted name of the minimum [Date] will be returned. - */ - fun resolveDate(context: Context) = - if (min != max) { - context.getString( - R.string.fmt_date_range, min.resolveDate(context), max.resolveDate(context)) - } else { - min.resolveDate(context) - } - - override fun compareTo(other: DateRange): Int { - return min.compareTo(other.min) - } - - companion object { - /** - * Create a [DateRange] from the given list of [Date]s. - * @param dates The [Date]s to use. - * @return A [DateRange] based on the minimum and maximum [Date]s from [dates], or a - * [DateRange] with a single minimum. If no [Date]s were given, null is returned. - */ - fun from(dates: List): DateRange? { - if (dates.isEmpty()) { - // Nothing to do. - return null - } - // Simultaneously find the minimum and maximum values in the given range. - // If this list has only one item, then that one date is the minimum and maximum. - var min = dates.first() - var max = min - for (i in 1..dates.lastIndex) { - if (dates[i] < min) { - min = dates[i] - } - if (dates[i] > max) { - max = dates[i] - } - } - return DateRange(min, max) - } - } -} - // --- MUSIC UID CREATION UTILITIES --- /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt index 4f8b9c2d7..5c8fa818f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Sort.kt @@ -543,10 +543,8 @@ data class Sort(val mode: Mode, val isAscending: Boolean) { val INT = NullableComparator() /** A re-usable instance configured for [Long]s. */ val LONG = NullableComparator() - /** A re-usable instance configured for [Date]s. */ - val DATE = NullableComparator() - /** A re-usable instance configured for [DateRange]s. */ - val DATE_RANGE = NullableComparator() + /** A re-usable instance configured for [Date.Range]s. */ + val DATE_RANGE = NullableComparator() } } From f416bb0d000447cfff65a7060ccb59055ac9d81c Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 16:32:29 -0700 Subject: [PATCH 08/24] music: localize list values When resolving the names of several artists or genres, use a localized separator instead of a comma. This makes list values more correct in other languages, if properly translated. --- CHANGELOG.md | 1 + .../java/org/oxycblt/auxio/music/Music.kt | 31 +++++++++++++++---- app/src/main/res/values/strings.xml | 3 ++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08afaeb6b..a3d67894a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ #### What's Improved - Formalized whitespace handling +- Value lists are now properly localized ## 3.0.0 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 528ff3531..04ba735f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -111,6 +111,27 @@ sealed class Music : Item { return COLLATOR.getCollationKey(sortName) } + /** + * Join a list of [Music]'s resolved names into a string in a localized manner, using + * [R.string.fmt_list]. + * @param context [Context] required to obtain localized formatting. + * @param values The list of [Music] to format. + * @return A single string consisting of the values delimited by a localized separator. + */ + protected fun resolveNames(context: Context, values: List): String { + if (values.isEmpty()) { + // Nothing to do. + return "" + } + + var joined = values.first().resolveName(context) + for (i in 1..values.lastIndex) { + // Chain all previous values with the next value in the list with another delimiter. + joined = context.getString(R.string.fmt_list, joined, values[i].resolveName(context)) + } + return joined + } + // Note: We solely use the UID in comparisons so that certain items that differ in all // but UID are treated differently. @@ -396,9 +417,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { * Resolves one or more [Artist]s into a single piece of human-readable names. * @param context [Context] required for [resolveName]. formatter. */ - fun resolveArtistContents(context: Context) = - // TODO Internationalize the list - artists.joinToString { it.resolveName(context) } + fun resolveArtistContents(context: Context) = resolveNames(context, artists) /** * Checks if the [Artist] *display* of this [Song] and another [Song] are equal. This will only @@ -430,7 +449,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { * Resolves one or more [Genre]s into a single piece human-readable names. * @param context [Context] required for [resolveName]. */ - fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } + fun resolveGenreContents(context: Context) = resolveNames(context, genres) // --- INTERNAL FIELDS --- @@ -657,7 +676,7 @@ class Album constructor(raw: Raw, override val songs: List) : MusicParent( * Resolves one or more [Artist]s into a single piece of human-readable names. * @param context [Context] required for [resolveName]. */ - fun resolveArtistContents(context: Context) = artists.joinToString { it.resolveName(context) } + fun resolveArtistContents(context: Context) = resolveNames(context, artists) /** * Checks if the [Artist] *display* of this [Album] and another [Album] are equal. This will @@ -1024,7 +1043,7 @@ class Artist constructor(private val raw: Raw, songAlbums: List) : MusicP * Resolves one or more [Genre]s into a single piece of human-readable names. * @param context [Context] required for [resolveName]. */ - fun resolveGenreContents(context: Context) = genres.joinToString { it.resolveName(context) } + fun resolveGenreContents(context: Context) = resolveNames(context, genres) /** * Checks if the [Genre] *display* of this [Artist] and another [Artist] are equal. This will diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 68503a76b..30261473b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -341,6 +341,9 @@ + + %1$s, %2$s + %d Selected From 4ed8a7e9679a97cdfaf888a9e2d4095cadd0c761 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 17:19:41 -0700 Subject: [PATCH 09/24] queue: scroll to top of list on song changes Always scroll to the top of the queue list when the current song changes. This way, the user can see future items rather than past items. In an ideal world, I would try to go to the center of the queue, but it seems like the "average" scroll tends to settle at the top no matter what I do, so whatever. There's also a slight in-accuracy in what the app considers the "Top" of the queue, but that's considered minimally detrimental given how much a QoL improvement this is. Resolves #210. --- CHANGELOG.md | 1 + .../main/java/org/oxycblt/auxio/music/Music.kt | 1 - .../auxio/playback/queue/QueueAdapter.kt | 3 ++- .../auxio/playback/queue/QueueFragment.kt | 18 +++++++++++++----- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d67894a..61e4cbaea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ #### What's Improved - Formalized whitespace handling - Value lists are now properly localized +- Queue no longer primarily shows previous songs when opened ## 3.0.0 diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 04ba735f7..8d86c5c78 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -520,7 +520,6 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with // the artist ordering within the song metadata. - // TODO: Make sure this works for artists only derived from album artists. val newIdx = _artists[i]._getOriginalPositionIn(_rawArtists) val other = _artists[newIdx] _artists[newIdx] = _artists[i] 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 102fa38a2..d8d761dc5 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 @@ -199,7 +199,8 @@ class QueueSongViewHolder private constructor(private val binding: ItemQueueSong binding.songAlbumCover.bind(song) binding.songName.text = song.resolveName(binding.context) binding.songInfo.text = song.resolveArtistContents(binding.context) - // TODO: Why is this here? + // Not swiping this ViewHolder if it's being re-bound, ensure that the background is + // not visible. See QueueDragCallback for why this is done. binding.background.isInvisible = true // Set up the drag handle to start a drag whenever it is touched. 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 b86db473e..29421415c 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 @@ -24,6 +24,7 @@ import androidx.fragment.app.activityViewModels import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel @@ -108,17 +109,24 @@ class QueueFragment : ViewBindingFragment(), QueueAdapter. queueModel.finishReplace() // If requested, scroll to a new item (occurs when the index moves) - // TODO: Scroll to center/top instead of bottom val scrollTo = queueModel.scrollTo if (scrollTo != null) { - // Do not scroll to indices that are in the currently visible range. As that would - // lead to the queue jumping around every time goto is called. val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager val start = lmm.findFirstCompletelyVisibleItemPosition() val end = lmm.findLastCompletelyVisibleItemPosition() - if (scrollTo !in start..end) { - logD("Scrolling to new position") + val notInitialized = start == RecyclerView.NO_POSITION || end == RecyclerView.NO_POSITION + // When we scroll, we want to scroll to the almost-top so the user can see + // future songs instead of past songs. The way we have to do this however is + // dependent on where we have to scroll to get to the currently playing song. + if (notInitialized || scrollTo < start) { + // We need to scroll upwards, or initialize the scroll, no need to offset binding.queueRecycler.scrollToPosition(scrollTo) + } else if (scrollTo > end) { + // We need to scroll downwards, we need to offset by a screen of songs. + // This does have some error due to what the layout manager returns being + // somewhat mutable. This is considered okay. + binding.queueRecycler.scrollToPosition( + min(queue.lastIndex, scrollTo + (end - start))) } } queueModel.finishScrollTo() From bf56a50b59c5619508542a9c8e5ca39dd9b90004 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 30 Dec 2022 19:26:09 -0700 Subject: [PATCH 10/24] all: refactor associating class naming Standardize from/new companion method usage, Callback/Listener usage, and companion object visibility across the app. --- .../java/org/oxycblt/auxio/MainActivity.kt | 12 +---- .../auxio/detail/DetailAppBarLayout.kt | 5 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 2 +- .../detail/recycler/AlbumDetailAdapter.kt | 22 ++++---- .../detail/recycler/ArtistDetailAdapter.kt | 22 ++++---- .../auxio/detail/recycler/DetailAdapter.kt | 10 ++-- .../detail/recycler/GenreDetailAdapter.kt | 12 ++--- .../org/oxycblt/auxio/home/HomeFragment.kt | 10 ++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 2 +- .../home/fastscroll/FastScrollPopupView.kt | 4 +- .../home/fastscroll/FastScrollRecyclerView.kt | 4 +- .../auxio/home/list/AlbumListFragment.kt | 2 +- .../auxio/home/list/ArtistListFragment.kt | 2 +- .../auxio/home/list/GenreListFragment.kt | 2 +- .../auxio/home/list/SongListFragment.kt | 2 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 10 ++-- .../auxio/home/tabs/TabCustomizeDialog.kt | 4 +- .../list/recycler/PlayingIndicatorAdapter.kt | 4 +- .../recycler/SelectionIndicatorAdapter.kt | 4 +- .../auxio/list/recycler/ViewHolders.kt | 22 ++++---- .../list/selection/SelectionViewModel.kt | 2 +- .../main/java/org/oxycblt/auxio/music/Date.kt | 16 ++++-- .../java/org/oxycblt/auxio/music/Music.kt | 4 +- .../org/oxycblt/auxio/music/MusicStore.kt | 32 +++++------ .../org/oxycblt/auxio/music/MusicViewModel.kt | 2 +- .../music/extractor/MediaStoreExtractor.kt | 8 +-- .../music/extractor/MetadataExtractor.kt | 4 +- .../auxio/music/extractor/SeparatorsDialog.kt | 14 ++--- .../auxio/music/picker/ArtistChoiceAdapter.kt | 6 +-- .../auxio/music/picker/GenreChoiceAdapter.kt | 6 +-- .../auxio/music/picker/PickerViewModel.kt | 2 +- .../auxio/music/storage/DirectoryAdapter.kt | 6 +-- .../auxio/music/storage/MusicDirsDialog.kt | 2 +- .../org/oxycblt/auxio/music/system/Indexer.kt | 44 +++++++-------- .../auxio/music/system/IndexerService.kt | 8 +-- .../auxio/playback/PlaybackPanelFragment.kt | 1 - .../auxio/playback/PlaybackViewModel.kt | 2 +- .../auxio/playback/queue/QueueAdapter.kt | 10 ++-- .../auxio/playback/queue/QueueFragment.kt | 3 +- .../auxio/playback/queue/QueueViewModel.kt | 2 +- .../replaygain/ReplayGainAudioProcessor.kt | 13 +++-- .../auxio/playback/state/InternalPlayer.kt | 5 +- .../playback/state/PlaybackStateManager.kt | 54 +++++++++---------- .../playback/system/MediaSessionComponent.kt | 14 ++--- .../playback/system/NotificationComponent.kt | 4 +- .../auxio/playback/system/PlaybackService.kt | 8 +-- .../org/oxycblt/auxio/search/SearchAdapter.kt | 14 ++--- .../oxycblt/auxio/search/SearchViewModel.kt | 6 +-- .../oxycblt/auxio/settings/AboutFragment.kt | 10 ++-- .../org/oxycblt/auxio/settings/Settings.kt | 12 ++--- .../auxio/settings/prefs/IntListPreference.kt | 4 +- .../settings/prefs/IntListPreferenceDialog.kt | 7 ++- .../settings/prefs/PreferenceFragment.kt | 2 +- .../org/oxycblt/auxio/ui/AuxioAppBarLayout.kt | 4 +- .../oxycblt/auxio/ui/accent/AccentAdapter.kt | 12 +++-- .../auxio/ui/accent/AccentCustomizeDialog.kt | 4 +- .../oxycblt/auxio/widgets/WidgetComponent.kt | 2 +- 57 files changed, 253 insertions(+), 253 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index f33f9d2b1..25d40c62c 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -42,20 +42,12 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * * TODO: Custom language support * - * TODO: Add multi-select - * * TODO: Use proper material attributes (Not the weird dimen attributes I currently have) * * TODO: Migrate to material animation system * * TODO: Unit testing * - * TODO: Standardize from/new usage - * - * TODO: Standardize companion object usage - * - * TODO: Standardize callback/listener naming. - * * @author Alexander Capehart (OxygenCobalt) */ class MainActivity : AppCompatActivity() { @@ -146,7 +138,7 @@ class MainActivity : AppCompatActivity() { return true } - companion object { - private const val KEY_INTENT_USED = BuildConfig.APPLICATION_ID + ".key.FILE_INTENT_USED" + private companion object { + const val KEY_INTENT_USED = BuildConfig.APPLICATION_ID + ".key.FILE_INTENT_USED" } } 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 8e4a4ed6d..c523bb5a9 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailAppBarLayout.kt @@ -166,8 +166,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - companion object { - private val TOOLBAR_TITLE_TEXT_FIELD: Field by - lazyReflectedField(Toolbar::class, "mTitleTextView") + private companion object { + val TOOLBAR_TITLE_TEXT_FIELD: Field by lazyReflectedField(Toolbar::class, "mTitleTextView") } } 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 f350fcba1..f29a4999e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.* * @author Alexander Capehart (OxygenCobalt) */ class DetailViewModel(application: Application) : - AndroidViewModel(application), MusicStore.Callback { + AndroidViewModel(application), MusicStore.Listener { private val musicStore = MusicStore.getInstance() private val settings = Settings(application) 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 a61135a61..769db5c4f 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 @@ -67,9 +67,9 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.new(parent) - DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.new(parent) - AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.new(parent) + AlbumDetailViewHolder.VIEW_TYPE -> AlbumDetailViewHolder.from(parent) + DiscHeaderViewHolder.VIEW_TYPE -> DiscHeaderViewHolder.from(parent) + AlbumSongViewHolder.VIEW_TYPE -> AlbumSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) } @@ -88,9 +88,9 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene return super.isItemFullWidth(position) || item is Album || item is DiscHeader } - companion object { + private companion object { /** A comparator that can be used with DiffUtil. */ - private val DIFF_CALLBACK = + val DIFF_CALLBACK = object : SimpleItemCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { @@ -110,7 +110,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene } /** - * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [new] to + * A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -161,7 +161,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = AlbumDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ @@ -180,7 +180,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite /** * A [RecyclerView.ViewHolder] that displays a [DiscHeader] to delimit different disc groups. Use - * [new] to create an instance. + * [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : @@ -202,7 +202,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = DiscHeaderViewHolder(ItemDiscHeaderBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ @@ -215,7 +215,7 @@ private class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : } /** - * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Album]. Use [new] to + * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Album]. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -269,7 +269,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = AlbumSongViewHolder(ItemAlbumSongBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ 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 f8c213852..d9ab4fbe8 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 @@ -54,9 +54,9 @@ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listen override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.new(parent) - ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.new(parent) - ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.new(parent) + ArtistDetailViewHolder.VIEW_TYPE -> ArtistDetailViewHolder.from(parent) + ArtistAlbumViewHolder.VIEW_TYPE -> ArtistAlbumViewHolder.from(parent) + ArtistSongViewHolder.VIEW_TYPE -> ArtistSongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) } @@ -76,9 +76,9 @@ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listen return super.isItemFullWidth(position) || item is Artist } - companion object { + private companion object { /** A comparator that can be used with DiffUtil. */ - private val DIFF_CALLBACK = + val DIFF_CALLBACK = object : SimpleItemCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { return when { @@ -97,7 +97,7 @@ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listen } /** - * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [new] to + * A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -156,7 +156,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = ArtistDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ @@ -172,7 +172,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It } /** - * A [RecyclerView.ViewHolder] that displays an [Album] in the context of an [Artist]. Use [new] to + * A [RecyclerView.ViewHolder] that displays an [Album] in the context of an [Artist]. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -211,7 +211,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = ArtistAlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ @@ -224,7 +224,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite } /** - * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Artist]. Use [new] to + * A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Artist]. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -260,7 +260,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = ArtistSongViewHolder(ItemSongBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index dcb21b67a..8775f4b9b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -57,8 +57,8 @@ abstract class DetailAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.new(parent) - SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.new(parent) + HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent) + SortHeaderViewHolder.VIEW_TYPE -> SortHeaderViewHolder.from(parent) else -> error("Invalid item type $viewType") } @@ -109,7 +109,7 @@ abstract class DetailAdapter( fun onOpenSortMenu(anchor: View) } - companion object { + protected companion object { /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = object : SimpleItemCallback() { @@ -128,7 +128,7 @@ abstract class DetailAdapter( /** * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [Header] that adds a - * button opening a menu for sorting. Use [new] to create an instance. + * button opening a menu for sorting. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : @@ -157,7 +157,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = SortHeaderViewHolder(ItemSortHeaderBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ 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 6d6326dc7..fad27aa8a 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 @@ -54,9 +54,9 @@ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listene override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.new(parent) - ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.new(parent) - SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent) + GenreDetailViewHolder.VIEW_TYPE -> GenreDetailViewHolder.from(parent) + ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent) + SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent) else -> super.onCreateViewHolder(parent, viewType) } @@ -75,7 +75,7 @@ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listene return super.isItemFullWidth(position) || item is Genre } - companion object { + private companion object { val DIFF_CALLBACK = object : SimpleItemCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { @@ -94,7 +94,7 @@ class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listene } /** - * A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [new] to + * A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to * create an instance. * @author Alexander Capehart (OxygenCobalt) */ @@ -130,7 +130,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = GenreDetailViewHolder(ItemDetailBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ 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 7c9ace57b..64a5b2696 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -493,12 +493,10 @@ class HomeFragment : } } - companion object { - private val VP_RECYCLER_FIELD: Field by - lazyReflectedField(ViewPager2::class, "mRecyclerView") - private val RV_TOUCH_SLOP_FIELD: Field by - lazyReflectedField(RecyclerView::class, "mTouchSlop") - private const val KEY_LAST_TRANSITION_AXIS = + private companion object { + val VP_RECYCLER_FIELD: Field by lazyReflectedField(ViewPager2::class, "mRecyclerView") + val RV_TOUCH_SLOP_FIELD: Field by lazyReflectedField(RecyclerView::class, "mTouchSlop") + const val KEY_LAST_TRANSITION_AXIS = BuildConfig.APPLICATION_ID + ".key.LAST_TRANSITION_AXIS" } } 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 d6be75fd4..157736366 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -39,7 +39,7 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class HomeViewModel(application: Application) : - AndroidViewModel(application), Settings.Callback, MusicStore.Callback { + AndroidViewModel(application), Settings.Listener, MusicStore.Listener { private val musicStore = MusicStore.getInstance() private val settings = Settings(application, this) diff --git a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt index ba1de4483..da7cd4554 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/fastscroll/FastScrollPopupView.kt @@ -169,8 +169,8 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleRes: Int = 0) } } - companion object { + private companion object { // Pre-calculate sqrt(2) - private const val SQRT2 = 1.4142135f + const val SQRT2 = 1.4142135f } } 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 4add92475..48fc32617 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 @@ -524,7 +524,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr else -> 0 } - companion object { - private const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500 + private companion object { + const val AUTO_HIDE_SCROLLBAR_DELAY_MILLIS = 1500 } } 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 01d2637fa..ad91fdade 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 @@ -152,7 +152,7 @@ class AlbumListFragment : get() = differ.currentList override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - AlbumViewHolder.new(parent) + AlbumViewHolder.from(parent) override fun onBindViewHolder(holder: AlbumViewHolder, position: Int) { holder.bind(differ.currentList[position], listener) 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 29fecfd17..2dd51edf6 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 @@ -127,7 +127,7 @@ class ArtistListFragment : get() = differ.currentList override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ArtistViewHolder.new(parent) + ArtistViewHolder.from(parent) override fun onBindViewHolder(holder: ArtistViewHolder, position: Int) { holder.bind(differ.currentList[position], listener) 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 1019a85eb..fa3484d90 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 @@ -126,7 +126,7 @@ class GenreListFragment : get() = differ.currentList override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - GenreViewHolder.new(parent) + GenreViewHolder.from(parent) override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { holder.bind(differ.currentList[position], listener) 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 1a2cc40c0..0b85bcb5f 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 @@ -166,7 +166,7 @@ class SongListFragment : get() = differ.currentList override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - SongViewHolder.new(parent) + SongViewHolder.from(parent) override fun onBindViewHolder(holder: SongViewHolder, position: Int) { holder.bind(differ.currentList[position], listener) 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 10a459d5c..18025e3b1 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 @@ -38,7 +38,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter(), TabAd touchHelper.startDrag(viewHolder) } - companion object { - private const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" + private companion object { + const val KEY_TABS = BuildConfig.APPLICATION_ID + ".key.PENDING_TABS" } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt index 17b016575..ceb1fbf0b 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt @@ -115,7 +115,7 @@ abstract class PlayingIndicatorAdapter : RecyclerV abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) } - companion object { - private val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any() + private companion object { + val PAYLOAD_PLAYING_INDICATOR_CHANGED = Any() } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt index 29ecc2582..64036c7cd 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt @@ -77,7 +77,7 @@ abstract class SelectionIndicatorAdapter : abstract fun updateSelectionIndicator(isSelected: Boolean) } - companion object { - private val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any() + private companion object { + val PAYLOAD_SELECTION_INDICATOR_CHANGED = Any() } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 9f3d07805..f8fa090b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -35,7 +35,7 @@ import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.inflater /** - * A [RecyclerView.ViewHolder] that displays a [Song]. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [Song]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class SongViewHolder private constructor(private val binding: ItemSongBinding) : @@ -70,7 +70,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = SongViewHolder(ItemSongBinding.inflate(parent.context.inflater)) + fun from(parent: View) = SongViewHolder(ItemSongBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = @@ -82,7 +82,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : } /** - * A [RecyclerView.ViewHolder] that displays a [Album]. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [Album]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class AlbumViewHolder private constructor(private val binding: ItemParentBinding) : @@ -117,7 +117,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = AlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater)) + fun from(parent: View) = AlbumViewHolder(ItemParentBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = @@ -131,7 +131,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding } /** - * A [RecyclerView.ViewHolder] that displays a [Artist]. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [Artist]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : @@ -175,7 +175,8 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = ArtistViewHolder(ItemParentBinding.inflate(parent.context.inflater)) + fun from(parent: View) = + ArtistViewHolder(ItemParentBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = @@ -189,7 +190,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin } /** - * A [RecyclerView.ViewHolder] that displays a [Genre]. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [Genre]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class GenreViewHolder private constructor(private val binding: ItemParentBinding) : @@ -228,7 +229,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = GenreViewHolder(ItemParentBinding.inflate(parent.context.inflater)) + fun from(parent: View) = GenreViewHolder(ItemParentBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = @@ -240,7 +241,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding } /** - * A [RecyclerView.ViewHolder] that displays a [Header]. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [Header]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class HeaderViewHolder private constructor(private val binding: ItemHeaderBinding) : @@ -262,7 +263,8 @@ class HeaderViewHolder private constructor(private val binding: ItemHeaderBindin * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater)) + fun from(parent: View) = + HeaderViewHolder(ItemHeaderBinding.inflate(parent.context.inflater)) /** A comparator that can be used with DiffUtil. */ val DIFF_CALLBACK = diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index ae6b0bb60..efc654077 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.music.* * A [ViewModel] that manages the current selection. * @author Alexander Capehart (OxygenCobalt) */ -class SelectionViewModel : ViewModel(), MusicStore.Callback { +class SelectionViewModel : ViewModel(), MusicStore.Listener { private val musicStore = MusicStore.getInstance() private val _selected = MutableStateFlow(listOf()) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/Date.kt index 11a2378bd..510eebb84 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Date.kt @@ -29,7 +29,8 @@ import org.oxycblt.auxio.util.nonZeroOrNull * An ISO-8601/RFC 3339 Date. * * This class only encodes the timestamp spec and it's conversion to a human-readable date, without - * any other time management or validation. In general, this should only be used for display. + * any other time management or validation. In general, this should only be used for display. Use + * [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) */ @@ -112,12 +113,17 @@ class Date private constructor(private val tokens: List) : Comparable /** * A range of [Date]s. This is used in contexts where the [Date] of an item is derived from - * several sub-items and thus can have a "range" of release dates. - * @param min The earliest [Date] in the range. - * @param max The latest [Date] in the range. May be the same as [min]. + * several sub-items and thus can have a "range" of release dates. Use [from] to create an + * instance. * @author Alexander Capehart */ - class Range private constructor(val min: Date, val max: Date) : Comparable { + class Range + private constructor( + /** The earliest [Date] in the range. */ + val min: Date, + /** the latest [Date] in the range. May be the same as [min]. ] */ + val max: Date + ) : Comparable { /** * Resolve this instance into a human-readable date range. diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 8d86c5c78..50ad6b6d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -280,9 +280,9 @@ sealed class Music : Item { } } - companion object { + private companion object { /** Cached collator instance re-used with [makeCollationKeyImpl]. */ - private val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY } + val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY } } } 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 185cbb6ad..df3fad818 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -33,42 +33,42 @@ import org.oxycblt.auxio.music.storage.useQuery * @author Alexander Capehart (OxygenCobalt) */ class MusicStore private constructor() { - private val callbacks = mutableListOf() + private val listeners = mutableListOf() /** * The current [Library]. May be null if a [Library] has not been successfully loaded yet. This * can change, so it's highly recommended to not access this directly and instead rely on - * [Callback]. + * [Listener]. */ var library: Library? = null set(value) { field = value - for (callback in callbacks) { + for (callback in listeners) { callback.onLibraryChanged(library) } } /** - * Add a [Callback] to this instance. This can be used to receive changes in the music library. - * Will invoke all [Callback] methods to initialize the instance with the current state. - * @param callback The [Callback] to add. - * @see Callback + * Add a [Listener] to this instance. This can be used to receive changes in the music library. + * Will invoke all [Listener] methods to initialize the instance with the current state. + * @param listener The [Listener] to add. + * @see Listener */ @Synchronized - fun addCallback(callback: Callback) { - callback.onLibraryChanged(library) - callbacks.add(callback) + fun addCallback(listener: Listener) { + listener.onLibraryChanged(library) + listeners.add(listener) } /** - * Remove a [Callback] from this instance, preventing it from recieving any further updates. - * @param callback The [Callback] to remove. Does nothing if the [Callback] was never added in + * Remove a [Listener] from this instance, preventing it from recieving any further updates. + * @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in * the first place. - * @see Callback + * @see Listener */ @Synchronized - fun removeCallback(callback: Callback) { - callbacks.remove(callback) + fun removeCallback(listener: Listener) { + listeners.remove(listener) } /** @@ -167,7 +167,7 @@ class MusicStore private constructor() { } /** A listener for changes in the music library. */ - interface Callback { + interface Listener { /** * Called when the current [Library] has changed. * @param library The new [Library], or null if no [Library] has been loaded yet. 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 c115d080b..117495e7d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -26,7 +26,7 @@ import org.oxycblt.auxio.music.system.Indexer * A [ViewModel] providing data specific to the music loading process. * @author Alexander Capehart (OxygenCobalt) */ -class MusicViewModel : ViewModel(), Indexer.Callback { +class MusicViewModel : ViewModel(), Indexer.Listener { private val indexer = Indexer.getInstance() private val _indexerState = MutableStateFlow(null) diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index 8ec2adadd..93b9e2aa4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -322,12 +322,12 @@ abstract class MediaStoreExtractor( genreNamesMap[raw.mediaStoreId]?.let { raw.genreNames = listOf(it) } } - companion object { + private companion object { /** * The base selector that works across all versions of android. Does not exclude * directories. */ - private const val BASE_SELECTOR = "NOT ${MediaStore.Audio.Media.SIZE}=0" + const val BASE_SELECTOR = "NOT ${MediaStore.Audio.Media.SIZE}=0" /** * The album artist of a song. This column has existed since at least API 21, but until API @@ -335,13 +335,13 @@ abstract class MediaStoreExtractor( * versions that Auxio supports. */ @Suppress("InlinedApi") - private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST + const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST /** * The external volume. This naming has existed since API 21, but no constant existed for it * until API 29. This will work on all versions that Auxio supports. */ - @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL + @Suppress("InlinedApi") const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index f50b34d59..74910c959 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -116,8 +116,8 @@ class MetadataExtractor( } } - companion object { - private const val TASK_CAPACITY = 8 + private companion object { + const val TASK_CAPACITY = 8 } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index 12d8eae5d..c302f8646 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -94,13 +94,13 @@ class SeparatorsDialog : ViewBindingDialogFragment() { return separators } - companion object { - private val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS" + private companion object { + val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS" // TODO: Move these to a more "Correct" location? - private const val SEPARATOR_COMMA = ',' - private const val SEPARATOR_SEMICOLON = ';' - private const val SEPARATOR_SLASH = '/' - private const val SEPARATOR_PLUS = '+' - private const val SEPARATOR_AND = '&' + const val SEPARATOR_COMMA = ',' + const val SEPARATOR_SEMICOLON = ';' + const val SEPARATOR_SLASH = '/' + const val SEPARATOR_PLUS = '+' + const val SEPARATOR_AND = '&' } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt index 89c502d04..f2bbf540d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt @@ -39,7 +39,7 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) : override fun getItemCount() = artists.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - ArtistChoiceViewHolder.new(parent) + ArtistChoiceViewHolder.from(parent) override fun onBindViewHolder(holder: ArtistChoiceViewHolder, position: Int) = holder.bind(artists[position], listener) @@ -58,7 +58,7 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener) : /** * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Artist] item, for - * use with [ArtistChoiceAdapter]. Use [new] to create an instance. + * use with [ArtistChoiceAdapter]. Use [from] to create an instance. */ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogRecyclerView.ViewHolder(binding.root) { @@ -79,7 +79,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = ArtistChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt index 0d6fceae1..527c9e9d9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt @@ -39,7 +39,7 @@ class GenreChoiceAdapter(private val listener: ClickableListListener) : override fun getItemCount() = genres.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - GenreChoiceViewHolder.new(parent) + GenreChoiceViewHolder.from(parent) override fun onBindViewHolder(holder: GenreChoiceViewHolder, position: Int) = holder.bind(genres[position], listener) @@ -58,7 +58,7 @@ class GenreChoiceAdapter(private val listener: ClickableListListener) : /** * A [DialogRecyclerView.ViewHolder] that displays a smaller variant of a typical [Genre] item, for - * use with [GenreChoiceAdapter]. Use [new] to create an instance. + * use with [GenreChoiceAdapter]. Use [from] to create an instance. */ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : DialogRecyclerView.ViewHolder(binding.root) { @@ -79,7 +79,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = GenreChoiceViewHolder(ItemPickerChoiceBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index 31939e371..825ed4061 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -28,7 +28,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * contain the music themselves and then exit if the library changes. * @author Alexander Capehart (OxygenCobalt) */ -class PickerViewModel : ViewModel(), MusicStore.Callback { +class PickerViewModel : ViewModel(), MusicStore.Listener { private val musicStore = MusicStore.getInstance() private val _currentItem = MutableStateFlow(null) diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt index f0474c364..dcdedc0ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/DirectoryAdapter.kt @@ -41,7 +41,7 @@ class DirectoryAdapter(private val listener: Listener) : override fun getItemCount() = dirs.size override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - MusicDirViewHolder.new(parent) + MusicDirViewHolder.from(parent) override fun onBindViewHolder(holder: MusicDirViewHolder, position: Int) = holder.bind(dirs[position], listener) @@ -86,7 +86,7 @@ class DirectoryAdapter(private val listener: Listener) : } /** - * A [RecyclerView.Recycler] that displays a [Directory]. Use [new] to create an instance. + * A [RecyclerView.Recycler] that displays a [Directory]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : @@ -107,7 +107,7 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = + fun from(parent: View) = MusicDirViewHolder(ItemMusicDirBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt index 90507e915..a0308a91e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt @@ -176,7 +176,7 @@ class MusicDirsDialog : private fun isUiModeInclude(binding: DialogMusicDirsBinding) = binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include - companion object { + private companion object { const val KEY_PENDING_DIRS = BuildConfig.APPLICATION_ID + ".key.PENDING_DIRS" const val KEY_PENDING_MODE = BuildConfig.APPLICATION_ID + ".key.SHOULD_INCLUDE" } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 028fee788..2e13d05d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -54,7 +54,7 @@ class Indexer private constructor() { private var lastResponse: Response? = null private var indexingState: Indexing? = null private var controller: Controller? = null - private var callback: Callback? = null + private var listener: Listener? = null /** Whether music loading is occurring or not. */ val isIndexing: Boolean @@ -71,7 +71,7 @@ class Indexer private constructor() { /** * Register a [Controller] for this instance. This instance will handle any commands to start * the music loading process. There can be only one [Controller] at a time. Will invoke all - * [Callback] methods to initialize the instance with the current state. + * [Listener] methods to initialize the instance with the current state. * @param controller The [Controller] to register. Will do nothing if already registered. */ @Synchronized @@ -105,14 +105,14 @@ class Indexer private constructor() { } /** - * Register the [Callback] for this instance. This can be used to receive rapid-fire updates to - * the current music loading state. There can be only one [Callback] at a time. Will invoke all - * [Callback] methods to initialize the instance with the current state. - * @param callback The [Callback] to add. + * Register the [Listener] for this instance. This can be used to receive rapid-fire updates to + * the current music loading state. There can be only one [Listener] at a time. Will invoke all + * [Listener] methods to initialize the instance with the current state. + * @param listener The [Listener] to add. */ @Synchronized - fun registerCallback(callback: Callback) { - if (BuildConfig.DEBUG && this.callback != null) { + fun registerCallback(listener: Listener) { + if (BuildConfig.DEBUG && this.listener != null) { logW("Listener is already registered") return } @@ -120,24 +120,24 @@ class Indexer private constructor() { // Initialize the listener with the current state. val currentState = indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) } - callback.onIndexerStateChanged(currentState) - this.callback = callback + listener.onIndexerStateChanged(currentState) + this.listener = listener } /** - * Unregister a [Callback] from this instance, preventing it from recieving any further updates. - * @param callback The [Callback] to unregister. Must be the current [Callback]. Does nothing if - * invoked by another [Callback] implementation. - * @see Callback + * Unregister a [Listener] from this instance, preventing it from recieving any further updates. + * @param listener The [Listener] to unregister. Must be the current [Listener]. Does nothing if + * invoked by another [Listener] implementation. + * @see Listener */ @Synchronized - fun unregisterCallback(callback: Callback) { - if (BuildConfig.DEBUG && this.callback !== callback) { + fun unregisterCallback(listener: Listener) { + if (BuildConfig.DEBUG && this.listener !== listener) { logW("Given controller did not match current controller") return } - this.callback = null + this.listener = null } /** @@ -388,7 +388,7 @@ class Indexer private constructor() { val state = indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) } controller?.onIndexerStateChanged(state) - callback?.onIndexerStateChanged(state) + listener?.onIndexerStateChanged(state) } /** @@ -411,7 +411,7 @@ class Indexer private constructor() { // Signal that the music loading process has been completed. val state = State.Complete(response) controller?.onIndexerStateChanged(state) - callback?.onIndexerStateChanged(state) + listener?.onIndexerStateChanged(state) } } } @@ -476,10 +476,10 @@ class Indexer private constructor() { * A listener for rapid-fire changes in the music loading state. * * This is only useful for code that absolutely must show the current loading process. - * Otherwise, [MusicStore.Callback] is highly recommended due to it's updates only consisting of + * Otherwise, [MusicStore.Listener] is highly recommended due to it's updates only consisting of * the [MusicStore.Library]. */ - interface Callback { + interface Listener { /** * Called when the current state of the Indexer changed. * @@ -495,7 +495,7 @@ class Indexer private constructor() { * Context that runs the music loading process. Implementations should be capable of running the * background for long periods of time without android killing the process. */ - interface Controller : Callback { + interface Controller : Listener { /** * Called when a new music loading process was requested. Implementations should forward * this to [index]. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index b024890eb..35f3e7f50 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -54,7 +54,7 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart (OxygenCobalt) */ -class IndexerService : Service(), Indexer.Controller, Settings.Callback { +class IndexerService : Service(), Indexer.Controller, Settings.Listener { private val indexer = Indexer.getInstance() private val musicStore = MusicStore.getInstance() private val playbackManager = PlaybackStateManager.getInstance() @@ -287,8 +287,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Callback { } } - companion object { - private const val WAKELOCK_TIMEOUT_MS = 60 * 1000L - private const val REINDEX_DELAY_MS = 500L + private companion object { + const val WAKELOCK_TIMEOUT_MS = 60 * 1000L + const val REINDEX_DELAY_MS = 500L } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 37bd179e7..fd9ebc425 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -127,7 +127,6 @@ class PlaybackPanelFragment : when (item.itemId) { R.id.action_open_equalizer -> { // Launch the system equalizer app, if possible. - // TODO: Move this to a utility val equalizerIntent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL) // Provide audio session ID so equalizer can show options for this app 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 42a37159b..ab12b5cd8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.util.context * @author Alexander Capehart (OxygenCobalt) */ class PlaybackViewModel(application: Application) : - AndroidViewModel(application), PlaybackStateManager.Callback { + AndroidViewModel(application), PlaybackStateManager.Listener { private val settings = Settings(application) private val playbackManager = PlaybackStateManager.getInstance() private var lastPositionJob: Job? = null 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 d8d761dc5..cc2f7c3d1 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 @@ -52,7 +52,7 @@ class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter(), QueueAdapter. val lmm = binding.queueRecycler.layoutManager as LinearLayoutManager val start = lmm.findFirstCompletelyVisibleItemPosition() val end = lmm.findLastCompletelyVisibleItemPosition() - val notInitialized = start == RecyclerView.NO_POSITION || end == RecyclerView.NO_POSITION + val notInitialized = + start == RecyclerView.NO_POSITION || end == RecyclerView.NO_POSITION // When we scroll, we want to scroll to the almost-top so the user can see // future songs instead of past songs. The way we have to do this however is // dependent on where we have to scroll to get to the currently playing song. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index 6502bef8c..f35019fde 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager * * @author Alexander Capehart (OxygenCobalt) */ -class QueueViewModel : ViewModel(), PlaybackStateManager.Callback { +class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { private val playbackManager = PlaybackStateManager.getInstance() private val _queue = MutableStateFlow(listOf()) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index f0ee563f9..6ff100fba 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -281,12 +281,11 @@ class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { private data class GainTag(val key: String, val value: Float) // TODO: Try to phase this out - companion object { - private const val TAG_RG_TRACK = "replaygain_track_gain" - private const val TAG_RG_ALBUM = "replaygain_album_gain" - private const val R128_TRACK = "r128_track_gain" - private const val R128_ALBUM = "r128_album_gain" - - private val REPLAY_GAIN_TAGS = arrayOf(TAG_RG_TRACK, TAG_RG_ALBUM, R128_ALBUM, R128_TRACK) + private companion object { + const val TAG_RG_TRACK = "replaygain_track_gain" + const val TAG_RG_ALBUM = "replaygain_album_gain" + const val R128_TRACK = "r128_track_gain" + const val R128_ALBUM = "r128_album_gain" + val REPLAY_GAIN_TAGS = arrayOf(TAG_RG_TRACK, TAG_RG_ALBUM, R128_ALBUM, R128_TRACK) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt index b7c533b85..7c2487458 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/InternalPlayer.kt @@ -85,6 +85,9 @@ interface InternalPlayer { data class Open(val uri: Uri) : Action() } + /** + * A representation of the current state of audio playback. Use [from] to create an instance. + */ class State private constructor( /** Whether the player is actively playing audio or set to play audio in the future. */ @@ -157,7 +160,7 @@ interface InternalPlayer { * @param isAdvancing Whether the player is actively playing audio in this moment. * @param positionMs The current position of the player. */ - fun new(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) = + fun from(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) = State( isPlaying, // Minor sanity check: Make sure that advancing can't occur if already paused. 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 f9fe57c12..18bbeea0f 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 @@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.playback.state.PlaybackStateManager.Callback +import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE @@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logW * - If you want to use the playback state with the ExoPlayer instance or system-side things, use * [org.oxycblt.auxio.playback.system.PlaybackService]. * - * Internal consumers should usually use [Callback], however the component that manages the player + * Internal consumers should usually use [Listener], however the component that manages the player * itself should instead use [InternalPlayer]. * * All access should be done with [PlaybackStateManager.getInstance]. @@ -54,7 +54,7 @@ import org.oxycblt.auxio.util.logW */ class PlaybackStateManager private constructor() { private val musicStore = MusicStore.getInstance() - private val callbacks = mutableListOf() + private val listeners = mutableListOf() private var internalPlayer: InternalPlayer? = null private var pendingAction: InternalPlayer.Action? = null private var isInitialized = false @@ -74,7 +74,7 @@ class PlaybackStateManager private constructor() { var index = -1 private set /** The current [InternalPlayer] state. */ - var playerState = InternalPlayer.State.new(isPlaying = false, isAdvancing = false, 0) + var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0) private set /** The current [RepeatMode] */ var repeatMode = RepeatMode.NONE @@ -93,32 +93,32 @@ class PlaybackStateManager private constructor() { get() = internalPlayer?.audioSessionId /** - * Add a [Callback] to this instance. This can be used to receive changes in the playback state. - * Will immediately invoke [Callback] methods to initialize the instance with the current state. - * @param callback The [Callback] to add. - * @see Callback + * Add a [Listener] to this instance. This can be used to receive changes in the playback state. + * Will immediately invoke [Listener] methods to initialize the instance with the current state. + * @param listener The [Listener] to add. + * @see Listener */ @Synchronized - fun addCallback(callback: Callback) { + fun addCallback(listener: Listener) { if (isInitialized) { - callback.onNewPlayback(index, queue, parent) - callback.onRepeatChanged(repeatMode) - callback.onShuffledChanged(isShuffled) - callback.onStateChanged(playerState) + listener.onNewPlayback(index, queue, parent) + listener.onRepeatChanged(repeatMode) + listener.onShuffledChanged(isShuffled) + listener.onStateChanged(playerState) } - callbacks.add(callback) + listeners.add(listener) } /** - * Remove a [Callback] from this instance, preventing it from recieving any further updates. - * @param callback The [Callback] to remove. Does nothing if the [Callback] was never added in + * Remove a [Listener] from this instance, preventing it from recieving any further updates. + * @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in * the first place. - * @see Callback + * @see Listener */ @Synchronized - fun removeCallback(callback: Callback) { - callbacks.remove(callback) + fun removeCallback(listener: Listener) { + listeners.remove(listener) } /** @@ -586,43 +586,43 @@ class PlaybackStateManager private constructor() { // --- CALLBACKS --- private fun notifyIndexMoved() { - for (callback in callbacks) { + for (callback in listeners) { callback.onIndexMoved(index) } } private fun notifyQueueChanged() { - for (callback in callbacks) { + for (callback in listeners) { callback.onQueueChanged(queue) } } private fun notifyQueueReworked() { - for (callback in callbacks) { + for (callback in listeners) { callback.onQueueReworked(index, queue) } } private fun notifyNewPlayback() { - for (callback in callbacks) { + for (callback in listeners) { callback.onNewPlayback(index, queue, parent) } } private fun notifyStateChanged() { - for (callback in callbacks) { + for (callback in listeners) { callback.onStateChanged(playerState) } } private fun notifyRepeatModeChanged() { - for (callback in callbacks) { + for (callback in listeners) { callback.onRepeatChanged(repeatMode) } } private fun notifyShuffledChanged() { - for (callback in callbacks) { + for (callback in listeners) { callback.onShuffledChanged(isShuffled) } } @@ -631,7 +631,7 @@ class PlaybackStateManager private constructor() { * The interface for receiving updates from [PlaybackStateManager]. Add the listener to * [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback]. */ - interface Callback { + interface Listener { /** * Called when the position of the currently playing item has changed, changing the current * [Song], but no other queue attribute has changed. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index c3d8041f7..c34bdb628 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -43,11 +43,11 @@ import org.oxycblt.auxio.util.logD * A component that mirrors the current playback state into the [MediaSessionCompat] and * [NotificationComponent]. * @param context [Context] required to initialize components. - * @param callback [Callback] to forward notification updates to. + * @param listener [Listener] to forward notification updates to. * @author Alexander Capehart (OxygenCobalt) */ -class MediaSessionComponent(private val context: Context, private val callback: Callback) : - MediaSessionCompat.Callback(), PlaybackStateManager.Callback, Settings.Callback { +class MediaSessionComponent(private val context: Context, private val listener: Listener) : + MediaSessionCompat.Callback(), PlaybackStateManager.Listener, Settings.Listener { private val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true @@ -113,7 +113,7 @@ class MediaSessionComponent(private val context: Context, private val callback: invalidateSessionState() notification.updatePlaying(playbackManager.playerState.isPlaying) if (!provider.isBusy) { - callback.onPostNotification(notification) + listener.onPostNotification(notification) } } @@ -306,7 +306,7 @@ class MediaSessionComponent(private val context: Context, private val callback: val metadata = builder.build() mediaSession.setMetadata(metadata) notification.updateMetadata(metadata) - callback.onPostNotification(notification) + listener.onPostNotification(notification) } }) } @@ -393,12 +393,12 @@ class MediaSessionComponent(private val context: Context, private val callback: } if (!provider.isBusy) { - callback.onPostNotification(notification) + listener.onPostNotification(notification) } } /** An interface for handling changes in the notification configuration. */ - interface Callback { + interface Listener { /** * Called when the [NotificationComponent] changes, requiring it to be re-posed. * @param notification The new [NotificationComponent]. diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt index 823efd20f..21cb16676 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/NotificationComponent.kt @@ -148,9 +148,9 @@ class NotificationComponent(private val context: Context, sessionToken: MediaSes iconRes, actionName, context.newBroadcastPendingIntent(actionName)) .build() - companion object { + private companion object { /** Notification channel used by solely the playback notification. */ - private val CHANNEL_INFO = + val CHANNEL_INFO = ChannelInfo( id = BuildConfig.APPLICATION_ID + ".channel.PLAYBACK", nameRes = R.string.lbl_playback) 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 d35555cfe..82f60bb51 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 @@ -79,9 +79,9 @@ class PlaybackService : Service(), Player.Listener, InternalPlayer, - MediaSessionComponent.Callback, - Settings.Callback, - MusicStore.Callback { + MediaSessionComponent.Listener, + Settings.Listener, + MusicStore.Listener { // Player components private lateinit var player: ExoPlayer private lateinit var replayGainProcessor: ReplayGainAudioProcessor @@ -217,7 +217,7 @@ class PlaybackService : get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD override fun getState(durationMs: Long) = - InternalPlayer.State.new( + InternalPlayer.State.from( player.playWhenReady, player.isPlaying, // The position value can be below zero or past the expected duration, make 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 3f9cca499..a1260859b 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -51,11 +51,11 @@ class SearchAdapter(private val listener: SelectableListListener) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { - SongViewHolder.VIEW_TYPE -> SongViewHolder.new(parent) - AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.new(parent) - ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.new(parent) - GenreViewHolder.VIEW_TYPE -> GenreViewHolder.new(parent) - HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.new(parent) + SongViewHolder.VIEW_TYPE -> SongViewHolder.from(parent) + AlbumViewHolder.VIEW_TYPE -> AlbumViewHolder.from(parent) + ArtistViewHolder.VIEW_TYPE -> ArtistViewHolder.from(parent) + GenreViewHolder.VIEW_TYPE -> GenreViewHolder.from(parent) + HeaderViewHolder.VIEW_TYPE -> HeaderViewHolder.from(parent) else -> error("Invalid item type $viewType") } @@ -81,9 +81,9 @@ class SearchAdapter(private val listener: SelectableListListener) : differ.submitList(newList, callback) } - companion object { + private companion object { /** A comparator that can be used with DiffUtil. */ - private val DIFF_CALLBACK = + val DIFF_CALLBACK = object : SimpleItemCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item) = when { 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 a088e32e7..0be8d3926 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class SearchViewModel(application: Application) : - AndroidViewModel(application), MusicStore.Callback { + AndroidViewModel(application), MusicStore.Listener { private val musicStore = MusicStore.getInstance() private val settings = Settings(context) private var lastQuery: String? = null @@ -212,11 +212,11 @@ class SearchViewModel(application: Application) : search(lastQuery) } - companion object { + private companion object { /** * Converts the output of [Normalizer] to remove any junk characters added by it's * replacements. */ - private val NORMALIZATION_SANITIZE_REGEX = Regex("\\p{InCombiningDiacriticalMarks}+") + val NORMALIZATION_SANITIZE_REGEX = Regex("\\p{InCombiningDiacriticalMarks}+") } } 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 81c52a22a..bb4d9109b 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -152,14 +152,14 @@ class AboutFragment : ViewBindingFragment() { startActivity(chooserIntent) } - companion object { + private companion object { /** The URL to the source code. */ - private const val LINK_SOURCE = "https://github.com/OxygenCobalt/Auxio" + const val LINK_SOURCE = "https://github.com/OxygenCobalt/Auxio" /** The URL to the app wiki. */ - private const val LINK_WIKI = "$LINK_SOURCE/wiki" + const val LINK_WIKI = "$LINK_SOURCE/wiki" /** The URL to the licenses wiki page. */ - private const val LINK_LICENSES = "$LINK_WIKI/Licenses" + const val LINK_LICENSES = "$LINK_WIKI/Licenses" /** The URL to the app author. */ - private const val LINK_AUTHOR = "https://github.com/OxygenCobalt" + const val LINK_AUTHOR = "https://github.com/OxygenCobalt" } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 50887fede..64bebac04 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -44,12 +44,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * mutability * @author Alexander Capehart (OxygenCobalt) */ -class Settings(private val context: Context, private val callback: Callback? = null) : +class Settings(private val context: Context, private val listener: Listener? = null) : SharedPreferences.OnSharedPreferenceChangeListener { private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) init { - if (callback != null) { + if (listener != null) { inner.registerOnSharedPreferenceChangeListener(this) } } @@ -154,7 +154,7 @@ class Settings(private val context: Context, private val callback: Callback? = n } /** - * Release this instance and any callbacks held by it. This is not needed if no [Callback] was + * Release this instance and any callbacks held by it. This is not needed if no [Listener] was * originally attached. */ fun release() { @@ -162,11 +162,11 @@ class Settings(private val context: Context, private val callback: Callback? = n } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - unlikelyToBeNull(callback).onSettingChanged(key) + unlikelyToBeNull(listener).onSettingChanged(key) } - /** Simplified callback for settings changes. */ - interface Callback { + /** Simplified listener for settings changes. */ + interface Listener { // TODO: Refactor this lifecycle /** * Called when a setting has changed. diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt index a25d7a718..8afa0ec8d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreference.kt @@ -162,8 +162,8 @@ constructor( } } - companion object { - private val PREFERENCE_DEFAULT_VALUE_FIELD: Field by + private companion object { + val PREFERENCE_DEFAULT_VALUE_FIELD: Field by lazyReflectedField(Preference::class, "mDefaultValue") } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt index b66bfe309..72f8f3383 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/IntListPreferenceDialog.kt @@ -24,7 +24,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R /** - * The companion dialog to [IntListPreference]. Use [new] to create an instance. + * The companion dialog to [IntListPreference]. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { @@ -62,11 +62,10 @@ class IntListPreferenceDialog : PreferenceDialogFragmentCompat() { * @param preference The [IntListPreference] to display. * @return A new instance. */ - fun new(preference: IntListPreference): IntListPreferenceDialog { - return IntListPreferenceDialog().apply { + fun from(preference: IntListPreference) = + IntListPreferenceDialog().apply { // Populate the key field required by PreferenceDialogFragmentCompat. arguments = Bundle().apply { putString(ARG_KEY, preference.key) } } - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt index da944e309..4a3d378c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/prefs/PreferenceFragment.kt @@ -76,7 +76,7 @@ class PreferenceFragment : PreferenceFragmentCompat() { is IntListPreference -> { // Copy the built-in preference dialog launching code into our project so // we can automatically use the provided preference class. - val dialog = IntListPreferenceDialog.new(preference) + val dialog = IntListPreferenceDialog.from(preference) dialog.setTargetFragment(this, 0) dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt index 91e122dad..b652b438a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/AuxioAppBarLayout.kt @@ -136,8 +136,8 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr } } - companion object { + private companion object { /** @see AppBarLayout.BaseBehavior.MAX_OFFSET_ANIMATION_DURATION */ - private const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600 + const val APP_BAR_LAYOUT_MAX_OFFSET_ANIMATION_DURATION = 600 } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index 790e9ca54..8ac060be5 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -41,7 +41,8 @@ class AccentAdapter(private val listener: ClickableListListener) : override fun getItemCount() = Accent.MAX - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = AccentViewHolder.new(parent) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + AccentViewHolder.from(parent) override fun onBindViewHolder(holder: AccentViewHolder, position: Int) = throw NotImplementedError() @@ -75,13 +76,13 @@ class AccentAdapter(private val listener: ClickableListListener) : notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED) } - companion object { - private val PAYLOAD_SELECTION_CHANGED = Any() + private companion object { + val PAYLOAD_SELECTION_CHANGED = Any() } } /** - * A [RecyclerView.ViewHolder] that displays an [Accent] choice. Use [new] to create an instance. + * A [RecyclerView.ViewHolder] that displays an [Accent] choice. Use [from] to create an instance. * @author Alexander Capehart (OxygenCobalt) */ class AccentViewHolder private constructor(private val binding: ItemAccentBinding) : @@ -124,6 +125,7 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin * @param parent The parent to inflate this instance from. * @return A new instance. */ - fun new(parent: View) = AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) + fun from(parent: View) = + AccentViewHolder(ItemAccentBinding.inflate(parent.context.inflater)) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index a90ce9720..c14e38985 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -85,7 +85,7 @@ class AccentCustomizeDialog : accentAdapter.setSelectedAccent(item) } - companion object { - private const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT" + private companion object { + const val KEY_PENDING_ACCENT = BuildConfig.APPLICATION_ID + ".key.PENDING_ACCENT" } } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index d51a34530..b6e9f437c 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class WidgetComponent(private val context: Context) : - PlaybackStateManager.Callback, Settings.Callback { + PlaybackStateManager.Listener, Settings.Listener { private val playbackManager = PlaybackStateManager.getInstance() private val settings = Settings(context, this) private val widgetProvider = WidgetProvider() From 493b0a9f32feb68eb5f69622a73cc7ba668cd0f7 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 31 Dec 2022 11:11:09 -0700 Subject: [PATCH 11/24] all: rework context-dependent object use Rework some of the taped together ways context-dependent objects were replied on in-app, such as removing redundant constructs and extremely hacky lifecycle mechanisms. --- CHANGELOG.md | 1 - .../java/org/oxycblt/auxio/MainFragment.kt | 6 +-- .../oxycblt/auxio/detail/DetailViewModel.kt | 4 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 28 +++++++------ .../org/oxycblt/auxio/home/HomeViewModel.kt | 16 +++++--- .../auxio/home/tabs/TabCustomizeDialog.kt | 16 +++----- .../list/selection/SelectionViewModel.kt | 4 +- .../org/oxycblt/auxio/music/MusicStore.kt | 4 +- .../org/oxycblt/auxio/music/MusicViewModel.kt | 4 +- .../auxio/music/extractor/SeparatorsDialog.kt | 8 ++-- .../auxio/music/picker/PickerViewModel.kt | 2 +- .../auxio/music/storage/MusicDirsDialog.kt | 31 ++++++++++---- .../org/oxycblt/auxio/music/system/Indexer.kt | 4 +- .../auxio/music/system/IndexerService.kt | 11 +++-- .../auxio/playback/PlaybackPanelFragment.kt | 22 ++++++---- .../auxio/playback/PlaybackViewModel.kt | 4 +- .../auxio/playback/queue/QueueFragment.kt | 11 ++--- .../auxio/playback/queue/QueueViewModel.kt | 4 +- .../replaygain/PreAmpCustomizeDialog.kt | 7 +--- .../playback/state/PlaybackStateManager.kt | 6 +-- .../playback/system/MediaSessionComponent.kt | 21 ++++++---- .../auxio/playback/system/PlaybackService.kt | 19 +++++---- .../oxycblt/auxio/search/SearchFragment.kt | 26 +++++++----- .../oxycblt/auxio/search/SearchViewModel.kt | 4 +- .../org/oxycblt/auxio/settings/Settings.kt | 41 +++++++------------ .../auxio/ui/ViewBindingDialogFragment.kt | 39 ------------------ .../oxycblt/auxio/ui/ViewBindingFragment.kt | 39 ------------------ .../auxio/ui/accent/AccentCustomizeDialog.kt | 5 +-- .../oxycblt/auxio/widgets/WidgetComponent.kt | 15 ++++--- prebuild.py | 5 ++- 30 files changed, 175 insertions(+), 232 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e4cbaea..e979d1af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,6 @@ audio focus was lost #### What's Changed - Ignore MediaStore tags is now Auxio's default and unchangeable behavior. The option has been removed. -- Removed the "Play from genre" option in the library/detail playback mode settings+ - "Use alternate notification action" is now "Custom notification action" - "Show covers" and "Ignore MediaStore covers" have been unified into "Album covers" diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index b692ae477..833b85a16 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -61,10 +61,8 @@ class MainFragment : private val selectionModel: SelectionViewModel by activityViewModels() private val callback = DynamicBackPressedCallback() private var lastInsets: WindowInsets? = null + private var elevationNormal = 0f private var initialNavDestinationChange = true - private val elevationNormal: Float by lifecycleObject { binding -> - binding.context.getDimen(R.dimen.elevation_normal) - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -76,6 +74,8 @@ class MainFragment : override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) + + elevationNormal = binding.context.getDimen(R.dimen.elevation_normal) // --- UI SETUP --- val context = requireActivity() 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 f29a4999e..4e6e60005 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -130,11 +130,11 @@ class DetailViewModel(application: Application) : } init { - musicStore.addCallback(this) + musicStore.addListener(this) } override fun onCleared() { - musicStore.removeCallback(this) + musicStore.removeListener(this) } override fun onLibraryChanged(library: MusicStore.Library?) { 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 64a5b2696..d83ee3618 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -72,17 +72,7 @@ class HomeFragment : private val homeModel: HomeViewModel by androidActivityViewModels() private val musicModel: MusicViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels() - - // lifecycleObject builds this in the creation step, so doing this is okay. - private val storagePermissionLauncher: ActivityResultLauncher by lifecycleObject { - registerForActivityResult(ActivityResultContracts.RequestPermission()) { - musicModel.refresh() - } - } - - private val sortItem: MenuItem by lifecycleObject { binding -> - binding.homeToolbar.menu.findItem(R.id.submenu_sorting) - } + private var storagePermissionLauncher: ActivityResultLauncher? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -105,6 +95,12 @@ class HomeFragment : override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) + // Have to set up the permission launcher before the view is shown + storagePermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + musicModel.refresh() + } + // --- UI SETUP --- binding.homeAppbar.addOnOffsetChangedListener(this) binding.homeToolbar.setOnMenuItemClickListener(this) @@ -171,6 +167,7 @@ class HomeFragment : override fun onDestroyBinding(binding: FragmentHomeBinding) { super.onDestroyBinding(binding) + storagePermissionLauncher = null binding.homeAppbar.removeOnOffsetChangedListener(this) binding.homeToolbar.setOnMenuItemClickListener(null) } @@ -285,7 +282,9 @@ class HomeFragment : } } - val sortMenu = requireNotNull(sortItem.subMenu) + val sortMenu = + unlikelyToBeNull( + requireBinding().homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu) val toHighlight = homeModel.getSortForTab(tabMode) for (option in sortMenu) { @@ -374,7 +373,10 @@ class HomeFragment : visibility = View.VISIBLE text = context.getString(R.string.lbl_grant) setOnClickListener { - storagePermissionLauncher.launch(Indexer.PERMISSION_READ_AUDIO) + requireNotNull(storagePermissionLauncher) { + "Permission launcher was not available" + } + .launch(Indexer.PERMISSION_READ_AUDIO) } } } 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 157736366..f9e88e3ef 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.home import android.app.Application +import android.content.SharedPreferences import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -39,9 +40,11 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class HomeViewModel(application: Application) : - AndroidViewModel(application), Settings.Listener, MusicStore.Listener { + AndroidViewModel(application), + MusicStore.Listener, + SharedPreferences.OnSharedPreferenceChangeListener { private val musicStore = MusicStore.getInstance() - private val settings = Settings(application, this) + private val settings = Settings(application) private val _songsList = MutableStateFlow(listOf()) /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ @@ -91,13 +94,14 @@ class HomeViewModel(application: Application) : val isFastScrolling: StateFlow = _isFastScrolling init { - musicStore.addCallback(this) + musicStore.addListener(this) + settings.addListener(this) } override fun onCleared() { super.onCleared() - musicStore.removeCallback(this) - settings.release() + musicStore.removeListener(this) + settings.removeListener(this) } override fun onLibraryChanged(library: MusicStore.Library?) { @@ -119,7 +123,7 @@ class HomeViewModel(application: Application) : } } - override fun onSettingChanged(key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { when (key) { context.getString(R.string.set_key_lib_tabs) -> { // Tabs changed, update the current tabs and set up a re-create event. 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 c9da12001..063086b15 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 @@ -28,7 +28,6 @@ import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment -import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD /** @@ -36,12 +35,8 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAdapter.Listener { - private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } - private val tabAdapter = TabAdapter(this) - private val touchHelper: ItemTouchHelper by lifecycleObject { - ItemTouchHelper(TabDragCallback(tabAdapter)) - } + private var touchHelper: ItemTouchHelper? = null override fun onCreateBinding(inflater: LayoutInflater) = DialogTabsBinding.inflate(inflater) @@ -50,13 +45,13 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> logD("Committing tab changes") - settings.libTabs = tabAdapter.tabs + Settings(requireContext()).libTabs = tabAdapter.tabs } .setNegativeButton(R.string.lbl_cancel, null) } override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { - var tabs = settings.libTabs + var tabs = Settings(requireContext()).libTabs // Try to restore a pending tab configuration that was saved prior. if (savedInstanceState != null) { val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS)) @@ -69,7 +64,8 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd tabAdapter.submitTabs(tabs) binding.tabRecycler.apply { adapter = tabAdapter - touchHelper.attachToRecyclerView(this) + touchHelper = + ItemTouchHelper(TabDragCallback(tabAdapter)).also { it.attachToRecyclerView(this) } } } @@ -105,7 +101,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd } override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { - touchHelper.startDrag(viewHolder) + requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder) } private companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index efc654077..754e8ca08 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -35,7 +35,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener { get() = _selected init { - musicStore.addCallback(this) + musicStore.addListener(this) } override fun onLibraryChanged(library: MusicStore.Library?) { @@ -58,7 +58,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener { override fun onCleared() { super.onCleared() - musicStore.removeCallback(this) + musicStore.removeListener(this) } /** 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 df3fad818..713163cbc 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -55,7 +55,7 @@ class MusicStore private constructor() { * @see Listener */ @Synchronized - fun addCallback(listener: Listener) { + fun addListener(listener: Listener) { listener.onLibraryChanged(library) listeners.add(listener) } @@ -67,7 +67,7 @@ class MusicStore private constructor() { * @see Listener */ @Synchronized - fun removeCallback(listener: Listener) { + fun removeListener(listener: Listener) { listeners.remove(listener) } 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 117495e7d..bb6205dc5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -39,11 +39,11 @@ class MusicViewModel : ViewModel(), Indexer.Listener { get() = _statistics init { - indexer.registerCallback(this) + indexer.registerListener(this) } override fun onCleared() { - indexer.unregisterCallback(this) + indexer.unregisterListener(this) } override fun onIndexerStateChanged(state: Indexer.State?) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt index c302f8646..96259f7c5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/SeparatorsDialog.kt @@ -27,7 +27,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment -import org.oxycblt.auxio.util.context /** * A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to @@ -35,8 +34,6 @@ import org.oxycblt.auxio.util.context * @author Alexander Capehart (OxygenCobalt) */ class SeparatorsDialog : ViewBindingDialogFragment() { - private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } - override fun onCreateBinding(inflater: LayoutInflater) = DialogSeparatorsBinding.inflate(inflater) @@ -45,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() { .setTitle(R.string.set_separators) .setNegativeButton(R.string.lbl_cancel, null) .setPositiveButton(R.string.lbl_save) { _, _ -> - settings.musicSeparators = getCurrentSeparators() + Settings(requireContext()).musicSeparators = getCurrentSeparators() } } @@ -61,7 +58,8 @@ class SeparatorsDialog : ViewBindingDialogFragment() { // More efficient to do one iteration through the separator list and initialize // the corresponding CheckBox for each character instead of doing an iteration // through the separator list for each CheckBox. - (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: settings.musicSeparators) + (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) + ?: Settings(requireContext()).musicSeparators) ?.forEach { when (it) { SEPARATOR_COMMA -> binding.separatorComma.isChecked = true diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index 825ed4061..0050a8bae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -47,7 +47,7 @@ class PickerViewModel : ViewModel(), MusicStore.Listener { get() = _genreChoices override fun onCleared() { - musicStore.removeCallback(this) + musicStore.removeListener(this) } override fun onLibraryChanged(library: MusicStore.Library?) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt index a0308a91e..6d4244d07 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/storage/MusicDirsDialog.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.os.storage.StorageManager import android.provider.DocumentsContract import android.view.LayoutInflater +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -42,10 +43,8 @@ import org.oxycblt.auxio.util.showToast class MusicDirsDialog : ViewBindingDialogFragment(), DirectoryAdapter.Listener { private val dirAdapter = DirectoryAdapter(this) - private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } - private val storageManager: StorageManager by lifecycleObject { binding -> - binding.context.getSystemServiceCompat(StorageManager::class) - } + private var openDocumentTreeLauncher: ActivityResultLauncher? = null + private var storageManager: StorageManager? = null override fun onCreateBinding(inflater: LayoutInflater) = DialogMusicDirsBinding.inflate(inflater) @@ -57,7 +56,10 @@ class MusicDirsDialog : .setNeutralButton(R.string.lbl_add, null) .setNegativeButton(R.string.lbl_cancel, null) .setPositiveButton(R.string.lbl_save) { _, _ -> - val dirs = settings.getMusicDirs(storageManager) + val settings = Settings(requireContext()) + val dirs = + settings.getMusicDirs( + requireNotNull(storageManager) { "StorageManager was not available" }) val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) if (dirs != newDirs) { logD("Committing changes") @@ -67,7 +69,11 @@ class MusicDirsDialog : } override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { - val launcher = + val context = requireContext() + val storageManager = + context.getSystemServiceCompat(StorageManager::class).also { storageManager = it } + + openDocumentTreeLauncher = registerForActivityResult( ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) @@ -79,7 +85,10 @@ class MusicDirsDialog : val dialog = it as AlertDialog dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { logD("Opening launcher") - launcher.launch(null) + requireNotNull(openDocumentTreeLauncher) { + "Document tree launcher was not available" + } + .launch(null) } } @@ -88,7 +97,7 @@ class MusicDirsDialog : itemAnimator = null } - var dirs = settings.getMusicDirs(storageManager) + var dirs = Settings(context).getMusicDirs(storageManager) if (savedInstanceState != null) { val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) @@ -127,6 +136,8 @@ class MusicDirsDialog : override fun onDestroyBinding(binding: DialogMusicDirsBinding) { super.onDestroyBinding(binding) + storageManager = null + openDocumentTreeLauncher = null binding.dirsRecycler.adapter = null } @@ -153,7 +164,9 @@ class MusicDirsDialog : DocumentsContract.buildDocumentUriUsingTree( uri, DocumentsContract.getTreeDocumentId(uri)) val treeUri = DocumentsContract.getTreeDocumentId(docUri) - val dir = Directory.fromDocumentTreeUri(storageManager, treeUri) + val dir = + Directory.fromDocumentTreeUri( + requireNotNull(storageManager) { "StorageManager was not available" }, treeUri) if (dir != null) { dirAdapter.add(dir) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 2e13d05d4..ee987d57e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -111,7 +111,7 @@ class Indexer private constructor() { * @param listener The [Listener] to add. */ @Synchronized - fun registerCallback(listener: Listener) { + fun registerListener(listener: Listener) { if (BuildConfig.DEBUG && this.listener != null) { logW("Listener is already registered") return @@ -131,7 +131,7 @@ class Indexer private constructor() { * @see Listener */ @Synchronized - fun unregisterCallback(listener: Listener) { + fun unregisterListener(listener: Listener) { if (BuildConfig.DEBUG && this.listener !== listener) { logW("Given controller did not match current controller") return diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 35f3e7f50..f3ef164bd 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.system import android.app.Service import android.content.Intent +import android.content.SharedPreferences import android.database.ContentObserver import android.os.Handler import android.os.IBinder @@ -54,7 +55,8 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart (OxygenCobalt) */ -class IndexerService : Service(), Indexer.Controller, Settings.Listener { +class IndexerService : + Service(), Indexer.Controller, SharedPreferences.OnSharedPreferenceChangeListener { private val indexer = Indexer.getInstance() private val musicStore = MusicStore.getInstance() private val playbackManager = PlaybackStateManager.getInstance() @@ -81,7 +83,8 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener { // Initialize any listener-dependent components last as we wouldn't want a listener race // condition to cause us to load music before we were fully initialize. indexerContentObserver = SystemContentObserver() - settings = Settings(this, this) + settings = Settings(this) + settings.addListener(this) indexer.registerController(this) // An indeterminate indexer and a missing library implies we are extremely early // in app initialization so start loading music. @@ -105,7 +108,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener { // Then cancel the listener-dependent components to ensure that stray reloading // events will not occur. indexerContentObserver.release() - settings.release() + settings.removeListener(this) indexer.unregisterController(this) // Then cancel any remaining music loading jobs. serviceJob.cancel() @@ -230,7 +233,7 @@ class IndexerService : Service(), Indexer.Controller, Settings.Listener { // --- SETTING CALLBACKS --- - override fun onSettingChanged(key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { when (key) { // Hook changes in music settings to a new music loading event. getString(R.string.set_key_exclude_non_music), diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index fd9ebc425..3a629f8b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -23,6 +23,7 @@ import android.media.audiofx.AudioEffect import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.widget.Toolbar import androidx.core.view.updatePadding @@ -53,13 +54,7 @@ class PlaybackPanelFragment : StyledSeekBar.Listener { private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() - // AudioEffect expects you to use startActivityForResult with the panel intent. There is no - // contract analogue for this intent, so the generic contract is used instead. - private val equalizerLauncher by lifecycleObject { - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { - // Nothing to do - } - } + private var equalizerLauncher: ActivityResultLauncher? = null override fun onCreateBinding(inflater: LayoutInflater) = FragmentPlaybackPanelBinding.inflate(inflater) @@ -70,6 +65,13 @@ class PlaybackPanelFragment : ) { super.onBindingCreated(binding, savedInstanceState) + // AudioEffect expects you to use startActivityForResult with the panel intent. There is no + // contract analogue for this intent, so the generic contract is used instead. + equalizerLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + // Nothing to do + } + // --- UI SETUP --- binding.root.setOnApplyWindowInsetsListener { view, insets -> val bars = insets.systemBarInsetsCompat @@ -116,6 +118,7 @@ class PlaybackPanelFragment : } override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) { + equalizerLauncher = null binding.playbackToolbar.setOnMenuItemClickListener(null) // Marquee elements leak if they are not disabled when the views are destroyed. binding.playbackSong.isSelected = false @@ -137,7 +140,10 @@ class PlaybackPanelFragment : // music playback. .putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) try { - equalizerLauncher.launch(equalizerIntent) + requireNotNull(equalizerLauncher) { + "Equalizer panel launcher was not available" + } + .launch(equalizerIntent) } catch (e: ActivityNotFoundException) { requireContext().showToast(R.string.err_no_app) } 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 ab12b5cd8..2f5ea9f14 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -93,11 +93,11 @@ class PlaybackViewModel(application: Application) : get() = playbackManager.currentAudioSessionId init { - playbackManager.addCallback(this) + playbackManager.addListener(this) } override fun onCleared() { - playbackManager.removeCallback(this) + playbackManager.removeListener(this) } override fun onIndexMoved(index: Int) { 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 07e502f57..e12fa0f0f 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 @@ -41,9 +41,7 @@ class QueueFragment : ViewBindingFragment(), QueueAdapter. private val queueModel: QueueViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val queueAdapter = QueueAdapter(this) - private val touchHelper: ItemTouchHelper by lifecycleObject { - ItemTouchHelper(QueueDragCallback(queueModel)) - } + private var touchHelper: ItemTouchHelper? = null override fun onCreateBinding(inflater: LayoutInflater) = FragmentQueueBinding.inflate(inflater) @@ -53,7 +51,10 @@ class QueueFragment : ViewBindingFragment(), QueueAdapter. // --- UI SETUP --- binding.queueRecycler.apply { adapter = queueAdapter - touchHelper.attachToRecyclerView(this) + touchHelper = + ItemTouchHelper(QueueDragCallback(queueModel)).also { + it.attachToRecyclerView(this) + } } // Sometimes the scroll can change without the listener being updated, so we also @@ -84,7 +85,7 @@ class QueueFragment : ViewBindingFragment(), QueueAdapter. } override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { - touchHelper.startDrag(viewHolder) + requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder) } private fun updateDivider() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt index f35019fde..fb30d6c5c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueViewModel.kt @@ -47,7 +47,7 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { var scrollTo: Int? = null init { - playbackManager.addCallback(this) + playbackManager.addListener(this) } /** @@ -135,6 +135,6 @@ class QueueViewModel : ViewModel(), PlaybackStateManager.Listener { override fun onCleared() { super.onCleared() - playbackManager.removeCallback(this) + playbackManager.removeListener(this) } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 3c08e6b47..b1666684a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -26,15 +26,12 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment -import org.oxycblt.auxio.util.context /** * aa [ViewBindingDialogFragment] that allows user configuration of the current [ReplayGainPreAmp]. * @author Alexander Capehart (OxygenCobalt) */ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { - private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } - override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) override fun onConfigDialog(builder: AlertDialog.Builder) { @@ -42,7 +39,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { .setTitle(R.string.set_pre_amp) .setPositiveButton(R.string.lbl_ok) { _, _ -> val binding = requireBinding() - settings.replayGainPreAmp = + Settings(requireContext()).replayGainPreAmp = ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) } .setNegativeButton(R.string.lbl_cancel, null) @@ -53,7 +50,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { // First initialization, we need to supply the sliders with the values from // settings. After this, the sliders save their own state, so we do not need to // do any restore behavior. - val preAmp = settings.replayGainPreAmp + val preAmp = Settings(requireContext()).replayGainPreAmp binding.withTagsSlider.value = preAmp.with binding.withoutTagsSlider.value = preAmp.without } 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 18bbeea0f..c5415c23b 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 @@ -99,7 +99,7 @@ class PlaybackStateManager private constructor() { * @see Listener */ @Synchronized - fun addCallback(listener: Listener) { + fun addListener(listener: Listener) { if (isInitialized) { listener.onNewPlayback(index, queue, parent) listener.onRepeatChanged(repeatMode) @@ -117,7 +117,7 @@ class PlaybackStateManager private constructor() { * @see Listener */ @Synchronized - fun removeCallback(listener: Listener) { + fun removeListener(listener: Listener) { listeners.remove(listener) } @@ -629,7 +629,7 @@ class PlaybackStateManager private constructor() { /** * The interface for receiving updates from [PlaybackStateManager]. Add the listener to - * [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback]. + * [PlaybackStateManager] using [addListener], remove them on destruction with [removeListener]. */ interface Listener { /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index c34bdb628..863d71b6b 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.playback.system import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.graphics.Bitmap import android.net.Uri import android.os.Bundle @@ -47,7 +48,9 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class MediaSessionComponent(private val context: Context, private val listener: Listener) : - MediaSessionCompat.Callback(), PlaybackStateManager.Listener, Settings.Listener { + MediaSessionCompat.Callback(), + PlaybackStateManager.Listener, + SharedPreferences.OnSharedPreferenceChangeListener { private val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true @@ -55,13 +58,13 @@ class MediaSessionComponent(private val context: Context, private val listener: } private val playbackManager = PlaybackStateManager.getInstance() - private val settings = Settings(context, this) + private val settings = Settings(context) private val notification = NotificationComponent(context, mediaSession.sessionToken) private val provider = BitmapProvider(context) init { - playbackManager.addCallback(this) + playbackManager.addListener(this) mediaSession.setCallback(this) } @@ -79,15 +82,15 @@ class MediaSessionComponent(private val context: Context, private val listener: */ fun release() { provider.release() - settings.release() - playbackManager.removeCallback(this) + settings.removeListener(this) + playbackManager.removeListener(this) mediaSession.apply { isActive = false release() } } - // --- PLAYBACKSTATEMANAGER CALLBACKS --- + // --- PLAYBACKSTATEMANAGER OVERRIDES --- override fun onIndexMoved(index: Int) { updateMediaMetadata(playbackManager.song, playbackManager.parent) @@ -139,9 +142,9 @@ class MediaSessionComponent(private val context: Context, private val listener: invalidateSecondaryAction() } - // --- SETTINGSMANAGER CALLBACKS --- + // --- SETTINGS OVERRIDES --- - override fun onSettingChanged(key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { when (key) { context.getString(R.string.set_key_cover_mode) -> updateMediaMetadata(playbackManager.song, playbackManager.parent) @@ -149,7 +152,7 @@ class MediaSessionComponent(private val context: Context, private val listener: } } - // --- MEDIASESSION CALLBACKS --- + // --- MEDIASESSION OVERRIDES --- override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { super.onPlayFromMediaId(mediaId, extras) 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 82f60bb51..d62053211 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 @@ -22,6 +22,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.SharedPreferences import android.media.AudioManager import android.media.audiofx.AudioEffect import android.os.IBinder @@ -80,8 +81,8 @@ class PlaybackService : Player.Listener, InternalPlayer, MediaSessionComponent.Listener, - Settings.Listener, - MusicStore.Listener { + MusicStore.Listener, + SharedPreferences.OnSharedPreferenceChangeListener { // Player components private lateinit var player: ExoPlayer private lateinit var replayGainProcessor: ReplayGainAudioProcessor @@ -144,12 +145,13 @@ class PlaybackService : .build() .also { it.addListener(this) } // Initialize the core service components - settings = Settings(this, this) + settings = Settings(this) + settings.addListener(this) foregroundManager = ForegroundManager(this) // Initialize any listener-dependent components last as we wouldn't want a listener race // condition to cause us to load music before we were fully initialize. playbackManager.registerInternalPlayer(this) - musicStore.addCallback(this) + musicStore.addListener(this) widgetComponent = WidgetComponent(this) mediaSessionComponent = MediaSessionComponent(this, this) registerReceiver( @@ -185,12 +187,12 @@ class PlaybackService : super.onDestroy() foregroundManager.release() - settings.release() + settings.removeListener(this) // Pause just in case this destruction was unexpected. playbackManager.setPlaying(false) playbackManager.unregisterInternalPlayer(this) - musicStore.removeCallback(this) + musicStore.removeListener(this) unregisterReceiver(systemReceiver) serviceJob.cancel() @@ -329,12 +331,13 @@ class PlaybackService : } } - // --- SETTINGSMANAGER OVERRIDES --- + // --- SETTINGS OVERRIDES --- - override fun onSettingChanged(key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { if (key == getString(R.string.set_key_replay_gain) || key == getString(R.string.set_key_pre_amp_with) || key == getString(R.string.set_key_pre_amp_without)) { + // ReplayGain changed, we need to set it up again. onTracksChanged(player.currentTracks) } } 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 0a7154855..f9055e96d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -53,10 +53,8 @@ import org.oxycblt.auxio.util.* class SearchFragment : ListFragment() { private val searchModel: SearchViewModel by androidViewModels() private val searchAdapter = SearchAdapter(this) + private var imm: InputMethodManager? = null private var launchedKeyboard = false - private val imm: InputMethodManager by lifecycleObject { binding -> - binding.context.getSystemServiceCompat(InputMethodManager::class) - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -74,13 +72,15 @@ class SearchFragment : ListFragment() { override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) + imm = binding.context.getSystemServiceCompat(InputMethodManager::class) + binding.searchToolbar.apply { // Initialize the current filtering mode. menu.findItem(searchModel.getFilterOptionId()).isChecked = true setNavigationOnClickListener { // Keyboard is no longer needed. - imm.hide() + hideKeyboard() findNavController().navigateUp() } @@ -95,7 +95,7 @@ class SearchFragment : ListFragment() { if (!launchedKeyboard) { // Auto-open the keyboard when this view is shown - imm.show(this) + showKeyboard(this) launchedKeyboard = true } } @@ -184,7 +184,7 @@ class SearchFragment : ListFragment() { else -> return } // Keyboard is no longer needed. - imm.hide() + hideKeyboard() findNavController().navigate(action) } @@ -193,7 +193,7 @@ class SearchFragment : ListFragment() { if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) && selected.isNotEmpty()) { // Make selection of obscured items easier by hiding the keyboard. - imm.hide() + hideKeyboard() } } @@ -201,15 +201,19 @@ class SearchFragment : ListFragment() { * Safely focus the keyboard on a particular [View]. * @param view The [View] to focus the keyboard on. */ - private fun InputMethodManager.show(view: View) { + private fun showKeyboard(view: View) { view.apply { requestFocus() - postDelayed(200) { showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) } + postDelayed(200) { + requireNotNull(imm) { "InputMethodManager was not available" } + .showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } } } /** Safely hide the keyboard from this view. */ - private fun InputMethodManager.hide() { - hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) + private fun hideKeyboard() { + requireNotNull(imm) { "InputMethodManager was not available" } + .hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 0be8d3926..72ea04fae 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -55,12 +55,12 @@ class SearchViewModel(application: Application) : get() = _searchResults init { - musicStore.addCallback(this) + musicStore.addListener(this) } override fun onCleared() { super.onCleared() - musicStore.removeCallback(this) + musicStore.removeListener(this) } override fun onLibraryChanged(library: MusicStore.Library?) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 64bebac04..aabb07823 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.settings import android.content.Context import android.content.SharedPreferences +import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Build import android.os.storage.StorageManager import androidx.appcompat.app.AppCompatDelegate @@ -40,20 +41,14 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** - * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Object - * mutability + * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Member + * mutability is dependent on how they are used in app. Immutable members are often only modified by + * the preferences view, while mutable members are modified elsewhere. * @author Alexander Capehart (OxygenCobalt) */ -class Settings(private val context: Context, private val listener: Listener? = null) : - SharedPreferences.OnSharedPreferenceChangeListener { +class Settings(private val context: Context) { private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - init { - if (listener != null) { - inner.registerOnSharedPreferenceChangeListener(this) - } - } - /** * Migrate any settings from an old version into their modern counterparts. This can cause data * loss depending on the feasibility of a migration. @@ -154,25 +149,19 @@ class Settings(private val context: Context, private val listener: Listener? = n } /** - * Release this instance and any callbacks held by it. This is not needed if no [Listener] was - * originally attached. + * Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates. + * @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add. */ - fun release() { - inner.unregisterOnSharedPreferenceChangeListener(this) + fun addListener(listener: OnSharedPreferenceChangeListener) { + inner.registerOnSharedPreferenceChangeListener(listener) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { - unlikelyToBeNull(listener).onSettingChanged(key) - } - - /** Simplified listener for settings changes. */ - interface Listener { - // TODO: Refactor this lifecycle - /** - * Called when a setting has changed. - * @param key The key of the setting that changed. - */ - fun onSettingChanged(key: String) + /** + * Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further + * settings updates from being sent to ti.t + */ + fun removeListener(listener: OnSharedPreferenceChangeListener) { + inner.unregisterOnSharedPreferenceChangeListener(listener) } // --- VALUES --- diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt index 177ae8735..fd362131d 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingDialogFragment.kt @@ -23,11 +23,8 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -37,7 +34,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ abstract class ViewBindingDialogFragment : DialogFragment() { private var _binding: VB? = null - private var lifecycleObjects = mutableListOf>() /** * Configure the [AlertDialog.Builder] during [onCreateDialog]. @@ -85,25 +81,6 @@ abstract class ViewBindingDialogFragment : DialogFragment() { } } - /** - * Delegate to automatically create and destroy an object derived from the [ViewBinding]. - * @param create Block to create the object from the [ViewBinding]. - */ - fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { - lifecycleObjects.add(LifecycleObject(null, create)) - - return object : ReadOnlyProperty { - private val objIdx = lifecycleObjects.lastIndex - - @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: Fragment, property: KProperty<*>) = - requireNotNull(lifecycleObjects[objIdx].data) { - "Cannot access lifecycle object when view does not exist" - } - as T - } - } - final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -119,9 +96,6 @@ abstract class ViewBindingDialogFragment : DialogFragment() { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val binding = unlikelyToBeNull(_binding) - // Populate lifecycle-dependent objects - lifecycleObjects.forEach { it.populate(binding) } // Configure binding onBindingCreated(requireBinding(), savedInstanceState) // Apply the newly-configured view to the dialog. @@ -132,21 +106,8 @@ abstract class ViewBindingDialogFragment : DialogFragment() { final override fun onDestroyView() { super.onDestroyView() onDestroyBinding(unlikelyToBeNull(_binding)) - // Clear the lifecycle-dependent objects - lifecycleObjects.forEach { it.clear() } // Clear binding _binding = null logD("Fragment destroyed") } - - /** Internal implementation of [lifecycleObject]. */ - private data class LifecycleObject(var data: T?, val create: (VB) -> T) { - fun populate(binding: VB) { - data = create(binding) - } - - fun clear() { - data = null - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt index f0a005582..b5ece20e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ViewBindingFragment.kt @@ -23,8 +23,6 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -34,7 +32,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ abstract class ViewBindingFragment : Fragment() { private var _binding: VB? = null - private var lifecycleObjects = mutableListOf>() /** * Inflate the [ViewBinding] during [onCreateView]. @@ -75,26 +72,6 @@ abstract class ViewBindingFragment : Fragment() { } } - /** - * Delegate to automatically create and destroy an object derived from the [ViewBinding]. - * @param create Block to create the object from the [ViewBinding]. - */ - fun lifecycleObject(create: (VB) -> T): ReadOnlyProperty { - // TODO: Phase this out. - lifecycleObjects.add(LifecycleObject(null, create)) - - return object : ReadOnlyProperty { - private val objIdx = lifecycleObjects.lastIndex - - @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: Fragment, property: KProperty<*>) = - requireNotNull(lifecycleObjects[objIdx].data) { - "Cannot access lifecycle object when view does not exist" - } - as T - } - } - final override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -103,9 +80,6 @@ abstract class ViewBindingFragment : Fragment() { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val binding = unlikelyToBeNull(_binding) - // Populate lifecycle-dependent objects - lifecycleObjects.forEach { it.populate(binding) } // Configure binding onBindingCreated(requireBinding(), savedInstanceState) logD("Fragment created") @@ -114,21 +88,8 @@ abstract class ViewBindingFragment : Fragment() { final override fun onDestroyView() { super.onDestroyView() onDestroyBinding(unlikelyToBeNull(_binding)) - // Clear the lifecycle-dependent objects - lifecycleObjects.forEach { it.clear() } // Clear binding _binding = null logD("Fragment destroyed") } - - /** Internal implementation of [lifecycleObject]. */ - private data class LifecycleObject(var data: T?, val create: (VB) -> T) { - fun populate(binding: VB) { - data = create(binding) - } - - fun clear() { - data = null - } - } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index c14e38985..1f3048725 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -27,7 +27,6 @@ import org.oxycblt.auxio.list.ClickableListListener import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment -import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -38,7 +37,6 @@ import org.oxycblt.auxio.util.unlikelyToBeNull class AccentCustomizeDialog : ViewBindingDialogFragment(), ClickableListListener { private var accentAdapter = AccentAdapter(this) - private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) @@ -46,6 +44,7 @@ class AccentCustomizeDialog : builder .setTitle(R.string.set_accent) .setPositiveButton(R.string.lbl_ok) { _, _ -> + val settings = Settings(requireContext()) if (accentAdapter.selectedAccent == settings.accent) { // Nothing to do. return@setPositiveButton @@ -66,7 +65,7 @@ class AccentCustomizeDialog : if (savedInstanceState != null) { Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) } else { - settings.accent + Settings(requireContext()).accent }) } diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index b6e9f437c..638299bb9 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -18,6 +18,7 @@ package org.oxycblt.auxio.widgets import android.content.Context +import android.content.SharedPreferences import android.graphics.Bitmap import android.os.Build import coil.request.ImageRequest @@ -41,14 +42,15 @@ import org.oxycblt.auxio.util.logD * @author Alexander Capehart (OxygenCobalt) */ class WidgetComponent(private val context: Context) : - PlaybackStateManager.Listener, Settings.Listener { + PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener { private val playbackManager = PlaybackStateManager.getInstance() - private val settings = Settings(context, this) + private val settings = Settings(context) private val widgetProvider = WidgetProvider() private val provider = BitmapProvider(context) init { - playbackManager.addCallback(this) + playbackManager.addListener(this) + settings.addListener(this) } /** Update [WidgetProvider] with the current playback state. */ @@ -104,9 +106,9 @@ class WidgetComponent(private val context: Context) : /** Release this instance, preventing any further events from updating the widget instances. */ fun release() { provider.release() - settings.release() + settings.removeListener(this) widgetProvider.reset(context) - playbackManager.removeCallback(this) + playbackManager.removeListener(this) } // --- CALLBACKS --- @@ -118,7 +120,8 @@ class WidgetComponent(private val context: Context) : override fun onStateChanged(state: InternalPlayer.State) = update() override fun onShuffledChanged(isShuffled: Boolean) = update() override fun onRepeatChanged(repeatMode: RepeatMode) = update() - override fun onSettingChanged(key: String) { + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { if (key == context.getString(R.string.set_key_cover_mode) || key == context.getString(R.string.set_key_round_mode)) { update() diff --git a/prebuild.py b/prebuild.py index 59ea9552b..f8353d1a7 100755 --- a/prebuild.py +++ b/prebuild.py @@ -75,11 +75,12 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk-build")): candidates.append(entry.path) if len(candidates) > 0: - print(WARN + "warn:" + NC + " NDK_PATH was not set or invalid. multiple " + + print(WARN + "warn:" + NC + " ANDROID_NDK_HOME was not set or invalid. multiple " + "candidates were found however:") for i, candidate in enumerate(candidates): print("[" + str(i) + "] " + candidate) - + print(WARN + "info:" + NC + " NDK r21e is recommended for this script. Other " + + "NDKs may result in unexpected behavior.") try: ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))] except: From f4aa20b2f17aeb2118041e6c7d813bf655a1c849 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 31 Dec 2022 11:28:22 -0700 Subject: [PATCH 12/24] actions: tweak workflow naming Tweak the naming of workflow steps to be more consistent and clear. --- .github/workflows/android.yml | 4 ++-- app/src/main/java/org/oxycblt/auxio/MainFragment.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d150e9336..7b03de1ec 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -12,13 +12,13 @@ jobs: steps: - uses: actions/checkout@v3 - - name: set up JDK 11 + - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' cache: gradle - - name: Run pre-build steps + - name: Set up NDK r21e uses: nttld/setup-ndk@v1.2.0 id: setup-ndk with: diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 833b85a16..94b8103f4 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -74,7 +74,7 @@ class MainFragment : override fun onBindingCreated(binding: FragmentMainBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) - + elevationNormal = binding.context.getDimen(R.dimen.elevation_normal) // --- UI SETUP --- From dc46c49f07a9194291213333b77ceccc44a9d490 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 31 Dec 2022 13:47:13 -0700 Subject: [PATCH 13/24] list: add editable list listener Add a listener for the editable lists in the queue and tab config views. This simply reduces the amount of duplicated code within both of those views. --- .github/workflows/android.yml | 4 +- CHANGELOG.md | 3 + .../detail/recycler/AlbumDetailAdapter.kt | 2 +- .../detail/recycler/ArtistDetailAdapter.kt | 4 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 1 + .../java/org/oxycblt/auxio/home/tabs/Tab.kt | 3 +- .../org/oxycblt/auxio/home/tabs/TabAdapter.kt | 40 ++------- .../auxio/home/tabs/TabCustomizeDialog.kt | 12 +-- .../org/oxycblt/auxio/image/ImageGroup.kt | 2 +- .../org/oxycblt/auxio/list/ListFragment.kt | 3 +- .../java/org/oxycblt/auxio/list/Listeners.kt | 84 +++++++++++++++---- .../auxio/list/recycler/ViewHolders.kt | 8 +- .../java/org/oxycblt/auxio/music/Music.kt | 2 +- .../auxio/music/picker/ArtistChoiceAdapter.kt | 2 +- .../picker/ArtistNavigationPickerDialog.kt | 5 +- .../auxio/music/picker/ArtistPickerDialog.kt | 3 +- .../picker/ArtistPlaybackPickerDialog.kt | 5 +- .../auxio/music/picker/GenreChoiceAdapter.kt | 2 +- .../music/picker/GenrePlaybackPickerDialog.kt | 3 +- .../music/system/IndexerNotifications.kt | 1 - .../auxio/music/system/IndexerService.kt | 2 +- .../auxio/playback/PlaybackPanelFragment.kt | 1 + .../auxio/playback/queue/QueueAdapter.kt | 39 ++------- .../auxio/playback/queue/QueueFragment.kt | 7 +- .../oxycblt/auxio/ui/accent/AccentAdapter.kt | 4 +- .../auxio/ui/accent/AccentCustomizeDialog.kt | 3 +- app/src/main/res/values/strings.xml | 11 ++- .../android/en-US/full_description.txt | 2 +- prebuild.py | 17 ++-- 29 files changed, 147 insertions(+), 128 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 7b03de1ec..448ac933e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -29,9 +29,9 @@ jobs: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Grant execute permission for gradlew run: chmod +x gradlew - - name: Build Debug APK with Gradle + - name: Build debug APK with Gradle run: ./gradlew app:packageDebug - - name: Upload a Build Artifact + - name: Upload debug APK artifact uses: actions/upload-artifact@v3.1.1 with: name: Auxio_Canary diff --git a/CHANGELOG.md b/CHANGELOG.md index e979d1af3..92e6b3fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ - Value lists are now properly localized - Queue no longer primarily shows previous songs when opened +#### What's Fixed +- Fixed mangled multi-value ID3v2 tags when UTF-16 is used + ## 3.0.0 #### What's New 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 769db5c4f..8cc214b14 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 @@ -227,7 +227,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA * @param listener A [SelectableListListener] to bind interactions to. */ fun bind(song: Song, listener: SelectableListListener) { - listener.bind(this, song, binding.songMenu) + listener.bind(song, this, menuButton = binding.songMenu) binding.songTrack.apply { if (song.track != null) { 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 d9ab4fbe8..84aedabfb 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 @@ -184,7 +184,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(album: Album, listener: SelectableListListener) { - listener.bind(this, album, binding.parentMenu) + listener.bind(album, this, menuButton = binding.parentMenu) binding.parentImage.bind(album) binding.parentName.text = album.resolveName(binding.context) binding.parentInfo.text = @@ -236,7 +236,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(song: Song, listener: SelectableListListener) { - listener.bind(this, song, binding.songMenu) + listener.bind(song, this, menuButton = binding.songMenu) binding.songAlbumCover.bind(song) binding.songName.text = song.resolveName(binding.context) binding.songInfo.text = song.album.resolveName(binding.context) 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 d83ee3618..6f6c89c92 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -354,6 +354,7 @@ class HomeFragment : } } is Indexer.Response.NoMusic -> { + // TODO: Move this state to the list fragments (makes life easier) logD("Updating UI to Response.NoMusic state") binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) 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 76f7cf95d..1e74ad396 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 @@ -17,6 +17,7 @@ package org.oxycblt.auxio.home.tabs +import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.logE @@ -25,7 +26,7 @@ import org.oxycblt.auxio.util.logE * @param mode The type of list in the home view this instance corresponds to. * @author Alexander Capehart (OxygenCobalt) */ -sealed class Tab(open val mode: MusicMode) { +sealed class Tab(open val mode: MusicMode) : Item { /** * A visible tab. This will be visible in the home and tab configuration views. * @param mode The type of list in the home view this instance corresponds to. 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 18025e3b1..756f8fea1 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 @@ -18,21 +18,22 @@ package org.oxycblt.auxio.home.tabs import android.annotation.SuppressLint -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemTabBinding +import org.oxycblt.auxio.list.EditableListListener import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.inflater /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. - * @param listener A [Listener] for tab interactions. + * @param listener A [EditableListListener] for tab interactions. */ -class TabAdapter(private val listener: Listener) : RecyclerView.Adapter() { +class TabAdapter(private val listener: EditableListListener) : + RecyclerView.Adapter() { /** The current array of [Tab]s. */ var tabs = arrayOf() private set @@ -75,23 +76,6 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter Invisible and vice versa). - * @param tabMode The [MusicMode] of the tab clicked. - */ - fun onToggleVisibility(tabMode: MusicMode) - - /** - * Called when the drag handle on a [RecyclerView.ViewHolder] is clicked, requesting that a - * drag should be started. - * @param viewHolder The [RecyclerView.ViewHolder] to start dragging. - */ - fun onPickUp(viewHolder: RecyclerView.ViewHolder) - } - private companion object { val PAYLOAD_TAB_CHANGED = Any() } @@ -106,12 +90,11 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : /** * Bind new data to this instance. * @param tab The new [Tab] to bind. - * @param listener A [TabAdapter.Listener] to bind interactions to. + * @param listener A [EditableListListener] to bind interactions to. */ @SuppressLint("ClickableViewAccessibility") - fun bind(tab: Tab, listener: TabAdapter.Listener) { - binding.root.setOnClickListener { listener.onToggleVisibility(tab.mode) } - + fun bind(tab: Tab, listener: EditableListListener) { + listener.bind(tab, this, dragHandle = binding.tabDragHandle) binding.tabCheckBox.apply { // Update the CheckBox name to align with the mode setText( @@ -126,15 +109,6 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : // the tab data since they are in the same data structure (Tab) isChecked = tab is Tab.Visible } - - // Set up the drag handle to start a drag whenever it is touched. - binding.tabDragHandle.setOnTouchListener { _, motionEvent -> - binding.tabDragHandle.performClick() - if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { - listener.onPickUp(this) - true - } else false - } } companion object { 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 063086b15..6c9706762 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 @@ -25,7 +25,8 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding -import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD @@ -34,7 +35,7 @@ import org.oxycblt.auxio.util.logD * A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration. * @author Alexander Capehart (OxygenCobalt) */ -class TabCustomizeDialog : ViewBindingDialogFragment(), TabAdapter.Listener { +class TabCustomizeDialog : ViewBindingDialogFragment(), EditableListListener { private val tabAdapter = TabAdapter(this) private var touchHelper: ItemTouchHelper? = null @@ -80,12 +81,11 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd binding.tabRecycler.adapter = null } - override fun onToggleVisibility(tabMode: MusicMode) { - logD("Toggling tab $tabMode") - + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { + check(item is Tab) { "Unexpected datatype: ${item::class.java}" } // We will need the exact index of the tab to update on in order to // notify the adapter of the change. - val index = tabAdapter.tabs.indexOfFirst { it.mode == tabMode } + val index = tabAdapter.tabs.indexOfFirst { it.mode == item.mode } val tab = tabAdapter.tabs[index] tabAdapter.setTab( index, diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 5b9e814a9..11fa59b22 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -107,7 +107,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr // Playback indicator should sit above the inner StyledImageView and custom view/ addView(playbackIndicatorView) - // Selction indicator should never be obscured, so place it at the top. + // Selection indicator should never be obscured, so place it at the top. addView( selectionIndicatorView, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT).apply { diff --git a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index f8071816e..250992d04 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -22,6 +22,7 @@ import android.view.View import androidx.annotation.MenuRes import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R @@ -53,7 +54,7 @@ abstract class ListFragment : SelectionFragment(), Selecta */ abstract fun onRealClick(music: Music) - override fun onClick(item: Item) { + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } if (selectionModel.selected.value.isNotEmpty()) { // Map clicking an item to selecting an item when items are already selected. diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index 926fb6904..c8f9ebb6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -17,8 +17,8 @@ package org.oxycblt.auxio.list +import android.view.MotionEvent import android.view.View -import android.widget.Button import androidx.recyclerview.widget.RecyclerView /** @@ -26,13 +26,63 @@ import androidx.recyclerview.widget.RecyclerView * @author Alexander Capehart (OxygenCobalt) */ interface ClickableListListener { - // TODO: Supply a ViewHolder on clicks - // (allows editable lists to be standardized into a listener.) /** * Called when an [Item] in the list is clicked. * @param item The [Item] that was clicked. + * @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked. */ - fun onClick(item: Item) + fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) + + /** + * Binds this instance to a list item. + * @param item The [Item] that this list entry is bound to. + * @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked. + * @param bodyView The [View] containing the main body of the list item. Any click events on + * this [View] are routed to the listener. Defaults to the root view. + */ + fun bind( + item: Item, + viewHolder: RecyclerView.ViewHolder, + bodyView: View = viewHolder.itemView + ) { + bodyView.setOnClickListener { onClick(item, viewHolder) } + } +} + +/** + * An extension of [ClickableListListener] that enables list editing functionality. + * @author Alexander Capehart (OxygenCobalt) + */ +interface EditableListListener : ClickableListListener { + /** + * Called when a [RecyclerView.ViewHolder] requests that it should be dragged. + * @param viewHolder The [RecyclerView.ViewHolder] that should start being dragged. + */ + fun onPickUp(viewHolder: RecyclerView.ViewHolder) + + /** + * Binds this instance to a list item. + * @param item The [Item] that this list entry is bound to. + * @param viewHolder The [RecyclerView.ViewHolder] to bind. + * @param bodyView The [View] containing the main body of the list item. Any click events on + * this [View] are routed to the listener. Defaults to the root view. + * @param dragHandle A touchable [View]. Any drag on this view will start a drag event. + */ + fun bind( + item: Item, + viewHolder: RecyclerView.ViewHolder, + bodyView: View = viewHolder.itemView, + dragHandle: View + ) { + bind(item, viewHolder, bodyView) + dragHandle.setOnTouchListener { _, motionEvent -> + dragHandle.performClick() + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { + onPickUp(viewHolder) + true + } else false + } + } } /** @@ -55,19 +105,23 @@ interface SelectableListListener : ClickableListListener { /** * Binds this instance to a list item. - * @param viewHolder The [RecyclerView.ViewHolder] to bind. * @param item The [Item] that this list entry is bound to. - * @param menuButton A [Button] that opens a menu. + * @param viewHolder The [RecyclerView.ViewHolder] to bind. + * @param bodyView The [View] containing the main body of the list item. Any click events on + * this [View] are routed to the listener. Defaults to the root view. + * @param menuButton A clickable [View]. Any click events on this [View] will open a menu. */ - fun bind(viewHolder: RecyclerView.ViewHolder, item: Item, menuButton: Button) { - viewHolder.itemView.apply { - // Map clicks to the click listener. - setOnClickListener { onClick(item) } - // Map long clicks to the selection listener. - setOnLongClickListener { - onSelect(item) - true - } + fun bind( + item: Item, + viewHolder: RecyclerView.ViewHolder, + bodyView: View = viewHolder.itemView, + menuButton: View + ) { + bind(item, viewHolder, bodyView) + // Map long clicks to the selection listener. + bodyView.setOnLongClickListener { + onSelect(item) + true } // Map the menu button to the menu opening listener. menuButton.setOnClickListener { onOpenMenu(item, it) } diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index f8fa090b8..15628c41c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -46,7 +46,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(song: Song, listener: SelectableListListener) { - listener.bind(this, song, binding.songMenu) + listener.bind(song, this, menuButton = binding.songMenu) binding.songAlbumCover.bind(song) binding.songName.text = song.resolveName(binding.context) binding.songInfo.text = song.resolveArtistContents(binding.context) @@ -93,7 +93,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(album: Album, listener: SelectableListListener) { - listener.bind(this, album, binding.parentMenu) + listener.bind(album, this, menuButton = binding.parentMenu) binding.parentImage.bind(album) binding.parentName.text = album.resolveName(binding.context) binding.parentInfo.text = album.resolveArtistContents(binding.context) @@ -142,7 +142,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(artist: Artist, listener: SelectableListListener) { - listener.bind(this, artist, binding.parentMenu) + listener.bind(artist, this, menuButton = binding.parentMenu) binding.parentImage.bind(artist) binding.parentName.text = artist.resolveName(binding.context) binding.parentInfo.text = @@ -201,7 +201,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding * @param listener An [SelectableListListener] to bind interactions to. */ fun bind(genre: Genre, listener: SelectableListListener) { - listener.bind(this, genre, binding.parentMenu) + listener.bind(genre, this, menuButton = binding.parentMenu) binding.parentImage.bind(genre) binding.parentName.text = genre.resolveName(binding.context) binding.parentInfo.text = diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 50ad6b6d8..62e05abe6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -282,7 +282,7 @@ sealed class Music : Item { private companion object { /** Cached collator instance re-used with [makeCollationKeyImpl]. */ - val COLLATOR = Collator.getInstance().apply { strength = Collator.PRIMARY } + val COLLATOR: Collator = Collator.getInstance().apply { strength = Collator.PRIMARY } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt index f2bbf540d..71bdc09b4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistChoiceAdapter.kt @@ -68,7 +68,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : * @param listener A [ClickableListListener] to bind interactions to. */ fun bind(artist: Artist, listener: ClickableListListener) { - binding.root.setOnClickListener { listener.onClick(artist) } + listener.bind(artist, this) binding.pickerImage.bind(artist) binding.pickerName.text = artist.resolveName(binding.context) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt index a8ea49687..bebfd66da 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistNavigationPickerDialog.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.picker import android.os.Bundle import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Artist @@ -40,8 +41,8 @@ class ArtistNavigationPickerDialog : ArtistPickerDialog() { super.onBindingCreated(binding, savedInstanceState) } - override fun onClick(item: Item) { - super.onClick(item) + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { + super.onClick(item, viewHolder) check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } // User made a choice, navigate to it. navModel.exploreNavigateTo(item) diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt index 30b5dd996..0bf780537 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPickerDialog.kt @@ -22,6 +22,7 @@ import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.ClickableListListener @@ -67,7 +68,7 @@ abstract class ArtistPickerDialog : binding.pickerRecycler.adapter = null } - override fun onClick(item: Item) { + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { findNavController().navigateUp() } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt index b81e2604a..24ed8af43 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/ArtistPlaybackPickerDialog.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.picker import android.os.Bundle import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Artist @@ -41,8 +42,8 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() { super.onBindingCreated(binding, savedInstanceState) } - override fun onClick(item: Item) { - super.onClick(item) + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { + super.onClick(item, viewHolder) // User made a choice, play the given song from that artist. check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" } val song = pickerModel.currentItem.value diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt index 527c9e9d9..49b5c758a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenreChoiceAdapter.kt @@ -68,7 +68,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) : * @param listener A [ClickableListListener] to bind interactions to. */ fun bind(genre: Genre, listener: ClickableListListener) { - binding.root.setOnClickListener { listener.onClick(genre) } + listener.bind(genre, this) binding.pickerImage.bind(genre) binding.pickerName.text = genre.resolveName(binding.context) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt index 9ba9368fe..0a197ab0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/GenrePlaybackPickerDialog.kt @@ -23,6 +23,7 @@ import androidx.appcompat.app.AlertDialog import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicPickerBinding import org.oxycblt.auxio.list.ClickableListListener @@ -74,7 +75,7 @@ class GenrePlaybackPickerDialog : binding.pickerRecycler.adapter = null } - override fun onClick(item: Item) { + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { // User made a choice, play the given song from that genre. check(item is Genre) { "Unexpected datatype: ${item::class.simpleName}" } val song = pickerModel.currentItem.value diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt index d9d34fa02..67d301c36 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerNotifications.kt @@ -72,7 +72,6 @@ class IndexingNotification(private val context: Context) : // Determinate state, show an active progress meter. Since these updates arrive // highly rapidly, only update every 1.5 seconds to prevent notification rate // limiting. - // TODO: Can I port this to the playback notification somehow? val now = SystemClock.elapsedRealtime() if (lastUpdateTime > -1 && (now - lastUpdateTime) < 1500) { return false diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index f3ef164bd..50548c430 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -198,7 +198,7 @@ class IndexerService : // 2. If a non-foreground service is killed, the app will probably still be alive, // and thus the music library will not be updated at all. // TODO: Assuming I unify this with PlaybackService, it's possible that I won't need - // this anymore. + // this anymore, or at least I only have to use it when the app task is not removed. if (!foregroundManager.tryStartForeground(observingNotification)) { observingNotification.post() } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 3a629f8b7..c3d69cd57 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -102,6 +102,7 @@ class PlaybackPanelFragment : binding.playbackSeekBar.listener = this // Set up actions + // TODO: Add better playback button accessibility binding.playbackRepeat.setOnClickListener { playbackModel.toggleRepeatMode() } binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() } binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() } 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 cc2f7c3d1..baa4aa76c 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 @@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.queue import android.annotation.SuppressLint import android.graphics.drawable.LayerDrawable -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.core.view.isInvisible @@ -27,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemQueueSongBinding +import org.oxycblt.auxio.list.EditableListListener import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SyncListDiffer @@ -38,10 +38,11 @@ import org.oxycblt.auxio.util.inflater /** * A [RecyclerView.Adapter] that shows an editable list of queue items. - * @param listener A [Listener] to bind interactions to. + * @param listener A [EditableListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter() { +class QueueAdapter(private val listener: EditableListListener) : + RecyclerView.Adapter() { private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFF_CALLBACK) // Since PlayingIndicator adapter relies on an item value, we cannot use it for this // adapter, as one item can appear at several points in the UI. Use a similar implementation @@ -121,22 +122,6 @@ class QueueAdapter(private val listener: Listener) : RecyclerView.Adapter - binding.songDragHandle.performClick() - if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { - listener.onPickUp(this) - true - } else false - } } override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { 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 e12fa0f0f..826d04270 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -26,6 +26,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentQueueBinding +import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment @@ -37,7 +39,7 @@ import org.oxycblt.auxio.util.logD * A [ViewBindingFragment] that displays an editable queue. * @author Alexander Capehart (OxygenCobalt) */ -class QueueFragment : ViewBindingFragment(), QueueAdapter.Listener { +class QueueFragment : ViewBindingFragment(), EditableListListener { private val queueModel: QueueViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val queueAdapter = QueueAdapter(this) @@ -79,8 +81,7 @@ class QueueFragment : ViewBindingFragment(), QueueAdapter. binding.queueRecycler.adapter = null } - override fun onClick(viewHolder: RecyclerView.ViewHolder) { - // Clicking on a queue item should start playing it. + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { queueModel.goto(viewHolder.bindingAdapterPosition) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt index 8ac060be5..a4c1e6015 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentAdapter.kt @@ -94,13 +94,13 @@ class AccentViewHolder private constructor(private val binding: ItemAccentBindin * @param listener A [ClickableListListener] to bind interactions to. */ fun bind(accent: Accent, listener: ClickableListListener) { + listener.bind(accent, this, binding.accent) binding.accent.apply { - setOnClickListener { listener.onClick(accent) } - backgroundTintList = context.getColorCompat(accent.primary) // Add a Tooltip based on the content description so that the purpose of this // button can be clear. contentDescription = context.getString(accent.name) TooltipCompat.setTooltipText(this, contentDescription) + backgroundTintList = context.getColorCompat(accent.primary) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index 1f3048725..2cb3c93d4 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.ui.accent import android.os.Bundle import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogAccentBinding @@ -79,7 +80,7 @@ class AccentCustomizeDialog : binding.accentRecycler.adapter = null } - override fun onClick(item: Item) { + override fun onClick(item: Item, viewHolder: RecyclerView.ViewHolder) { check(item is Accent) { "Unexpected datatype: ${item::class.java}" } accentAdapter.setSelectedAccent(item) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30261473b..7b03934ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,9 +60,9 @@ Mixtapes Mixtape - + Mixes - + Mix @@ -341,7 +341,10 @@ - + %1$s, %2$s @@ -365,7 +368,7 @@ Albums loaded: %d Artists loaded: %d Genres loaded: %d - + Total duration: %s diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index e55914587..0d9401379 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,4 +1,4 @@ -Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of Exoplayer, Auxio has superior library support and listening quality compared to other apps that use outdated android functionality. In short, It plays music. +Auxio is a local music player with a fast, reliable UI/UX without the many useless features present in other music players. Built off of Exoplayer, Auxio has superior library support and listening quality compared to other apps that use outdated android functionality. In short, It plays music. Features diff --git a/prebuild.py b/prebuild.py index f8353d1a7..09e632feb 100755 --- a/prebuild.py +++ b/prebuild.py @@ -19,13 +19,14 @@ import re # WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION AND # THE GRADLE DEPENDENCY. IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. -# EXO_VERSION = "2.18.1" +# EXO_VERSION = "2.18.2" FLAC_VERSION = "1.3.2" -FATAL="\033[1;31m" -WARN="\033[1;91m" -INFO="\033[1;94m" -OK="\033[1;92m" +OK="\033[1;32m" # Bold green +FATAL="\033[1;31m" # Bold red +WARN="\033[1;33m" # Bold yellow +RUN="\033[1;34m" # Bold blue +INFO="\033[1m" # Bold white NC="\033[0m" # We do some shell scripting later on, so we can't support windows. @@ -36,7 +37,7 @@ if system not in ["Linux", "Darwin"]: sys.exit(1) def sh(cmd): - print(INFO + "execute: " + NC + cmd) + print(RUN + "execute: " + NC + cmd) code = subprocess.call(["sh", "-c", "set -e; " + cmd]) if code != 0: print(FATAL + "fatal:" + NC + " command failed with exit code " + str(code)) @@ -79,11 +80,11 @@ if ndk_path is None or not os.path.isfile(os.path.join(ndk_path, "ndk-build")): "candidates were found however:") for i, candidate in enumerate(candidates): print("[" + str(i) + "] " + candidate) - print(WARN + "info:" + NC + " NDK r21e is recommended for this script. Other " + + print(INFO + "info:" + NC + " NDK r21e is recommended for this script. Other " + "NDKs may result in unexpected behavior.") try: ndk_path = candidates[int(input("enter the ndk to use [default 0]: "))] - except: + except ValueError: ndk_path = candidates[0] else: print(FATAL + "fatal:" + NC + " the android ndk was not installed at a " + From 7721e6409681a99577b0bcade9a93db80fa771fa Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sat, 31 Dec 2022 19:50:54 -0700 Subject: [PATCH 14/24] music: split off extractor parsing Split off parsing-related components from extractor into a new parsing module. A lot of these methods are used in non-extractor code, so it makes more sense for them to not be part of the extractors. The code that is really extractor-specific can remain within the extractor files. --- app/src/main/AndroidManifest.xml | 2 +- .../java/org/oxycblt/auxio/MainFragment.kt | 2 +- .../auxio/detail/DetailAppBarLayout.kt | 6 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 5 +- .../org/oxycblt/auxio/image/ImageGroup.kt | 2 +- .../java/org/oxycblt/auxio/music/Music.kt | 17 ++- .../auxio/music/extractor/CacheExtractor.kt | 5 +- .../music/extractor/MediaStoreExtractor.kt | 26 ++++- .../music/extractor/MetadataExtractor.kt | 20 ++-- .../{extractor => parsing}/ParsingUtil.kt | 104 +++++------------- .../oxycblt/auxio/music/parsing/Separators.kt | 13 +++ .../SeparatorsDialog.kt | 30 ++--- .../auxio/playback/PlaybackPanelFragment.kt | 2 +- .../settings/prefs/PreferenceFragment.kt | 44 ++++---- ...arLayout.kt => CoordinatorAppBarLayout.kt} | 2 +- app/src/main/res/layout/fragment_about.xml | 4 +- app/src/main/res/layout/fragment_home.xml | 4 +- app/src/main/res/layout/fragment_search.xml | 4 +- app/src/main/res/layout/fragment_settings.xml | 4 +- app/src/main/res/navigation/nav_main.xml | 2 +- 20 files changed, 143 insertions(+), 155 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{extractor => parsing}/ParsingUtil.kt (81%) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/parsing/Separators.kt rename app/src/main/java/org/oxycblt/auxio/music/{extractor => parsing}/SeparatorsDialog.kt (76%) rename app/src/main/java/org/oxycblt/auxio/ui/{AuxioAppBarLayout.kt => CoordinatorAppBarLayout.kt} (99%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 26eacc16f..1c35add27 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -116,7 +116,7 @@ - +