Skip to content

refactor: use MenuProvider in StudyOptionsActivity, remove starting reviewer in undo#21339

Open
ericli3690 wants to merge 12 commits into
ankidroid:mainfrom
ericli3690:ericli3690-study-options-activity-undo-menu
Open

refactor: use MenuProvider in StudyOptionsActivity, remove starting reviewer in undo#21339
ericli3690 wants to merge 12 commits into
ankidroid:mainfrom
ericli3690:ericli3690-study-options-activity-undo-menu

Conversation

@ericli3690

@ericli3690 ericli3690 commented Jul 4, 2026

Copy link
Copy Markdown
Member

Warning

Note

Assisted-by: Claude Sonnet 4.5

Purpose / Description

  • Split off from feat(reminders): streamline review reminders UI #21140
  • StudyOptionsActivity is currently using onOptionsItemSelected etc. This PR migrates it to the modern MenuProvider interface.
  • Also, addresses a TODO comment left a while ago by lukstbit: I think it's safe to remove the call to open the reviewer:
                     // TODO why are we going to the Reviewer from here? Desktop doesn't do this
                    Reviewer
                        .getIntent(this@StudyOptionsActivity)
                        .apply { flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT }
                        .also { startActivity(it) }
                    finish()

Fixes

Originally created as a response to one of David's comments:

Optional/for a TODO/ignore this
Take a look at CardBrowser + CardBrowserFragment's use of `MenuProvider
Would this simplify the logic? Make one provider for each state: fragmented and non-fragmented, and the fragments control their menus

