Skip to content

Commit 7dd5b80

Browse files
authored
Merge pull request #6149 from vector-im/johannes/widget-system-permissions
Make widget web view request system permissions for camera and microphone (PSF-1061)
2 parents 7fdf138 + 4ebb26d commit 7dd5b80

File tree

7 files changed

+225
-13
lines changed

7 files changed

+225
-13
lines changed

changelog.d/6149.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Make widget web view request system permissions for camera and microphone (PSF-1061)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.webview
18+
19+
import android.webkit.PermissionRequest
20+
21+
interface WebChromeEventListener {
22+
23+
/**
24+
* Triggered when the web view requests permissions.
25+
*
26+
* @param request The permission request.
27+
*/
28+
fun onPermissionRequest(request: PermissionRequest)
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.webview
18+
19+
interface WebEventListener : WebViewEventListener, WebChromeEventListener

vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt

+22-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import android.view.Menu
2626
import android.view.MenuItem
2727
import android.view.View
2828
import android.view.ViewGroup
29+
import android.webkit.PermissionRequest
30+
import androidx.activity.result.contract.ActivityResultContracts
2931
import androidx.core.view.isInvisible
3032
import androidx.core.view.isVisible
3133
import com.airbnb.mvrx.Fail
@@ -42,7 +44,8 @@ import im.vector.app.core.platform.OnBackPressed
4244
import im.vector.app.core.platform.VectorBaseFragment
4345
import im.vector.app.core.utils.openUrlInExternalBrowser
4446
import im.vector.app.databinding.FragmentRoomWidgetBinding
45-
import im.vector.app.features.webview.WebViewEventListener
47+
import im.vector.app.features.webview.WebEventListener
48+
import im.vector.app.features.widgets.webview.WebviewPermissionUtils
4649
import im.vector.app.features.widgets.webview.clearAfterWidget
4750
import im.vector.app.features.widgets.webview.setupForWidget
4851
import kotlinx.parcelize.Parcelize
@@ -60,9 +63,11 @@ data class WidgetArgs(
6063
val urlParams: Map<String, String> = emptyMap()
6164
) : Parcelable
6265

63-
class WidgetFragment @Inject constructor() :
66+
class WidgetFragment @Inject constructor(
67+
private val permissionUtils: WebviewPermissionUtils
68+
) :
6469
VectorBaseFragment<FragmentRoomWidgetBinding>(),
65-
WebViewEventListener,
70+
WebEventListener,
6671
OnBackPressed {
6772

6873
private val fragmentArgs: WidgetArgs by args()
@@ -271,6 +276,20 @@ class WidgetFragment @Inject constructor() :
271276
viewModel.handle(WidgetAction.OnWebViewLoadingError(url, true, errorCode, description))
272277
}
273278

279+
private val permissionResultLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
280+
permissionUtils.onPermissionResult(result)
281+
}
282+
283+
override fun onPermissionRequest(request: PermissionRequest) {
284+
permissionUtils.promptForPermissions(
285+
title = R.string.room_widget_resource_permission_title,
286+
request = request,
287+
context = requireContext(),
288+
activity = requireActivity(),
289+
activityResultLauncher = permissionResultLauncher
290+
)
291+
}
292+
274293
private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) {
275294
navigator.openTerms(
276295
context = requireContext(),

vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt

+68-6
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,31 @@
1515
*/
1616
package im.vector.app.features.widgets.webview
1717

18-
import android.annotation.SuppressLint
18+
import android.Manifest
1919
import android.content.Context
2020
import android.webkit.PermissionRequest
21+
import androidx.activity.result.ActivityResultLauncher
2122
import androidx.annotation.StringRes
23+
import androidx.annotation.VisibleForTesting
24+
import androidx.fragment.app.FragmentActivity
2225
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2326
import im.vector.app.R
27+
import im.vector.app.core.utils.checkPermissions
28+
import java.lang.NullPointerException
29+
import javax.inject.Inject
2430

25-
object WebviewPermissionUtils {
31+
class WebviewPermissionUtils @Inject constructor() {
2632

27-
@SuppressLint("NewApi")
28-
fun promptForPermissions(@StringRes title: Int, request: PermissionRequest, context: Context) {
33+
private var permissionRequest: PermissionRequest? = null
34+
private var selectedPermissions = listOf<String>()
35+
36+
fun promptForPermissions(
37+
@StringRes title: Int,
38+
request: PermissionRequest,
39+
context: Context,
40+
activity: FragmentActivity,
41+
activityResultLauncher: ActivityResultLauncher<Array<String>>
42+
) {
2943
val allowedPermissions = request.resources.map {
3044
it to false
3145
}.toMutableList()
@@ -37,16 +51,56 @@ object WebviewPermissionUtils {
3751
allowedPermissions[which] = allowedPermissions[which].first to isChecked
3852
}
3953
.setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ ->
40-
request.grant(allowedPermissions.mapNotNull { perm ->
54+
permissionRequest = request
55+
selectedPermissions = allowedPermissions.mapNotNull { perm ->
4156
perm.first.takeIf { perm.second }
42-
}.toTypedArray())
57+
}
58+
59+
val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission ->
60+
webPermissionToAndroidPermission(permission)
61+
}
62+
63+
// When checkPermissions returns false, some of the required Android permissions will
64+
// have to be requested and the flow completes asynchronously via onPermissionResult
65+
if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) {
66+
request.grant(selectedPermissions.toTypedArray())
67+
reset()
68+
}
4369
}
4470
.setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ ->
4571
request.deny()
4672
}
4773
.show()
4874
}
4975

76+
fun onPermissionResult(result: Map<String, Boolean>) {
77+
if (permissionRequest == null) {
78+
throw NullPointerException("permissionRequest was null! Make sure to call promptForPermissions first.")
79+
}
80+
val grantedPermissions = filterPermissionsToBeGranted(selectedPermissions, result)
81+
if (grantedPermissions.isNotEmpty()) {
82+
permissionRequest?.grant(grantedPermissions.toTypedArray())
83+
} else {
84+
permissionRequest?.deny()
85+
}
86+
reset()
87+
}
88+
89+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
90+
fun filterPermissionsToBeGranted(selectedWebPermissions: List<String>, androidPermissionResult: Map<String, Boolean>): List<String> {
91+
return selectedWebPermissions.filter { webPermission ->
92+
val androidPermission = webPermissionToAndroidPermission(webPermission)
93+
?: return@filter true // No corresponding Android permission exists
94+
return@filter androidPermissionResult[androidPermission]
95+
?: return@filter true // Android permission already granted before
96+
}
97+
}
98+
99+
private fun reset() {
100+
permissionRequest = null
101+
selectedPermissions = listOf()
102+
}
103+
50104
private fun webPermissionToHumanReadable(permission: String, context: Context): String {
51105
return when (permission) {
52106
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> context.getString(R.string.room_widget_webview_access_microphone)
@@ -55,4 +109,12 @@ object WebviewPermissionUtils {
55109
else -> permission
56110
}
57111
}
112+
113+
private fun webPermissionToAndroidPermission(permission: String): String? {
114+
return when (permission) {
115+
PermissionRequest.RESOURCE_AUDIO_CAPTURE -> Manifest.permission.RECORD_AUDIO
116+
PermissionRequest.RESOURCE_VIDEO_CAPTURE -> Manifest.permission.CAMERA
117+
else -> null
118+
}
119+
}
58120
}

vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import android.webkit.WebView
2525
import im.vector.app.R
2626
import im.vector.app.features.themes.ThemeUtils
2727
import im.vector.app.features.webview.VectorWebViewClient
28-
import im.vector.app.features.webview.WebViewEventListener
28+
import im.vector.app.features.webview.WebEventListener
2929

3030
@SuppressLint("NewApi")
31-
fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) {
31+
fun WebView.setupForWidget(eventListener: WebEventListener) {
3232
// xml value seems ignored
3333
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface))
3434

