-
Notifications
You must be signed in to change notification settings - Fork 731
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
Make widget web view request system permissions for camera and microphone (PSF-1061) #6149
Conversation
…hone Previously the widget web view prompted to grant the widget permissions but it didn't actually request those permissions from the system. So if the web view requested, e.g. the camera permission but the app hadn't previously been granted that permission, the web view wouldn't get camera access even when the widget permission request had been confirmed. With this commit, the app will also request camera and microphone permissions from the system when needed. Signed-off-by: Johannes Marbach <[email protected]>
|
||
object WebviewPermissionUtils { | ||
|
||
private var permissionRequest: PermissionRequest? = 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'm a little weary of including mutable static state in the object
instance, would prefer for this class to become instantiable (and then injected where needed) or have the activity/fragment to hold on to the state instead
what do you think?
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 had also thought of this, yeah. I had previously decided against putting the state into the fragment because that way state and logic are split between the fragment and WebviewPermissionUtils
which I found harder to reason about. I made the utils into a normal class now and used a private instance variable on the fragment. If this needs to be injectable, I might need some help with that. 🙈
} | ||
.setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ -> | ||
request.deny() | ||
} | ||
.show() | ||
} | ||
|
||
fun onPermissionResult(result: Map<String, Boolean>) { | ||
permissionRequest?.let { request -> |
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.
is there a case where we would call onPermissionResult
without having a valid permissionRequest
available?
I would lean towards throwing instead of silently swallowing the missing promptForPermissions
(if the ordering is implicitly required)
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.
No, that shouldn't happen, I believe. The order is required as you say. I've added an NPE instead of the let
. Let me know if that's what you had in mind.
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.
sounds good to me, thanks! 💯
return@filter result[androidPermission] | ||
?: return@filter true // Android permission already granted before | ||
} | ||
if (grantedPermissions.isNotEmpty()) { |
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.
tiny nit - semantically it could be cleaner to avoid mapping within the filter
, then the filter could be replaced with an any
, extensions can also be created to help avoid needing inline comments
val permissions = selectedWebPermissions
.onlySupportedAndroidPermissions()
.mapWithResult(result)
when {
permissions.any { (_, isGranted) -> isGranted } -> {
request.grant(permissions.map { (name, _ ) -> name }.toTypedArray())
}
else -> deny()
}
reset()
private fun List<String>.onlySupportedAndroidPermissions() = filter { webPermissionToAndroidPermission(it) != null }
private fun List<String>.mapWithResult(result: Map<String, Boolean>) = mapNotNull { permission ->
result[permission]?.let{ isGranted -> permission to isGranted }
}
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 think in this snippet we'd put the Android permission strings into request.grant(...)
, right? We have to put the web permission strings into that one. That's essentially the reason why I mapped inside the filter.
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 agree that the filter
clause isn't very easy to parse though.
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.
ah I see! 🤦 yeah there's a few steps happening here but the scope small and it gets the job done (which is why this is an opinionated non blocking nit from me 😄 )
EDIT - updated the snippet to filter by android permissions instead of mapping to them
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 kept nagging me so I added a unit test for this part to be able to safely change the implementation. I found that your updated snippet still doesn't work, sadly.
First, onlySupportedAndroidPermissions
is actually not needed / desired because there are web permissions such as PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID
for which (I think) no Android system permission exists. In those cases we still want to grant those permissions to the web view (based on the fact that the user confirmed them in the widget permission dialog) so we can't filter them out.
Secondly, the chained call of mapWithResult
uses the web permission string as key in the Android permission result map which will always result in null
.
I've tried to resolve this but in the end it comes down to having the current complicated filter/map logic inside mapWithResult
which seemed more or less equivalent to what I have now. 😕
At least we have the tests now though so this endeavor hasn't been in vein. 😀
@@ -72,6 +72,7 @@ class WidgetFragment @Inject constructor() : | |||
|
|||
private val fragmentArgs: WidgetArgs by args() | |||
private val viewModel: WidgetViewModel by activityViewModel() | |||
private val permissionUtils = WebviewPermissionUtils() |
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 we want to inject this via dagger (not really needed whilst the WebviewPermissionUtils
has no dependencies of its own) we can include the instance via the injectable constructor of the fragment
WidgetFragment @Inject constructor()
becomes...
WidgetFragment @Inject constructor(
private val permissionUtils: WebviewPermissionUtils
)
and the WebviewPermissionUtils
itself will need to be marked as injectable
class WebviewPermissionUtils @Inject constructor()
Fragments in Element are injectable because of the fragment module
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.
Ah nice! Thanks a lot for explaining. Made the change to be a good citizen and help remember for next time. 🙂
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.
future us will appreciate the injectable boilerplate being done 😄
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 have added small comments. I need to test it.
@@ -0,0 +1 @@ | |||
Make widget web view request system permissions for camera and microphone (PSF-1061) |
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.
Do we want the PSF number into the changelog entry?
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.
Happy to remove it but having it in the changelog / release notes would allow us to directly cross reference with Jira. I think it's essentially also not better or worse than including it in commits and pull requests – which we already do today.
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 will be visible from the release page, so would be better to have a corresponding github issue...
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 assumed with the filename it'll link to this PR and, therefore, had intentionally explained the issue that is fixed in detail up in the description to avoid having to create a separate issue just to close it again.
vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt
Outdated
Show resolved
Hide resolved
@@ -271,6 +278,20 @@ class WidgetFragment @Inject constructor() : | |||
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description)) | |||
} | |||
|
|||
private val permissionResultLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> |
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 don't know if we could use existing methods in PermissionTools file to avoid having other logic for permissions handling?
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.
The registration methods available there, sadly don't expose the map of permission results: https://github.com/vector-im/element-android/blob/4094a66f3ce65d5fa92ac31b7749c731b3f33cf5/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt#L67
vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt
Outdated
Show resolved
Hide resolved
vector/src/androidTest/java/im/vector/app/features/widgets/WebviewPermissionUtilsTest.kt
Outdated
Show resolved
Hide resolved
vector/src/androidTest/java/im/vector/app/features/widgets/WebviewPermissionUtilsTest.kt
Outdated
Show resolved
Hide resolved
After some tests, I have a remark concerning the UX: |
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.
Just small remarks otherwise LGTM
vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt
Outdated
Show resolved
Hide resolved
vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt
Outdated
Show resolved
Hide resolved
The reason the intermediate dialog is needed is that the app currently doesn't keep track of which widget runs in the web view. So if we wouldn't always show the first prompt, then the second prompt would effectively grant the permission to all widgets – which we likely don't want. I agree that the UX isn't particularly great. This PR was only meant to fix the bug where the web view wasn't actually granted the permission though. We already have https://element-io.atlassian.net/browse/PSF-1033 (Web view permissions are not persisted - Android) to persist the permissions per widget which can serve as the basis for generally improving the flow. |
vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt
Show resolved
Hide resolved
Matrix SDKIntegration Tests Results:
|
Type of change
Content
Previously the widget web view prompted to grant the widget permissions but it didn't actually request those permissions from the system. So if the web view requested, e.g. the camera permission but the app hadn't previously been granted that permission, the web view wouldn't get camera access even when the widget permission request had been confirmed.
With this change, the app will also request camera and microphone permissions from the system when needed.
Motivation and context
The concrete use case that led to this change is running Element Call in a widget web view after a fresh app install (where no system permissions have been granted before and so the widget would never actually get camera or microphone access).
Screenshots / GIFs
Before this change, only the widget permission dialog was displayed:
After this change, once the widget permission dialog is confirmed, the actual system permission dialogs appear (unless permission has already been granted before):
Tests
Tested devices
Checklist