Skip to content

Commit 9a53408

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 845d78f commit 9a53408

File tree

6 files changed

+351
-4
lines changed

6 files changed

+351
-4
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
@@ -36,7 +36,9 @@ 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 com.google.android.material.button.MaterialButton
41+
import com.google.android.material.checkbox.MaterialCheckBox
4042
import com.google.android.material.textfield.TextInputLayout
4143
import com.google.android.material.timepicker.MaterialTimePicker
4244
import com.google.android.material.timepicker.TimeFormat
@@ -132,6 +134,7 @@ class AddEditReminderDialog : DialogFragment() {
132134
setUpDeckSpinner()
133135
setUpAdvancedDropdown()
134136
setUpCardThresholdInput()
137+
setUpCountCheckboxes()
135138

136139
// For getting the result of the deck selection sub-dialog from ScheduleReminders
137140
// See ScheduleReminders.onDeckSelected for more information
@@ -234,6 +237,54 @@ class AddEditReminderDialog : DialogFragment() {
234237
}
235238
}
236239

240+
/**
241+
* Convenience data class for setting up the checkboxes for whether to count new, learning, and review cards
242+
* when considering the card trigger threshold.
243+
* @see setUpCountCheckboxes
244+
*/
245+
private data class CountViewsAndActions(
246+
val section: LinearLayout,
247+
val checkbox: MaterialCheckBox,
248+
val actionOnClick: () -> Unit,
249+
val state: LiveData<Boolean>,
250+
)
251+
252+
/**
253+
* Sets up the checkboxes for whether to count new, learning, and review cards when considering the card trigger threshold.
254+
* @see CountViewsAndActions
255+
*/
256+
private fun setUpCountCheckboxes() {
257+
val countViewsAndActionsItems =
258+
listOf(
259+
CountViewsAndActions(
260+
section = contentView.findViewById(R.id.add_edit_reminder_count_new_section),
261+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_new_checkbox),
262+
actionOnClick = viewModel::toggleCountNew,
263+
state = viewModel.countNew,
264+
),
265+
CountViewsAndActions(
266+
section = contentView.findViewById(R.id.add_edit_reminder_count_lrn_section),
267+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_lrn_checkbox),
268+
actionOnClick = viewModel::toggleCountLrn,
269+
state = viewModel.countLrn,
270+
),
271+
CountViewsAndActions(
272+
section = contentView.findViewById(R.id.add_edit_reminder_count_rev_section),
273+
checkbox = contentView.findViewById(R.id.add_edit_reminder_count_rev_checkbox),
274+
actionOnClick = viewModel::toggleCountRev,
275+
state = viewModel.countRev,
276+
),
277+
)
278+
279+
countViewsAndActionsItems.forEach { item ->
280+
item.section.setOnClickListener { item.actionOnClick() }
281+
item.checkbox.setOnClickListener { item.actionOnClick() }
282+
item.state.observe(this) { value ->
283+
item.checkbox.isChecked = value
284+
}
285+
}
286+
}
287+
237288
/**
238289
* Show the time picker dialog for selecting a time with a given hour and minute.
239290
* Does not automatically dismiss the old dialog.

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ class AddEditReminderDialogViewModel(
8484
)
8585
val cardTriggerThreshold: LiveData<Int> = _cardTriggerThreshold
8686

87+
private val _countNew = MutableLiveData(INITIAL_COUNT_NEW)
88+
val countNew: LiveData<Boolean> = _countNew
89+
90+
private val _countLrn = MutableLiveData(INITIAL_COUNT_LRN)
91+
val countLrn: LiveData<Boolean> = _countLrn
92+
93+
private val _countRev = MutableLiveData(INITIAL_COUNT_REV)
94+
val countRev: LiveData<Boolean> = _countRev
95+
8796
private val _advancedSettingsOpen = MutableLiveData(INITIAL_ADVANCED_SETTINGS_OPEN)
8897
val advancedSettingsOpen: LiveData<Boolean> = _advancedSettingsOpen
8998

@@ -102,6 +111,21 @@ class AddEditReminderDialogViewModel(
102111
_cardTriggerThreshold.value = threshold
103112
}
104113

114+
fun toggleCountNew() {
115+
Timber.d("Toggled count new from %s", _countNew.value)
116+
_countNew.value = !(_countNew.value ?: false)
117+
}
118+
119+
fun toggleCountLrn() {
120+
Timber.d("Toggled count lrn from %s", _countLrn.value)
121+
_countLrn.value = !(_countLrn.value ?: false)
122+
}
123+
124+
fun toggleCountRev() {
125+
Timber.d("Toggled count rev from %s", _countRev.value)
126+
_countRev.value = !(_countRev.value ?: false)
127+
}
128+
105129
fun toggleAdvancedSettingsOpen() {
106130
Timber.d("Toggled advanced settings open from %s", _advancedSettingsOpen.value)
107131
_advancedSettingsOpen.value = !(_advancedSettingsOpen.value ?: false)
@@ -131,6 +155,9 @@ class AddEditReminderDialogViewModel(
131155
is AddEditReminderDialog.DialogMode.Add -> true
132156
is AddEditReminderDialog.DialogMode.Edit -> dialogMode.reminderToBeEdited.enabled
133157
},
158+
countNew = countNew.value ?: INITIAL_COUNT_NEW,
159+
countLrn = countLrn.value ?: INITIAL_COUNT_LRN,
160+
countRev = countRev.value ?: INITIAL_COUNT_REV,
134161
)
135162

136163
companion object {
@@ -148,5 +175,25 @@ class AddEditReminderDialogViewModel(
148175
* We start with it closed to avoid overwhelming the user.
149176
*/
150177
private const val INITIAL_ADVANCED_SETTINGS_OPEN = false
178+
179+
/**
180+
* The default setting for whether new cards are counted when checking the card trigger threshold.
181+
* This value, and the other default settings for whether certain kinds of cards are counted
182+
* when checking the card trigger threshold, are all set to true, as removing some card types
183+
* from card trigger threshold consideration is a form of advanced review reminder customization.
184+
*/
185+
private const val INITIAL_COUNT_NEW = true
186+
187+
/**
188+
* The default setting for whether cards in learning are counted when checking the card trigger threshold.
189+
* @see INITIAL_COUNT_NEW
190+
*/
191+
private const val INITIAL_COUNT_LRN = true
192+
193+
/**
194+
* The default setting for whether cards in review are counted when checking the card trigger threshold.
195+
* @see INITIAL_COUNT_NEW
196+
*/
197+
private const val INITIAL_COUNT_REV = true
151198
}
152199
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,14 @@ sealed class ReviewReminderScope : Parcelable {
177177
* Preferably, also add some unit tests to ensure your migration works properly on all user devices once your update is rolled out.
178178
* See ReviewRemindersDatabaseTest for examples on how to do this.
179179
*
180-
* TODO: add remaining fields planned for GSoC 2025.
181-
*
182180
* @param id Unique, auto-incremented ID of the review reminder.
183181
* @param time See [ReviewReminderTime].
184182
* @param cardTriggerThreshold See [ReviewReminderCardTriggerThreshold].
185183
* @param scope See [ReviewReminderScope].
186184
* @param enabled Whether the review reminder's notifications are active or disabled.
185+
* @param countNew Whether new cards are counted when checking the [cardTriggerThreshold].
186+
* @param countLrn Whether learning cards are counted when checking the [cardTriggerThreshold].
187+
* @param countRev Whether review cards are counted when checking the [cardTriggerThreshold].
187188
*/
188189
@Serializable
189190
@Parcelize
@@ -194,6 +195,9 @@ data class ReviewReminder private constructor(
194195
val cardTriggerThreshold: ReviewReminderCardTriggerThreshold,
195196
val scope: ReviewReminderScope,
196197
var enabled: Boolean,
198+
val countNew: Boolean,
199+
val countLrn: Boolean,
200+
val countRev: Boolean,
197201
) : Parcelable,
198202
ReviewReminderSchema {
199203
companion object {
@@ -207,12 +211,18 @@ data class ReviewReminder private constructor(
207211
cardTriggerThreshold: ReviewReminderCardTriggerThreshold,
208212
scope: ReviewReminderScope = ReviewReminderScope.Global,
209213
enabled: Boolean = true,
214+
countNew: Boolean = true,
215+
countLrn: Boolean = true,
216+
countRev: Boolean = true,
210217
) = ReviewReminder(
211218
id = ReviewReminderId.getAndIncrementNextFreeReminderId(),
212219
time,
213220
cardTriggerThreshold,
214221
scope,
215222
enabled,
223+
countNew,
224+
countLrn,
225+
countRev,
216226
)
217227
}
218228

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
@@ -182,6 +182,69 @@
182182

183183
</LinearLayout>
184184

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

187250
</LinearLayout>

0 commit comments

Comments
 (0)