Skip to content

Commit

Permalink
Switch the auto advance feature to use the settings in the deck optio…
Browse files Browse the repository at this point in the history
…ns (#15224)

* Use auto advance options in the deck config

* Implement 'Wait for audio'

* Fix AutomaticAnswer tests

* Remove unused strings

* Fix config key in tests

* Fix config values in test fromPreferenceValue test

* Save config in tests

* Fix invalid u8

* More updates to tests

* Change translation key
  • Loading branch information
abdnh authored Jan 20, 2024
1 parent 00ee3b2 commit 9458ab0
Show file tree
Hide file tree
Showing 11 changed files with 74 additions and 145 deletions.
28 changes: 24 additions & 4 deletions AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,12 @@ abstract class AbstractFlashcardViewer :
}
}

private suspend fun automaticAnswerShouldWaitForAudio(): Boolean {
return withCol {
decks.confForDid(currentCard!!.did).optBoolean("waitForAudio", true)
}
}

internal inner class ReadTextListener : ReadText.ReadTextListener {
override fun onDone(playedSide: SoundSide?) {
Timber.d("done reading text")
Expand All @@ -1318,6 +1324,11 @@ abstract class AbstractFlashcardViewer :
}
val content = htmlGenerator!!.generateHtml(getColUnsafe, currentCard!!, Side.FRONT)
automaticAnswer.onDisplayQuestion()
launchCatchingTask {
if (!automaticAnswerShouldWaitForAudio()) {
automaticAnswer.scheduleAutomaticDisplayAnswer()
}
}
updateCard(content)
hideEaseButtons()
// If Card-based TTS is enabled, we "automatic display" after the TTS has finished as we don't know the duration
Expand Down Expand Up @@ -1359,6 +1370,11 @@ abstract class AbstractFlashcardViewer :
isSelecting = false
val answerContent = htmlGenerator!!.generateHtml(getColUnsafe, currentCard!!, Side.BACK)
automaticAnswer.onDisplayAnswer()
launchCatchingTask {
if (!automaticAnswerShouldWaitForAudio()) {
automaticAnswer.scheduleAutomaticDisplayQuestion()
}
}
updateCard(answerContent)
displayAnswerBottomBar()
}
Expand Down Expand Up @@ -1473,10 +1489,14 @@ abstract class AbstractFlashcardViewer :
*/
open fun onSoundGroupCompleted() {
Timber.v("onSoundGroupCompleted")
if (isDisplayingAnswer) {
automaticAnswer.scheduleAutomaticDisplayQuestion()
} else {
automaticAnswer.scheduleAutomaticDisplayAnswer()
launchCatchingTask {
if (automaticAnswerShouldWaitForAudio()) {
if (isDisplayingAnswer) {
automaticAnswer.scheduleAutomaticDisplayQuestion()
} else {
automaticAnswer.scheduleAutomaticDisplayAnswer()
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,6 @@ object UsageAnalytics {
"learnCutoff", // Learn ahead limit
"timeLimit", // Timebox time limit
"timeoutAnswer", // Automatic display answer
"automaticAnswerAction", // Timeout answer
"timeoutAnswerSeconds", // Time to show answer
"timeoutQuestionSeconds", // Time to show next question
"keepScreenOn", // Disable screen timeout
"newTimezoneHandling", // New timezone handling
"doubleTapTimeInterval", // Double tap time interval (milliseconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
*/
package com.ichi2.anki.preferences

import androidx.preference.ListPreference
import androidx.preference.SwitchPreferenceCompat
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.R
import com.ichi2.anki.launchCatchingTask
import com.ichi2.anki.preferences.Preferences.Companion.getDayOffset
import com.ichi2.anki.preferences.Preferences.Companion.setDayOffset
import com.ichi2.anki.reviewer.AutomaticAnswerAction
import com.ichi2.preferences.NumberRangePreferenceCompat
import com.ichi2.preferences.SliderPreference

Expand Down Expand Up @@ -63,20 +61,6 @@ class ReviewingSettingsFragment : SettingsFragment() {
}
}

/**
* Timeout answer
* An integer representing the action when "Automatic Answer" flips a card from answer to question
* 0 represents "bury", 1-4 represents the named buttons
* @see com.ichi2.anki.reviewer.AutomaticAnswerAction
* We use the same key in the collection config
* @see com.ichi2.anki.reviewer.AutomaticAnswerAction.CONFIG_KEY
* */
requirePreference<ListPreference>(R.string.automatic_answer_action_preference).apply {
launchCatchingTask { setValueIndex(withCol { config.get(AutomaticAnswerAction.CONFIG_KEY) ?: 0 }) }
setOnPreferenceChangeListener { newValue ->
launchCatchingTask { withCol { config.set(AutomaticAnswerAction.CONFIG_KEY, (newValue as String).toInt()) } }
}
}
// New timezone handling
requirePreference<SwitchPreferenceCompat>(R.string.new_timezone_handling_preference).apply {
launchCatchingTask {
Expand Down
87 changes: 33 additions & 54 deletions AnkiDroid/src/main/java/com/ichi2/anki/reviewer/AutomaticAnswer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ package com.ichi2.anki.reviewer
import android.content.SharedPreferences
import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting
import com.ichi2.anki.R
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.Reviewer
import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.reviewer.AnswerButtons.*
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.libanki.Collection
import com.ichi2.libanki.DeckConfig
import com.ichi2.libanki.DeckId
import com.ichi2.utils.HandlerUtils
import timber.log.Timber
Expand Down Expand Up @@ -260,52 +262,30 @@ class AutomaticAnswerSettings(

companion object {
/**
* Obtains the options for [AutomaticAnswer] in the deck config ("review" section)
* @return null if the deck is dynamic (use global settings),
* or if "useGeneralTimeoutSettings" is set
* Obtains the options for [AutomaticAnswer] in the deck config
*/
fun queryDeckSpecificOptions(
action: AutomaticAnswerAction,
fun queryOptions(
preferences: SharedPreferences,
col: Collection,
selectedDid: DeckId
): AutomaticAnswerSettings? {
// Dynamic don't have review options; attempt to get deck-specific auto-advance options
// but be prepared to go with all default if it's a dynamic deck
if (col.decks.isDyn(selectedDid)) {
return null
}

val revOptions = col.decks.confForDid(selectedDid).getJSONObject("rev")
): AutomaticAnswerSettings {
val conf = col.decks.confForDid(selectedDid)
val action = getAction(conf)
val useTimer = preferences.getBoolean("timeoutAnswer", false)
val waitQuestionSecond = conf.optInt("secondsToShowQuestion", 0)
val waitAnswerSecond = conf.optInt("secondsToShowAnswer", 0)

if (revOptions.optBoolean("useGeneralTimeoutSettings", true)) {
// we want to use the general settings, no need for per-deck settings
return null
}

val useTimer = revOptions.optBoolean("timeoutAnswer", false)
val waitQuestionSecond = revOptions.optInt("timeoutQuestionSeconds", 60)
val waitAnswerSecond = revOptions.optInt("timeoutAnswerSeconds", 20)
return AutomaticAnswerSettings(action, useTimer, waitQuestionSecond, waitAnswerSecond)
}

fun queryFromPreferences(preferences: SharedPreferences, action: AutomaticAnswerAction): AutomaticAnswerSettings {
val prefUseTimer: Boolean = preferences.getBoolean("timeoutAnswer", false)
val prefWaitQuestionSecond: Int = preferences.getInt("timeoutQuestionSeconds", 60)
val prefWaitAnswerSecond: Int = preferences.getInt("timeoutAnswerSeconds", 20)
return AutomaticAnswerSettings(action, prefUseTimer, prefWaitQuestionSecond, prefWaitAnswerSecond)
}

fun createInstance(preferences: SharedPreferences, col: Collection): AutomaticAnswerSettings {
// deck specific options take precedence over general (preference-based) options.
// the action can only be set via preferences (but is stored in the collection).
val action = getAction(col)
return queryDeckSpecificOptions(action, col, col.decks.selected()) ?: queryFromPreferences(preferences, action)
return queryOptions(preferences, col, col.decks.selected())
}

private fun getAction(col: Collection): AutomaticAnswerAction {
private fun getAction(conf: DeckConfig): AutomaticAnswerAction {
return try {
val value: Int = col.config.get(AutomaticAnswerAction.CONFIG_KEY) ?: return AutomaticAnswerAction.BURY_CARD
AutomaticAnswerAction.fromPreferenceValue(value)
val value: Int = conf.optInt(AutomaticAnswerAction.CONFIG_KEY)
AutomaticAnswerAction.fromConfigValue(value)
} catch (e: Exception) {
AutomaticAnswerAction.BURY_CARD
}
Expand All @@ -314,61 +294,60 @@ class AutomaticAnswerSettings(
}

/**
* Represents a value from [R.array.automatic_answer_values]/[R.array.automatic_answer_options]
* Represents a value from [anki.deck_config.DeckConfig.Config.AnswerAction]
* Executed when answering a card (showing the question).
*/
enum class AutomaticAnswerAction(private val preferenceValue: Int) {
enum class AutomaticAnswerAction(private val configValue: Int) {
/** Default: least invasive action */
BURY_CARD(0),
ANSWER_AGAIN(1),
ANSWER_HARD(2),
ANSWER_GOOD(3),
ANSWER_EASY(4);
ANSWER_GOOD(2),
ANSWER_HARD(3),
SHOW_REMINDER(4);

fun execute(reviewer: Reviewer) {
val numberOfButtons = 4
val actualAction = handleInvalidButtons(numberOfButtons)
val action = actualAction.toCommand(numberOfButtons)
Timber.i("Executing %s", action)
reviewer.executeCommand(action)
if (action != null) {
Timber.i("Executing %s", action)
reviewer.executeCommand(action)
} else {
reviewer.showSnackbar(TR.studyingAnswerTimeElapsed())
}
}

/** Handle **Hard/Easy** uf they don't appear */
private fun handleInvalidButtons(numberOfButtons: Int): AutomaticAnswerAction {
return when (this) {
ANSWER_HARD -> if (AnswerButtons.canAnswerHard(numberOfButtons)) ANSWER_HARD else ANSWER_GOOD
ANSWER_EASY -> if (AnswerButtons.canAnswerEasy(numberOfButtons)) ANSWER_EASY else ANSWER_GOOD
// Again and Good always appear. So does Bury
else -> this
}
}

/** Convert to a [ViewerCommand] */
private fun toCommand(numberOfButtons: Int): ViewerCommand {
private fun toCommand(numberOfButtons: Int): ViewerCommand? {
return when (this) {
BURY_CARD -> ViewerCommand.BURY_CARD
ANSWER_AGAIN -> AGAIN.toViewerCommand(numberOfButtons)
ANSWER_HARD -> HARD.toViewerCommand(numberOfButtons)
ANSWER_GOOD -> GOOD.toViewerCommand(numberOfButtons)
ANSWER_EASY -> EASY.toViewerCommand(numberOfButtons)
else -> null
}
}

companion object {
/**
* An integer representing the action when Automatic Answer flips a card from answer to question
*
* 0 represents "bury", 1-4 represents the named buttons
*
* Although AnkiMobile and AnkiDroid have the feature, this config key is currently AnkiDroid only
*
* @see AutomaticAnswerAction
*/
const val CONFIG_KEY = "automaticAnswerAction"
const val CONFIG_KEY = "answerAction"

/** convert from [R.array.automatic_answer_values] ([R.array.automatic_answer_options]) to the enum */
fun fromPreferenceValue(i: Int): AutomaticAnswerAction {
return values().firstOrNull { it.preferenceValue == i } ?: BURY_CARD
/** convert from [anki.deck_config.DeckConfig.Config.AnswerAction] to the enum */
fun fromConfigValue(i: Int): AutomaticAnswerAction {
return values().firstOrNull { it.configValue == i } ?: BURY_CARD
}
}
}
6 changes: 1 addition & 5 deletions AnkiDroid/src/main/res/values/10-preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- generic strings -->
<string name="disabled">Disabled</string>
<string name="pref_summary_seconds">%s s</string>
<plurals name="pref_summ_minutes">
<item quantity="one">%d min</item>
<item quantity="other">%d mins</item>
Expand Down Expand Up @@ -131,10 +130,7 @@
<string name="notification_minimum_cards_due_vibrate" maxLength="41">Vibrate</string>
<string name="notification_minimum_cards_due_blink" maxLength="41">Blink light</string>
<string name="timeout_answer_text" maxLength="41">Automatic display answer</string>
<string name="timeout_answer" maxLength="41">Timeout answer</string>
<string name="timeout_answer_summ">Show answer automatically without user input. Delay includes time for automatically played audio files.</string>
<string name="timeout_answer_seconds" maxLength="41">Time to show answer</string>
<string name="timeout_question_seconds" maxLength="41">Time to show next question</string>
<string name="timeout_answer_summ2">Show answer automatically without user input. Configure from deck options.</string>
<string name="select_locale_title">Select language</string>
<string name="software_render" maxLength="41">Disable card hardware render</string>
<string name="software_render_summ">Hardware render is faster but may have problems, specifically on Android 8/8.1. If you cannot see parts of the card review user interface, try this setting.</string>
Expand Down
2 changes: 0 additions & 2 deletions AnkiDroid/src/main/res/values/11-arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@
<string name="gesture_show_all_hints">Show All Hints</string>
<string name="record_voice">Record Voice</string>
<string name="replay_voice">Replay Voice</string>
<string name="automatic_answer_option_2">Hard (Good if unavailable)</string>
<string name="automatic_answer_option_4">Easy (Good if unavailable)</string>
<string name="save_voice">Save Recording</string>

</resources>
14 changes: 0 additions & 14 deletions AnkiDroid/src/main/res/values/constants.xml
Original file line number Diff line number Diff line change
Expand Up @@ -167,20 +167,6 @@
<item>0</item>
<item>1</item>
</string-array>
<string-array name="automatic_answer_options">
<item>@string/menu_bury_card</item>
<item>@string/ease_button_again</item>
<item>@string/automatic_answer_option_2</item>
<item>@string/ease_button_good</item>
<item>@string/automatic_answer_option_4</item>
</string-array>
<string-array name="automatic_answer_values">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</string-array>
<string-array name="full_screen_mode_values">
<item>0</item>
<item>1</item>
Expand Down
3 changes: 0 additions & 3 deletions AnkiDroid/src/main/res/values/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
<string name="learn_cutoff_preference">learnCutoff</string>
<string name="time_limit_preference">timeLimit</string>
<string name="timeout_answer_preference">timeoutAnswer</string>
<string name="automatic_answer_action_preference">automaticAnswerAction</string>
<string name="timeout_answer_seconds_preference">timeoutAnswerSeconds</string>
<string name="timeout_question_seconds_preference">timeoutQuestionSeconds</string>
<string name="keep_screen_on_preference">keepScreenOn</string>
<string name="new_timezone_handling_preference">newTimezoneHandling</string>
<string name="double_tap_time_interval_preference">doubleTapTimeInterval</string>
Expand Down
26 changes: 1 addition & 25 deletions AnkiDroid/src/main/res/xml/preferences_reviewing.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,8 @@
android:defaultValue="false"
android:disableDependentsState="false"
android:key="@string/timeout_answer_preference"
android:summary="@string/timeout_answer_summ"
android:summary="@string/timeout_answer_summ2"
android:title="@string/timeout_answer_text" />
<ListPreference
android:entries="@array/automatic_answer_options"
android:dependency="@string/timeout_answer_preference"
android:entryValues="@array/automatic_answer_values"
android:defaultValue="0"
android:key="@string/automatic_answer_action_preference"
android:title="@string/timeout_answer"
app1:useSimpleSummaryProvider="true"/>
<com.ichi2.preferences.SliderPreference
android:key="@string/timeout_answer_seconds_preference"
android:dependency="@string/timeout_answer_preference"
android:title="@string/timeout_answer_seconds"
android:defaultValue="20"
android:valueFrom="0"
android:valueTo="30"
app1:displayFormat="@string/pref_summary_seconds"/>
<com.ichi2.preferences.SliderPreference
android:key="@string/timeout_question_seconds_preference"
android:dependency="@string/timeout_answer_preference"
android:defaultValue="60"
android:valueFrom="0"
android:valueTo="60"
android:title="@string/timeout_question_seconds"
app1:displayFormat="@string/pref_summary_seconds"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_cat_advanced">
<SwitchPreferenceCompat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.ichi2.anki.reviewer
import com.ichi2.anki.Reviewer
import com.ichi2.anki.cardviewer.ViewerCommand
import com.ichi2.anki.reviewer.AutomaticAnswerAction.*
import com.ichi2.anki.reviewer.AutomaticAnswerAction.Companion.fromPreferenceValue
import com.ichi2.anki.reviewer.AutomaticAnswerAction.Companion.fromConfigValue
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
Expand All @@ -31,11 +31,11 @@ class AutomaticAnswerActionTest {

@Test
fun fromPreferenceValue() {
assertThat(fromPreferenceValue(0), equalTo(BURY_CARD))
assertThat(fromPreferenceValue(1), equalTo(ANSWER_AGAIN))
assertThat(fromPreferenceValue(2), equalTo(ANSWER_HARD))
assertThat(fromPreferenceValue(3), equalTo(ANSWER_GOOD))
assertThat(fromPreferenceValue(4), equalTo(ANSWER_EASY))
assertThat(fromConfigValue(0), equalTo(BURY_CARD))
assertThat(fromConfigValue(1), equalTo(ANSWER_AGAIN))
assertThat(fromConfigValue(2), equalTo(ANSWER_GOOD))
assertThat(fromConfigValue(3), equalTo(ANSWER_HARD))
assertThat(fromConfigValue(4), equalTo(SHOW_REMINDER))
}

@Test
Expand All @@ -45,7 +45,6 @@ class AutomaticAnswerActionTest {
assertExecuteReturns(ANSWER_AGAIN, ViewerCommand.FLIP_OR_ANSWER_EASE1)
assertExecuteReturns(ANSWER_HARD, ViewerCommand.FLIP_OR_ANSWER_EASE2)
assertExecuteReturns(ANSWER_GOOD, ViewerCommand.FLIP_OR_ANSWER_EASE3)
assertExecuteReturns(ANSWER_EASY, ViewerCommand.FLIP_OR_ANSWER_EASE4)
}

private fun assertExecuteReturns(action: AutomaticAnswerAction, expectedCommand: ViewerCommand) {
Expand Down
Loading

0 comments on commit 9458ab0

Please sign in to comment.