-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
improvement: permission explanations #18978
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
improvement: permission explanations #18978
Conversation
|
Need people to review. Please let me know if:
Haven't tested on different API versions yet. The above image is how it looks on my phone, which is a Samsung S23, API 34. I wanted to get UX / implementation feedback first before committing more time to testing. |
438094f to
95af0d4
Compare
|
Oops, forgot the copyright header and had to add the new activity to the ActivityList for tests. Should be fixed now. |
95af0d4 to
633a0a3
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.
Could you try reusing PermissionsActivity? Adding these permissions under an Optional section there should work.
0b0c9bc to
09f0b86
Compare
|
Refactored to use PermissionsActivity. Refactored the permission fragments to minimize code duplication, fixed some issues. See the PR description for an updated changelog. I've done more testing and things seem to be working. The only slightly weird bit of behaviour is that if the user clicks on a permission item to enable it, but then hits "do not grant" in the dialog, the fragment thinks the dialog to enable permissions failed and opens the OS settings. I can't think of a way to stop this from happening, but I think it's actually alright behaviour. If the user clicks on a permission item that's currently disabled, then we can assume that they want to grant that permission, and so it makes sense that we show them the ability to toggle the permission in the OS settings if they immediately click "do not grant" on the dialog. Important question: Does anyone know how I can test the externally-managed-storage permissions? I haven't been able to test these yet. Also, as I mentioned in Discord, Full30and31PermissionsFragment and TiramisuPermissionsFragment are now essentially identical. Can I delete TiramisuPermissionsFragment? |
| // Microphone | ||
| // Minimum API 23, which is less than AnkiDroid's minimum targeted API version, so we show it unconditionally | ||
| recordAudioPermission.apply { | ||
| isVisible = true | ||
| offerToGrantOrRevokeOnClick(Permissions.recordAudioPermission) | ||
| } |
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.
We currently show this when a user wants to record audio.
We /may/ want to inform users, but I feel it's just additional cognitive overhead here.
We don't need mic access to run the app
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 actually disagree with this point. Notice in the UI above that when the user is in the OS settings, they can click on a little info button next to the microphone permission (the microphone permission shows up in the OS settings because we've declared it in the manifest file). I personally feel it would be weird to a user if they clicked an info button next to the microphone permission and proceeded to... not learn any new info about the microphone permission. Picture, for instance, a user who has forgotten that they once granted AnkiDroid the permission to access the microphone. Opening up the OS settings and seeing an explanation-less microphone permission might upset them a bit.
| <!-- TODO: Move this to a proper string resource file and collect all other strings associated with this feature once it is stable. --> | ||
| <string name="schedule_reminders_do_not_translate" maxLength="28" comment="Do not translate this string, this feature is still in development. Name of the screen that appears when review reminders are being scheduled.">Schedule reminders</string> | ||
| <string name="manually_grant_permissions_do_not_translate" comment="Do not translate this string, this feature is still in development. Pop-up message that is shown to the user when they want to grant an app permission that must be granted via the OS settings screen.">This permission must be manually granted from Settings</string> | ||
| <string name="revoke_permissions_do_not_translate" comment="Do not translate this string, this feature is still in development. Pop-up message that is shown to the user when they are revoking an app permission.">This permission must be manually revoked from Settings</string> |
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.
This doesn't seem to be a dev only feature
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.
Oop, good point. I've moved them to 10-preferences, let me know if someplace else would be better!
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.
Found a better place in 01-core. I'll group these, and the rest of the strings I need for this feature, together with the strings used on the first-time-install permissions screen.
09f0b86 to
4462657
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.
My first remark is that this commit is too big.
For example, setOnSwitchClickListener is called in three places, so I think that a first commit that add the parameter to the listener would be an acceptable commit by itself.
Something that shows that there is a problem its that your commit message seems like a bunch of unrelated changes. I don't really feel that reading this commit message helped me understand what I was going to review.
I think your commit message would generally need a first section that explains why you're doing this.
In this case, it would be something such as
Android offers each app a way to explains to show to the user which permissions the app request and why. This was not yet used by AnkiDroid. This commit introduce this page. In order to access it, the user must (list the steps. I'm really not sure what need to be done here)
Admitttedly, it'd be better if we could have the image in the commit message, as your PR description makes it very clear how to access this view, but we don't have such a feature, so it'll need to be text, or at worse a link to the image hosted on github.
Then you can explain why, in order to achieve your goal, you have to do other things.
| android:configChanges="keyboardHidden|orientation|screenSize" | ||
| /> | ||
|
|
||
| <!-- Activity that hosts PermissionsFragments for requesting permissions from the user --> |
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 don't close the comment at the end of the line. Be consistent with the remaining of the code. <!-- is multiline.
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.
Fixed.
| ActivityResultContracts.RequestMultiplePermissions(), | ||
| ) { requestedPermissions -> | ||
| if (!requestedPermissions.all { it.value }) { | ||
| // The permission dialog did not show up or the user denied the permission |
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.
If I understand correctly the code, requestedPermissions is a set of permission.
If there is at most one element, then your code should just use first or some method to get this element and note explicitely that it's the only element, maybe even check that it's the case.
If it can contains multiple element, the comment should indicate that this code is executed if any of the permission is denied.
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.
That would make the code more verbose, though. I feel like requestedPermission.all is clean enough and that adding special case handling for if the hashmap contains one item or multiple is a bit too complex.
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 would make the code more complex?
Now that I've read more, I see there can indeed be multiple permissions. In which case, I just request that you updated your comment to use "permissions" instead of "permission"
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 implementing this at #19033!
| class AllPermissionsExplanation : PermissionsFragment(R.layout.all_permissions_explanation) { | ||
| /** | ||
| * Attempts to open the dialog for granting a permission. Falls back to opening the OS settings if | ||
| * the dialog fails to show up. This may happen if the user has previously denied the permission multiple times, |
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.
According to the comment below, it also shows the os settings if some permission is rejected by the user. This should be documented
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.
Implemented.
| val recordAudioPermission = view.findViewById<PermissionItem>(R.id.record_audio_permission_item) | ||
|
|
||
| // External storage | ||
| if (Permissions.canManageExternalStorage(requireContext())) { |
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 admit that I found canManageExternalStorage name to be strange. I was wondering why you'd add a preference if we can already manage it.
I think it would be helpful if, when you use a non-documented method, that you add documentation to it if you can.
In this case, I discovered while reading the implementation that actually it means "Is it possible for this installation of android to eventually manager external storage(potentially after requesting the permission to android)" and not "can this installation of ankidroid can currently manage the external storage"
I would admittedly prefer a clearer name, but I can't think of any name that is not absurdly long, such as manifestDeclaresExternalStoragePermission. So at least, having its meaning documented would help the next reader that is not familiar with the permission code
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 documenting this at #19034!
| } | ||
| } | ||
| // Notifications | ||
| if (Permissions.postNotification != null) { |
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'd go with
Permissions.postNotification ?.let {
notificationPermission.apply {
isVisible = true
offerToGrantOrRevokeOnClick(it)
}
}
Admittedly, here, postNotification is a value, so we know it can't become null, you don't have to use !! inside the code, so implementor choice.
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.
Implemented!
|
|
||
| /** | ||
| * Screen responsible for getting permissions from the user. | ||
| * Also the activity that shows up when the user selects a more-info button in the OS permission settings. |
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.
"os permission settings" seems strange. It seems you are discussing the permission of the os.
Instead, I'd rephrase it as
When the user open the settings in Android, and navigate to AnkiDroid permission, there will find a "More info" button which will open this activity.
This way, it's clear that:
- This action is related to action in android
- android setting contains a subsection specific for ankidroid
- this subsection can open android defined activity
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.
Good point, fixed.
| // if the API version is at least 31. We double check the API version to pass the lint check. | ||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
| showAllPermissionsExplanation() | ||
| } |
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 extra safety, I'd appreciate if the else branch could send a report to ankidroid. Because even if you are in theory right, Android has given us a lot of unexpected surprise. If you're right, the extra code takes very little space, otherwise it's great to know. The else branch could also open requestPermissionsInPermissionSet so that something occurs for the user
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, how do I send a report to AnkiDroid? Just via Timber.e, or is there some other library call I need to use?
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, this is why you asked about Timber.e
You need to use CrashReportService.sendExceptionReport
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 I've decided to follow your advice to create a new activity instead, this code is no longer necessary and has been removed.
|
|
||
| when (intent.action) { | ||
| Intent.ACTION_VIEW_PERMISSION_USAGE, Intent.ACTION_VIEW_PERMISSION_USAGE_FOR_PERIOD -> { | ||
| // These actions can trigger this activity due to an intent filter in the manifest |
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.
That's not exactly an explanation I fear.
That's technically true, but not really interesting for most reader.
I think instead, it'd be useful to state:
When the user opened android settings, navigated to android permission and tapped "learn more", this activity will be used with one of those two intents. This can only occurs with API 31 or more, so the following `if` should always be true.
This both explain your if and clarify to the reader when this case occur.
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.
Moved this docstring to the top of AllPermissionsExplanationActivity and implemented your suggested change.
| setContentView(R.layout.permissions_activity) | ||
| setTransparentStatusBar() | ||
|
|
||
| when (intent.action) { |
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 must admit that it's not clear to me why this activity deal with permission_usage. It seems that it reuse almost no code in common.
Currently it uses the same permissions_activity which has exactly three values, a title, a fragment with the actual content and a button. And even the button, you hide it.
I think it'd be easier to reason about this code if you created a new layout, with the same title. And don't need to sometime hide part of 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.
I made this PR reuse PermissionsActivity because Brayan told me to in order to reduce redundant code.
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 I have the link please?
I feel like there is almost no code in common, so I don't see how it would create redundancy.
If you want to avoid creating a new layout, I can understand that. But you can have two different activities using the same layout.
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.
Here's the link to Brayan's comment: #18978 (review)
I do find your rationale compelling, though; it's why I initially created a new Activity before Brayan told me to change it. I've brought back the new standalone activity now. It hosts a PermissionsFragment since doing so allows me to reuse some code from the other permission fragments.
| ) { requestedPermissions -> | ||
| if (!requestedPermissions.all { it.value }) { | ||
| // The permission dialog did not show up or the user denied the permission | ||
| // Offer the ability to manually grant the permission via the OS settings |
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 find strange to put the "settings" at the end while it's an intermediary step. Also, it misses the fact that it's not any part of the os settings, but the ankidroid specific one
So I'd phrase it as
Offers to open the OS settings section for ankidroid. In this section, the user ca manually grant the permission
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.
Fixed!
|
Thanks Arthur for the review. I'll try to break this up into smaller commits. Will also add a more clean immediate explanation of the feature in the description. Brayan's also let me know that I can delete TiramisuPermissionsFragment, so I'll do that too. |
|
@Arthur-Milchior I'm going to wait until #19033 is fully merged before I start making edits here, since you rename some of the variables I'm working with etc. and I don't want to make merging too messy. I'll focus on cleaning up my other draft PRs in the meantime. |
|
It's merged |
4462657 to
6a42c70
Compare
|
I like it! |
00cb4c5 to
dcec758
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.
Looks solid, cheers!
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.
Looks great. Left some comments.
...oid/src/main/java/com/ichi2/anki/ui/windows/permissions/AllPermissionsExplanationFragment.kt
Outdated
Show resolved
Hide resolved
...oid/src/main/java/com/ichi2/anki/ui/windows/permissions/AllPermissionsExplanationFragment.kt
Outdated
Show resolved
Hide resolved
...oid/src/main/java/com/ichi2/anki/ui/windows/permissions/AllPermissionsExplanationFragment.kt
Outdated
Show resolved
Hide resolved
dcec758 to
2079497
Compare
GSoC 2025: Review Reminders - Adds permission explanation screen to OS settings to explain to the user why we need the permissions we request, via a new AllPermissionsExplanationFragment fragment hosted in a new PermissionsExplanationActivity. Arthur requested that I create a new activity for this purpose. - Launches the above activity via an intent filter defined in AndroidManifest.xml, as per [the docs](https://developer.android.com/training/permissions/explaining-access#privacy-dashboard). - Added comments to AndroidManifest.xml to flag that future maintainers should add any new runtime permissions to AllPermissionsExplanationFragment. - Moved the logic for requesting external storage from the children of PermissionsFragment to the PermissionsFragment abstract class itself. This allows the methods to be called in both the PermissionsFragment children and the AllPermissionsExplanationFragment, minimizing code duplication. Added logic for setting a PermissionItem to revoke a permission on click, too. - Added strings for telling the user that they need to grant or revoke permissions manually from the OS settings, strings for the permissions explanations, etc. - Increased the amount of maximum lines in a PermissionItem's description so I could fit a longer explanation of why AnkiDroid needs notification permissions. - Added POST_NOTIFICATIONS and RECORD_AUDIO to constants.xml so they can be referred to by AllPermissionsExplanationFragment.
2079497 to
f25cc1e
Compare
|
|
Maintainers: Please Sync Translations to produce a commit with only the automated changes from this PR. Read more about updating strings on the wiki, |
Adds permission explanation screen to OS settings to explain to the user why we need the permissions we request, via a new AllPermissionsExplanationFragment fragment hosted in a new PermissionsExplanationActivity. Arthur requested that I create a new activity for this purpose.
Purpose / Description
How the permissions explanation screen looks on a Play Store build:

An example of the permissions explanation screen on a full debug build (not Play Store) (the strings in this screenshot are outdated, see the above screenshot for updated strings):

The snackbar that shows up when you try to revoke a permission:

Fixes
Approach
AndroidManifest, it seems there are only really three kinds of permissions that we need to request from the user: externally managed storage, notifications, and microphone. Legacy storage permissions are only available until API 29, and this permissions explanation screen only shows up on API 31+. Please suggest wording changes if you dislike any wording I've used.How Has This Been Tested?
Checklist