Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tests)+refactor(new reviewer): move preferences to Prefs #17829

Merged
merged 1 commit into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ object UsageAnalytics {
"useInputTag", // Type answer into the card
"disableExtendedTextUi", // Disable Single-Field Edit Mode
"noteEditorNewlineReplace", // Replace newlines with HTML
"autoFocusTypeInAnswer", // Focus ‘type in answer’
PrefKey.AUTO_FOCUS_TYPE_ANSWER, // Focus ‘type in answer’
BrayanDSO marked this conversation as resolved.
Show resolved Hide resolved
"mediaImportAllowAllFiles", // Allow all files in media imports
"providerEnabled", // Enable AnkiDroid API
// ******************************** App bar buttons ****************************************
Expand Down
3 changes: 3 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/settings/PrefKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ object PrefKey {

// **************************************** Reviewer **************************************** //
const val FRAME_STYLE = "reviewerFrameStyle"
const val HIDE_SYSTEM_BARS = "hideSystemBars"
const val IGNORE_DISPLAY_CUTOUT = "ignoreDisplayCutout"
const val AUTO_FOCUS_TYPE_ANSWER = "autoFocusTypeInAnswer"

// ************************************** Accessibility ************************************* //
const val ANSWER_BUTTON_SIZE = "answerButtonSize"
Expand Down
19 changes: 13 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,49 @@ import androidx.core.content.edit
import com.ichi2.anki.AnkiDroidApp
import com.ichi2.anki.BuildConfig
import com.ichi2.anki.settings.enums.FrameStyle
import com.ichi2.anki.settings.enums.HideSystemBars
import com.ichi2.anki.settings.enums.PrefEnum
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

// TODO move this to `com.ichi2.anki.preferences`
// after the UI classes of that package are moved to `com.ichi2.anki.ui.preferences`
object Prefs {
private val prefs by lazy { AnkiDroidApp.sharedPrefs() }
private val sharedPrefs get() = AnkiDroidApp.sharedPrefs()

@VisibleForTesting
fun getBoolean(
key: String,
defValue: Boolean,
): Boolean = prefs.getBoolean(key, defValue)
): Boolean = sharedPrefs.getBoolean(key, defValue)

@VisibleForTesting
fun putBoolean(
key: String,
value: Boolean,
) {
prefs.edit { putBoolean(key, value) }
sharedPrefs.edit { putBoolean(key, value) }
}

@VisibleForTesting
fun getString(
key: String,
defValue: String?,
): String? = prefs.getString(key, defValue)
): String? = sharedPrefs.getString(key, defValue)

@VisibleForTesting
fun putString(
key: String,
value: String?,
) {
prefs.edit { putString(key, value) }
sharedPrefs.edit { putString(key, value) }
}

@VisibleForTesting
fun getInt(
key: String,
defValue: Int,
): Int = prefs.getInt(key, defValue)
): Int = sharedPrefs.getInt(key, defValue)

