package com.github.alinz.reactnativewebviewbridge; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.Picture; import android.net.Uri; import android.util.Log; import android.os.Build; import android.text.TextUtils; import android.view.ViewGroup.LayoutParams; import android.webkit.*; import android.net.http.SslError; import android.app.Activity; 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.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 java.net.URL; import java.net.MalformedURLException; import javax.annotation.Nullable; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.views.webview.WebViewConfig; import android.webkit.ValueCallback; 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"; public static final int COMMAND_SEND_TO_BRIDGE = 101; private static final String BLANK_URL = "about:blank"; private WebViewConfig mWebViewConfig; public WebViewBridgeManager() { mWebViewConfig = new WebViewConfig() { public void configWebView(WebView webView) { } }; } public WebViewBridgeManager(WebViewConfig webViewConfig) { mWebViewConfig = webViewConfig; } @Override public String getName() { return REACT_CLASS; } @Override public @Nullable Map getCommandsMap() { Map commandsMap = super.getCommandsMap(); commandsMap.put("sendToBridge", COMMAND_SEND_TO_BRIDGE); return commandsMap; } protected static class ReactWebChromeClient extends WebChromeClient { public void onProgressChanged(WebView view, int newProgress) { if (newProgress < 80) { ((ReactWebView) view).callInjectedOnStartLoadingJavaScript(); } } } @Override protected WebView createViewInstance(ThemedReactContext reactContext) { ReactWebView webView = new ReactWebView(reactContext); webView.setWebChromeClient(new WebChromeClient() { @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { callback.invoke(origin, true, false); } }); reactContext.addLifecycleEventListener(webView); mWebViewConfig.configWebView(webView); webView.getSettings().setBuiltInZoomControls(true); webView.getSettings().setDisplayZoomControls(false); // Fixes broken full-screen modals/galleries due to body height being 0. webView.setLayoutParams( new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); } webView.setWebChromeClient(new ReactWebChromeClient()); webView.addJavascriptInterface(new JavascriptBridge(webView), "WebViewBridge"); webView.addJavascriptInterface(new StatusBridge(reactContext, webView), "StatusBridge"); return webView; } @Override public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) { super.receiveCommand(root, commandId, args); switch (commandId) { case COMMAND_SEND_TO_BRIDGE: sendToBridge(root, args.getString(0)); break; default: //do nothing!!!! } } private void sendToBridge(WebView root, String message) { String script = "WebViewBridge.onMessage('" + message + "');"; WebViewBridgeManager.evaluateJavascript(root, script); } static private void evaluateJavascript(WebView root, String javascript) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { root.evaluateJavascript(javascript, null); } else { root.loadUrl("javascript:" + javascript); } } @ReactProp(name = "allowFileAccessFromFileURLs") public void setAllowFileAccessFromFileURLs(WebView root, boolean allows) { root.getSettings().setAllowFileAccessFromFileURLs(allows); } @ReactProp(name = "allowUniversalAccessFromFileURLs") public void setAllowUniversalAccessFromFileURLs(WebView root, boolean allows) { 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); } @ReactProp(name = "localStorageEnabled") public void setLocalStorageEnabled(WebView view, boolean enabled) { if (enabled) { view.getSettings().setDomStorageEnabled(true); view.getSettings().setDatabaseEnabled(true); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { view.getSettings().setDatabasePath("/data/data/" + view.getContext().getPackageName() + "/databases/"); } } } @Override protected void addEventEmitters(ThemedReactContext reactContext, WebView view) { // Do not register default touch emitter and let WebView implementation handle touches view.setWebViewClient(new ReactWebViewClient()); } 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 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() { if (injectedOnStartLoadingJS != null && !TextUtils.isEmpty(injectedOnStartLoadingJS)) { evaluateJavascript(injectedOnStartLoadingJS, new ValueCallback() { @Override public void onReceiveValue(String value) { } } ); } } 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); } }