From 59c13bf8c1b98b5ed0b9eb73743c82ebaea291ae Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Wed, 25 May 2022 12:35:43 +0200 Subject: [PATCH] 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 --- .../webview/WebChromeEventListener.kt | 32 ++++++++++ .../app/features/widgets/WidgetFragment.kt | 20 +++++- .../widgets/webview/WebviewPermissionUtils.kt | 61 ++++++++++++++++++- .../features/widgets/webview/WidgetWebView.kt | 5 +- 4 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt diff --git a/vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt b/vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt new file mode 100644 index 0000000000..8a12052def --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/webview/WebChromeEventListener.kt @@ -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 + } + +} diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index cd2a4dcdf4..c8a13d11cf 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -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(), 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(), diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt index 12b58cc208..f1111a4650 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt @@ -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() + @SuppressLint("NewApi") - fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, context: Context) { + fun promptForPermissions( + @StringRes title: Int, + request: PermissionRequest, + context: Context, + activity: FragmentActivity, + activityResultLauncher: ActivityResultLauncher> + ) { 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) { + 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 + } + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 7147529e5f..a49eb802da 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -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)