@VisibleForTesting
fun <E> getEnum(
Expand Down Expand Up @@ -129,9 +130,15 @@ object Prefs {

// **************************************** Reviewer **************************************** //

val ignoreDisplayCutout by booleanPref(PrefKey.IGNORE_DISPLAY_CUTOUT, false)
val autoFocusTypeAnswer by booleanPref(PrefKey.AUTO_FOCUS_TYPE_ANSWER, true)

val frameStyle: FrameStyle
get() = getEnum(PrefKey.FRAME_STYLE, FrameStyle.CARD)

val hideSystemBars: HideSystemBars
get() = getEnum(PrefKey.HIDE_SYSTEM_BARS, HideSystemBars.NONE)

// ************************************** Accessibility ************************************* //

val answerButtonsSize: Int
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2024 Brayan Oliveira <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki.settings.enums

enum class HideSystemBars(
override val entryValue: String,
) : PrefEnum {
NONE("0"),
STATUS_BAR("1"),
NAVIGATION_BAR("2"),
ALL("3"),
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import com.ichi2.anki.previewer.CardViewerActivity
import com.ichi2.anki.previewer.CardViewerFragment
import com.ichi2.anki.settings.Prefs
import com.ichi2.anki.settings.enums.FrameStyle
import com.ichi2.anki.settings.enums.HideSystemBars
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
import com.ichi2.anki.snackbar.SnackbarBuilder
import com.ichi2.anki.snackbar.showSnackbar
Expand Down Expand Up @@ -237,7 +238,7 @@ class ReviewerFragment :
}
}
}
val autoFocusTypeAnswer = sharedPrefs().getBoolean(getString(R.string.type_in_answer_focus_key), true)
val autoFocusTypeAnswer = Prefs.autoFocusTypeAnswer
viewModel.typeAnswerFlow.collectIn(lifecycleScope) { typeInAnswer ->
typeAnswerEditText.text = null
if (typeInAnswer == null) {
Expand Down Expand Up @@ -452,9 +453,8 @@ class ReviewerFragment :
}

private fun setupImmersiveMode(view: View) {
val hideSystemBarsSetting = HideSystemBars.from(requireContext())
val barsToHide =
when (hideSystemBarsSetting) {
when (Prefs.hideSystemBars) {
HideSystemBars.NONE -> return
HideSystemBars.STATUS_BAR -> WindowInsetsCompat.Type.statusBars()
HideSystemBars.NAVIGATION_BAR -> WindowInsetsCompat.Type.navigationBars()
Expand All @@ -467,7 +467,7 @@ class ReviewerFragment :
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}

val ignoreDisplayCutout = sharedPrefs().getBoolean(getString(R.string.ignore_display_cutout_key), false)
val ignoreDisplayCutout = Prefs.ignoreDisplayCutout
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val defaultTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
val typeMask =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.analytics.UsageAnalytics.preferencesWhoseChangesShouldBeReported
import com.ichi2.anki.preferences.PreferenceTestUtils
import com.ichi2.anki.preferences.SettingsFragment
import com.ichi2.anki.settings.PrefKey
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Test
Expand Down Expand Up @@ -84,8 +85,8 @@ class PreferencesAnalyticsTest : RobolectricTest() {
"hideAnswerButtons",
"hideHardAndEasy",
"reviewerFrameStyle",
"hideSystemBars",
"ignoreDisplayCutout",
PrefKey.HIDE_SYSTEM_BARS,
PrefKey.IGNORE_DISPLAY_CUTOUT,
)

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,17 @@ import com.ichi2.anki.RobolectricTest
import com.ichi2.anki.preferences.PreferenceTestUtils
import com.ichi2.anki.preferences.PreferenceTestUtils.getAttrsFromXml
import com.ichi2.anki.preferences.SettingsFragment
import com.ichi2.anki.settings.enums.PrefEnum
import com.ichi2.testutils.EmptyApplication
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyString
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.spy
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.whenever
import org.robolectric.annotation.Config
import kotlin.reflect.KVisibility
Expand All @@ -42,36 +41,24 @@ import kotlin.reflect.full.memberProperties
@RunWith(AndroidJUnit4::class)
@Config(application = EmptyApplication::class)
class PrefsRobolectricTest : RobolectricTest() {
@Before
fun setup() {
AnkiDroidApp.sharedPreferencesTestingOverride =
SPMockBuilder().createSharedPreferences()
}

private fun getKeysAndDefaultValues(): MutableMap<String, Any?> {
val settingsSpy = spy(Prefs)
val spy = spy(SPMockBuilder().createSharedPreferences())
AnkiDroidApp.sharedPreferencesTestingOverride = spy
val keysAndDefaultValues: MutableMap<String, Any?> = mutableMapOf()
var key = ""

doAnswer { invocation ->
key = invocation.arguments[0] as String
keysAndDefaultValues[key] = null
val key = invocation.arguments[0] as String
keysAndDefaultValues[key] = invocation.arguments[1]
invocation.callRealMethod()
}.run {
whenever(settingsSpy).getBoolean(anyString(), anyBoolean())
whenever(settingsSpy).getString(anyString(), anyString())
whenever(settingsSpy).getInt(anyString(), anyInt())
whenever(spy).getBoolean(anyString(), anyBoolean())
whenever(spy).getString(anyString(), anyOrNull())
whenever(spy).getInt(anyString(), anyInt())
}

for (property in Prefs::class.memberProperties) {
if (property.visibility != KVisibility.PUBLIC) continue
val defaultValue = property.getter.call(settingsSpy)
keysAndDefaultValues[key] =
if (defaultValue is PrefEnum) {
defaultValue.entryValue
} else {
defaultValue
}
property.getter.call(Prefs)
}
return keysAndDefaultValues
}
Expand Down
48 changes: 27 additions & 21 deletions AnkiDroid/src/test/java/com/ichi2/anki/settings/PrefsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,37 @@
*/
package com.ichi2.anki.settings

import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import com.github.ivanshafran.sharedpreferencesmock.SPMockBuilder
import com.ichi2.anki.AnkiDroidApp
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyString
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.spy
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.whenever
import kotlin.reflect.KMutableProperty
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties

class PrefsTest {
@BeforeEach
fun setup() {
AnkiDroidApp.sharedPreferencesTestingOverride =
SPMockBuilder().createSharedPreferences()
class PrefsTest : OnSharedPreferenceChangeListener {
private var lastSetKey: String? = null

override fun onSharedPreferenceChanged(
sharedPreferences: SharedPreferences?,
key: String?,
) {
lastSetKey = key
}

@Test
fun `booleanSetting getter and setter work`() {
AnkiDroidApp.sharedPreferencesTestingOverride = SPMockBuilder().createSharedPreferences()
var setting by Prefs.booleanPref("boolKey", false)
assertThat(setting, equalTo(false))

Expand All @@ -49,6 +55,7 @@ class PrefsTest {

@Test
fun `stringSetting getter and setter work`() {
AnkiDroidApp.sharedPreferencesTestingOverride = SPMockBuilder().createSharedPreferences()
var setting by Prefs.stringPref("stringKey", "defaultValue")
assertThat(setting, equalTo("defaultValue"))

Expand All @@ -58,34 +65,33 @@ class PrefsTest {

@Test
fun `getters and setters use the same key`() {
val settingsSpy = spy(Prefs)
var key = ""
val sharedPrefsSpy = spy(SPMockBuilder().createSharedPreferences())
AnkiDroidApp.sharedPreferencesTestingOverride = sharedPrefsSpy

var getterKey = ""
doAnswer { invocation ->
key = invocation.arguments[0] as String
getterKey = invocation.arguments[0] as String
invocation.callRealMethod()
}.run {
whenever(settingsSpy).getBoolean(anyString(), anyBoolean())
whenever(settingsSpy).putBoolean(anyString(), anyBoolean())
whenever(settingsSpy).getString(anyString(), anyString())
whenever(settingsSpy).putString(anyString(), anyString())
whenever(settingsSpy).getInt(anyString(), anyInt())
whenever(sharedPrefsSpy).getBoolean(anyString(), anyBoolean())
whenever(sharedPrefsSpy).getString(anyString(), anyOrNull())
whenever(sharedPrefsSpy).getInt(anyString(), anyInt())
}

sharedPrefsSpy.registerOnSharedPreferenceChangeListener(this)
for (property in Prefs::class.memberProperties) {
if (property.visibility != KVisibility.PUBLIC || property !is KMutableProperty<*>) continue

property.getter.call(settingsSpy)
val getterKey = key
property.getter.call(Prefs)

when (property.returnType.classifier) {
Boolean::class -> property.setter.call(settingsSpy, false)
String::class -> property.setter.call(settingsSpy, "foo")
Boolean::class -> property.setter.call(Prefs, false)
String::class -> property.setter.call(Prefs, "foo")
else -> continue
}
val setterKey = key

assertThat("The getter and setter of '${property.name}' use the same key", getterKey, equalTo(setterKey))
assertThat("The getter and setter of '${property.name}' use the same key", getterKey, equalTo(lastSetKey))
}
sharedPrefsSpy.unregisterOnSharedPreferenceChangeListener(this)
}
}
Loading