settings: improve preference management

Rework the preference classes to reduce the horrible bloat of the
recursivelyHandlePreference function.

This was mostly implementing new methods into IntListPreference and
adding a new preference to represent the weird, "generic" dialogs that
are used at points. While some preferences still need to be tweaked in
recursivelyHandlePreference, it is nowhere near as bad as it was prior.
This commit is contained in:
OxygenCobalt 2022-06-20 11:09:43 -06:00
parent 532a30325a
commit bd92ba2175
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 204 additions and 133 deletions

View file

@ -9,6 +9,8 @@
- Fixed broken tablet layouts
- Fixed seam that would appear on some album covers
- Fixed visual issue with the queue opening animation
- Fixed crash if settings was navigated away before playback state
finished saving
#### Dev/Meta
- Migrated preferences from shared object to utility

View file

@ -21,6 +21,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
@ -145,10 +146,12 @@ class HomeViewModel(application: Application) :
}
}
override fun onLibrarySettingsChanged() {
override fun onSettingChanged(key: String) {
if (key == application.getString(R.string.set_lib_tabs)) {
tabs = visibleTabs
_shouldRecreateTabs.value = true
}
}
override fun onCleared() {
super.onCleared()

View file

@ -26,6 +26,7 @@ import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.media.session.MediaButtonReceiver
import com.google.android.exoplayer2.Player
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
@ -163,9 +164,12 @@ class MediaSessionComponent(private val context: Context, private val player: Pl
// --- SETTINGSMANAGER CALLBACKS ---
override fun onCoverSettingsChanged() {
override fun onSettingChanged(key: String) {
if (key == context.getString(R.string.set_key_show_covers) ||
key == context.getString(R.string.set_key_show_covers)) {
updateMediaMetadata(playbackManager.song)
}
}
// --- EXOPLAYER CALLBACKS ---

View file

@ -46,6 +46,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
@ -289,23 +290,24 @@ class PlaybackService :
// --- SETTINGSMANAGER OVERRIDES ---
override fun onReplayGainSettingsChanged() {
onTracksInfoChanged(player.currentTracksInfo)
}
override fun onCoverSettingsChanged() {
override fun onSettingChanged(key: String) {
when (key) {
getString(R.string.set_replay_gain),
getString(R.string.set_pre_amp_with),
getString(R.string.set_pre_amp_without) -> onTracksInfoChanged(player.currentTracksInfo)
getString(R.string.set_show_covers),
getString(R.string.set_quality_covers) ->
playbackManager.song?.let { song ->
notificationComponent.updateMetadata(song, playbackManager.parent)
}
}
override fun onNotifSettingsChanged() {
getString(R.string.set_key_alt_notif_action) ->
if (settings.useAltNotifAction) {
onShuffledChanged(playbackManager.isShuffled)
} else {
onRepeatChanged(playbackManager.repeatMode)
}
}
}
// --- NOTIFICATION CALLBACKS ---

View file

@ -42,6 +42,12 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.requireAttached
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Shortcut delegate in order to receive a [Settings] that will be created/destroyed
* in each lifecycle.
*
* TODO: Replace with generalized method
*/
fun Fragment.settings(): ReadOnlyProperty<Fragment, Settings> =
object : ReadOnlyProperty<Fragment, Settings>, DefaultLifecycleObserver {
private var settings: Settings? = null
@ -71,10 +77,20 @@ fun Fragment.settings(): ReadOnlyProperty<Fragment, Settings> =
}
override fun onDestroy(owner: LifecycleOwner) {
settings?.release()
settings = null
}
}
/**
* Auxio's settings.
*
* This object wraps [SharedPreferences] in a type-safe manner, allowing access to all of the
* major settings that Auxio uses. Mutability is determined by use, as some values are written
* by PreferenceManager and others are written by Auxio's code.
*
* @author OxygenCobalt
*/
class Settings(private val context: Context, private val callback: Callback? = null) :
SharedPreferences.OnSharedPreferenceChangeListener {
private val inner = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
@ -90,18 +106,16 @@ class Settings(private val context: Context, private val callback: Callback? = n
}
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()
unlikelyToBeNull(callback).onSettingChanged(key)
}
/**
* 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 onSettingChanged(key: String)
}
// --- VALUES ---
@ -354,16 +368,4 @@ class Settings(private val context: Context, private val callback: Callback? = n
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() {}
}
}

View file

@ -33,15 +33,16 @@ import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.dirs.MusicDirsDialog
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
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.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.ui.accent.AccentCustomizeDialog
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.getSystemBarInsetsCompat
import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logEOrThrow
import org.oxycblt.auxio.util.showToast
/**
@ -82,22 +83,53 @@ class SettingsListFragment : PreferenceFragmentCompat() {
@Suppress("Deprecation")
override fun onDisplayPreferenceDialog(preference: Preference) {
if (preference is IntListPreference) {
when (preference) {
is IntListPreference -> {
// Creating our own preference dialog is hilariously difficult. For one, we need
// to override this random method within the class in order to launch the dialog in
// the first (because apparently you can't just implement some interface that
// automatically provides this behavior), then we also need to use a deprecated method
// to adequately supply a "target fragment" (otherwise we will crash since the dialog
// requires one), and then we need to actually show the dialog, making sure we use
// the parent FragmentManager as again, it will crash if we don't.
// automatically provides this behavior), then we also need to use a deprecated
// method to adequately supply a "target fragment" (otherwise we will crash since
// the dialog requires one), and then we need to actually show the dialog, making
// sure we use the parent FragmentManager as again, it will crash if we don't.
//
// Fragments were a mistake.
val dialog = IntListPreferenceDialog.from(preference)
dialog.setTargetFragment(this, 0)
dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG)
} else {
super.onDisplayPreferenceDialog(preference)
}
is WrappedDialogPreference ->
when (preference.key) {
getString(R.string.set_key_accent) ->
AccentCustomizeDialog()
.show(childFragmentManager, AccentCustomizeDialog.TAG)
getString(R.string.set_key_lib_tabs) ->
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
getString(R.string.set_key_pre_amp) ->
PreAmpCustomizeDialog()
.show(childFragmentManager, PreAmpCustomizeDialog.TAG)
getString(R.string.set_key_music_dirs) ->
MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG)
else -> logEOrThrow("Unexpected dialog key ${preference.key}")
}
else -> super.onDisplayPreferenceDialog(preference)
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState(requireContext()) {
context?.showToast(R.string.lbl_state_saved)
}
}
getString(R.string.set_key_reindex) -> {
playbackModel.savePlaybackState(requireContext()) { context?.hardRestart() }
}
else -> return super.onPreferenceTreeClick(preference)
}
return true
}
/** Recursively handle a preference, doing any specific actions on it. */
@ -113,28 +145,18 @@ class SettingsListFragment : PreferenceFragmentCompat() {
preference.apply {
when (key) {
getString(R.string.set_key_theme) -> {
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value ->
AppCompatDelegate.setDefaultNightMode(value as Int)
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())
true
}
}
getString(R.string.set_key_accent) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
AccentDialog().show(childFragmentManager, AccentDialog.TAG)
true
}
// TODO: Replace with preference impl
summary = context.getString(settings.accent.name)
}
getString(R.string.set_key_black_theme) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
if (requireContext().isNight) {
requireActivity().recreate()
}
@ -142,13 +164,6 @@ class SettingsListFragment : PreferenceFragmentCompat() {
true
}
}
getString(R.string.set_key_lib_tabs) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
TabCustomizeDialog().show(childFragmentManager, TabCustomizeDialog.TAG)
true
}
}
getString(R.string.set_key_show_covers),
getString(R.string.set_key_quality_covers) -> {
onPreferenceChangeListener =
@ -157,51 +172,6 @@ class SettingsListFragment : PreferenceFragmentCompat() {
true
}
}
getString(R.string.set_key_replay_gain) -> {
notifyDependencyChange(settings.replayGainMode == ReplayGainMode.OFF)
onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, value ->
notifyDependencyChange(
ReplayGainMode.fromIntCode(value as Int) == ReplayGainMode.OFF)
true
}
}
getString(R.string.set_key_pre_amp) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
PreAmpCustomizeDialog()
.show(childFragmentManager, PreAmpCustomizeDialog.TAG)
true
}
}
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)
}
true
}
}
getString(R.string.set_key_reindex) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
playbackModel.savePlaybackState(requireContext()) {
requireContext().hardRestart()
}
true
}
}
getString(R.string.set_key_music_dirs) -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG)
true
}
}
}
}
}

