@@ -21,6 +21,7 @@ import android.content.SharedPreferences
2121import androidx.annotation.VisibleForTesting
2222import androidx.core.content.edit
2323import com.ichi2.anki.AnkiDroidApp
24+ import com.ichi2.anki.CollectionManager.withCol
2425import com.ichi2.anki.common.utils.android.isRobolectric
2526import com.ichi2.anki.libanki.DeckId
2627import 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