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

View File

@ -15,17 +15,30 @@
*/ */
package im.vector.app.features.widgets.webview package im.vector.app.features.widgets.webview
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.utils.checkPermissions
object WebviewPermissionUtils { object WebviewPermissionUtils {
private var permissionRequest: PermissionRequest? = null
private var selectedPermissions = listOf<String>()
@SuppressLint("NewApi") @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 { val allowedPermissions = request.resources.map {
it to false it to false
}.toMutableList() }.toMutableList()
@ -37,9 +50,21 @@ object WebviewPermissionUtils {
allowedPermissions[which] = allowedPermissions[which].first to isChecked allowedPermissions[which] = allowedPermissions[which].first to isChecked
} }
.setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ -> .setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ ->
request.grant(allowedPermissions.mapNotNull { perm -> permissionRequest = request
selectedPermissions = allowedPermissions.mapNotNull { perm ->
perm.first.takeIf { perm.second } 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) { _, _ -> .setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ ->
request.deny() request.deny()
@ -47,6 +72,28 @@ object WebviewPermissionUtils {
.show() .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 { private fun webPermissionToHumanReadable(permission: String, context: Context): String {
return when (permission) { return when (permission) {
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone) PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone)
@ -55,4 +102,12 @@ object WebviewPermissionUtils {
else -> permission 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.R
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.webview.VectorWebViewClient import im.vector.app.features.webview.VectorWebViewClient
import im.vector.app.features.webview.WebChromeEventListener
import im.vector.app.features.webview.WebViewEventListener import im.vector.app.features.webview.WebViewEventListener
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) { fun WebView.setupForWidget(webViewEventListener: WebViewEventListener, webChromeEventListener: WebChromeEventListener) {
// xml value seems ignored // xml value seems ignored
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
@ -59,7 +60,7 @@ fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) {
// Permission requests // Permission requests
webChromeClient = object : WebChromeClient() { webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) { override fun onPermissionRequest(request: PermissionRequest) {
WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, context) webChromeEventListener.onPermissionRequest(request)
} }
} }
webViewClient = VectorWebViewClient(webViewEventListener) webViewClient = VectorWebViewClient(webViewEventListener)