Refactor settings management

Heavily refactor the structure of settings to fix some bugs.
This commit is contained in:
OxygenCobalt 2020-12-03 18:35:47 -07:00
parent 21dbad7091
commit b8b6e8421b
14 changed files with 148 additions and 223 deletions

View file

@ -7,14 +7,12 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import org.oxycblt.auxio.databinding.ActivityMainBinding import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.playback.PlaybackService import org.oxycblt.auxio.playback.PlaybackService
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.settings.SettingsViewModel
import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.handleTransparentSystemBars import org.oxycblt.auxio.ui.handleTransparentSystemBars
import org.oxycblt.auxio.ui.toColor import org.oxycblt.auxio.ui.toColor
@ -24,8 +22,6 @@ import org.oxycblt.auxio.ui.toColor
// TODO: Landscape UI layouts // TODO: Landscape UI layouts
// FIXME: Compat issue with Versions 5 that leads to progress bar looking off // FIXME: Compat issue with Versions 5 that leads to progress bar looking off
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val settingsModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -37,42 +33,16 @@ class MainActivity : AppCompatActivity() {
val settingsManager = SettingsManager.init(applicationContext) val settingsManager = SettingsManager.init(applicationContext)
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(settingsManager.theme)
settingsManager.getTheme()
)
accent = settingsManager.getAccent() accent = settingsManager.accent
// Apply the theme // Apply the theme
setTheme(accent.second) setTheme(accent.second)
if (settingsManager.getEdgeToEdge() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { if (settingsManager.edgeEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
doEdgeToEdgeSetup(binding) doEdgeToEdgeSetup(binding)
} }
// --- VIEWMODEL SETUP ---
settingsModel.theme.observe(this) {
if (it != null) {
doThemeRecreate(it)
}
}
settingsModel.accent.observe(this) {
if (it != null) {
recreate()
settingsModel.doneWithAccentUpdate()
}
}
settingsModel.edge.observe(this) {
if (it != null) {
recreate()
settingsModel.doneWithEdgeUpdate()
}
}
} }
override fun onStart() { override fun onStart() {
@ -88,8 +58,7 @@ class MainActivity : AppCompatActivity() {
window?.apply { window?.apply {
statusBarColor = Color.TRANSPARENT statusBarColor = Color.TRANSPARENT
// Use a heavily transparent scrim on the nav bar as otherwise the transparency wont // Use a heavily transparent scrim on the nav bar as full transparency is borked
// work.
navigationBarColor = R.color.nav_color.toColor(this@MainActivity) navigationBarColor = R.color.nav_color.toColor(this@MainActivity)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

View file

@ -39,12 +39,13 @@ class LibraryViewModel : ViewModel() {
private var mSearchHasFocus = false private var mSearchHasFocus = false
val searchHasFocus: Boolean get() = mSearchHasFocus val searchHasFocus: Boolean get() = mSearchHasFocus
init { private val settingsManager = SettingsManager.getInstance()
val prefsManager = SettingsManager.getInstance()
init {
// The SortMode isn't really that urgent, so throw it into a coroutine because I can
viewModelScope.launch { viewModelScope.launch {
mSortMode.value = withContext(Dispatchers.IO) { mSortMode.value = withContext(Dispatchers.IO) {
prefsManager.getLibrarySortMode() settingsManager.librarySortMode
} }
} }
} }
@ -133,13 +134,7 @@ class LibraryViewModel : ViewModel() {
if (mode != mSortMode.value) { if (mode != mSortMode.value) {
mSortMode.value = mode mSortMode.value = mode
viewModelScope.launch { settingsManager.librarySortMode = mode
withContext(Dispatchers.IO) {
val prefsManager = SettingsManager.getInstance()
prefsManager.setLibrarySortMode(mSortMode.value!!)
}
}
} }
} }
} }

View file

@ -0,0 +1,30 @@
package org.oxycblt.auxio.settings
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatDelegate
import org.oxycblt.auxio.R
/**
* Convert a string representing a theme entry name to an actual theme int that can be used.
* This is only done because PreferenceFragment does not like int arrays.
*/
fun String.toThemeInt(): Int {
return when (this) {
SettingsManager.EntryNames.THEME_AUTO -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
SettingsManager.EntryNames.THEME_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
SettingsManager.EntryNames.THEME_DARK -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
@DrawableRes
fun Int.toThemeIcon(): Int {
return when (this) {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night
else -> R.drawable.ic_auto
}
}

View file

@ -6,9 +6,10 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.activityViewModels
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.children
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -20,56 +21,13 @@ import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.getDetailedAccentSummary import org.oxycblt.auxio.ui.getDetailedAccentSummary
class SettingsListFragment : PreferenceFragmentCompat() { class SettingsListFragment : PreferenceFragmentCompat() {
private val settingsModel: SettingsViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
// --- PREFERENCE ITEM SETUP --- // --- PREFERENCE ITEM SETUP ---
val themePref = findPreference<Preference>(SettingsManager.Keys.KEY_THEME)?.apply { preferenceScreen.children.forEach {
setIcon( recursivelyHandleChildren(it)
when (AppCompatDelegate.getDefaultNightMode()) {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night
else -> R.drawable.ic_auto
}
)
}
val accentPref = findPreference<Preference>(SettingsManager.Keys.KEY_ACCENT)?.apply {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
showAccentDialog()
true
}
summary = getDetailedAccentSummary(requireActivity(), accent)
}
// --- VIEWMODEL SETUP ---
settingsModel.theme.observe(viewLifecycleOwner) {
if (it != null) {
themePref?.setIcon(
when (it) {
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> R.drawable.ic_auto
AppCompatDelegate.MODE_NIGHT_NO -> R.drawable.ic_day
AppCompatDelegate.MODE_NIGHT_YES -> R.drawable.ic_night
else -> R.drawable.ic_auto
}
)
settingsModel.doneWithThemeUpdate()
}
}
settingsModel.accent.observe(viewLifecycleOwner) {
if (it != null) {
accentPref?.summary = getDetailedAccentSummary(requireActivity(), it)
}
} }
Log.d(this::class.simpleName, "Fragment created.") Log.d(this::class.simpleName, "Fragment created.")
@ -79,6 +37,51 @@ class SettingsListFragment : PreferenceFragmentCompat() {
setPreferencesFromResource(R.xml.prefs_main, rootKey) setPreferencesFromResource(R.xml.prefs_main, rootKey)
} }
private fun recursivelyHandleChildren(pref: Preference) {
if (pref is PreferenceCategory) {
pref.children.forEach {
recursivelyHandleChildren(it)
}
} else {
handlePreference(pref)
}
}
private fun handlePreference(it: Preference) {
it.apply {
when (it.key) {
SettingsManager.Keys.KEY_THEME -> {
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, value ->
AppCompatDelegate.setDefaultNightMode((value as String).toThemeInt())
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
true
}
}
SettingsManager.Keys.KEY_ACCENT -> {
onPreferenceClickListener = Preference.OnPreferenceClickListener {
showAccentDialog()
true
}
summary = getDetailedAccentSummary(requireActivity(), accent)
}
SettingsManager.Keys.KEY_EDGE_TO_EDGE -> {
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
requireActivity().recreate()
true
}
}
}
}
}
private fun showAccentDialog() { private fun showAccentDialog() {
MaterialDialog(requireActivity()).show { MaterialDialog(requireActivity()).show {
title(R.string.setting_accent) title(R.string.setting_accent)
@ -89,7 +92,9 @@ class SettingsListFragment : PreferenceFragmentCompat() {
val recycler = RecyclerView(requireContext()).apply { val recycler = RecyclerView(requireContext()).apply {
adapter = AccentAdapter { adapter = AccentAdapter {
if (it.first != accent.first) { if (it.first != accent.first) {
SettingsManager.getInstance().setAccent(it) SettingsManager.getInstance().accent = it
requireActivity().recreate()
} }
this@show.dismiss() this@show.dismiss()

View file

@ -19,6 +19,51 @@ class SettingsManager private constructor(context: Context) :
sharedPrefs.registerOnSharedPreferenceChangeListener(this) sharedPrefs.registerOnSharedPreferenceChangeListener(this)
} }
val theme: Int
get() {
return sharedPrefs.getString(Keys.KEY_THEME, EntryNames.THEME_AUTO)?.toThemeInt()
?: AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
var accent: Pair<Int, Int>
get() {
val accentIndex = sharedPrefs.getInt(Keys.KEY_ACCENT, 5)
// Accent is stored as an index [to be efficient], so retrieve it when done.
return ACCENTS[accentIndex]
}
set(value) {
val accentIndex = ACCENTS.indexOf(value)
check(accentIndex != -1) { "Invalid accent" }
sharedPrefs.edit()
.putInt(Keys.KEY_ACCENT, accentIndex)
.apply()
}
val edgeEnabled: Boolean
get() {
return sharedPrefs.getBoolean(Keys.KEY_EDGE_TO_EDGE, false)
}
var librarySortMode: SortMode
get() {
return SortMode.fromInt(
sharedPrefs.getInt(
Keys.KEY_LIBRARY_SORT_MODE,
SortMode.CONSTANT_ALPHA_DOWN
)
) ?: SortMode.ALPHA_DOWN
}
set(value) {
sharedPrefs.edit()
.putInt(Keys.KEY_LIBRARY_SORT_MODE, value.toInt())
.apply()
}
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>() private val callbacks = mutableListOf<Callback>()
fun addCallback(callback: Callback) { fun addCallback(callback: Callback) {
@ -29,74 +74,9 @@ class SettingsManager private constructor(context: Context) :
callbacks.remove(callback) callbacks.remove(callback)
} }
fun getTheme(): Int {
// Turn the string from SharedPreferences into an actual theme value that can
// be used, as apparently the preference system provided by androidx doesn't like integers
// for some...reason.
return when (sharedPrefs.getString(Keys.KEY_THEME, Theme.THEME_AUTO)) {
Theme.THEME_AUTO -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
Theme.THEME_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
Theme.THEME_DARK -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
}
fun getAccent(): Pair<Int, Int> {
val accentIndex = sharedPrefs.getInt(Keys.KEY_ACCENT, 5)
return ACCENTS[accentIndex]
}
fun setAccent(accent: Pair<Int, Int>) {
val accentIndex = ACCENTS.indexOf(accent)
check(accentIndex != -1) { "Invalid accent" }
sharedPrefs.edit()
.putInt(Keys.KEY_ACCENT, accentIndex)
.apply()
callbacks.forEach {
it.onAccentUpdate(getAccent())
}
}
fun getEdgeToEdge(): Boolean {
return sharedPrefs.getBoolean(Keys.KEY_EDGE_TO_EDGE, false)
}
fun setLibrarySortMode(sortMode: SortMode) {
sharedPrefs.edit()
.putInt(Keys.KEY_LIBRARY_SORT_MODE, sortMode.toInt())
.apply()
}
fun getLibrarySortMode(): SortMode {
return SortMode.fromInt(
sharedPrefs.getInt(
Keys.KEY_LIBRARY_SORT_MODE,
SortMode.CONSTANT_ALPHA_DOWN
)
) ?: SortMode.ALPHA_DOWN
}
// --- OVERRIDES --- // --- OVERRIDES ---
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
Keys.KEY_THEME -> {
callbacks.forEach {
it.onThemeUpdate(getTheme())
}
}
Keys.KEY_EDGE_TO_EDGE -> {
callbacks.forEach {
it.onEdgeToEdgeUpdate(getEdgeToEdge())
}
}
}
} }
companion object { companion object {
@ -135,7 +115,7 @@ class SettingsManager private constructor(context: Context) :
const val KEY_EDGE_TO_EDGE = "KEY_EDGE" const val KEY_EDGE_TO_EDGE = "KEY_EDGE"
} }
private object Theme { object EntryNames {
const val THEME_AUTO = "AUTO" const val THEME_AUTO = "AUTO"
const val THEME_LIGHT = "LIGHT" const val THEME_LIGHT = "LIGHT"
const val THEME_DARK = "DARK" const val THEME_DARK = "DARK"
@ -145,9 +125,5 @@ class SettingsManager private constructor(context: Context) :
* An interface for receiving some settings updates. * An interface for receiving some settings updates.
* [SharedPreferences.OnSharedPreferenceChangeListener]. * [SharedPreferences.OnSharedPreferenceChangeListener].
*/ */
interface Callback { interface Callback
fun onThemeUpdate(newTheme: Int) {}
fun onAccentUpdate(newAccent: Pair<Int, Int>) {}
fun onEdgeToEdgeUpdate(isEdgeToEdge: Boolean) {}
}
} }

View file

@ -1,58 +0,0 @@
package org.oxycblt.auxio.settings
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
/**
* A [ViewModel] that provides a better interface for observing settings updates compared to
* [SettingsManager.Callback] or the default SharedPreferences listener.
* // TODO: Roll values & updates into this viewmodel
* @author OxygenCobalt
*/
class SettingsViewModel : ViewModel(), SettingsManager.Callback {
private val mTheme = MutableLiveData<Int?>()
val theme: LiveData<Int?> get() = mTheme
private val mAccent = MutableLiveData<Pair<Int, Int>?>()
val accent: LiveData<Pair<Int, Int>?> get() = mAccent
private val mEdge = MutableLiveData<Boolean?>()
val edge: LiveData<Boolean?> = mEdge
private val settingsManager = SettingsManager.getInstance()
init {
settingsManager.addCallback(this)
}
fun doneWithThemeUpdate() {
mTheme.value = null
}
fun doneWithAccentUpdate() {
mAccent.value = null
}
fun doneWithEdgeUpdate() {
mEdge.value = null
}
override fun onThemeUpdate(newTheme: Int) {
mTheme.value = newTheme
}
override fun onAccentUpdate(newAccent: Pair<Int, Int>) {
mAccent.value = newAccent
}
override fun onEdgeToEdgeUpdate(isEdgeToEdge: Boolean) {
mEdge.value = isEdgeToEdge
}
override fun onCleared() {
super.onCleared()
settingsManager.removeCallback(this)
}
}

View file

@ -27,6 +27,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/album_toolbar" android:id="@+id/album_toolbar"
style="@style/Toolbar.Style.Icon" style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_album_actions" app:menu="@menu/menu_album_actions"
app:title="@string/label_library" /> app:title="@string/label_library" />

View file

@ -27,6 +27,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/artist_toolbar" android:id="@+id/artist_toolbar"
style="@style/Toolbar.Style.Icon" style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_detail" app:menu="@menu/menu_detail"
app:title="@string/label_library" /> app:title="@string/label_library" />

View file

@ -27,6 +27,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/genre_toolbar" android:id="@+id/genre_toolbar"
style="@style/Toolbar.Style.Icon" style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_detail" app:menu="@menu/menu_detail"
app:title="@string/label_library" /> app:title="@string/label_library" />

View file

@ -14,6 +14,8 @@
android:id="@+id/library_toolbar" android:id="@+id/library_toolbar"
style="@style/Toolbar.Style" style="@style/Toolbar.Style"
android:theme="@style/Toolbar.Style.Search" android:theme="@style/Toolbar.Style.Search"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal"
app:menu="@menu/menu_library" app:menu="@menu/menu_library"
app:title="@string/label_library" /> app:title="@string/label_library" />

View file

@ -14,6 +14,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/queue_toolbar" android:id="@+id/queue_toolbar"
style="@style/Toolbar.Style.Icon" style="@style/Toolbar.Style.Icon"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:navigationIcon="@drawable/ic_down" app:navigationIcon="@drawable/ic_down"
app:title="@string/label_queue" /> app:title="@string/label_queue" />

View file

@ -10,6 +10,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/song_toolbar" android:id="@+id/song_toolbar"
style="@style/Toolbar.Style" style="@style/Toolbar.Style"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:title="@string/setting_title" /> app:title="@string/setting_title" />

View file

@ -13,6 +13,7 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/song_toolbar" android:id="@+id/song_toolbar"
style="@style/Toolbar.Style" style="@style/Toolbar.Style"
android:background="?android:attr/windowBackground"
android:elevation="@dimen/elevation_normal" android:elevation="@dimen/elevation_normal"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"

View file

@ -47,7 +47,7 @@
<item name="colorControlHighlight">@color/selection_color</item> <item name="colorControlHighlight">@color/selection_color</item>
</style> </style>
<!-- Toolbar Title Theme --> <!-- Toolbar Title EntryNames -->
<style name="TextAppearance.Toolbar.Header" parent="TextAppearance.Widget.AppCompat.Toolbar.Title"> <style name="TextAppearance.Toolbar.Header" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:fontFamily">@font/inter_black</item> <item name="android:fontFamily">@font/inter_black</item>
<item name="android:textColor">?attr/colorPrimary</item> <item name="android:textColor">?attr/colorPrimary</item>
@ -73,7 +73,7 @@
<item name="cornerRadius">0dp</item> <item name="cornerRadius">0dp</item>
</style> </style>
<!-- Custom Dialog Theme --> <!-- Custom Dialog EntryNames -->
<style name="Theme.CustomDialog" parent="Theme.MaterialComponents.DayNight.Dialog"> <style name="Theme.CustomDialog" parent="Theme.MaterialComponents.DayNight.Dialog">
<item name="colorBackgroundFloating">@color/background</item> <item name="colorBackgroundFloating">@color/background</item>
<item name="android:windowTitleStyle">@style/TextAppearance.Dialog.Title</item> <item name="android:windowTitleStyle">@style/TextAppearance.Dialog.Title</item>