diff --git a/android/build.gradle b/android/build.gradle index dd20c37..8edcb67 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -30,5 +30,5 @@ repositories { } dependencies { - compile 'com.facebook.react:react-native:0.39.+' + compile 'com.facebook.react:react-native:0.38.+' } diff --git a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java index 3aa3445..da34c8f 100644 --- a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java +++ b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java @@ -1,22 +1,50 @@ package com.github.alinz.reactnativewebviewbridge; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Picture; +import android.net.Uri; +import android.os.Build; +import android.text.TextUtils; +import android.view.ViewGroup.LayoutParams; +import android.webkit.GeolocationPermissions; +import android.webkit.WebChromeClient; import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; +import com.facebook.common.logging.FLog; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.module.annotations.ReactModule; +import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.views.webview.ReactWebViewManager; +import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.annotations.ReactProp; +import com.facebook.react.uimanager.events.ContentSizeChangeEvent; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.views.webview.events.TopLoadingErrorEvent; +import com.facebook.react.views.webview.events.TopLoadingFinishEvent; +import com.facebook.react.views.webview.events.TopLoadingStartEvent; +import com.facebook.react.views.webview.events.TopMessageEvent; +import com.facebook.react.views.webview.ReactWebViewManager; import android.text.TextUtils; import android.graphics.Bitmap; import java.util.Map; import javax.annotation.Nullable; -import android.webkit.WebChromeClient; -import android.view.ViewGroup.LayoutParams; import com.facebook.react.common.build.ReactBuildConfig; -import android.os.Build; -import android.webkit.GeolocationPermissions; import com.facebook.react.views.webview.WebViewConfig; import android.webkit.ValueCallback; @@ -24,6 +52,7 @@ public class WebViewBridgeManager extends ReactWebViewManager { private static final String REACT_CLASS = "RCTWebViewBridge"; private static final String HTML_ENCODING = "UTF-8"; private static final String HTML_MIME_TYPE = "text/html; charset=utf-8"; + private static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE"; private static final String HTTP_METHOD_POST = "POST"; @@ -50,9 +79,7 @@ public class WebViewBridgeManager extends ReactWebViewManager { } @Override - public - @Nullable - Map getCommandsMap() { + public @Nullable Map getCommandsMap() { Map commandsMap = super.getCommandsMap(); commandsMap.put("sendToBridge", COMMAND_SEND_TO_BRIDGE); @@ -66,7 +93,7 @@ public class WebViewBridgeManager extends ReactWebViewManager { private int last = 100; public void onProgressChanged(WebView view, int newProgress) { - if (newProgress < 70) { + if (newProgress < 90) { if(was == false || last < newProgress){ was = true; last = newProgress; @@ -144,6 +171,18 @@ public class WebViewBridgeManager extends ReactWebViewManager { root.getSettings().setAllowUniversalAccessFromFileURLs(allows); } + @Override + @ReactProp(name = "injectedJavaScript") + public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) { + ((ReactWebView) view).setInjectedJavaScript(injectedJavaScript); + } + + @Override + @ReactProp(name = "messagingEnabled") + public void setMessagingEnabled(WebView view, boolean enabled) { + ((ReactWebView) view).setMessagingEnabled(enabled); + } + @ReactProp(name = "injectedOnStartLoadingJavaScript") public void setInjectedOnStartLoadingJavaScript(WebView view, @Nullable String injectedJavaScript) { ((ReactWebView) view).setInjectedOnStartLoadingJavaScript(injectedJavaScript); @@ -155,15 +194,78 @@ public class WebViewBridgeManager extends ReactWebViewManager { view.setWebViewClient(new ReactWebViewClient()); } - protected static class ReactWebView extends ReactWebViewManager.ReactWebView { + private static class ReactWebView extends WebView implements LifecycleEventListener { + private @Nullable String injectedJS; private @Nullable String injectedOnStartLoadingJS; + private boolean messagingEnabled = false; + + private class ReactWebViewBridge { + ReactWebView mContext; + + ReactWebViewBridge(ReactWebView c) { + mContext = c; + } + + @JavascriptInterface + public void postMessage(String message) { + mContext.onMessage(message); + } + } + + /** + * WebView must be created with an context of the current activity + * + * Activity Context is required for creation of dialogs internally by WebView + * Reactive Native needed for access to ReactNative internal system functionality + * + */ + public ReactWebView(ThemedReactContext reactContext) { + super(reactContext); + } + + @Override + public void onHostResume() { + // do nothing + } + + @Override + public void onHostPause() { + // do nothing + } + + @Override + public void onHostDestroy() { + cleanupCallbacksAndDestroy(); + } + + public void setInjectedJavaScript(@Nullable String js) { + injectedJS = js; + } public void setInjectedOnStartLoadingJavaScript(@Nullable String js) { injectedOnStartLoadingJS = js; } - public ReactWebView(ThemedReactContext reactContext) { - super(reactContext); + public void setMessagingEnabled(boolean enabled) { + if (messagingEnabled == enabled) { + return; + } + + messagingEnabled = enabled; + if (enabled) { + addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME); + linkBridge(); + } else { + removeJavascriptInterface(BRIDGE_NAME); + } + } + + public void callInjectedJavaScript() { + if (getSettings().getJavaScriptEnabled() && + injectedJS != null && + !TextUtils.isEmpty(injectedJS)) { + loadUrl("javascript:(function() {\n" + injectedJS + ";\n})();"); + } } public void callInjectedOnStartLoadingJavaScript() { @@ -178,15 +280,152 @@ public class WebViewBridgeManager extends ReactWebViewManager { ); } } - } - protected static class ReactWebViewClient extends ReactWebViewManager.ReactWebViewClient { - @Override - public void onPageStarted(WebView webView, String url, Bitmap favicon) { - ReactWebView reactWebView = (ReactWebView) webView; - reactWebView.callInjectedOnStartLoadingJavaScript(); - super.onPageStarted(webView, url, favicon); + public void linkBridge() { + if (messagingEnabled) { + if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + // See isNative in lodash + String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')"; + evaluateJavascript(testPostMessageNative, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value.equals("true")) { + FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined"); + } + } + }); + } + + loadUrl("javascript:(" + + "window.originalPostMessage = window.postMessage," + + "window.postMessage = function(data) {" + + BRIDGE_NAME + ".postMessage(String(data));" + + "}" + + ")"); + } + } + + public void onMessage(String message) { + dispatchEvent(this, new TopMessageEvent(this.getId(), message)); + } + + private void cleanupCallbacksAndDestroy() { + setWebViewClient(null); + destroy(); } } + private static class ReactWebViewClient extends WebViewClient { + + private boolean mLastLoadFailed = false; + + @Override + public void onPageFinished(WebView webView, String url) { + super.onPageFinished(webView, url); + + if (!mLastLoadFailed) { + ReactWebView reactWebView = (ReactWebView) webView; + reactWebView.callInjectedJavaScript(); + reactWebView.linkBridge(); + emitFinishEvent(webView, url); + } + } + + @Override + public void onPageStarted(WebView webView, String url, Bitmap favicon) { + super.onPageStarted(webView, url, favicon); + mLastLoadFailed = false; + + ReactWebView reactWebView = (ReactWebView) webView; + reactWebView.callInjectedOnStartLoadingJavaScript(); + + dispatchEvent( + webView, + new TopLoadingStartEvent( + webView.getId(), + createWebViewEvent(webView, url))); + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (url.startsWith("http://") || url.startsWith("https://") || + url.startsWith("file://")) { + return false; + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + view.getContext().startActivity(intent); + return true; + } + } + + @Override + public void onReceivedError( + WebView webView, + int errorCode, + String description, + String failingUrl) { + super.onReceivedError(webView, errorCode, description, failingUrl); + mLastLoadFailed = true; + + // In case of an error JS side expect to get a finish event first, and then get an error event + // Android WebView does it in the opposite way, so we need to simulate that behavior + emitFinishEvent(webView, failingUrl); + + WritableMap eventData = createWebViewEvent(webView, failingUrl); + eventData.putDouble("code", errorCode); + eventData.putString("description", description); + + dispatchEvent( + webView, + new TopLoadingErrorEvent(webView.getId(), eventData)); + } + + @Override + public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) { + super.doUpdateVisitedHistory(webView, url, isReload); + + dispatchEvent( + webView, + new TopLoadingStartEvent( + webView.getId(), + createWebViewEvent(webView, url))); + } + + private void emitFinishEvent(WebView webView, String url) { + dispatchEvent( + webView, + new TopLoadingFinishEvent( + webView.getId(), + createWebViewEvent(webView, url))); + } + + private WritableMap createWebViewEvent(WebView webView, String url) { + WritableMap event = Arguments.createMap(); + event.putDouble("target", webView.getId()); + // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks + // like onPageFinished + event.putString("url", url); + event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100); + event.putString("title", webView.getTitle()); + event.putBoolean("canGoBack", webView.canGoBack()); + event.putBoolean("canGoForward", webView.canGoForward()); + return event; + } + } + + @Override + public void onDropViewInstance(WebView webView) { + //super.onDropViewInstance(webView); + ((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((ReactWebView) webView); + ((ReactWebView) webView).cleanupCallbacksAndDestroy(); + } + + private static void dispatchEvent(WebView webView, Event event) { + ReactContext reactContext = (ReactContext) webView.getContext(); + EventDispatcher eventDispatcher = + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + eventDispatcher.dispatchEvent(event); + } + }