Skip to content

Commit a390cfb

Browse files
committed
feat(reminders): threshold filters
GSoC 2025: Review Reminders Add a group of new advanced review reminder options: count new cards, count cards in learning, and count cards in review. When the review reminder is about to send a notification and checks to see if the amount of cards in the deck is greater than the card trigger threshold, it examines these options to check if it should count and consider new cards, cards in learning, and cards in review. Adds three new checkboxes to the AddEditReminderDialog to toggle these booleans on or off. Edits some logic in NotificationService to add up cards only from selected card type when determining whether the card trigger threshold is met. Adds three new boolean fields to store the states of these settings to ReviewReminder. Adds unit tests.
1 parent 2d3626e commit a390cfb

File tree

6 files changed

+361
-4
lines changed

6 files changed

+361
-4
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/AddEditReminderDialog.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ import androidx.fragment.app.DialogFragment
3636
import androidx.fragment.app.setFragmentResult
3737
import androidx.fragment.app.setFragmentResultListener
3838
import androidx.fragment.app.viewModels
39+
import androidx.lifecycle.LiveData
3940
import androidx.lifecycle.viewmodel.initializer
4041
import androidx.lifecycle.viewmodel.viewModelFactory
4142
import com.google.android.material.button.MaterialButton
43+
import com.google.android.material.checkbox.MaterialCheckBox
4244
import com.google.android.material.timepicker.MaterialTimePicker
4345
import com.google.android.material.timepicker.TimeFormat
4446
import com.ichi2.anki.DeckSpinnerSelection
@@ -99,6 +101,9 @@ class AddEditReminderDialog : DialogFragment() {
99101
is ReviewReminderScope.DeckSpecific -> mode.schedulerScope.did
100102
},
101103
initialCardTriggerThreshold = INITIAL_CARD_THRESHOLD,
104+
initialCountNew = INITIAL_COUNT_NEW,
105+
initialCountLrn = INITIAL_COUNT_LRN,
106+
initialCountRev = INITIAL_COUNT_REV,
102107
initialAdvancedSettingsOpen = INITIAL_ADVANCED_SETTINGS_OPEN,
103108
)
104109
is DialogMode.Edit ->
@@ -110,6 +115,9 @@ class AddEditReminderDialog : DialogFragment() {
110115
is ReviewReminderScope.DeckSpecific -> mode.reminderToBeEdited.scope.did
111116
},
112117
initialCardTriggerThreshold = mode.reminderToBeEdited.cardTriggerThreshold.threshold,
118+
initialCountNew = mode.reminderToBeEdited.countNew,
119+
initialCountLrn = mode.reminderToBeEdited.countLrn,
120+
initialCountRev = mode.reminderToBeEdited.countRev,
113121
initialAdvancedSettingsOpen = INITIAL_ADVANCED_SETTINGS_OPEN,
114122
)
115123
}
@@ -165,6 +173,7 @@ class AddEditReminderDialog : DialogFragment() {
165173
setUpDeckSpinner()
166174
setUpAdvancedDropdown()
167175
setUpCardThresholdInput()
176+
setUpCountCheckboxes()
168177

169178
// For getting the result of the deck selection sub-dialog from ScheduleReminders
170179
// See ScheduleReminders.onDeckSelected for more information
@@ -257,6 +266,54 @@ class AddEditReminderDialog : DialogFragment() {
257266
}
258267
}
259268