View file

@ -20,11 +20,16 @@ package org.oxycblt.auxio.settings.ui
import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet
import android.widget.ImageView
import androidx.core.content.res.getResourceIdOrThrow
import androidx.core.content.res.getTextArrayOrThrow
import androidx.preference.DialogPreference
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.lazyReflectedField
import org.oxycblt.auxio.util.logD
class IntListPreference
@JvmOverloads
@ -36,22 +41,39 @@ constructor(
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes) {
val entries: Array<CharSequence>
val values: IntArray
private var offValue: Int? = -1
private var icons: TypedArray? = null
private var currentValue: Int? = null
// Reflect into Preference to get the (normally inaccessible) default value.
private val defValue: Int
get() = PREFERENCE_DEFAULT_VALUE_FIELD.get(this) as Int
override fun onDependencyChanged(dependency: Preference, disableDependent: Boolean) {
super.onDependencyChanged(dependency, disableDependent)
logD("dependency changed: $dependency")
}
init {
val prefAttrs =
context.obtainStyledAttributes(
attrs, R.styleable.IntListPreference, defStyleAttr, defStyleRes)
entries = prefAttrs.getTextArray(R.styleable.IntListPreference_entries)
entries = prefAttrs.getTextArrayOrThrow(R.styleable.IntListPreference_entries)
values =
context.resources.getIntArray(
prefAttrs.getResourceId(R.styleable.IntListPreference_entryValues, -1))
prefAttrs.getResourceIdOrThrow(R.styleable.IntListPreference_entryValues))
val offValueId = prefAttrs.getResourceId(R.styleable.IntListPreference_offValue, -1)
if (offValueId > -1) {
offValue = context.resources.getInteger(offValueId)
}
val iconsId = prefAttrs.getResourceId(R.styleable.IntListPreference_entryIcons, -1)
if (iconsId > -1) {
icons = context.resources.obtainTypedArray(iconsId)
}
prefAttrs.recycle()
@ -71,6 +93,19 @@ constructor(
}
}
override fun shouldDisableDependents(): Boolean = currentValue == offValue
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val index = getValueIndex()
if (index > -1) {
val resourceId = icons?.getResourceId(index, -1) ?: -1
if (resourceId > -1) {
(holder.findViewById(android.R.id.icon) as ImageView).setImageResource(resourceId)
}
}
}
fun getValueIndex(): Int {
val curValue = currentValue
@ -91,6 +126,7 @@ constructor(
currentValue = value
callChangeListener(value)
notifyDependencyChange(shouldDisableDependents())
persistInt(value)
notifyChanged()
}

View file

@ -0,0 +1,35 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.ui
import android.content.Context
import android.util.AttributeSet
import androidx.preference.DialogPreference
/**
* Wraps [DialogPreference] as to make it type-distinct from other preferences while also
* making it possible to use in a PreferenceScreen.
*/
class WrappedDialogPreference
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = androidx.preference.R.attr.dialogPreferenceStyle,
defStyleRes: Int = 0
) : DialogPreference(context, attrs, defStyleAttr, defStyleRes)

View file

@ -33,7 +33,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* Dialog responsible for showing the list of accents to select.
* @author OxygenCobalt
*/
class AccentDialog : ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
private var accentAdapter = AccentAdapter(this)
private val settings: Settings by settings()

View file

@ -23,6 +23,7 @@ import android.os.Build
import coil.request.ImageRequest
import coil.size.Size
import coil.transform.RoundedCornersTransformation
import org.oxycblt.auxio.R
import org.oxycblt.auxio.image.BitmapProvider
import org.oxycblt.auxio.image.SquareFrameTransform
import org.oxycblt.auxio.music.MusicParent
@ -140,7 +141,12 @@ class WidgetComponent(private val context: Context) :
override fun onPlayingChanged(isPlaying: Boolean) = update()
override fun onShuffledChanged(isShuffled: Boolean) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onCoverSettingsChanged() = update()
override fun onSettingChanged(key: String) {
if (key == context.getString(R.string.set_key_show_covers) ||
key == context.getString(R.string.set_key_quality_covers)) {
update()
}
}
/*
* An immutable condensed variant of the current playback state, used so that PlaybackStateManager

View file

@ -8,5 +8,7 @@
<declare-styleable name="IntListPreference">
<attr name="entries" format="reference" />
<attr name="entryValues" format="reference" />
<attr name="entryIcons" format="reference" />
<attr name="offValue" format="reference" />
</declare-styleable>
</resources>

View file

@ -44,6 +44,12 @@
<item>@string/set_theme_night</item>
</string-array>
<array name="icons_theme">
<item>@drawable/ic_auto</item>
<item>@drawable/ic_light</item>
<item>@drawable/ic_dark</item>
</array>
<integer-array name="values_theme">
<item>@integer/theme_auto</item>
<item>@integer/theme_light</item>

View file

@ -12,9 +12,10 @@
app:iconSpaceReserved="false"
app:isPreferenceVisible="@bool/enable_theme_settings"
app:key="@string/set_key_theme"
app:entryIcons="@array/icons_theme"
app:title="@string/set_theme" />
<Preference
<org.oxycblt.auxio.settings.ui.WrappedDialogPreference
app:icon="@drawable/ic_accent"
app:key="@string/set_key_accent"
app:title="@string/set_accent" />
@ -33,7 +34,7 @@
app:layout="@layout/item_header"
app:title="@string/set_display">
<Preference
<org.oxycblt.auxio.settings.ui.WrappedDialogPreference
app:iconSpaceReserved="false"
app:key="@string/set_key_lib_tabs"
app:summary="@string/set_lib_tabs_desc"
@ -89,10 +90,11 @@
app:entries="@array/entries_replay_gain"
app:entryValues="@array/values_replay_gain"
app:iconSpaceReserved="false"
app:offValue="@integer/replay_gain_off"
app:key="@string/set_key_replay_gain"
app:title="@string/set_replay_gain" />
<Preference
<org.oxycblt.auxio.settings.ui.WrappedDialogPreference
app:allowDividerBelow="false"
app:dependency="@string/set_key_replay_gain"
app:iconSpaceReserved="false"
@ -156,7 +158,7 @@
app:summary="@string/set_reindex_desc"
app:title="@string/set_reindex" />
<Preference
<org.oxycblt.auxio.settings.ui.WrappedDialogPreference
app:iconSpaceReserved="false"
app:key="@string/set_key_music_dirs"
app:summary="@string/set_dirs_desc"