Skip to content

Commit bd07c88

Browse files
committed
feat(reminders): handle deck deletion
GSoC 2025: Review Reminders - Added logic for checking if a deck exists before returning deck-specific reminders to ReviewRemindersDatabase. This code uses the `decks.have` method, since it seemed like the most straightforward way to accomplish deck-existence-checking. - `have` was previously marked as "unused". I've removed this annotation. - Added a deleteAllRemindersForDeck helper method. It's public because it will also be used in NotificationService. - Had to mark a few database methods as suspending since they use the collection to check if a deck exists. This in turn meant wrapping some of the tests in the test file with `runTest`. Also had to explicitly create decks using `addDeck` in the test file. Moved the dummy review reminder declarations to setUp to accommodate this. - Added new tests for deck deletion functionality.
1 parent 3566f0c commit bd07c88

File tree

3 files changed

+372
-234
lines changed

3 files changed

+372
-234
lines changed

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

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import android.content.SharedPreferences
2121
import androidx.annotation.VisibleForTesting
2222
import androidx.core.content.edit
2323
import com.ichi2.anki.AnkiDroidApp
24+
import com.ichi2.anki.CollectionManager.withCol
2425
import com.ichi2.anki.common.utils.android.isRobolectric
2526
import com.ichi2.anki.libanki.DeckId
2627
import kotlinx.serialization.InternalSerializationApi
@@ -229,11 +230,19 @@ object ReviewRemindersDatabase {
229230
}
230231

231232
/**
232-
* Get the [ReviewReminder]s for a specific deck.
233+
* Get the [ReviewReminder]s for a specific deck. Deletes the review reminders for this deck if the deck does not exist.
233234
* @throws SerializationException If the reminders map has not been stored in SharedPreferences as a valid JSON string.
234235
* @throws IllegalArgumentException If the decoded reminders map is not a HashMap<[ReviewReminderId], [ReviewReminder]>.
235236
*/
236-
fun getRemindersForDeck(did: DeckId): HashMap<ReviewReminderId, ReviewReminder> = getRemindersForKey(DECK_SPECIFIC_KEY + did)
237+
suspend fun getRemindersForDeck(did: DeckId): HashMap<ReviewReminderId, ReviewReminder> {
238+
val doesDeckExist = withCol { decks.have(did) }
239+
return if (doesDeckExist) {
240+
getRemindersForKey(DECK_SPECIFIC_KEY + did)
241+
} else {
242+
deleteAllRemindersForDeck(did)
243+
hashMapOf()
244+
}
245+
}
237246

238247
/**
239248
* Get the app-wide [ReviewReminder]s.
@@ -244,15 +253,42 @@ object ReviewRemindersDatabase {
244253

245254
/**
246255
* Get all [ReviewReminder]s that are associated with a specific deck, all in a single flattened map.
256+
* For each deck, deletes the deck's review reminders if the deck does not exist.
247257
* @throws SerializationException If the reminders maps have not been stored in SharedPreferences as valid JSON strings.
248258
* @throws IllegalArgumentException If the decoded reminders maps are not instances of HashMap<[ReviewReminderId], [ReviewReminder]>.
249259
*/
250-
fun getAllDeckSpecificReminders(): HashMap<ReviewReminderId, ReviewReminder> =
251-
remindersSharedPrefs
252-
.all
253-
.filterKeys { it.startsWith(DECK_SPECIFIC_KEY) }
254-
.flatMap { (key, value) -> decodeJson(value.toString(), deckKeyForMigrationPurposes = key).entries }
255-
.associateTo(hashMapOf()) { it.toPair() }
260+
suspend fun getAllDeckSpecificReminders(): HashMap<ReviewReminderId, ReviewReminder> {
261+
// Get all deck-specific reminders
262+
val deckSpecificRemindersMap =
263+
remindersSharedPrefs
264+
.all
265+
.filterKeys { it.startsWith(DECK_SPECIFIC_KEY) }
266+
.toMutableMap()
267+
// Delete deck-specific reminders for decks that do not exist
268+
// Opens a SharedPreferences transaction and the collection only once
269+
remindersSharedPrefs.edit {
270+
withCol {
271+
deckSpecificRemindersMap.entries.removeIf { (key, _) ->
272+
val did = key.removePrefix(DECK_SPECIFIC_KEY).toLong()
273+
val doesDeckExist = decks.have(did)
274+
if (doesDeckExist) {
275+
false // Keep this group of review reminders
276+
} else {
277+
remove(key) // Remove from SharedPreferences
278+
true // Remove from deckSpecificRemindersMap
279+
}
280+
}
281+
}
282+
}
283+
// Decode the remaining deck-specific reminders and return
284+
return deckSpecificRemindersMap
285+
.flatMap { (key, value) ->
286+
decodeJson(
287+
value.toString(),
288+
deckKeyForMigrationPurposes = key,
289+
).entries
290+
}.associateTo(hashMapOf()) { it.toPair() }
291+
}
256292

