33import android .Manifest ;
44import android .annotation .TargetApi ;
55import android .app .Activity ;
6+ import android .app .AlertDialog ;
7+ import android .content .DialogInterface ;
68import android .content .pm .PackageManager ;
9+ import android .util .Log ;
710import android .net .Uri ;
811import android .os .Build ;
12+ import android .os .Handler ;
913import android .os .Message ;
1014import android .view .Gravity ;
1115import android .view .View ;
1721import android .webkit .WebChromeClient ;
1822import android .webkit .WebView ;
1923import android .webkit .WebViewClient ;
24+ import android .widget .Button ;
2025import android .widget .FrameLayout ;
2126
2227import androidx .annotation .RequiresApi ;
3742import java .util .List ;
3843
3944public 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
0 commit comments