Skip to content

Conversation

@ericli3690
Copy link
Member

@ericli3690 ericli3690 commented Aug 29, 2025

Purpose / Description

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.

UI

image

Fixes

  • GSoC 2025: Review Reminders

Approach

One of the two planned review reminder advanced options! Just needs some extra checks when handling the Count objects.

How Has This Been Tested?

  • Unit tests pass.
  • Builds and runs on a physical Samsung S23, API 34.
  • Based on my own personal testing, it seems the feature works! I created a few review reminders, reviewed a few cards, etc.

Checklist

  • You have a descriptive commit message with a short title (first line, max 50 chars).
  • You have commented your code, particularly in hard-to-understand areas
  • You have performed a self-review of your own code
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@ericli3690 ericli3690 added Needs Review GSoC Pull requests authored by a Google Summer of Code participant [Candidate/Selected], for GSoC mentors Blocked by dependency Currently blocked by some other dependent / related change labels Aug 29, 2025
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from 1ca08cc to 0793e7a Compare September 5, 2025 21:46
@ericli3690 ericli3690 marked this pull request as draft September 5, 2025 22:56
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from 0793e7a to f2c9dde Compare September 6, 2025 05:01
@ericli3690 ericli3690 marked this pull request as ready for review September 6, 2025 05:21
@ericli3690
Copy link
Member Author

  • Rebased.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch 3 times, most recently from 5a3ddeb to a390cfb Compare September 7, 2025 17:20
@ericli3690
Copy link
Member Author

  • Rebased.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from a390cfb to 9a53408 Compare September 9, 2025 04:49
@ericli3690
Copy link
Member Author

  • Rebased.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from 9a53408 to ae683e5 Compare September 9, 2025 05:23
@ericli3690
Copy link
Member Author

Forgot to stage some code. Fixed.

@ericli3690 ericli3690 marked this pull request as draft September 26, 2025 04:11
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from ae683e5 to e128d8a Compare November 25, 2025 06:31
@ericli3690 ericli3690 removed Blocked by dependency Currently blocked by some other dependent / related change Has Conflicts labels Nov 25, 2025
@ericli3690 ericli3690 marked this pull request as ready for review November 25, 2025 06:58
@ericli3690
Copy link
Member Author

Rebased, fully unblocked, and ready for review!

@ericli3690 ericli3690 added the Blocked by dependency Currently blocked by some other dependent / related change label Dec 26, 2025
@ericli3690
Copy link
Member Author

I've decided to block this behind #19157, as some of this PR's code will need to be updated once that guy's merged.

