settings: decouple

Decouple the settings god object into feature-specific settings.

This should make testing settings-dependent code much easier, as it no
longer requires a context.
This commit is contained in:
Alexander Capehart 2023-01-06 16:17:57 -07:00
parent 3502af33e7
commit 1b19b698a1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
51 changed files with 1038 additions and 597 deletions

View file

@ -18,7 +18,9 @@
package org.oxycblt.auxio package org.oxycblt.auxio
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
/** /**
@ -28,5 +30,10 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class StubTest { class StubTest {
// TODO: Add tests // TODO: Make tests
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("org.oxycblt.auxio", appContext.packageName)
}
} }

View file

@ -25,12 +25,14 @@ import androidx.core.graphics.drawable.IconCompat
import coil.ImageLoader import coil.ImageLoader
import coil.ImageLoaderFactory import coil.ImageLoaderFactory
import coil.request.CachePolicy import coil.request.CachePolicy
import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher import org.oxycblt.auxio.image.extractor.AlbumCoverFetcher
import org.oxycblt.auxio.image.extractor.ArtistImageFetcher import org.oxycblt.auxio.image.extractor.ArtistImageFetcher
import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory import org.oxycblt.auxio.image.extractor.ErrorCrossfadeTransitionFactory
import org.oxycblt.auxio.image.extractor.GenreImageFetcher import org.oxycblt.auxio.image.extractor.GenreImageFetcher
import org.oxycblt.auxio.image.extractor.MusicKeyer 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. * Auxio: A simple, rational music player for android.
@ -40,7 +42,9 @@ class AuxioApp : Application(), ImageLoaderFactory {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Migrate any settings that may have changed in an app update. // 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 // Adding static shortcuts in a dynamic manner is better than declaring them
// manually, as it will properly handle the difference between debug and release // manually, as it will properly handle the difference between debug and release
// Auxio instances. // Auxio instances.

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.system.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.system.PlaybackService 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.androidViewModels
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -81,7 +81,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun setupTheme() { private fun setupTheme() {
val settings = Settings(this) val settings = UISettings.from(this)
// Apply the theme configuration. // Apply the theme configuration.
AppCompatDelegate.setDefaultNightMode(settings.theme) AppCompatDelegate.setDefaultNightMode(settings.theme)
// Apply the color scheme. The black theme requires it's own set of themes since // Apply the color scheme. The black theme requires it's own set of themes since

View file

@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
@ -123,7 +123,7 @@ class AlbumDetailFragment :
override fun onRealClick(item: Music) { override fun onRealClick(item: Music) {
val song = requireIs<Song>(item) val song = requireIs<Song>(item)
when (Settings(requireContext()).detailPlaybackMode) { when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
// "Play from shown item" and "Play from album" functionally have the same // "Play from shown item" and "Play from album" functionally have the same
// behavior since a song can only have one album. // behavior since a song can only have one album.
null, null,
@ -149,12 +149,12 @@ class AlbumDetailFragment :
override fun onOpenSortMenu(anchor: View) { override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_album_sort) { 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(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
detailModel.albumSort = detailModel.albumSortSort =
if (item.itemId == R.id.option_sort_asc) { if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked) sort.withAscending(item.isChecked)
} else { } else {

View file

@ -37,7 +37,7 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort 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.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -123,7 +123,7 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
override fun onRealClick(item: Music) { override fun onRealClick(item: Music) {
when (item) { when (item) {
is Song -> { is Song -> {
when (Settings(requireContext()).detailPlaybackMode) { when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
// When configured to play from the selected item, we already have an Artist // When configured to play from the selected item, we already have an Artist
// to play from. // to play from.
null -> null ->
@ -158,13 +158,13 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
override fun onOpenSortMenu(anchor: View) { override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_artist_sort) { 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(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
detailModel.artistSort = detailModel.artistSongSort =
if (item.itemId == R.id.option_sort_asc) { if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked) sort.withAscending(item.isChecked)
} else { } else {

View file

@ -33,8 +33,10 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.* 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.music.storage.MimeType
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
@ -47,7 +49,7 @@ import org.oxycblt.auxio.util.*
class DetailViewModel(application: Application) : class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener { AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settings = Settings(application) private val musicSettings = MusicSettings.from(application)
private var currentSongJob: Job? = null private var currentSongJob: Job? = null
@ -75,10 +77,10 @@ class DetailViewModel(application: Application) :
get() = _albumList get() = _albumList
/** The current [Sort] used for [Song]s in [albumList]. */ /** The current [Sort] used for [Song]s in [albumList]. */
var albumSort: Sort var albumSortSort: Sort
get() = settings.detailAlbumSort get() = musicSettings.albumSongSort
set(value) { set(value) {
settings.detailAlbumSort = value musicSettings.albumSongSort = value
// Refresh the album list to reflect the new sort. // Refresh the album list to reflect the new sort.
currentAlbum.value?.let(::refreshAlbumList) currentAlbum.value?.let(::refreshAlbumList)
} }
@ -95,10 +97,10 @@ class DetailViewModel(application: Application) :
val artistList: StateFlow<List<Item>> = _artistList val artistList: StateFlow<List<Item>> = _artistList
/** The current [Sort] used for [Song]s in [artistList]. */ /** The current [Sort] used for [Song]s in [artistList]. */
var artistSort: Sort var artistSongSort: Sort
get() = settings.detailArtistSort get() = musicSettings.artistSongSort
set(value) { set(value) {
settings.detailArtistSort = value musicSettings.artistSongSort = value
// Refresh the artist list to reflect the new sort. // Refresh the artist list to reflect the new sort.
currentArtist.value?.let(::refreshArtistList) currentArtist.value?.let(::refreshArtistList)
} }
@ -115,10 +117,10 @@ class DetailViewModel(application: Application) :
val genreList: StateFlow<List<Item>> = _genreList val genreList: StateFlow<List<Item>> = _genreList
/** The current [Sort] used for [Song]s in [genreList]. */ /** The current [Sort] used for [Song]s in [genreList]. */
var genreSort: Sort var genreSongSort: Sort
get() = settings.detailGenreSort get() = musicSettings.genreSongSort
set(value) { set(value) {
settings.detailGenreSort = value musicSettings.genreSongSort = value
// Refresh the genre list to reflect the new sort. // Refresh the genre list to reflect the new sort.
currentGenre.value?.let(::refreshGenreList) 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 // 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. // 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. // Songs without disc tags become part of Disc 1.
val byDisc = songs.groupBy { it.disc ?: 1 } val byDisc = songs.groupBy { it.disc ?: 1 }
if (byDisc.size > 1) { if (byDisc.size > 1) {
@ -363,7 +365,7 @@ class DetailViewModel(application: Application) :
if (artist.songs.isNotEmpty()) { if (artist.songs.isNotEmpty()) {
logD("Songs present in this artist, adding header") logD("Songs present in this artist, adding header")
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
data.addAll(artistSort.songs(artist.songs)) data.addAll(artistSongSort.songs(artist.songs))
} }
_artistList.value = data.toList() _artistList.value = data.toList()
@ -376,7 +378,7 @@ class DetailViewModel(application: Application) :
data.add(Header(R.string.lbl_artists)) data.add(Header(R.string.lbl_artists))
data.addAll(genre.artists) data.addAll(genre.artists)
data.add(SortHeader(R.string.lbl_songs)) data.add(SortHeader(R.string.lbl_songs))
data.addAll(genreSort.songs(genre.songs)) data.addAll(genreSongSort.songs(genre.songs))
_genreList.value = data _genreList.value = data
} }

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort 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.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -123,7 +123,7 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
when (item) { when (item) {
is Artist -> navModel.exploreNavigateTo(item) is Artist -> navModel.exploreNavigateTo(item)
is Song -> is Song ->
when (Settings(requireContext()).detailPlaybackMode) { when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
// When configured to play from the selected item, we already have a Genre // When configured to play from the selected item, we already have a Genre
// to play from. // to play from.
null -> null ->
@ -156,12 +156,12 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
override fun onOpenSortMenu(anchor: View) { override fun onOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_genre_sort) { 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(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
item.isChecked = !item.isChecked item.isChecked = !item.isChecked
detailModel.genreSort = detailModel.genreSongSort =
if (item.itemId == R.id.option_sort_asc) { if (item.itemId == R.id.option_sort_asc) {
sort.withAscending(item.isChecked) sort.withAscending(item.isChecked)
} else { } else {

View file

@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.list.selection.SelectionFragment import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.* 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.music.system.Indexer
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Tab>
/** 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<Tab>
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)
}
}

View file

@ -25,7 +25,9 @@ import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.* 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.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -38,7 +40,8 @@ class HomeViewModel(application: Application) :
MusicStore.Listener, MusicStore.Listener,
SharedPreferences.OnSharedPreferenceChangeListener { SharedPreferences.OnSharedPreferenceChangeListener {
private val musicStore = MusicStore.getInstance() 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<Song>()) private val _songsList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */ /** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
@ -89,13 +92,13 @@ class HomeViewModel(application: Application) :
init { init {
musicStore.addListener(this) musicStore.addListener(this)
settings.addListener(this) homeSettings.addListener(this)
} }
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
musicStore.removeListener(this) musicStore.removeListener(this)
settings.removeListener(this) homeSettings.removeListener(this)
} }
override fun onLibraryChanged(library: Library?) { override fun onLibraryChanged(library: Library?) {
@ -103,17 +106,17 @@ class HomeViewModel(application: Application) :
logD("Library changed, refreshing library") logD("Library changed, refreshing library")
// Get the each list of items in the library to use as our list data. // Get the each list of items in the library to use as our list data.
// Applying the preferred sorting to them. // Applying the preferred sorting to them.
_songsList.value = settings.libSongSort.songs(library.songs) _songsList.value = musicSettings.songSort.songs(library.songs)
_albumsLists.value = settings.libAlbumSort.albums(library.albums) _albumsLists.value = musicSettings.albumSort.albums(library.albums)
_artistsList.value = _artistsList.value =
settings.libArtistSort.artists( musicSettings.artistSort.artists(
if (settings.shouldHideCollaborators) { if (homeSettings.shouldHideCollaborators) {
// Hide Collaborators is enabled, filter out collaborators. // Hide Collaborators is enabled, filter out collaborators.
library.artists.filter { !it.isCollaborator } library.artists.filter { !it.isCollaborator }
} else { } else {
library.artists 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) = fun getSortForTab(tabMode: MusicMode) =
when (tabMode) { when (tabMode) {
MusicMode.SONGS -> settings.libSongSort MusicMode.SONGS -> musicSettings.songSort
MusicMode.ALBUMS -> settings.libAlbumSort MusicMode.ALBUMS -> musicSettings.albumSort
MusicMode.ARTISTS -> settings.libArtistSort MusicMode.ARTISTS -> musicSettings.artistSort
MusicMode.GENRES -> settings.libGenreSort 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. // Can simply re-sort the current list of items without having to access the library.
when (_currentTabMode.value) { when (_currentTabMode.value) {
MusicMode.SONGS -> { MusicMode.SONGS -> {
settings.libSongSort = sort musicSettings.songSort = sort
_songsList.value = sort.songs(_songsList.value) _songsList.value = sort.songs(_songsList.value)
} }
MusicMode.ALBUMS -> { MusicMode.ALBUMS -> {
settings.libAlbumSort = sort musicSettings.albumSort = sort
_albumsLists.value = sort.albums(_albumsLists.value) _albumsLists.value = sort.albums(_albumsLists.value)
} }
MusicMode.ARTISTS -> { MusicMode.ARTISTS -> {
settings.libArtistSort = sort musicSettings.artistSort = sort
_artistsList.value = sort.artists(_artistsList.value) _artistsList.value = sort.artists(_artistsList.value)
} }
MusicMode.GENRES -> { MusicMode.GENRES -> {
settings.libGenreSort = sort musicSettings.genreSort = sort
_genresList.value = sort.genres(_genresList.value) _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 * @return A list of the [MusicMode]s for each visible [Tab] in the configuration, ordered in
* the same way as the configuration. * the same way as the configuration.
*/ */
private fun makeTabModes() = settings.libTabs.filterIsInstance<Tab.Visible>().map { it.mode } private fun makeTabModes() =
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.mode }
} }

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.list.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately

View file

@ -37,9 +37,9 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
/** /**
@ -130,7 +130,7 @@ class SongListFragment :
} }
override fun onRealClick(item: Song) { override fun onRealClick(item: Song) {
when (Settings(requireContext()).libPlaybackMode) { when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item) MusicMode.ARTISTS -> playbackModel.playFromArtist(item)

View file

@ -25,8 +25,8 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.list.EditableListListener import org.oxycblt.auxio.list.EditableListListener
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -46,13 +46,13 @@ class TabCustomizeDialog :
.setTitle(R.string.set_lib_tabs) .setTitle(R.string.set_lib_tabs)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
logD("Committing tab changes") logD("Committing tab changes")
Settings(requireContext()).libTabs = tabAdapter.tabs HomeSettings.from(requireContext()).homeTabs = tabAdapter.tabs
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
override fun onBindingCreated(binding: DialogTabsBinding, savedInstanceState: Bundle?) { 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. // Try to restore a pending tab configuration that was saved prior.
if (savedInstanceState != null) { if (savedInstanceState != null) {
val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS)) val savedTabs = Tab.fromIntCode(savedInstanceState.getInt(KEY_TABS))

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -28,7 +28,7 @@ import androidx.core.widget.ImageViewCompat
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import kotlin.math.max import kotlin.math.max
import org.oxycblt.auxio.R 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.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val indicatorMatrix = Matrix() private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF() private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = 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 * 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) { set(value) {
field = value field = value
(background as? MaterialShapeDrawable)?.let { bg -> (background as? MaterialShapeDrawable)?.let { bg ->
if (settings.roundMode) { if (UISettings.from(context).roundMode) {
bg.setCornerSize(value) bg.setCornerSize(value)
} else { } else {
bg.setCornerSize(0f) bg.setCornerSize(0f)

View file

@ -39,7 +39,7 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song 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.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
@ -81,7 +81,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
background = background =
MaterialShapeDrawable().apply { MaterialShapeDrawable().apply {
fillColor = context.getColorCompat(R.color.sel_cover_bg) 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. // Only use the specified corner radius when round mode is enabled.
setCornerSize(cornerRadius) setCornerSize(cornerRadius)
} }

View file

@ -29,8 +29,8 @@ import java.io.InputStream
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.image.CoverMode import org.oxycblt.auxio.image.CoverMode
import org.oxycblt.auxio.image.ImageSettings
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
@ -47,10 +47,8 @@ object Covers {
* loading failed or should not occur. * loading failed or should not occur.
*/ */
suspend fun fetch(context: Context, album: Album): InputStream? { suspend fun fetch(context: Context, album: Album): InputStream? {
val settings = Settings(context)
return try { return try {
when (settings.coverMode) { when (ImageSettings.from(context).coverMode) {
CoverMode.OFF -> null CoverMode.OFF -> null
CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album) CoverMode.MEDIA_STORE -> fetchMediaStoreCovers(context, album)
CoverMode.QUALITY -> fetchQualityCovers(context, album) CoverMode.QUALITY -> fetchQualityCovers(context, album)

View file

@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
/** /**
* A [ViewModel] that manages the current selection. * A [ViewModel] that manages the current selection.

View file

@ -20,9 +20,9 @@ package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -33,7 +33,7 @@ import org.oxycblt.auxio.util.logD
* *
* @author Alexander Capehart * @author Alexander Capehart
*/ */
class Library(rawSongs: List<Song.Raw>, settings: Settings) { class Library(rawSongs: List<Song.Raw>, settings: MusicSettings) {
/** All [Song]s that were detected on the device. */ /** All [Song]s that were detected on the device. */
val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) }) val songs = Sort(Sort.Mode.ByName, true).songs(rawSongs.map { Song(it, settings) })
/** All [Album]s found on the device. */ /** All [Album]s found on the device. */

View file

@ -33,7 +33,6 @@ import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.parsing.parseId3GenreNames import org.oxycblt.auxio.music.parsing.parseId3GenreNames
import org.oxycblt.auxio.music.parsing.parseMultiValue import org.oxycblt.auxio.music.parsing.parseMultiValue
import org.oxycblt.auxio.music.storage.* import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -308,10 +307,10 @@ sealed class MusicParent : Music() {
/** /**
* A song. Perhaps the foundation of the entirety of Auxio. * A song. Perhaps the foundation of the entirety of Auxio.
* @param raw The [Song.Raw] to derive the member data from. * @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) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Song constructor(raw: Raw, settings: Settings) : Music() { class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
override val uid = override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID. // Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) } raw.musicBrainzId?.toUuidOrNull()?.let { UID.musicBrainz(MusicMode.SONGS, it) }
@ -381,10 +380,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val album: Album val album: Album
get() = unlikelyToBeNull(_album) get() = unlikelyToBeNull(_album)
private val artistMusicBrainzIds = private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings)
raw.artistMusicBrainzIds.parseMultiValue(settings.musicSeparators) private val artistNames = raw.artistNames.parseMultiValue(musicSettings)
private val artistNames = raw.artistNames.parseMultiValue(settings.musicSeparators) private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings)
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings.musicSeparators)
private val rawArtists = private val rawArtists =
artistNames.mapIndexed { i, name -> artistNames.mapIndexed { i, name ->
Artist.Raw( Artist.Raw(
@ -394,10 +392,9 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
} }
private val albumArtistMusicBrainzIds = private val albumArtistMusicBrainzIds =
raw.albumArtistMusicBrainzIds.parseMultiValue(settings.musicSeparators) raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings.musicSeparators) private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings)
private val albumArtistSortNames = private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings)
raw.albumArtistSortNames.parseMultiValue(settings.musicSeparators)
private val rawAlbumArtists = private val rawAlbumArtists =
albumArtistNames.mapIndexed { i, name -> albumArtistNames.mapIndexed { i, name ->
Artist.Raw( Artist.Raw(
@ -465,7 +462,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" }, name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName, sortName = raw.albumSortName,
type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings.musicSeparators)), type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)),
rawArtists = rawArtists =
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
@ -484,7 +481,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
*/ */
val _rawGenres = val _rawGenres =
raw.genreNames raw.genreNames
.parseId3GenreNames(settings.musicSeparators) .parseId3GenreNames(musicSettings)
.map { Genre.Raw(it) } .map { Genre.Raw(it) }
.ifEmpty { listOf(Genre.Raw()) } .ifEmpty { listOf(Genre.Raw()) }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -28,6 +28,7 @@ import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import java.io.File import java.io.File
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.parsing.parseId3v2Position
import org.oxycblt.auxio.music.storage.Directory 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.safeQuery
import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.storageVolumesCompat
import org.oxycblt.auxio.music.storage.useQuery import org.oxycblt.auxio.music.storage.useQuery
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
@ -86,20 +86,20 @@ abstract class MediaStoreExtractor(
open fun init(): Cursor { open fun init(): Cursor {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
cacheExtractor.init() cacheExtractor.init()
val settings = Settings(context) val musicSettings = MusicSettings.from(context)
val storageManager = context.getSystemServiceCompat(StorageManager::class) val storageManager = context.getSystemServiceCompat(StorageManager::class)
val args = mutableListOf<String>() val args = mutableListOf<String>()
var selector = BASE_SELECTOR var selector = BASE_SELECTOR
// Filter out audio that is not music, if enabled. // Filter out audio that is not music, if enabled.
if (settings.excludeNonMusic) { if (musicSettings.excludeNonMusic) {
logD("Excluding non-music") logD("Excluding non-music")
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1" selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
} }
// Set up the projection to follow the music directory configuration. // Set up the projection to follow the music directory configuration.
val dirs = settings.getMusicDirs(storageManager) val dirs = musicSettings.musicDirs
if (dirs.dirs.isNotEmpty()) { if (dirs.dirs.isNotEmpty()) {
selector += " AND " selector += " AND "
if (!dirs.shouldInclude) { if (!dirs.shouldInclude) {

View file

@ -230,7 +230,7 @@ class Task(context: Context, private val raw: Song.Raw) {
* Frames. * Frames.
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more * @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values. * 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 * 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. * included in they cannot be parsed. Will be null if a year value could not be parsed.
*/ */

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.music.parsing package org.oxycblt.auxio.music.parsing
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
/// --- GENERIC PARSING --- /// --- 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 * 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 * than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences. * 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. * @return A new list of one or more [String]s.
*/ */
fun List<String>.parseMultiValue(separators: String) = fun List<String>.parseMultiValue(settings: MusicSettings) =
if (size == 1) { if (size == 1) {
first().maybeParseBySeparators(separators) first().maybeParseBySeparators(settings)
} else { } else {
// Nothing to do. // Nothing to do.
this this
@ -82,7 +83,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
/** /**
* Fix trailing whitespace or blank contents in a [String]. * 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. * empty.
*/ */
fun String.correctWhitespace() = trim().ifBlank { null } fun String.correctWhitespace() = trim().ifBlank { null }
@ -95,15 +96,13 @@ fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/** /**
* Attempt to parse a string by the user's separator preferences. * Attempt to parse a string by the user's separator preferences.
* @param separators A string of characters to split by. Can be empty. * @param settings [Settings] required to obtain user separator configuration.
* @return A list of one or more [String]s that were split up by the given separators. * @return A list of one or more [String]s that were split up by the user-defined separators.
*/ */
private fun String.maybeParseBySeparators(separators: String) = private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
if (separators.isNotEmpty()) { // Get the separators the user desires. If null, there's nothing to do.
splitEscaped { separators.contains(it) }.correctWhitespace() return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
} else { }
listOf(this)
}
/// --- ID3v2 PARSING --- /// --- 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 * 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 * representations of genre fields into their named counterparts, and split up singular ID3v2-style
* integer genre fields into one or more genres. * integer genre fields into one or more genres.
* @param separators A string of characters to split by. Can be empty. * @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more genre names. * @return A list of one or more genre names..
*/ */
fun List<String>.parseId3GenreNames(separators: String) = fun List<String>.parseId3GenreNames(settings: MusicSettings) =
if (size == 1) { if (size == 1) {
first().parseId3MultiValueGenre(separators) first().parseId3MultiValueGenre(settings)
} else { } else {
// Nothing to split, just map any ID3v1 genres to their name counterparts. // Nothing to split, just map any ID3v1 genres to their name counterparts.
map { it.parseId3v1Genre() ?: it } 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? { private fun String.parseId3v1Genre(): String? {
// ID3v1 genres are a plain integer value without formatting, so in that case // ID3v1 genres are a plain integer value without formatting, so in that case
// try to index the genre table with such. // 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)\\))*)(.+)?") 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<String>? { private fun String.parseId3v2Genre(): List<String>? {
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
val genres = mutableSetOf<String>() val genres = mutableSetOf<String>()

View file

@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
/** /**
@ -42,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
.setTitle(R.string.set_separators) .setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
Settings(requireContext()).musicSeparators = getCurrentSeparators() MusicSettings.from(requireContext()).multiValueSeparators = getCurrentSeparators()
} }
} }
@ -59,8 +59,8 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// the corresponding CheckBox for each character instead of doing an iteration // the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox. // through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS) (savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
?: Settings(requireContext()).musicSeparators) ?: MusicSettings.from(requireContext()).multiValueSeparators)
?.forEach { .forEach {
when (it) { when (it) {
Separators.COMMA -> binding.separatorComma.isChecked = true Separators.COMMA -> binding.separatorComma.isChecked = true
Separators.SEMICOLON -> binding.separatorSemicolon.isChecked = true Separators.SEMICOLON -> binding.separatorSemicolon.isChecked = true

View file

@ -21,6 +21,8 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
/** /**

View file

@ -30,7 +30,7 @@ import androidx.core.view.isVisible
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding 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.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -56,14 +56,11 @@ class MusicDirsDialog :
.setNeutralButton(R.string.lbl_add, null) .setNeutralButton(R.string.lbl_add, null)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val settings = Settings(requireContext()) val settings = MusicSettings.from(requireContext())
val dirs =
settings.getMusicDirs(
requireNotNull(storageManager) { "StorageManager was not available" })
val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding())) val newDirs = MusicDirectories(dirAdapter.dirs, isUiModeInclude(requireBinding()))
if (dirs != newDirs) { if (settings.musicDirs != newDirs) {
logD("Committing changes") logD("Committing changes")
settings.setMusicDirs(newDirs) settings.musicDirs = newDirs
} }
} }
} }
@ -104,7 +101,7 @@ class MusicDirsDialog :
itemAnimator = null itemAnimator = null
} }
var dirs = Settings(context).getMusicDirs(storageManager) var dirs = MusicSettings.from(context).musicDirs
if (savedInstanceState != null) { if (savedInstanceState != null) {
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
if (pendingDirs != null) { if (pendingDirs != null) {

View file

@ -28,8 +28,8 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.extractor.* import org.oxycblt.auxio.music.extractor.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW 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 // 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. // and reliable compared to using MediaStore to obtain grouping information.
val buildStart = System.currentTimeMillis() 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") logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
return library return library
} }

View file

@ -33,11 +33,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.storage.contentResolverSafe import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -68,7 +68,7 @@ class IndexerService :
private lateinit var observingNotification: ObservingNotification private lateinit var observingNotification: ObservingNotification
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var indexerContentObserver: SystemContentObserver private lateinit var indexerContentObserver: SystemContentObserver
private lateinit var settings: Settings private lateinit var settings: MusicSettings
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -83,7 +83,7 @@ class IndexerService :
// Initialize any listener-dependent components last as we wouldn't want a listener race // 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. // condition to cause us to load music before we were fully initialize.
indexerContentObserver = SystemContentObserver() indexerContentObserver = SystemContentObserver()
settings = Settings(this) settings = MusicSettings.from(this)
settings.addListener(this) settings.addListener(this)
indexer.registerController(this) indexer.registerController(this)
// An indeterminate indexer and a missing library implies we are extremely early // An indeterminate indexer and a missing library implies we are extremely early

View file

@ -24,7 +24,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.ViewBindingFragment import org.oxycblt.auxio.ui.ViewBindingFragment
@ -66,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
// Set up actions // Set up actions
binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() } 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 // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources. // using a ColorStateList in the resources.
@ -86,8 +85,8 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
binding.playbackInfo.isSelected = false binding.playbackInfo.isSelected = false
} }
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) { private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, actionMode: ActionMode) {
when (settings.playbackBarAction) { when (actionMode) {
ActionMode.NEXT -> { ActionMode.NEXT -> {
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24) setIconResource(R.drawable.ic_skip_next_24)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -25,9 +25,9 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.home.HomeSettings
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.playback.state.* import org.oxycblt.auxio.playback.state.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
/** /**
@ -36,7 +36,9 @@ import org.oxycblt.auxio.util.context
*/ */
class PlaybackViewModel(application: Application) : class PlaybackViewModel(application: Application) :
AndroidViewModel(application), PlaybackStateManager.Listener { 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 val playbackManager = PlaybackStateManager.getInstance()
private var lastPositionJob: Job? = null private var lastPositionJob: Job? = null
@ -249,17 +251,17 @@ class PlaybackViewModel(application: Application) :
private fun playImpl( private fun playImpl(
song: Song?, song: Song?,
parent: MusicParent?, 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)) { check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent" "Song to play not in parent"
} }
val sort = val sort =
when (parent) { when (parent) {
is Genre -> settings.detailGenreSort is Genre -> musicSettings.genreSongSort
is Artist -> settings.detailArtistSort is Artist -> musicSettings.artistSongSort
is Album -> settings.detailAlbumSort is Album -> musicSettings.albumSongSort
null -> settings.libSongSort null -> musicSettings.songSort
} }
playbackManager.play(song, parent, sort, shuffled) playbackManager.play(song, parent, sort, shuffled)
} }
@ -301,7 +303,7 @@ class PlaybackViewModel(application: Application) :
* @param album The [Album] to add. * @param album The [Album] to add.
*/ */
fun playNext(album: Album) { 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. * @param artist The [Artist] to add.
*/ */
fun playNext(artist: Artist) { 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. * @param genre The [Genre] to add.
*/ */
fun playNext(genre: Genre) { 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. * @param album The [Album] to add.
*/ */
fun addToQueue(album: Album) { 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. * @param artist The [Artist] to add.
*/ */
fun addToQueue(artist: Artist) { 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. * @param genre The [Genre] to add.
*/ */
fun addToQueue(genre: Genre) { 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<Music>): List<Song> { private fun selectionToSongs(selection: List<Music>): List<Song> {
return selection.flatMap { return selection.flatMap {
when (it) { when (it) {
is Album -> settings.detailAlbumSort.songs(it.songs) is Album -> musicSettings.albumSongSort.songs(it.songs)
is Artist -> settings.detailArtistSort.songs(it.songs) is Artist -> musicSettings.artistSongSort.songs(it.songs)
is Genre -> settings.detailGenreSort.songs(it.songs) is Genre -> musicSettings.genreSongSort.songs(it.songs)
is Song -> listOf(it) is Song -> listOf(it)
} }
} }

View file

@ -24,7 +24,7 @@ import androidx.appcompat.app.AlertDialog
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.ui.ViewBindingDialogFragment
/** /**
@ -39,11 +39,11 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
.setTitle(R.string.set_pre_amp) .setTitle(R.string.set_pre_amp)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
val binding = requireBinding() val binding = requireBinding()
Settings(requireContext()).replayGainPreAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp =
ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value)
} }
.setNeutralButton(R.string.lbl_reset) { _, _ -> .setNeutralButton(R.string.lbl_reset) { _, _ ->
Settings(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f) PlaybackSettings.from(requireContext()).replayGainPreAmp = ReplayGainPreAmp(0f, 0f)
} }
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
} }
@ -53,7 +53,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment<DialogPreAmpBinding>() {
// First initialization, we need to supply the sliders with the values from // 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 // settings. After this, the sliders save their own state, so we do not need to
// do any restore behavior. // do any restore behavior.
val preAmp = Settings(requireContext()).replayGainPreAmp val preAmp = PlaybackSettings.from(requireContext()).replayGainPreAmp
binding.withTagsSlider.value = preAmp.with binding.withTagsSlider.value = preAmp.with
binding.withoutTagsSlider.value = preAmp.without binding.withoutTagsSlider.value = preAmp.without
} }

View file

@ -31,8 +31,8 @@ import kotlin.math.pow
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.extractor.TextTags import org.oxycblt.auxio.music.extractor.TextTags
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.logD
class ReplayGainAudioProcessor(private val context: Context) : class ReplayGainAudioProcessor(private val context: Context) :
BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener { BaseAudioProcessor(), Player.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context) private val settings = PlaybackSettings.from(context)
private var lastFormat: Format? = null private var lastFormat: Format? = null
private var volume = 1f private var volume = 1f

View file

@ -24,6 +24,7 @@ import android.database.sqlite.SQLiteOpenHelper
import android.provider.BaseColumns import android.provider.BaseColumns
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**

View file

@ -21,6 +21,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.* 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.playback.state.PlaybackStateManager.Listener
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE

View file

@ -34,11 +34,11 @@ import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.ActionMode 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.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.Queue
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD 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 playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context) private val settings = PlaybackSettings.from(context)
private val notification = NotificationComponent(context, mediaSession.sessionToken) private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)

View file

@ -44,15 +44,16 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Library import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.InternalPlayer import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider import org.oxycblt.auxio.widgets.WidgetProvider
@ -92,7 +93,8 @@ class PlaybackService :
// Managers // Managers
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private lateinit var settings: Settings private lateinit var musicSettings: MusicSettings
private lateinit var playbackSettings: PlaybackSettings
// State // State
private lateinit var foregroundManager: ForegroundManager private lateinit var foregroundManager: ForegroundManager
@ -143,7 +145,8 @@ class PlaybackService :
.also { it.addListener(this) } .also { it.addListener(this) }
replayGainProcessor.addToListeners(player) replayGainProcessor.addToListeners(player)
// Initialize the core service components // Initialize the core service components
settings = Settings(this) musicSettings = MusicSettings.from(this)
playbackSettings = PlaybackSettings.from(this)
foregroundManager = ForegroundManager(this) foregroundManager = ForegroundManager(this)
// Initialize any listener-dependent components last as we wouldn't want a listener race // 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. // condition to cause us to load music before we were fully initialize.
@ -213,7 +216,7 @@ class PlaybackService :
get() = player.audioSessionId get() = player.audioSessionId
override val shouldRewindWithPrev: Boolean override val shouldRewindWithPrev: Boolean
get() = settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
override fun getState(durationMs: Long) = override fun getState(durationMs: Long) =
InternalPlayer.State.from( InternalPlayer.State.from(
@ -286,7 +289,7 @@ class PlaybackService :
if (playbackManager.repeatMode == RepeatMode.TRACK) { if (playbackManager.repeatMode == RepeatMode.TRACK) {
playbackManager.rewind() playbackManager.rewind()
// May be configured to pause when we repeat a track. // May be configured to pause when we repeat a track.
if (settings.pauseOnRepeat) { if (playbackSettings.pauseOnRepeat) {
playbackManager.setPlaying(false) playbackManager.setPlaying(false)
} }
} else { } else {
@ -352,7 +355,7 @@ class PlaybackService :
} }
// Shuffle all -> Start new playback from all songs // Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> { 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 // Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> { is InternalPlayer.Action.Open -> {
@ -360,8 +363,8 @@ class PlaybackService :
playbackManager.play( playbackManager.play(
song, song,
null, null,
settings.libSongSort, musicSettings.songSort,
playbackManager.queue.isShuffled && settings.keepShuffle) playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
} }
} }
} }
@ -431,7 +434,7 @@ class PlaybackService :
// ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached, // ACTION_HEADSET_PLUG will fire when this BroadcastReciever is initially attached,
// which would result in unexpected playback. Work around it by dropping the first // which would result in unexpected playback. Work around it by dropping the first
// call to this function, which should come from that Intent. // call to this function, which should come from that Intent.
if (settings.headsetAutoplay && if (playbackSettings.headsetAutoplay &&
playbackManager.queue.currentSong != null && playbackManager.queue.currentSong != null &&
initialHeadsetPlugEventHandled) { initialHeadsetPlugEventHandled) {
logD("Device connected, resuming") logD("Device connected, resuming")

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
/** /**
@ -137,7 +137,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
override fun onRealClick(item: Music) { override fun onRealClick(item: Music) {
when (item) { when (item) {
is Song -> is Song ->
when (Settings(requireContext()).libPlaybackMode) { when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item) MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item) MusicMode.ARTISTS -> playbackModel.playFromArtist(item)

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -31,7 +31,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.* 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.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -42,7 +44,7 @@ import org.oxycblt.auxio.util.logD
class SearchViewModel(application: Application) : class SearchViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener { AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()
private val settings = Settings(context) private val settings = SearchSettings.from(application)
private var lastQuery: String? = null private var lastQuery: String? = null
private var currentSearchJob: Job? = null private var currentSearchJob: Job? = null

View file

@ -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 * 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 * 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.Context
import android.content.SharedPreferences 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 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 * Abstract user configuration information. This interface has no functionality whatsoever. Concrete
* mutability is dependent on how they are used in app. Immutable members are often only modified by * implementations should be preferred instead.
* the preferences view, while mutable members are modified elsewhere.
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
class Settings(private val context: Context) { interface Settings {
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) /** Migrate any settings fields from older versions into their new counterparts. */
/**
* Migrate any settings from an old version into their modern counterparts. This can cause data
* loss depending on the feasibility of a migration.
*/
fun migrate() { fun migrate() {
if (inner.contains(OldKeys.KEY_ACCENT3)) { throw NotImplementedError()
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()
}
}
} }
/** /**
* Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates. * Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
* @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add. * @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
*/ */
fun addListener(listener: OnSharedPreferenceChangeListener) { fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
inner.registerOnSharedPreferenceChangeListener(listener) throw NotImplementedError()
} }
/** /**
* Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further * Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
* settings updates from being sent to ti.t * settings updates from being sent to ti.t
*/ */
fun removeListener(listener: OnSharedPreferenceChangeListener) { fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
inner.unregisterOnSharedPreferenceChangeListener(listener) throw NotImplementedError()
}
// --- 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<Tab>
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))
} }
/** /**
* Set the configuration on how to handle particular directories in the music library. * A framework-backed [Settings] implementation.
* @param musicDirs The new [MusicDirectories] configuration. * @param context [Context] required.
*/ */
fun setMusicDirs(musicDirs: MusicDirectories) { abstract class Real(protected val context: Context) : Settings {
inner.edit { protected val sharedPreferences: SharedPreferences =
putStringSet( PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
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()
}
}
/** override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
* A string of characters representing the desired separator characters to denote multi-value sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
* 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()
}
} }
/** The type of Music the search view is currently filtering to. */ override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
var searchFilterMode: MusicMode? sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
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()
}
} }
/** 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"
} }
} }

View file

@ -32,8 +32,8 @@ import coil.Coil
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicViewModel import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.SettingsFragmentDirections import org.oxycblt.auxio.settings.SettingsFragmentDirections
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -149,8 +149,6 @@ class PreferenceFragment : PreferenceFragmentCompat() {
} }
private fun setupPreference(preference: Preference) { private fun setupPreference(preference: Preference) {
val settings = Settings(requireContext())
if (!preference.isVisible) { if (!preference.isVisible) {
// Nothing to do. // Nothing to do.
return return
@ -170,7 +168,7 @@ class PreferenceFragment : PreferenceFragmentCompat() {
} }
} }
getString(R.string.set_key_accent) -> { 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) -> { getString(R.string.set_key_black_theme) -> {
preference.onPreferenceChangeListener = preference.onPreferenceChangeListener =

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -25,7 +25,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.list.ClickableListListener 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.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -44,7 +44,7 @@ class AccentCustomizeDialog :
builder builder
.setTitle(R.string.set_accent) .setTitle(R.string.set_accent)
.setPositiveButton(R.string.lbl_ok) { _, _ -> .setPositiveButton(R.string.lbl_ok) { _, _ ->
val settings = Settings(requireContext()) val settings = UISettings.from(requireContext())
if (accentAdapter.selectedAccent == settings.accent) { if (accentAdapter.selectedAccent == settings.accent) {
// Nothing to do. // Nothing to do.
return@setPositiveButton return@setPositiveButton
@ -65,7 +65,7 @@ class AccentCustomizeDialog :
if (savedInstanceState != null) { if (savedInstanceState != null) {
Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT))
} else { } else {
Settings(requireContext()).accent UISettings.from(requireContext()).accent
}) })
} }

View file

@ -32,7 +32,7 @@ import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Queue import org.oxycblt.auxio.playback.state.Queue
import org.oxycblt.auxio.playback.state.RepeatMode 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.getDimenPixels
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.logD
class WidgetComponent(private val context: Context) : class WidgetComponent(private val context: Context) :
PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener { PlaybackStateManager.Listener, SharedPreferences.OnSharedPreferenceChangeListener {
private val playbackManager = PlaybackStateManager.getInstance() private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context) private val settings = UISettings.from(context)
private val widgetProvider = WidgetProvider() private val widgetProvider = WidgetProvider()
private val provider = BitmapProvider(context) private val provider = BitmapProvider(context)

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.system.PlaybackService import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.* 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. // 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. // On API 31+, the bar should always be round in order to fit in with other widgets.
val background = val background =
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (useRoundedRemoteViews(context)) {
R.drawable.ui_widget_bar_round R.drawable.ui_widget_bar_round
} else { } else {
R.drawable.ui_widget_bar_system 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 // On API 31+, the background should always be round in order to fit in with other
// widgets. // widgets.
val background = val background =
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { if (useRoundedRemoteViews(context)) {
R.drawable.ui_widget_bg_round R.drawable.ui_widget_bg_round
} else { } else {
R.drawable.ui_widget_bg_system R.drawable.ui_widget_bg_system

View file

@ -27,6 +27,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import kotlin.math.sqrt import kotlin.math.sqrt
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isLandscape import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent 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

View file

@ -126,8 +126,8 @@ class DateTest {
@Test @Test
fun date_fromYearDate() { fun date_fromYearDate() {
assertEquals("2016", Date.from(2016).toString()) assertEquals("2016-08-16", Date.from(20160816).toString())
assertEquals("2016", Date.from("2016").toString()) assertEquals("2016-08-16", Date.from("20160816").toString())
} }
@Test @Test

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}

View file

@ -19,22 +19,27 @@ package org.oxycblt.auxio.music.parsing
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.FakeMusicSettings
class ParsingUtilTest { class ParsingUtilTest {
@Test @Test
fun parseMultiValue_single() { 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 @Test
fun parseMultiValue_many() { 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 @Test
fun parseMultiValue_several() { fun parseMultiValue_several() {
assertEquals( 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 @Test
@ -105,37 +110,45 @@ class ParsingUtilTest {
fun parseId3v2Genre_multi() { fun parseId3v2Genre_multi() {
assertEquals( assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"), listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(",")) listOf("Post-Rock", "Shoegaze", "Glitch")
.parseId3GenreNames(SeparatorMusicSettings(",")))
} }
@Test @Test
fun parseId3v2Genre_multiId3v1() { fun parseId3v2Genre_multiId3v1() {
assertEquals( assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"), listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("176", "178", "Glitch").parseId3GenreNames(",")) listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
} }
@Test @Test
fun parseId3v2Genre_wackId3() { fun parseId3v2Genre_wackId3() {
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(",")) assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(",")))
} }
@Test @Test
fun parseId3v2Genre_singleId3v23() { fun parseId3v2Genre_singleId3v23() {
assertEquals( assertEquals(
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"), listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(",")) listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
} }
@Test @Test
fun parseId3v2Genre_singleSeparated() { fun parseId3v2Genre_singleSeparated() {
assertEquals( assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"), listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(",")) listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
} }
@Test @Test
fun parsId3v2Genre_singleId3v1() { 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()
} }
} }