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)