Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/harden-android-media-capture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@phantom/react-native-webview": patch
---

Honor `mediaCapturePermissionGrantType="deny"` on Android by rejecting camera
and microphone resources before the existing permission flow can display a
prompt. Other permission resources and grant-type values retain their current
behavior.
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,6 @@ dependencies {
implementation 'com.facebook.react:react-native:+'
implementation "org.jetbrains.kotlin:kotlin-stdlib:${safeExtGet('kotlinVersion')}"
implementation "androidx.webkit:webkit:${safeExtGet('webkitVersion')}"

testImplementation "junit:junit:4.13.2"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.reactnativecommunity.webview;

import android.webkit.PermissionRequest;

import java.util.ArrayList;
import java.util.List;

final class RNCMediaCapturePermission {
private static final String GRANT_TYPE_DENY = "deny";

private RNCMediaCapturePermission() {}

static String[] filterRequestedResources(String grantType, String[] resources) {
if (!GRANT_TYPE_DENY.equals(grantType)) {
return resources;
}

List<String> filteredResources = new ArrayList<>(resources.length);
for (String resource : resources) {
if (!isMediaCaptureResource(resource)) {
filteredResources.add(resource);
}
}
return filteredResources.toArray(new String[0]);
}

private static boolean isMediaCaptureResource(String resource) {
return PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(resource)
|| PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,19 @@ public void onPermissionRequest(final PermissionRequest request) {
// Permissions that we need to ask permission for from the OS
requestedAndroidPermissions = new ArrayList<>();

for (String requestedResource : request.getResources()) {
String[] requestedResources = RNCMediaCapturePermission.filterRequestedResources(
mWebView.getMediaCapturePermissionGrantType(),
request.getResources()
);

if (requestedResources.length == 0) {
permissionRequest = null;
grantedPermissions = null;
request.deny();
return;
}

for (String requestedResource : requestedResources) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
String androidPermission = null;
String requestPermissionIdentifier = null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class RNCWebView extends WebView implements LifecycleEventListener {
protected ProgressChangedFilter progressChangedFilter;
protected boolean mActive = true;
protected @Nullable String mLastCommittedUrl;
protected @Nullable String mMediaCapturePermissionGrantType;

/**
* WebView must be created with an context of the current activity
Expand Down Expand Up @@ -265,6 +266,14 @@ public void setLastCommittedUrl(@Nullable String url) {
return this.mLastCommittedUrl;
}

public void setMediaCapturePermissionGrantType(@Nullable String value) {
this.mMediaCapturePermissionGrantType = value;
}

public @Nullable String getMediaCapturePermissionGrantType() {
return this.mMediaCapturePermissionGrantType;
}

@SuppressLint("RestrictedApi")
protected void createRNCWebViewBridge(RNCWebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,10 @@ class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
view.settings.setGeolocationEnabled(value)
}

fun setMediaCapturePermissionGrantType(viewWrapper: RNCWebViewWrapper, value: String?) {
viewWrapper.webView.setMediaCapturePermissionGrantType(value)
}

fun setLackPermissionToDownloadMessage(value: String?) {
mLackPermissionToDownloadMessage = value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public void setAllowsProtectedMedia(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsProtectedMedia(view, value);
}

@Override
@ReactProp(name = "mediaCapturePermissionGrantType")
public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMediaCapturePermissionGrantType(view, value);
}

@Override
@ReactProp(name = "androidLayerType")
public void setAndroidLayerType(RNCWebViewWrapper view, @Nullable String value) {
Expand Down Expand Up @@ -423,9 +429,6 @@ public void setTextInteractionEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setHasOnFileDownload(RNCWebViewWrapper view, boolean value) {}

@Override
public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable String value) {}

@Override
public void setFraudulentWebsiteWarningEnabled(RNCWebViewWrapper view, boolean value) {}
/* !iOS PROPS - no implemented here */
Expand Down Expand Up @@ -567,4 +570,4 @@ public void onDropViewInstance(@NonNull RNCWebViewWrapper view) {
mRNCWebViewManagerImpl.onDropViewInstance(view);
super.onDropViewInstance(view);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public void setAllowsProtectedMedia(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsProtectedMedia(view, value);
}

@ReactProp(name = "mediaCapturePermissionGrantType")
public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMediaCapturePermissionGrantType(view, value);
}

@ReactProp(name = "androidLayerType")
public void setAndroidLayerType(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setAndroidLayerType(view, value);
Expand Down Expand Up @@ -337,4 +342,4 @@ public void onDropViewInstance(@NonNull RNCWebViewWrapper view) {
mRNCWebViewManagerImpl.onDropViewInstance(view);
super.onDropViewInstance(view);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.reactnativecommunity.webview;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertSame;

import android.webkit.PermissionRequest;

import org.junit.Test;

public class RNCMediaCapturePermissionTest {
@Test
public void denyRemovesCameraAndMicrophoneResources() {
String[] resources = {
PermissionRequest.RESOURCE_VIDEO_CAPTURE,
PermissionRequest.RESOURCE_AUDIO_CAPTURE,
};

assertArrayEquals(
new String[0],
RNCMediaCapturePermission.filterRequestedResources("deny", resources)
);
}

@Test
public void denyPreservesUnrelatedResources() {
String[] resources = {
PermissionRequest.RESOURCE_VIDEO_CAPTURE,
PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID,
};

assertArrayEquals(
new String[] { PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID },
RNCMediaCapturePermission.filterRequestedResources("deny", resources)
);
}

@Test
public void otherPoliciesPreserveExistingBehavior() {
String[] resources = { PermissionRequest.RESOURCE_VIDEO_CAPTURE };

assertSame(
resources,
RNCMediaCapturePermission.filterRequestedResources("prompt", resources)
);
}
}
8 changes: 5 additions & 3 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1537,9 +1537,11 @@ Possible values:

Note that a grant may still result in a prompt, for example if the user has never been prompted for the permission before.

| Type | Required | Platform |
| ------ | -------- | -------- |
| string | No | iOS |
On Android, `deny` blocks camera and microphone requests without showing a WebView or operating-system permission prompt. Other values preserve the existing prompt behavior.

| Type | Required | Platform |
| ------ | -------- | ------------ |
| string | No | iOS, Android |

Example:

Expand Down
9 changes: 9 additions & 0 deletions src/WebViewTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,15 @@ export interface AndroidWebViewProps extends WebViewSharedProps {
*/
allowsProtectedMedia?: boolean;

/**
* This property specifies how to handle media capture permission requests.
* On Android, `deny` blocks camera and microphone requests without showing
* a WebView or operating-system permission prompt. Other values preserve the
* existing prompt behavior.
* @platform android
*/
mediaCapturePermissionGrantType?: MediaCapturePermissionGrantType;

/**
* Function that is invoked when the `WebView` receives an SSL error for a sub-resource.
*
Expand Down
Loading