Преглед на файлове

Synchronously decide if Android WebView should load a URL or not. (#1590)

This solves a class of issues when the WebView loses "context"
that a subsequent page load is the same as what was attempted
to be loaded previously. This solves a bug where a HTTP redirect
in combination with history manipulations causes a user to be
stuck and prevented from going back. Since WebView requests are
allowed to happen normally, debugging the WebView and tracking
redirects and page load initiators is more accurate and easier.
This will also bypass bridge latency and provide a faster navigation.

To do this, we must lock in the shouldOverrideUrlLoading callback
and send an event to JS. Currently, this callback is ran on
the main UI thread, of which we have no control over. This is
problematic as using the bridge in most ways seems to require
the main UI thread, which will cause a deadlock. However, using
BatchedBridge for Java->JS and a synchronous method for JS->Java
doesn't cause any problems. Additionally, it's been designed so
that if WebView suddenly runs the callback on a different thread
allowing for concurrency, it will continue to work.
Daniel Vicory преди 3 години
родител
ревизия
4d4b5e2387
No account linked to committer's email address

+ 61
- 18
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java Целия файл

4
 import android.annotation.TargetApi;
4
 import android.annotation.TargetApi;
5
 import android.app.DownloadManager;
5
 import android.app.DownloadManager;
6
 import android.content.Context;
6
 import android.content.Context;
7
-import android.content.Intent;
8
 import android.content.pm.ActivityInfo;
7
 import android.content.pm.ActivityInfo;
9
 import android.content.pm.PackageManager;
8
 import android.content.pm.PackageManager;
10
 import android.graphics.Bitmap;
9
 import android.graphics.Bitmap;
14
 import android.net.Uri;
13
 import android.net.Uri;
15
 import android.os.Build;
14
 import android.os.Build;
16
 import android.os.Environment;
15
 import android.os.Environment;
17
-import androidx.annotation.RequiresApi;
18
-import androidx.core.content.ContextCompat;
16
+import android.os.SystemClock;
19
 import android.text.TextUtils;
17
 import android.text.TextUtils;
20
 import android.util.Log;
18
 import android.util.Log;
21
 import android.view.Gravity;
19
 import android.view.Gravity;
41
 import android.webkit.WebViewClient;
39
 import android.webkit.WebViewClient;
42
 import android.widget.FrameLayout;
40
 import android.widget.FrameLayout;
43
 
41
 
42
+import androidx.annotation.Nullable;
43
+import androidx.annotation.RequiresApi;
44
+import androidx.core.content.ContextCompat;
45
+import androidx.core.util.Pair;
46
+
47
+import com.facebook.common.logging.FLog;
44
 import com.facebook.react.views.scroll.ScrollEvent;
48
 import com.facebook.react.views.scroll.ScrollEvent;
45
 import com.facebook.react.views.scroll.ScrollEventType;
49
 import com.facebook.react.views.scroll.ScrollEventType;
46
 import com.facebook.react.views.scroll.OnScrollDispatchHelper;
50
 import com.facebook.react.views.scroll.OnScrollDispatchHelper;
64
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
68
 import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
65
 import com.facebook.react.uimanager.events.Event;
69
 import com.facebook.react.uimanager.events.Event;
66
 import com.facebook.react.uimanager.events.EventDispatcher;
70
 import com.facebook.react.uimanager.events.EventDispatcher;
71
+import com.reactnativecommunity.webview.RNCWebViewModule.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState;
67
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
72
 import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
68
 import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
73
 import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
69
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
74
 import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
84
 import java.util.HashMap;
89
 import java.util.HashMap;
85
 import java.util.Locale;
90
 import java.util.Locale;
86
 import java.util.Map;
91
 import java.util.Map;
87
-
88
-import javax.annotation.Nullable;
92
+import java.util.concurrent.atomic.AtomicReference;
89
 
93
 
90
 /**
94
 /**
91
  * Manages instances of {@link WebView}
95
  * Manages instances of {@link WebView}
113
  */
117
  */
114
 @ReactModule(name = RNCWebViewManager.REACT_CLASS)
118
 @ReactModule(name = RNCWebViewManager.REACT_CLASS)
115
 public class RNCWebViewManager extends SimpleViewManager<WebView> {
119
 public class RNCWebViewManager extends SimpleViewManager<WebView> {
120
+  private static final String TAG = "RNCWebViewManager";
116
 
121
 
117
   public static final int COMMAND_GO_BACK = 1;
122
   public static final int COMMAND_GO_BACK = 1;
118
   public static final int COMMAND_GO_FORWARD = 2;
123
   public static final int COMMAND_GO_FORWARD = 2;
136
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
141
   // Use `webView.loadUrl("about:blank")` to reliably reset the view
137
   // state and release page resources (including any running JavaScript).
142
   // state and release page resources (including any running JavaScript).
138
   protected static final String BLANK_URL = "about:blank";
143
   protected static final String BLANK_URL = "about:blank";
144
+  protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250;
139
   protected WebViewConfig mWebViewConfig;
145
   protected WebViewConfig mWebViewConfig;
140
 
146
 
141
   protected RNCWebChromeClient mWebChromeClient = null;
147
   protected RNCWebChromeClient mWebChromeClient = null;
806
 
812
 
807
     @Override
813
     @Override
808
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
814
     public boolean shouldOverrideUrlLoading(WebView view, String url) {
809
-      progressChangedFilter.setWaitingForCommandLoadUrl(true);
810
-      dispatchEvent(
811
-        view,
812
-        new TopShouldStartLoadWithRequestEvent(
813
-          view.getId(),
814
-          createWebViewEvent(view, url)));
815
-      return true;
816
-    }
815
+      final RNCWebView rncWebView = (RNCWebView) view;
816
+      final boolean isJsDebugging = ((ReactContext) view.getContext()).getJavaScriptContextHolder().get() == 0;
817
 
817
 
818
+      if (!isJsDebugging && rncWebView.mCatalystInstance != null) {
819
+        final Pair<Integer, AtomicReference<ShouldOverrideCallbackState>> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock();
820
+        final int lockIdentifier = lock.first;
821
+        final AtomicReference<ShouldOverrideCallbackState> lockObject = lock.second;
822
+
823
+        final WritableMap event = createWebViewEvent(view, url);
824
+        event.putInt("lockIdentifier", lockIdentifier);
825
+        rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event);
826
+
827
+        try {
828
+          assert lockObject != null;
829
+          synchronized (lockObject) {
830
+            final long startTime = SystemClock.elapsedRealtime();
831
+            while (lockObject.get() == ShouldOverrideCallbackState.UNDECIDED) {
832
+              if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
833
+                FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading.");
834
+                RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
835
+                return false;
836
+              }
837
+              lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT);
838
+            }
839
+          }
840
+        } catch (InterruptedException e) {
841
+          FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e);
842
+          RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
843
+          return false;
844
+        }
845
+
846
+        final boolean shouldOverride = lockObject.get() == ShouldOverrideCallbackState.SHOULD_OVERRIDE;
847
+        RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
848
+
849
+        return shouldOverride;
850
+      } else {
851
+        FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load.");
852
+        progressChangedFilter.setWaitingForCommandLoadUrl(true);
853
+        dispatchEvent(
854
+          view,
855
+          new TopShouldStartLoadWithRequestEvent(
856
+            view.getId(),
857
+            createWebViewEvent(view, url)));
858
+        return true;
859
+      }
860
+    }
818
 
861
 
819
     @TargetApi(Build.VERSION_CODES.N)
862
     @TargetApi(Build.VERSION_CODES.N)
820
     @Override
863
     @Override
1164
      */
1207
      */
1165
     public RNCWebView(ThemedReactContext reactContext) {
1208
     public RNCWebView(ThemedReactContext reactContext) {
1166
       super(reactContext);
1209
       super(reactContext);
1210
+      this.createCatalystInstance();
1167
       progressChangedFilter = new ProgressChangedFilter();
1211
       progressChangedFilter = new ProgressChangedFilter();
1168
     }
1212
     }
1169
 
1213
 
1272
 
1316
 
1273
       if (enabled) {
1317
       if (enabled) {
1274
         addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
1318
         addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
1275
-        this.createCatalystInstance();
1276
       } else {
1319
       } else {
1277
         removeJavascriptInterface(JAVASCRIPT_INTERFACE);
1320
         removeJavascriptInterface(JAVASCRIPT_INTERFACE);
1278
       }
1321
       }
1328
             data.putString("data", message);
1371
             data.putString("data", message);
1329
 
1372
 
1330
             if (mCatalystInstance != null) {
1373
             if (mCatalystInstance != null) {
1331
-              mContext.sendDirectMessage(data);
1374
+              mContext.sendDirectMessage("onMessage", data);
1332
             } else {
1375
             } else {
1333
               dispatchEvent(webView, new TopMessageEvent(webView.getId(), data));
1376
               dispatchEvent(webView, new TopMessageEvent(webView.getId(), data));
1334
             }
1377
             }
1339
         eventData.putString("data", message);
1382
         eventData.putString("data", message);
1340
 
1383
 
1341
         if (mCatalystInstance != null) {
1384
         if (mCatalystInstance != null) {
1342
-          this.sendDirectMessage(eventData);
1385
+          this.sendDirectMessage("onMessage", eventData);
1343
         } else {
1386
         } else {
1344
           dispatchEvent(this, new TopMessageEvent(this.getId(), eventData));
1387
           dispatchEvent(this, new TopMessageEvent(this.getId(), eventData));
1345
         }
1388
         }
1346
       }
1389
       }
1347
     }
1390
     }
1348
 
1391
 
1349
-    protected void sendDirectMessage(WritableMap data) {
1392
+    protected void sendDirectMessage(final String method, WritableMap data) {
1350
       WritableNativeMap event = new WritableNativeMap();
1393
       WritableNativeMap event = new WritableNativeMap();
1351
       event.putMap("nativeEvent", data);
1394
       event.putMap("nativeEvent", data);
1352
 
1395
 
1353
       WritableNativeArray params = new WritableNativeArray();
1396
       WritableNativeArray params = new WritableNativeArray();
1354
       params.pushMap(event);
1397
       params.pushMap(event);
1355
 
1398
 
1356
-      mCatalystInstance.callFunction(messagingModuleName, "onMessage", params);
1399
+      mCatalystInstance.callFunction(messagingModuleName, method, params);
1357
     }
1400
     }
1358
 
1401
 
1359
     protected void onScrollChanged(int x, int y, int oldX, int oldY) {
1402
     protected void onScrollChanged(int x, int y, int oldX, int oldY) {

+ 44
- 0
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java Целия файл

12
 import android.os.Parcelable;
12
 import android.os.Parcelable;
13
 import android.provider.MediaStore;
13
 import android.provider.MediaStore;
14
 
14
 
15
+import androidx.annotation.Nullable;
15
 import androidx.annotation.RequiresApi;
16
 import androidx.annotation.RequiresApi;
16
 import androidx.core.content.ContextCompat;
17
 import androidx.core.content.ContextCompat;
17
 import androidx.core.content.FileProvider;
18
 import androidx.core.content.FileProvider;
19
+import androidx.core.util.Pair;
18
 
20
 
19
 import android.util.Log;
21
 import android.util.Log;
20
 import android.webkit.MimeTypeMap;
22
 import android.webkit.MimeTypeMap;
35
 import java.io.IOException;
37
 import java.io.IOException;
36
 import java.util.ArrayList;
38
 import java.util.ArrayList;
37
 import java.util.Arrays;
39
 import java.util.Arrays;
40
+import java.util.HashMap;
41
+import java.util.concurrent.atomic.AtomicReference;
38
 
42
 
39
 import static android.app.Activity.RESULT_OK;
43
 import static android.app.Activity.RESULT_OK;
40
 
44
 
50
   private File outputVideo;
54
   private File outputVideo;
51
   private DownloadManager.Request downloadRequest;
55
   private DownloadManager.Request downloadRequest;
52
 
56
 
57
+  protected static class ShouldOverrideUrlLoadingLock {
58
+    protected enum ShouldOverrideCallbackState {
59
+      UNDECIDED,
60
+      SHOULD_OVERRIDE,
61
+      DO_NOT_OVERRIDE,
62
+    }
63
+
64
+    private int nextLockIdentifier = 0;
65
+    private final HashMap<Integer, AtomicReference<ShouldOverrideCallbackState>> shouldOverrideLocks = new HashMap<>();
66
+
67
+    public synchronized Pair<Integer, AtomicReference<ShouldOverrideCallbackState>> getNewLock() {
68
+      final int lockIdentifier = nextLockIdentifier++;
69
+      final AtomicReference<ShouldOverrideCallbackState> shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED);
70
+      shouldOverrideLocks.put(lockIdentifier, shouldOverride);
71
+      return new Pair<>(lockIdentifier, shouldOverride);
72
+    }
73
+
74
+    @Nullable
75
+    public synchronized AtomicReference<ShouldOverrideCallbackState> getLock(Integer lockIdentifier) {
76
+      return shouldOverrideLocks.get(lockIdentifier);
77
+    }
78
+
79
+    public synchronized void removeLock(Integer lockIdentifier) {
80
+      shouldOverrideLocks.remove(lockIdentifier);
81
+    }
82
+  }
83
+
84
+  protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock();
85
+
53
   private enum MimeType {
86
   private enum MimeType {
54
     DEFAULT("*/*"),
87
     DEFAULT("*/*"),
55
     IMAGE("image"),
88
     IMAGE("image"),
105
     promise.resolve(result);
138
     promise.resolve(result);
106
   }
139
   }
107
 
140
 
141
+  @ReactMethod(isBlockingSynchronousMethod = true)
142
+  public void onShouldStartLoadWithRequestCallback(final boolean shouldStart, final int lockIdentifier) {
143
+    final AtomicReference<ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState> lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier);
144
+    if (lockObject != null) {
145
+      synchronized (lockObject) {
146
+        lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE);
147
+        lockObject.notify();
148
+      }
149
+    }
150
+  }
151
+
108
   public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
152
   public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
109
 
153
 
110
     if (filePathCallback == null && filePathCallbackLegacy == null) {
154
     if (filePathCallback == null && filePathCallbackLegacy == null) {

+ 7
- 3
src/WebView.android.tsx Целия файл

77
     lastErrorEvent: null,
77
     lastErrorEvent: null,
78
   };
78
   };
79
 
79
 
80
+  onShouldStartLoadWithRequest: ReturnType<typeof createOnShouldStartLoadWithRequest> | null = null;
80
 
81
 
81
   webViewRef = React.createRef<NativeWebViewAndroid>();
82
   webViewRef = React.createRef<NativeWebViewAndroid>();
82
 
83
 
280
   onShouldStartLoadWithRequestCallback = (
281
   onShouldStartLoadWithRequestCallback = (
281
     shouldStart: boolean,
282
     shouldStart: boolean,
282
     url: string,
283
     url: string,
284
+    lockIdentifier?: number,
283
   ) => {
285
   ) => {
284
-    if (shouldStart) {
286
+    if (lockIdentifier) {
287
+      NativeModules.RNCWebView.onShouldStartLoadWithRequestCallback(shouldStart, lockIdentifier);
288
+    } else if (shouldStart) {
285
       UIManager.dispatchViewManagerCommand(
289
       UIManager.dispatchViewManagerCommand(
286
         this.getWebViewHandle(),
290
         this.getWebViewHandle(),
287
         this.getCommands().loadUrl,
291
         this.getCommands().loadUrl,
338
     const NativeWebView
342
     const NativeWebView
339
       = (nativeConfig.component as typeof NativeWebViewAndroid) || RNCWebView;
343
       = (nativeConfig.component as typeof NativeWebViewAndroid) || RNCWebView;
340
 
344
 
341
-    const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
345
+    this.onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
342
       this.onShouldStartLoadWithRequestCallback,
346
       this.onShouldStartLoadWithRequestCallback,
343
       // casting cause it's in the default props
347
       // casting cause it's in the default props
344
       originWhitelist as readonly string[],
348
       originWhitelist as readonly string[],
358
         onHttpError={this.onHttpError}
362
         onHttpError={this.onHttpError}
359
         onRenderProcessGone={this.onRenderProcessGone}
363
         onRenderProcessGone={this.onRenderProcessGone}
360
         onMessage={this.onMessage}
364
         onMessage={this.onMessage}
361
-        onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
365
+        onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
362
         ref={this.webViewRef}
366
         ref={this.webViewRef}
363
         // TODO: find a better way to type this.
367
         // TODO: find a better way to type this.
364
         source={resolveAssetSource(source as ImageSourcePropType)}
368
         source={resolveAssetSource(source as ImageSourcePropType)}