diff --git a/Libraries/Components/WebView/WebView.android.js b/Libraries/Components/WebView/WebView.android.js index ebf06456671dd7..8b939e1fdb5cf5 100644 --- a/Libraries/Components/WebView/WebView.android.js +++ b/Libraries/Components/WebView/WebView.android.js @@ -33,6 +33,8 @@ var WebViewState = keyMirror({ ERROR: null, }); +type Event = Object; + /** * Renders a native WebView. */ @@ -51,6 +53,7 @@ var WebView = React.createClass({ onNavigationStateChange: PropTypes.func, startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load style: View.propTypes.style, + onShouldStartLoadWithRequest: PropTypes.func, html: deprecatedPropType( PropTypes.string, @@ -195,6 +198,15 @@ var WebView = React.createClass({ console.warn('WebView: `source.body` is not supported when using GET.'); } + var onShouldOverrideUrlLoading = this.props.onShouldStartLoadWithRequest + && ((event: Event) => { + var shouldOverride = !this.props.onShouldStartLoadWithRequest(event.nativeEvent); + UIManager.dispatchViewManagerCommandSync( + this.getWebViewHandle(), + UIManager.RCTWebView.Commands.shouldOverrideWithResult, + [shouldOverride]); + }); + var webView = ; return ( diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index b5939823acf242..d73b543f5d4f8d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -585,8 +585,7 @@ public void onCancel() { } } - public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray args) { - UiThreadUtil.assertOnUiThread(); + private void dispatchCommandCommon(int reactTag, int commandId, @Nullable ReadableArray args) { View view = mTagsToViews.get(reactTag); if (view == null) { throw new IllegalViewOperationException("Trying to send command to a non-existing view " + @@ -597,6 +596,16 @@ public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray viewManager.receiveCommand(view, commandId, args); } + public void dispatchCommandSync(int reactTag, int commandId, @Nullable ReadableArray args) { + dispatchCommandCommon(reactTag, commandId, args); + } + + public void dispatchCommand(int reactTag, int commandId, @Nullable ReadableArray args) { + UiThreadUtil.assertOnUiThread(); + dispatchCommandCommon(reactTag, commandId, args); + } + + /** * Show a {@link PopupMenu}. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java index b407c416a5a9f7..981459762b8e86 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java @@ -538,6 +538,11 @@ public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArra mOperationsQueue.enqueueDispatchCommand(reactTag, commandId, commandArgs); } + public void dispatchViewManagerCommandSync(int reactTag, int commandId, ReadableArray commandArgs) { + assertViewExists(reactTag, "dispatchViewManagerCommandSync"); + mOperationsQueue.executeDispatchCommand(reactTag, commandId, commandArgs); + } + /** * Show a PopupMenu. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index ddeb94e6db2ef3..6598a676dc1145 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -356,6 +356,11 @@ public void dispatchViewManagerCommand(int reactTag, int commandId, ReadableArra mUIImplementation.dispatchViewManagerCommand(reactTag, commandId, commandArgs); } + @ReactMethod + public void dispatchViewManagerCommandSync(int reactTag, int commandId, ReadableArray commandArgs) { + mUIImplementation.dispatchViewManagerCommandSync(reactTag, commandId, commandArgs); + } + /** * Show a PopupMenu. * diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index b3b931e0f8ef52..6f2ab709b51ca4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -236,6 +236,10 @@ public DispatchCommandOperation(int tag, int command, @Nullable ReadableArray ar public void execute() { mNativeViewHierarchyManager.dispatchCommand(mTag, mCommand, mArgs); } + + public void executeSync() { + mNativeViewHierarchyManager.dispatchCommandSync(mTag, mCommand, mArgs); + } } private final class ShowPopupMenuOperation extends ViewOperation { @@ -583,6 +587,14 @@ public void enqueueDispatchCommand( mOperations.add(new DispatchCommandOperation(reactTag, commandId, commandArgs)); } + public void executeDispatchCommand( + int reactTag, + int commandId, + ReadableArray commandArgs) { + DispatchCommandOperation operation = new DispatchCommandOperation(reactTag, commandId, commandArgs); + operation.executeSync(); + } + public void enqueueUpdateExtraData(int reactTag, Object extraData) { mOperations.add(new UpdateViewExtraData(reactTag, extraData)); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java index 40c2845c8adb52..43484abd2500e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/Event.java @@ -63,6 +63,10 @@ public boolean canCoalesce() { return true; } + public boolean isSync() { + return false; + } + /** * Given two events, coalesce them into a single event that will be sent to JS instead of two * separate events. By default, just chooses the one the is more recent. diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java index 537f1bfb46ba46..bdcedadb9a51e4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/EventDispatcher.java @@ -112,6 +112,32 @@ public EventDispatcher(ReactApplicationContext reactContext) { public void dispatchEvent(Event event) { Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized"); synchronized (mEventsStagingLock) { + + if (event.isSync()) { + synchronized (mEventsToDispatchLock) { + addEventToEventsToDispatch(event); + mReactContext.runOnJSQueueThread(new Runnable() { + @Override + public void run() { + synchronized (mEventsToDispatchLock) { + for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) { + Event event = mEventsToDispatch[eventIdx]; + if (event == null) { + continue; + } + + event.dispatch(mRCTEventEmitter); + event.dispose(); + + } + clearEventsToDispatch(); + } + } + }); + } + return; + } + mEventStaging.add(event); Systrace.startAsyncFlow( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java index 616418bba35b37..896d3a9b315ea4 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/ReactWebViewManager.java @@ -17,6 +17,7 @@ import android.graphics.Bitmap; import android.os.Build; +import android.os.ConditionVariable; import android.text.TextUtils; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -24,6 +25,7 @@ import com.facebook.catalyst.views.webview.events.TopLoadingErrorEvent; import com.facebook.catalyst.views.webview.events.TopLoadingFinishEvent; import com.facebook.catalyst.views.webview.events.TopLoadingStartEvent; +import com.facebook.react.views.webview.events.ShouldOverrideUrlLoadingEvent; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactContext; @@ -40,6 +42,7 @@ import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.Event; import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.common.MapBuilder; /** * Manages instances of {@link WebView} @@ -53,6 +56,7 @@ * - topLoadingFinish * - topLoadingStart * - topLoadingError + * - topShouldOverrideUrlLoading * * Each event will carry the following properties: * - target - view's react tag @@ -74,6 +78,7 @@ public class ReactWebViewManager extends SimpleViewManager { public static final int COMMAND_GO_BACK = 1; public static final int COMMAND_GO_FORWARD = 2; public static final int COMMAND_RELOAD = 3; + public static final int COMMAND_SHOULD_OVERRIDE_WITH_RESULT = 4; // Use `webView.loadUrl("about:blank")` to reliably reset the view // state and release page resources (including any running JavaScript). @@ -143,6 +148,23 @@ public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload createWebViewEvent(webView, url))); } + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + ReactWebView view = ((ReactWebView)webView); + view.mShouldOverrideUrlLoadingResult = false; + dispatchEvent( + webView, + new ShouldOverrideUrlLoadingEvent( + webView.getId(), + SystemClock.nanoTime(), + createWebViewEvent(webView, url))); + + view.mShouldOverrideUrlLoadingConditionVariable.close(); + view.mShouldOverrideUrlLoadingConditionVariable.block(250); + + return view.mShouldOverrideUrlLoadingResult; + } + private void emitFinishEvent(WebView webView, String url) { dispatchEvent( webView, @@ -180,6 +202,9 @@ private WritableMap createWebViewEvent(WebView webView, String url) { private static class ReactWebView extends WebView implements LifecycleEventListener { private @Nullable String injectedJS; + protected ConditionVariable mShouldOverrideUrlLoadingConditionVariable; + protected boolean mShouldOverrideUrlLoadingResult; + /** * WebView must be created with an context of the current activity * @@ -189,6 +214,7 @@ private static class ReactWebView extends WebView implements LifecycleEventListe */ public ReactWebView(ThemedReactContext reactContext) { super(reactContext); + mShouldOverrideUrlLoadingConditionVariable = new ConditionVariable(); } @Override @@ -222,6 +248,11 @@ private void cleanupCallbacksAndDestroy() { setWebViewClient(null); destroy(); } + + private void shouldOverrideWithResult(ReadableArray args) { + this.mShouldOverrideUrlLoadingResult = args.getBoolean(0); + this.mShouldOverrideUrlLoadingConditionVariable.open(); + } } public ReactWebViewManager() { @@ -342,7 +373,8 @@ protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { return MapBuilder.of( "goBack", COMMAND_GO_BACK, "goForward", COMMAND_GO_FORWARD, - "reload", COMMAND_RELOAD); + "reload", COMMAND_RELOAD, + "shouldOverrideWithResult", COMMAND_SHOULD_OVERRIDE_WITH_RESULT); } @Override @@ -357,6 +389,9 @@ public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray case COMMAND_RELOAD: root.reload(); break; + case COMMAND_SHOULD_OVERRIDE_WITH_RESULT: + ((ReactWebView) root).shouldOverrideWithResult(args); + break; } } @@ -366,4 +401,11 @@ public void onDropViewInstance(WebView webView) { ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((ReactWebView) webView); ((ReactWebView) webView).cleanupCallbacksAndDestroy(); } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.builder() + .put("topShouldOverrideUrlLoading", MapBuilder.of("registrationName", "onShouldOverrideUrlLoading")) + .build(); + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/ShouldOverrideUrlLoadingEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/ShouldOverrideUrlLoadingEvent.java new file mode 100644 index 00000000000000..949769e006c8fc --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/webview/events/ShouldOverrideUrlLoadingEvent.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.views.webview.events; + +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Event emitted when loading has started + */ +public class ShouldOverrideUrlLoadingEvent extends Event { + + public static final String EVENT_NAME = "topShouldOverrideUrlLoading"; + private WritableMap mEventData; + + public ShouldOverrideUrlLoadingEvent(int viewId, long timestampMs, WritableMap eventData) { + super(viewId, timestampMs); + mEventData = eventData; + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public boolean isSync() { + return true; + } + + @Override + public short getCoalescingKey() { + // All events for a given view can be coalesced. + return 0; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData); + } +}