Skip to content

Conversation

@ericli3690
Copy link
Member

@ericli3690 ericli3690 commented Aug 9, 2025

Review reminders for a deck should be greyed out when the deck they are tied to cannot be found. This PR implements this logic.

Description and Approach

Edits ScheduleRemindersAdapter so that if a deck is not accessible to the user, the corresponding item in the ScheduleReminders list is grey and crossed-out. This is done by errorReminderIfDeckNotFound. Since global review reminders don't have a corresponding deck, they are always displayed as active.

To ensure the style works with all possible app themes, I determine which theme is active using attr constants and convert them to theme colors via a getThemeColor method. However, since this method will run a lot (twice for each element in the list), I make it cache its return values.

Checking whether a deck is accessible to the user via canUserAccessDeck requires a suspending call to the collection, which the Adapter cannot trigger, so I moved this logic to ScheduleReminders with a callback to continue Adapter logic after whether the deck is accessible has been retrieved.

I also realized that this pattern of using a callback could also be applied to another already-existing function in ScheduleReminders that I thought was a bit messy: setDeckNameFromScopeForView. I originally created this method because I needed to retrieve the deck's name, which required launching a coroutine, which needed to be done in ScheduleReminders. However, the fact that setDeckNameFromScopeForView was also handling UI made the method feel impure and ugly. By using a callback, I can keep all the UI logic for individual list elements within ScheduleRemindersAdapter while still allowing the code flow to pass through ScheduleReminders in order to retrieve the deck's name.

Fixes

  • For GSoC 2025: Review Reminders

How Has This Been Tested?

  • Tests pass.
  • Tested on a physical Samsung S23, API 34.

Learning

Old PR description:

  • A major problem I initially thought hard about was handling the fact that deck deletion is reversible: that is, a user can click an undo button after deleting a deck, which restores the collection to its previous form. However, this means that I cannot eagerly delete review reminders the moment the user clicks delete. At first, I came up with a complicated idea for storing a Pref of the most recently deleted decks and checking the Pref whenever the review reminders were accessed, but that idea was overly complicated.
  • I then realized I could attach a callback to the deck deletion undo snackbar that would fire only if the snackbar naturally disappeared and the undo button was not pressed. However, there was still a problem with this approach: namely, that the user can cause deck deletions from other devices and cause them to appear via a sync!
  • I realized I was approaching the problem completely wrong. Deck deletion should not be handled when the user clicks delete: it should be handled lazily, whenever the review reminders are actually needed. Hence I implemented this current approach of only deleting review reminders for decks if the deck is found not to exist during a review reminder access operation.
    We've now realized that it's best to not delete review reminders when the corresponding deck cannot be found, as there's a lot of reasons why a deck might be inaccessible (ex. the user deleted a deck, the user moved their AnkiDroid directory, the user is about to restore from a backup...).

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 requested a review from Copilot August 9, 2025 20:41
@ericli3690 ericli3690 added GSoC Pull requests authored by a Google Summer of Code participant [Candidate/Selected], for GSoC mentors Needs Review labels Aug 9, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements deck deletion handling for review reminders, ensuring that reminders associated with deleted decks are automatically cleaned up. The implementation uses a lazy deletion approach that checks deck existence when reminders are accessed rather than eagerly deleting upon deck removal.

Key changes include:

  • Added lazy deck existence checking using the decks.have method before returning deck-specific reminders
  • Implemented a deleteAllRemindersForDeck helper method for explicit cleanup
  • Converted database methods to suspend functions to support deck existence validation
  • Enhanced test coverage with deck deletion scenarios

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
libanki/src/main/java/com/ichi2/anki/libanki/Decks.kt Removed unused annotation from have method
AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabaseTest.kt Updated tests to use runTest and added deck deletion test cases
AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewReminderMigrationSettingsTest.kt Added schema version validation test
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt Added deck existence checks and deletion logic to database methods
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminderMigrationSettings.kt Added schema migration infrastructure
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt Made ReviewReminder implement ReviewReminderSchema interface

@ericli3690
Copy link
Member Author

ericli3690 commented Aug 9, 2025

Ready for review! #18856 should be merged first before this is merged, though. This PR is based on top of that one.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from bd07c88 to b0e76c4 Compare August 9, 2025 22:53
@ericli3690
Copy link
Member Author

  • Added some loggers.

@ericli3690 ericli3690 added the Blocked by dependency Currently blocked by some other dependent / related change label Aug 21, 2025
@ericli3690 ericli3690 removed Blocked by dependency Currently blocked by some other dependent / related change Needs Review labels Aug 26, 2025
@ericli3690 ericli3690 marked this pull request as draft August 26, 2025 22:28
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from b0e76c4 to 4fe2961 Compare August 26, 2025 22:30
@ericli3690 ericli3690 marked this pull request as ready for review August 26, 2025 22:45
@criticalAY criticalAY self-requested a review August 27, 2025 10:30
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from 4fe2961 to bffdf9f Compare August 29, 2025 00:25
@ericli3690
Copy link
Member Author

Rebased on main.

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.

This looks solid code-wise

What was the consensus on how multiple collections would be handled.

If I change 'AnkiDroid Directory', I presume this would remove the reminders

@ericli3690
Copy link
Member Author

Tested switching the AnkiDroid Directory just now and yes, it does delete the review reminders. Should we let this be intended behaviour? If a user has their collection, adds review reminders to a deck, switches the directory, views their review reminders (essentially triggering a review reminder "garbage collection"), and then switches the directory back, they'll suddenly no longer have review reminders for that deck. The problem is that we're not storing review reminders with the deck in the collection itself, or else this would be easy. I guess the "optimal" solution would be to add a new field to the upstream Anki database? That feels a bit excessive, though, especially if these reminders are only present on AnkiDroid.

Alternatively I could change the code so that if a review reminder's associated deck does not exist, we don't delete the review reminder but rather leave it hanging there and just don't display it in the UI / mute its notifications. Then, if the deck comes back into existence later, the previously-dead review reminder is reactivated. The downside here is that dead review reminders would gradually accumulate in SharedPreferences.

Perhaps the current system is the best? We could mark changing the AnkiDroid Directory as a destructive operation, perhaps. After all, it is an advanced feature.


Regarding the problem of multiple collections, I think the general consensus that was reached in the Discord #multiprofile channel was that if a review reminder for a deck is created and you switch profiles, you should still get reminders for that profile. The notification would have the profile name appended to the title or something similar to that, and upon clicking it, the profile would automatically switch and the deck would open. This kind of logic would also need to be implemented for the widget apparently? I'm not too familiar with potential implementations of the multiprofile system myself.

Right now, the review reminder deck deletion logic handling is simply checking if the deck exists in the collection, and if it doesn't, it deletes the associated review reminders. I'm not sure what the multiprofile equivalent of checking for deck existence would be. @criticalAY has taken over the project, I believe. Ashish, what's your plan for having multiple collections, and will this PR's method of checking if a deck exists by simply running withCol { decks.have(did) } still work after you implement the system?

@ericli3690 ericli3690 added Needs reviewer reply Waiting for a reply from another reviewer Needs Review and removed Needs Review labels Sep 6, 2025
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from bffdf9f to a89e22c Compare September 6, 2025 16:38
@ericli3690
Copy link
Member Author

  • Changed two loggers from .d to .i.

@ericli3690
Copy link
Member Author

Update: see discussion in Discord from September 10th, 2025. The core idea is:

Instead of deleting review reminders when their associated deck cannot be found, we can grey them out in ScheduleReminders and not fire any notifications. If the deck is found again the future (ex. due to a sync, backup restoration, AnkiDroid Directory change, etc.), the review reminder is no longer greyed out and its notifications are unmuted. When the user clicks on a greyed-out review reminder in ScheduleReminders, they can choose to reassign its deck or delete it. In other words, deleting a deck no longer immediately deletes associated review reminders: the user must manually do so from ScheduleReminders.

This will fix the problem with AnkiDroid Directory and a whole host of other issues with the current deck deletion code. It also resolves my past self's concerns here:

Alternatively I could change the code so that if a review reminder's associated deck does not exist, we don't delete the review reminder but rather leave it hanging there and just don't display it in the UI / mute its notifications. Then, if the deck comes back into existence later, the previously-dead review reminder is reactivated. The downside here is that dead review reminders would gradually accumulate in SharedPreferences.

...since the user will be able to manually view and delete accumulating dead review reminders.

What's nice about this approach is that it also works in an acceptable way with the proposed multiprofile implementation. When a profile is switched to, so long as each profile has its own review reminders SharedPrefs file, the user will only get review reminders for that specific profile and not any others. Of course, this is not the intended way the multiprofile feature will work in its final version. According to discussion in #multiprofile, all profiles should have their valid review reminder notifications fired, even if the profile is not active. Hence, a multiprofile developer will need to edit NotificationService later on to make sure all profiles have their valid review reminder notifications fired by checking the corresponding profile. They will also need to edit BootService and other boot-related files to ensure all profiles have their review reminder notifications set after a device reboot.

As I mentioned above, I'm proposing adding a profileID field to ReviewReminder so that when a notification is about to be fired, the corresponding collection can be checked to see if the deck still exists. Hopefully when Ashish sees this he'll let us know what the plan is for what the profileID is going to be (uuid, string, etc.)


I'll work on implementing this as soon as possible. This PR is likely now dependent on #19109. Marking this as a draft; I'll mark it as open once it's ready for review!

@ericli3690 ericli3690 marked this pull request as draft September 10, 2025 06:07
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from a89e22c to 159304c Compare September 29, 2025 16:28
@ericli3690 ericli3690 removed the Needs reviewer reply Waiting for a reply from another reviewer label Sep 29, 2025
@ericli3690 ericli3690 marked this pull request as ready for review September 29, 2025 17:02
@ericli3690
Copy link
Member Author

Done! Now, instead of deleting review reminders when a deck cannot be found, we simply grey the review reminder out. This means all previous concerns about actions like switching the AnkiDroid Directory or restoring from backups are moot. This does mean that dead review reminders will gradually accumulate in SharedPreferences, but since the user can view dead reminders by going to Settings > Review Reminders, I think this is an alright trade-off.

I've also received an answer from Ashish regarding a profileID in the ReviewReminder schema and implemented it at #19242, so I've removed the "needs reviewer reply" tag from this PR.

Ready for review!

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from 159304c to 52d681f Compare October 2, 2025 16:07
@ericli3690
Copy link
Member Author

Rebased and resolved merge conflict.

GSoC 2025: Review Reminders

Edits ScheduleRemindersAdapter so that if a deck is not accessible to the user, the corresponding item in the ScheduleReminders list is grey and crossed-out. This is done by `errorReminderIfDeckNotFound`. Since global review reminders don't have a corresponding deck, they are always displayed as active.

To ensure the style works with all possible app themes, I determine which theme is active using `attr` constants and convert them to theme colors via a `getThemeColor` method. However, since this method will run a lot (twice for each element in the list), I make it cache its return values.

Checking whether a deck is accessible to the user via `canUserAccessDeck` requires a suspending call to the collection, which the Adapter cannot trigger, so I moved this logic to ScheduleReminders with a callback to continue Adapter logic after whether the deck is accessible has been retrieved.

I also realized that this pattern of using a callback could also be applied to another already-existing function in ScheduleReminders that I thought was a bit messy: `setDeckNameFromScopeForView`. I originally created this method because I needed to retrieve the deck's name, which required launching a coroutine, which needed to be done in ScheduleReminders. However, the fact that `setDeckNameFromScopeForView` was also handling UI made the method feel impure and ugly. By using a callback, I can keep all the UI logic for individual list elements within ScheduleRemindersAdapter while still allowing the code flow to pass through ScheduleReminders in order to retrieve the deck's name.
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-deck-deletion branch from 52d681f to bbbe6e4 Compare October 17, 2025 16:58
@ericli3690
Copy link
Member Author

Rebased and resolved merge conflict.

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.

Solid solution. Thanks!

@ericli3690 ericli3690 added Needs Second Approval Has one approval, one more approval to merge and removed Needs Review labels Oct 27, 2025
Copy link
Member

@mikehardy mikehardy left a comment

Choose a reason for hiding this comment

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

Does seem like a nice solution

@mikehardy mikehardy added this pull request to the merge queue Nov 2, 2025
@mikehardy mikehardy added Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) and removed Needs Second Approval Has one approval, one more approval to merge labels Nov 2, 2025
Merged via the queue into ankidroid:main with commit f387509 Nov 2, 2025
10 checks passed
@github-actions github-actions bot removed the Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) label Nov 2, 2025
@github-actions github-actions bot added this to the 2.23 release milestone Nov 2, 2025
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants