-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Created ReviewRemindersDatabase and ReviewReminder #18364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Created ReviewRemindersDatabase and ReviewReminder #18364
Conversation
Hi, here's a first draft of what I'd like the review reminder storage local database storage system to be like. Looking forward to constructive criticism. For now, I’ve put these files in anki/notifications since that seemed a reasonable place to put them. Let me know if I should move them elsewhere, such as by creating a new anki/reviewreminders folder. |
val nextFreeIdKey = intPreferencesKey("next_free_id") | ||
val previouslyUsedIdsKey = stringPreferencesKey("previously_used_ids") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Define constants for the key names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point, hardcoding it was silly of me. Fixed.
Pair(nextFreeId, previouslyUsedIds) | ||
} ?: Pair(FIRST_REMINDER_ID, "[]") | ||
val previouslyUsedIdsList = Json.decodeFromString<MutableList<Int>>(previouslyUsedIds) | ||
// Use previously used IDs if available, else increment next free ID |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can have a line break before the comment to improve readability.
gradle/libs.versions.toml
Outdated
@@ -70,6 +70,7 @@ commonsCompress = "1.27.1" | |||
# https://commons.apache.org/proper/commons-io/changes-report.html | |||
commonsIo = "2.19.0" | |||
coroutines = '1.10.2' | |||
datastorePreferences = '1.1.6' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest stable version is 1.1.7
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, Google updated it to 1.1.7 just last week. Fixed.
AnkiDroid/src/main/java/com/ichi2/anki/notifications/ReviewRemindersDatabase.kt
Outdated
Show resolved
Hide resolved
91dbce9
to
26c5e13
Compare
Had to do a second force push because I forgot to move the files to a new package as outlined at #18318. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this PR.
I admit I don't understand everything.
The main question for me really remain, why introducing dataStore. I'd really want a comment explaining what advantage it has. What limitation you had with the standard preferences we are currently using that caused you to want to use something else. I'd want it in the commit message adding the dependency to it.
|
||
/** | ||
* Review reminder data class. Handles the fields of a review reminders and the logic behind creating one. | ||
* Below, a public way of creating [ReviewReminder] is exposed via a companion object so that IDs are not abused. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean by "abused"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now clarified it a bit more. I was referring to developers creating overlapping IDs, creating IDs out of order, etc. In general we want IDs to increment in a nice orderly fashion without developer interference.
import kotlinx.serialization.Serializable | ||
|
||
/** | ||
* Review reminder data class. Handles the fields of a review reminders and the logic behind creating one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first sentence is not useful. You're just repeating the declaration. It's clear it's a data class named ReviewReminder
@ConsistentCopyVisibility | ||
data class ReviewReminder private constructor( | ||
val id: Int, | ||
val type: Int, // ReviewReminderTypes ordinal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you directly use the ReviewReminder? It seems that enum can be serialized too.
val type: Int, // ReviewReminderTypes ordinal | ||
val hour: Int, | ||
val minute: Int, | ||
val cardTriggerThreshold: Int, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'd really need to document that, it's not very clear unless one have read your proposal and remember it, or know the feature well
companion object { | ||
/** | ||
* Create a new review reminder. This will allocate a new ID for the reminder. | ||
* @param context The context to use for Datastore database access. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to add documentation for all params.
Context is used in all AnkiDroid, and I assume in all android app, it's self explanatory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I've now removed params annotations wherever I feel like they are self-explanatory.
val reminderToRemove = reminders.find { it.id == reminderId } | ||
if (reminderToRemove == null) { | ||
throw IllegalArgumentException("Delete Reminder: Reminder with ID $reminderId not found in deck $did") | ||
} | ||
reminders.remove(reminderToRemove) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe you can simplify that to
val removed = reminders.removeIf { it.id == reminderId }
if (!removed) {
throw IllegalArgumentException("Delete Reminder: Reminder with ID $reminderId not found in deck $did")
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've fully removed the addReviewReminder, editReviewReminder, and deleteReviewReminder methods and instead plan to move them to the onClick methods of the UI buttons for adding, editing, and deleting ReviewReminders respectively, since that's probably the only place adding, editing, and deleting code will ever be called anyways. Putting them into ReviewRemindersDatabase just makes ReviewRemindersDatabase more cluttered and messy in my opinion.
Thanks for reminding me about the removeIf method, though. I'll keep it in mind when I create deletion functionality.
* @param reminderId The ID of the reminder to delete. | ||
*/ | ||
suspend fun deleteReviewReminderForDeck( | ||
did: Long, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, use DeckId as a type. Here and everywhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, I'll make sure to do this every time going forward.
* @param did The deck ID. | ||
* @return The list of reminders. | ||
*/ | ||
suspend fun getRemindersForDeck(did: Long): List<ReviewReminder> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for it not to be private? I assume it's for test. If so, here and elsewhere, use VisibleForTest
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get
and set
functions are intentionally public, yes. I plan on using them from the ScheduleReminders UI.
reminderId: Int, | ||
updatedReminder: ReviewReminder, | ||
) { | ||
val reminders = getRemindersForDeck(did).toMutableList() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like you have a lot of code repetition here.
As you always use toMutableList
maybe the function should directly return a mutable list (and note that mutating the input returned won't be registered unless it's through setRemindersForDeck
)
You can also have an helper function that ensures as follow
private suspend fun editReminder(did: DeckId, reminderEditor: (MutableList<ReviewReminder> )->List<ReviewReminder>) {
setRemindersForDeck(did, reminderEditor(getRemindersForDeck(did)))
}
/**
* Add a review reminder for a specific deck.
* @param did The deck ID.
* @param newReminder The new review reminder to add.
*/
suspend fun addReviewReminderForDeck(
did: Long,
newReminder: ReviewReminder,
) {
editReminder(did) {reminders ->
reminders.add(newReminder)
return@editReminder reminders
}
}
And reuse editReminder
in both method below
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I like this a lot, thanks. Implemented!
@@ -0,0 +1,195 @@ | |||
/* | |||
* Copyright (c) 2025 Eric Li <[email protected]> | |||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe it's usually better to have test in the same commit as what is tested. At least if it's already written.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. I've merged all my commits together.
9dcf3d3
to
1f79657
Compare
Repeated force push because I forgot to rebase, my bad. |
Hi, I've refactored to use SharedPreferences instead of Preferences Datastore. In retrospect, thanks for telling me to use it, it really is a lot more simple and I should have started with this method. Using Datastore was like trying to crack a nut with a sledgehammer. The only negative consequence is that my branch I'm trying to merge these changes in from is still called Regarding other overly-complex points I've simplified: the fancy ID-allocation system is now gone, and I've decided to move the addReminderForDeck, editReminderForDeck, and deleteReminderForDeck methods to the UI onClick methods since that should be the only place they're called from, and so they don't need to be in ReviewRemindersDatabase. I've also done my best to clean up the docstrings, as per feedback. Previously they were more ad-hoc, now I've tried my best to make them genuine pieces of documentation. |
1f79657
to
1df1d45
Compare
I realized the SharedPreferences keys are too short. Since there's tons of other things being stored in SharedPreferences, it's probably a good idea to append |
1df1d45
to
c825609
Compare
I realized while I was building out the ScheduleReminders RecyclerView more that:
|
c825609
to
cb8bb87
Compare
Rebased and fixed a List that should have been a MutableList. |
AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewReminderTest.kt
Outdated
Show resolved
Hide resolved
cb8bb87
to
d73fe2f
Compare
d73fe2f
to
0c4180c
Compare
|
Please do not review this PR. I'm actively refactoring it as I've realized I can streamline the UX. Sorry for asking for reviews and then going back and changing things again, I really do appreciate the constructive criticism of my code and am sorry for pulling it out of the merge pipeline when it's so close to the end. |
0c4180c
to
fc1f2ab
Compare
Refactored. Removed ReviewReminderTypes and replaced it with snoozing. Looking forward to reviews! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solid niceee
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt
Outdated
Show resolved
Hide resolved
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt
Outdated
Show resolved
Hide resolved
AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewRemindersDatabase.kt
Outdated
Show resolved
Hide resolved
fc1f2ab
to
da51859
Compare
Thanks Ashish! I've added some try-catch blocks to handle corrupt JSON strings and added a few unit tests to test that behaviour. |
* Edit all [ReviewReminder]s that are associated with a specific deck by operating on a single mutable list. | ||
*/ | ||
fun editAllDeckSpecificReminders(reminderEditor: (MutableList<ReviewReminder>) -> List<ReviewReminder>) { | ||
val newReminders = reminderEditor(getAllDeckSpecificReminders()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getAllDeckSpecificReminders()
internally decodes every JSON string in SharedPreferences, and then inside reminderEditor, the flat list is returned, and must be re-grouped again.
So:
- Decodes once, which is nice
- But there's no grouped structure to begin with, and grouping happens only at the end, forcing repeated transformations.
What if we use this
fun editAllDeckSpecificReminders(
reminderEditor: (MutableList<ReviewReminder>) -> List<ReviewReminder>
) {
val existingRemindersGrouped = getAllDeckRemindersGrouped()
val updatedReminders = reminderEditor(existingRemindersGrouped.values.flatten().toMutableList())
val updatedGrouped = updatedReminders.groupBy { it.did }
val existingKeys = existingRemindersGrouped.keys.map { DECK_SPECIFIC_KEY + it }
context.sharedPrefs().edit {
existingKeys.forEach { remove(it) }
updatedGrouped.forEach { (did, reminders) ->
putString(DECK_SPECIFIC_KEY + did, Json.encodeToString(reminders))
}
}
}
private fun getAllDeckRemindersGrouped(): Map<DeckId, MutableList<ReviewReminder>> {
return context.sharedPrefs().all
.filterKeys { it.startsWith(DECK_SPECIFIC_KEY) }
.mapNotNull { (key, value) ->
val did = key.removePrefix(DECK_SPECIFIC_KEY).toLongOrNull() ?: return@mapNotNull null
val reminders = try {
Json.decodeFromString<MutableList<ReviewReminder>>(value.toString())
} catch (e: Exception) {
null
} ?: return@mapNotNull null
did to reminders
}
.toMap()
}
Here, SharedPreferences read + decode only happens once, and the grouping logic is explicit and ig Less parsing, and a cleaner transformation chain.
You may inspect more and reject this above suggestion if its not valid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I understand what you mean now. Sure, I'll do that, it is quite elegant.
da51859
to
1bb931d
Compare
|
1bb931d
to
2ca75ff
Compare
|
2ca75ff
to
e12c282
Compare
|
@ericli3690 |
val did: DeckId, | ||
var enabled: Boolean, | ||
) { | ||
init { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a single require
with a compound condition
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I think I'd actually like to keep the separate require
lines if possible. Throwing everything together into a single compound condition makes the code a bit harder to read.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't that make it so that if the require
fails, it's hard to tell why it failed? I think if we break it up over multiple lines, and one particular condition fails, then we get a more clear and specific error message.
val existingReminders = getRemindersForKey(key) | ||
val updatedReminders = reminderEditor(existingReminders) | ||
context.sharedPrefs().edit { | ||
putString(key, Json.encodeToString(updatedReminders)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add try-catch
around Json.encodeToString() in case it fails, so SharedPreferences doesn’t get into a bad state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point. Sure, I'll add that in to be safe, though I won't be able to add unit tests for it. The type-checking of reminderEditor
's contract prevents me from triggering a serialization error from these editing methods. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few thoughts, feel free to discard
* - [DISABLED]: The snooze button will never appear on notifications set by this review reminder. | ||
* @see SnoozeAmount | ||
*/ | ||
object SpecialSnoozeAmounts { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using a sealed class
to make a union (serializability untested):
@Serializable
sealed class SnoozeAmount {
data class Limited(val maxSnoozes: Int, val interval: Duration): SnoozeAmount()
/** The snooze button will always be available */
data object Infinite: SnoozeAmount()
/** The snooze button will never appear */
data object Disabled: SnoozeAmount()
}
* @param hour | ||
* @param minute | ||
* @param snoozeAmountAllowed Amount of times the user can hit snooze on the notification. See [SpecialSnoozeAmounts] for distinguished values. | ||
* @param snoozeTimeInterval Time interval between snoozes, in minutes. Unused if snoozesAllowed is a [SpecialSnoozeAmounts] value. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have expected this to be set for INFINITE
, I might be missing something from my mental model of the class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doh! Nope, you're right. Typo on my part.
val hour: Int, | ||
val minute: Int, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider combining Hour
and Minute
in a class, so invalid values can't be passed in
context.sharedPrefs().getInt( | ||
NEXT_FREE_ID_KEY, | ||
FIRST_REMINDER_ID, | ||
) | ||
context.sharedPrefs().edit { | ||
putInt(NEXT_FREE_ID_KEY, nextFreeId + 1) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in a previous review, look into the Prefs
class
@After | ||
override fun tearDown() { | ||
super.tearDown() | ||
// Reset the database after each test | ||
activity.sharedPrefs().edit { clear() } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, no, since there's only one test in this file right now. I added it in case someone else decides to write more tests for this file later, because they might forget to implement tearDown()
.
Thanks for the reviews. @SanjaySargam Regarding whether review reminders should remain in SharedPreferences after the corresponding deck is deleted: no, they should not, and don't worry, it's on my to-do list. Thanks for reminding me! |
GSoC 2025: Review Reminders - Created `ReviewRemindersDatabase`, which contains methods for storing review reminder data in SharedPreferences - Created `ReviewReminder`, which defines the review reminder data class - Added `ReviewRemindersDatabaseTest` and `ReviewReminderTest` to test these two classes - Added a new string preference to preferences.xml for saving the next free review reminder ID
e12c282
to
9d0e81e
Compare
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All looks VERY well documented!
import kotlin.time.Duration | ||
import kotlin.time.Duration.Companion.minutes | ||
|
||
internal typealias ReviewReminderId = Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
optional: look into value class
for a low/0-cost abstraction which stops an Int
being passed in where a ReviewReminderId
is required
.withLocale(Locale.getDefault()), | ||
) | ||
|
||
fun toMinutesFromMidnight(): Int = hour * 60 + minute |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may be able to do something with the Duration
API here, so you're working with a Duration
object, rather than an untyped number of minutes
fun elapsedSinceMidnight(): Duration = (hour.hours + minute.minutes)
val seconds = time.elapsedSinceMidnight().inWholeSeconds
/** | ||
* The "deck ID" field for review reminders that are app-wide rather than deck-specific. | ||
*/ | ||
const val APP_WIDE_REMINDER_DECK_ID = -1L |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may or may not want this as a sealed class, depending on how low-level this API will be
* @return The next free reminder ID. | ||
*/ | ||
private fun getAndIncrementNextFreeReminderId(): ReviewReminderId { | ||
val nextFreeId = Prefs.getInt(R.string.review_reminders_next_free_id, FIRST_REMINDER_ID) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look into by intPref
. Use this new property as an opportunity to document the preference
Index: AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt (revision 9d0e81e75ce77a0d7ce8949b5344af2318389b23)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ReviewReminder.kt (date 1750647615025)
@@ -16,7 +16,7 @@
package com.ichi2.anki.reviewreminders
-import com.ichi2.anki.R
+import com.ichi2.anki.reviewreminders.ReviewReminder.Companion.APP_WIDE_REMINDER_DECK_ID
import com.ichi2.anki.settings.Prefs
import com.ichi2.libanki.DeckId
import kotlinx.serialization.Serializable
@@ -131,11 +131,6 @@
*/
const val APP_WIDE_REMINDER_DECK_ID = -1L
- /**
- * IDs start at this value and climb upwards by one each time.
- */
- private const val FIRST_REMINDER_ID = 0
-
/**
* Create a new review reminder. This will allocate a new ID for the reminder.
* @return A new [ReviewReminder] object.
@@ -163,8 +158,8 @@
* @return The next free reminder ID.
*/
private fun getAndIncrementNextFreeReminderId(): ReviewReminderId {
- val nextFreeId = Prefs.getInt(R.string.review_reminders_next_free_id, FIRST_REMINDER_ID)
- Prefs.putInt(R.string.review_reminders_next_free_id, nextFreeId + 1)
+ val nextFreeId = Prefs.reviewReminderNextFreeId
+ Prefs.reviewReminderNextFreeId = nextFreeId + 1
Timber.d("Generated next free review reminder ID: $nextFreeId")
return nextFreeId
}
Index: AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt (revision 9d0e81e75ce77a0d7ce8949b5344af2318389b23)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt (date 1750647582518)
@@ -204,4 +204,6 @@
var isDevOptionsEnabled: Boolean
get() = getBoolean(R.string.dev_options_enabled_by_user_key, false) || BuildConfig.DEBUG
set(value) = putBoolean(R.string.dev_options_enabled_by_user_key, value)
+
+ var reviewReminderNextFreeId by intPref(R.string.review_reminders_next_free_id, defaultValue = 0)
}
/** | ||
* Get and return the next free reminder ID which can be associated with a new review reminder. | ||
* Also increment the next free reminder ID stored in SharedPreferences. | ||
* Since there are 4 billion IDs available, this should not overflow in practice. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Since there are 4 billion IDs available, this should not overflow in practice. |
// If, during development, the review reminders storage schema (i.e. ReviewReminder) is modified, | ||
// this class will attempt to read review reminders stored in the old schema via the new schema, causing a SerializationException. | ||
// To delete all review reminder keys in SharedPreferences, uncomment the following line. | ||
// AnkiDroidApp.sharedPrefs().edit { AnkiDroidApp.sharedPrefs().all.keys.filter { it.startsWith("review_reminders_") }.forEach { remove(it) } } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this discussed and confirmed with mentors? We may want to use a separate file for these
- This will significantly increase the on-disk size of the default
SharedPreferences
- maybe to the point of affecting app loading times - We'll want to exclude this data from ACRA
private fun decodeJson(jsonString: String): HashMap<ReviewReminderId, ReviewReminder> = | ||
try { | ||
// If, during development, the review reminders storage schema (i.e. ReviewReminder) is modified, | ||
// this class will attempt to read review reminders stored in the old schema via the new schema, causing a SerializationException. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll want to plan how to update the schema
Purpose / Description
Review Reminders database system.
ReviewRemindersDatabase
, which contains methods for reading and writing to a Preferences Datastore instance for storing review reminder dataReviewReminder
, which defines the review reminder data classReviewRemindersDatabase
andReviewReminder
Fixes
Approach
Using Preferences Datastore was the original method I proposed for storing review reminder data in my original project proposal: see [1]
Why did I initially choose Preferences Datastore for storing review reminder data?
However, we've now chosen to use SharedPreferences instead. It's consistent with what the rest of AnkiDroid uses and doesn't necessitate a whole new import. It's also a lot simpler and straightforward.
How Has This Been Tested?
ReviewRemindersDatabaseTest
directly or indirectly exercises all methods withinReviewRemindersDatabase
.ReviewReminderTest
exercises the ID allocation method ofReviewReminder
Learning
I've tried my best to make my code clean and safe. In particular, allocating and deallocating IDs is handled internally and any developer creating a new
ReviewReminder
should be unable to manually forge an ID.Originally, I planned to create different types of review reminders. I've now abandoned that in favour of a simpler approach involving snooze functionality.
Checklist