// Update the menu for a fragmented state
// Added last so all changes take priority
onAllFragmentsLoaded {
addPrepareMenuProvider { menu ->
if (!fragmented) return@addPrepareMenuProvider
/** Return the menu item with a particular identifier or `null` */
operator fun Menu.get(id: Int): MenuItem? = this.findItem(id)
// NoteEditorFragment: Remove save/preview note options if there are no notes
if (viewModel.rowCount == 0) {
menu[R.id.action_save]?.isVisible = false
menu[R.id.action_preview]?.isVisible = false
}
menu[R.id.action_edit_note]?.isVisible = false
// TODO: https://github.com/ankidroid/Anki-Android/issues/20206
// This blocks a user from previewing all cards
menu[R.id.action_preview_many]?.isVisible = false
}

See below for discussion of David's concern above.

Approach

  • Simple refactor and removal of the reviewer-opening line

How Has This Been Tested?

  • No regressions on tablets, wide screens, including DeckPicker back button and undo button
  • Back button works on StudyOptionsActivity
  • Undo button works on StudyOptionsActivity
  • Undo does not start reviewer anymore; if, for instance, the option to be undone is a deck selection, pressing undo navigates to the deck that was previously selected rather than opening the reviewer
  • Physical Samsung S23, API 36

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

@ericli3690 ericli3690 self-assigned this Jul 4, 2026
@ericli3690 ericli3690 force-pushed the ericli3690-study-options-activity-undo-menu branch from 39d6ae5 to 9313e44 Compare July 4, 2026 05:38
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Snapshot diff report vs main. Open screenshot-diff for diffs.

  • AddEditReminderDialogScreenshotTest: 4 changes
  • PreferencesScreenshotTest: 1 change
  • ReviewRemindersScreenshotTest: 11 changes
All 16 changed screenshots

AddEditReminderDialogScreenshotTest

  • add_mode_advanced_open_compare.png
  • add_mode_compare.png
  • edit_mode_advanced_open_compare.png
  • edit_mode_compare.png

PreferencesScreenshotTest

  • ScheduleRemindersFragment_compare.png

ReviewRemindersScreenshotTest

  • settingsHostTablet_scheduleReminders_compare.png
  • settingsHostTablet_troubleshooting_compare.png
  • settingsHost_scheduleReminders_compare.png
  • settingsHost_scheduleReminders_scrolled_compare.png
  • settingsHost_troubleshooting_compare.png
  • standaloneActivityHost_scheduleReminders_compare.png
  • standaloneActivityHost_troubleshooting_compare.png
  • studyOptionsFragmentHost_scheduleReminders_compare.png
  • studyOptionsFragmentHost_troubleshooting_compare.png
  • studyOptionsFrameHost_scheduleReminders_compare.png
  • studyOptionsFrameHost_troubleshooting_compare.png

@ericli3690 ericli3690 marked this pull request as ready for review July 4, 2026 06:10
@ericli3690 ericli3690 added Needs Review Blocked by dependency Currently blocked by some other dependent / related change labels Jul 4, 2026
@ericli3690

Copy link
Copy Markdown
Member Author

@david-allison This is a follow up addressing your comment here: #21140 (comment)

Or at least, that's what this PR was initially supposed to be. I don't think I read your comment carefully enough at first haha and I ended up just doing a little refactor cleanup before realizing that you wanted to separate out the undo button into... the fragments? Rather than the activity? Perhaps I'm misunderstanding you.

Your original comment annotated StudyOptionsActivity and mentions showing the undo button conditionally by checking whether the activity is fragmented rather than by doing && (currentFragment is StudyOptionsFragment). The problem is that StudyOptionsActivity doesn't have a fragmented mode. It only shows up on narrow screens and takes up the full screen. When StudyOptionsFragment is in a side panel, the host activity is DeckPicker. Thus it makes no sense for StudyOptionsActivity to check if it is "fragmented": it's always not fragmented.

Perhaps what you mean instead is that we should have StudyOptionsFragment own the undo button rather than having StudyOptionsActivity own it? But that then causes problems when StudyOptionsFragment is in a right-side panel on wide screens, as its undo button ID will collide with the DeckPicker activity's undo button ID, even if we hide one of them. What we must do is use a completely different ID rather than action_undo for the StudyOptionsFragment undo button and programmatically hide it when StudyOptionsFragment is fragmented and on the right side on a wide screen. Feels a bit clunky to me, but I can do it if you'd like. I would not be able to use onAllFragmentsLoaded as you originally suggested if we implement it this way, because that requires a FragmentActivity receiver.

I think the original reason why the undo button is located in StudyOptionsActivity is because it's mutually exclusive with DeckPicker's undo button (they're different activities) so we don't need any complex fragmentation checks if we just have one undo button in DeckPicker's menu and one undo button in StudyOptionsActivity's menu. If we move it to StudyOptionsFragment, then all of a sudden we need fragmentation checks.

Come to think of it, why does StudyOptionsActivity need an undo button anyways? The only use I see is for undoing custom study choices like increasing the today's new cards... actually, I guess that's useful, nevermind.

Perhaps the best decision is just what the UI streamlining PR already has? I.e.:

undoMenuItem.isVisible = undoState.hasAction && (currentFragment is StudyOptionsFragment)

The above comment is a bit stream-of-consciousness, sorry about that, but would love to hear your thoughts. Even if we do nothing to address the && (currentFragment is StudyOptionsFragment) code smell, this PR is (I believe) a nice little refactor and eliminates some weird behaviour upon pressing undo.

@ericli3690 ericli3690 requested a review from david-allison July 4, 2026 06:25
R.id.action_undo -> {
launchCatchingTask {
undoAndShowSnackbar()
// TODO why are we going to the Reviewer from here? Desktop doesn't do this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This comment was originally added by @lukstbit at dd4c35d. The code to open the reviewer was initially added in 3e7ad6a9e6. I see no reason to keep it. The behaviour of the undo button seems simpler if we just... only perform the undo. The desktop reviewer doesn't open the reviewer when undo is pressed from the study options view, so lukstbit's point is valid.

ericli3690 added 12 commits July 4, 2026 21:28
After the review reminders UI refactoring changes that come after this commit, there is a chance that when the troubleshooting screen is rotated twice in quick succession from within the settings screen that it crashes due to removeOnWindowFocusChangeListener trying to run on a detached viewTreeObserver. This change fixes this by only removing the listener if it is existent and alive.
Previously, there was a horizontal divider between the toolbar and review reminders list / troubleshooting menu, along with horizontal dividers between each individual review reminder listing. I think this should be removed to bring the screen more in line stylistically with how the rest of the settings menu looks (there are no dividers between Preferences, nor between the toolbar and primary content).
The AddEditReminderDialog's TimePicker breaks and fails to save information if rotated in some scenarios after the upcoming commits which rejiggle the review reminders UI are implemented. This is due to latent issues in the TimePicker component. See the TODO comment and docstrings for more details.
A future change will involve setting the subtitle of StudyOptionsActivity from a fragment nested inside it (ScheduleRemindersFragment, ReminderTroubleshootingFragment). Hence, we modify the contract of the pre-existing setToolbarText methods in AnkiActivity, which is inherited by StudyOptionsActivity.
…Activity

StudyOptionsActivity was given explicit handling for orientation changes wayyyy back in 2012 to solve a bug: ankidroid@905fd21

But it's no longer needed today, and in fact keeping it causes issues with when StudyOptionsActivity hosts review reminder UI in an upcoming commit.

In particular, notice that the onConfigurationChanged method in the linked commit is no longer present in StudyOptionsActivity.
…n host

ScheduleReminders can be hosted in multiple places, each of which presents their own unique UI challenges:

1. Long-pressing a deck from DeckPicker: On small screens, there is no suitable activity currently available, and so we should launch one to host ScheduleRemindersFragment in the form of SingleFragmentActivity. On large screens, there MAY be a right-side panel available (it is hidden in the edge case where the collection is empty); the UI looks cleanest if it is used.
2. Clicking on the "bell" icon from StudyOptionsFragment: On small screens, StudyOptionsActivity is currently active, and so it should be used as the host rather than launching a new activity. However, it manages an external toolbar and so using an internal toolbar would not work. We need to latch onto the external support action bar and modify it. On large screens, the right-side panel is available similar to option 1 if the collection is non-empty.
3. The screen may be launched from Settings, in which case the user is inspecting all reminders across all decks. Here, it should match the style of the other Settings Preferences screens, i.e. a collapsing toolbar, and should align whether on a small screen or large screen.

In my opinion, the cleanest way to handle this is to enumerate the possible hosts ScheduleReminders can have via FragmentHost, define what the ToolbarType should be for each host, and then modify the toolbar and styling appropriately. This keeps all logic centralized within ScheduleRemindersFragment and maintains a single source of truth.

Additionally, due to an issue with `fitsSystemWindows`, we sometimes need to set the upper padding programmatically. See the TODO comment in this commit. Also, as edge-to-edge is gradually implemented, fitsSystemWindows will stop being necessary and some of the UI code here will be not needed. This is the case, for instance, in DeckPicker. Hence we must also record whether a host has edge-to-edge enabled. See the TODO comment.

Since ReminderTroubleshootingFragment is launched from ScheduleRemindersFragment, it also has similar UI issues and also needs to be aware of its host.

Proper launching of ReminderTroubleshootingFragment is also implemented. A subtitle style is added.
Adds a FragmentManager.showDialogFragment extension function. The normal showDialogFragment function is an extension on FragmentActivity and attaches the dialog to the supportFragmentManager, which is activity-scoped. This:

1. Causes issues when ScheduleRemindersFragment is hosted within Settings, as Preferences.kt launches its children within its childFragmentManager, meaning that AddEditReminderDialog's setFragmentResultListener needs to be changed to `requireActivity().supportFragmentManager.setFragmentResultListener`.
2. However, even though the above is implementable and works (I tried it), it is kind of bad because we've attached the listener to the entire activity. If the ScheduleRemindersFragment dies and thus there is no "correct" place for AddEditReminderDialog to return, the listener is technically still alive on the activity. This feels wrong.

A better solution in my opinion is to attach the AddEditReminderDialog result listeners to the childFragmentManager of ScheduleRemindersFragment. While this can be done via a direct call to showDialogFragmentImpl, I thought calling a function with the word "Impl" in it was a bit ugly and so created an extension function.
Deletes old "search" override code in the Settings screen. Adds a new preferences XML file solely for indexing purposes, but directs the HeaderFragment to simply open ScheduleRemindersFragment on click.
… StudyOptionsFragment

Adds logic for opening ScheduleRemindersFragment in the correct fragment container hosts in DeckPicker and StudyOptionsFragment. Mirrors `openStudyOptions` and `tryShowStudyOptionsPanel` by adding `openScheduleReminders` and `tryShowScheduleRemindersPanel`. Reloads the StudyOptionsFragment and removes ScheduleRemindersFragment if an appbar menu button is clicked while the fragment is open on large screens to prevent stale state from showing.

Assisted-by: Composer 2.5 Fast [only for debugging test failure, Flow.kt]
Yanks the troubleshooting snackbar setup code into a helper function to declutter `onViewCreated` a bit.
Screenshot tests for the two key review reminder UI interfaces. In particular, making sure the ScheduleRemindersFragment looks good no matter where it is opened from. As edits to the UI accumulate over the coming years, we don't want ScheduleRemindersFragment to silently regress in one of the four hosts.

Instrumented tests coming soon.

Assisted-by: Claude Sonnet 4.6 Medium [Initial test drafts]
…eviewer in undo

Move to using the modern MenuProvider system for the undo button in StudyOptionsActivity. Also, removed starting the reviewer when undo is clicked, addressing a pending TODO comment. It was initially added here ankidroid@3e7ad6a9e6 but doesn't match Desktop behavior. I think it's probably safe to delete, tested it and I see now issues.

Assisted-by: Claude Sonnet 4.5
@ericli3690 ericli3690 force-pushed the ericli3690-study-options-activity-undo-menu branch from 9313e44 to 34fac85 Compare July 5, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Blocked by dependency Currently blocked by some other dependent / related change Needs Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant