diff --git a/.changeset/harden-android-media-capture.md b/.changeset/harden-android-media-capture.md new file mode 100644 index 0000000000..dda15e459a --- /dev/null +++ b/.changeset/harden-android-media-capture.md @@ -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. diff --git a/android/build.gradle b/android/build.gradle index cb3476ad25..e906547634 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" } diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCMediaCapturePermission.java b/android/src/main/java/com/reactnativecommunity/webview/RNCMediaCapturePermission.java new file mode 100644 index 0000000000..abd48ea5d4 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCMediaCapturePermission.java @@ -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 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); + } +} diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java index 6dab254a9e..f76e98232a 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java @@ -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) { String androidPermission = null; String requestPermissionIdentifier = null; diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java index efa843b072..e399315916 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java @@ -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 @@ -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)){ diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt index ebfd70763c..0938533a80 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManagerImpl.kt @@ -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 } diff --git a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 53c40af53f..5441ecda55 100644 --- a/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/newarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -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) { @@ -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 */ @@ -567,4 +570,4 @@ public void onDropViewInstance(@NonNull RNCWebViewWrapper view) { mRNCWebViewManagerImpl.onDropViewInstance(view); super.onDropViewInstance(view); } -} \ No newline at end of file +} diff --git a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java index 27b7a03d3a..8f4d3e8a59 100644 --- a/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/oldarch/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -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); @@ -337,4 +342,4 @@ public void onDropViewInstance(@NonNull RNCWebViewWrapper view) { mRNCWebViewManagerImpl.onDropViewInstance(view); super.onDropViewInstance(view); } -} \ No newline at end of file +} diff --git a/android/src/test/java/com/reactnativecommunity/webview/RNCMediaCapturePermissionTest.java b/android/src/test/java/com/reactnativecommunity/webview/RNCMediaCapturePermissionTest.java new file mode 100644 index 0000000000..a35ed231ea --- /dev/null +++ b/android/src/test/java/com/reactnativecommunity/webview/RNCMediaCapturePermissionTest.java @@ -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) + ); + } +} diff --git a/docs/Reference.md b/docs/Reference.md index 6d0b99acc0..86d1b677dc 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -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: diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index 9eb2ab7256..1e88861ecd 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -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. *