@ericli3690 ericli3690 marked this pull request as draft January 9, 2026 06:28
@ericli3690 ericli3690 removed the Blocked by dependency Currently blocked by some other dependent / related change label Jan 9, 2026
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from e128d8a to 74874af Compare January 17, 2026 07:00
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from 74874af to d022fa4 Compare January 17, 2026 07:20
@ericli3690
Copy link
Member Author

  • Rebased! Resolved merge conflicts, cleaned up the test file.
  • Switched AddEditReminderDialog over to viewbinding (related to Add View Binding #11116)

Unblocked and ready to review!

@ericli3690 ericli3690 marked this pull request as ready for review January 17, 2026 08:00
david-allison

This comment was marked as resolved.

@david-allison david-allison added Needs Author Reply Waiting for a reply from the original author Deprecation Warnings on deprecated API usage and removed Deprecation Warnings on deprecated API usage labels Jan 17, 2026
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from d022fa4 to 8866c30 Compare January 18, 2026 01:28
@ericli3690
Copy link
Member Author

ericli3690 commented Jan 18, 2026

Thanks for the test patch! I've decided to go the full mile and add a few more helper functions on top, hopefully the file is more readable now.

As for the UI coloring, I love the idea. It was a bit tricky to implement in a way that would work with the translation system, but this is what I came up with:

// Manually split the string resource so that we can color just the review state part
val (reviewState, colorAttr) = REVIEW_STATE_STRINGS_AND_COLORS.entries.elementAt(i)
val splitString = getString(R.string.review_reminders_include_review_state_for_threshold_do_not_translate).split("%s")
item.textView.text =
    buildSpannedString {
        append(splitString[0])
        color(MaterialColors.getColor(requireContext(), colorAttr, 0)) {
            append(getString(reviewState))
        }
        append(splitString[1])
    }

Hopefully that works. I opted for no underline because it made it look clickable and the English text I've used is taken from upstream's wording; attached below is an image of the new UI. Should I move the new strings to a different file? Thoughts on the styling? Is it more readable now?

Screenshot_20260117_140615_AnkiDroid

@ericli3690 ericli3690 removed the Needs Author Reply Waiting for a reply from the original author label Jan 18, 2026
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, with colored text for the corresponding review state boolean. 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. Modifies some unit test utilities for convenience.
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-new-lrn-rev branch from 8866c30 to f4af44f Compare January 18, 2026 01:42
@david-allison
Copy link
Member

I'd strongly prefer lowercase: new, learn, review

Check with the other mentors on wording, but I'd prefer shorter text:

  • Include **review** cards is obvious [to me].

Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, thanks so much!!


// Manually split the string resource so that we can color just the review state part
val (reviewState, colorAttr) = REVIEW_STATE_STRINGS_AND_COLORS.entries.elementAt(i)
val splitString = getString(R.string.review_reminders_include_review_state_for_threshold_do_not_translate).split("%s")
Copy link
Member

@david-allison david-allison Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome.

I'd consider defining partition (pseudocode below, needs docs, maybe tests).

  • Minor bug that if %s is missing, things blow up - probably fine to ignore
  • Minor bug that the string is truncated if there are too many %s - fixed below
  • Ensure that a change: %s to %1$s would fail a unit test
    fun String.partition(delimiter: String): Pair<String, String> = this.split(delimiter, limit = 2)
    val (before, after) = "a %s b".partition("%s")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, elegant! Implemented, I've added unit tests too.

/**
* Convenience method for chaining [addDeck] and [addNoteToDeck].
*
* Usage: `val deckId = addDeck("My Deck").withNote(count = 5, queueType = QueueType.New)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

give naming a second consideration, maybe even define withNote to mean 1, and withNotes to mean more than 1.

Subject: [PATCH] 
---
Index: AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt
--- a/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt	(revision f4af44f18bcf1ad2a3348d3ec51a9e8d3b314072)
+++ b/AnkiDroid/src/test/java/com/ichi2/anki/services/NotificationServiceTest.kt	(date 1768720363077)
@@ -87,7 +87,7 @@
     @Test
     fun `onReceive with less cards than card threshold should not fire notification but schedule next`() =
         runTest {
-            val did1 = addDeck("Deck", setAsSelected = true).withNote(count = 2)
+            val did1 = addDeck("Deck", setAsSelected = true).withNotes(count = 2)
             val reviewReminderDeckSpecific = createTestReminder(deckId = did1, thresholdInt = 3)
             val reviewReminderAppWide = createTestReminder(thresholdInt = 3)
             ReviewRemindersDatabase.storeReminders(reviewReminderDeckSpecific, reviewReminderAppWide)
@@ -103,7 +103,7 @@
     @Test
     fun `onReceive with happy path for single deck should fire notification and schedule next`() =
         runTest {
-            val did1 = addDeck("Deck", setAsSelected = true).withNote(count = 2)
+            val did1 = addDeck("Deck", setAsSelected = true).withNotes(count = 2)
             val reviewReminder = createAndSaveDummyDeckSpecificReminder(did1)
 
             triggerDummyReminderNotification(reviewReminder)
@@ -115,8 +115,8 @@
     @Test
     fun `onReceive with happy path for global reminder should fire notification and schedule next`() =
         runTest {
-            addDeck("Deck1").withNote(count = 2)
-            addDeck("Deck2").withNote(count = 2)
+            addDeck("Deck1").withNotes(count = 2)
+            addDeck("Deck2").withNotes(count = 2)
             val reviewReminder = createTestReminder(thresholdInt = 4)
 
             triggerDummyReminderNotification(reviewReminder)
@@ -210,7 +210,7 @@
     @Test
     fun `onReceive with blocked collection should not fire notification but schedule next`() =
         runTest {
-            val did1 = addDeck("Deck", setAsSelected = true).withNote(count = 2)
+            val did1 = addDeck("Deck", setAsSelected = true).withNotes(count = 2)
             val reviewReminderDeckSpecific = createAndSaveDummyDeckSpecificReminder(did1)
             val reviewReminderAppWide = createAndSaveDummyAppWideReminder()
 
@@ -226,7 +226,7 @@
     @Test
     fun `onReceive with snoozed notification should fire notification but not schedule next`() =
         runTest {
-            val did1 = addDeck("Deck", setAsSelected = true).withNote(count = 2)
+            val did1 = addDeck("Deck", setAsSelected = true).withNotes(count = 2)
             val reviewReminderDeckSpecific = createAndSaveDummyDeckSpecificReminder(did1)
             val reviewReminderAppWide = createAndSaveDummyAppWideReminder()
 
@@ -243,7 +243,7 @@
     @Test
     fun `onReceive with rev cards not counted and only rev cards present should not fire notification`() =
         runTest {
-            val did1 = addDeck("Deck", setAsSelected = true).withNote(count = 2, queueType = QueueType.Rev)
+            val did1 = addDeck("Deck", setAsSelected = true).withNotes(count = 2, queueType = QueueType.Rev)
             val reviewReminder = createTestReminder(deckId = did1, countRev = false)
             ReviewRemindersDatabase.storeReminders(reviewReminder)
 
@@ -255,7 +255,7 @@
     @Test
     fun `onReceive with new cards not counted and only new cards present should not fire notification`() =
         runTest {
-            val did1 = addDeck("Deck").withNote(count = 2)
+            val did1 = addDeck("Deck").withNotes(count = 2)
             val reviewReminder = createTestReminder(deckId = did1, countNew = false)
             ReviewRemindersDatabase.storeReminders(reviewReminder)
 
@@ -267,7 +267,7 @@
     @Test
     fun `onReceive with lrn cards not counted and only lrn cards present should not fire notification`() =
         runTest {
-            val did1 = addDeck("Deck").withNote(count = 2, queueType = QueueType.Lrn)
+            val did1 = addDeck("Deck").withNotes(count = 2, queueType = QueueType.Lrn)
             val reviewReminder = createTestReminder(deckId = did1, countLrn = false)
             ReviewRemindersDatabase.storeReminders(reviewReminder)
 
@@ -319,8 +319,8 @@
     @Test
     fun `snooze actions of different notifications and different intervals should be different`() =
         runTest {
-            val did1 = addDeck("Deck1").withNote(count = 2)
-            val did2 = addDeck("Deck2").withNote(count = 2)
+            val did1 = addDeck("Deck1").withNotes(count = 2)
+            val did2 = addDeck("Deck2").withNotes(count = 2)
             val reviewReminderOne = createTestReminder(deckId = did1, thresholdInt = 1)
             val reviewReminderTwo = createTestReminder(deckId = did2, thresholdInt = 1)
             ReviewRemindersDatabase.storeReminders(reviewReminderOne, reviewReminderTwo)
Index: libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt b/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt
--- a/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt	(revision f4af44f18bcf1ad2a3348d3ec51a9e8d3b314072)
+++ b/libanki/testutils/src/main/java/com/ichi2/anki/libanki/testutils/AnkiTest.kt	(date 1768720430579)
@@ -226,10 +226,18 @@
     /**
      * Convenience method for chaining [addDeck] and [addNoteToDeck].
      *
-     * Usage: `val deckId = addDeck("My Deck").withNote(count = 5, queueType = QueueType.New)`
+     * Usage: `val deckId = addDeck("My Deck").withNote(queueType = QueueType.New)`
      */
-    fun DeckId.withNote(
-        count: Int = 1,
+    fun DeckId.withNote(queueType: QueueType = QueueType.New): DeckId
+        = withNotes(count = 1, queueType = queueType)
+
+    /**
+     * Convenience method for chaining [addDeck] and [addNoteToDeck].
+     *
+     * Usage: `val deckId = addDeck("My Deck").withNotes(count = 5, queueType = QueueType.New)`
+     */
+    fun DeckId.withNotes(
+        count: Int,
         queueType: QueueType = QueueType.New,
     ): DeckId =
         this.apply {

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Jan 19, 2026
@ericli3690
Copy link
Member Author

I've decided to keep the existing text ("Include [type] cards for card threshold") for now and will create an issue later to address possibly changing it to be more precise. I think that we should probably err on the side of verbosity to ensure users and translators understand what the string is supposed to mean. I've implemented making the text lowercase, too.

Due to #20163, we're putting this PR on hold until migrations on the main branch are stable. I'll also change this code to have the proper migration.

@ericli3690 ericli3690 marked this pull request as draft January 19, 2026 17:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GSoC Pull requests authored by a Google Summer of Code participant [Candidate/Selected], for GSoC mentors Has Conflicts Needs Author Reply Waiting for a reply from the original author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants