Skip to content
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

Merged
merged 14 commits into from
May 31, 2022

Conversation

Johennes
Copy link
Contributor

@Johennes Johennes commented May 25, 2022

Type of change

  • Feature
  • Bugfix
  • Technical
  • Other :

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:

Screenshot_20220525_123021

After this change, once the widget permission dialog is confirmed, the actual system permission dialogs appear (unless permission has already been granted before):

Screenshot_20220525_123041
Screenshot_20220525_123051

Tests

  • Perform a clean app install or reset the app's camera & microphone permissions
  • Join #ecwidgettest:matrix.org
  • Tap through into Element Call in the widget web view
  • Confirm the widget permission dialog
  • Notice the new system permission dialogs

Tested devices

  • Physical
  • Emulator
  • OS version(s): 12

Checklist

…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]>
@Johennes Johennes marked this pull request as draft May 25, 2022 10:51
@Johennes Johennes changed the title Make widget web view request system permissions for camera and microphone Make widget web view request system permissions for camera and microphone (PSF-1061) May 25, 2022
@github-actions
Copy link

github-actions bot commented May 25, 2022

Unit Test Results

146 files  +  4  146 suites  +4   2m 9s ⏱️ -21s
235 tests +  6  235 ✔️ +  6  0 💤 ±0  0 ±0 
784 runs  +24  784 ✔️ +24  0 💤 ±0  0 ±0 

Results for commit 4ebb26d. ± Comparison against base commit 483b1ab.

♻️ This comment has been updated with latest results.

@Johennes Johennes marked this pull request as ready for review May 25, 2022 11:39
@Johennes Johennes requested review from a team, ouchadam and ganfra and removed request for a team May 25, 2022 11:39

object WebviewPermissionUtils {

private var permissionRequest: PermissionRequest? = null
Copy link
Contributor

@ouchadam ouchadam May 25, 2022

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?

Copy link
Contributor Author

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 ->
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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! 💯

@Johennes Johennes requested a review from ouchadam May 25, 2022 13:58
return@filter result[androidPermission]
?: return@filter true // Android permission already granted before
}
if (grantedPermissions.isNotEmpty()) {
Copy link
Contributor

@ouchadam ouchadam May 25, 2022

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 }
}

Copy link
Contributor Author

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.

Copy link
Contributor Author

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. ☹️

Copy link
Contributor

@ouchadam ouchadam May 25, 2022

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

Copy link
Contributor Author

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()
Copy link
Contributor

@ouchadam ouchadam May 25, 2022

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

Copy link
Contributor Author

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. 🙂

Copy link
Contributor

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 😄

@mnaturel mnaturel self-requested a review May 30, 2022 12:29
Copy link
Contributor

@mnaturel mnaturel left a 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)
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Member

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...

Copy link
Contributor Author

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.

@@ -271,6 +278,20 @@ class WidgetFragment @Inject constructor() :
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
}

private val permissionResultLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mnaturel
Copy link
Contributor

mnaturel commented May 30, 2022

After some tests, I have a remark concerning the UX:
Even if I granted the microphone permission once, the dialog with checkboxes to request permission appears again. I am wondering if we could directly request Android permissions without the extra dialog with checkboxes? It will be less painful for users.

Copy link
Member

@ganfra ganfra left a 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

@Johennes
Copy link
Contributor Author

After some tests, I have a remark concerning the UX: Even if I granted the microphone permission once, the dialog with checkboxes to request permission appears again. I am wondering if we could directly request Android permissions without the extra dialog with checkboxes? It will be less painful for users.

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.

@Johennes Johennes merged commit 7dd5b80 into develop May 31, 2022
@Johennes Johennes deleted the johannes/widget-system-permissions branch May 31, 2022 08:54
@github-actions
Copy link

Matrix SDK

Integration Tests Results:

  • [org.matrix.android.sdk.session]
    = passed=20 failures=0 errors=0 skipped=3
  • [org.matrix.android.sdk.account]
    = passed=3 failures=0 errors=0 skipped=2
  • [org.matrix.android.sdk.internal]
    = passed=27 failures=1 errors=0 skipped=1
  • [org.matrix.android.sdk.ordering]
    = passed=16 failures=0 errors=0 skipped=0
  • [org.matrix.android.sdk.PermalinkParserTest]
    = passed=2 failures=0 errors=0 skipped=0

@Johennes Johennes mentioned this pull request May 31, 2022
15 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants