Files
react-native-webview-bridge/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java
T

434 lines
16 KiB
Java

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<String, Integer> getCommandsMap() {
Map<String, Integer> 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<String>() {
@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<String>() {
@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);
}
}