269+
/**
270+
* Convenience data class for setting up the checkboxes for whether to count new, learning, and review cards
271+
* when considering the card trigger threshold.
272+
* @see setUpCountCheckboxes
273+
*/
274+
private data class CountViewsAndActions(
275+
val section: LinearLayout,
276+
val checkbox: MaterialCheckBox,
277+
val actionOnClick: () -> Unit,
278+
val state: LiveData<Boolean>,
279+
)
280+
281+
/**
282+
* Sets up the checkboxes for whether to count new, learning, and review cards when considering the card trigger threshold.
283+
* @see CountViewsAndActions
284+
*/
285+
private fun setUpCountCheckboxes() {
286+
val countViewsAndActionsItems =
287+
listOf(
288+
CountViewsAndActions(
289+
section = contentView.findViewById(R.id.add_edit_reminder_count_new_section),
290+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_new_checkbox),
291+
actionOnClick = viewModel::toggleCountNew,
292+
state = viewModel.countNew,
293+
),
294+
CountViewsAndActions(
295+
section = contentView.findViewById(R.id.add_edit_reminder_count_lrn_section),
296+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_lrn_checkbox),
297+
actionOnClick = viewModel::toggleCountLrn,
298+
state = viewModel.countLrn,
299+
),
300+
CountViewsAndActions(
301+
section = contentView.findViewById(R.id.add_edit_reminder_count_rev_section),
302+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_rev_checkbox),
303+
actionOnClick = viewModel::toggleCountRev,
304+
state = viewModel.countRev,
305+
),
306+
)
307+
308+
countViewsAndActionsItems.forEach { item ->
309+
item.section.setOnClickListener { item.actionOnClick() }
310+
item.checkbox.setOnClickListener { item.actionOnClick() }
311+
item.state.observe(this) { value ->
312+
item.checkbox.isChecked = value
313+
}
314+
}
315+
}
316+
260317
/**
261318
* Show the time picker dialog for selecting a time with a given hour and minute.
262319
* Does not automatically dismiss the old dialog.
@@ -334,6 +391,9 @@ class AddEditReminderDialog : DialogFragment() {
334391
is DialogMode.Add -> true
335392
is DialogMode.Edit -> mode.reminderToBeEdited.enabled
336393
},
394+
countNew = viewModel.countNew.value ?: INITIAL_COUNT_NEW,
395+
countLrn = viewModel.countLrn.value ?: INITIAL_COUNT_LRN,
396+
countRev = viewModel.countRev.value ?: INITIAL_COUNT_REV,
337397
)
338398

339399
Timber.d("Reminder to be returned: %s", reminderToBeReturned)
@@ -398,6 +458,27 @@ class AddEditReminderDialog : DialogFragment() {
398458
*/
399459
private const val INITIAL_CARD_THRESHOLD: Int = 1
400460

