Skip to content

Commit e128d8a

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 614e087 commit e128d8a

File tree

6 files changed

+402
-56
lines changed

6 files changed

+402
-56
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ import androidx.fragment.app.DialogFragment
3535
import androidx.fragment.app.setFragmentResult
3636
import androidx.fragment.app.setFragmentResultListener
3737
import androidx.fragment.app.viewModels
38+
import androidx.lifecycle.LiveData
3839
import com.google.android.material.button.MaterialButton
40+
import com.google.android.material.checkbox.MaterialCheckBox
3941
import com.google.android.material.textfield.TextInputLayout
4042
import com.google.android.material.timepicker.MaterialTimePicker
4143
import com.google.android.material.timepicker.TimeFormat
@@ -134,6 +136,7 @@ class AddEditReminderDialog : DialogFragment() {
134136
setInitialDeckSelection()
135137
setUpAdvancedDropdown()
136138
setUpCardThresholdInput()
139+
setUpCountCheckboxes()
137140

138141
// For getting the result of the deck selection sub-dialog from ScheduleReminders
139142
// See ScheduleReminders.onDeckSelected for more information
@@ -264,6 +267,54 @@ class AddEditReminderDialog : DialogFragment() {
264267
}
265268
}
266269

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

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,33 @@ class AddEditReminderDialogViewModel(
8989
)
9090
val cardTriggerThreshold: LiveData<Int> = _cardTriggerThreshold
9191

92+
private val _countNew =
93+
MutableLiveData(
94+
when (dialogMode) {
95+
is AddEditReminderDialog.DialogMode.Add -> INITIAL_COUNT_NEW
96+
is AddEditReminderDialog.DialogMode.Edit -> dialogMode.reminderToBeEdited.countNew
97+
},
98+
)
99+
val countNew: LiveData<Boolean> = _countNew
100+
101+
private val _countLrn =
102+
MutableLiveData(
103+
when (dialogMode) {
104+
is AddEditReminderDialog.DialogMode.Add -> INITIAL_COUNT_LRN
105+
is AddEditReminderDialog.DialogMode.Edit -> dialogMode.reminderToBeEdited.countLrn
106+
},
107+
)
108+
val countLrn: LiveData<Boolean> = _countLrn
109+
110+
private val _countRev =
111+
MutableLiveData(
112+
when (dialogMode) {
113+
is AddEditReminderDialog.DialogMode.Add -> INITIAL_COUNT_REV
114+
is AddEditReminderDialog.DialogMode.Edit -> dialogMode.reminderToBeEdited.countRev
115+
},
116+
)
117+
val countRev: LiveData<Boolean> = _countRev
118+
92119
private val _advancedSettingsOpen = MutableLiveData(INITIAL_ADVANCED_SETTINGS_OPEN)
93120
val advancedSettingsOpen: LiveData<Boolean> = _advancedSettingsOpen
94121

@@ -107,6 +134,21 @@ class AddEditReminderDialogViewModel(
107134
_cardTriggerThreshold.value = threshold
108135
}
109136

137+
fun toggleCountNew() {
138+
Timber.d("Toggled count new from %s", _countNew.value)
139+
_countNew.value = !(_countNew.value ?: false)
140+
}
141+
142+
fun toggleCountLrn() {
143+
Timber.d("Toggled count lrn from %s", _countLrn.value)
144+
_countLrn.value = !(_countLrn.value ?: false)
145+
}
146+
147+
fun toggleCountRev() {
148+
Timber.d("Toggled count rev from %s", _countRev.value)
149+
_countRev.value = !(_countRev.value ?: false)
150+
}
151+
110152
fun toggleAdvancedSettingsOpen() {
111153
Timber.d("Toggled advanced settings open from %s", _advancedSettingsOpen.value)
112154
_advancedSettingsOpen.value = !(_advancedSettingsOpen.value ?: false)
@@ -136,6 +178,9 @@ class AddEditReminderDialogViewModel(
136178
is AddEditReminderDialog.DialogMode.Add -> true
137179
is AddEditReminderDialog.DialogMode.Edit -> dialogMode.reminderToBeEdited.enabled
138180
},
181+
countNew = countNew.value ?: INITIAL_COUNT_NEW,
182+
countLrn = countLrn.value ?: INITIAL_COUNT_LRN,
183+
countRev = countRev.value ?: INITIAL_COUNT_REV,
139184
)
140185

141186
companion object {
@@ -153,5 +198,25 @@ class AddEditReminderDialogViewModel(
153198
* We start with it closed to avoid overwhelming the user.
154199
*/
155200
private const val INITIAL_ADVANCED_SETTINGS_OPEN = false
201+
202+
/**
203+
* The default setting for whether new cards are counted when checking the card trigger threshold.
204+
* This value, and the other default settings for whether certain kinds of cards are counted
205+
* when checking the card trigger threshold, are all set to true, as removing some card types
206+
* from card trigger threshold consideration is a form of advanced review reminder customization.
207+
*/
208+
private const val INITIAL_COUNT_NEW = true
209+
210+
/**
211+
* The default setting for whether cards in learning are counted when checking the card trigger threshold.
212+
* @see INITIAL_COUNT_NEW
213+
*/
214+
private const val INITIAL_COUNT_LRN = true
215+
216+
/**
217+
* The default setting for whether cards in review are counted when checking the card trigger threshold.
218+
* @see INITIAL_COUNT_NEW
219+
*/
220+
private const val INITIAL_COUNT_REV = true
156221
}
157222
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,15 +178,16 @@ sealed class ReviewReminderScope : Parcelable {
178178
* Preferably, also add some unit tests to ensure your migration works properly on all user devices once your update is rolled out.
179179
* See ReviewRemindersDatabaseTest for examples on how to do this.
180180
*
181-
* TODO: add remaining fields planned for GSoC 2025.
182-
*
183181
* @param id Unique, auto-incremented ID of the review reminder.
184182
* @param time See [ReviewReminderTime].
185183
* @param cardTriggerThreshold See [ReviewReminderCardTriggerThreshold].
186184
* @param scope See [ReviewReminderScope].
187185
* @param enabled Whether the review reminder's notifications are active or disabled.
188186
* @param profileID ID representing the profile which created this review reminder, as review reminders for
189187
* multiple profiles might be active simultaneously.
188+
* @param countNew Whether new cards are counted when checking the [cardTriggerThreshold].
189+
* @param countLrn Whether learning cards are counted when checking the [cardTriggerThreshold].
190+
* @param countRev Whether review cards are counted when checking the [cardTriggerThreshold].
190191
*/
191192
@Serializable
192193
@Parcelize
@@ -198,6 +199,9 @@ data class ReviewReminder private constructor(
198199
val scope: ReviewReminderScope,
199200
var enabled: Boolean,
200201
val profileID: String,
202+
val countNew: Boolean,
203+
val countLrn: Boolean,
204+
val countRev: Boolean,
201205
) : Parcelable,
202206
ReviewReminderSchema {
203207
companion object {
@@ -212,13 +216,19 @@ data class ReviewReminder private constructor(
212216
scope: ReviewReminderScope = ReviewReminderScope.Global,
213217
enabled: Boolean = true,
214218
profileID: String = "",
219+
countNew: Boolean = true,
220+
countLrn: Boolean = true,
221+
countRev: Boolean = true,
215222
) = ReviewReminder(
216223
id = ReviewReminderId.getAndIncrementNextFreeReminderId(),
217224
time,
218225
cardTriggerThreshold,
219226
scope,
220227
enabled,
221228
profileID,
229+
countNew,
230+
countLrn,
231+
countRev,
222232
)
223233
}
224234

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import com.ichi2.anki.R
3232
import com.ichi2.anki.canUserAccessDeck
3333
import com.ichi2.anki.common.annotations.LegacyNotifications
3434
import com.ichi2.anki.libanki.Decks
35+
import com.ichi2.anki.libanki.sched.Counts
3536
import com.ichi2.anki.preferences.PENDING_NOTIFICATIONS_ONLY
3637
import com.ichi2.anki.preferences.sharedPrefs
3738
import com.ichi2.anki.reviewreminders.ReviewReminder
@@ -104,8 +105,16 @@ class NotificationService : BroadcastReceiver() {
104105
}
105106
}
106107
val dueCardsTotal = dueCardsCount.count()
107-
if (dueCardsTotal < reviewReminder.cardTriggerThreshold.threshold) {
108-
Timber.d("Aborting notification due to threshold: $dueCardsTotal < ${reviewReminder.cardTriggerThreshold.threshold}")
108+
val consideredCardsCount =
109+
Counts().apply {
110+
if (reviewReminder.countNew) addNew(dueCardsCount.new)
111+
if (reviewReminder.countLrn) addLrn(dueCardsCount.lrn)
112+
if (reviewReminder.countRev) addRev(dueCardsCount.rev)
113+
}
114+
val consideredCardsTotal = consideredCardsCount.count()
115+
Timber.d("Due cards count: $dueCardsCount, Considered cards count: $consideredCardsCount")
116+
if (consideredCardsTotal < reviewReminder.cardTriggerThreshold.threshold) {
117+
Timber.d("Aborting notification due to threshold: $consideredCardsTotal < ${reviewReminder.cardTriggerThreshold.threshold}")
109118
return
110119
}
111120

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

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

190190
</LinearLayout>
191191

192+
<LinearLayout
193+
android:id="@+id/add_edit_reminder_count_new_section"
194+
android:layout_width="match_parent"
195+
android:layout_height="wrap_content"
196+
android:orientation="horizontal">
197+
198+
<com.google.android.material.checkbox.MaterialCheckBox
199+
android:id="@+id/add_edit_reminder_count_new_checkbox"
200+
android:layout_width="wrap_content"
201+
android:layout_height="wrap_content" />
202+
203+
<TextView
204+
android:id="@+id/add_edit_reminder_count_new_label"
205+
android:layout_width="0dp"
206+
android:layout_height="wrap_content"
207+
android:layout_weight="1"
208+
android:text="Count new cards when checking card threshold"
209+
tools:ignore="HardcodedText" />
210+
211+
</LinearLayout>
212+
213+
<LinearLayout
214+
android:id="@+id/add_edit_reminder_count_lrn_section"
215+
android:layout_width="match_parent"
216+
android:layout_height="wrap_content"
217+
android:orientation="horizontal">
218+
219+
<com.google.android.material.checkbox.MaterialCheckBox
220+
android:id="@+id/add_edit_reminder_count_lrn_checkbox"
221+
android:layout_width="wrap_content"
222+
android:layout_height="wrap_content" />
223+
224+
<TextView
225+
android:id="@+id/add_edit_reminder_count_lrn_label"
226+
android:layout_width="0dp"
227+
android:layout_height="wrap_content"
228+
android:layout_weight="1"
229+
android:text="Count cards in learning when checking card threshold"
230+
tools:ignore="HardcodedText" />
231+
232+
</LinearLayout>
233+
234+
<LinearLayout
235+
android:id="@+id/add_edit_reminder_count_rev_section"
236+
android:layout_width="match_parent"
237+
android:layout_height="wrap_content"
238+
android:orientation="horizontal">
239+
240+
<com.google.android.material.checkbox.MaterialCheckBox
241+
android:id="@+id/add_edit_reminder_count_rev_checkbox"
242+
android:layout_width="wrap_content"
243+
android:layout_height="wrap_content" />
244+
245+
<TextView
246+
android:id="@+id/add_edit_reminder_count_rev_label"
247+
android:layout_width="0dp"
248+
android:layout_height="wrap_content"
249+
android:layout_weight="1"
250+
android:text="Count cards in review when checking card threshold"
251+
tools:ignore="HardcodedText" />
252+
253+
</LinearLayout>
254+
192255
</LinearLayout>
193256

194257
</LinearLayout>

0 commit comments

Comments
 (0)