diff --git a/CHANGELOG.md b/CHANGELOG.md index 5911649e4..1000ca237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 b62e285d1..6d839d81d 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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,9 +146,11 @@ class HomeViewModel(application: Application) : } } - override fun onLibrarySettingsChanged() { - tabs = visibleTabs - _shouldRecreateTabs.value = true + override fun onSettingChanged(key: String) { + if (key == application.getString(R.string.set_lib_tabs)) { + tabs = visibleTabs + _shouldRecreateTabs.value = true + } } override fun onCleared() { 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 e0752ffb8..ac86b2c2b 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 @@ -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,8 +164,11 @@ class MediaSessionComponent(private val context: Context, private val player: Pl // --- SETTINGSMANAGER CALLBACKS --- - override fun onCoverSettingsChanged() { - updateMediaMetadata(playbackManager.song) + 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 --- 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 ac4b61a95..0b4c729a9 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 @@ -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,21 +290,22 @@ class PlaybackService : // --- SETTINGSMANAGER OVERRIDES --- - override fun onReplayGainSettingsChanged() { - onTracksInfoChanged(player.currentTracksInfo) - } - - override fun onCoverSettingsChanged() { - playbackManager.song?.let { song -> - notificationComponent.updateMetadata(song, playbackManager.parent) - } - } - - override fun onNotifSettingsChanged() { - if (settings.useAltNotifAction) { - onShuffledChanged(playbackManager.isShuffled) - } else { - onRepeatChanged(playbackManager.repeatMode) + 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) + } + getString(R.string.set_key_alt_notif_action) -> + if (settings.useAltNotifAction) { + onShuffledChanged(playbackManager.isShuffled) + } else { + onRepeatChanged(playbackManager.repeatMode) + } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt index 59d258602..53643a2f8 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/Settings.kt @@ -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 = object : ReadOnlyProperty, DefaultLifecycleObserver { private var settings: Settings? = null @@ -71,10 +77,20 @@ fun Fragment.settings(): ReadOnlyProperty = } 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() {} - } } 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 1e31e4f6d..815cb7009 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -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,24 +83,55 @@ class SettingsListFragment : PreferenceFragmentCompat() { @Suppress("Deprecation") override fun onDisplayPreferenceDialog(preference: Preference) { - if (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. - // - // Fragments were a mistake. - val dialog = IntListPreferenceDialog.from(preference) - dialog.setTargetFragment(this, 0) - dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) - } else { - super.onDisplayPreferenceDialog(preference) + 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. + // + // Fragments were a mistake. + val dialog = IntListPreferenceDialog.from(preference) + dialog.setTargetFragment(this, 0) + dialog.show(parentFragmentManager, IntListPreferenceDialog.TAG) + } + 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. */ private fun recursivelyHandlePreference(preference: Preference) { if (!preference.isVisible) return @@ -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 - } - } } } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt index d96461ad4..85c54ff7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt @@ -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 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() } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt new file mode 100644 index 000000000..627a0fa2a --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/WrappedDialogPreference.kt @@ -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 . + */ + +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) diff --git a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt rename to app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt index 2802153cb..ed1447646 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/accent/AccentCustomizeDialog.kt @@ -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(), AccentAdapter.Listener { +class AccentCustomizeDialog : + ViewBindingDialogFragment(), AccentAdapter.Listener { private var accentAdapter = AccentAdapter(this) private val settings: Settings by settings() 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 54048527b..699e28e87 100644 --- a/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/widgets/WidgetComponent.kt @@ -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 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 33611e92d..750140147 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -8,5 +8,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 542db3ec1..66698961f 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -44,6 +44,12 @@ @string/set_theme_night + + @drawable/ic_auto + @drawable/ic_light + @drawable/ic_dark + + @integer/theme_auto @integer/theme_light diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index 7477e03d3..f236c40f1 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -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" /> - @@ -33,7 +34,7 @@ app:layout="@layout/item_header" app:title="@string/set_display"> - - -