257293
/**
258294
* Edit the [ReviewReminder]s for a specific key.
@@ -273,17 +309,24 @@ object ReviewRemindersDatabase {
273309
}
274310

275311
/**
276-
* Edit the [ReviewReminder]s for a specific deck.
312+
* Edit the [ReviewReminder]s for a specific deck. Deletes the review reminders for this deck if the deck does not exist.
277313
* This assumes the resulting map contains only reminders of scope [ReviewReminderScope.DeckSpecific].
278314
* @param did
279315
* @param reminderEditor A lambda that takes the current map and returns the updated map.
280316
* @throws SerializationException If the current reminders map has not been stored in SharedPreferences as a valid JSON string.
281317
* @throws IllegalArgumentException If the decoded current reminders map is not a HashMap<[ReviewReminderId], [ReviewReminder]>.
282318
*/
283-
fun editRemindersForDeck(
319+
suspend fun editRemindersForDeck(
284320
did: DeckId,
285321
reminderEditor: (HashMap<ReviewReminderId, ReviewReminder>) -> Map<ReviewReminderId, ReviewReminder>,
286-
) = editRemindersForKey(DECK_SPECIFIC_KEY + did, reminderEditor)
322+
) {
323+
val doesDeckExist = withCol { decks.have(did) }
324+
if (doesDeckExist) {
325+
editRemindersForKey(DECK_SPECIFIC_KEY + did, reminderEditor)
326+
} else {
327+
deleteAllRemindersForDeck(did)
328+
}
329+
}
287330

288331
/**
289332
* Edit the app-wide [ReviewReminder]s.
@@ -294,4 +337,20 @@ object ReviewRemindersDatabase {
294337
*/
295338
fun editAllAppWideReminders(reminderEditor: (HashMap<ReviewReminderId, ReviewReminder>) -> Map<ReviewReminderId, ReviewReminder>) =
296339
editRemindersForKey(APP_WIDE_KEY, reminderEditor)
340+
341+
/**
342+
* Delete all [ReviewReminder]s for a specific deck.
343+
* Fully removes the stored JSON string representing the stored review reminders from SharedPreferences.
344+
* Does nothing if no review reminders for this deck have been stored.
345+
*
346+
* Public so that if a notification is being fired for a deck that has been deleted, the notification can be
347+
* cancelled and the review reminders deleted. In general, deleting review reminders when a deck has been deleted
348+
* is handled lazily: i.e., we do not immediately delete reminders for a deck when it is deleted but rather
349+
* wait until the reminders are requested for display or for notification to check if a deletion should be performed.
350+
*/
351+
fun deleteAllRemindersForDeck(did: DeckId) {
352+
remindersSharedPrefs.edit {
353+
remove(DECK_SPECIFIC_KEY + did)
354+
}
355+
}
297356
}

0 commit comments

Comments
 (0)