diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index a5a18507d..8d2cdc271 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -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; @@ -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; @@ -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; @@ -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; @@ -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} @@ -113,6 +117,7 @@ */ @ReactModule(name = RNCWebViewManager.REACT_CLASS) public class RNCWebViewManager extends SimpleViewManager { + private static final String TAG = "RNCWebViewManager"; public static final int COMMAND_GO_BACK = 1; public static final int COMMAND_GO_FORWARD = 2; @@ -136,6 +141,7 @@ public class RNCWebViewManager extends SimpleViewManager { // 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; @@ -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; @@ -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> lock = RNCWebViewModule.shouldOverrideUrlLoadingLock.getNewLock(); + final int lockIdentifier = lock.first; + final AtomicReference 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 @@ -1149,6 +1207,7 @@ protected static class RNCWebView extends WebView implements LifecycleEventListe */ public RNCWebView(ThemedReactContext reactContext) { super(reactContext); + this.createCatalystInstance(); progressChangedFilter = new ProgressChangedFilter(); } @@ -1257,7 +1316,6 @@ public void setMessagingEnabled(boolean enabled) { if (enabled) { addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE); - this.createCatalystInstance(); } else { removeJavascriptInterface(JAVASCRIPT_INTERFACE); } @@ -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)); } @@ -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) { diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java index 465353d3d..d0e7fb367 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java @@ -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; @@ -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; @@ -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> shouldOverrideLocks = new HashMap<>(); + + public synchronized Pair> getNewLock() { + final int lockIdentifier = nextLockIdentifier++; + final AtomicReference shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED); + shouldOverrideLocks.put(lockIdentifier, shouldOverride); + return new Pair<>(lockIdentifier, shouldOverride); + } + + @Nullable + public synchronized AtomicReference 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"), @@ -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 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) { diff --git a/docs/Reference.md b/docs/Reference.md index 8740d9d03..2683272b3 100644 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -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) @@ -781,7 +782,7 @@ A Boolean value indicating whether JavaScript can open windows without user inte ### `androidHardwareAccelerationDisabled`[⬆](#props-index) -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 | | ---- | -------- | -------- | @@ -789,6 +790,24 @@ Boolean value to disable Hardware Acceleration in the `WebView`. Used on Android --- +### `androidLayerType`[⬆](#props-index) + +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) Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin. diff --git a/package.json b/package.json index faa147c3e..527bed6c1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "Thibault Malbranche " ], "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", diff --git a/src/WebView.android.tsx b/src/WebView.android.tsx index 5e68a3577..bdc802769 100644 --- a/src/WebView.android.tsx +++ b/src/WebView.android.tsx @@ -61,6 +61,7 @@ class WebView extends React.Component { saveFormDataDisabled: false, cacheEnabled: true, androidHardwareAccelerationDisabled: false, + androidLayerType: 'none', originWhitelist: defaultOriginWhitelist, }; @@ -76,6 +77,7 @@ class WebView extends React.Component { lastErrorEvent: null, }; + onShouldStartLoadWithRequest: ReturnType | null = null; webViewRef = React.createRef(); @@ -279,8 +281,11 @@ class WebView extends React.Component { 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, @@ -337,7 +342,7 @@ class WebView extends React.Component { 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[], @@ -357,7 +362,7 @@ class WebView extends React.Component { 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)} diff --git a/src/WebViewTypes.ts b/src/WebViewTypes.ts index df880159b..525d895f7 100644 --- a/src/WebViewTypes.ts +++ b/src/WebViewTypes.ts @@ -182,6 +182,8 @@ export type OverScrollModeType = 'always' | 'content' | 'never'; export type CacheMode = 'LOAD_DEFAULT' | 'LOAD_CACHE_ONLY' | 'LOAD_CACHE_ELSE_NETWORK' | 'LOAD_NO_CACHE'; +export type AndroidLayerType = 'none' | 'software' | 'hardware'; + export interface WebViewSourceUri { /** * The URI to load in the `WebView`. Can be a local or remote file. @@ -284,6 +286,7 @@ export interface AndroidNativeWebViewProps extends CommonNativeWebViewProps { allowFileAccessFromFileURLs?: boolean; allowUniversalAccessFromFileURLs?: boolean; androidHardwareAccelerationDisabled?: boolean; + androidLayerType?: AndroidLayerType; domStorageEnabled?: boolean; geolocationEnabled?: boolean; javaScriptEnabled?: boolean; @@ -809,6 +812,18 @@ export interface AndroidWebViewProps extends WebViewSharedProps { */ androidHardwareAccelerationDisabled?: boolean; + /** + * https://developer.android.com/reference/android/webkit/WebView#setLayerType(int,%20android.graphics.Paint) + * Sets the layerType. Possible values are: + * + * - `'none'` (default) + * - `'software'` + * - `'hardware'` + * + * @platform android + */ + androidLayerType?: AndroidLayerType; + /** * Boolean value to enable third party cookies in the `WebView`. Used on * Android Lollipop and above only as third party cookies are enabled by