diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt index 9ef8a42ef0bb..54f6465b41af 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/HeaderFragment.kt @@ -18,7 +18,9 @@ package com.ichi2.anki.preferences import android.os.Bundle import android.view.View import android.view.ViewGroup +import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.bytehamster.lib.preferencesearch.SearchConfiguration @@ -34,19 +36,14 @@ import com.ichi2.preferences.HeaderPreference import com.ichi2.utils.AdaptionUtil class HeaderFragment : PreferenceFragmentCompat(), TitleProvider { - private var selectedHeaderPreference: HeaderPreference? = null - private var selectedHeaderPreferenceKey: String = DEFAULT_SELECTED_HEADER - override val title: CharSequence get() = getString(R.string.settings) + private var highlightedPreferenceKey: String = "" + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preference_headers, rootKey) - selectedHeaderPreferenceKey = savedInstanceState?.getString(KEY_SELECTED_HEADER_PREF) ?: DEFAULT_SELECTED_HEADER - - highlightHeaderPreference(requirePreference(selectedHeaderPreferenceKey)) - requirePreference(R.string.pref_backup_limits_screen_key) .title = CollectionManager.TR.preferencesBackups() @@ -63,33 +60,28 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider { requireActivity() as AppCompatActivity, requirePreference(R.string.search_preference_key).searchConfiguration ) - } - private fun highlightHeaderPreference(headerPreference: HeaderPreference) { - if (resources.isWindowCompact()) { - return - } - selectedHeaderPreference?.setHighlighted(false) - // highlight the newly selected header - selectedHeaderPreference = headerPreference.apply { - setHighlighted(true) - selectedHeaderPreferenceKey = this.key - } - } + if (!resources.isWindowCompact()) { + parentFragmentManager.findFragmentById(R.id.settings_container)?.let { + val key = getHeaderKeyForFragment(it) ?: return@let + highlightPreference(key) + } - override fun onPreferenceTreeClick(preference: Preference): Boolean { - highlightHeaderPreference(preference as HeaderPreference) - return super.onPreferenceTreeClick(preference) - } + parentFragmentManager.addOnBackStackChangedListener { + val fragment = parentFragmentManager.findFragmentById(R.id.settings_container) + ?: return@addOnBackStackChangedListener - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putString(KEY_SELECTED_HEADER_PREF, selectedHeaderPreferenceKey) + val key = getHeaderKeyForFragment(fragment) ?: return@addOnBackStackChangedListener + highlightPreference(key) + } + } } - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - highlightHeaderPreference(requirePreference(selectedHeaderPreferenceKey)) + private fun highlightPreference(@StringRes keyRes: Int) { + val key = getString(keyRes) + findPreference(highlightedPreferenceKey)?.setHighlighted(false) + findPreference(key)?.setHighlighted(true) + highlightedPreferenceKey = key } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -101,9 +93,6 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider { } companion object { - private const val KEY_SELECTED_HEADER_PREF = "selected_header_pref" - private const val DEFAULT_SELECTED_HEADER = "generalScreen" - fun configureSearchBar(activity: AppCompatActivity, searchConfiguration: SearchConfiguration) { val setDuePreferenceTitle = TR.actionsSetDueDate().toSentenceCase(activity, R.string.sentence_set_due_date) with(searchConfiguration) { @@ -163,5 +152,29 @@ class HeaderFragment : PreferenceFragmentCompat(), TitleProvider { searchConfiguration.ignorePreference(activity.getString(R.string.user_actions_controls_category_key)) } + + /** + * @return the key for the [HeaderPreference] that corresponds to the given [fragment] + * in the Preference tree. + * + * e.g. Sync > Custom sync server settings -> returns the key for the Sync header + */ + @StringRes + fun getHeaderKeyForFragment(fragment: Fragment): Int? { + return when (fragment) { + is GeneralSettingsFragment -> R.string.pref_general_screen_key + is ReviewingSettingsFragment -> R.string.pref_reviewing_screen_key + is SyncSettingsFragment, is CustomSyncServerSettingsFragment -> R.string.pref_sync_screen_key + is NotificationsSettingsFragment -> R.string.pref_notifications_screen_key + is AppearanceSettingsFragment, is CustomButtonsSettingsFragment -> R.string.pref_appearance_screen_key + is ControlsSettingsFragment -> R.string.pref_controls_screen_key + is AccessibilitySettingsFragment -> R.string.pref_accessibility_screen_key + is BackupLimitsSettingsFragment -> R.string.pref_backup_limits_screen_key + is AdvancedSettingsFragment -> R.string.pref_advanced_screen_key + is DevOptionsFragment, is ReviewerOptionsFragment -> R.string.pref_dev_options_screen_key + is AboutFragment -> R.string.about_screen_key + else -> null + } + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/preferences/HeaderPreference.kt b/AnkiDroid/src/main/java/com/ichi2/preferences/HeaderPreference.kt index b997acf748df..3df95ea7d5a4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/preferences/HeaderPreference.kt +++ b/AnkiDroid/src/main/java/com/ichi2/preferences/HeaderPreference.kt @@ -17,10 +17,10 @@ package com.ichi2.preferences import android.content.Context import android.util.AttributeSet +import androidx.appcompat.widget.ThemeUtils import androidx.core.content.withStyledAttributes import androidx.preference.Preference import androidx.preference.PreferenceViewHolder -import com.google.android.material.color.MaterialColors import com.ichi2.anki.LanguageUtils import com.ichi2.anki.R @@ -35,8 +35,8 @@ constructor( defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, defStyleRes: Int = androidx.preference.R.style.Preference ) : Preference(context, attrs, defStyleAttr, defStyleRes) { + private var isHighlighted = false - private val highlightColor: Int = MaterialColors.getColor(context, R.attr.currentDeckBackgroundColor, 0) init { context.withStyledAttributes(attrs, R.styleable.HeaderPreference) { @@ -50,7 +50,8 @@ constructor( override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) if (isHighlighted) { - holder.itemView.setBackgroundColor(highlightColor) + val color = ThemeUtils.getThemeAttrColor(context, R.attr.currentDeckBackgroundColor) + holder.itemView.setBackgroundColor(color) } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt index f422f1d79889..85d29a5a3251 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferenceTestUtils.kt @@ -24,7 +24,7 @@ import com.ichi2.utils.getInstanceFromClassName import org.xmlpull.v1.XmlPullParser object PreferenceTestUtils { - private fun getAttrFromXml(context: Context, @XmlRes xml: Int, attrName: String, namespace: String = AnkiDroidApp.ANDROID_NAMESPACE): List { + fun getAttrFromXml(context: Context, @XmlRes xml: Int, attrName: String, namespace: String = AnkiDroidApp.ANDROID_NAMESPACE): List { val occurrences = mutableListOf() val xrp = context.resources.getXml(xml).apply { @@ -44,6 +44,29 @@ object PreferenceTestUtils { return occurrences.toList() } + fun getAttrsFromXml( + context: Context, + @XmlRes xml: Int, + attrNames: List, + namespace: String = AnkiDroidApp.ANDROID_NAMESPACE + ): List> { + val occurrences = mutableListOf>() + + val xrp = context.resources.getXml(xml).apply { + setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true) + setFeature(XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES, true) + } + + while (xrp.eventType != XmlPullParser.END_DOCUMENT) { + if (xrp.eventType == XmlPullParser.START_TAG) { + val attrValues = attrNames.associateWith { xrp.getAttributeValue(namespace, it) } + occurrences.add(attrValues) + } + xrp.next() + } + return occurrences.toList() + } + /** @return fragments found on [xml] */ private fun getFragmentsFromXml(context: Context, @XmlRes xml: Int): List { return getAttrFromXml(context, xml, "fragment").map { getInstanceFromClassName(it) } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferencesTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferencesTest.kt index c27a41dd32f5..2879e1b4ea0a 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferencesTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/preferences/PreferencesTest.kt @@ -17,14 +17,18 @@ package com.ichi2.anki.preferences import android.content.Context import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment import androidx.fragment.app.commitNow import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.R import com.ichi2.anki.RobolectricTest +import com.ichi2.anki.preferences.HeaderFragment.Companion.getHeaderKeyForFragment +import com.ichi2.anki.preferences.PreferenceTestUtils.getAttrFromXml import com.ichi2.libanki.exception.ConfirmModSchemaException import com.ichi2.preferences.HeaderPreference import com.ichi2.testutils.getJavaMethodAsAccessible +import com.ichi2.utils.getInstanceFromClassName import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.junit.Before @@ -32,6 +36,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import kotlin.reflect.jvm.jvmName +import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class PreferencesTest : RobolectricTest() { @@ -98,6 +104,36 @@ class PreferencesTest : RobolectricTest() { } } + @Test + fun `All preferences fragments highlight the correct header`() { + val headers = PreferenceTestUtils.getAttrsFromXml( + targetContext, + R.xml.preference_headers, + listOf("key", "fragment") + ).filter { it["fragment"] != null } + + assertTrue(headers.all { it["key"] != null }) + + fun assertThatFragmentLeadsToHeaderKey(fragmentClass: String, parentFragmentClass: String? = null) { + val fragment = getInstanceFromClassName(fragmentClass) + val headerFragmentClass = parentFragmentClass ?: fragmentClass + val expectedKey = headers.first { it["fragment"] == headerFragmentClass }["key"]!!.removePrefix("@").toInt() + val key = getHeaderKeyForFragment(fragment) + assertEquals(expectedKey, key) + + if (fragment is SettingsFragment) { + val subFragments = getAttrFromXml(targetContext, fragment.preferenceResource, "fragment") + for (subFragment in subFragments) { + assertThatFragmentLeadsToHeaderKey(subFragment, headerFragmentClass) + } + } + } + + for (header in headers) { + assertThatFragmentLeadsToHeaderKey(header["fragment"]!!) + } + } + @Test @Config(qualifiers = "ar") fun buildHeaderSummary_RTL_Test() {