package com.reactnativecommunity.webview;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.Request;
import okhttp3.HttpUrl;
import okhttp3.Response;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import static okhttp3.internal.Util.UTF_8;
import android.util.Log;
import android.annotation.SuppressLint;
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;
import android.graphics.Color;
import android.Manifest;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.URLUtil;
import android.webkit.ServiceWorkerController;
import android.webkit.ServiceWorkerClient;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
import com.facebook.react.views.scroll.OnScrollDispatchHelper;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.CatalystInstance;
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.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
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.reactnativecommunity.webview.events.TopLoadingErrorEvent;
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Manages instances of {@link WebView}
*
* Can accept following commands:
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
*
* {@link WebView} instances could emit following direct events:
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
*
* Each event will carry the following properties:
* - target - view's react tag
* - url - url set for the webview
* - loading - whether webview is in a loading state
* - title - title of the current page
* - canGoBack - boolean, whether there is anything on a history stack to go back
* - canGoForward - boolean, whether it is possible to request GO_FORWARD command
*/
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
public class RNCWebViewManager extends SimpleViewManager {
public static String activeUrl = null;
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_STOP_LOADING = 4;
public static final int COMMAND_POST_MESSAGE = 5;
public static final int COMMAND_INJECT_JAVASCRIPT = 6;
public static final int COMMAND_LOAD_URL = 7;
public static final int COMMAND_FOCUS = 8;
// android commands
public static final int COMMAND_CLEAR_FORM_DATA = 1000;
public static final int COMMAND_CLEAR_CACHE = 1001;
public static final int COMMAND_CLEAR_HISTORY = 1002;
protected static final String REACT_CLASS = "RNCWebView";
protected static final String HEADER_CONTENT_TYPE = "content-type";
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
protected static final String HTTP_METHOD_POST = "POST";
// 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 WebViewConfig mWebViewConfig;
protected RNCWebChromeClient mWebChromeClient = null;
protected boolean mAllowsFullscreenVideo = false;
protected @Nullable String mUserAgent = null;
protected @Nullable String mUserAgentWithApplicationName = null;
protected static String userAgent;
protected static OkHttpClient httpClient;
public RNCWebViewManager() {
mWebViewConfig = new WebViewConfig() {
public void configWebView(WebView webView) {
}
};
httpClient = new Builder()
.followRedirects(false)
.followSslRedirects(false)
.build();
}
public RNCWebViewManager(WebViewConfig webViewConfig) {
mWebViewConfig = webViewConfig;
}
protected static void dispatchEvent(WebView webView, Event event) {
ReactContext reactContext = (ReactContext) webView.getContext();
EventDispatcher eventDispatcher =
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
}
@Override
public String getName() {
return REACT_CLASS;
}
protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
return new RNCWebView(reactContext);
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected WebView createViewInstance(ThemedReactContext reactContext) {
RNCWebView webView = createRNCWebViewInstance(reactContext);
userAgent = webView.getSettings().getUserAgentString();
setupWebChromeClient(reactContext, webView);
reactContext.addLifecycleEventListener(webView);
mWebViewConfig.configWebView(webView);
WebSettings settings = webView.getSettings();
settings.setBuiltInZoomControls(true);
settings.setDisplayZoomControls(false);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(false);
settings.setAllowContentAccess(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
settings.setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(webView, false);
}
setMixedContentMode(webView, "never");
// 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.setDownloadListener(new DownloadListener() {
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
webView.setIgnoreErrFailedForThisURL(url);
RNCWebViewModule module = getModule(reactContext);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
String fileName = URLUtil.guessFileName(url, contentDisposition, mimetype);
String downloadMessage = "Downloading " + fileName;
//Attempt to add cookie, if it exists
URL urlObj = null;
try {
urlObj = new URL(url);
String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
String cookie = CookieManager.getInstance().getCookie(baseUrl);
request.addRequestHeader("Cookie", cookie);
} catch (MalformedURLException e) {
System.out.println("Error getting cookie for DownloadManager: " + e.toString());
e.printStackTrace();
}
//Finish setting up request
request.addRequestHeader("User-Agent", userAgent);
request.setTitle(fileName);
request.setDescription(downloadMessage);
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
module.setDownloadRequest(request);
if (module.grantFileDownloaderPermissions()) {
module.downloadFile();
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ServiceWorkerController swController = ServiceWorkerController.getInstance();
swController.setServiceWorkerClient(new ServiceWorkerClient() {
@Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient");
WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, false, webView);
if (response != null) {
Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient -> return intersept response");
return response;
}
Log.d(REACT_CLASS, "shouldInterceptRequest / ServiceWorkerClient -> intercept response is nil, delegating up");
return super.shouldInterceptRequest(request);
}
});
}
return webView;
}
private Boolean urlStringLooksInvalid(String urlString) {
return urlString == null || HttpUrl.parse(urlString) == null;
}
private Boolean responseRequiresJSInjection(Response response) {
// we don't want to inject JS into redirects
if (response.isRedirect()) {
return false;
}
// ...okhttp appends charset to content type sometimes, like "text/html; charset=UTF8"
final String contentTypeAndCharset = response.header(HEADER_CONTENT_TYPE, UNKNOWN_MIME_TYPE);
// ...and we only want to inject it in to HTML, really
return contentTypeAndCharset.startsWith(HTML_MIME_TYPE);
}
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request, Boolean onlyMainFrame, RNCWebView webView) {
Uri url = request.getUrl();
String urlStr = url.toString();
Log.d(REACT_CLASS, "new request ");
Log.d(REACT_CLASS, "url " + urlStr);
Log.d(REACT_CLASS, "host " + request.getUrl().getHost());
Log.d(REACT_CLASS, "path " + request.getUrl().getPath());
Log.d(REACT_CLASS, "main " + request.isForMainFrame());
Log.d(REACT_CLASS, "headers " + request.getRequestHeaders().toString());
Log.d(REACT_CLASS, "method " + request.getMethod());
if (onlyMainFrame && !request.isForMainFrame() ||
urlStringLooksInvalid(urlStr)) {
return null;//super.shouldInterceptRequest(webView, request);
}
Response response = null;
try {
Request.Builder reqBuilder = new Request.Builder().url(urlStr);
Map requestHeaders = request.getRequestHeaders();
for(String header: requestHeaders.keySet()) {
reqBuilder.header(header, requestHeaders.get(header));
}
Request httpRequest = reqBuilder.build();
response = httpClient.newCall(httpRequest).execute();
} catch (Exception e) {
Log.w(REACT_CLASS, "Error executing URL, ignoring: " + urlStr);
return null;
}
if (response == null) {
Log.w(REACT_CLASS, "Unexpected null response, ignore: " + urlStr);
}
Log.d(REACT_CLASS, "response headers " + response.headers().toString());
Log.d(REACT_CLASS, "response code " + response.code());
Log.d(REACT_CLASS, "response suc " + response.isSuccessful());
if (!responseRequiresJSInjection(response)) {
return null;
}
InputStream is = response.body().byteStream();
MediaType contentType = response.body().contentType();
Charset charset = contentType != null ? contentType.charset(UTF_8) : UTF_8;
RNCWebView reactWebView = (RNCWebView) webView;
if (response.code() == HttpURLConnection.HTTP_OK ||
response.headers().get("content-type").toLowerCase().equals(HTML_MIME_TYPE)) {
is = new InputStreamWithInjectedJS(is, reactWebView.injectedJSBeforeContentLoaded, charset);
}
Log.d(REACT_CLASS, "inject our custom JS to this request");
Map responseHeaders = new HashMap<>();
for (String hname: response.headers().names()) {
Log.d(REACT_CLASS, "HEAD " + hname + " " + response.headers().get(hname));
responseHeaders.put(hname, response.headers().get(hname));
}
return new WebResourceResponse("text/html", charset.name(), response.code(), "phrase", responseHeaders, is);
}
@ReactProp(name = "javaScriptEnabled")
public void setJavaScriptEnabled(WebView view, boolean enabled) {
view.getSettings().setJavaScriptEnabled(enabled);
}
@ReactProp(name = "showsHorizontalScrollIndicator")
public void setShowsHorizontalScrollIndicator(WebView view, boolean enabled) {
view.setHorizontalScrollBarEnabled(enabled);
}
@ReactProp(name = "showsVerticalScrollIndicator")
public void setShowsVerticalScrollIndicator(WebView view, boolean enabled) {
view.setVerticalScrollBarEnabled(enabled);
}
@ReactProp(name = "cacheEnabled")
public void setCacheEnabled(WebView view, boolean enabled) {
if (enabled) {
Context ctx = view.getContext();
if (ctx != null) {
view.getSettings().setAppCachePath(ctx.getCacheDir().getAbsolutePath());
view.getSettings().setCacheMode(WebSettings.LOAD_DEFAULT);
view.getSettings().setAppCacheEnabled(true);
}
} else {
view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
view.getSettings().setAppCacheEnabled(false);
}
}
@ReactProp(name = "cacheMode")
public void setCacheMode(WebView view, String cacheModeString) {
Integer cacheMode;
switch (cacheModeString) {
case "LOAD_CACHE_ONLY":
cacheMode = WebSettings.LOAD_CACHE_ONLY;
break;
case "LOAD_CACHE_ELSE_NETWORK":
cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK;
break;
case "LOAD_NO_CACHE":
cacheMode = WebSettings.LOAD_NO_CACHE;
break;
case "LOAD_DEFAULT":
default:
cacheMode = WebSettings.LOAD_DEFAULT;
break;
}
view.getSettings().setCacheMode(cacheMode);
}
@ReactProp(name = "androidHardwareAccelerationDisabled")
public void setHardwareAccelerationDisabled(WebView view, boolean disabled) {
if (disabled) {
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
}
@ReactProp(name = "overScrollMode")
public void setOverScrollMode(WebView view, String overScrollModeString) {
Integer overScrollMode;
switch (overScrollModeString) {
case "never":
overScrollMode = View.OVER_SCROLL_NEVER;
break;
case "content":
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS;
break;
case "always":
default:
overScrollMode = View.OVER_SCROLL_ALWAYS;
break;
}
view.setOverScrollMode(overScrollMode);
}
@ReactProp(name = "thirdPartyCookiesEnabled")
public void setThirdPartyCookiesEnabled(WebView view, boolean enabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled);
}
}
@ReactProp(name = "textZoom")
public void setTextZoom(WebView view, int value) {
view.getSettings().setTextZoom(value);
}
@ReactProp(name = "scalesPageToFit")
public void setScalesPageToFit(WebView view, boolean enabled) {
view.getSettings().setLoadWithOverviewMode(enabled);
view.getSettings().setUseWideViewPort(enabled);
}
@ReactProp(name = "domStorageEnabled")
public void setDomStorageEnabled(WebView view, boolean enabled) {
view.getSettings().setDomStorageEnabled(enabled);
}
@ReactProp(name = "userAgent")
public void setUserAgent(WebView view, @Nullable String userAgent) {
if (userAgent != null) {
mUserAgent = userAgent;
} else {
mUserAgent = null;
}
this.setUserAgentString(view);
}
@ReactProp(name = "applicationNameForUserAgent")
public void setApplicationNameForUserAgent(WebView view, @Nullable String applicationName) {
if(applicationName != null) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
String defaultUserAgent = WebSettings.getDefaultUserAgent(view.getContext());
mUserAgentWithApplicationName = defaultUserAgent + " " + applicationName;
}
} else {
mUserAgentWithApplicationName = null;
}
this.setUserAgentString(view);
}
protected void setUserAgentString(WebView view) {
if(mUserAgent != null) {
view.getSettings().setUserAgentString(mUserAgent);
} else if(mUserAgentWithApplicationName != null) {
view.getSettings().setUserAgentString(mUserAgentWithApplicationName);
} else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// handle unsets of `userAgent` prop as long as device is >= API 17
view.getSettings().setUserAgentString(WebSettings.getDefaultUserAgent(view.getContext()));
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@ReactProp(name = "mediaPlaybackRequiresUserAction")
public void setMediaPlaybackRequiresUserAction(WebView view, boolean requires) {
view.getSettings().setMediaPlaybackRequiresUserGesture(requires);
}
@ReactProp(name = "allowFileAccessFromFileURLs")
public void setAllowFileAccessFromFileURLs(WebView view, boolean allow) {
view.getSettings().setAllowFileAccessFromFileURLs(allow);
}
@ReactProp(name = "allowUniversalAccessFromFileURLs")
public void setAllowUniversalAccessFromFileURLs(WebView view, boolean allow) {
view.getSettings().setAllowUniversalAccessFromFileURLs(allow);
}
@ReactProp(name = "saveFormDataDisabled")
public void setSaveFormDataDisabled(WebView view, boolean disable) {
view.getSettings().setSaveFormData(!disable);
}
@ReactProp(name = "injectedJavaScript")
public void setInjectedJavaScript(WebView view, @Nullable String injectedJavaScript) {
((RNCWebView) view).setInjectedJavaScript(injectedJavaScript);
}
@ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) {
((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded);
}
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(WebView view, boolean enabled) {
((RNCWebView) view).setMessagingEnabled(enabled);
}
@ReactProp(name = "messagingModuleName")
public void setMessagingModuleName(WebView view, String moduleName) {
((RNCWebView) view).setMessagingModuleName(moduleName);
}
@ReactProp(name = "incognito")
public void setIncognito(WebView view, boolean enabled) {
// Remove all previous cookies
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().removeAllCookies(null);
} else {
CookieManager.getInstance().removeAllCookie();
}
// Disable caching
view.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);
view.getSettings().setAppCacheEnabled(!enabled);
view.clearHistory();
view.clearCache(enabled);
// No form data or autofill enabled
view.clearFormData();
view.getSettings().setSavePassword(!enabled);
view.getSettings().setSaveFormData(!enabled);
}
@ReactProp(name = "source")
public void setSource(WebView view, @Nullable ReadableMap source) {
if (source != null) {
if (source.hasKey("html")) {
String html = source.getString("html");
String baseUrl = source.hasKey("baseUrl") ? source.getString("baseUrl") : "";
view.loadDataWithBaseURL(baseUrl, html, HTML_MIME_TYPE, HTML_ENCODING, null);
return;
}
if (source.hasKey("uri")) {
String url = source.getString("uri");
String previousUrl = view.getUrl();
if (previousUrl != null && previousUrl.equals(url)) {
return;
}
if (source.hasKey("method")) {
String method = source.getString("method");
if (method.equalsIgnoreCase(HTTP_METHOD_POST)) {
byte[] postData = null;
if (source.hasKey("body")) {
String body = source.getString("body");
try {
postData = body.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
postData = body.getBytes();
}
}
if (postData == null) {
postData = new byte[0];
}
view.postUrl(url, postData);
return;
}
}
HashMap headerMap = new HashMap<>();
if (source.hasKey("headers")) {
ReadableMap headers = source.getMap("headers");
ReadableMapKeySetIterator iter = headers.keySetIterator();
while (iter.hasNextKey()) {
String key = iter.nextKey();
if ("user-agent".equals(key.toLowerCase(Locale.ENGLISH))) {
if (view.getSettings() != null) {
view.getSettings().setUserAgentString(headers.getString(key));
}
} else {
headerMap.put(key, headers.getString(key));
}
}
}
view.loadUrl(url, headerMap);
return;
}
}
view.loadUrl(BLANK_URL);
}
@ReactProp(name = "onContentSizeChange")
public void setOnContentSizeChange(WebView view, boolean sendContentSizeChangeEvents) {
((RNCWebView) view).setSendContentSizeChangeEvents(sendContentSizeChangeEvents);
}
@ReactProp(name = "mixedContentMode")
public void setMixedContentMode(WebView view, @Nullable String mixedContentMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (mixedContentMode == null || "never".equals(mixedContentMode)) {
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
} else if ("always".equals(mixedContentMode)) {
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
} else if ("compatibility".equals(mixedContentMode)) {
view.getSettings().setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
}
}
}
@ReactProp(name = "urlPrefixesForDefaultIntent")
public void setUrlPrefixesForDefaultIntent(
WebView view,
@Nullable ReadableArray urlPrefixesForDefaultIntent) {
RNCWebViewClient client = ((RNCWebView) view).getRNCWebViewClient();
if (client != null && urlPrefixesForDefaultIntent != null) {
client.setUrlPrefixesForDefaultIntent(urlPrefixesForDefaultIntent);
}
}
@ReactProp(name = "allowsFullscreenVideo")
public void setAllowsFullscreenVideo(
WebView view,
@Nullable Boolean allowsFullscreenVideo) {
mAllowsFullscreenVideo = allowsFullscreenVideo != null && allowsFullscreenVideo;
setupWebChromeClient((ReactContext)view.getContext(), view);
}
@ReactProp(name = "allowFileAccess")
public void setAllowFileAccess(
WebView view,
@Nullable Boolean allowFileAccess) {
view.getSettings().setAllowFileAccess(allowFileAccess != null && allowFileAccess);
}
@ReactProp(name = "geolocationEnabled")
public void setGeolocationEnabled(
WebView view,
@Nullable Boolean isGeolocationEnabled) {
view.getSettings().setGeolocationEnabled(isGeolocationEnabled != null && isGeolocationEnabled);
}
@ReactProp(name = "onScroll")
public void setOnScroll(WebView view, boolean hasScrollEvent) {
((RNCWebView) view).setHasScrollEvent(hasScrollEvent);
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
// Do not register default touch emitter and let WebView implementation handle touches
view.setWebViewClient(new RNCWebViewClient());
}
@Override
public Map getExportedCustomDirectEventTypeConstants() {
Map export = super.getExportedCustomDirectEventTypeConstants();
if (export == null) {
export = MapBuilder.newHashMap();
}
export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
return export;
}
@Override
public @Nullable
Map getCommandsMap() {
return MapBuilder.builder()
.put("goBack", COMMAND_GO_BACK)
.put("goForward", COMMAND_GO_FORWARD)
.put("reload", COMMAND_RELOAD)
.put("stopLoading", COMMAND_STOP_LOADING)
.put("postMessage", COMMAND_POST_MESSAGE)
.put("injectJavaScript", COMMAND_INJECT_JAVASCRIPT)
.put("loadUrl", COMMAND_LOAD_URL)
.put("requestFocus", COMMAND_FOCUS)
.put("clearFormData", COMMAND_CLEAR_FORM_DATA)
.put("clearCache", COMMAND_CLEAR_CACHE)
.put("clearHistory", COMMAND_CLEAR_HISTORY)
.build();
}
@Override
public void receiveCommand(WebView root, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case COMMAND_GO_BACK:
root.goBack();
break;
case COMMAND_GO_FORWARD:
root.goForward();
break;
case COMMAND_RELOAD:
root.reload();
break;
case COMMAND_STOP_LOADING:
root.stopLoading();
break;
case COMMAND_POST_MESSAGE:
try {
RNCWebView reactWebView = (RNCWebView) root;
JSONObject eventInitDict = new JSONObject();
eventInitDict.put("data", args.getString(0));
reactWebView.evaluateJavascriptWithFallback("(function () {" +
"var event;" +
"var data = " + eventInitDict.toString() + ";" +
"try {" +
"event = new MessageEvent('message', data);" +
"} catch (e) {" +
"event = document.createEvent('MessageEvent');" +
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
"}" +
"document.dispatchEvent(event);" +
"})();");
} catch (JSONException e) {
throw new RuntimeException(e);
}
break;
case COMMAND_INJECT_JAVASCRIPT:
RNCWebView reactWebView = (RNCWebView) root;
reactWebView.evaluateJavascriptWithFallback(args.getString(0));
break;
case COMMAND_LOAD_URL:
if (args == null) {
throw new RuntimeException("Arguments for loading an url are null!");
}
root.loadUrl(args.getString(0));
break;
case COMMAND_FOCUS:
root.requestFocus();
break;
case COMMAND_CLEAR_FORM_DATA:
root.clearFormData();
break;
case COMMAND_CLEAR_CACHE:
boolean includeDiskFiles = args != null && args.getBoolean(0);
root.clearCache(includeDiskFiles);
break;
case COMMAND_CLEAR_HISTORY:
root.clearHistory();
break;
}
}
@Override
public void onDropViewInstance(WebView webView) {
super.onDropViewInstance(webView);
((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView);
((RNCWebView) webView).cleanupCallbacksAndDestroy();
}
public static RNCWebViewModule getModule(ReactContext reactContext) {
return reactContext.getNativeModule(RNCWebViewModule.class);
}
protected void setupWebChromeClient(ReactContext reactContext, WebView webView) {
if (mAllowsFullscreenVideo) {
int initialRequestedOrientation = reactContext.getCurrentActivity().getRequestedOrientation();
mWebChromeClient = new RNCWebChromeClient(reactContext, webView) {
@Override
public Bitmap getDefaultVideoPoster() {
return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
if (mVideoView != null) {
callback.onCustomViewHidden();
return;
}
mVideoView = view;
mCustomViewCallback = callback;
mReactContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY);
mReactContext.getCurrentActivity().getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
mVideoView.setBackgroundColor(Color.BLACK);
getRootView().addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS);
mWebView.setVisibility(View.GONE);
mReactContext.addLifecycleEventListener(this);
}
@Override
public void onHideCustomView() {
if (mVideoView == null) {
return;
}
mVideoView.setVisibility(View.GONE);
getRootView().removeView(mVideoView);
mCustomViewCallback.onCustomViewHidden();
mVideoView = null;
mCustomViewCallback = null;
mWebView.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mReactContext.getCurrentActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
}
mReactContext.getCurrentActivity().setRequestedOrientation(initialRequestedOrientation);
mReactContext.removeLifecycleEventListener(this);
}
};
webView.setWebChromeClient(mWebChromeClient);
} else {
if (mWebChromeClient != null) {
mWebChromeClient.onHideCustomView();
}
mWebChromeClient = new RNCWebChromeClient(reactContext, webView) {
@Override
public Bitmap getDefaultVideoPoster() {
return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
}
};
webView.setWebChromeClient(mWebChromeClient);
}
}
public static class InputStreamWithInjectedJS extends InputStream {
private InputStream pageIS;
private InputStream scriptIS;
private Charset charset;
private static final String REACT_CLASS = "InpStreamWithInjectedJS";
private static Map script = new HashMap<>();
private int GREATER_THAN_SIGN = 62;
private int LESS_THAN_SIGN = 60;
private int SCRIPT_TAG_LENGTH = 7;
private boolean hasJS = false;
private boolean tagWasFound = false;
private int[] tag = new int[SCRIPT_TAG_LENGTH];
private boolean readFromTagVector = false;
private int tagVectorIdx = 0;
private int maxTagVectorIdx = SCRIPT_TAG_LENGTH;
private boolean scriptWasInjected = false;
private StringBuffer contentBuffer = new StringBuffer();
private static Charset getCharset(String charsetName) {
Charset cs = StandardCharsets.UTF_8;
try {
if (charsetName != null) {
cs = Charset.forName(charsetName);
}
} catch (UnsupportedCharsetException e) {
Log.d(REACT_CLASS, "wrong charset: " + charsetName);
}
return cs;
}
private static InputStream getScript(Charset charset) {
String js = script.get(charset);
if (js == null) {
String defaultJs = script.get(StandardCharsets.UTF_8);
js = new String(defaultJs.getBytes(StandardCharsets.UTF_8), charset);
script.put(charset, js);
}
return new ByteArrayInputStream(js.getBytes(charset));
}
InputStreamWithInjectedJS(InputStream is, String js, Charset charset) {
if (js == null) {
this.pageIS = is;
} else {
this.hasJS = true;
this.charset = charset;
Charset cs = StandardCharsets.UTF_8;
String jsScript = "";
script.put(cs, jsScript);
this.pageIS = is;
}
}
private int readScript() throws IOException {
int nextByte = scriptIS.read();
if (nextByte == -1) {
scriptIS.close();
scriptWasInjected = true;
if(readFromTagVector) {
return readTag();
} else {
return pageIS.read();
}
} else {
return nextByte;
}
}
private int readTag() {
int nextByte = tag[tagVectorIdx];
tagVectorIdx++;
if(tagVectorIdx > maxTagVectorIdx) {
readFromTagVector = false;
}
return nextByte;
}
private boolean checkHeadTag(int nextByte) {
int bufferLength = contentBuffer.length();
if (nextByte == GREATER_THAN_SIGN &&
bufferLength >= 6 &&
contentBuffer.substring(bufferLength - 6).equals("")) {
Log.d(REACT_CLASS, " tag was found");
this.scriptIS = getScript(this.charset);
tagWasFound = true;
return true;
}
return false;
}
private boolean checkScriptTagByByte(int index, int anotherByte) {
if(index == 1) {
// 115 = "s"
return anotherByte == 115;
} else if(index == 2) {
// 99 = "c"
return anotherByte == 99;
}
return true;
}
private boolean checkScriptTag(int nextByte) throws IOException {
if (nextByte == LESS_THAN_SIGN) {
StringBuilder tagBuffer = new StringBuilder();
tag[0] = nextByte;
tagBuffer.append((char) nextByte);
readFromTagVector = true;
tagVectorIdx = 1;
maxTagVectorIdx = SCRIPT_TAG_LENGTH - 1;
for (int i = 1; i < SCRIPT_TAG_LENGTH; i++) {
int anotherByte = pageIS.read();
tag[i] = anotherByte;
tagBuffer.append((char) anotherByte);
contentBuffer.append((char) anotherByte);
if (!checkScriptTagByByte(i, anotherByte) || anotherByte == -1) {
maxTagVectorIdx = i;
return false;
}
}
if(tagBuffer.length() == SCRIPT_TAG_LENGTH) {
String sub = tagBuffer.substring(0, SCRIPT_TAG_LENGTH);
if (sub.equals("