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:
parent
9a38d59f9a
commit
59c13bf8c1
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue