Skip to content

Commit 773a3a4

Browse files
committed
Synchronously decide if Android WebView should load a URL or not.
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.
1 parent 6598325 commit 773a3a4

File tree

3 files changed

+112
-21
lines changed

3 files changed

+112
-21
lines changed

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

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import android.annotation.TargetApi;
55
import android.app.DownloadManager;
66
import android.content.Context;
7-
import android.content.Intent;
87
import android.content.pm.ActivityInfo;
98
import android.content.pm.PackageManager;
109
import android.graphics.Bitmap;
@@ -14,8 +13,7 @@
1413
import android.net.Uri;
1514
import android.os.Build;
1615
import android.os.Environment;
17-
import androidx.annotation.RequiresApi;
18-
import androidx.core.content.ContextCompat;
16+
import android.os.SystemClock;
1917
import android.text.TextUtils;
2018
import android.util.Log;
2119
import android.view.Gravity;
@@ -41,6 +39,12 @@
4139
import android.webkit.WebViewClient;
4240
import android.widget.FrameLayout;
4341

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;
4448
import com.facebook.react.views.scroll.ScrollEvent;
4549
import com.facebook.react.views.scroll.ScrollEventType;
4650
import com.facebook.react.views.scroll.OnScrollDispatchHelper;
@@ -64,6 +68,7 @@
6468
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
6569
import com.facebook.react.uimanager.events.Event;
6670
import com.facebook.react.uimanager.events.EventDispatcher;
71+
import com.reactnativecommunity.webview.RNCWebViewModule.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState;
6772
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
6873
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
6974
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
@@ -84,8 +89,7 @@
8489
import java.util.HashMap;
8590
import java.util.Locale;
8691
import java.util.Map;
87-
88-
import javax.annotation.Nullable;
92+
import java.util.concurrent.atomic.AtomicReference;
8993

9094
/**
9195
* Manages instances of {@link WebView}
@@ -113,6 +117,7 @@
113117
*/
114118
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
115119
public class RNCWebViewManager extends SimpleViewManager<WebView> {
120+
private static final String TAG = "RNCWebViewManager";
116121

117122
public static final int COMMAND_GO_BACK = 1;
118123
public static final int COMMAND_GO_FORWARD = 2;
@@ -136,6 +141,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
136141
// Use `webView.loadUrl("about:blank")` to reliably reset the view
137142
// state and release page resources (including any running JavaScript).
138143
protected static final String BLANK_URL = "about:blank";
144+
protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250;
139145
protected WebViewConfig mWebViewConfig;
140146

141147
protected RNCWebChromeClient mWebChromeClient = null;
@@ -806,15 +812,52 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) {
806812

807813
@Override
808814
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;
817817

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+
}
818861

819862
@TargetApi(Build.VERSION_CODES.N)
820863
@Override
@@ -1164,6 +1207,7 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe
11641207
*/
11651208
public RNCWebView(ThemedReactContext reactContext) {
11661209
super(reactContext);
1210+
this.createCatalystInstance();
11671211
progressChangedFilter = new ProgressChangedFilter();
11681212
}
11691213

@@ -1272,7 +1316,6 @@ public void setMessagingEnabled(boolean enabled) {
12721316

12731317
if (enabled) {
12741318
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
1275-
this.createCatalystInstance();
12761319
} else {
12771320
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
12781321
}
@@ -1328,7 +1371,7 @@ public void run() {
13281371
data.putString("data", message);
13291372

13301373
if (mCatalystInstance != null) {
1331-
mContext.sendDirectMessage(data);
1374+
mContext.sendDirectMessage("onMessage", data);
13321375
} else {
13331376
dispatchEvent(webView, new TopMessageEvent(webView.getId(), data));
13341377
}
@@ -1339,21 +1382,21 @@ public void run() {
13391382
eventData.putString("data", message);
13401383

13411384
if (mCatalystInstance != null) {
1342-
this.sendDirectMessage(eventData);
1385+
this.sendDirectMessage("onMessage", eventData);
13431386
} else {
13441387
dispatchEvent(this, new TopMessageEvent(this.getId(), eventData));
13451388
}
13461389
}
13471390
}
13481391

1349-
protected void sendDirectMessage(WritableMap data) {
1392+
protected void sendDirectMessage(final String method, WritableMap data) {
13501393
WritableNativeMap event = new WritableNativeMap();
13511394
event.putMap("nativeEvent", data);
13521395

13531396
WritableNativeArray params = new WritableNativeArray();
13541397
params.pushMap(event);
13551398

1356-
mCatalystInstance.callFunction(messagingModuleName, "onMessage", params);
1399+
mCatalystInstance.callFunction(messagingModuleName, method, params);
13571400
}
13581401

13591402
protected void onScrollChanged(int x, int y, int oldX, int oldY) {

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
import android.os.Parcelable;
1313
import android.provider.MediaStore;
1414

15+
import androidx.annotation.Nullable;
1516
import androidx.annotation.RequiresApi;
1617
import androidx.core.content.ContextCompat;
1718
import androidx.core.content.FileProvider;
19+
import androidx.core.util.Pair;
1820

1921
import android.util.Log;
2022
import android.webkit.MimeTypeMap;
@@ -35,6 +37,8 @@
3537
import java.io.IOException;
3638
import java.util.ArrayList;
3739
import java.util.Arrays;
40+
import java.util.HashMap;
41+
import java.util.concurrent.atomic.AtomicReference;
3842

3943
import static android.app.Activity.RESULT_OK;
4044

@@ -50,6 +54,35 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
5054
private File outputVideo;
5155
private DownloadManager.Request downloadRequest;
5256

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+
5386
private enum MimeType {
5487
DEFAULT("*/*"),
5588
IMAGE("image"),
@@ -105,6 +138,17 @@ public void isFileUploadSupported(final Promise promise) {
105138
promise.resolve(result);
106139
}
107140

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+
108152
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
109153

110154
if (filePathCallback == null && filePathCallbackLegacy == null) {

src/WebView.android.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
7777
lastErrorEvent: null,
7878
};
7979

80+
onShouldStartLoadWithRequest: ReturnType<typeof createOnShouldStartLoadWithRequest> | null = null;
8081

8182
webViewRef = React.createRef<NativeWebViewAndroid>();
8283

@@ -280,8 +281,11 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
280281
onShouldStartLoadWithRequestCallback = (
281282
shouldStart: boolean,
282283
url: string,
284+
lockIdentifier?: number,
283285
) => {
284-
if (shouldStart) {
286+
if (lockIdentifier) {
287+
NativeModules.RNCWebView.onShouldStartLoadWithRequestCallback(shouldStart, lockIdentifier);
288+
} else if (shouldStart) {
285289
UIManager.dispatchViewManagerCommand(
286290
this.getWebViewHandle(),
287291
this.getCommands().loadUrl,
@@ -338,7 +342,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
338342
const NativeWebView
339343
= (nativeConfig.component as typeof NativeWebViewAndroid) || RNCWebView;
340344

341-
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
345+
this.onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
342346
this.onShouldStartLoadWithRequestCallback,
343347
// casting cause it's in the default props
344348
originWhitelist as readonly string[],
@@ -358,7 +362,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
358362
onHttpError={this.onHttpError}
359363
onRenderProcessGone={this.onRenderProcessGone}
360364
onMessage={this.onMessage}
361-
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
365+
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
362366
ref={this.webViewRef}
363367
// TODO: find a better way to type this.
364368
source={resolveAssetSource(source as ImageSourcePropType)}

0 commit comments

Comments
 (0)