feat(android): Enhanced camera/audio and geolocation permissions (#1239)

This commit is contained in:
Thibaud Michel 2021-05-21 00:15:22 +02:00 committed by GitHub
parent 0419e32f3f
commit 9f6037e4b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 182 additions and 1 deletions

View File

@ -2,6 +2,7 @@ package com.reactnativecommunity.webview;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.ActivityInfo;
@ -46,6 +47,8 @@ import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;
import com.facebook.common.logging.FLog;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
import com.facebook.react.views.scroll.OnScrollDispatchHelper;
@ -86,7 +89,9 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@ -1086,12 +1091,35 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
protected static final int COMMON_PERMISSION_REQUEST = 3;
protected ReactContext mReactContext;
protected View mWebView;
protected View mVideoView;
protected WebChromeClient.CustomViewCallback mCustomViewCallback;
/*
* - Permissions -
* As native permissions are asynchronously handled by the PermissionListener, many fields have
* to be stored to send permissions results to the webview
*/
// Webview camera & audio permission callback
protected PermissionRequest permissionRequest;
// Webview camera & audio permission already granted
protected List<String> grantedPermissions;
// Webview geolocation permission callback
protected GeolocationPermissions.Callback geolocationPermissionCallback;
// Webview geolocation permission origin callback
protected String geolocationPermissionOrigin;
// true if native permissions dialog is shown, false otherwise
protected boolean permissionsRequestShown = false;
// Pending Android permissions for the next request
protected List<String> pendingPermissions = new ArrayList<>();
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
public RNCWebChromeClient(ReactContext reactContext, WebView webView) {
@ -1180,11 +1208,164 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
event));
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPermissionRequest(final PermissionRequest request) {
grantedPermissions = new ArrayList<>();
ArrayList<String> requestedAndroidPermissions = new ArrayList<>();
for (String requestedResource : request.getResources()) {
String androidPermission = null;
if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
androidPermission = Manifest.permission.RECORD_AUDIO;
} else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
androidPermission = Manifest.permission.CAMERA;
}
// TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID.
if (androidPermission != null) {
if (ContextCompat.checkSelfPermission(mReactContext, androidPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(requestedResource);
} else {
requestedAndroidPermissions.add(androidPermission);
}
}
}
// If all the permissions are already granted, send the response to the WebView synchronously
if (requestedAndroidPermissions.isEmpty()) {
request.grant(grantedPermissions.toArray(new String[0]));
grantedPermissions = null;
return;
}
// Otherwise, ask to Android System for native permissions asynchronously
this.permissionRequest = request;
requestPermissions(requestedAndroidPermissions);
}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
if (ContextCompat.checkSelfPermission(mReactContext, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
/*
* Keep the trace of callback and origin for the async permission request
*/
geolocationPermissionCallback = callback;
geolocationPermissionOrigin = origin;
requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION));
} else {
callback.invoke(origin, true, false);
}
}
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = mReactContext.getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
private synchronized void requestPermissions(List<String> permissions) {
/*
* If permissions request dialog is displayed on the screen and another request is sent to the
* activity, the last permission asked is skipped. As a work-around, we use pendingPermissions
* to store next required permissions.
*/
if (permissionsRequestShown) {
pendingPermissions.addAll(permissions);
return;
}
PermissionAwareActivity activity = getPermissionAwareActivity();
permissionsRequestShown = true;
activity.requestPermissions(
permissions.toArray(new String[0]),
COMMON_PERMISSION_REQUEST,
webviewPermissionsListener
);
// Pending permissions have been sent, the list can be cleared
pendingPermissions.clear();
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private PermissionListener webviewPermissionsListener = (requestCode, permissions, grantResults) -> {
permissionsRequestShown = false;
/*
* As a "pending requests" approach is used, requestCode cannot help to define if the request
* came from geolocation or camera/audio. This is why shouldAnswerToPermissionRequest is used
*/
boolean shouldAnswerToPermissionRequest = false;
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)
&& geolocationPermissionCallback != null
&& geolocationPermissionOrigin != null) {
if (granted) {
geolocationPermissionCallback.invoke(geolocationPermissionOrigin, true, false);
} else {
geolocationPermissionCallback.invoke(geolocationPermissionOrigin, false, false);
}
geolocationPermissionCallback = null;
geolocationPermissionOrigin = null;
}
if (permission.equals(Manifest.permission.RECORD_AUDIO)) {
if (granted && grantedPermissions != null) {
grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);
}
shouldAnswerToPermissionRequest = true;
}
if (permission.equals(Manifest.permission.CAMERA)) {
if (granted && grantedPermissions != null) {
grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);
}
shouldAnswerToPermissionRequest = true;
}
}
if (shouldAnswerToPermissionRequest
&& permissionRequest != null
&& grantedPermissions != null) {
permissionRequest.grant(grantedPermissions.toArray(new String[0]));
permissionRequest = null;
grantedPermissions = null;
}
if (!pendingPermissions.isEmpty()) {
requestPermissions(pendingPermissions);
return false;
}
return true;
};
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType);
}