@@ -59,10 +59,10 @@ fun WebView.setupForWidget(webViewEventListener: WebViewEventListener) {
5959
// Permission requests
6060
webChromeClient = object : WebChromeClient() {
6161
override fun onPermissionRequest(request: PermissionRequest) {
62-
WebviewPermissionUtils.promptForPermissions(R.string.room_widget_resource_permission_title, request, context)
62+
eventListener.onPermissionRequest(request)
6363
}
6464
}
65-
webViewClient = VectorWebViewClient(webViewEventListener)
65+
webViewClient = VectorWebViewClient(eventListener)
6666

6767
val cookieManager = CookieManager.getInstance()
6868
cookieManager.setAcceptThirdPartyCookies(this, false)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.widgets
18+
19+
import android.Manifest
20+
import android.webkit.PermissionRequest
21+
import im.vector.app.features.widgets.webview.WebviewPermissionUtils
22+
import org.amshove.kluent.shouldBeEqualTo
23+
import org.junit.FixMethodOrder
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
import org.junit.runners.JUnit4
27+
import org.junit.runners.MethodSorters
28+
29+
@RunWith(JUnit4::class)
30+
@FixMethodOrder(MethodSorters.JVM)
31+
class WebviewPermissionUtilsTest {
32+
33+
private val utils = WebviewPermissionUtils()
34+
35+
@Test
36+
fun filterPermissionsToBeGranted_selectedAndGrantedNothing() {
37+
val permissions = utils.filterPermissionsToBeGranted(
38+
selectedWebPermissions = listOf(),
39+
androidPermissionResult = mapOf())
40+
permissions shouldBeEqualTo listOf()
41+
}
42+
43+
@Test
44+
fun filterPermissionsToBeGranted_selectedNothingGrantedCamera() {
45+
val permissions = utils.filterPermissionsToBeGranted(
46+
selectedWebPermissions = listOf(),
47+
androidPermissionResult = mapOf(Manifest.permission.CAMERA to true))
48+
permissions shouldBeEqualTo listOf()
49+
}
50+
51+
@Test
52+
fun filterPermissionsToBeGranted_selectedAndPreviouslyGrantedCamera() {
53+
val permissions = utils.filterPermissionsToBeGranted(
54+
selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
55+
androidPermissionResult = mapOf())
56+
permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
57+
}
58+
59+
@Test
60+
fun filterPermissionsToBeGranted_selectedAndGrantedCamera() {
61+
val permissions = utils.filterPermissionsToBeGranted(
62+
selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
63+
androidPermissionResult = mapOf(Manifest.permission.CAMERA to true))
64+
permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)
65+
}
66+
67+
@Test
68+
fun filterPermissionsToBeGranted_selectedAndDeniedCamera() {
69+
val permissions = utils.filterPermissionsToBeGranted(
70+
selectedWebPermissions = listOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE),
71+
androidPermissionResult = mapOf(Manifest.permission.CAMERA to false))
72+
permissions shouldBeEqualTo listOf()
73+
}
74+
75+
@Test
76+
fun filterPermissionsToBeGranted_selectedProtectedMediaGrantedNothing() {
77+
val permissions = utils.filterPermissionsToBeGranted(
78+
selectedWebPermissions = listOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID),
79+
androidPermissionResult = mapOf(Manifest.permission.CAMERA to false))
80+
permissions shouldBeEqualTo listOf(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)
81+
}
82+
}

0 commit comments

Comments
 (0)