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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,6 +50,8 @@ import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy
import org.oxycblt.auxio.list.selection.SelectionFragment
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel

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

View file

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

View file

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

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 kotlin.math.max
import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.getDrawableCompat
@ -52,7 +52,6 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
private val indicatorMatrix = Matrix()
private val indicatorMatrixSrc = RectF()
private val indicatorMatrixDst = RectF()
private val settings = Settings(context)
/**
* The corner radius of this view. This allows the outer ImageGroup to apply it's corner radius
@ -62,7 +61,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
set(value) {
field = value
(background as? MaterialShapeDrawable)?.let { bg ->
if (settings.roundMode) {
if (UISettings.from(context).roundMode) {
bg.setCornerSize(value)
} else {
bg.setCornerSize(0f)

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -230,7 +230,7 @@ class Task(context: Context, private val raw: Song.Raw) {
* Frames.
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values.
* @retrn A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
* @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
* hour/minute value from TIME. No second value is included. The latter two fields may not be
* included in they cannot be parsed. Will be null if a year value could not be parsed.
*/

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.music.parsing
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.util.nonZeroOrNull
/// --- GENERIC PARSING ---
@ -25,12 +26,12 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences.
* @param separators A string of characters to split by. Can be empty.
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A new list of one or more [String]s.
*/
fun List<String>.parseMultiValue(separators: String) =
fun List<String>.parseMultiValue(settings: MusicSettings) =
if (size == 1) {
first().maybeParseBySeparators(separators)
first().maybeParseBySeparators(settings)
} else {
// Nothing to do.
this
@ -82,7 +83,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<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.
*/
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.
* @param separators A string of characters to split by. Can be empty.
* @return A list of one or more [String]s that were split up by the given separators.
* @param settings [Settings] required to obtain user separator configuration.
* @return A list of one or more [String]s that were split up by the user-defined separators.
*/
private fun String.maybeParseBySeparators(separators: String) =
if (separators.isNotEmpty()) {
splitEscaped { separators.contains(it) }.correctWhitespace()
} else {
listOf(this)
}
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
// Get the separators the user desires. If null, there's nothing to do.
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
}
/// --- ID3v2 PARSING ---
@ -119,20 +118,30 @@ fun String.parseId3v2Position() = split('/', limit = 2)[0].toIntOrNull()?.nonZer
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
* integer genre fields into one or more genres.
* @param separators A string of characters to split by. Can be empty.
* @return A list of one or more genre names.
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more genre names..
*/
fun List<String>.parseId3GenreNames(separators: String) =
fun List<String>.parseId3GenreNames(settings: MusicSettings) =
if (size == 1) {
first().parseId3MultiValueGenre(separators)
first().parseId3MultiValueGenre(settings)
} else {
// Nothing to split, just map any ID3v1 genres to their name counterparts.
map { it.parseId3v1Genre() ?: it }
}
private fun String.parseId3MultiValueGenre(separators: String) =
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(separators)
/**
* Parse a single ID3v1/ID3v2 integer genre field into their named representations.
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more genre names.
*/
private fun String.parseId3MultiValueGenre(settings: MusicSettings) =
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings)
/**
* Parse an ID3v1 integer genre field.
* @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is
* "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre.
*/
private fun String.parseId3v1Genre(): String? {
// ID3v1 genres are a plain integer value without formatting, so in that case
// try to index the genre table with such.
@ -155,6 +164,11 @@ private fun String.parseId3v1Genre(): String? {
*/
private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/**
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
* named/integer genres.
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
*/
private fun String.parseId3v2Genre(): List<String>? {
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
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.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
/**
@ -42,7 +42,7 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
.setTitle(R.string.set_separators)
.setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ ->
Settings(requireContext()).musicSeparators = getCurrentSeparators()
MusicSettings.from(requireContext()).multiValueSeparators = getCurrentSeparators()
}
}
@ -59,8 +59,8 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
// the corresponding CheckBox for each character instead of doing an iteration
// through the separator list for each CheckBox.
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
?: Settings(requireContext()).musicSeparators)
?.forEach {
?: MusicSettings.from(requireContext()).multiValueSeparators)
.forEach {
when (it) {
Separators.COMMA -> binding.separatorComma.isChecked = true
Separators.SEMICOLON -> binding.separatorSemicolon.isChecked = true

View file

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

View file

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

View file

@ -28,8 +28,8 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.extractor.*
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.logW
@ -224,7 +224,7 @@ class Indexer private constructor() {
// Build the rest of the music library from the song list. This is much more powerful
// and reliable compared to using MediaStore to obtain grouping information.
val buildStart = System.currentTimeMillis()
val library = Library(rawSongs, Settings(context))
val library = Library(rawSongs, MusicSettings.from(context))
logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms")
return library
}

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -21,6 +21,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.state.PlaybackStateManager.Listener
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE

View file

@ -34,11 +34,11 @@ import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Queue
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.logD
/**
@ -59,7 +59,7 @@ class MediaSessionComponent(private val context: Context, private val listener:
}
private val playbackManager = PlaybackStateManager.getInstance()
private val settings = Settings(context)
private val settings = PlaybackSettings.from(context)
private val notification = NotificationComponent(context, mediaSession.sessionToken)
private val provider = BitmapProvider(context)

View file

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

View file

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

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

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
* it under the terms of the GNU General Public License as published by
@ -19,445 +19,49 @@ package org.oxycblt.auxio.settings
import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.os.storage.StorageManager
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.image.CoverMode
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.storage.Directory
import org.oxycblt.auxio.music.storage.MusicDirectories
import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.ui.accent.Accent
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [SharedPreferences] wrapper providing type-safe interfaces to all of the app's settings. Member
* mutability is dependent on how they are used in app. Immutable members are often only modified by
* the preferences view, while mutable members are modified elsewhere.
* Abstract user configuration information. This interface has no functionality whatsoever. Concrete
* implementations should be preferred instead.
* @author Alexander Capehart (OxygenCobalt)
*/
class Settings(private val context: Context) {
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
/**
* Migrate any settings from an old version into their modern counterparts. This can cause data
* loss depending on the feasibility of a migration.
*/
interface Settings {
/** Migrate any settings fields from older versions into their new counterparts. */
fun migrate() {
if (inner.contains(OldKeys.KEY_ACCENT3)) {
logD("Migrating ${OldKeys.KEY_ACCENT3}")
var accent = inner.getInt(OldKeys.KEY_ACCENT3, 5)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Accents were previously frozen as soon as the OS was updated to android twelve,
// as dynamic colors were enabled by default. This is no longer the case, so we need
// to re-update the setting to dynamic colors here.
accent = 16
}
inner.edit {
putInt(context.getString(R.string.set_key_accent), accent)
remove(OldKeys.KEY_ACCENT3)
apply()
}
}
if (inner.contains(OldKeys.KEY_SHOW_COVERS) || inner.contains(OldKeys.KEY_QUALITY_COVERS)) {
logD("Migrating cover settings")
val mode =
when {
!inner.getBoolean(OldKeys.KEY_SHOW_COVERS, true) -> CoverMode.OFF
!inner.getBoolean(OldKeys.KEY_QUALITY_COVERS, true) -> CoverMode.MEDIA_STORE
else -> CoverMode.QUALITY
}
inner.edit {
putInt(context.getString(R.string.set_key_cover_mode), mode.intCode)
remove(OldKeys.KEY_SHOW_COVERS)
remove(OldKeys.KEY_QUALITY_COVERS)
}
}
if (inner.contains(OldKeys.KEY_ALT_NOTIF_ACTION)) {
logD("Migrating ${OldKeys.KEY_ALT_NOTIF_ACTION}")
val mode =
if (inner.getBoolean(OldKeys.KEY_ALT_NOTIF_ACTION, false)) {
ActionMode.SHUFFLE
} else {
ActionMode.REPEAT
}
inner.edit {
putInt(context.getString(R.string.set_key_notif_action), mode.intCode)
remove(OldKeys.KEY_ALT_NOTIF_ACTION)
apply()
}
}
fun Int.migratePlaybackMode() =
when (this) {
// Convert PlaybackMode into MusicMode
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> MusicMode.SONGS
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> MusicMode.ARTISTS
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> MusicMode.ALBUMS
IntegerTable.PLAYBACK_MODE_IN_GENRE -> MusicMode.GENRES
else -> null
}
if (inner.contains(OldKeys.KEY_LIB_PLAYBACK_MODE)) {
logD("Migrating ${OldKeys.KEY_LIB_PLAYBACK_MODE}")
val mode =
inner
.getInt(OldKeys.KEY_LIB_PLAYBACK_MODE, IntegerTable.PLAYBACK_MODE_ALL_SONGS)
.migratePlaybackMode()
?: MusicMode.SONGS
inner.edit {
putInt(context.getString(R.string.set_key_library_song_playback_mode), mode.intCode)
remove(OldKeys.KEY_LIB_PLAYBACK_MODE)
apply()
}
}
if (inner.contains(OldKeys.KEY_DETAIL_PLAYBACK_MODE)) {
logD("Migrating ${OldKeys.KEY_DETAIL_PLAYBACK_MODE}")
val mode =
inner.getInt(OldKeys.KEY_DETAIL_PLAYBACK_MODE, Int.MIN_VALUE).migratePlaybackMode()
inner.edit {
putInt(
context.getString(R.string.set_key_detail_song_playback_mode),
mode?.intCode ?: Int.MIN_VALUE)
remove(OldKeys.KEY_DETAIL_PLAYBACK_MODE)
apply()
}
}
throw NotImplementedError()
}
/**
* Add a [SharedPreferences.OnSharedPreferenceChangeListener] to monitor for settings updates.
* @param listener The [SharedPreferences.OnSharedPreferenceChangeListener] to add.
*/
fun addListener(listener: OnSharedPreferenceChangeListener) {
inner.registerOnSharedPreferenceChangeListener(listener)
fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
throw NotImplementedError()
}
/**
* Unregister a [SharedPreferences.OnSharedPreferenceChangeListener], preventing any further
* settings updates from being sent to ti.t
*/
fun removeListener(listener: OnSharedPreferenceChangeListener) {
inner.unregisterOnSharedPreferenceChangeListener(listener)
}
// --- VALUES ---
/** The current theme. Represented by the [AppCompatDelegate] constants. */
val theme: Int
get() =
inner.getInt(
context.getString(R.string.set_key_theme),
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
/** Whether to use a black background when a dark theme is currently used. */
val useBlackTheme: Boolean
get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false)
/** The current [Accent] (Color Scheme). */
var accent: Accent
get() =
Accent.from(inner.getInt(context.getString(R.string.set_key_accent), Accent.DEFAULT))
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_accent), value.index)
apply()
}
}
/** The tabs to show in the home UI. */
var libTabs: Array<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))
fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
throw NotImplementedError()
}
/**
* Set the configuration on how to handle particular directories in the music library.
* @param musicDirs The new [MusicDirectories] configuration.
* A framework-backed [Settings] implementation.
* @param context [Context] required.
*/
fun setMusicDirs(musicDirs: MusicDirectories) {
inner.edit {
putStringSet(
context.getString(R.string.set_key_music_dirs),
musicDirs.dirs.map(Directory::toDocumentTreeUri).toSet())
putBoolean(
context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude)
apply()
}
}
abstract class Real(protected val context: Context) : Settings {
protected val sharedPreferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
/**
* A string of characters representing the desired separator characters to denote multi-value
* tags.
*/
var musicSeparators: String
// Differ from convention and store a string of separator characters instead of an int
// code. This makes it easier to use in Regexes and makes it more extendable.
get() = inner.getString(context.getString(R.string.set_key_separators), "") ?: ""
set(value) {
inner.edit {
putString(context.getString(R.string.set_key_separators), value)
apply()
}
override fun addListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
}
/** The type of Music the search view is currently filtering to. */
var searchFilterMode: MusicMode?
get() =
MusicMode.fromIntCode(
inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE))
set(value) {
inner.edit {
putInt(
context.getString(R.string.set_key_search_filter),
value?.intCode ?: Int.MIN_VALUE)
apply()
}
override fun removeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
}
/** The Song [Sort] mode used in the Home UI. */
var libSongSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode)
apply()
}
}
/** The Album [Sort] mode used in the Home UI. */
var libAlbumSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode)
apply()
}
}
/** The Artist [Sort] mode used in the Home UI. */
var libArtistSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode)
apply()
}
}
/** The Genre [Sort] mode used in the Home UI. */
var libGenreSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode)
apply()
}
}
/** The [Sort] mode used in the Album Detail UI. */
var detailAlbumSort: Sort
get() {
var sort =
Sort.fromIntCode(
inner.getInt(
context.getString(R.string.set_key_detail_album_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByDisc, true)
// Correct legacy album sort modes to Disc
if (sort.mode is Sort.Mode.ByName) {
sort = sort.withMode(Sort.Mode.ByDisc)
}
return sort
}
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_detail_album_sort), value.intCode)
apply()
}
}
/** The [Sort] mode used in the Artist Detail UI. */
var detailArtistSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_artist_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByDate, false)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_detail_artist_sort), value.intCode)
apply()
}
}
/** The [Sort] mode used in the Genre Detail UI. */
var detailGenreSort: Sort
get() =
Sort.fromIntCode(
inner.getInt(context.getString(R.string.set_key_detail_genre_sort), Int.MIN_VALUE))
?: Sort(Sort.Mode.ByName, true)
set(value) {
inner.edit {
putInt(context.getString(R.string.set_key_detail_genre_sort), value.intCode)
apply()
}
}
/** Legacy keys that are no longer used, but still have to be migrated. */
private object OldKeys {
const val KEY_ACCENT3 = "auxio_accent"
const val KEY_ALT_NOTIF_ACTION = "KEY_ALT_NOTIF_ACTION"
const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS"
const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS"
const val KEY_LIB_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2"
const val KEY_DETAIL_PLAYBACK_MODE = "auxio_detail_song_play_mode"
}
}

View file

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

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

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

View file

@ -31,7 +31,6 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.*
/**
@ -197,7 +196,7 @@ class WidgetProvider : AppWidgetProvider() {
// Below API 31, enable a rounded bar only if round mode is enabled.
// On API 31+, the bar should always be round in order to fit in with other widgets.
val background =
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (useRoundedRemoteViews(context)) {
R.drawable.ui_widget_bar_round
} else {
R.drawable.ui_widget_bar_system
@ -216,7 +215,7 @@ class WidgetProvider : AppWidgetProvider() {
// On API 31+, the background should always be round in order to fit in with other
// widgets.
val background =
if (Settings(context).roundMode && Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (useRoundedRemoteViews(context)) {
R.drawable.ui_widget_bg_round
} else {
R.drawable.ui_widget_bg_system

View file

@ -27,6 +27,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import kotlin.math.sqrt
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent
@ -132,3 +133,12 @@ fun AppWidgetManager.updateAppWidgetCompat(
}
}
}
/**
* Returns whether rounded UI elements are appropriate for the widget, either based on the current
* settings or if the widget has to fit in aesthetically with other widgets.
* @param context [Context] configuration to use.
* @return true if to use round mode, false otherwise.
*/
fun useRoundedRemoteViews(context: Context) =
UISettings.from(context).roundMode && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

