Skip to content

Commit e88168f

Browse files
authored
Merge pull request #6 from phantom/kuba/android-camera-access-security
feat: implement alert asking for permissions on android
2 parents 6923b67 + 7c908d0 commit e88168f

File tree

2 files changed

+132
-21
lines changed

2 files changed

+132
-21
lines changed

android/src/main/java/com/reactnativecommunity/webview/RNCWebChromeClient.java

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import android.Manifest;
44
import android.annotation.TargetApi;
55
import android.app.Activity;
6+
import android.app.AlertDialog;
7+
import android.content.DialogInterface;
68
import android.content.pm.PackageManager;
9+
import android.util.Log;
710
import android.net.Uri;
811
import android.os.Build;
12+
import android.os.Handler;
913
import android.os.Message;
1014
import android.view.Gravity;
1115
import android.view.View;
@@ -17,6 +21,7 @@
1721
import android.webkit.WebChromeClient;
1822
import android.webkit.WebView;
1923
import android.webkit.WebViewClient;
24+
import android.widget.Button;
2025
import android.widget.FrameLayout;
2126

2227
import androidx.annotation.RequiresApi;
@@ -37,6 +42,7 @@
3742
import java.util.List;
3843

3944
public class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener {
45+
private static final String TAG = RNCWebChromeClient.class.getSimpleName();
4046
protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams(
4147
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
4248

@@ -65,6 +71,10 @@ public class RNCWebChromeClient extends WebChromeClient implements LifecycleEven
6571
protected PermissionRequest permissionRequest;
6672
// Webview camera & audio permission already granted
6773
protected List<String> grantedPermissions;
74+
// Webview requested permissions from the OS
75+
protected List<String> requestedAndroidPermissions;
76+
// Webview granted permissions requested separately through isDialog
77+
protected List<String> alertPermissions;
6878

6979
// Webview geolocation permission callback
7080
protected GeolocationPermissions.Callback geolocationPermissionCallback;
@@ -144,54 +154,133 @@ public void onProgressChanged(WebView webView, int newProgress) {
144154

145155
@Override
146156
public void onPermissionRequest(final PermissionRequest request) {
147-
157+
permissionRequest = request;
148158
grantedPermissions = new ArrayList<>();
159+
alertPermissions = new ArrayList<>();
160+
// Permissions that we need to ask permission for from the OS
161+
requestedAndroidPermissions = new ArrayList<>();
149162

150-
ArrayList<String> requestedAndroidPermissions = new ArrayList<>();
151163
for (String requestedResource : request.getResources()) {
152164
String androidPermission = null;
165+
String requestPermissionIdentifier = null;
153166

154167
if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
155168
androidPermission = Manifest.permission.RECORD_AUDIO;
169+
requestPermissionIdentifier = "microphone";
156170
} else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
157171
androidPermission = Manifest.permission.CAMERA;
172+
requestPermissionIdentifier = "camera";
158173
} else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) {
159174
if (mAllowsProtectedMedia) {
160-
grantedPermissions.add(requestedResource);
175+
grantedPermissions.add(requestedResource);
161176
} else {
162-
/**
163-
* Legacy handling (Kept in case it was working under some conditions (given Android version or something))
164-
*
165-
* Try to ask user to grant permission using Activity.requestPermissions
166-
*
167-
* Find more details here: https://github.com/react-native-webview/react-native-webview/pull/2732
168-
*/
169-
androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID;
170-
} }
177+
// TODO: Make sure this is properly taken care of too
178+
/**
179+
* Legacy handling (Kept in case it was working under some conditions (given Android version or something))
180+
*
181+
* Try to ask user to grant permission using Activity.requestPermissions
182+
*
183+
* Find more details here: https://github.com/react-native-webview/react-native-webview/pull/2732
184+
*/
185+
androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID;
186+
}
187+
}
188+
171189
// TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID.
172190
if (androidPermission != null) {
173191
if (ContextCompat.checkSelfPermission(this.mWebView.getThemedReactContext(), androidPermission) == PackageManager.PERMISSION_GRANTED) {
174-
grantedPermissions.add(requestedResource);
192+
Activity activity = this.mWebView.getThemedReactContext().getCurrentActivity();
193+
if (activity == null) {
194+
// We just skip this one, but it should never happen
195+
Log.w(TAG, "Current activity is null, cannot show AlertDialog. Denying permission request.");
196+
continue;
197+
}
198+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
199+
builder.setMessage(this.makeAlertMessage(request, requestPermissionIdentifier));
200+
builder.setCancelable(false);
201+
String finalAndroidPermission = androidPermission;
202+
builder.setPositiveButton("Allow", (dialog, which) -> {
203+
this.grantedPermissions.add(requestedResource);
204+
this.alertPermissions.remove(finalAndroidPermission);
205+
if (!this.alertPermissions.isEmpty()) {
206+
// There will be more alerts to show, so we just return
207+
return;
208+
}
209+
this.alertPermissions = null;
210+
if (this.requestedAndroidPermissions.isEmpty()) {
211+
// Wrapping up the permission request
212+
this.permissionRequest.grant(this.grantedPermissions.toArray(new String[0]));
213+
this.permissionRequest = null;
214+
this.grantedPermissions = null;
215+
} else {
216+
// Need to request permissions from the OS, will grant or deny later
217+
this.requestPermissions(this.requestedAndroidPermissions);
218+
}
219+
});
220+
builder.setNegativeButton("Deny", (dialog, which) -> {
221+
this.alertPermissions.remove(finalAndroidPermission);
222+
if (!this.alertPermissions.isEmpty()) {
223+
// There will be more alerts to show, so we just return
224+
return;
225+
}
226+
this.alertPermissions = null;
227+
if (!this.requestedAndroidPermissions.isEmpty()) {
228+
// Need to request permissions from the OS, will grant or deny later and return now
229+
this.requestPermissions(this.requestedAndroidPermissions);
230+
return;
231+
}
232+
// Wrapping up the permission request, this time with grant or deny
233+
if (!this.grantedPermissions.isEmpty()) {
234+
this.permissionRequest.grant(this.grantedPermissions.toArray(new String[0]));
235+
} else {
236+
this.permissionRequest.deny();
237+
}
238+
this.permissionRequest = null;
239+
this.grantedPermissions = null;
240+
});
241+
AlertDialog alertDialog = builder.create();
242+
alertDialog.show();
243+
this.alertPermissions.add(finalAndroidPermission);
244+
//Delay making `allow` clickable for 500ms to avoid unwanted presses.
245+
Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
246+
posButton.setEnabled(false);
247+
this.runDelayed(() -> posButton.setEnabled(true), 500);
175248
} else {
176249
requestedAndroidPermissions.add(androidPermission);
177250
}
178251
}
179252
}
180253

254+
// if we have alerts in progress we will handle below with the last alert
255+
if (!this.alertPermissions.isEmpty()) {
256+
return;
257+
}
258+
181259
// If all the permissions are already granted, send the response to the WebView synchronously
182260
if (requestedAndroidPermissions.isEmpty()) {
183-
request.grant(grantedPermissions.toArray(new String[0]));
184-
grantedPermissions = null;
261+
if (!grantedPermissions.isEmpty()) {
262+
request.grant(grantedPermissions.toArray(new String[0]));
263+
grantedPermissions = null;
264+
} else {
265+
request.deny();
266+
}
185267
return;
186268
}
187269

188270
// Otherwise, ask to Android System for native permissions asynchronously
189-
190-
this.permissionRequest = request;
191-
192271
requestPermissions(requestedAndroidPermissions);
193272
}
194273

274+
private String makeAlertMessage(PermissionRequest request, String requestIdentifier) {
275+
Uri originUri = request.getOrigin();
276+
String host = originUri.getHost();
277+
return String.format("Allow " + host + " to use your " + requestIdentifier + "?");
278+
}
279+
280+
private void runDelayed(Runnable function, long delayMillis) {
281+
Handler handler = new Handler();
282+
handler.postDelayed(function, delayMillis);
283+
}
195284

196285
@Override
197286
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
@@ -208,7 +297,29 @@ public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermiss
208297
requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION));
209298

210299
} else {
211-
callback.invoke(origin, true, false);
300+
String alertMessage = String.format("Allow %s to use your location?", origin);
301+
Activity activity = this.mWebView.getThemedReactContext().getCurrentActivity();
302+
if (activity == null) {
303+
Log.w(TAG, "Current activity is null, cannot show AlertDialog. Calling negative callback.");
304+
callback.invoke(origin, false, false);
305+
return;
306+
}
307+
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
308+
builder.setMessage(alertMessage);
309+
builder.setCancelable(false);
310+
builder.setPositiveButton("Allow", (dialog, which) -> {
311+
callback.invoke(origin, true, false);
312+
});
313+
builder.setNegativeButton("Don't allow", (dialog, which) -> {
314+
callback.invoke(origin, false, false);
315+
});
316+
AlertDialog alertDialog = builder.create();
317+
alertDialog.show();
318+
//Delay making `allow` clickable for 500ms to avoid unwanted presses.
319+
Button posButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE);
320+
posButton.setEnabled(false);
321+
this.runDelayed(() -> posButton.setEnabled(true), 500);
322+
212323
}
213324
}
214325

@@ -223,7 +334,6 @@ private PermissionAwareActivity getPermissionAwareActivity() {
223334
}
224335

225336
private synchronized void requestPermissions(List<String> permissions) {
226-
227337
/*
228338
* If permissions request dialog is displayed on the screen and another request is sent to the
229339
* activity, the last permission asked is skipped. As a work-around, we use pendingPermissions
@@ -257,7 +367,7 @@ private synchronized void requestPermissions(List<String> permissions) {
257367
* As a "pending requests" approach is used, requestCode cannot help to define if the request
258368
* came from geolocation or camera/audio. This is why shouldAnswerToPermissionRequest is used
259369
*/
260-
boolean shouldAnswerToPermissionRequest = false;
370+
boolean shouldAnswerToPermissionRequest = grantedPermissions != null && !grantedPermissions.isEmpty() && permissionRequest != null;
261371

262372
for (int i = 0; i < permissions.length; i++) {
263373

android/src/main/java/com/reactnativecommunity/webview/RNCWebView.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ public boolean getMessagingEnabled() {
251251
return this.messagingEnabled;
252252
}
253253

254+
@SuppressLint("RestrictedApi")
254255
protected void createRNCWebViewBridge(RNCWebView webView) {
255256
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)){
256257
if (this.bridgeListener == null) {

0 commit comments

Comments
 (0)