Make widget web view request system permissions for camera and microphone

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 <johannesm@element.io>
This commit is contained in:
Johannes Marbach 2022-05-25 12:35:43 +02:00
parent 9a38d59f9a
commit 59c13bf8c1
4 changed files with 112 additions and 6 deletions

View File

@ -0,0 +1,32 @@
/*
* Copyright 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.webview
import android.webkit.PermissionRequest
interface WebChromeEventListener {
/**
* Triggered when the web view requests permissions.
*
* @param request The permission request.
*/
fun onPermissionRequest(request: PermissionRequest) {
// NO-OP
}
}

View File

@ -26,6 +26,8 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.webkit.PermissionRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
@ -42,7 +44,9 @@ import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.openUrlInExternalBrowser
import im.vector.app.databinding.FragmentRoomWidgetBinding
import im.vector.app.features.webview.WebChromeEventListener
import im.vector.app.features.webview.WebViewEventListener
import im.vector.app.features.widgets.webview.WebviewPermissionUtils
import im.vector.app.features.widgets.webview.clearAfterWidget
import im.vector.app.features.widgets.webview.setupForWidget
import kotlinx.parcelize.Parcelize
@ -63,6 +67,7 @@ data class WidgetArgs(
class WidgetFragment @Inject constructor() :
VectorBaseFragment<FragmentRoomWidgetBinding>(),
WebViewEventListener,
WebChromeEventListener,
OnBackPressed {
private val fragmentArgs: WidgetArgs by args()
@ -75,7 +80,7 @@ class WidgetFragment @Inject constructor() :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
views.widgetWebView.setupForWidget(this)
views.widgetWebView.setupForWidget(this, this)
if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().setWebView(views.widgetWebView)
}
@ -271,6 +276,19 @@ class WidgetFragment @Inject constructor() :
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
}
private val permissionResultLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
WebviewPermissionUtils.onPermissionResult(result)
}
override fun onPermissionRequest(request: PermissionRequest) {
WebviewPermissionUtils.promptForPermissions(
title = R.string.room_widget_resource_permission_title,
request = request,
context = requireContext(),
activity = requireActivity(),
activityResultLauncher = permissionResultLauncher)
}
private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) {
navigator.openTerms(
context = requireContext(),

View File

@ -15,17 +15,30 @@
*/
package im.vector.app.features.widgets.webview
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.webkit.PermissionRequest
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.utils.checkPermissions
object WebviewPermissionUtils {
private var permissionRequest: PermissionRequest? = null
private var selectedPermissions = listOf<String>()
@SuppressLint("NewApi")
fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, context: Context) {
fun promptForPermissions(
@StringRes title: Int,
request: PermissionRequest,
context: Context,
activity: FragmentActivity,
activityResultLauncher: ActivityResultLauncher<Array<String>>
) {
val allowedPermissions = request.resources.map {
it to false
}.toMutableList()
@ -37,9 +50,21 @@ object WebviewPermissionUtils {
allowedPermissions[which] = allowedPermissions[which].first to isChecked
}
.setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ ->
request.grant(allowedPermissions.mapNotNull { perm ->
permissionRequest = request
selectedPermissions = allowedPermissions.mapNotNull { perm ->
perm.first.takeIf { perm.second }
}.toTypedArray())
}
val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission ->
webPermissionToAndroidPermission(permission)
}
// When checkPermissions returns false, some of the required Android permissions will
// have to be requested and the flow completes asynchronously via onPermissionResult
if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) {
request.grant(selectedPermissions.toTypedArray())
reset()
}
}
.setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ ->
request.deny()
@ -47,6 +72,28 @@ object WebviewPermissionUtils {
.show()
}
fun onPermissionResult(result: Map<String, Boolean>) {
permissionRequest?.let { request ->
val grantedPermissions = selectedPermissions.filter { webPermission ->
val androidPermission = webPermissionToAndroidPermission(webPermission)
?: return@filter true // No corresponding Android permission exists
return@filter result[androidPermission]
?: return@filter true // Android permission already granted before
}
if (grantedPermissions.isNotEmpty()) {
request.grant(grantedPermissions.toTypedArray())
} else {
request.deny()
}
reset()
}
}
private fun reset() {
permissionRequest = null
selectedPermissions = listOf()
}
private fun webPermissionToHumanReadable(permission: String, context: Context): String {
return when (permission) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone)
@ -55,4 +102,12 @@ object WebviewPermissionUtils {
else -> permission
}
}
private fun webPermissionToAndroidPermission(permission: String): String? {
return when (permission) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA
else -> null
}
}
}

View File

@ -25,10 +25,11 @@ import android.webkit.WebView
import im.vector.app.R
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.webview.VectorWebViewClient
import im.vector.app.features.webview.WebChromeEventListener
import im.vector.app.features.webview.WebViewEventListener
@SuppressLint("NewApi")
fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) {
fun WebView.setupForWidget(webViewEventListener: WebViewEventListener, webChromeEventListener: WebChromeEventListener) {
// xml value seems ignored
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
@ -59,7 +60,7 @@ fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) {
// Permission requests
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, context)
webChromeEventListener.onPermissionRequest(request)
}
}
webViewClient = VectorWebViewClient(webViewEventListener)