feat(reminders): request notification permissions#19167
feat(reminders): request notification permissions#19167david-allison merged 13 commits intoankidroid:mainfrom
Conversation
|
Important Maintainers: This PR contains Strings changes
|
37e535a to
e53a1b6
Compare
|
e53a1b6 to
e8b030c
Compare
This comment was marked as resolved.
This comment was marked as resolved.
e8b030c to
d11214a
Compare
|
d11214a to
d611d76
Compare
This comment was marked as resolved.
This comment was marked as resolved.
d611d76 to
0703e5d
Compare
|
87fb916 to
8a3a21d
Compare
|
Dependencies have been merged. Rebased and ready for review! |
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,
500ec32 to
5cba550
Compare
|
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!! |
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.
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.
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
Fixes
Approach
How Has This Been Tested?
Learning (optional, can help others)
shouldShowPermissionRequestRationale!!Checklist