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 @@