Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android sync java on should start load with request #8

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import android.annotation.TargetApi;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
Expand All @@ -14,8 +13,7 @@
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
Expand All @@ -41,6 +39,12 @@
import android.webkit.WebViewClient;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair;

import com.facebook.common.logging.FLog;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
import com.facebook.react.views.scroll.OnScrollDispatchHelper;
Expand All @@ -64,6 +68,7 @@
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.reactnativecommunity.webview.RNCWebViewModule.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
Expand All @@ -84,8 +89,7 @@
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.annotation.Nullable;
import java.util.concurrent.atomic.AtomicReference;

/**
* Manages instances of {@link WebView}
Expand Down Expand Up @@ -113,6 +117,7 @@
*/
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
public class RNCWebViewManager extends SimpleViewManager<WebView> {
private static final String TAG = "RNCWebViewManager";

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

protected RNCWebChromeClient mWebChromeClient = null;
Expand Down Expand Up @@ -299,6 +305,21 @@ public void setHardwareAccelerationDisabled(WebView view, boolean disabled) {
}
}

@ReactProp(name = "androidLayerType")
public void setLayerType(WebView view, String layerTypeString) {
int layerType = View.LAYER_TYPE_NONE;
switch (layerTypeString) {
case "hardware":
layerType = View.LAYER_TYPE_HARDWARE;
break;
case "software":
layerType = View.LAYER_TYPE_SOFTWARE;
break;
}
view.setLayerType(layerType, null);
}


@ReactProp(name = "overScrollMode")
public void setOverScrollMode(WebView view, String overScrollModeString) {
Integer overScrollMode;
Expand Down Expand Up @@ -791,15 +812,52 @@ public void onPageStarted(WebView webView, String url, Bitmap favicon) {

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
progressChangedFilter.setWaitingForCommandLoadUrl(true);
dispatchEvent(
view,
new TopShouldStartLoadWithRequestEvent(
view.getId(),
createWebViewEvent(view, url)));
return true;
}
final RNCWebView rncWebView = (RNCWebView) view;
final boolean isJsDebugging = ((ReactContext) view.getContext()).getJavaScriptContextHolder().get() == 0;

if (!isJsDebugging && rncWebView.mCatalystInstance != null) {
final Pair<Integer, AtomicReference<ShouldOverrideCallbackState>> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock();
final int lockIdentifier = lock.first;
final AtomicReference<ShouldOverrideCallbackState> lockObject = lock.second;

final WritableMap event = createWebViewEvent(view, url);
event.putInt("lockIdentifier", lockIdentifier);
rncWebView.sendDirectMessage("onShouldStartLoadWithRequest", event);

try {
assert lockObject != null;
synchronized (lockObject) {
final long startTime = SystemClock.elapsedRealtime();
while (lockObject.get() == ShouldOverrideCallbackState.UNDECIDED) {
if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading.");
RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
return false;
}
lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT);
}
}
} catch (InterruptedException e) {
FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e);
RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
return false;
}

final boolean shouldOverride = lockObject.get() == ShouldOverrideCallbackState.SHOULD_OVERRIDE;
RNCWebViewModule.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);

return shouldOverride;
} else {
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.");
progressChangedFilter.setWaitingForCommandLoadUrl(true);
dispatchEvent(
view,
new TopShouldStartLoadWithRequestEvent(
view.getId(),
createWebViewEvent(view, url)));
return true;
}
}

@TargetApi(Build.VERSION_CODES.N)
@Override
Expand Down Expand Up @@ -1149,6 +1207,7 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe
*/
public RNCWebView(ThemedReactContext reactContext) {
super(reactContext);
this.createCatalystInstance();
progressChangedFilter = new ProgressChangedFilter();
}

Expand Down Expand Up @@ -1257,7 +1316,6 @@ public void setMessagingEnabled(boolean enabled) {

if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
this.createCatalystInstance();
} else {
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
Expand Down Expand Up @@ -1313,7 +1371,7 @@ public void run() {
data.putString("data", message);

if (mCatalystInstance != null) {
mContext.sendDirectMessage(data);
mContext.sendDirectMessage("onMessage", data);
} else {
dispatchEvent(webView, new TopMessageEvent(webView.getId(), data));
}
Expand All @@ -1324,21 +1382,21 @@ public void run() {
eventData.putString("data", message);

if (mCatalystInstance != null) {
this.sendDirectMessage(eventData);
this.sendDirectMessage("onMessage", eventData);
} else {
dispatchEvent(this, new TopMessageEvent(this.getId(), eventData));
}
}
}

protected void sendDirectMessage(WritableMap data) {
protected void sendDirectMessage(final String method, WritableMap data) {
WritableNativeMap event = new WritableNativeMap();
event.putMap("nativeEvent", data);

WritableNativeArray params = new WritableNativeArray();
params.pushMap(event);

mCatalystInstance.callFunction(messagingModuleName, "onMessage", params);
mCatalystInstance.callFunction(messagingModuleName, method, params);
}

protected void onScrollChanged(int x, int y, int oldX, int oldY) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import android.os.Parcelable;
import android.provider.MediaStore;

import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.util.Pair;

import android.util.Log;
import android.webkit.MimeTypeMap;
Expand All @@ -35,6 +37,8 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;

import static android.app.Activity.RESULT_OK;

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

protected static class ShouldOverrideUrlLoadingLock {
protected enum ShouldOverrideCallbackState {
UNDECIDED,
SHOULD_OVERRIDE,
DO_NOT_OVERRIDE,
}

private int nextLockIdentifier = 0;
private final HashMap<Integer, AtomicReference<ShouldOverrideCallbackState>> shouldOverrideLocks = new HashMap<>();

public synchronized Pair<Integer, AtomicReference<ShouldOverrideCallbackState>> getNewLock() {
final int lockIdentifier = nextLockIdentifier++;
final AtomicReference<ShouldOverrideCallbackState> shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED);
shouldOverrideLocks.put(lockIdentifier, shouldOverride);
return new Pair<>(lockIdentifier, shouldOverride);
}

@Nullable
public synchronized AtomicReference<ShouldOverrideCallbackState> getLock(Integer lockIdentifier) {
return shouldOverrideLocks.get(lockIdentifier);
}

public synchronized void removeLock(Integer lockIdentifier) {
shouldOverrideLocks.remove(lockIdentifier);
}
}

protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock();

private enum MimeType {
DEFAULT("*/*"),
IMAGE("image"),
Expand Down Expand Up @@ -105,6 +138,17 @@ public void isFileUploadSupported(final Promise promise) {
promise.resolve(result);
}

@ReactMethod(isBlockingSynchronousMethod = true)
public void onShouldStartLoadWithRequestCallback(final boolean shouldStart, final int lockIdentifier) {
final AtomicReference<ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState> lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier);
if (lockObject != null) {
synchronized (lockObject) {
lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE);
lockObject.notify();
}
}
}

public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {

if (filePathCallback == null && filePathCallbackLegacy == null) {
Expand Down
21 changes: 20 additions & 1 deletion docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This document lays out the current public properties and methods for the React N
- [`javaScriptEnabled`](Reference.md#javascriptenabled)
- [`javaScriptCanOpenWindowsAutomatically`](Reference.md#javascriptcanopenwindowsautomatically)
- [`androidHardwareAccelerationDisabled`](Reference.md#androidHardwareAccelerationDisabled)
- [`androidLayerType`](Reference.md#androidLayerType)
- [`mixedContentMode`](Reference.md#mixedcontentmode)
- [`thirdPartyCookiesEnabled`](Reference.md#thirdpartycookiesenabled)
- [`userAgent`](Reference.md#useragent)
Expand Down Expand Up @@ -781,14 +782,32 @@ A Boolean value indicating whether JavaScript can open windows without user inte

### `androidHardwareAccelerationDisabled`[⬆](#props-index)<!-- Link generated with jump2header -->

Boolean value to disable Hardware Acceleration in the `WebView`. Used on Android only as Hardware Acceleration is a feature only for Android. The default value is `false`.
**Deprecated.** Use the `androidLayerType` prop instead.

| Type | Required | Platform |
| ---- | -------- | -------- |
| bool | No | Android |

---

### `androidLayerType`[⬆](#props-index)<!-- Link generated with jump2header -->

Specifies the layer type.

Possible values for `androidLayerType` are:

- `none` (default) - The view does not have a layer.
- `software` - The view has a software layer. A software layer is backed by a bitmap and causes the view to be rendered using Android's software rendering pipeline, even if hardware acceleration is enabled.
- `hardware` - The view has a hardware layer. A hardware layer is backed by a hardware specific texture and causes the view to be rendered using Android's hardware rendering pipeline, but only if hardware acceleration is turned on for the view hierarchy.

Avoid setting both `androidLayerType` and `androidHardwareAccelerationDisabled` props at the same time, as this may cause undefined behaviour.

| Type | Required | Platform |
| ------ | -------- | -------- |
| string | No | Android |

---

### `mixedContentMode`[⬆](#props-index)<!-- Link generated with jump2header -->

Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"Thibault Malbranche <[email protected]>"
],
"license": "MIT",
"version": "10.7.0",
"version": "10.8.0",
"homepage": "https://github.com/react-native-community/react-native-webview#readme",
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
Expand Down
11 changes: 8 additions & 3 deletions src/WebView.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
saveFormDataDisabled: false,
cacheEnabled: true,
androidHardwareAccelerationDisabled: false,
androidLayerType: 'none',
originWhitelist: defaultOriginWhitelist,
};

Expand All @@ -76,6 +77,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
lastErrorEvent: null,
};

onShouldStartLoadWithRequest: ReturnType<typeof createOnShouldStartLoadWithRequest> | null = null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps rename things in this file to make it more clear to the reader that this is distinct? It's kind of confusing the way things are written now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll play with it and see how much better I can make it.


webViewRef = React.createRef<NativeWebViewAndroid>();

Expand Down Expand Up @@ -279,8 +281,11 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
url: string,
lockIdentifier?: number,
) => {
if (shouldStart) {
if (lockIdentifier) {
NativeModules.RNCWebView.onShouldStartLoadWithRequestCallback(shouldStart, lockIdentifier);
} else if (shouldStart) {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().loadUrl,
Expand Down Expand Up @@ -337,7 +342,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
const NativeWebView
= (nativeConfig.component as typeof NativeWebViewAndroid) || RNCWebView;

const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
// casting cause it's in the default props
originWhitelist as readonly string[],
Expand All @@ -357,7 +362,7 @@ class WebView extends React.Component<AndroidWebViewProps, State> {
onHttpError={this.onHttpError}
onRenderProcessGone={this.onRenderProcessGone}
onMessage={this.onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
ref={this.webViewRef}
// TODO: find a better way to type this.
source={resolveAssetSource(source as ImageSourcePropType)}
Expand Down
Loading