Skip to content

Commit f473efc

Browse files
committed
feat(reminders): integrate AddEditReminderDialog into ScheduleReminders
GSoC 2025: Review Reminders - Adds a fragment result listener to ScheduleReminders to detect when an AddEditReminderDialog has completed, and if so, edits the database and UI accordingly. - Filled out the `addReminder` and `editReminder` methods to show the AddEditReminderDialog.
1 parent af180b5 commit f473efc

File tree

1 file changed

+155
-2
lines changed

1 file changed

+155
-2
lines changed

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

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import androidx.appcompat.app.AppCompatActivity
2525
import androidx.core.os.BundleCompat
2626
import androidx.fragment.app.Fragment
2727
import androidx.fragment.app.setFragmentResult
28+
import androidx.fragment.app.setFragmentResultListener
2829
import androidx.recyclerview.widget.DividerItemDecoration
2930
import androidx.recyclerview.widget.LinearLayoutManager
3031
import androidx.recyclerview.widget.RecyclerView
@@ -38,6 +39,10 @@ import com.ichi2.anki.launchCatchingTask
3839
import com.ichi2.anki.libanki.DeckId
3940
import com.ichi2.anki.model.SelectableDeck
4041
import com.ichi2.anki.showError
42+
import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
43+
import com.ichi2.anki.snackbar.SnackbarBuilder
44+
import com.ichi2.anki.snackbar.showSnackbar
45+
import com.ichi2.anki.utils.ext.showDialogFragment
4146
import com.ichi2.anki.withProgress
4247
import kotlinx.serialization.SerializationException
4348
import timber.log.Timber
@@ -47,7 +52,8 @@ import timber.log.Timber
4752
*/
4853
class ScheduleReminders :
4954
Fragment(R.layout.fragment_schedule_reminders),
50-
DeckSelectionDialog.DeckSelectionListener {
55+
DeckSelectionDialog.DeckSelectionListener,
56+
BaseSnackbarBuilderProvider {
5157
/**
5258
* Whether this fragment has been opened to edit all review reminders or just a specific deck's reminders.
5359
* @see ReviewReminderScope
@@ -64,6 +70,10 @@ class ScheduleReminders :
6470
private lateinit var recyclerView: RecyclerView
6571
private lateinit var adapter: ScheduleRemindersAdapter
6672

73+
override val baseSnackbarBuilder: SnackbarBuilder = {
74+
anchorView = requireView().findViewById<ExtendedFloatingActionButton>(R.id.schedule_reminders_add_reminder_fab)
75+
}
76+
6777
/**
6878
* The reminders currently being displayed in the UI. To make changes to this list show up on screen,
6979
* use [triggerUIUpdate]. Note that editing this map does not also automatically write to the database.
@@ -110,6 +120,26 @@ class ScheduleReminders :
110120

111121
// Retrieve reminders based on the editing scope
112122
launchCatchingTask { loadDatabaseRemindersIntoUI() }
123+
124+
// If the user creates or edits a review reminder, the dialog for doing so opens
125+
// Once their changes are complete, the dialog closes and this fragment is reloaded
126+
// Hence, we check for any fragment results here and update the database accordingly
127+
setFragmentResultListener(ADD_EDIT_DIALOG_RESULT_REQUEST_KEY) { _, bundle ->
128+
val modeOfFinishedDialog =
129+
BundleCompat.getParcelable(
130+
requireArguments(),
131+
ACTIVE_DIALOG_MODE_ARGUMENTS_KEY,
132+
AddEditReminderDialog.DialogMode::class.java,
133+
) ?: return@setFragmentResultListener
134+
val newOrModifiedReminder =
135+
BundleCompat.getParcelable(
136+
bundle,
137+
ADD_EDIT_DIALOG_RESULT_REQUEST_KEY,
138+
ReviewReminder::class.java,
139+
)
140+
Timber.d("Dialog result received with recent dialog mode: %s", modeOfFinishedDialog)
141+
handleAddEditDialogResult(newOrModifiedReminder, modeOfFinishedDialog)
142+
}
113143
}
114144

115145
private fun reloadToolbarText() {
@@ -142,6 +172,112 @@ class ScheduleReminders :
142172
Timber.d("Database review reminders successfully loaded")
143173
}
144174

175+
/**
176+
* When a [AddEditReminderDialog] instance finishes, we handle the result of the dialog fragment via this method.
177+
*/
178+
private fun handleAddEditDialogResult(
179+
newOrModifiedReminder: ReviewReminder?,
180+
modeOfFinishedDialog: AddEditReminderDialog.DialogMode,
181+
) {
182+
Timber.d("Handling add/edit dialog result: mode=%s reminder=%s", modeOfFinishedDialog, newOrModifiedReminder)
183+
updateDatabaseForAddEditDialog(newOrModifiedReminder, modeOfFinishedDialog)
184+
updateUIForAddEditDialog(newOrModifiedReminder, modeOfFinishedDialog)
185+
// Feedback
186+
showSnackbar(
187+
when (modeOfFinishedDialog) {
188+
is AddEditReminderDialog.DialogMode.Add -> "Successfully added new review reminder"
189+
is AddEditReminderDialog.DialogMode.Edit -> {
190+
when (newOrModifiedReminder) {
191+
null -> "Successfully deleted review reminder"
192+
else -> "Successfully edited review reminder"
193+
}
194+
}
195+
},
196+
)
197+
}
198+
199+
/**
200+
* Write the new or modified reminder to the database.
201+
* @see handleAddEditDialogResult
202+
*/
203+
private fun updateDatabaseForAddEditDialog(
204+
newOrModifiedReminder: ReviewReminder?,
205+
modeOfFinishedDialog: AddEditReminderDialog.DialogMode,
206+
) {
207+
launchCatchingTask {
208+
catchDatabaseExceptions {
209+
if (modeOfFinishedDialog is AddEditReminderDialog.DialogMode.Edit) {
210+
// Delete the existing reminder if we're in edit mode
211+
// This action must be separated from writing the modified reminder because the user may have updated the reminder's deck,
212+
// meaning we need to delete the old reminder in the old deck, then add a new reminder to the new deck
213+
val reminderToDelete = modeOfFinishedDialog.reminderToBeEdited
214+
Timber.d("Deleting old reminder from database")
215+
when (reminderToDelete.scope) {
216+
is ReviewReminderScope.Global -> ReviewRemindersDatabase.editAllAppWideReminders(deleteReminder(reminderToDelete))
217+
is ReviewReminderScope.DeckSpecific ->
218+
ReviewRemindersDatabase.editRemindersForDeck(
219+
reminderToDelete.scope.did,
220+
deleteReminder(reminderToDelete),
221+
)
222+
}
223+
}
224+
newOrModifiedReminder?.let { reminder ->
225+
Timber.d("Writing new or modified reminder to database")
226+
when (reminder.scope) {
227+
is ReviewReminderScope.Global -> ReviewRemindersDatabase.editAllAppWideReminders(upsertReminder(reminder))
228+
is ReviewReminderScope.DeckSpecific ->
229+
ReviewRemindersDatabase.editRemindersForDeck(
230+
reminder.scope.did,
231+
upsertReminder(reminder),
232+
)
233+
}
234+
}
235+
}
236+
}
237+
}
238+
239+
/**
240+
* Lambda that can be fed into [ReviewRemindersDatabase.editRemindersForDeck] or
241+
* [ReviewRemindersDatabase.editAllAppWideReminders] which deletes the given review reminder.
242+
*/
243+
private fun deleteReminder(reminder: ReviewReminder) =
244+
{ reminders: HashMap<ReviewReminderId, ReviewReminder> ->
245+
reminders.remove(reminder.id)
246+
reminders
247+
}
248+
249+
/**
250+
* Lambda that can be fed into [ReviewRemindersDatabase.editRemindersForDeck] or
251+
* [ReviewRemindersDatabase.editAllAppWideReminders] which updates the given review reminder if it
252+
* exists or inserts it if it doesn't (an "upsert" operation)
253+
*/
254+
private fun upsertReminder(reminder: ReviewReminder) =
255+
{ reminders: HashMap<ReviewReminderId, ReviewReminder> ->
256+
reminders[reminder.id] = reminder
257+
reminders
258+
}
259+
260+
/**
261+
* Update the RecyclerView with the new or modified reminder.
262+
* @see handleAddEditDialogResult
263+
*/
264+
private fun updateUIForAddEditDialog(
265+
newOrModifiedReminder: ReviewReminder?,
266+
modeOfFinishedDialog: AddEditReminderDialog.DialogMode,
267+
) {
268+
if (modeOfFinishedDialog is AddEditReminderDialog.DialogMode.Edit) {
269+
Timber.d("Deleting old reminder from UI")
270+
reminders.remove(modeOfFinishedDialog.reminderToBeEdited.id)
271+
}
272+
newOrModifiedReminder?.let {
273+
if (scheduleRemindersScope == ReviewReminderScope.Global || scheduleRemindersScope == it.scope) {
274+
Timber.d("Adding new reminder to UI")
275+
reminders[it.id] = it
276+
}
277+
}
278+
triggerUIUpdate()
279+
}
280+
145281
/**
146282
* Sets a TextView's text based on a [ReviewReminderScope].
147283
* The text is either the scope's associated deck's name, or "All Decks" if the scope is global.
@@ -152,7 +288,7 @@ class ScheduleReminders :
152288
view: TextView,
153289
) {
154290
when (scope) {
155-
is ReviewReminderScope.Global -> view.text = "All Decks"
291+
is ReviewReminderScope.Global -> view.text = getString(R.string.card_browser_all_decks)
156292
is ReviewReminderScope.DeckSpecific -> {
157293
launchCatchingTask {
158294
val deckName = cachedDeckNames.getOrPut(scope.did) { scope.getDeckName() }
@@ -202,6 +338,11 @@ class ScheduleReminders :
202338
*/
203339
private fun addReminder() {
204340
Timber.d("Adding new review reminder")
341+
val dialogMode = AddEditReminderDialog.DialogMode.Add(scheduleRemindersScope)
342+
val dialog = AddEditReminderDialog.getInstance(dialogMode)
343+
// Save the dialog mode so that we refer back to it once the dialog closes
344+
requireArguments().putParcelable(ACTIVE_DIALOG_MODE_ARGUMENTS_KEY, dialogMode)
345+
showDialogFragment(dialog)
205346
}
206347

207348
/**
@@ -210,6 +351,11 @@ class ScheduleReminders :
210351
*/
211352
private fun editReminder(reminder: ReviewReminder) {
212353
Timber.d("Editing review reminder: %s", reminder.id)
354+
val dialogMode = AddEditReminderDialog.DialogMode.Edit(reminder)
355+
val dialog = AddEditReminderDialog.getInstance(dialogMode)
356+
// Save the dialog mode so that we refer back to it once the dialog closes
357+
requireArguments().putParcelable(ACTIVE_DIALOG_MODE_ARGUMENTS_KEY, dialogMode)
358+
showDialogFragment(dialog)
213359
}
214360

215361
/**
@@ -266,8 +412,15 @@ class ScheduleReminders :
266412
*/
267413
const val DECK_SELECTION_RESULT_REQUEST_KEY = "reminder_deck_selection_result_request_key"
268414

415+
/**
416+
* TODO: Move to string resources for translation once review reminders are stable.
417+
*/
269418
private const val SERIALIZATION_ERROR_MESSAGE =
270419
"Something went wrong. A serialization error was encountered while working with review reminders."
420+
421+
/**
422+
* TODO: Move to string resources for translation once review reminders are stable.
423+
*/
271424
private const val DATA_TYPE_ERROR_MESSAGE =
272425
"Something went wrong. An unexpected data type was found while working with review reminders."
273426

0 commit comments

Comments
 (0)