Skip to content

feat(reminders): request notification permissions#19167

Merged
david-allison merged 13 commits intoankidroid:mainfrom
ericli3690:ericli3690-review-reminders-permission-request
Jan 9, 2026
Merged

feat(reminders): request notification permissions#19167
david-allison merged 13 commits intoankidroid:mainfrom
ericli3690:ericli3690-review-reminders-permission-request

Conversation

@ericli3690
Copy link
Member

@ericli3690 ericli3690 commented Aug 31, 2025

Purpose / Description

Creates a BottomSheet for requesting notification permissions from the user. This BottomSheet shows up when the user first triggers a sync and when the user first creates a review reminder, as these are the two features that rely on notifications. Also refactors some of the backend Permissions logic to ensure we don't try to launch the system UI permissions request dialog when we know it will fail (i.e. when the user has conclusively denied us a permission), as that might decrease our Play Store discoverability.

For more details, read the extended discussion below or the commit messages, which I've tried to make as detailed as possible.

UI

image image

Fixes

Approach

  • Creates a new PermissionsBottomSheet. My intention is: mandatory permissions -> use PermissionsActivity; optional permissions -> use PermissionsBottomSheet. I think the BottomSheet is less intrusive compared to the full-screen PermissionsActivity, as befits an optional permission.
  • For more details on thoughts and reasoning, see the detailed discussion in this PR thread.

How Has This Been Tested?

  • Physical Samsung S23, API 34.
  • Medium Phone, API 35.
  • A fresh install of the app does not trigger the notifications request dialog upon logging in (sometimes LeakCanary will cause the system UI dialog to show up, but that's unrelated to our codebase)
  • Syncing for the first time causes the BottomSheet to show up. Clicking on the button triggers the system UI dialog or directs the user to Settings instead if anything goes wrong.
  • Adding the first review reminder also causes the BottomSheet to show up, as above.
  • Rejecting notification permissions twice in a row causes the BottomSheet dialog to never show up again
  • Can register for notifications through the review reminders screen or the sync user flow
  • PermissionsExplanationFragment still works as expected and has full functionality
  • Clicking on a permission toggle when it is enabled offers the ability to revoke it from the OS settings, like before
  • Audio permission requesting in PermissionsExplanationFragment also safely falls back to opening the OS settings screen after two rejections
  • Viewing a permission request BottomSheet and dismissing it without pressing the toggle does not count as "denying" the permission and future BottomSheets will still trigger.

Learning (optional, can help others)

  • Spent a lot of time stuck on a bug involving fragmentManagers, but managed to figure it out.
  • Curse shouldShowPermissionRequestRationale!!

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
  • UI changes: include screenshots of all affected screens (in particular showing any new or changed strings)
  • UI Changes: You have tested your change using the Google Accessibility Scanner

@github-actions
Copy link
Contributor

Important

Maintainers: This PR contains Strings changes

  1. Sync Translations before merging this PR and wait for the action to complete
  2. Review and merge the auto-generated PR in order to sync all user-submitted translations
  3. Sync Translations again and merge the PR so the huge automated string changes caused by merging this PR are by themselves and easy to review

@ericli3690 ericli3690 added Needs Review Blocked by dependency Currently blocked by some other dependent / related change GSoC Pull requests authored by a Google Summer of Code participant [Candidate/Selected], for GSoC mentors labels Aug 31, 2025
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch 2 times, most recently from 37e535a to e53a1b6 Compare August 31, 2025 16:10
@ericli3690
Copy link
Member Author

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch from e53a1b6 to e8b030c Compare September 6, 2025 05:36
@ericli3690

This comment was marked as resolved.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch from e8b030c to d11214a Compare September 6, 2025 19:34
@ericli3690 ericli3690 removed the Strings label Sep 6, 2025
@ericli3690
Copy link
Member Author

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch from d11214a to d611d76 Compare September 9, 2025 03:01
@ericli3690

This comment was marked as resolved.

@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch from d611d76 to 0703e5d Compare September 10, 2025 19:26
@ericli3690
Copy link
Member Author

@ericli3690 ericli3690 marked this pull request as draft September 26, 2025 04:11
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch 2 times, most recently from 87fb916 to 8a3a21d Compare October 2, 2025 17:44
@ericli3690 ericli3690 marked this pull request as ready for review October 2, 2025 18:00
@ericli3690 ericli3690 added Needs Review and removed Blocked by dependency Currently blocked by some other dependent / related change Has Conflicts labels Oct 2, 2025
@ericli3690
Copy link
Member Author

Dependencies have been merged. Rebased and ready for review!

@david-allison david-allison added the Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) label Jan 8, 2026
As discussed in Discord, we have decided to use the PermissionsActivity which shows up when the app is initially installed to request mandatory permissions from the user. For optional permissions, we have decided to try and request them in a less intrusive way. Here, I've created a PermissionsBottomSheet that can pop up and request optional permissions from the user. I use this bottom sheet in a later commit for requesting notification permissions.

I've modeled PermissionsBottomSheet after PermissionsActivity. I'm fairly sure I cannot reuse PermissionsActivity's code because it is a full-screen activity, whereas PermissionsBottomSheet needs to inherit BottomSheetFragment and be a fragment.
GSoC 2025: Review Reminders

Adds two new boolean Preferences for saving whether we have requested notification permissions from the user: one for whether it has been done for the sync process yet (we will request notification permissions the first time the user logs in to an account, as only then can they trigger a sync) and one for whether it has been done for the review reminders feature yet (we will request notification permissions the first time the user creates a review reminder).

As discussed extensively at [19167](ankidroid#19167), `shouldShowRequestPermissionRationale` is unable to distinguish between first-time permission requests and permission requests after conclusive user rejection. See [this post](https://stackoverflow.com/questions/41310510/what-is-the-difference-between-shouldshowrequestpermissionrationale-and-requestp) for more information. To make up for this shortfall in Android API functionality, we need to store some state ourselves. In particular, we need to remember if the user has seen the permission request BottomSheet for specific reasons and if the user has seen the system UI dialog for requesting permissions for specific permissions - the former to ensure we don't spam the user with BottomSheets if they've already denied a permission for a specific feature, and the latter to ensure we don't trigger the system UI permission request dialog if the user has already conclusively rejected a permission. Triggering the system UI dialog after the user has conclusively rejected a permission may lead to lower Play Store discoverability. This is also more extensible in the future, should a future developer choose to request the notification permission in a different circumstance, too.

I have decided to request the permission once for the sync feature and once for the review reminder feature because if the user denies the permission for the sync feature and then goes to enable review reminders, they may change their mind about providing AnkiDroid with notification permissions; and vice versa.

If the permission is denied, I have chosen not to downgrade any review reminders functionality: the user will still be able to create, edit, and delete them. However, they will not receive any notifications. I do not believe it makes sense to restrict the user's ability to edit review reminders even though they have notifications disabled, as a user might want to customize their reminders to their liking before enabling notifications, etc. However, I will add a small notification box to inform the user that notifications are disabled when they are on the page, as editing review reminders while notifications are disabled is unlikely to be intentional.
GSoC 2025: Review Reminders

Deletes all old logic for requesting notification permissions. Deletes `checkNotificationPermission` as it has been moved to Permissions as `showNotificationsPermissionBottomSheetIfNeeded`. Removes code for triggering a notifications permission request from LoginFragment as it will be replaced in a future commit.
GSoC 2025: Review Reminders

We would like to request permissions from the user by launching the system UI permissions request dialog only if the user has not already conclusively denied us the permission. Launching the system UI dialog after the user has conclusively denied the permission may cause lower Play Store discoverability.

We'd like to use Google's provided `shouldShowRequestPermissionRationale`, but as abundantly discussed at [19167](ankidroid#19167), this method is highly flawed. In particular, it does not differentiate between first-time permission requests and permission requests that occur after the user has conclusively denied review reminders. In the former case, we want to show the system UI dialog. In the latter, we do not. Hence, in general, we will need to store a Pref for each permission we request to check if the system UI dialog has been launched for that purpose before.

Since this pattern is applicable to all permissions AnkiDroid requests, including notifications, sound recording, and whatever future permissions we may require, I've built some helper functions here to facilitate this. Since I plan on calling them from non-PermissionFragment fragments, I've put them in Permissions. However, this necessitates pulling some code from PermissionsFragment into Permissions, which in turn means modifying some imports. While I was at it, I simplified some code that was unnecessarily calling `RequestMultiplePermissions` with a single-element list when it could just directly call `RequestPermission`. Hence this large-ish commit; these changes are hard to separate without doing line-by-line staging.

The actual primary backend in this commit:
1. Renamed `offerToGrantOrRevokeOnClick` to `revokeIfGrantedOnClickElse` and made it take a callback. This simplifies it a bit and allows for more intricate logic in the `else` case.
2. `canPermissionBeRequested`. Checks a provided Prefs flag and `shouldShowRequestPermissionRationale` to determine if a permission has been conclusively rejected by the user.
3. `requestPermissionThroughDialogOrSettings`. Launches the OS settings menu to let the user manually grant a permission if the user has previously conclusively denied a permission. Otherwise, launches the system UI permissions request dialog.
GSoC 2025: Review Reminders

Created the PermissionsFragment which will be hosted inside PermissionsBottomSheet for requesting notification permissions from the user. Created a new PermissionSet for the notifications permission in InitialActivity, which is gated behind API 33. In a way, this means I've sort of resurrected TiramisuPermissionsFragment, so maybe I shouldn't have been so quick to delete it, whoops.
GSoC 2025: Review Reminders

This method takes a callback so that I can do some conditional preference-setting in a later commit. Launches the notification permission BottomSheet if notification permissions are not granted and the user has not conclusively rejected notification permissions.
GSoC 2025: Review Reminders

Whenever the user logs into an AnkiWeb account, after the log in fragment has been dismissed, trigger a notification permissions check and open the notifications permission bottom sheet if we should request it.

I've carefully placed this method in callbacks so that it is not immediately dismissed when the log in fragment and activity closes. Because of this constraint, I moved the actual code for triggering the BottomSheet to a helper function in Permissions which is called in both DeckPicker and LoginFragment.

I've confirmed this works in three primary scenarios:
1. The user opens the app for the first time, presses "Get Started", presses sync, then logs in. A sync automatically starts and the permission dialog shows up upon a successful login.
2. The user opens the app for the first time, presses "Get Started", goes to settings, then logs in. The permission dialog shows up upon a successful login. No sync happens, as was the behaviour before.
3. The user opens the app for the first time, presses "Sync with AnkiWeb", and logs in. A sync automatically starts and the permission dialog shows up upon a successful login.
GSoC 2025: Review Reminders

Request notification permissions when the user first creates a review reminder. Involves a simple edit to AddEditReminderDialog.
For if callers need to modify / dismiss the created snackbar instance.
GSoC 2025: Review Reminders

If notification permissions are not granted and the user is trying to edit review reminders, we won't directly block them, but they're likely making a mistake. Hence, we show a persistent Snackbar popup that tells them that notification permissions are not granted. The Snackbar has an "Enable" button which, when pressed, calls Permissions's requestPermissionThroughDialogOrSettings to try and grant the notification permission.
GSoC 2025: Review Reminders

Added some docs to explain what happens to permission request persistence prefs if the user restores their data from a backup or migrates to a new device.
- Adds tests for requestPermissionThroughDialogOrSettings and showNotificationsPermissionBottomSheetIfNeeded
The following set of actions triggers an infinitely spinning progress indicator:

1. Be on a metered network connection, ex. using data rather than Wi-Fi
2. New install of the app
3. Choose to sync from AnkiWeb on the introduction screen
4. Log in to an account which has a collection
5. Immediately dismiss the notification permission request bottom sheet by swiping down or pressing on the X
6. Choose not to sync on the metered network connection warning dialog due to being on a metered network

This bug was technically actually present before my notification permission bottom sheet change, but it was hidden due to a coincidental lucky call to `onResume` that would trigger after the user interacted with the OS permission request dialog. This bug can be recreated on the `main` branch if the line triggering the OS permission request dialog in DeckPicker's `sync` method is removed.

There are two bugs at play here. The principal root cause is that a UI refresh is only triggered after a sync is completed successfully with a call to `refreshState` from `Sync.kt`'s method `DeckPicker.handleNewSync`. However, if the metered network warning dialog shows up and the user chooses not to sync, this never happens, so the progress bar spins forever. This issue is fixed by adding a `refreshState` call immediately after the metered network warning dialog is shown. I've also moved a `syncOnResume = false` block a few lines up to ensure no weird race condition shenanigans occur.

However, with this fix applied, the loading icon still persists. This is because of a race condition between when the UI-reloading MutableSharedFlow has its collector registered and when the above newly-added `refreshState` call `emit`s to that MutableSharedFlow. In short, the latter happens before the former. To fix this, I've added `replay = 1` to the definition of `flowOfDecksReloaded`.

However,
@ericli3690 ericli3690 force-pushed the ericli3690-review-reminders-permission-request branch from 500ec32 to 5cba550 Compare January 9, 2026 06:27
@ericli3690 ericli3690 added Needs Second Approval Has one approval, one more approval to merge and removed Needs Author Reply Waiting for a reply from the original author Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) labels Jan 9, 2026
@ericli3690
Copy link
Member Author

So close to the finish line! While applying David's nits I found a rare UI bug that can occur. Basically, after a specific series of button presses, the Snackbar on ScheduleReminders that tells users notification permissions are missing shows up even when notification permissions are granted. It needs to be manually dismissed if we detect that notification permissions have been granted, but doing so requires some changes to the Snackbar utility functions. I'd like someone to give my changes there a quick skim as it's slightly non-trivial. See this commit and this one which follows it.

Otherwise, the only major change I've applied as a result of David's feedback is auto-dismissing the permissions bottom sheet: see here and here.

All other changes are purely cosmetic / documentation. To whomever reviews this PR, if that all looks good, feel free to put it in the merge queue. Thanks!!

Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

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

Looks great!

@david-allison david-allison added Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) and removed Needs Second Approval Has one approval, one more approval to merge labels Jan 9, 2026
@david-allison david-allison added this pull request to the merge queue Jan 9, 2026
github-merge-queue bot pushed a commit that referenced this pull request Jan 9, 2026
GSoC 2025: Review Reminders

Adds two new boolean Preferences for saving whether we have requested notification permissions from the user: one for whether it has been done for the sync process yet (we will request notification permissions the first time the user logs in to an account, as only then can they trigger a sync) and one for whether it has been done for the review reminders feature yet (we will request notification permissions the first time the user creates a review reminder).

As discussed extensively at [19167](#19167), `shouldShowRequestPermissionRationale` is unable to distinguish between first-time permission requests and permission requests after conclusive user rejection. See [this post](https://stackoverflow.com/questions/41310510/what-is-the-difference-between-shouldshowrequestpermissionrationale-and-requestp) for more information. To make up for this shortfall in Android API functionality, we need to store some state ourselves. In particular, we need to remember if the user has seen the permission request BottomSheet for specific reasons and if the user has seen the system UI dialog for requesting permissions for specific permissions - the former to ensure we don't spam the user with BottomSheets if they've already denied a permission for a specific feature, and the latter to ensure we don't trigger the system UI permission request dialog if the user has already conclusively rejected a permission. Triggering the system UI dialog after the user has conclusively rejected a permission may lead to lower Play Store discoverability. This is also more extensible in the future, should a future developer choose to request the notification permission in a different circumstance, too.

I have decided to request the permission once for the sync feature and once for the review reminder feature because if the user denies the permission for the sync feature and then goes to enable review reminders, they may change their mind about providing AnkiDroid with notification permissions; and vice versa.

If the permission is denied, I have chosen not to downgrade any review reminders functionality: the user will still be able to create, edit, and delete them. However, they will not receive any notifications. I do not believe it makes sense to restrict the user's ability to edit review reminders even though they have notifications disabled, as a user might want to customize their reminders to their liking before enabling notifications, etc. However, I will add a small notification box to inform the user that notifications are disabled when they are on the page, as editing review reminders while notifications are disabled is unlikely to be intentional.
github-merge-queue bot pushed a commit that referenced this pull request Jan 9, 2026
GSoC 2025: Review Reminders

We would like to request permissions from the user by launching the system UI permissions request dialog only if the user has not already conclusively denied us the permission. Launching the system UI dialog after the user has conclusively denied the permission may cause lower Play Store discoverability.

We'd like to use Google's provided `shouldShowRequestPermissionRationale`, but as abundantly discussed at [19167](#19167), this method is highly flawed. In particular, it does not differentiate between first-time permission requests and permission requests that occur after the user has conclusively denied review reminders. In the former case, we want to show the system UI dialog. In the latter, we do not. Hence, in general, we will need to store a Pref for each permission we request to check if the system UI dialog has been launched for that purpose before.

Since this pattern is applicable to all permissions AnkiDroid requests, including notifications, sound recording, and whatever future permissions we may require, I've built some helper functions here to facilitate this. Since I plan on calling them from non-PermissionFragment fragments, I've put them in Permissions. However, this necessitates pulling some code from PermissionsFragment into Permissions, which in turn means modifying some imports. While I was at it, I simplified some code that was unnecessarily calling `RequestMultiplePermissions` with a single-element list when it could just directly call `RequestPermission`. Hence this large-ish commit; these changes are hard to separate without doing line-by-line staging.

The actual primary backend in this commit:
1. Renamed `offerToGrantOrRevokeOnClick` to `revokeIfGrantedOnClickElse` and made it take a callback. This simplifies it a bit and allows for more intricate logic in the `else` case.
2. `canPermissionBeRequested`. Checks a provided Prefs flag and `shouldShowRequestPermissionRationale` to determine if a permission has been conclusively rejected by the user.
3. `requestPermissionThroughDialogOrSettings`. Launches the OS settings menu to let the user manually grant a permission if the user has previously conclusively denied a permission. Otherwise, launches the system UI permissions request dialog.
Merged via the queue into ankidroid:main with commit 1f91b28 Jan 9, 2026
15 checks passed
@github-actions github-actions bot added this to the 2.24 release milestone Jan 9, 2026
@github-actions github-actions bot removed the Pending Merge Things with approval that are waiting future merge (e.g. targets a future release, CI wait, etc) label Jan 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GSoC Pull requests authored by a Google Summer of Code participant [Candidate/Selected], for GSoC mentors Next version Changes to be merged in the next version, to keep the current release stable.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ask the Notifications permission in the permission screen in Android 13+

4 participants