diff --git a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt index 72626319e..7de0b5199 100644 --- a/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt +++ b/app/src/androidTest/java/org/oxycblt/auxio/StubTest.kt @@ -18,7 +18,9 @@ package org.oxycblt.auxio import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.* +import org.junit.Test import org.junit.runner.RunWith /** @@ -28,5 +30,10 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class StubTest { - // TODO: Add tests + // TODO: Make tests + @Test + fun useAppContext() { + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.oxycblt.auxio", appContext.packageName) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index 1e645d506..b2ff89af4 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -25,12 +25,14 @@ import androidx.core.graphics.drawable.IconCompat import coil.ImageLoader import coil.ImageLoaderFactory import coil.request.CachePolicy +import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher import org.oxycblt.auxio.image.extractor.ArtistImageFetcher import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory import org.oxycblt.auxio.image.extractor.GenreImageFetcher import org.oxycblt.auxio.image.extractor.MusicKeyer -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.ui.UISettings /** * Auxio: A simple, rational music player for android. @@ -40,7 +42,9 @@ class AuxioApp : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() // Migrate any settings that may have changed in an app update. - Settings(this).migrate() + ImageSettings.from(this).migrate() + PlaybackSettings.from(this).migrate() + UISettings.from(this).migrate() // Adding static shortcuts in a dynamic manner is better than declaring them // manually, as it will properly handle the difference between debug and release // Auxio instances. diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 25d40c62c..6b37b178d 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.system.IndexerService import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.system.PlaybackService -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() { } private fun setupTheme() { - val settings = Settings(this) + val settings = UISettings.from(this) // Apply the theme configuration. AppCompatDelegate.setDefaultNightMode(settings.theme) // Apply the color scheme. The black theme requires it's own set of themes since diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index d94592b23..a69841ff0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.* /** @@ -123,7 +123,7 @@ class AlbumDetailFragment : override fun onRealClick(item: Music) { val song = requireIs(item) - when (Settings(requireContext()).detailPlaybackMode) { + when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) { // "Play from shown item" and "Play from album" functionally have the same // behavior since a song can only have one album. null, @@ -149,12 +149,12 @@ class AlbumDetailFragment : override fun onOpenSortMenu(anchor: View) { openMenu(anchor, R.menu.menu_album_sort) { - val sort = detailModel.albumSort + val sort = detailModel.albumSortSort unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending setOnMenuItemClickListener { item -> item.isChecked = !item.isChecked - detailModel.albumSort = + detailModel.albumSortSort = if (item.itemId == R.id.option_sort_asc) { sort.withAscending(item.isChecked) } else { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index f87f9ab56..7fbe191a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD @@ -123,7 +123,7 @@ class ArtistDetailFragment : ListFragment(), Detai override fun onRealClick(item: Music) { when (item) { is Song -> { - when (Settings(requireContext()).detailPlaybackMode) { + when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) { // When configured to play from the selected item, we already have an Artist // to play from. null -> @@ -158,13 +158,13 @@ class ArtistDetailFragment : ListFragment(), Detai override fun onOpenSortMenu(anchor: View) { openMenu(anchor, R.menu.menu_artist_sort) { - val sort = detailModel.artistSort + val sort = detailModel.artistSongSort unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending setOnMenuItemClickListener { item -> item.isChecked = !item.isChecked - detailModel.artistSort = + detailModel.artistSongSort = if (item.itemId == R.id.option_sort_asc) { sort.withAscending(item.isChecked) } else { 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 37545b526..4d5db2ec0 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -33,8 +33,10 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.storage.MimeType -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.* /** @@ -47,7 +49,7 @@ import org.oxycblt.auxio.util.* class DetailViewModel(application: Application) : AndroidViewModel(application), MusicStore.Listener { private val musicStore = MusicStore.getInstance() - private val settings = Settings(application) + private val musicSettings = MusicSettings.from(application) private var currentSongJob: Job? = null @@ -75,10 +77,10 @@ class DetailViewModel(application: Application) : get() = _albumList /** The current [Sort] used for [Song]s in [albumList]. */ - var albumSort: Sort - get() = settings.detailAlbumSort + var albumSortSort: Sort + get() = musicSettings.albumSongSort set(value) { - settings.detailAlbumSort = value + musicSettings.albumSongSort = value // Refresh the album list to reflect the new sort. currentAlbum.value?.let(::refreshAlbumList) } @@ -95,10 +97,10 @@ class DetailViewModel(application: Application) : val artistList: StateFlow> = _artistList /** The current [Sort] used for [Song]s in [artistList]. */ - var artistSort: Sort - get() = settings.detailArtistSort + var artistSongSort: Sort + get() = musicSettings.artistSongSort set(value) { - settings.detailArtistSort = value + musicSettings.artistSongSort = value // Refresh the artist list to reflect the new sort. currentArtist.value?.let(::refreshArtistList) } @@ -115,10 +117,10 @@ class DetailViewModel(application: Application) : val genreList: StateFlow> = _genreList /** The current [Sort] used for [Song]s in [genreList]. */ - var genreSort: Sort - get() = settings.detailGenreSort + var genreSongSort: Sort + get() = musicSettings.genreSongSort set(value) { - settings.detailGenreSort = value + musicSettings.genreSongSort = value // Refresh the genre list to reflect the new sort. currentGenre.value?.let(::refreshGenreList) } @@ -309,7 +311,7 @@ class DetailViewModel(application: Application) : // To create a good user experience regarding disc numbers, we group the album's // songs up by disc and then delimit the groups by a disc header. - val songs = albumSort.songs(album.songs) + val songs = albumSortSort.songs(album.songs) // Songs without disc tags become part of Disc 1. val byDisc = songs.groupBy { it.disc ?: 1 } if (byDisc.size > 1) { @@ -363,7 +365,7 @@ class DetailViewModel(application: Application) : if (artist.songs.isNotEmpty()) { logD("Songs present in this artist, adding header") data.add(SortHeader(R.string.lbl_songs)) - data.addAll(artistSort.songs(artist.songs)) + data.addAll(artistSongSort.songs(artist.songs)) } _artistList.value = data.toList() @@ -376,7 +378,7 @@ class DetailViewModel(application: Application) : data.add(Header(R.string.lbl_artists)) data.addAll(genre.artists) data.add(SortHeader(R.string.lbl_songs)) - data.addAll(genreSort.songs(genre.songs)) + data.addAll(genreSongSort.songs(genre.songs)) _genreList.value = data } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 302d3abfb..38568826e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD @@ -123,7 +123,7 @@ class GenreDetailFragment : ListFragment(), Detail when (item) { is Artist -> navModel.exploreNavigateTo(item) is Song -> - when (Settings(requireContext()).detailPlaybackMode) { + when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) { // When configured to play from the selected item, we already have a Genre // to play from. null -> @@ -156,12 +156,12 @@ class GenreDetailFragment : ListFragment(), Detail override fun onOpenSortMenu(anchor: View) { openMenu(anchor, R.menu.menu_genre_sort) { - val sort = detailModel.genreSort + val sort = detailModel.genreSongSort unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending setOnMenuItemClickListener { item -> item.isChecked = !item.isChecked - detailModel.genreSort = + detailModel.genreSongSort = if (item.itemId == R.id.option_sort_asc) { sort.withAscending(item.isChecked) } else { 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 c88f25b8e..8b89fef47 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt new file mode 100644 index 000000000..e581a55a9 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeSettings.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.home + +import android.content.Context +import androidx.core.content.edit +import org.oxycblt.auxio.R +import org.oxycblt.auxio.home.tabs.Tab +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.unlikelyToBeNull + +/** + * User configuration specific to the home UI. + * @author Alexander Capehart (OxygenCobalt) + */ +interface HomeSettings : Settings { + /** The tabs to show in the home UI. */ + var homeTabs: Array + /** Whether to hide artists considered "collaborators" from the home UI. */ + val shouldHideCollaborators: Boolean + + private class Real(context: Context) : Settings.Real(context), HomeSettings { + override var homeTabs: Array + get() = + Tab.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)) + ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_lib_tabs), Tab.toIntCode(value)) + apply() + } + } + + override val shouldHideCollaborators: Boolean + get() = + sharedPreferences.getBoolean( + context.getString(R.string.set_key_hide_collaborators), false) + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): HomeSettings = Real(context) + } +} 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 9661e3e2e..0283fc579 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -25,7 +25,9 @@ import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD @@ -38,7 +40,8 @@ class HomeViewModel(application: Application) : MusicStore.Listener, SharedPreferences.OnSharedPreferenceChangeListener { private val musicStore = MusicStore.getInstance() - private val settings = Settings(application) + private val homeSettings = HomeSettings.from(application) + private val musicSettings = MusicSettings.from(application) private val _songsList = MutableStateFlow(listOf()) /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ @@ -89,13 +92,13 @@ class HomeViewModel(application: Application) : init { musicStore.addListener(this) - settings.addListener(this) + homeSettings.addListener(this) } override fun onCleared() { super.onCleared() musicStore.removeListener(this) - settings.removeListener(this) + homeSettings.removeListener(this) } override fun onLibraryChanged(library: Library?) { @@ -103,17 +106,17 @@ class HomeViewModel(application: Application) : logD("Library changed, refreshing library") // Get the each list of items in the library to use as our list data. // Applying the preferred sorting to them. - _songsList.value = settings.libSongSort.songs(library.songs) - _albumsLists.value = settings.libAlbumSort.albums(library.albums) + _songsList.value = musicSettings.songSort.songs(library.songs) + _albumsLists.value = musicSettings.albumSort.albums(library.albums) _artistsList.value = - settings.libArtistSort.artists( - if (settings.shouldHideCollaborators) { + musicSettings.artistSort.artists( + if (homeSettings.shouldHideCollaborators) { // Hide Collaborators is enabled, filter out collaborators. library.artists.filter { !it.isCollaborator } } else { library.artists }) - _genresList.value = settings.libGenreSort.genres(library.genres) + _genresList.value = musicSettings.genreSort.genres(library.genres) } } @@ -156,10 +159,10 @@ class HomeViewModel(application: Application) : */ fun getSortForTab(tabMode: MusicMode) = when (tabMode) { - MusicMode.SONGS -> settings.libSongSort - MusicMode.ALBUMS -> settings.libAlbumSort - MusicMode.ARTISTS -> settings.libArtistSort - MusicMode.GENRES -> settings.libGenreSort + MusicMode.SONGS -> musicSettings.songSort + MusicMode.ALBUMS -> musicSettings.albumSort + MusicMode.ARTISTS -> musicSettings.artistSort + MusicMode.GENRES -> musicSettings.genreSort } /** @@ -171,19 +174,19 @@ class HomeViewModel(application: Application) : // Can simply re-sort the current list of items without having to access the library. when (_currentTabMode.value) { MusicMode.SONGS -> { - settings.libSongSort = sort + musicSettings.songSort = sort _songsList.value = sort.songs(_songsList.value) } MusicMode.ALBUMS -> { - settings.libAlbumSort = sort + musicSettings.albumSort = sort _albumsLists.value = sort.albums(_albumsLists.value) } MusicMode.ARTISTS -> { - settings.libArtistSort = sort + musicSettings.artistSort = sort _artistsList.value = sort.artists(_artistsList.value) } MusicMode.GENRES -> { - settings.libGenreSort = sort + musicSettings.genreSort = sort _genresList.value = sort.genres(_genresList.value) } } @@ -203,5 +206,6 @@ class HomeViewModel(application: Application) : * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in * the same way as the configuration. */ - private fun makeTabModes() = settings.libTabs.filterIsInstance().map { it.mode } + private fun makeTabModes() = + homeSettings.homeTabs.filterIsInstance().map { it.mode } } 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 175ac2706..011ad304c 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 @@ -34,6 +34,7 @@ import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SyncListDiffer import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.util.collectImmediately 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 8b9db0d83..2ab60aad5 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 @@ -37,9 +37,9 @@ import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Sort +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.secsToMs -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.collectImmediately /** @@ -130,7 +130,7 @@ class SongListFragment : } override fun onRealClick(item: Song) { - when (Settings(requireContext()).libPlaybackMode) { + when (PlaybackSettings.from(requireContext()).inListPlaybackMode) { MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ARTISTS -> playbackModel.playFromArtist(item) 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 393a4e182..e514413a4 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,8 +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.home.HomeSettings import org.oxycblt.auxio.list.EditableListListener -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD @@ -46,13 +46,13 @@ class TabCustomizeDialog : .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> logD("Committing tab changes") - Settings(requireContext()).libTabs = tabAdapter.tabs + HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs } .setNegativeButton(R.string.lbl_cancel, null) } override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { - var tabs = Settings(requireContext()).libTabs + var tabs = HomeSettings.from(requireContext()).homeTabs // Try to restore a pending tab configuration that was saved prior. if (savedInstanceState != null) { val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS)) diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt new file mode 100644 index 000000000..cffa6df22 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageSettings.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.image + +import android.content.Context +import androidx.core.content.edit +import org.oxycblt.auxio.R +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.logD + +/** + * User configuration specific to image loading. + * @author Alexander Capehart (OxygenCobalt) + */ +interface ImageSettings : Settings { + /** The strategy to use when loading album covers. */ + val coverMode: CoverMode + + private class Real(context: Context) : Settings.Real(context), ImageSettings { + override val coverMode: CoverMode + get() = + CoverMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) + ?: CoverMode.MEDIA_STORE + + override fun migrate() { + // Show album covers and Ignore MediaStore covers were unified in 3.0.0 + if (sharedPreferences.contains(OLD_KEY_SHOW_COVERS) || + sharedPreferences.contains(OLD_KEY_QUALITY_COVERS)) { + logD("Migrating cover settings") + + val mode = + when { + !sharedPreferences.getBoolean(OLD_KEY_SHOW_COVERS, true) -> CoverMode.OFF + !sharedPreferences.getBoolean(OLD_KEY_QUALITY_COVERS, true) -> + CoverMode.MEDIA_STORE + else -> CoverMode.QUALITY + } + + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_cover_mode), mode.intCode) + remove(OLD_KEY_SHOW_COVERS) + remove(OLD_KEY_QUALITY_COVERS) + } + } + } + + private companion object { + const val OLD_KEY_SHOW_COVERS = "KEY_SHOW_COVERS" + const val OLD_KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" + } + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): ImageSettings = Real(context) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt index f5df8f9bb..5da781bb3 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/PlaybackIndicatorView.kt @@ -28,7 +28,7 @@ import androidx.core.widget.ImageViewCompat import com.google.android.material.shape.MaterialShapeDrawable import kotlin.math.max import org.oxycblt.auxio.R -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDrawableCompat @@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val indicatorMatrix = Matrix() private val indicatorMatrixSrc = RectF() private val indicatorMatrixDst = RectF() - private val settings = Settings(context) /** * The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius @@ -62,7 +61,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr set(value) { field = value (background as? MaterialShapeDrawable)?.let { bg -> - if (settings.roundMode) { + if (UISettings.from(context).roundMode) { bg.setCornerSize(value) } else { bg.setCornerSize(0f) diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 590404a6a..d838a7b63 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getDrawableCompat @@ -81,7 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr background = MaterialShapeDrawable().apply { fillColor = context.getColorCompat(R.color.sel_cover_bg) - if (Settings(context).roundMode) { + if (UISettings.from(context).roundMode) { // Only use the specified corner radius when round mode is enabled. setCornerSize(cornerRadius) } 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 31a4bda55..b26141f7b 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 @@ -29,8 +29,8 @@ import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.image.CoverMode +import org.oxycblt.auxio.image.ImageSettings import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -47,10 +47,8 @@ object Covers { * loading failed or should not occur. */ suspend fun fetch(context: Context, album: Album): InputStream? { - val settings = Settings(context) - return try { - when (settings.coverMode) { + when (ImageSettings.from(context).coverMode) { CoverMode.OFF -> null CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album) CoverMode.QUALITY -> fetchQualityCovers(context, album) 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 925d79bb7..cb42d096e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore /** * A [ViewModel] that manages the current selection. diff --git a/app/src/main/java/org/oxycblt/auxio/music/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/Library.kt index 6bbb91023..e85bc28d6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Library.kt @@ -20,9 +20,9 @@ package org.oxycblt.auxio.music import android.content.Context import android.net.Uri import android.provider.OpenableColumns +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.useQuery -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD /** @@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.logD * * @author Alexander Capehart */ -class Library(rawSongs: List, settings: Settings) { +class Library(rawSongs: List, settings: MusicSettings) { /** All [Song]s that were detected on the device. */ val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) }) /** All [Album]s found on the device. */ 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 ad24c9b64..3c8c031be 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -33,7 +33,6 @@ import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.parsing.parseId3GenreNames import org.oxycblt.auxio.music.parsing.parseMultiValue import org.oxycblt.auxio.music.storage.* -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.unlikelyToBeNull @@ -308,10 +307,10 @@ sealed class MusicParent : Music() { /** * A song. Perhaps the foundation of the entirety of Auxio. * @param raw The [Song.Raw] to derive the member data from. - * @param settings [Settings] to determine the artist configuration. + * @param musicSettings [MusicSettings] to perform further user-configured parsing. * @author Alexander Capehart (OxygenCobalt) */ -class Song constructor(raw: Raw, settings: Settings) : Music() { +class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() { override val uid = // Attempt to use a MusicBrainz ID first before falling back to a hashed UID. raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } @@ -381,10 +380,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { val album: Album get() = unlikelyToBeNull(_album) - private val artistMusicBrainzIds = - raw.artistMusicBrainzIds.parseMultiValue(settings.musicSeparators) - private val artistNames = raw.artistNames.parseMultiValue(settings.musicSeparators) - private val artistSortNames = raw.artistSortNames.parseMultiValue(settings.musicSeparators) + private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings) + private val artistNames = raw.artistNames.parseMultiValue(musicSettings) + private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings) private val rawArtists = artistNames.mapIndexed { i, name -> Artist.Raw( @@ -394,10 +392,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { } private val albumArtistMusicBrainzIds = - raw.albumArtistMusicBrainzIds.parseMultiValue(settings.musicSeparators) - private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings.musicSeparators) - private val albumArtistSortNames = - raw.albumArtistSortNames.parseMultiValue(settings.musicSeparators) + raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings) + private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings) + private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings) private val rawAlbumArtists = albumArtistNames.mapIndexed { i, name -> Artist.Raw( @@ -465,7 +462,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(), name = requireNotNull(raw.albumName) { "Invalid raw: No album name" }, sortName = raw.albumSortName, - type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings.musicSeparators)), + type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)), rawArtists = rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) @@ -484,7 +481,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() { */ val _rawGenres = raw.genreNames - .parseId3GenreNames(settings.musicSeparators) + .parseId3GenreNames(musicSettings) .map { Genre.Raw(it) } .ifEmpty { listOf(Genre.Raw()) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt new file mode 100644 index 000000000..6de1e1a63 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 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 android.os.storage.StorageManager +import androidx.core.content.edit +import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.storage.Directory +import org.oxycblt.auxio.music.storage.MusicDirectories +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.getSystemServiceCompat + +/** + * User configuration specific to music system. + * @author Alexander Capehart (OxygenCobalt) + */ +interface MusicSettings : Settings { + /** The configuration on how to handle particular directories in the music library. */ + var musicDirs: MusicDirectories + /** Whether to exclude non-music audio files from the music library. */ + val excludeNonMusic: Boolean + /** Whether to be actively watching for changes in the music library. */ + val shouldBeObserving: Boolean + /** A [String] of characters representing the desired characters to denote multi-value tags. */ + var multiValueSeparators: String + /** The [Sort] mode used in [Song] lists. */ + var songSort: Sort + /** The [Sort] mode used in [Album] lists. */ + var albumSort: Sort + /** The [Sort] mode used in [Artist] lists. */ + var artistSort: Sort + /** The [Sort] mode used in [Genre] lists. */ + var genreSort: Sort + /** The [Sort] mode used in an [Album]'s [Song] list. */ + var albumSongSort: Sort + /** The [Sort] mode used in an [Artist]'s [Song] list. */ + var artistSongSort: Sort + /** The [Sort] mode used in an [Genre]'s [Song] list. */ + var genreSongSort: Sort + + private class Real(context: Context) : Settings.Real(context), MusicSettings { + private val storageManager = context.getSystemServiceCompat(StorageManager::class) + + override var musicDirs: MusicDirectories + get() { + val dirs = + (sharedPreferences.getStringSet( + context.getString(R.string.set_key_music_dirs), null) + ?: emptySet()) + .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } + return MusicDirectories( + dirs, + sharedPreferences.getBoolean( + context.getString(R.string.set_key_music_dirs_include), false)) + } + set(value) { + sharedPreferences.edit { + putStringSet( + context.getString(R.string.set_key_music_dirs), + value.dirs.map(Directory::toDocumentTreeUri).toSet()) + putBoolean( + context.getString(R.string.set_key_music_dirs_include), value.shouldInclude) + apply() + } + } + + override val excludeNonMusic: Boolean + get() = + sharedPreferences.getBoolean( + context.getString(R.string.set_key_exclude_non_music), true) + + override val shouldBeObserving: Boolean + get() = + sharedPreferences.getBoolean(context.getString(R.string.set_key_observing), false) + + override var multiValueSeparators: String + // Differ from convention and store a string of separator characters instead of an int + // code. This makes it easier to use and more extendable. + get() = + sharedPreferences.getString(context.getString(R.string.set_key_separators), "") + ?: "" + set(value) { + sharedPreferences.edit { + putString(context.getString(R.string.set_key_separators), value) + apply() + } + } + + override var songSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByName, true) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode) + apply() + } + } + + override var albumSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByName, true) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode) + apply() + } + } + + override var artistSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByName, true) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) + apply() + } + } + + override var genreSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByName, true) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode) + apply() + } + } + + override var albumSongSort: Sort + get() { + var sort = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByDisc, true) + + // Correct legacy album sort modes to Disc + if (sort.mode is Sort.Mode.ByName) { + sort = sort.withMode(Sort.Mode.ByDisc) + } + + return sort + } + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode) + apply() + } + } + + override var artistSongSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByDate, false) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode) + apply() + } + } + + override var genreSongSort: Sort + get() = + Sort.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)) + ?: Sort(Sort.Mode.ByName, true) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode) + apply() + } + } + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): MusicSettings = Real(context) + } +} 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 bc811f508..62b983672 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 @@ -28,6 +28,7 @@ import androidx.core.database.getIntOrNull import androidx.core.database.getStringOrNull import java.io.File import org.oxycblt.auxio.music.Date +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.storage.Directory @@ -37,7 +38,6 @@ import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.storage.safeQuery import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.useQuery -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.nonZeroOrNull @@ -86,20 +86,20 @@ abstract class MediaStoreExtractor( open fun init(): Cursor { val start = System.currentTimeMillis() cacheExtractor.init() - val settings = Settings(context) + val musicSettings = MusicSettings.from(context) val storageManager = context.getSystemServiceCompat(StorageManager::class) val args = mutableListOf() var selector = BASE_SELECTOR // Filter out audio that is not music, if enabled. - if (settings.excludeNonMusic) { + if (musicSettings.excludeNonMusic) { logD("Excluding non-music") selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1" } // Set up the projection to follow the music directory configuration. - val dirs = settings.getMusicDirs(storageManager) + val dirs = musicSettings.musicDirs if (dirs.dirs.isNotEmpty()) { selector += " AND " if (!dirs.shouldInclude) { 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 4299a6d2b..f4a203778 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 @@ -230,7 +230,7 @@ class Task(context: Context, private val raw: Song.Raw) { * Frames. * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more * values. - * @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a + * @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a * hour/minute value from TIME. No second value is included. The latter two fields may not be * included in they cannot be parsed. Will be null if a year value could not be parsed. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/parsing/ParsingUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/parsing/ParsingUtil.kt index fe7543062..658b1cbea 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/parsing/ParsingUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/parsing/ParsingUtil.kt @@ -17,6 +17,7 @@ package org.oxycblt.auxio.music.parsing +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.util.nonZeroOrNull /// --- GENERIC PARSING --- @@ -25,12 +26,12 @@ import org.oxycblt.auxio.util.nonZeroOrNull * Parse a multi-value tag based on the user configuration. If the value is already composed of more * than one value, nothing is done. Otherwise, this function will attempt to split it based on the * user's separator preferences. - * @param separators A string of characters to split by. Can be empty. + * @param settings [MusicSettings] required to obtain user separator configuration. * @return A new list of one or more [String]s. */ -fun List.parseMultiValue(separators: String) = +fun List.parseMultiValue(settings: MusicSettings) = if (size == 1) { - first().maybeParseBySeparators(separators) + first().maybeParseBySeparators(settings) } else { // Nothing to do. this @@ -82,7 +83,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List { /** * Fix trailing whitespace or blank contents in a [String]. - * @return A string with trailing whitespace removed or null if the [String] was all whitespace or + * @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or * empty. */ fun String.correctWhitespace() = trim().ifBlank { null } @@ -95,15 +96,13 @@ fun List.correctWhitespace() = mapNotNull { it.correctWhitespace() } /** * Attempt to parse a string by the user's separator preferences. - * @param separators A string of characters to split by. Can be empty. - * @return A list of one or more [String]s that were split up by the given separators. + * @param settings [Settings] required to obtain user separator configuration. + * @return A list of one or more [String]s that were split up by the user-defined separators. */ -private fun String.maybeParseBySeparators(separators: String) = - if (separators.isNotEmpty()) { - splitEscaped { separators.contains(it) }.correctWhitespace() - } else { - listOf(this) - } +private fun String.maybeParseBySeparators(settings: MusicSettings): List { + // Get the separators the user desires. If null, there's nothing to do. + return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace() +} /// --- ID3v2 PARSING --- @@ -119,20 +118,30 @@ fun String.parseId3v2Position() = split('/', limit = 2)[0].toIntOrNull()?.nonZer * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * representations of genre fields into their named counterparts, and split up singular ID3v2-style * integer genre fields into one or more genres. - * @param separators A string of characters to split by. Can be empty. - * @return A list of one or more genre names. + * @param settings [MusicSettings] required to obtain user separator configuration. + * @return A list of one or more genre names.. */ -fun List.parseId3GenreNames(separators: String) = +fun List.parseId3GenreNames(settings: MusicSettings) = if (size == 1) { - first().parseId3MultiValueGenre(separators) + first().parseId3MultiValueGenre(settings) } else { // Nothing to split, just map any ID3v1 genres to their name counterparts. map { it.parseId3v1Genre() ?: it } } -private fun String.parseId3MultiValueGenre(separators: String) = - parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(separators) +/** + * Parse a single ID3v1/ID3v2 integer genre field into their named representations. + * @param settings [MusicSettings] required to obtain user separator configuration. + * @return A list of one or more genre names. + */ +private fun String.parseId3MultiValueGenre(settings: MusicSettings) = + parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings) +/** + * Parse an ID3v1 integer genre field. + * @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is + * "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre. + */ 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. @@ -155,6 +164,11 @@ private fun String.parseId3v1Genre(): String? { */ private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") +/** + * Parse an ID3v2 integer genre field, which has support for multiple genre values and combined + * named/integer genres. + * @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre. + */ private fun String.parseId3v2Genre(): List? { val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues val genres = mutableSetOf() diff --git a/app/src/main/java/org/oxycblt/auxio/music/parsing/SeparatorsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/parsing/SeparatorsDialog.kt index de4802a25..6289ddc43 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/parsing/SeparatorsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/parsing/SeparatorsDialog.kt @@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSeparatorsBinding -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment /** @@ -42,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment() { .setTitle(R.string.set_separators) .setNegativeButton(R.string.lbl_cancel, null) .setPositiveButton(R.string.lbl_save) { _, _ -> - Settings(requireContext()).musicSeparators = getCurrentSeparators() + MusicSettings.from(requireContext()).multiValueSeparators = getCurrentSeparators() } } @@ -59,8 +59,8 @@ class SeparatorsDialog : ViewBindingDialogFragment() { // the corresponding CheckBox for each character instead of doing an iteration // through the separator list for each CheckBox. (savedInstanceState?.getString(KEY_PENDING_SEPARATORS) - ?: Settings(requireContext()).musicSeparators) - ?.forEach { + ?: MusicSettings.from(requireContext()).multiValueSeparators) + .forEach { when (it) { Separators.COMMA -> binding.separatorComma.isChecked = true Separators.SEMICOLON -> binding.separatorSemicolon.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 15e33b3ff..3c6732f58 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 @@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.util.unlikelyToBeNull /** 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 b16db8f79..e9cb75d42 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 @@ -30,7 +30,7 @@ import androidx.core.view.isVisible import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicDirsBinding -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD @@ -56,14 +56,11 @@ class MusicDirsDialog : .setNeutralButton(R.string.lbl_add, null) .setNegativeButton(R.string.lbl_cancel, null) .setPositiveButton(R.string.lbl_save) { _, _ -> - val settings = Settings(requireContext()) - val dirs = - settings.getMusicDirs( - requireNotNull(storageManager) { "StorageManager was not available" }) + val settings = MusicSettings.from(requireContext()) val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) - if (dirs != newDirs) { + if (settings.musicDirs != newDirs) { logD("Committing changes") - settings.setMusicDirs(newDirs) + settings.musicDirs = newDirs } } } @@ -104,7 +101,7 @@ class MusicDirsDialog : itemAnimator = null } - var dirs = Settings(context).getMusicDirs(storageManager) + var dirs = MusicSettings.from(context).musicDirs if (savedInstanceState != null) { val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) if (pendingDirs != null) { 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 ac2af7502..826a77b3a 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 @@ -28,8 +28,8 @@ import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.extractor.* -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW @@ -224,7 +224,7 @@ class Indexer private constructor() { // Build the rest of the music library from the song list. This is much more powerful // and reliable compared to using MediaStore to obtain grouping information. val buildStart = System.currentTimeMillis() - val library = Library(rawSongs, Settings(context)) + val library = Library(rawSongs, MusicSettings.from(context)) logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms") return library } 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 9696b44e0..51bdd9465 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 @@ -33,11 +33,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.service.ForegroundManager -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD @@ -68,7 +68,7 @@ class IndexerService : private lateinit var observingNotification: ObservingNotification private lateinit var wakeLock: PowerManager.WakeLock private lateinit var indexerContentObserver: SystemContentObserver - private lateinit var settings: Settings + private lateinit var settings: MusicSettings override fun onCreate() { super.onCreate() @@ -83,7 +83,7 @@ class IndexerService : // 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) + settings = MusicSettings.from(this) settings.addListener(this) indexer.registerController(this) // An indeterminate indexer and a missing library implies we are extremely early diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index af4ad3af3..1ca050570 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -24,7 +24,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment @@ -66,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment() { // Set up actions binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() } - setupSecondaryActions(binding, Settings(context)) + setupSecondaryActions(binding, PlaybackSettings.from(context).playbackBarAction) // Load the track color in manually as it's unclear whether the track actually supports // using a ColorStateList in the resources. @@ -86,8 +85,8 @@ class PlaybackBarFragment : ViewBindingFragment() { binding.playbackInfo.isSelected = false } - private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) { - when (settings.playbackBarAction) { + private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) { + when (actionMode) { ActionMode.NEXT -> { binding.playbackSecondaryAction.apply { setIconResource(R.drawable.ic_skip_next_24) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt new file mode 100644 index 000000000..8832de932 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackSettings.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.playback + +import android.content.Context +import androidx.core.content.edit +import org.oxycblt.auxio.IntegerTable +import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.playback.replaygain.ReplayGainMode +import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.logD + +/** + * User configuration specific to the playback system. + * @author Alexander Capehart (OxygenCobalt) + */ +interface PlaybackSettings : Settings { + /** The action to display on the playback bar. */ + val playbackBarAction: ActionMode + /** The action to display in the playback notification. */ + val playbackNotificationAction: ActionMode + /** Whether to start playback when a headset is plugged in. */ + val headsetAutoplay: Boolean + /** The current ReplayGain configuration. */ + val replayGainMode: ReplayGainMode + /** The current ReplayGain pre-amp configuration. */ + var replayGainPreAmp: ReplayGainPreAmp + /** + * What type of MusicParent to play from when a Song is played from a list of other items. Null + * if to play from all Songs. + */ + val inListPlaybackMode: MusicMode + /** + * What type of MusicParent to play from when a Song is played from within an item (ex. like in + * the detail view). Null if to play from the item it was played in. + */ + val inParentPlaybackMode: MusicMode? + /** Whether to keep shuffle on when playing a new Song. */ + val keepShuffle: Boolean + /** Whether to rewind when the skip previous button is pressed before skipping back. */ + val rewindWithPrev: Boolean + /** Whether a song should pause after every repeat. */ + val pauseOnRepeat: Boolean + + private class Real(context: Context) : Settings.Real(context), PlaybackSettings { + override val inListPlaybackMode: MusicMode + get() = + MusicMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_library_song_playback_mode), + Int.MIN_VALUE)) + ?: MusicMode.SONGS + + override val inParentPlaybackMode: MusicMode? + get() = + MusicMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_detail_song_playback_mode), + Int.MIN_VALUE)) + + override val playbackBarAction: ActionMode + get() = + ActionMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)) + ?: ActionMode.NEXT + + override val playbackNotificationAction: ActionMode + get() = + ActionMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) + ?: ActionMode.REPEAT + + override val headsetAutoplay: Boolean + get() = + sharedPreferences.getBoolean( + context.getString(R.string.set_key_headset_autoplay), false) + + override val replayGainMode: ReplayGainMode + get() = + ReplayGainMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) + ?: ReplayGainMode.DYNAMIC + + override var replayGainPreAmp: ReplayGainPreAmp + get() = + ReplayGainPreAmp( + sharedPreferences.getFloat( + context.getString(R.string.set_key_pre_amp_with), 0f), + sharedPreferences.getFloat( + context.getString(R.string.set_key_pre_amp_without), 0f)) + set(value) { + sharedPreferences.edit { + putFloat(context.getString(R.string.set_key_pre_amp_with), value.with) + putFloat(context.getString(R.string.set_key_pre_amp_without), value.without) + apply() + } + } + + override val keepShuffle: Boolean + get() = + sharedPreferences.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) + + override val rewindWithPrev: Boolean + get() = + sharedPreferences.getBoolean(context.getString(R.string.set_key_rewind_prev), true) + + override val pauseOnRepeat: Boolean + get() = + sharedPreferences.getBoolean( + context.getString(R.string.set_key_repeat_pause), false) + + override fun migrate() { + // "Use alternate notification action" was converted to an ActionMode setting in 3.0.0. + if (sharedPreferences.contains(OLD_KEY_ALT_NOTIF_ACTION)) { + logD("Migrating $OLD_KEY_ALT_NOTIF_ACTION") + + val mode = + if (sharedPreferences.getBoolean(OLD_KEY_ALT_NOTIF_ACTION, false)) { + ActionMode.SHUFFLE + } else { + ActionMode.REPEAT + } + + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_notif_action), mode.intCode) + remove(OLD_KEY_ALT_NOTIF_ACTION) + apply() + } + } + + // PlaybackMode was converted to MusicMode in 3.0.0 + + fun Int.migratePlaybackMode() = + when (this) { + // Convert PlaybackMode into MusicMode + IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS + IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS + IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS + IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES + else -> null + } + + if (sharedPreferences.contains(OLD_KEY_LIB_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_LIB_PLAYBACK_MODE") + + val mode = + sharedPreferences + .getInt(OLD_KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS) + .migratePlaybackMode() + ?: MusicMode.SONGS + + sharedPreferences.edit { + putInt( + context.getString(R.string.set_key_library_song_playback_mode), + mode.intCode) + remove(OLD_KEY_LIB_PLAYBACK_MODE) + apply() + } + } + + if (sharedPreferences.contains(OLD_KEY_DETAIL_PLAYBACK_MODE)) { + logD("Migrating $OLD_KEY_DETAIL_PLAYBACK_MODE") + + val mode = + sharedPreferences + .getInt(OLD_KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE) + .migratePlaybackMode() + + sharedPreferences.edit { + putInt( + context.getString(R.string.set_key_detail_song_playback_mode), + mode?.intCode ?: Int.MIN_VALUE) + remove(OLD_KEY_DETAIL_PLAYBACK_MODE) + apply() + } + } + } + + companion object { + const val OLD_KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" + const val OLD_KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" + const val OLD_KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode" + } + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): PlaybackSettings = Real(context) + } +} 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 6a453726b..23f6e880d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -25,9 +25,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.oxycblt.auxio.home.HomeSettings import org.oxycblt.auxio.music.* import org.oxycblt.auxio.playback.state.* -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.context /** @@ -36,7 +36,9 @@ import org.oxycblt.auxio.util.context */ class PlaybackViewModel(application: Application) : AndroidViewModel(application), PlaybackStateManager.Listener { - private val settings = Settings(application) + private val homeSettings = HomeSettings.from(application) + private val musicSettings = MusicSettings.from(application) + private val playbackSettings = PlaybackSettings.from(application) private val playbackManager = PlaybackStateManager.getInstance() private var lastPositionJob: Job? = null @@ -249,17 +251,17 @@ class PlaybackViewModel(application: Application) : private fun playImpl( song: Song?, parent: MusicParent?, - shuffled: Boolean = playbackManager.queue.isShuffled && settings.keepShuffle + shuffled: Boolean = playbackManager.queue.isShuffled && playbackSettings.keepShuffle ) { check(song == null || parent == null || parent.songs.contains(song)) { "Song to play not in parent" } val sort = when (parent) { - is Genre -> settings.detailGenreSort - is Artist -> settings.detailArtistSort - is Album -> settings.detailAlbumSort - null -> settings.libSongSort + is Genre -> musicSettings.genreSongSort + is Artist -> musicSettings.artistSongSort + is Album -> musicSettings.albumSongSort + null -> musicSettings.songSort } playbackManager.play(song, parent, sort, shuffled) } @@ -301,7 +303,7 @@ class PlaybackViewModel(application: Application) : * @param album The [Album] to add. */ fun playNext(album: Album) { - playbackManager.playNext(settings.detailAlbumSort.songs(album.songs)) + playbackManager.playNext(musicSettings.albumSongSort.songs(album.songs)) } /** @@ -309,7 +311,7 @@ class PlaybackViewModel(application: Application) : * @param artist The [Artist] to add. */ fun playNext(artist: Artist) { - playbackManager.playNext(settings.detailArtistSort.songs(artist.songs)) + playbackManager.playNext(musicSettings.artistSongSort.songs(artist.songs)) } /** @@ -317,7 +319,7 @@ class PlaybackViewModel(application: Application) : * @param genre The [Genre] to add. */ fun playNext(genre: Genre) { - playbackManager.playNext(settings.detailGenreSort.songs(genre.songs)) + playbackManager.playNext(musicSettings.genreSongSort.songs(genre.songs)) } /** @@ -341,7 +343,7 @@ class PlaybackViewModel(application: Application) : * @param album The [Album] to add. */ fun addToQueue(album: Album) { - playbackManager.addToQueue(settings.detailAlbumSort.songs(album.songs)) + playbackManager.addToQueue(musicSettings.albumSongSort.songs(album.songs)) } /** @@ -349,7 +351,7 @@ class PlaybackViewModel(application: Application) : * @param artist The [Artist] to add. */ fun addToQueue(artist: Artist) { - playbackManager.addToQueue(settings.detailArtistSort.songs(artist.songs)) + playbackManager.addToQueue(musicSettings.artistSongSort.songs(artist.songs)) } /** @@ -357,7 +359,7 @@ class PlaybackViewModel(application: Application) : * @param genre The [Genre] to add. */ fun addToQueue(genre: Genre) { - playbackManager.addToQueue(settings.detailGenreSort.songs(genre.songs)) + playbackManager.addToQueue(musicSettings.genreSongSort.songs(genre.songs)) } /** @@ -434,9 +436,9 @@ class PlaybackViewModel(application: Application) : private fun selectionToSongs(selection: List): List { return selection.flatMap { when (it) { - is Album -> settings.detailAlbumSort.songs(it.songs) - is Artist -> settings.detailArtistSort.songs(it.songs) - is Genre -> settings.detailGenreSort.songs(it.songs) + is Album -> musicSettings.albumSongSort.songs(it.songs) + is Artist -> musicSettings.artistSongSort.songs(it.songs) + is Genre -> musicSettings.genreSongSort.songs(it.songs) is Song -> listOf(it) } } 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 5cada49e2..fa589f015 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 @@ -24,7 +24,7 @@ import androidx.appcompat.app.AlertDialog import kotlin.math.abs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment /** @@ -39,11 +39,11 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { .setTitle(R.string.set_pre_amp) .setPositiveButton(R.string.lbl_ok) { _, _ -> val binding = requireBinding() - Settings(requireContext()).replayGainPreAmp = + PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) } .setNeutralButton(R.string.lbl_reset) { _, _ -> - Settings(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f) + PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f) } .setNegativeButton(R.string.lbl_cancel, null) } @@ -53,7 +53,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(requireContext()).replayGainPreAmp + val preAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp binding.withTagsSlider.value = preAmp.with binding.withoutTagsSlider.value = preAmp.without } 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 6c29f4e0b..8461eb6a1 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 @@ -31,8 +31,8 @@ import kotlin.math.pow import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.extractor.TextTags +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD /** @@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.logD class ReplayGainAudioProcessor(private val context: Context) : BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener { private val playbackManager = PlaybackStateManager.getInstance() - private val settings = Settings(context) + private val settings = PlaybackSettings.from(context) private var lastFormat: Format? = null private var volume = 1f diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt index cb5f86284..b9ea4a594 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateDatabase.kt @@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper import android.provider.BaseColumns import androidx.core.database.sqlite.transaction import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.util.* /** 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 ce556e0a6..8a88c400e 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 @@ -21,6 +21,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE 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 b86aff5f9..c00b943b6 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 @@ -34,11 +34,11 @@ import org.oxycblt.auxio.image.BitmapProvider import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.ActionMode +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD /** @@ -59,7 +59,7 @@ class MediaSessionComponent(private val context: Context, private val listener: } private val playbackManager = PlaybackStateManager.getInstance() - private val settings = Settings(context) + private val settings = PlaybackSettings.from(context) private val notification = NotificationComponent(context, mediaSession.sessionToken) private val provider = BitmapProvider(context) 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 ece6fba6d..790445ad3 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 @@ -44,15 +44,16 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.service.ForegroundManager -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider @@ -92,7 +93,8 @@ class PlaybackService : // Managers private val playbackManager = PlaybackStateManager.getInstance() private val musicStore = MusicStore.getInstance() - private lateinit var settings: Settings + private lateinit var musicSettings: MusicSettings + private lateinit var playbackSettings: PlaybackSettings // State private lateinit var foregroundManager: ForegroundManager @@ -143,7 +145,8 @@ class PlaybackService : .also { it.addListener(this) } replayGainProcessor.addToListeners(player) // Initialize the core service components - settings = Settings(this) + musicSettings = MusicSettings.from(this) + playbackSettings = PlaybackSettings.from(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. @@ -213,7 +216,7 @@ class PlaybackService : get() = player.audioSessionId override val shouldRewindWithPrev: Boolean - get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD + get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD override fun getState(durationMs: Long) = InternalPlayer.State.from( @@ -286,7 +289,7 @@ class PlaybackService : if (playbackManager.repeatMode == RepeatMode.TRACK) { playbackManager.rewind() // May be configured to pause when we repeat a track. - if (settings.pauseOnRepeat) { + if (playbackSettings.pauseOnRepeat) { playbackManager.setPlaying(false) } } else { @@ -352,7 +355,7 @@ class PlaybackService : } // Shuffle all -> Start new playback from all songs is InternalPlayer.Action.ShuffleAll -> { - playbackManager.play(null, null, settings.libSongSort, true) + playbackManager.play(null, null, musicSettings.songSort, true) } // Open -> Try to find the Song for the given file and then play it from all songs is InternalPlayer.Action.Open -> { @@ -360,8 +363,8 @@ class PlaybackService : playbackManager.play( song, null, - settings.libSongSort, - playbackManager.queue.isShuffled && settings.keepShuffle) + musicSettings.songSort, + playbackManager.queue.isShuffled && playbackSettings.keepShuffle) } } } @@ -431,7 +434,7 @@ class PlaybackService : // ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached, // which would result in unexpected playback. Work around it by dropping the first // call to this function, which should come from that Intent. - if (settings.headsetAutoplay && + if (playbackSettings.headsetAutoplay && playbackManager.queue.currentSong != null && initialHeadsetPlugEventHandled) { logD("Device connected, resuming") 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 59f622cf5..48e037aaa 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.* /** @@ -137,7 +137,7 @@ class SearchFragment : ListFragment() { override fun onRealClick(item: Music) { when (item) { is Song -> - when (Settings(requireContext()).libPlaybackMode) { + when (PlaybackSettings.from(requireContext()).inListPlaybackMode) { MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ARTISTS -> playbackModel.playFromArtist(item) diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt new file mode 100644 index 000000000..05dac481a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.search + +import android.content.Context +import androidx.core.content.edit +import org.oxycblt.auxio.R +import org.oxycblt.auxio.music.MusicMode +import org.oxycblt.auxio.settings.Settings + +/** + * User configuration specific to the search UI. + * @author Alexander Capehart (OxygenCobalt) + */ +interface SearchSettings : Settings { + /** The type of Music the search view is currently filtering to. */ + var searchFilterMode: MusicMode? + + private class Real(context: Context) : Settings.Real(context), SearchSettings { + override var searchFilterMode: MusicMode? + get() = + MusicMode.fromIntCode( + sharedPreferences.getInt( + context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)) + set(value) { + sharedPreferences.edit { + putInt( + context.getString(R.string.set_key_search_filter), + value?.intCode ?: Int.MIN_VALUE) + apply() + } + } + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): SearchSettings = Real(context) + } +} 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 5b57859c0..1a4ea647d 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -31,7 +31,9 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.* -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.music.Library +import org.oxycblt.auxio.music.MusicStore +import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.logD @@ -42,7 +44,7 @@ import org.oxycblt.auxio.util.logD class SearchViewModel(application: Application) : AndroidViewModel(application), MusicStore.Listener { private val musicStore = MusicStore.getInstance() - private val settings = Settings(context) + private val settings = SearchSettings.from(application) private var lastQuery: String? = null private var currentSearchJob: Job? = null 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 1ce8fac29..f9a82fca7 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Auxio Project + * Copyright (c) 2023 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 @@ -19,445 +19,49 @@ 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 -import androidx.core.content.edit import androidx.preference.PreferenceManager -import org.oxycblt.auxio.IntegerTable -import org.oxycblt.auxio.R -import org.oxycblt.auxio.home.tabs.Tab -import org.oxycblt.auxio.image.CoverMode -import org.oxycblt.auxio.music.MusicMode -import org.oxycblt.auxio.music.Sort -import org.oxycblt.auxio.music.storage.Directory -import org.oxycblt.auxio.music.storage.MusicDirectories -import org.oxycblt.auxio.playback.ActionMode -import org.oxycblt.auxio.playback.replaygain.ReplayGainMode -import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp -import org.oxycblt.auxio.ui.accent.Accent -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.unlikelyToBeNull /** - * A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. 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. + * Abstract user configuration information. This interface has no functionality whatsoever. Concrete + * implementations should be preferred instead. * @author Alexander Capehart (OxygenCobalt) */ -class Settings(private val context: Context) { - private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - - /** - * Migrate any settings from an old version into their modern counterparts. This can cause data - * loss depending on the feasibility of a migration. - */ +interface Settings { + /** Migrate any settings fields from older versions into their new counterparts. */ fun migrate() { - if (inner.contains(OldKeys.KEY_ACCENT3)) { - logD("Migrating ${OldKeys.KEY_ACCENT3}") - - var accent = inner.getInt(OldKeys.KEY_ACCENT3, 5) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // Accents were previously frozen as soon as the OS was updated to android twelve, - // as dynamic colors were enabled by default. This is no longer the case, so we need - // to re-update the setting to dynamic colors here. - accent = 16 - } - - inner.edit { - putInt(context.getString(R.string.set_key_accent), accent) - remove(OldKeys.KEY_ACCENT3) - apply() - } - } - - if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) { - logD("Migrating cover settings") - - val mode = - when { - !inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF - !inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE - else -> CoverMode.QUALITY - } - - inner.edit { - putInt(context.getString(R.string.set_key_cover_mode), mode.intCode) - remove(OldKeys.KEY_SHOW_COVERS) - remove(OldKeys.KEY_QUALITY_COVERS) - } - } - - if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) { - logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}") - - val mode = - if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) { - ActionMode.SHUFFLE - } else { - ActionMode.REPEAT - } - - inner.edit { - putInt(context.getString(R.string.set_key_notif_action), mode.intCode) - remove(OldKeys.KEY_ALT_NOTIF_ACTION) - apply() - } - } - - fun Int.migratePlaybackMode() = - when (this) { - // Convert PlaybackMode into MusicMode - IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS - IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS - IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS - IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES - else -> null - } - - if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) { - logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}") - - val mode = - inner - .getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS) - .migratePlaybackMode() - ?: MusicMode.SONGS - - inner.edit { - putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode) - remove(OldKeys.KEY_LIB_PLAYBACK_MODE) - apply() - } - } - - if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) { - logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}") - - val mode = - inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode() - - inner.edit { - putInt( - context.getString(R.string.set_key_detail_song_playback_mode), - mode?.intCode ?: Int.MIN_VALUE) - remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE) - apply() - } - } + throw NotImplementedError() } /** * Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates. * @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add. */ - fun addListener(listener: OnSharedPreferenceChangeListener) { - inner.registerOnSharedPreferenceChangeListener(listener) + fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + throw NotImplementedError() } /** * Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further * settings updates from being sent to ti.t */ - fun removeListener(listener: OnSharedPreferenceChangeListener) { - inner.unregisterOnSharedPreferenceChangeListener(listener) - } - - // --- VALUES --- - - /** The current theme. Represented by the [AppCompatDelegate] constants. */ - val theme: Int - get() = - inner.getInt( - context.getString(R.string.set_key_theme), - AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - - /** Whether to use a black background when a dark theme is currently used. */ - val useBlackTheme: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false) - - /** The current [Accent] (Color Scheme). */ - var accent: Accent - get() = - Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT)) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_accent), value.index) - apply() - } - } - - /** The tabs to show in the home UI. */ - var libTabs: Array - get() = - Tab.fromIntCode( - inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)) - ?: unlikelyToBeNull(Tab.fromIntCode(Tab.SEQUENCE_DEFAULT)) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_lib_tabs), Tab.toIntCode(value)) - apply() - } - } - - /** Whether to hide artists considered "collaborators" from the home UI. */ - val shouldHideCollaborators: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_hide_collaborators), false) - - /** Whether to round additional UI elements that require album covers to be rounded. */ - val roundMode: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_round_mode), false) - - /** The action to display on the playback bar. */ - val playbackBarAction: ActionMode - get() = - ActionMode.fromIntCode( - inner.getInt(context.getString(R.string.set_key_bar_action), Int.MIN_VALUE)) - ?: ActionMode.NEXT - - /** The action to display in the playback notification. */ - val playbackNotificationAction: ActionMode - get() = - ActionMode.fromIntCode( - inner.getInt(context.getString(R.string.set_key_notif_action), Int.MIN_VALUE)) - ?: ActionMode.REPEAT - - /** Whether to start playback when a headset is plugged in. */ - val headsetAutoplay: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_headset_autoplay), false) - - /** The current ReplayGain configuration. */ - val replayGainMode: ReplayGainMode - get() = - ReplayGainMode.fromIntCode( - inner.getInt(context.getString(R.string.set_key_replay_gain), Int.MIN_VALUE)) - ?: ReplayGainMode.DYNAMIC - - /** The current ReplayGain pre-amp configuration. */ - var replayGainPreAmp: ReplayGainPreAmp - get() = - ReplayGainPreAmp( - inner.getFloat(context.getString(R.string.set_key_pre_amp_with), 0f), - inner.getFloat(context.getString(R.string.set_key_pre_amp_without), 0f)) - set(value) { - inner.edit { - putFloat(context.getString(R.string.set_key_pre_amp_with), value.with) - putFloat(context.getString(R.string.set_key_pre_amp_without), value.without) - apply() - } - } - - /** What MusicParent item to play from when a Song is played from the home view. */ - val libPlaybackMode: MusicMode - get() = - MusicMode.fromIntCode( - inner.getInt( - context.getString(R.string.set_key_library_song_playback_mode), Int.MIN_VALUE)) - ?: MusicMode.SONGS - - /** - * What MusicParent item to play from when a Song is played from the detail view. Will be null - * if configured to play from the currently shown item. - */ - val detailPlaybackMode: MusicMode? - get() = - MusicMode.fromIntCode( - inner.getInt( - context.getString(R.string.set_key_detail_song_playback_mode), Int.MIN_VALUE)) - - /** Whether to keep shuffle on when playing a new Song. */ - val keepShuffle: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) - - /** Whether to rewind when the skip previous button is pressed before skipping back. */ - val rewindWithPrev: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true) - - /** Whether a song should pause after every repeat. */ - val pauseOnRepeat: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) - - /** Whether to be actively watching for changes in the music library. */ - val shouldBeObserving: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_observing), false) - - /** The strategy used when loading album covers. */ - val coverMode: CoverMode - get() = - CoverMode.fromIntCode( - inner.getInt(context.getString(R.string.set_key_cover_mode), Int.MIN_VALUE)) - ?: CoverMode.MEDIA_STORE - - /** Whether to exclude non-music audio files from the music library. */ - val excludeNonMusic: Boolean - get() = inner.getBoolean(context.getString(R.string.set_key_exclude_non_music), true) - - /** - * Set the configuration on how to handle particular directories in the music library. - * @param storageManager [StorageManager] required to parse directories. - * @return The [MusicDirectories] configuration. - */ - fun getMusicDirs(storageManager: StorageManager): MusicDirectories { - val dirs = - (inner.getStringSet(context.getString(R.string.set_key_music_dirs), null) ?: emptySet()) - .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } - return MusicDirectories( - dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)) + fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + throw NotImplementedError() } /** - * Set the configuration on how to handle particular directories in the music library. - * @param musicDirs The new [MusicDirectories] configuration. + * A framework-backed [Settings] implementation. + * @param context [Context] required. */ - fun setMusicDirs(musicDirs: MusicDirectories) { - inner.edit { - putStringSet( - context.getString(R.string.set_key_music_dirs), - musicDirs.dirs.map(Directory::toDocumentTreeUri).toSet()) - putBoolean( - context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude) - apply() - } - } + abstract class Real(protected val context: Context) : Settings { + protected val sharedPreferences: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context.applicationContext) - /** - * A string of characters representing the desired separator characters to denote multi-value - * tags. - */ - var musicSeparators: String - // Differ from convention and store a string of separator characters instead of an int - // code. This makes it easier to use in Regexes and makes it more extendable. - get() = inner.getString(context.getString(R.string.set_key_separators), "") ?: "" - set(value) { - inner.edit { - putString(context.getString(R.string.set_key_separators), value) - apply() - } + override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + sharedPreferences.registerOnSharedPreferenceChangeListener(listener) } - /** The type of Music the search view is currently filtering to. */ - var searchFilterMode: MusicMode? - get() = - MusicMode.fromIntCode( - inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)) - set(value) { - inner.edit { - putInt( - context.getString(R.string.set_key_search_filter), - value?.intCode ?: Int.MIN_VALUE) - apply() - } + override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } - - /** The Song [Sort] mode used in the Home UI. */ - var libSongSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByName, true) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode) - apply() - } - } - - /** The Album [Sort] mode used in the Home UI. */ - var libAlbumSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByName, true) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode) - apply() - } - } - - /** The Artist [Sort] mode used in the Home UI. */ - var libArtistSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByName, true) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) - apply() - } - } - - /** The Genre [Sort] mode used in the Home UI. */ - var libGenreSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByName, true) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode) - apply() - } - } - - /** The [Sort] mode used in the Album Detail UI. */ - var detailAlbumSort: Sort - get() { - var sort = - Sort.fromIntCode( - inner.getInt( - context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByDisc, true) - - // Correct legacy album sort modes to Disc - if (sort.mode is Sort.Mode.ByName) { - sort = sort.withMode(Sort.Mode.ByDisc) - } - - return sort - } - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode) - apply() - } - } - - /** The [Sort] mode used in the Artist Detail UI. */ - var detailArtistSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByDate, false) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode) - apply() - } - } - - /** The [Sort] mode used in the Genre Detail UI. */ - var detailGenreSort: Sort - get() = - Sort.fromIntCode( - inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByName, true) - set(value) { - inner.edit { - putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode) - apply() - } - } - - /** Legacy keys that are no longer used, but still have to be migrated. */ - private object OldKeys { - const val KEY_ACCENT3 = "auxio_accent" - const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION" - const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS" - const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" - const val KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" - const val KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode" } } 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 3eebc3176..cb7bdc82c 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 @@ -32,8 +32,8 @@ import coil.Coil import org.oxycblt.auxio.R import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.SettingsFragmentDirections +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -149,8 +149,6 @@ class PreferenceFragment : PreferenceFragmentCompat() { } private fun setupPreference(preference: Preference) { - val settings = Settings(requireContext()) - if (!preference.isVisible) { // Nothing to do. return @@ -170,7 +168,7 @@ class PreferenceFragment : PreferenceFragmentCompat() { } } getString(R.string.set_key_accent) -> { - preference.summary = getString(settings.accent.name) + preference.summary = getString(UISettings.from(requireContext()).accent.name) } getString(R.string.set_key_black_theme) -> { preference.onPreferenceChangeListener = diff --git a/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt new file mode 100644 index 000000000..0faf91fbc --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/ui/UISettings.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 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.ui + +import android.content.Context +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit +import org.oxycblt.auxio.R +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.accent.Accent +import org.oxycblt.auxio.util.logD + +interface UISettings : Settings { + /** The current theme. Represented by the AppCompatDelegate constants. */ + val theme: Int + /** Whether to use a black background when a dark theme is currently used. */ + val useBlackTheme: Boolean + /** The current [Accent] (Color Scheme). */ + var accent: Accent + /** Whether to round additional UI elements that require album covers to be rounded. */ + val roundMode: Boolean + + private class Real(context: Context) : Settings.Real(context), UISettings { + override val theme: Int + get() = + sharedPreferences.getInt( + context.getString(R.string.set_key_theme), + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + override val useBlackTheme: Boolean + get() = + sharedPreferences.getBoolean(context.getString(R.string.set_key_black_theme), false) + + override var accent: Accent + get() = + Accent.from( + sharedPreferences.getInt( + context.getString(R.string.set_key_accent), Accent.DEFAULT)) + set(value) { + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_accent), value.index) + apply() + } + } + + override val roundMode: Boolean + get() = + sharedPreferences.getBoolean(context.getString(R.string.set_key_round_mode), false) + + override fun migrate() { + if (sharedPreferences.contains(OLD_KEY_ACCENT3)) { + logD("Migrating $OLD_KEY_ACCENT3") + + var accent = sharedPreferences.getInt(OLD_KEY_ACCENT3, 5) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // Accents were previously frozen as soon as the OS was updated to android + // twelve, + // as dynamic colors were enabled by default. This is no longer the case, so we + // need + // to re-update the setting to dynamic colors here. + accent = 16 + } + + sharedPreferences.edit { + putInt(context.getString(R.string.set_key_accent), accent) + remove(OLD_KEY_ACCENT3) + apply() + } + } + } + + companion object { + const val OLD_KEY_ACCENT3 = "auxio_accent" + } + } + + companion object { + /** + * Get a framework-backed implementation. + * @param context [Context] required. + */ + fun from(context: Context): UISettings = Real(context) + } +} 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 f4d6237cc..c1ec29ad2 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 @@ -25,7 +25,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.list.ClickableListListener -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -44,7 +44,7 @@ class AccentCustomizeDialog : builder .setTitle(R.string.set_accent) .setPositiveButton(R.string.lbl_ok) { _, _ -> - val settings = Settings(requireContext()) + val settings = UISettings.from(requireContext()) if (accentAdapter.selectedAccent == settings.accent) { // Nothing to do. return@setPositiveButton @@ -65,7 +65,7 @@ class AccentCustomizeDialog : if (savedInstanceState != null) { Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) } else { - Settings(requireContext()).accent + UISettings.from(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 6b11f4488..a4c62022d 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.getDimenPixels import org.oxycblt.auxio.util.logD @@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD class WidgetComponent(private val context: Context) : PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener { private val playbackManager = PlaybackStateManager.getInstance() - private val settings = Settings(context) + private val settings = UISettings.from(context) private val widgetProvider = WidgetProvider() private val provider = BitmapProvider(context) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt index 4c5259d9a..a333ec2f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetProvider.kt @@ -31,7 +31,6 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.system.PlaybackService -import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.* /** @@ -197,7 +196,7 @@ class WidgetProvider : AppWidgetProvider() { // Below API 31, enable a rounded bar only if round mode is enabled. // On API 31+, the bar should always be round in order to fit in with other widgets. val background = - if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (useRoundedRemoteViews(context)) { R.drawable.ui_widget_bar_round } else { R.drawable.ui_widget_bar_system @@ -216,7 +215,7 @@ class WidgetProvider : AppWidgetProvider() { // On API 31+, the background should always be round in order to fit in with other // widgets. val background = - if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (useRoundedRemoteViews(context)) { R.drawable.ui_widget_bg_round } else { R.drawable.ui_widget_bg_system 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 58f75b0a0..81b4dd6ac 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetUtil.kt @@ -27,6 +27,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.LayoutRes import kotlin.math.sqrt +import org.oxycblt.auxio.ui.UISettings import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.newMainPendingIntent @@ -132,3 +133,12 @@ fun AppWidgetManager.updateAppWidgetCompat( } } } + +/** + * Returns whether rounded UI elements are appropriate for the widget, either based on the current + * settings or if the widget has to fit in aesthetically with other widgets. + * @param context [Context] configuration to use. + * @return true if to use round mode, false otherwise. + */ +fun useRoundedRemoteViews(context: Context) = + UISettings.from(context).roundMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S diff --git a/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt b/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt index d88825eb1..721cfd629 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/DateTest.kt @@ -126,8 +126,8 @@ class DateTest { @Test fun date_fromYearDate() { - assertEquals("2016", Date.from(2016).toString()) - assertEquals("2016", Date.from("2016").toString()) + assertEquals("2016-08-16", Date.from(20160816).toString()) + assertEquals("2016-08-16", Date.from("20160816").toString()) } @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt new file mode 100644 index 000000000..795e7b29c --- /dev/null +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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 org.oxycblt.auxio.music.storage.MusicDirectories + +interface FakeMusicSettings : MusicSettings { + override var musicDirs: MusicDirectories + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override val excludeNonMusic: Boolean + get() = throw NotImplementedError() + override val shouldBeObserving: Boolean + get() = throw NotImplementedError() + override var multiValueSeparators: String + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var songSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var albumSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var artistSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var genreSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var albumSongSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var artistSongSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() + override var genreSongSort: Sort + get() = throw NotImplementedError() + set(_) = throw NotImplementedError() +} diff --git a/app/src/test/java/org/oxycblt/auxio/music/parsing/ParsingUtilTest.kt b/app/src/test/java/org/oxycblt/auxio/music/parsing/ParsingUtilTest.kt index 64e7463d4..687653a14 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/parsing/ParsingUtilTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/parsing/ParsingUtilTest.kt @@ -19,22 +19,27 @@ package org.oxycblt.auxio.music.parsing import org.junit.Assert.assertEquals import org.junit.Test +import org.oxycblt.auxio.music.FakeMusicSettings class ParsingUtilTest { @Test fun parseMultiValue_single() { - assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(",")) + assertEquals( + listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(SeparatorMusicSettings(","))) } @Test fun parseMultiValue_many() { - assertEquals(listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(",")) + assertEquals( + listOf("a", "b", "c"), + listOf("a", "b", "c").parseMultiValue(SeparatorMusicSettings(","))) } @Test fun parseMultiValue_several() { assertEquals( - listOf("a", "b", "c", "d", "e", "f"), listOf("a,b;c/d+e&f").parseMultiValue(",;/+&")) + listOf("a", "b", "c", "d", "e", "f"), + listOf("a,b;c/d+e&f").parseMultiValue(SeparatorMusicSettings(",;/+&"))) } @Test @@ -105,37 +110,45 @@ class ParsingUtilTest { fun parseId3v2Genre_multi() { assertEquals( listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(",")) + listOf("Post-Rock", "Shoegaze", "Glitch") + .parseId3GenreNames(SeparatorMusicSettings(","))) } @Test fun parseId3v2Genre_multiId3v1() { assertEquals( listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("176", "178", "Glitch").parseId3GenreNames(",")) + listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(","))) } @Test fun parseId3v2Genre_wackId3() { - assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(",")) + assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(","))) } @Test fun parseId3v2Genre_singleId3v23() { assertEquals( listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"), - listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(",")) + listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(","))) } @Test fun parseId3v2Genre_singleSeparated() { assertEquals( listOf("Post-Rock", "Shoegaze", "Glitch"), - listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(",")) + listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(","))) } @Test fun parsId3v2Genre_singleId3v1() { - assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(",")) + assertEquals( + listOf("Post-Rock"), listOf("176").parseId3GenreNames(SeparatorMusicSettings(","))) + } + + class SeparatorMusicSettings(private val separators: String) : FakeMusicSettings { + override var multiValueSeparators: String + get() = separators + set(_) = throw NotImplementedError() } }