461+
/**
462+
* The default setting for whether new cards are counted when checking the card trigger threshold.
463+
* Defaults to true, as removing some card types from card trigger threshold consideration is a form
464+
* of advanced review reminder customization.
465+
*/
466+
private const val INITIAL_COUNT_NEW = true
467+
468+
/**
469+
* The default setting for whether cards in learning are counted when checking the card trigger threshold.
470+
* Defaults to true, as removing some card types from card trigger threshold consideration is a form
471+
* of advanced review reminder customization.
472+
*/
473+
private const val INITIAL_COUNT_LRN = true
474+
475+
/**
476+
* The default setting for whether cards in review are counted when checking the card trigger threshold.
477+
* Defaults to true, as removing some card types from card trigger threshold consideration is a form
478+
* of advanced review reminder customization.
479+
*/
480+
private const val INITIAL_COUNT_REV = true
481+
401482
/**
402483
* Whether the advanced settings dropdown is initially open.
403484
* We start with it closed to avoid overwhelming the user.

AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/AddEditReminderDialogViewModel.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ class AddEditReminderDialogViewModel(
3333
initialDeckSelected: DeckId,
3434
initialCardTriggerThreshold: Int,
3535
initialAdvancedSettingsOpen: Boolean,
36+
initialCountNew: Boolean,
37+
initialCountLrn: Boolean,
38+
initialCountRev: Boolean,
3639
) : ViewModel() {
3740
private val _time = MutableLiveData(initialTime)
3841
val time: LiveData<ReviewReminderTime> = _time
@@ -48,6 +51,15 @@ class AddEditReminderDialogViewModel(
4851
private val _cardTriggerThreshold = MutableLiveData(initialCardTriggerThreshold)
4952
val cardTriggerThreshold: LiveData<Int> = _cardTriggerThreshold
5053

54+
private val _countNew = MutableLiveData(initialCountNew)
55+
val countNew: LiveData<Boolean> = _countNew
56+
57+
private val _countLrn = MutableLiveData(initialCountLrn)
58+
val countLrn: LiveData<Boolean> = _countLrn
59+
60+
private val _countRev = MutableLiveData(initialCountRev)
61+
val countRev: LiveData<Boolean> = _countRev
62+
5163
private val _advancedSettingsOpen = MutableLiveData(initialAdvancedSettingsOpen)
5264
val advancedSettingsOpen: LiveData<Boolean> = _advancedSettingsOpen
5365

@@ -66,6 +78,21 @@ class AddEditReminderDialogViewModel(
6678
_cardTriggerThreshold.value = threshold
6779
}
6880

81+
fun toggleCountNew() {
82+
Timber.d("Toggled count new from %s", _countNew.value)
83+
_countNew.value = !(_countNew.value ?: false)
84+
}
85+
86+
fun toggleCountLrn() {
87+
Timber.d("Toggled count lrn from %s", _countLrn.value)
88+
_countLrn.value = !(_countLrn.value ?: false)
89+
}
90+
91+
fun toggleCountRev() {
92+
Timber.d("Toggled count rev from %s", _countRev.value)
93+
_countRev.value = !(_countRev.value ?: false)
94+
}
95+
6996
fun toggleAdvancedSettingsOpen() {
7097
Timber.d("Toggled advanced settings open from %s", _advancedSettingsOpen.value)
7198
_advancedSettingsOpen.value = !(_advancedSettingsOpen.value ?: false)

AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,14 @@ sealed class ReviewReminderScope : Parcelable {
162162
* Preferably, also add some unit tests to ensure your migration works properly on all user devices once your update is rolled out.
163163
* See ReviewRemindersDatabaseTest for examples on how to do this.
164164
*
165-
* TODO: add remaining fields planned for GSoC 2025.
166-
*
167165
* @param id Unique, auto-incremented ID of the review reminder.
168166
* @param time See [ReviewReminderTime].
169167
* @param cardTriggerThreshold See [ReviewReminderCardTriggerThreshold].
170168
* @param scope See [ReviewReminderScope].
171169
* @param enabled Whether the review reminder's notifications are active or disabled.
170+
* @param countNew Whether new cards are counted when checking the [cardTriggerThreshold].
171+
* @param countLrn Whether learning cards are counted when checking the [cardTriggerThreshold].
172+
* @param countRev Whether review cards are counted when checking the [cardTriggerThreshold].
172173
*/
173174
@Serializable
174175
@Parcelize
@@ -179,6 +180,9 @@ data class ReviewReminder private constructor(
179180
val cardTriggerThreshold: ReviewReminderCardTriggerThreshold,
180181
val scope: ReviewReminderScope,
181182
var enabled: Boolean,
183+
val countNew: Boolean,
184+
val countLrn: Boolean,
185+
val countRev: Boolean,
182186
) : Parcelable,
183187
ReviewReminderSchema {
184188
companion object {
@@ -192,12 +196,18 @@ data class ReviewReminder private constructor(
192196
cardTriggerThreshold: ReviewReminderCardTriggerThreshold,
193197
scope: ReviewReminderScope = ReviewReminderScope.Global,
194198
enabled: Boolean = true,
199+
countNew: Boolean = true,
200+
countLrn: Boolean = true,
201+
countRev: Boolean = true,
195202
) = ReviewReminder(
196203
id = ReviewReminderId.getAndIncrementNextFreeReminderId(),
197204
time,
198205
cardTriggerThreshold,
199206
scope,
200207
enabled,
208+
countNew,
209+
countLrn,
210+
countRev,
201211
)
202212
}
203213

AnkiDroid/src/main/java/com/ichi2/anki/services/NotificationService.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import com.ichi2.anki.IntentHandler
3131
import com.ichi2.anki.R
3232
import com.ichi2.anki.common.annotations.LegacyNotifications
3333
import com.ichi2.anki.libanki.Decks
34+
import com.ichi2.anki.libanki.sched.Counts
3435
import com.ichi2.anki.preferences.PENDING_NOTIFICATIONS_ONLY
3536
import com.ichi2.anki.preferences.sharedPrefs
3637
import com.ichi2.anki.reviewreminders.ReviewReminder
@@ -96,8 +97,16 @@ class NotificationService : BroadcastReceiver() {
9697
}
9798
}
9899
val dueCardsTotal = dueCardsCount.count()
99-
if (dueCardsTotal < reviewReminder.cardTriggerThreshold.threshold) {
100-
Timber.d("Aborting notification due to threshold: $dueCardsTotal < ${reviewReminder.cardTriggerThreshold.threshold}")
100+
val consideredCardsCount =
101+
Counts().apply {
102+
if (reviewReminder.countNew) addNew(dueCardsCount.new)
103+
if (reviewReminder.countLrn) addLrn(dueCardsCount.lrn)
104+
if (reviewReminder.countRev) addRev(dueCardsCount.rev)
105+
}
106+
val consideredCardsTotal = consideredCardsCount.count()
107+
Timber.d("Due cards count: $dueCardsCount, Considered cards count: $consideredCardsCount")
108+
if (consideredCardsTotal < reviewReminder.cardTriggerThreshold.threshold) {
109+
Timber.d("Aborting notification due to threshold: $consideredCardsTotal < ${reviewReminder.cardTriggerThreshold.threshold}")
101110
return
102111
}
103112

AnkiDroid/src/main/res/layout/add_edit_reminder_dialog.xml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,69 @@
175175

176176
</LinearLayout>
177177

178+
<LinearLayout
179+
android:id="@+id/add_edit_reminder_count_new_section"
180+
android:layout_width="match_parent"
181+
android:layout_height="wrap_content"
182+
android:orientation="horizontal">
183+
184+
<com.google.android.material.checkbox.MaterialCheckBox
185+
android:id="@+id/add_edit_reminder_count_new_checkbox"
186+
android:layout_width="wrap_content"
187+
android:layout_height="wrap_content" />
188+
189+
<TextView
190+
android:id="@+id/add_edit_reminder_count_new_label"
191+
android:layout_width="0dp"
192+
android:layout_height="wrap_content"
193+
android:layout_weight="1"
194+
android:text="Count new cards when checking card threshold"
195+
tools:ignore="HardcodedText" />
196+
197+
</LinearLayout>
198+
199+
<LinearLayout
200+
android:id="@+id/add_edit_reminder_count_lrn_section"
201+
android:layout_width="match_parent"
202+
android:layout_height="wrap_content"
203+
android:orientation="horizontal">
204+
205+
<com.google.android.material.checkbox.MaterialCheckBox
206+
android:id="@+id/add_edit_reminder_count_lrn_checkbox"
207+
android:layout_width="wrap_content"
208+
android:layout_height="wrap_content" />
209+
210+
<TextView
211+
android:id="@+id/add_edit_reminder_count_lrn_label"
212+
android:layout_width="0dp"
213+
android:layout_height="wrap_content"
214+
android:layout_weight="1"
215+
android:text="Count cards in learning when checking card threshold"
216+
tools:ignore="HardcodedText" />
217+
218+
</LinearLayout>
219+
220+
<LinearLayout
221+
android:id="@+id/add_edit_reminder_count_rev_section"
222+
android:layout_width="match_parent"
223+
android:layout_height="wrap_content"
224+
android:orientation="horizontal">
225+
226+
<com.google.android.material.checkbox.MaterialCheckBox
227+
android:id="@+id/add_edit_reminder_count_rev_checkbox"
228+
android:layout_width="wrap_content"
229+
android:layout_height="wrap_content" />
230+
231+
<TextView
232+
android:id="@+id/add_edit_reminder_count_rev_label"
233+
android:layout_width="0dp"
234+
android:layout_height="wrap_content"
235+
android:layout_weight="1"
236+
android:text="Count cards in review when checking card threshold"
237+
tools:ignore="HardcodedText" />
238+
239+
</LinearLayout>
240+
178241
</LinearLayout>
179242

180243
</LinearLayout>

0 commit comments

Comments
 (0)