From 532a30325af18e4c2516917af5dd620fcc5d0150 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Mon, 20 Jun 2022 09:05:04 -0600 Subject: [PATCH] settings: revamp settingsmanager into settings Revamp the shared object SettingsManager into a standalone utility called Settings. This makes many things easier in Auxio. It completely unifies the key format that we use (Android Strings instead of Java consts), eliminates the pretty dumb initialization method that we use, and eliminates the dubiousness of holding a Context-related utility in a global field. The only cost was having to migrate even more ViewModels to Android ViewModels. Whatever. --- CHANGELOG.md | 4 + .../main/java/org/oxycblt/auxio/AuxioApp.kt | 8 - .../java/org/oxycblt/auxio/MainActivity.kt | 52 +-- .../java/org/oxycblt/auxio/MainFragment.kt | 3 +- .../oxycblt/auxio/detail/DetailViewModel.kt | 16 +- .../org/oxycblt/auxio/home/HomeFragment.kt | 5 +- .../org/oxycblt/auxio/home/HomeViewModel.kt | 40 +- .../auxio/home/list/HomeListFragment.kt | 4 +- .../auxio/home/list/SongListFragment.kt | 5 +- .../auxio/home/tabs/TabCustomizeDialog.kt | 9 +- .../org/oxycblt/auxio/image/BaseFetcher.kt | 10 +- .../oxycblt/auxio/image/StyledImageView.kt | 6 +- .../org/oxycblt/auxio/music/MusicStore.kt | 8 +- .../auxio/music/backend/MediaStoreBackend.kt | 14 +- .../auxio/music/dirs/MusicDirsDialog.kt | 17 +- .../auxio/playback/PlaybackBarFragment.kt | 3 +- .../auxio/playback/PlaybackPanelFragment.kt | 3 +- .../auxio/playback/PlaybackViewModel.kt | 70 ++-- .../auxio/playback/queue/QueueFragment.kt | 4 +- .../replaygain/PreAmpCustomizeDialog.kt | 9 +- .../replaygain/ReplayGainAudioProcessor.kt | 13 +- .../playback/state/PlaybackStateManager.kt | 61 ++- .../playback/system/MediaSessionComponent.kt | 12 +- .../auxio/playback/system/PlaybackService.kt | 38 +- .../oxycblt/auxio/search/SearchFragment.kt | 5 +- .../oxycblt/auxio/search/SearchViewModel.kt | 25 +- .../oxycblt/auxio/settings/AboutFragment.kt | 4 +- .../org/oxycblt/auxio/settings/Settings.kt | 369 +++++++++++++++++ .../oxycblt/auxio/settings/SettingsCompat.kt | 38 +- .../auxio/settings/SettingsListFragment.kt | 34 +- .../oxycblt/auxio/settings/SettingsManager.kt | 377 ------------------ .../java/org/oxycblt/auxio/ui/MenuFragment.kt | 3 +- .../oxycblt/auxio/ui/accent/AccentDialog.kt | 11 +- .../org/oxycblt/auxio/util/FrameworkUtil.kt | 5 + .../oxycblt/auxio/widgets/WidgetComponent.kt | 9 +- app/src/main/res/values/config.xml | 1 + app/src/main/res/values/integers.xml | 62 --- app/src/main/res/values/settings.xml | 94 +++++ app/src/main/res/xml/prefs_main.xml | 40 +- 39 files changed, 742 insertions(+), 749 deletions(-) create mode 100644 app/src/main/java/org/oxycblt/auxio/settings/Settings.kt delete mode 100644 app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt delete mode 100644 app/src/main/res/values/integers.xml create mode 100644 app/src/main/res/values/settings.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b36ae31f..5911649e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - Fixed seam that would appear on some album covers - Fixed visual issue with the queue opening animation +#### Dev/Meta +- Migrated preferences from shared object to utility +- Removed 2.0.0 compat code + ## v2.4.0 #### What's New diff --git a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt index f9c45ab8e..c3335cc44 100644 --- a/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt +++ b/app/src/main/java/org/oxycblt/auxio/AuxioApp.kt @@ -30,15 +30,11 @@ import org.oxycblt.auxio.image.ArtistImageFetcher import org.oxycblt.auxio.image.CrossfadeTransitionFactory import org.oxycblt.auxio.image.GenreImageFetcher import org.oxycblt.auxio.image.MusicKeyer -import org.oxycblt.auxio.settings.SettingsManager -import org.oxycblt.auxio.util.logD class AuxioApp : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() - logD(BuildConfig.APPLICATION_ID + ".MainActivity") - // 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. @@ -54,10 +50,6 @@ class AuxioApp : Application(), ImageLoaderFactory { action = INTENT_KEY_SHORTCUT_SHUFFLE }) .build())) - - // Init SettingsManager here so that there aren't any race conditions - // [e.g PlaybackService gets SettingsManager before activity can init SettingsManager] - SettingsManager.init(applicationContext) } override fun newImageLoader(): ImageLoader { diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index 3cfe33345..3a3643098 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -18,11 +18,9 @@ package org.oxycblt.auxio import android.content.Intent -import android.net.Uri import android.os.Build import android.os.Bundle import android.view.View -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.WindowCompat @@ -31,7 +29,9 @@ import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.music.IndexerService import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.system.PlaybackService -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings +import org.oxycblt.auxio.util.androidViewModels import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.logD @@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.logD * @author OxygenCobalt */ class MainActivity : AppCompatActivity() { - private val playbackModel: PlaybackViewModel by viewModels() + private val playbackModel: PlaybackViewModel by androidViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,17 +72,13 @@ class MainActivity : AppCompatActivity() { startService(Intent(this, PlaybackService::class.java)) // If we have a valid intent, use that. Otherwise, restore the playback state. - val action = intentToDelayedAction(intent) ?: PlaybackViewModel.DelayedAction.RestoreState - - playbackModel.startDelayedAction(this, action) + playbackModel.startDelayedAction( + intentToDelayedAction(intent) ?: PlaybackViewModel.DelayedAction.RestoreState) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - - intentToDelayedAction(intent)?.let { action -> - playbackModel.startDelayedAction(this, action) - } + intentToDelayedAction(intent)?.let(playbackModel::startDelayedAction) } private fun intentToDelayedAction(intent: Intent?): PlaybackViewModel.DelayedAction? { @@ -99,45 +95,25 @@ class MainActivity : AppCompatActivity() { } } - /** - * Extracts a [Uri] from an intent as long as: - * - The intent is ACTION_VIEW - * - The intent has not already been used. - */ - private fun retrieveViewUri(intent: Intent?): Uri? { - if (intent != null) { - val action = intent.action - val isConsumed = intent.getBooleanExtra(KEY_INTENT_USED, false) - - if (action == Intent.ACTION_VIEW && !isConsumed) { - // Mark the intent as used so this does not fire again - intent.putExtra(KEY_INTENT_USED, true) - return intent.data - } - } - - return null - } - private fun setupTheme() { - val settingsManager = SettingsManager.getInstance() + val settings = Settings(this) // Disable theme customization above Android 12, as it's far enough in as a version to // the point where most phones should have an automatic option for light/dark theming. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - AppCompatDelegate.setDefaultNightMode(settingsManager.theme) + AppCompatDelegate.setDefaultNightMode(settings.theme) } else { AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } // The black theme has a completely separate set of styles since style attributes cannot // be modified at runtime. - if (isNight && settingsManager.useBlackTheme) { - logD("Applying black theme [accent ${settingsManager.accent}]") - setTheme(settingsManager.accent.blackTheme) + if (isNight && settings.useBlackTheme) { + logD("Applying black theme [accent ${settings.accent}]") + setTheme(settings.accent.blackTheme) } else { - logD("Applying normal theme [accent ${settingsManager.accent}]") - setTheme(settingsManager.accent.theme) + logD("Applying normal theme [accent ${settings.accent}]") + setTheme(settings.accent.theme) } } diff --git a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt index 8a0c71c3e..789cc596c 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainFragment.kt @@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.launch /** @@ -40,7 +41,7 @@ import org.oxycblt.auxio.util.launch * @author OxygenCobalt */ class MainFragment : ViewBindingFragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() private var callback: DynamicBackPressedCallback? = null diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index de1195df2..4831fbffe 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -36,7 +36,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MimeType import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.Sort @@ -62,7 +62,7 @@ class DetailViewModel(application: Application) : ) private val musicStore = MusicStore.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(application) private val _currentSong = MutableStateFlow(null) val currentSong: StateFlow @@ -77,9 +77,9 @@ class DetailViewModel(application: Application) : get() = _albumData var albumSort: Sort - get() = settingsManager.detailAlbumSort + get() = settings.detailAlbumSort set(value) { - settingsManager.detailAlbumSort = value + settings.detailAlbumSort = value currentAlbum.value?.let(::refreshAlbumData) } @@ -91,9 +91,9 @@ class DetailViewModel(application: Application) : val artistData: StateFlow> = _artistData var artistSort: Sort - get() = settingsManager.detailArtistSort + get() = settings.detailArtistSort set(value) { - settingsManager.detailArtistSort = value + settings.detailArtistSort = value currentArtist.value?.let(::refreshArtistData) } @@ -105,9 +105,9 @@ class DetailViewModel(application: Application) : val genreData: StateFlow> = _genreData var genreSort: Sort - get() = settingsManager.detailGenreSort + get() = settings.detailGenreSort set(value) { - settingsManager.detailGenreSort = value + settings.detailGenreSort = value currentGenre.value?.let(::refreshGenreData) } diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 9a36d6b10..4aa15df15 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -55,6 +55,7 @@ import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.launch @@ -71,9 +72,9 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author OxygenCobalt */ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() - private val homeModel: HomeViewModel by activityViewModels() + private val homeModel: HomeViewModel by androidActivityViewModels() private val indexerModel: IndexerViewModel by activityViewModels() private var storagePermissionLauncher: ActivityResultLauncher? = null diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index a04cd1862..b62e285d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -17,7 +17,8 @@ package org.oxycblt.auxio.home -import androidx.lifecycle.ViewModel +import android.app.Application +import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.oxycblt.auxio.home.tabs.Tab @@ -26,18 +27,20 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort +import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.logD /** * The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state. * @author OxygenCobalt */ -class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback { +class HomeViewModel(application: Application) : + AndroidViewModel(application), Settings.Callback, MusicStore.Callback { private val musicStore = MusicStore.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(application, this) private val _songs = MutableStateFlow(listOf()) val songs: StateFlow> @@ -60,7 +63,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback /** Internal getter for getting the visible library tabs */ private val visibleTabs: List - get() = settingsManager.libTabs.filterIsInstance().map { it.mode } + get() = settings.libTabs.filterIsInstance().map { it.mode } private val _currentTab = MutableStateFlow(tabs[0]) val currentTab: StateFlow = _currentTab @@ -77,7 +80,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback init { musicStore.addCallback(this) - settingsManager.addCallback(this) } /** Update the current tab based off of the new ViewPager position. */ @@ -93,10 +95,10 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback /** Get the specific sort for the given [DisplayMode]. */ fun getSortForDisplay(displayMode: DisplayMode): Sort { return when (displayMode) { - DisplayMode.SHOW_SONGS -> settingsManager.libSongSort - DisplayMode.SHOW_ALBUMS -> settingsManager.libAlbumSort - DisplayMode.SHOW_ARTISTS -> settingsManager.libArtistSort - DisplayMode.SHOW_GENRES -> settingsManager.libGenreSort + DisplayMode.SHOW_SONGS -> settings.libSongSort + DisplayMode.SHOW_ALBUMS -> settings.libAlbumSort + DisplayMode.SHOW_ARTISTS -> settings.libArtistSort + DisplayMode.SHOW_GENRES -> settings.libGenreSort } } @@ -105,19 +107,19 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback logD("Updating ${_currentTab.value} sort to $sort") when (_currentTab.value) { DisplayMode.SHOW_SONGS -> { - settingsManager.libSongSort = sort + settings.libSongSort = sort _songs.value = sort.songs(_songs.value) } DisplayMode.SHOW_ALBUMS -> { - settingsManager.libAlbumSort = sort + settings.libAlbumSort = sort _albums.value = sort.albums(_albums.value) } DisplayMode.SHOW_ARTISTS -> { - settingsManager.libArtistSort = sort + settings.libArtistSort = sort _artists.value = sort.artists(_artists.value) } DisplayMode.SHOW_GENRES -> { - settingsManager.libGenreSort = sort + settings.libGenreSort = sort _genres.value = sort.genres(_genres.value) } } @@ -136,10 +138,10 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback override fun onLibraryChanged(library: MusicStore.Library?) { if (library != null) { - _songs.value = settingsManager.libSongSort.songs(library.songs) - _albums.value = settingsManager.libAlbumSort.albums(library.albums) - _artists.value = settingsManager.libArtistSort.artists(library.artists) - _genres.value = settingsManager.libGenreSort.genres(library.genres) + _songs.value = settings.libSongSort.songs(library.songs) + _albums.value = settings.libAlbumSort.albums(library.albums) + _artists.value = settings.libArtistSort.artists(library.artists) + _genres.value = settings.libGenreSort.genres(library.genres) } } @@ -151,6 +153,6 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback override fun onCleared() { super.onCleared() musicStore.removeCallback(this) - settingsManager.removeCallback(this) + settings.release() } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt index dabe612dc..a54fb4d12 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/HomeListFragment.kt @@ -20,13 +20,13 @@ package org.oxycblt.auxio.home.list import android.os.Bundle import android.view.LayoutInflater import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.MenuFragment import org.oxycblt.auxio.ui.MenuItemListener +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.applySpans /** @@ -38,7 +38,7 @@ abstract class HomeListFragment : MenuItemListener, FastScrollRecyclerView.PopupProvider, FastScrollRecyclerView.OnFastScrollListener { - protected val homeModel: HomeViewModel by activityViewModels() + protected val homeModel: HomeViewModel by androidActivityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeListBinding.inflate(inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 6f10c9c0a..8f1a8f49e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -22,6 +22,8 @@ import android.view.View import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.MenuItemListener @@ -39,6 +41,7 @@ import org.oxycblt.auxio.util.logEOrThrow */ class SongListFragment : HomeListFragment() { private val homeAdapter = SongsAdapter(this) + private val settings: Settings by settings() override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { super.onBindingCreated(binding, savedInstanceState) @@ -80,7 +83,7 @@ class SongListFragment : HomeListFragment() { override fun onItemClick(item: Item) { check(item is Song) - playbackModel.play(item) + playbackModel.play(item, settings.songPlaybackMode) } override fun onOpenMenu(item: Item, anchor: View) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index a888adff3..b461af70d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -25,7 +25,8 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD @@ -36,8 +37,8 @@ import org.oxycblt.auxio.util.requireAttached * @author OxygenCobalt */ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAdapter.Listener { - private val settingsManager = SettingsManager.getInstance() private val tabAdapter = TabAdapter(this) + private val settings: Settings by settings() private var touchHelper: ItemTouchHelper? = null private var callback: TabDragCallback? = null @@ -48,7 +49,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd .setTitle(R.string.set_lib_tabs) .setPositiveButton(R.string.lbl_ok) { _, _ -> logD("Committing tab changes") - settingsManager.libTabs = tabAdapter.data.tabs + settings.libTabs = tabAdapter.data.tabs } .setNegativeButton(R.string.lbl_cancel, null) } @@ -59,7 +60,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment(), TabAd logD("Found saved tab state") tabAdapter.data.submitTabs(savedTabs) } else { - tabAdapter.data.submitTabs(settingsManager.libTabs) + tabAdapter.data.submitTabs(settings.libTabs) } binding.tabRecycler.apply { diff --git a/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt b/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt index efa31f0f9..30490afdc 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/BaseFetcher.kt @@ -45,7 +45,7 @@ import kotlinx.coroutines.withContext import okio.buffer import okio.source import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -56,19 +56,19 @@ import org.oxycblt.auxio.util.logW * TODO: Artist images */ abstract class BaseFetcher : Fetcher { - private val settingsManager = SettingsManager.getInstance() - /** * Fetch the artwork of an [album]. This call respects user configuration and has proper * redundancy in the case that metadata fails to load. */ protected suspend fun fetchArt(context: Context, album: Album): InputStream? { - if (!settingsManager.showCovers) { + val settings = Settings(context) + + if (!settings.showCovers) { return null } return try { - if (settingsManager.useQualityCovers) { + if (settings.useQualityCovers) { fetchQualityCovers(context, album) } else { fetchMediaStoreCovers(context, album) diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index bc90edb2e..70dc392ed 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -34,7 +34,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.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getDrawableSafe @@ -51,11 +51,13 @@ class StyledImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr) { + private val settings = Settings(context) + var cornerRadius = 0f set(value) { field = value (background as? MaterialShapeDrawable)?.let { bg -> - if (!isInEditMode && SettingsManager.getInstance().roundCovers) { + if (settings.roundCovers) { bg.setCornerSize(value) } else { bg.setCornerSize(0f) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index db6bd855a..e967ba74d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -34,9 +34,11 @@ class MusicStore private constructor() { var library: Library? = null set(value) { - field = value - for (callback in callbacks) { - callback.onLibraryChanged(library) + synchronized(this) { + field = value + for (callback in callbacks) { + callback.onLibraryChanged(library) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt index 02629b1c4..da11cc99d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt @@ -41,7 +41,7 @@ import org.oxycblt.auxio.music.no import org.oxycblt.auxio.music.queryCursor import org.oxycblt.auxio.music.storageVolumesCompat import org.oxycblt.auxio.music.useQuery -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.logD @@ -125,10 +125,10 @@ abstract class MediaStoreBackend : Indexer.Backend { protected val volumes = mutableListOf() override fun query(context: Context): Cursor { - val settingsManager = SettingsManager.getInstance() + val settings = Settings(context) val storageManager = context.getSystemServiceSafe(StorageManager::class) volumes.addAll(storageManager.storageVolumesCompat) - val dirs = settingsManager.getMusicDirs(context, storageManager) + val dirs = settings.getMusicDirs(storageManager) val args = mutableListOf() var selector = BASE_SELECTOR @@ -144,8 +144,7 @@ abstract class MediaStoreBackend : Indexer.Backend { " AND NOT (" } - // Since selector arguments are contained within a single parentheses, we need to - // do a bunch of stuff. + // Each impl adds the directories that they want selected. for (i in dirs.dirs.indices) { if (addDirToSelectorArgs(dirs.dirs[i], args)) { selector += @@ -510,8 +509,9 @@ open class VolumeAwareMediaStoreBackend : MediaStoreBackend() { val relativePath = cursor.getStringOrNull(relativePathIndex) // We now have access to the volume name, so we try to leverage it instead. - // I have no idea how well this works in practice, so we still leverage - // the API 21 path grokking in the case that these fields are not sane. + // I have no idea how well this works in practice, but I assume that the fields + // probably exist. + // TODO: Remove redundant null checks for fields you are pretty sure are not null. if (volumeName != null && relativePath != null) { // Iterating through the volume list is cheaper than creating a map, // interestingly enough. diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index 5b103a47d..ae0153a6f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -25,14 +25,15 @@ import android.view.LayoutInflater import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible -import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.getSystemServiceSafe import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.logD @@ -44,11 +45,10 @@ import org.oxycblt.auxio.util.showToast */ class MusicDirsDialog : ViewBindingDialogFragment(), MusicDirAdapter.Listener { - private val settingsManager = SettingsManager.getInstance() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() - private val playbackModel: PlaybackViewModel by activityViewModels() private val dirAdapter = MusicDirAdapter(this) - + private val settings: Settings by settings() private var storageManager: StorageManager? = null override fun onCreateBinding(inflater: LayoutInflater) = @@ -80,7 +80,7 @@ class MusicDirsDialog : } dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { - val dirs = settingsManager.getMusicDirs(requireContext(), requireStorageManager()) + val dirs = settings.getMusicDirs(requireStorageManager()) if (dirs.dirs != dirAdapter.data.currentList || dirs.shouldInclude != isInclude(requireBinding())) { @@ -99,7 +99,7 @@ class MusicDirsDialog : } val storageManager = requireStorageManager() - var dirs = settingsManager.getMusicDirs(requireContext(), storageManager) + var dirs = settings.getMusicDirs(storageManager) if (savedInstanceState != null) { val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) @@ -187,8 +187,7 @@ class MusicDirsDialog : binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include private fun saveAndRestart() { - settingsManager.setMusicDirs( - MusicDirs(dirAdapter.data.currentList, isInclude(requireBinding()))) + settings.setMusicDirs(MusicDirs(dirAdapter.data.currentList, isInclude(requireBinding()))) playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt index 15ab6f5db..d264f6e50 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackBarFragment.kt @@ -28,6 +28,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.getColorStateListSafe import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.getSystemGestureInsetsCompat @@ -40,7 +41,7 @@ import org.oxycblt.auxio.util.textSafe * @author OxygenCobalt */ class PlaybackBarFragment : ViewBindingFragment() { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt index 7a02fbb28..4607a1bc5 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackPanelFragment.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.getDrawableSafe import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.getSystemGestureInsetsCompat @@ -53,7 +54,7 @@ class PlaybackPanelFragment : ViewBindingFragment(), StyledSeekBar.Callback, Toolbar.OnMenuItemClickListener { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val navModel: NavigationViewModel by activityViewModels() private var queueItem: MenuItem? = null diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 260573a20..7e21a6dd3 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -17,9 +17,10 @@ package org.oxycblt.auxio.playback +import android.app.Application import android.content.Context import android.net.Uri -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -31,9 +32,12 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackMode +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.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings +import org.oxycblt.auxio.util.application import org.oxycblt.auxio.util.logE /** @@ -44,12 +48,13 @@ import org.oxycblt.auxio.util.logE * * @author OxygenCobalt */ -class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore.Callback { +class PlaybackViewModel(application: Application) : + AndroidViewModel(application), PlaybackStateManager.Callback, MusicStore.Callback { private val musicStore = MusicStore.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(application) private val playbackManager = PlaybackStateManager.getInstance() - private var pendingDelayedAction: DelayedActionImpl? = null + private var pendingDelayedAction: DelayedAction? = null private val _song = MutableStateFlow(null) /** The current song. */ @@ -85,12 +90,10 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore // --- PLAYING FUNCTIONS --- - /** - * Play a [song] with the [mode] specified. [mode] will default to the preferred song playback - * mode of the user if not specified. - */ - fun play(song: Song, mode: PlaybackMode = settingsManager.songPlaybackMode) { - playbackManager.play(song, mode) + /** Play a [song] with the [mode] specified, */ + fun play(song: Song, mode: PlaybackMode) { + playbackManager.play( + song, settings.keepShuffle && playbackManager.isPlaying, mode, settings) } /** @@ -103,7 +106,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore return } - playbackManager.play(album, shuffled) + playbackManager.play(album, shuffled, settings) } /** @@ -116,7 +119,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore return } - playbackManager.play(artist, shuffled) + playbackManager.play(artist, shuffled, settings) } /** @@ -129,12 +132,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore return } - playbackManager.play(genre, shuffled) + playbackManager.play(genre, shuffled, settings) } /** Shuffle all songs */ fun shuffleAll() { - playbackManager.shuffleAll() + playbackManager.shuffleAll(settings) } /** @@ -149,26 +152,29 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore * We would normally want to put this kind of functionality into PlaybackService, but it's * lifecycle makes that more or less impossible. */ - fun startDelayedAction(context: Context, action: DelayedAction) { + fun startDelayedAction(action: DelayedAction) { val library = musicStore.library - val actionImpl = DelayedActionImpl(context.applicationContext, action) if (library != null) { - performActionImpl(actionImpl, library) + performActionImpl(action, library) } else { - pendingDelayedAction = actionImpl + pendingDelayedAction = action } } - private fun performActionImpl(action: DelayedActionImpl, library: MusicStore.Library) { - when (action.inner) { + private fun performActionImpl(action: DelayedAction, library: MusicStore.Library) { + when (action) { is DelayedAction.RestoreState -> { if (!playbackManager.isInitialized) { - viewModelScope.launch { playbackManager.restoreState(action.context) } + viewModelScope.launch { + playbackManager.restoreState(PlaybackStateDatabase.getInstance(application)) + } } } is DelayedAction.ShuffleAll -> shuffleAll() is DelayedAction.Open -> { - library.findSongForUri(action.context, action.inner.uri)?.let(::play) + library.findSongForUri(application, action.uri)?.let { song -> + play(song, settings.songPlaybackMode) + } } } } @@ -219,17 +225,17 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore /** Add an [Album] to the top of the queue. */ fun playNext(album: Album) { - playbackManager.playNext(settingsManager.detailAlbumSort.songs(album.songs)) + playbackManager.playNext(settings.detailAlbumSort.songs(album.songs)) } /** Add an [Artist] to the top of the queue. */ fun playNext(artist: Artist) { - playbackManager.playNext(settingsManager.detailArtistSort.songs(artist.songs)) + playbackManager.playNext(settings.detailArtistSort.songs(artist.songs)) } /** Add a [Genre] to the top of the queue. */ fun playNext(genre: Genre) { - playbackManager.playNext(settingsManager.detailGenreSort.songs(genre.songs)) + playbackManager.playNext(settings.detailGenreSort.songs(genre.songs)) } /** Add a [Song] to the end of the queue. */ @@ -239,17 +245,17 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore /** Add an [Album] to the end of the queue. */ fun addToQueue(album: Album) { - playbackManager.addToQueue(settingsManager.detailAlbumSort.songs(album.songs)) + playbackManager.addToQueue(settings.detailAlbumSort.songs(album.songs)) } /** Add an [Artist] to the end of the queue. */ fun addToQueue(artist: Artist) { - playbackManager.addToQueue(settingsManager.detailArtistSort.songs(artist.songs)) + playbackManager.addToQueue(settings.detailArtistSort.songs(artist.songs)) } /** Add a [Genre] to the end of the queue. */ fun addToQueue(genre: Genre) { - playbackManager.addToQueue(settingsManager.detailGenreSort.songs(genre.songs)) + playbackManager.addToQueue(settings.detailGenreSort.songs(genre.songs)) } // --- STATUS FUNCTIONS --- @@ -261,7 +267,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore /** Flip the shuffle status, e.g from on to off. Will keep song by default. */ fun invertShuffled() { - playbackManager.reshuffle(!playbackManager.isShuffled) + playbackManager.reshuffle(!playbackManager.isShuffled, settings) } /** Increment the repeat mode, e.g from [RepeatMode.NONE] to [RepeatMode.ALL] */ @@ -277,7 +283,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore */ fun savePlaybackState(context: Context, onDone: () -> Unit) { viewModelScope.launch { - playbackManager.saveState(context) + playbackManager.saveState(PlaybackStateDatabase.getInstance(context)) onDone() } } @@ -289,8 +295,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore data class Open(val uri: Uri) : DelayedAction() } - private data class DelayedActionImpl(val context: Context, val inner: DelayedAction) - // --- OVERRIDES --- override fun onCleared() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index 045afb466..28123006f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -20,7 +20,6 @@ package org.oxycblt.auxio.playback.queue import android.os.Bundle import android.view.LayoutInflater import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView @@ -28,6 +27,7 @@ import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.requireAttached @@ -36,7 +36,7 @@ import org.oxycblt.auxio.util.requireAttached * @author OxygenCobalt */ class QueueFragment : ViewBindingFragment(), QueueItemListener { - private val playbackModel: PlaybackViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private var queueAdapter = QueueAdapter(this) private var touchHelper: ItemTouchHelper? = null private var callback: QueueDragCallback? = null diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt index 8c26c01a0..0d66abab0 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/PreAmpCustomizeDialog.kt @@ -25,7 +25,8 @@ import kotlin.math.abs import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogPreAmpBinding -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.textSafe @@ -34,7 +35,7 @@ import org.oxycblt.auxio.util.textSafe * @author OxygenCobalt */ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { - private val settingsManager = SettingsManager.getInstance() + private val settings: Settings by settings() override fun onCreateBinding(inflater: LayoutInflater) = DialogPreAmpBinding.inflate(inflater) @@ -43,7 +44,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { .setTitle(R.string.set_pre_amp) .setPositiveButton(R.string.lbl_ok) { _, _ -> val binding = requireBinding() - settingsManager.replayGainPreAmp = + settings.replayGainPreAmp = ReplayGainPreAmp(binding.withTagsSlider.value, binding.withoutTagsSlider.value) } .setNegativeButton(R.string.lbl_cancel, null) @@ -54,7 +55,7 @@ class PreAmpCustomizeDialog : ViewBindingDialogFragment() { // First initialization, we need to supply the sliders with the values from // settings. After this, the sliders save their own state, so we do not need to // do any restore behavior. - val preAmp = settingsManager.replayGainPreAmp + val preAmp = settings.replayGainPreAmp binding.withTagsSlider.value = preAmp.with binding.withoutTagsSlider.value = preAmp.without } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index ab690055d..609b1738f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -17,6 +17,7 @@ package org.oxycblt.auxio.playback.replaygain +import android.content.Context import com.google.android.exoplayer2.C import com.google.android.exoplayer2.audio.AudioProcessor import com.google.android.exoplayer2.audio.BaseAudioProcessor @@ -27,7 +28,7 @@ import java.nio.ByteBuffer import kotlin.math.pow import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.playback.state.PlaybackStateManager -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.clamp import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -42,12 +43,12 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * * @author OxygenCobalt */ -class ReplayGainAudioProcessor : BaseAudioProcessor() { +class ReplayGainAudioProcessor(context: Context) : BaseAudioProcessor() { private data class Gain(val track: Float, val album: Float) private data class GainTag(val key: String, val value: Float) private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(context) private var volume = 1f set(value) { @@ -63,20 +64,20 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() { * based off Vanilla Music's implementation, but has diverged to a significant extent. */ fun applyReplayGain(metadata: Metadata?) { - if (settingsManager.replayGainMode == ReplayGainMode.OFF) { + if (settings.replayGainMode == ReplayGainMode.OFF) { logD("ReplayGain not enabled") volume = 1f return } val gain = metadata?.let(::parseReplayGain) - val preAmp = settingsManager.replayGainPreAmp + val preAmp = settings.replayGainPreAmp val adjust = if (gain != null) { // ReplayGain is configurable, so determine what to do based off of the mode. val useAlbumGain = - when (settingsManager.replayGainMode) { + when (settings.replayGainMode) { ReplayGainMode.OFF -> throw IllegalStateException() // User wants track gain to be preferred. Default to album gain only if diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 5b82d3665..451bc540d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -17,7 +17,6 @@ package org.oxycblt.auxio.playback.state -import android.content.Context import kotlin.math.max import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -28,7 +27,7 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -51,7 +50,6 @@ import org.oxycblt.auxio.util.logW */ class PlaybackStateManager private constructor() { private val musicStore = MusicStore.getInstance() - private val settingsManager = SettingsManager.getInstance() /** The currently playing song. Null if there isn't one */ val song @@ -150,7 +148,7 @@ class PlaybackStateManager private constructor() { * Play a [song]. * @param playbackMode The [PlaybackMode] to construct the queue off of. */ - fun play(song: Song, playbackMode: PlaybackMode) { + fun play(song: Song, shuffle: Boolean, playbackMode: PlaybackMode, settings: Settings) { val library = musicStore.library ?: return synchronized(this) { @@ -162,7 +160,7 @@ class PlaybackStateManager private constructor() { PlaybackMode.IN_GENRE -> song.genre } - applyNewQueue(library, settingsManager.keepShuffle && isShuffled, song) + applyNewQueue(library, settings, shuffle, song) seekTo(0) notifyNewPlayback() notifyShuffledChanged() @@ -175,12 +173,12 @@ class PlaybackStateManager private constructor() { * Play a [parent], such as an artist or album. * @param shuffled Whether the queue is shuffled or not */ - fun play(parent: MusicParent, shuffled: Boolean) { + fun play(parent: MusicParent, shuffled: Boolean, settings: Settings) { val library = musicStore.library ?: return synchronized(this) { this.parent = parent - applyNewQueue(library, shuffled, null) + applyNewQueue(library, settings, shuffled, null) seekTo(0) notifyNewPlayback() notifyShuffledChanged() @@ -190,11 +188,11 @@ class PlaybackStateManager private constructor() { } /** Shuffle all songs. */ - fun shuffleAll() { + fun shuffleAll(settings: Settings) { val library = musicStore.library ?: return synchronized(this) { parent = null - applyNewQueue(library, true, null) + applyNewQueue(library, settings, true, null) seekTo(0) notifyNewPlayback() notifyShuffledChanged() @@ -291,17 +289,22 @@ class PlaybackStateManager private constructor() { } /** Set whether this instance is [shuffled]. Updates the queue accordingly. */ - fun reshuffle(shuffled: Boolean) { + fun reshuffle(shuffled: Boolean, settings: Settings) { val library = musicStore.library ?: return synchronized(this) { val song = song ?: return - applyNewQueue(library, shuffled, song) + applyNewQueue(library, settings, shuffled, song) notifyQueueChanged() notifyShuffledChanged() } } - private fun applyNewQueue(library: MusicStore.Library, shuffled: Boolean, keep: Song?) { + private fun applyNewQueue( + library: MusicStore.Library, + settings: Settings, + shuffled: Boolean, + keep: Song? + ) { val newQueue = (parent?.songs ?: library.songs).toMutableList() val newIndex: Int @@ -317,10 +320,10 @@ class PlaybackStateManager private constructor() { val sort = parent.let { parent -> when (parent) { - null -> settingsManager.libSongSort - is Album -> settingsManager.detailAlbumSort - is Artist -> settingsManager.detailArtistSort - is Genre -> settingsManager.detailGenreSort + null -> settings.libSongSort + is Album -> settings.detailAlbumSort + is Artist -> settings.detailArtistSort + is Genre -> settings.detailGenreSort } } @@ -372,26 +375,12 @@ class PlaybackStateManager private constructor() { /** Rewind to the beginning of a song. */ fun rewind() = seekTo(0) - /** Repeat the current song (in line with the user configuration). */ - fun repeat() { - rewind() - if (settingsManager.pauseOnRepeat) { - synchronized(this) { - isPlaying = false - } - } - } - // --- PERSISTENCE FUNCTIONS --- - /** - * Restore the state from the database - * @param context [Context] required. - */ - suspend fun restoreState(context: Context) { + /** Restore the state from the [database] */ + suspend fun restoreState(database: PlaybackStateDatabase) { val library = musicStore.library ?: return val start: Long - val database = PlaybackStateDatabase.getInstance(context) val state: PlaybackStateDatabase.SavedState? logD("Getting state from DB") @@ -421,17 +410,13 @@ class PlaybackStateManager private constructor() { } } - /** - * Save the current state to the database. - * @param context [Context] required - */ - suspend fun saveState(context: Context) { + /** Save the current state to the [database]. */ + suspend fun saveState(database: PlaybackStateDatabase) { logD("Saving state to DB") // Pack the entire state and save it to the database. withContext(Dispatchers.IO) { val start = System.currentTimeMillis() - val database = PlaybackStateDatabase.getInstance(context) database.write( synchronized(this) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 02b8bcb62..e0752ffb8 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD /** @@ -46,9 +46,9 @@ class MediaSessionComponent(private val context: Context, private val player: Pl Player.Listener, MediaSessionCompat.Callback(), PlaybackStateManager.Callback, - SettingsManager.Callback { + Settings.Callback { private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(context, this) private val mediaSession = MediaSessionCompat(context, context.packageName).apply { isActive = true } private val provider = BitmapProvider(context) @@ -59,7 +59,6 @@ class MediaSessionComponent(private val context: Context, private val player: Pl init { player.addListener(this) playbackManager.addCallback(this) - settingsManager.addCallback(this) mediaSession.setCallback(this) } @@ -69,9 +68,9 @@ class MediaSessionComponent(private val context: Context, private val player: Pl fun release() { provider.release() + settings.release() player.removeListener(this) playbackManager.removeCallback(this) - settingsManager.removeCallback(this) mediaSession.apply { isActive = false @@ -218,7 +217,8 @@ class MediaSessionComponent(private val context: Context, private val player: Pl override fun onSetShuffleMode(shuffleMode: Int) { playbackManager.reshuffle( shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL || - shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP) + shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP, + settings) } override fun onStop() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 192c9dfe2..ac4b61a95 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -48,9 +48,10 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor +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.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetProvider @@ -74,10 +75,10 @@ class PlaybackService : Player.Listener, NotificationComponent.Callback, PlaybackStateManager.Controller, - SettingsManager.Callback { + Settings.Callback { // Player components private lateinit var player: ExoPlayer - private val replayGainProcessor = ReplayGainAudioProcessor() + private lateinit var replayGainProcessor: ReplayGainAudioProcessor // System backend components private lateinit var notificationComponent: NotificationComponent @@ -87,7 +88,7 @@ class PlaybackService : // Managers private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() + private lateinit var settings: Settings // State private var isForeground = false @@ -105,6 +106,8 @@ class PlaybackService : // --- PLAYER SETUP --- + replayGainProcessor = ReplayGainAudioProcessor(this) + player = newPlayer() player.addListener(this) @@ -138,8 +141,9 @@ class PlaybackService : // --- PLAYBACKSTATEMANAGER SETUP --- + settings = Settings(this, this) + playbackManager.registerController(this) - settingsManager.addCallback(this) logD("Service created") } @@ -166,7 +170,7 @@ class PlaybackService : playbackManager.isPlaying = false playbackManager.unregisterController(this) - settingsManager.removeCallback(this) + settings.release() unregisterReceiver(systemReceiver) serviceJob.cancel() @@ -196,7 +200,10 @@ class PlaybackService : when (state) { Player.STATE_ENDED -> { if (playbackManager.repeatMode == RepeatMode.TRACK) { - playbackManager.repeat() + playbackManager.rewind() + if (settings.pauseOnRepeat) { + playbackManager.isPlaying = false + } } else { playbackManager.next() } @@ -261,7 +268,7 @@ class PlaybackService : } override fun shouldPrevRewind() = - settingsManager.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD + settings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD override fun onPlayingChanged(isPlaying: Boolean) { player.playWhenReady = isPlaying @@ -269,13 +276,13 @@ class PlaybackService : } override fun onRepeatChanged(repeatMode: RepeatMode) { - if (!settingsManager.useAltNotifAction) { + if (!settings.useAltNotifAction) { notificationComponent.updateRepeatMode(repeatMode) } } override fun onShuffledChanged(isShuffled: Boolean) { - if (settingsManager.useAltNotifAction) { + if (settings.useAltNotifAction) { notificationComponent.updateShuffled(isShuffled) } } @@ -293,7 +300,7 @@ class PlaybackService : } override fun onNotifSettingsChanged() { - if (settingsManager.useAltNotifAction) { + if (settings.useAltNotifAction) { onShuffledChanged(playbackManager.isShuffled) } else { onRepeatChanged(playbackManager.repeatMode) @@ -353,7 +360,9 @@ class PlaybackService : private fun stopAndSave() { ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) isForeground = false - saveScope.launch { playbackManager.saveState(this@PlaybackService) } + saveScope.launch { + playbackManager.saveState(PlaybackStateDatabase.getInstance(this@PlaybackService)) + } } /** A [BroadcastReceiver] for receiving general playback events from the system. */ @@ -386,7 +395,8 @@ class PlaybackService : ACTION_PLAY_PAUSE -> playbackManager.isPlaying = !playbackManager.isPlaying ACTION_INC_REPEAT_MODE -> playbackManager.repeatMode = playbackManager.repeatMode.increment() - ACTION_INVERT_SHUFFLE -> playbackManager.reshuffle(!playbackManager.isShuffled) + ACTION_INVERT_SHUFFLE -> + playbackManager.reshuffle(!playbackManager.isShuffled, settings) ACTION_SKIP_PREV -> playbackManager.prev() ACTION_SKIP_NEXT -> playbackManager.next() ACTION_EXIT -> { @@ -408,7 +418,7 @@ class PlaybackService : */ private fun maybeResumeFromPlug() { if (playbackManager.song != null && - settingsManager.headsetAutoplay && + settings.headsetAutoplay && initialHeadsetPlugEventHandled) { logD("Device connected, resuming") playbackManager.isPlaying = true diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 01afdbf55..89f33127a 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -36,6 +36,8 @@ import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item import org.oxycblt.auxio.ui.MenuFragment @@ -57,6 +59,7 @@ class SearchFragment : private val searchModel: SearchViewModel by androidViewModels() private val searchAdapter = SearchAdapter(this) + private val settings: Settings by settings() private var imm: InputMethodManager? = null private var launchedKeyboard = false @@ -124,7 +127,7 @@ class SearchFragment : override fun onItemClick(item: Item) { when (item) { - is Song -> playbackModel.play(item) + is Song -> playbackModel.play(item, settings.songPlaybackMode) is MusicParent -> navModel.exploreNavigateTo(item) } } diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index 359e6841b..93aec29eb 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.launch import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Header import org.oxycblt.auxio.ui.Item @@ -45,23 +45,18 @@ import org.oxycblt.auxio.util.logD class SearchViewModel(application: Application) : AndroidViewModel(application), MusicStore.Callback { private val musicStore = MusicStore.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(application) private val _searchResults = MutableStateFlow(listOf()) /** Current search results from the last [search] call. */ val searchResults: StateFlow> get() = _searchResults - private var _filterMode: DisplayMode? = null val filterMode: DisplayMode? - get() = _filterMode + get() = settings.searchFilterMode private var lastQuery: String? = null - init { - _filterMode = settingsManager.searchFilterMode - } - /** * Use [query] to perform a search of the music library. Will push results to [searchResults]. */ @@ -84,28 +79,28 @@ class SearchViewModel(application: Application) : // Note: a filter mode of null means to not filter at all. - if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) { + if (filterMode == null || filterMode == DisplayMode.SHOW_ARTISTS) { library.artists.filterByOrNull(query)?.let { artists -> results.add(Header(-1, R.string.lbl_artists)) results.addAll(sort.artists(artists)) } } - if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) { + if (filterMode == null || filterMode == DisplayMode.SHOW_ALBUMS) { library.albums.filterByOrNull(query)?.let { albums -> results.add(Header(-2, R.string.lbl_albums)) results.addAll(sort.albums(albums)) } } - if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) { + if (filterMode == null || filterMode == DisplayMode.SHOW_GENRES) { library.genres.filterByOrNull(query)?.let { genres -> results.add(Header(-3, R.string.lbl_genres)) results.addAll(sort.genres(genres)) } } - if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) { + if (filterMode == null || filterMode == DisplayMode.SHOW_SONGS) { library.songs.filterByOrNull(query)?.let { songs -> results.add(Header(-4, R.string.lbl_songs)) results.addAll(sort.songs(songs)) @@ -120,7 +115,7 @@ class SearchViewModel(application: Application) : * Update the current filter mode with a menu [id]. New value will be pushed to [filterMode]. */ fun updateFilterModeWithId(@IdRes id: Int) { - _filterMode = + val newFilterMode = when (id) { R.id.option_filter_songs -> DisplayMode.SHOW_SONGS R.id.option_filter_albums -> DisplayMode.SHOW_ALBUMS @@ -129,9 +124,9 @@ class SearchViewModel(application: Application) : else -> null } - logD("Updating filter mode to $_filterMode") + logD("Updating filter mode to $newFilterMode") - settingsManager.searchFilterMode = _filterMode + settings.searchFilterMode = filterMode search(lastQuery) } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt index 11db91c56..de861189a 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/AboutFragment.kt @@ -25,7 +25,6 @@ import android.os.Bundle import android.view.LayoutInflater import androidx.core.net.toUri import androidx.core.view.updatePadding -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.oxycblt.auxio.BuildConfig @@ -37,6 +36,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.launch @@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.textSafe * @author OxygenCobalt */ class AboutFragment : ViewBindingFragment() { - private val homeModel: HomeViewModel by activityViewModels() + private val homeModel: HomeViewModel by androidActivityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = FragmentAboutBinding.inflate(inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt new file mode 100644 index 000000000..59d258602 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.oxycblt.auxio.settings + +import android.content.Context +import android.content.SharedPreferences +import android.os.storage.StorageManager +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.preference.PreferenceManager +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty +import org.oxycblt.auxio.R +import org.oxycblt.auxio.home.tabs.Tab +import org.oxycblt.auxio.music.Directory +import org.oxycblt.auxio.music.dirs.MusicDirs +import org.oxycblt.auxio.playback.replaygain.ReplayGainMode +import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp +import org.oxycblt.auxio.playback.state.PlaybackMode +import org.oxycblt.auxio.ui.DisplayMode +import org.oxycblt.auxio.ui.Sort +import org.oxycblt.auxio.ui.accent.Accent +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.requireAttached +import org.oxycblt.auxio.util.unlikelyToBeNull + +fun Fragment.settings(): ReadOnlyProperty = + object : ReadOnlyProperty, DefaultLifecycleObserver { + private var settings: Settings? = null + + init { + lifecycle.addObserver( + object : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + viewLifecycleOwnerLiveData.observe(this@settings) { viewLifecycleOwner -> + viewLifecycleOwner.lifecycle.addObserver(this) + } + } + }) + } + + override fun getValue(thisRef: Fragment, property: KProperty<*>): Settings { + requireAttached() + + val currentSettings = settings + if (currentSettings != null) { + return currentSettings + } + + val newSettings = Settings(requireContext()) + settings = newSettings + return newSettings + } + + override fun onDestroy(owner: LifecycleOwner) { + settings = null + } + } + +class Settings(private val context: Context, private val callback: Callback? = null) : + SharedPreferences.OnSharedPreferenceChangeListener { + private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) + + init { + if (callback != null) { + inner.registerOnSharedPreferenceChangeListener(this) + } + } + + fun release() { + inner.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + val callback = unlikelyToBeNull(callback) + when (key) { + context.getString(R.string.set_key_alt_notif_action) -> + callback.onNotifSettingsChanged() + context.getString(R.string.set_key_show_covers), + context.getString(R.string.set_key_quality_covers) -> callback.onCoverSettingsChanged() + context.getString(R.string.set_key_lib_tabs) -> callback.onLibrarySettingsChanged() + context.getString(R.string.set_key_replay_gain), + context.getString(R.string.set_key_pre_amp_with), + context.getString(R.string.set_key_pre_amp_without) -> + callback.onReplayGainSettingsChanged() + } + } + + // --- VALUES --- + + /** The current theme */ + val theme: Int + get() = + inner.getInt( + context.getString(R.string.set_key_theme), + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + + /** Whether the dark theme should be black or not */ + val useBlackTheme: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_black_theme), false) + + /** The current accent. */ + var accent: Accent + get() = handleAccentCompat(context, inner) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_accent), value.index) + apply() + } + } + + /** + * Whether to display the RepeatMode or the shuffle status on the notification. False if repeat, + * true if shuffle. + */ + val useAltNotifAction: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_alt_notif_action), false) + + /** The current library tabs preferred by the user. */ + var libTabs: Array + get() = + Tab.fromSequence( + inner.getInt(context.getString(R.string.set_key_lib_tabs), Tab.SEQUENCE_DEFAULT)) + ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT)) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_tabs), Tab.toSequence(value)) + apply() + } + } + + /** Whether to load embedded covers */ + val showCovers: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_show_covers), true) + + /** Whether to ignore MediaStore covers */ + val useQualityCovers: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_quality_covers), false) + + /** Whether to round album covers */ + val roundCovers: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_round_covers), false) + + /** Whether to resume playback when a headset is connected (may not work well in all cases) */ + 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.OFF + + /** 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 queue to create when a song is selected (ex. From All Songs or Search) */ + val songPlaybackMode: PlaybackMode + get() = + PlaybackMode.fromInt( + inner.getInt(context.getString(R.string.set_key_song_play_mode), Int.MIN_VALUE)) + ?: PlaybackMode.ALL_SONGS + + /** Whether shuffle should stay on when a new song is selected. */ + val keepShuffle: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_keep_shuffle), true) + + /** Whether to rewind when the back button is pressed. */ + val rewindWithPrev: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_rewind_prev), true) + + /** + * Whether [org.oxycblt.auxio.playback.state.RepeatMode.TRACK] should pause when the track + * repeats + */ + val pauseOnRepeat: Boolean + get() = inner.getBoolean(context.getString(R.string.set_key_repeat_pause), false) + + /** Get the list of directories that music should be hidden/loaded from. */ + fun getMusicDirs(storageManager: StorageManager): MusicDirs { + val key = context.getString(R.string.set_key_music_dirs) + + if (!inner.contains(key)) { + logD("Attempting to migrate excluded directories") + // We need to migrate this setting now while we have a context. Note that while + // this does do IO work, the old excluded directory database is so small as to make + // it negligible. + setMusicDirs(MusicDirs(handleExcludedCompat(context, storageManager), false)) + } + + val dirs = + (inner.getStringSet(key, null) ?: emptySet()).mapNotNull { + Directory.fromDocumentUri(storageManager, it) + } + + return MusicDirs( + dirs, inner.getBoolean(context.getString(R.string.set_key_music_dirs_include), false)) + } + + /** Set the list of directories that music should be hidden/loaded from. */ + fun setMusicDirs(musicDirs: MusicDirs) { + inner.edit { + putStringSet( + context.getString(R.string.set_key_music_dirs), + musicDirs.dirs.map(Directory::toDocumentUri).toSet()) + putBoolean( + context.getString(R.string.set_key_music_dirs_include), musicDirs.shouldInclude) + + // TODO: This is a stopgap measure before automatic rescanning, remove + commit() + } + } + + /** The current filter mode of the search tab */ + var searchFilterMode: DisplayMode? + get() = + DisplayMode.fromInt( + inner.getInt(context.getString(R.string.set_key_search_filter), Int.MIN_VALUE)) + set(value) { + inner.edit { + putInt( + context.getString(R.string.set_key_search_filter), + value?.intCode ?: Int.MIN_VALUE) + apply() + } + } + + /** The song sort mode on HomeFragment */ + var libSongSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_songs_sort), Int.MIN_VALUE)) + ?: Sort.ByName(true) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_songs_sort), value.intCode) + apply() + } + } + + /** The album sort mode on HomeFragment */ + var libAlbumSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_albums_sort), Int.MIN_VALUE)) + ?: Sort.ByName(true) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_albums_sort), value.intCode) + apply() + } + } + + /** The artist sort mode on HomeFragment */ + var libArtistSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_artists_sort), Int.MIN_VALUE)) + ?: Sort.ByName(true) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) + apply() + } + } + + /** The genre sort mode on HomeFragment */ + var libGenreSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_genres_sort), Int.MIN_VALUE)) + ?: Sort.ByName(true) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_genres_sort), value.intCode) + apply() + } + } + + /** The detail album sort mode */ + var detailAlbumSort: Sort + get() { + var sort = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_album_sort), Int.MIN_VALUE)) + ?: Sort.ByDisc(true) + + // Correct legacy album sort modes to Disc + if (sort is Sort.ByName) { + sort = Sort.ByDisc(sort.isAscending) + } + + return sort + } + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_album_sort), value.intCode) + apply() + } + } + + /** The detail artist sort mode */ + var detailArtistSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_artist_sort), Int.MIN_VALUE)) + ?: Sort.ByYear(false) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_artists_sort), value.intCode) + apply() + } + } + + /** The detail genre sort mode */ + var detailGenreSort: Sort + get() = + Sort.fromIntCode( + inner.getInt(context.getString(R.string.set_key_lib_genre_sort), Int.MIN_VALUE)) + ?: Sort.ByName(true) + set(value) { + inner.edit { + putInt(context.getString(R.string.set_key_lib_genre_sort), value.intCode) + apply() + } + } + + /** + * An interface for receiving some preference updates. Use/Extend this instead of + * [SharedPreferences.OnSharedPreferenceChangeListener] if possible, as it doesn't require a + * context. + */ + interface Callback { + fun onLibrarySettingsChanged() {} + fun onNotifSettingsChanged() {} + fun onCoverSettingsChanged() {} + fun onReplayGainSettingsChanged() {} + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt index 6a132c5fc..0747919c8 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsCompat.kt @@ -26,6 +26,7 @@ import android.os.storage.StorageManager import android.util.Log import androidx.core.content.edit import java.io.File +import org.oxycblt.auxio.R import org.oxycblt.auxio.music.Directory import org.oxycblt.auxio.music.directoryCompat import org.oxycblt.auxio.music.isInternalCompat @@ -34,35 +35,11 @@ import org.oxycblt.auxio.ui.accent.Accent import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.queryAll -// A couple of utils for migrating from old settings values to the new formats +// A couple of utils for migrating from old settings values to the new formats. +// Usually, these will last for 6 months before being removed. -fun handleAccentCompat(prefs: SharedPreferences): Accent { - if (prefs.contains(OldKeys.KEY_ACCENT2)) { - Log.d("Auxio.SettingsCompat", "Migrating ${OldKeys.KEY_ACCENT2}") - - var accent = prefs.getInt(OldKeys.KEY_ACCENT2, 5) - - // Blue grey was merged with Light Blue in 2.0.0 - if (accent >= 17) { - accent = 6 - } - - // Deep Orange was merged with red in 2.0.0 - if (accent == 14) { - accent = 0 - } - - // Correct accents beyond deep orange (Brown/Grey) - if (accent > 14) { - accent-- - } - - prefs.edit { - putInt(OldKeys.KEY_ACCENT3, accent) - remove(OldKeys.KEY_ACCENT2) - apply() - } - } +fun handleAccentCompat(context: Context, prefs: SharedPreferences): Accent { + val currentKey = context.getString(R.string.set_key_accent) if (prefs.contains(OldKeys.KEY_ACCENT3)) { Log.d("Auxio.SettingsCompat", "Migrating ${OldKeys.KEY_ACCENT3}") @@ -76,13 +53,13 @@ fun handleAccentCompat(prefs: SharedPreferences): Accent { } prefs.edit { - putInt(SettingsManager.KEY_ACCENT, accent) + putInt(currentKey, accent) remove(OldKeys.KEY_ACCENT3) apply() } } - return Accent.from(prefs.getInt(SettingsManager.KEY_ACCENT, Accent.DEFAULT)) + return Accent.from(prefs.getInt(currentKey, Accent.DEFAULT)) } /** @@ -154,6 +131,5 @@ class LegacyExcludedDatabase(context: Context) : /** Cache of the old keys used in Auxio. */ private object OldKeys { - const val KEY_ACCENT2 = "KEY_ACCENT2" const val KEY_ACCENT3 = "auxio_accent" } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index ebb27193c..1e31e4f6d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -22,7 +22,6 @@ import android.view.View import androidx.annotation.DrawableRes import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.updatePadding -import androidx.fragment.app.activityViewModels import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat @@ -38,6 +37,7 @@ import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.settings.ui.IntListPreference import org.oxycblt.auxio.settings.ui.IntListPreferenceDialog import org.oxycblt.auxio.ui.accent.AccentDialog +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.hardRestart import org.oxycblt.auxio.util.isNight @@ -54,8 +54,8 @@ import org.oxycblt.auxio.util.showToast */ @Suppress("UNUSED") class SettingsListFragment : PreferenceFragmentCompat() { - private val playbackModel: PlaybackViewModel by activityViewModels() - val settingsManager = SettingsManager.getInstance() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() + private val settings: Settings by settings() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -112,7 +112,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { preference.apply { when (key) { - SettingsManager.KEY_THEME -> { + getString(R.string.set_key_theme) -> { setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon()) onPreferenceChangeListener = @@ -122,16 +122,17 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_ACCENT -> { + getString(R.string.set_key_accent) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { AccentDialog().show(childFragmentManager, AccentDialog.TAG) true } - summary = context.getString(settingsManager.accent.name) + // TODO: Replace with preference impl + summary = context.getString(settings.accent.name) } - SettingsManager.KEY_BLACK_THEME -> { + getString(R.string.set_key_black_theme) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { if (requireContext().isNight) { @@ -141,23 +142,23 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_LIB_TABS -> { + getString(R.string.set_key_lib_tabs) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG) true } } - SettingsManager.KEY_SHOW_COVERS, - SettingsManager.KEY_QUALITY_COVERS -> { + getString(R.string.set_key_show_covers), + getString(R.string.set_key_quality_covers) -> { onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> Coil.imageLoader(requireContext()).apply { this.memoryCache?.clear() } true } } - SettingsManager.KEY_REPLAY_GAIN -> { - notifyDependencyChange(settingsManager.replayGainMode == ReplayGainMode.OFF) + getString(R.string.set_key_replay_gain) -> { + notifyDependencyChange(settings.replayGainMode == ReplayGainMode.OFF) onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value -> notifyDependencyChange( @@ -165,7 +166,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_PRE_AMP -> { + getString(R.string.set_key_pre_amp) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { PreAmpCustomizeDialog() @@ -173,9 +174,10 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_SAVE_STATE -> { + getString(R.string.set_key_save_state) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { + // FIXME: Callback can still occur on non-attached fragment playbackModel.savePlaybackState(requireContext()) { requireContext().showToast(R.string.lbl_state_saved) } @@ -183,7 +185,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_REINDEX -> { + getString(R.string.set_key_reindex) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { playbackModel.savePlaybackState(requireContext()) { @@ -193,7 +195,7 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_MUSIC_DIRS -> { + getString(R.string.set_key_music_dirs) -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG) diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt deleted file mode 100644 index ba18cbf36..000000000 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Copyright (c) 2021 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.settings - -import android.content.Context -import android.content.SharedPreferences -import android.os.storage.StorageManager -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit -import androidx.preference.PreferenceManager -import org.oxycblt.auxio.home.tabs.Tab -import org.oxycblt.auxio.music.Directory -import org.oxycblt.auxio.music.dirs.MusicDirs -import org.oxycblt.auxio.playback.replaygain.ReplayGainMode -import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp -import org.oxycblt.auxio.playback.state.PlaybackMode -import org.oxycblt.auxio.ui.DisplayMode -import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.accent.Accent -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.unlikelyToBeNull - -/** - * Wrapper around the [SharedPreferences] class that writes & reads values without a context. - * @author OxygenCobalt - */ -class SettingsManager private constructor(context: Context) : - SharedPreferences.OnSharedPreferenceChangeListener { - - private val inner = PreferenceManager.getDefaultSharedPreferences(context) - - // --- VALUES --- - - /** The current theme */ - val theme: Int - get() = inner.getInt(KEY_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - - /** Whether the dark theme should be black or not */ - val useBlackTheme: Boolean - get() = inner.getBoolean(KEY_BLACK_THEME, false) - - /** The current accent. */ - var accent: Accent - get() = handleAccentCompat(inner) - set(value) { - inner.edit { - putInt(KEY_ACCENT, value.index) - apply() - } - } - - /** - * Whether to display the RepeatMode or the shuffle status on the notification. False if repeat, - * true if shuffle. - */ - val useAltNotifAction: Boolean - get() = inner.getBoolean(KEY_USE_ALT_NOTIFICATION_ACTION, false) - - /** The current library tabs preferred by the user. */ - var libTabs: Array - get() = - Tab.fromSequence(inner.getInt(KEY_LIB_TABS, Tab.SEQUENCE_DEFAULT)) - ?: unlikelyToBeNull(Tab.fromSequence(Tab.SEQUENCE_DEFAULT)) - set(value) { - inner.edit { - putInt(KEY_LIB_TABS, Tab.toSequence(value)) - apply() - } - } - - /** Whether to load embedded covers */ - val showCovers: Boolean - get() = inner.getBoolean(KEY_SHOW_COVERS, true) - - /** Whether to ignore MediaStore covers */ - val useQualityCovers: Boolean - get() = inner.getBoolean(KEY_QUALITY_COVERS, false) - - /** Whether to round album covers */ - val roundCovers: Boolean - get() = inner.getBoolean(KEY_ROUND_COVERS, false) - - /** Whether to resume playback when a headset is connected (may not work well in all cases) */ - val headsetAutoplay: Boolean - get() = inner.getBoolean(KEY_HEADSET_AUTOPLAY, false) - - /** The current ReplayGain configuration */ - val replayGainMode: ReplayGainMode - get() = - ReplayGainMode.fromIntCode(inner.getInt(KEY_REPLAY_GAIN, Int.MIN_VALUE)) - ?: ReplayGainMode.OFF - - /** The current ReplayGain pre-amp configuration */ - var replayGainPreAmp: ReplayGainPreAmp - get() = - ReplayGainPreAmp( - inner.getFloat(KEY_PRE_AMP_WITH, 0f), inner.getFloat(KEY_PRE_AMP_WITHOUT, 0f)) - set(value) { - inner.edit { - putFloat(KEY_PRE_AMP_WITH, value.with) - putFloat(KEY_PRE_AMP_WITHOUT, value.without) - apply() - } - } - - /** What queue to create when a song is selected (ex. From All Songs or Search) */ - val songPlaybackMode: PlaybackMode - get() = - PlaybackMode.fromInt(inner.getInt(KEY_SONG_PLAYBACK_MODE, Int.MIN_VALUE)) - ?: PlaybackMode.ALL_SONGS - - /** Whether shuffle should stay on when a new song is selected. */ - val keepShuffle: Boolean - get() = inner.getBoolean(KEY_KEEP_SHUFFLE, true) - - /** Whether to rewind when the back button is pressed. */ - val rewindWithPrev: Boolean - get() = inner.getBoolean(KEY_PREV_REWIND, true) - - /** - * Whether [org.oxycblt.auxio.playback.state.RepeatMode.TRACK] should pause when the track - * repeats - */ - val pauseOnRepeat: Boolean - get() = inner.getBoolean(KEY_PAUSE_ON_REPEAT, false) - - /** Get the list of directories that music should be hidden/loaded from. */ - fun getMusicDirs(context: Context, storageManager: StorageManager): MusicDirs { - if (!inner.contains(KEY_MUSIC_DIRS)) { - logD("Attempting to migrate excluded directories") - // We need to migrate this setting now while we have a context. Note that while - // this does do IO work, the old excluded directory database is so small as to make - // it negligible. - setMusicDirs(MusicDirs(handleExcludedCompat(context, storageManager), false)) - } - - val dirs = - (inner.getStringSet(KEY_MUSIC_DIRS, null) ?: emptySet()).mapNotNull { - Directory.fromDocumentUri(storageManager, it) - } - - return MusicDirs(dirs, inner.getBoolean(KEY_SHOULD_INCLUDE, false)) - } - - /** Set the list of directories that music should be hidden/loaded from. */ - fun setMusicDirs(musicDirs: MusicDirs) { - inner.edit { - putStringSet(KEY_MUSIC_DIRS, musicDirs.dirs.map(Directory::toDocumentUri).toSet()) - putBoolean(KEY_SHOULD_INCLUDE, musicDirs.shouldInclude) - - // TODO: This is a stopgap measure before automatic rescanning, remove - commit() - } - } - - /** The current filter mode of the search tab */ - var searchFilterMode: DisplayMode? - get() = DisplayMode.fromInt(inner.getInt(KEY_SEARCH_FILTER_MODE, Int.MIN_VALUE)) - set(value) { - inner.edit { - putInt(KEY_SEARCH_FILTER_MODE, value?.intCode ?: Int.MIN_VALUE) - apply() - } - } - - /** The song sort mode on HomeFragment */ - var libSongSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_LIB_SONGS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) - set(value) { - inner.edit { - putInt(KEY_LIB_SONGS_SORT, value.intCode) - apply() - } - } - - /** The album sort mode on HomeFragment */ - var libAlbumSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_LIB_ALBUMS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) - set(value) { - inner.edit { - putInt(KEY_LIB_ALBUMS_SORT, value.intCode) - apply() - } - } - - /** The artist sort mode on HomeFragment */ - var libArtistSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_LIB_ARTISTS_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) - set(value) { - inner.edit { - putInt(KEY_LIB_ARTISTS_SORT, value.intCode) - apply() - } - } - - /** The genre sort mode on HomeFragment */ - var libGenreSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_LIB_GENRES_SORT, Int.MIN_VALUE)) ?: Sort.ByName(true) - set(value) { - inner.edit { - putInt(KEY_LIB_GENRES_SORT, value.intCode) - apply() - } - } - - /** The detail album sort mode */ - var detailAlbumSort: Sort - get() { - var sort = - Sort.fromIntCode(inner.getInt(KEY_DETAIL_ALBUM_SORT, Int.MIN_VALUE)) - ?: Sort.ByDisc(true) - - // Correct legacy album sort modes to Disc - if (sort is Sort.ByName) { - sort = Sort.ByDisc(sort.isAscending) - } - - return sort - } - set(value) { - inner.edit { - putInt(KEY_DETAIL_ALBUM_SORT, value.intCode) - apply() - } - } - - /** The detail artist sort mode */ - var detailArtistSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_DETAIL_ARTIST_SORT, Int.MIN_VALUE)) - ?: Sort.ByYear(false) - set(value) { - inner.edit { - putInt(KEY_DETAIL_ARTIST_SORT, value.intCode) - apply() - } - } - - /** The detail genre sort mode */ - var detailGenreSort: Sort - get() = - Sort.fromIntCode(inner.getInt(KEY_DETAIL_GENRE_SORT, Int.MIN_VALUE)) - ?: Sort.ByName(true) - set(value) { - inner.edit { - putInt(KEY_DETAIL_GENRE_SORT, value.intCode) - apply() - } - } - - init { - inner.registerOnSharedPreferenceChangeListener(this) - } - - // --- CALLBACKS --- - - private val callbacks = mutableListOf() - - fun addCallback(callback: Callback) { - callbacks.add(callback) - } - - fun removeCallback(callback: Callback) { - callbacks.remove(callback) - } - - // --- OVERRIDES --- - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - when (key) { - KEY_USE_ALT_NOTIFICATION_ACTION -> callbacks.forEach { it.onNotifSettingsChanged() } - KEY_SHOW_COVERS, - KEY_QUALITY_COVERS -> callbacks.forEach { it.onCoverSettingsChanged() } - KEY_LIB_TABS -> callbacks.forEach { it.onLibrarySettingsChanged() } - KEY_REPLAY_GAIN, - KEY_PRE_AMP_WITH, - KEY_PRE_AMP_WITHOUT -> callbacks.forEach { it.onReplayGainSettingsChanged() } - } - } - - /** - * An interface for receiving some preference updates. Use/Extend this instead of - * [SharedPreferences.OnSharedPreferenceChangeListener] if possible, as it doesn't require a - * context. - */ - interface Callback { - fun onLibrarySettingsChanged() {} - fun onNotifSettingsChanged() {} - fun onCoverSettingsChanged() {} - fun onReplayGainSettingsChanged() {} - } - - companion object { - // Note: The old way of naming keys was to prefix them with KEY_. Now it's to prefix them - // with auxio_. - const val KEY_THEME = "KEY_THEME2" - const val KEY_BLACK_THEME = "KEY_BLACK_THEME" - const val KEY_ACCENT = "auxio_accent2" - - const val KEY_LIB_TABS = "auxio_lib_tabs" - const val KEY_SHOW_COVERS = "KEY_SHOW_COVERS" - const val KEY_QUALITY_COVERS = "KEY_QUALITY_COVERS" - const val KEY_ROUND_COVERS = "auxio_round_covers" - const val KEY_USE_ALT_NOTIFICATION_ACTION = "KEY_ALT_NOTIF_ACTION" - - const val KEY_HEADSET_AUTOPLAY = "auxio_headset_autoplay" - const val KEY_REPLAY_GAIN = "auxio_replay_gain" - const val KEY_PRE_AMP = "auxio_pre_amp" - const val KEY_PRE_AMP_WITH = "auxio_pre_amp_with" - const val KEY_PRE_AMP_WITHOUT = "auxio_pre_amp_without" - - const val KEY_SONG_PLAYBACK_MODE = "KEY_SONG_PLAY_MODE2" - const val KEY_KEEP_SHUFFLE = "KEY_KEEP_SHUFFLE" - const val KEY_PREV_REWIND = "KEY_PREV_REWIND" - const val KEY_PAUSE_ON_REPEAT = "KEY_LOOP_PAUSE" - - const val KEY_SAVE_STATE = "auxio_save_state" - const val KEY_REINDEX = "auxio_reindex" - const val KEY_MUSIC_DIRS = "auxio_music_dirs" - const val KEY_SHOULD_INCLUDE = "auxio_include_dirs" - - const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER" - - const val KEY_LIB_SONGS_SORT = "auxio_songs_sort" - const val KEY_LIB_ALBUMS_SORT = "auxio_albums_sort" - const val KEY_LIB_ARTISTS_SORT = "auxio_artists_sort" - const val KEY_LIB_GENRES_SORT = "auxio_genres_sort" - - const val KEY_DETAIL_ALBUM_SORT = "auxio_album_sort" - const val KEY_DETAIL_ARTIST_SORT = "auxio_artist_sort" - const val KEY_DETAIL_GENRE_SORT = "auxio_genre_sort" - - @Volatile private var INSTANCE: SettingsManager? = null - - /** - * Init the single instance of [SettingsManager]. Done so that every object can have access - * to it regardless of if it has a context. - */ - fun init(context: Context): SettingsManager { - if (INSTANCE == null) { - synchronized(this) { INSTANCE = SettingsManager(context) } - } - - return getInstance() - } - - /** Get the single instance of [SettingsManager]. */ - fun getInstance(): SettingsManager { - val instance = INSTANCE - - if (instance != null) { - return instance - } - - error("SettingsManager must be initialized with init() before getting its instance") - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt index ce53bcd00..ae6eb0241 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/MenuFragment.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logEOrThrow import org.oxycblt.auxio.util.showToast @@ -41,7 +42,7 @@ import org.oxycblt.auxio.util.showToast abstract class MenuFragment : ViewBindingFragment() { private var currentMenu: PopupMenu? = null - protected val playbackModel: PlaybackViewModel by activityViewModels() + protected val playbackModel: PlaybackViewModel by androidActivityViewModels() protected val navModel: NavigationViewModel by activityViewModels() /** diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt index 1e9826c6c..2802153cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt @@ -23,7 +23,8 @@ import androidx.appcompat.app.AlertDialog import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogAccentBinding -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.settings.settings import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -33,8 +34,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * @author OxygenCobalt */ class AccentDialog : ViewBindingDialogFragment(), AccentAdapter.Listener { - private val settingsManager = SettingsManager.getInstance() private var accentAdapter = AccentAdapter(this) + private val settings: Settings by settings() override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) @@ -42,9 +43,9 @@ class AccentDialog : ViewBindingDialogFragment(), AccentAda builder .setTitle(R.string.set_accent) .setPositiveButton(R.string.lbl_ok) { _, _ -> - if (accentAdapter.selectedAccent != settingsManager.accent) { + if (accentAdapter.selectedAccent != settings.accent) { logD("Applying new accent") - settingsManager.accent = unlikelyToBeNull(accentAdapter.selectedAccent) + settings.accent = unlikelyToBeNull(accentAdapter.selectedAccent) requireActivity().recreate() } @@ -59,7 +60,7 @@ class AccentDialog : ViewBindingDialogFragment(), AccentAda if (savedInstanceState != null) { Accent.from(savedInstanceState.getInt(KEY_PENDING_ACCENT)) } else { - settingsManager.accent + settings.accent }) } diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index ece102877..e5df26252 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -27,7 +27,9 @@ import android.os.Build import android.view.View import android.view.WindowInsets import android.widget.TextView +import androidx.activity.viewModels import androidx.annotation.ColorRes +import androidx.appcompat.app.AppCompatActivity import androidx.core.graphics.Insets import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.WindowInsetsCompat @@ -175,6 +177,9 @@ fun Fragment.launch( inline fun Fragment.androidViewModels() = viewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } +inline fun AppCompatActivity.androidViewModels() = + viewModels { ViewModelProvider.AndroidViewModelFactory(application) } + inline fun Fragment.androidActivityViewModels() = activityViewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) diff --git a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt index fd9e5e642..54048527b 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode -import org.oxycblt.auxio.settings.SettingsManager +import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.util.getDimenSizeSafe import org.oxycblt.auxio.util.logD @@ -41,15 +41,14 @@ import org.oxycblt.auxio.util.logD * @author OxygenCobalt */ class WidgetComponent(private val context: Context) : - PlaybackStateManager.Callback, SettingsManager.Callback { + PlaybackStateManager.Callback, Settings.Callback { private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() + private val settings = Settings(context, this) private val widget = WidgetProvider() private val provider = BitmapProvider(context) init { playbackManager.addCallback(this) - settingsManager.addCallback(this) if (playbackManager.isInitialized) { update() @@ -129,9 +128,9 @@ class WidgetComponent(private val context: Context) : */ fun release() { provider.release() + settings.release() widget.reset(context) playbackManager.removeCallback(this) - settingsManager.removeCallback(this) } // --- CALLBACKS --- diff --git a/app/src/main/res/values/config.xml b/app/src/main/res/values/config.xml index 09a31ac51..1f8fb8828 100644 --- a/app/src/main/res/values/config.xml +++ b/app/src/main/res/values/config.xml @@ -2,4 +2,5 @@ true 1 + 150 diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml deleted file mode 100644 index 26c4c8d7d..000000000 --- a/app/src/main/res/values/integers.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 150 - - - - - @string/set_theme_auto - @string/set_theme_day - @string/set_theme_night - - - - @integer/theme_auto - @integer/theme_light - @integer/theme_dark - - - - @string/lbl_play_all - @string/lbl_play_artist - @string/lbl_play_album - @string/lbl_play_genre - - - - @integer/play_mode_songs - @integer/play_mode_artist - @integer/play_mode_album - @integer/play_mode_genre - - - - @string/lbl_off - @string/set_replay_gain_track - @string/set_replay_gain_album - @string/set_replay_gain_dynamic - - - - @integer/replay_gain_off - @integer/replay_gain_track - @integer/replay_gain_album - @integer/replay_gain_dynamic - - - -1 - 1 - 2 - - 0xA103 - 0xA104 - 0xA105 - 0xA106 - - 0xA110 - 0xA111 - 0xA112 - 0xA113 - \ No newline at end of file diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml new file mode 100644 index 000000000..542db3ec1 --- /dev/null +++ b/app/src/main/res/values/settings.xml @@ -0,0 +1,94 @@ + + + + KEY_THEME2 + KEY_BLACK_THEME + auxio_accent2 + + auxio_lib_tabs + KEY_SHOW_COVERS + KEY_QUALITY_COVERS + auxio_round_covers + KEY_ALT_NOTIF_ACTION + + auxio_headset_autoplay + auxio_replay_gain + auxio_pre_amp + auxio_pre_amp_with + auxio_pre_amp_without + + KEY_SONG_PLAY_MODE2 + KEY_KEEP_SHUFFLE + KEY_PREV_REWIND + KEY_LOOP_PAUSE + + auxio_save_state + auxio_reindex + auxio_music_dirs + auxio_include_dirs + + KEY_SEARCH_FILTER + + auxio_songs_sort + auxio_albums_sort + auxio_artists_sort + auxio_genres_sort + + auxio_album_sort + auxio_artist_sort + auxio_genre_sort + + + @string/set_theme_auto + @string/set_theme_day + @string/set_theme_night + + + + @integer/theme_auto + @integer/theme_light + @integer/theme_dark + + + + @string/lbl_play_all + @string/lbl_play_artist + @string/lbl_play_album + @string/lbl_play_genre + + + + @integer/play_mode_songs + @integer/play_mode_artist + @integer/play_mode_album + @integer/play_mode_genre + + + + @string/lbl_off + @string/set_replay_gain_track + @string/set_replay_gain_album + @string/set_replay_gain_dynamic + + + + @integer/replay_gain_off + @integer/replay_gain_track + @integer/replay_gain_album + @integer/replay_gain_dynamic + + + -1 + 1 + 2 + + 0xA103 + 0xA104 + 0xA105 + 0xA106 + + 0xA110 + 0xA111 + 0xA112 + 0xA113 + \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index 098f373db..7477e03d3 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -11,19 +11,19 @@ app:icon="@drawable/ic_light" app:iconSpaceReserved="false" app:isPreferenceVisible="@bool/enable_theme_settings" - app:key="KEY_THEME2" + app:key="@string/set_key_theme" app:title="@string/set_theme" /> @@ -35,29 +35,29 @@ @@ -65,7 +65,7 @@ app:allowDividerBelow="false" app:defaultValue="false" app:iconSpaceReserved="false" - app:key="KEY_ALT_NOTIF_ACTION" + app:key="@string/set_key_alt_notif_action" app:summaryOff="@string/set_alt_repeat" app:summaryOn="@string/set_alt_shuffle" app:title="@string/set_alt_action" /> @@ -79,7 +79,7 @@ @@ -89,14 +89,14 @@ app:entries="@array/entries_replay_gain" app:entryValues="@array/values_replay_gain" app:iconSpaceReserved="false" - app:key="auxio_replay_gain" + app:key="@string/set_key_replay_gain" app:title="@string/set_replay_gain" /> @@ -111,14 +111,14 @@ app:entries="@array/entries_song_playback_mode" app:entryValues="@array/values_song_playback_mode" app:iconSpaceReserved="false" - app:key="KEY_SONG_PLAY_MODE2" + app:key="@string/set_key_song_play_mode" app:title="@string/set_song_mode" app:useSimpleSummaryProvider="true" /> @@ -126,7 +126,7 @@ app:allowDividerBelow="false" app:defaultValue="true" app:iconSpaceReserved="false" - app:key="KEY_PREV_REWIND" + app:key="@string/set_key_rewind_prev" app:summary="@string/set_rewind_prev_desc" app:title="@string/set_rewind_prev" /> @@ -134,7 +134,7 @@ app:allowDividerBelow="false" app:defaultValue="false" app:iconSpaceReserved="false" - app:key="KEY_LOOP_PAUSE" + app:key="@string/set_key_repeat_pause" app:summary="@string/set_repeat_pause_desc" app:title="@string/set_repeat_pause" /> @@ -146,19 +146,19 @@