View file

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

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.Test
import org.oxycblt.auxio.music.FakeMusicSettings
class ParsingUtilTest {
@Test
fun parseMultiValue_single() {
assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(","))
assertEquals(
listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(SeparatorMusicSettings(",")))
}
@Test
fun parseMultiValue_many() {
assertEquals(listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(","))
assertEquals(
listOf("a", "b", "c"),
listOf("a", "b", "c").parseMultiValue(SeparatorMusicSettings(",")))
}
@Test
fun parseMultiValue_several() {
assertEquals(
listOf("a", "b", "c", "d", "e", "f"), listOf("a,b;c/d+e&f").parseMultiValue(",;/+&"))
listOf("a", "b", "c", "d", "e", "f"),
listOf("a,b;c/d+e&f").parseMultiValue(SeparatorMusicSettings(",;/+&")))
}
@Test
@ -105,37 +110,45 @@ class ParsingUtilTest {
fun parseId3v2Genre_multi() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(","))
listOf("Post-Rock", "Shoegaze", "Glitch")
.parseId3GenreNames(SeparatorMusicSettings(",")))
}
@Test
fun parseId3v2Genre_multiId3v1() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("176", "178", "Glitch").parseId3GenreNames(","))
listOf("176", "178", "Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
}
@Test
fun parseId3v2Genre_wackId3() {
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(","))
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(SeparatorMusicSettings(",")))
}
@Test
fun parseId3v2Genre_singleId3v23() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(","))
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
}
@Test
fun parseId3v2Genre_singleSeparated() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(","))
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(SeparatorMusicSettings(",")))
}
@Test
fun parsId3v2Genre_singleId3v1() {
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(","))
assertEquals(
listOf("Post-Rock"), listOf("176").parseId3GenreNames(SeparatorMusicSettings(",")))
}
class SeparatorMusicSettings(private val separators: String) : FakeMusicSettings {
override var multiValueSeparators: String
get() = separators
set(_) = throw NotImplementedError()
}
}