Files
react-native-webview/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
T

1599 lines
56 KiB
Java
Raw Normal View History

package com.reactnativecommunity.webview;
2018-07-30 20:21:48 -07:00
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;
2020-05-04 20:55:55 +03:00
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;
2018-07-30 20:21:48 -07:00
import android.annotation.TargetApi;
import android.app.DownloadManager;
2018-07-30 20:21:48 -07:00
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
2018-07-30 20:21:48 -07:00
import android.graphics.Bitmap;
import android.graphics.Color;
import android.Manifest;
2018-07-30 20:21:48 -07:00
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
2018-07-30 20:21:48 -07:00
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
2018-07-30 20:21:48 -07:00
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
2018-07-30 20:21:48 -07:00
import android.webkit.ConsoleMessage;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
2018-07-30 20:21:48 -07:00
import android.webkit.GeolocationPermissions;
import android.webkit.JavascriptInterface;
import android.webkit.PermissionRequest;
import android.webkit.URLUtil;
import android.webkit.ServiceWorkerController;
import android.webkit.ServiceWorkerClient;
2018-07-30 20:21:48 -07:00
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceRequest;
2019-09-20 17:43:28 -07:00
import android.webkit.WebResourceResponse;
2018-07-30 20:21:48 -07:00
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;
2018-07-30 20:21:48 -07:00
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.CatalystInstance;
2018-07-30 20:21:48 -07:00
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;
2018-07-30 20:21:48 -07:00
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;
2018-07-30 20:21:48 -07:00
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;
2019-09-20 17:43:28 -07:00
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;
2018-07-30 20:21:48 -07:00
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;
2018-07-30 20:21:48 -07:00
/**
* Manages instances of {@link WebView}
* <p>
2018-07-30 20:21:48 -07:00
* Can accept following commands:
* - GO_BACK
* - GO_FORWARD
* - RELOAD
* - LOAD_URL
* <p>
2018-07-30 20:21:48 -07:00
* {@link WebView} instances could emit following direct events:
* - topLoadingFinish
* - topLoadingStart
* - topLoadingStart
* - topLoadingProgress
* - topShouldStartLoadWithRequest
* <p>
2018-07-30 20:21:48 -07:00
* 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
2018-07-30 20:21:48 -07:00
*/
@ReactModule(name = RNCWebViewManager.REACT_CLASS)
public class RNCWebViewManager extends SimpleViewManager<WebView> {
2018-07-30 20:21:48 -07:00
public static String activeUrl = null;
2018-07-30 20:21:48 -07:00
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";
2018-07-30 20:21:48 -07:00
// 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() {
2018-07-30 20:21:48 -07:00
mWebViewConfig = new WebViewConfig() {
public void configWebView(WebView webView) {
}
};
httpClient = new Builder()
.followRedirects(false)
.followSslRedirects(false)
.build();
2018-07-30 20:21:48 -07:00
}
public RNCWebViewManager(WebViewConfig webViewConfig) {
2018-07-30 20:21:48 -07:00
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);
}
2018-07-30 20:21:48 -07:00
@Override
public String getName() {
return REACT_CLASS;
}
protected RNCWebView createRNCWebViewInstance(ThemedReactContext reactContext) {
return new RNCWebView(reactContext);
2018-07-30 20:21:48 -07:00
}
@Override
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected WebView createViewInstance(ThemedReactContext reactContext) {
RNCWebView webView = createRNCWebViewInstance(reactContext);
userAgent = webView.getSettings().getUserAgentString();
setupWebChromeClient(reactContext, webView);
2018-07-30 20:21:48 -07:00
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));
2018-07-30 20:21:48 -07:00
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);
}
});
}
2018-07-30 20:21:48 -07:00
return webView;
}
private Boolean urlStringLooksInvalid(String urlString) {
2020-05-04 20:55:55 +03:00
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) {
2020-05-04 20:55:55 +03:00
Uri url = request.getUrl();
String urlStr = url.toString();
2020-05-04 20:55:55 +03:00
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());
2020-05-04 20:55:55 +03:00
if (onlyMainFrame && !request.isForMainFrame() ||
urlStringLooksInvalid(urlStr)) {
return null;//super.shouldInterceptRequest(webView, request);
}
2020-05-04 20:55:55 +03:00
Response response = null;
try {
Request.Builder reqBuilder = new Request.Builder().url(urlStr);
2020-05-04 20:55:55 +03:00
Map<String, String> requestHeaders = request.getRequestHeaders();
for(String header: requestHeaders.keySet()) {
reqBuilder.header(header, requestHeaders.get(header));
}
2020-05-04 20:55:55 +03:00
Request httpRequest = reqBuilder.build();
response = httpClient.newCall(httpRequest).execute();
2020-05-04 20:55:55 +03:00
} 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<String, String> 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);
}
2018-07-30 20:21:48 -07:00
@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);
}
2018-07-30 20:21:48 -07:00
@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);
}
2018-07-30 20:21:48 -07:00
@ReactProp(name = "scalesPageToFit")
public void setScalesPageToFit(WebView view, boolean enabled) {
view.getSettings().setLoadWithOverviewMode(enabled);
view.getSettings().setUseWideViewPort(enabled);
2018-07-30 20:21:48 -07:00
}
@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()));
2018-07-30 20:21:48 -07:00
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
2018-07-30 20:21:48 -07:00
@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);
}
2018-07-30 20:21:48 -07:00
@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);
2018-07-30 20:21:48 -07:00
}
2020-02-11 15:06:06 +02:00
@ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
public void setInjectedJavaScriptBeforeContentLoaded(WebView view, @Nullable String injectedJavaScriptBeforeContentLoaded) {
((RNCWebView) view).setInjectedJavaScriptBeforeContentLoaded(injectedJavaScriptBeforeContentLoaded);
}
2018-07-30 20:21:48 -07:00
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(WebView view, boolean enabled) {
((RNCWebView) view).setMessagingEnabled(enabled);
2018-07-30 20:21:48 -07:00
}
@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);
}
2018-07-30 20:21:48 -07:00
@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);
2018-07-30 20:21:48 -07:00
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");
2018-10-13 00:40:20 +02:00
if (method.equalsIgnoreCase(HTTP_METHOD_POST)) {
2018-07-30 20:21:48 -07:00
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<String, String> 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);
2018-07-30 20:21:48 -07:00
}
@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();
2018-07-30 20:21:48 -07:00
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);
}
2018-07-30 20:21:48 -07:00
@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);
}
2018-07-30 20:21:48 -07:00
@Override
protected void addEventEmitters(ThemedReactContext reactContext, WebView view) {
// Do not register default touch emitter and let WebView implementation handle touches
view.setWebViewClient(new RNCWebViewClient());
2018-07-30 20:21:48 -07:00
}
@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"));
2019-05-17 15:25:53 +08:00
export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
2019-09-20 17:43:28 -07:00
export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
return export;
}
2018-07-30 20:21:48 -07:00
@Override
public @Nullable
Map<String, Integer> getCommandsMap() {
return MapBuilder.<String, Integer>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();
2018-07-30 20:21:48 -07:00
}
@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;
2018-07-30 20:21:48 -07:00
JSONObject eventInitDict = new JSONObject();
eventInitDict.put("data", args.getString(0));
reactWebView.evaluateJavascriptWithFallback("(function () {" +
2018-07-30 20:21:48 -07:00
"var event;" +
"var data = " + eventInitDict.toString() + ";" +
"try {" +
"event = new MessageEvent('message', data);" +
2018-07-30 20:21:48 -07:00
"} catch (e) {" +
"event = document.createEvent('MessageEvent');" +
"event.initMessageEvent('message', true, true, data.data, data.origin, data.lastEventId, data.source);" +
2018-07-30 20:21:48 -07:00
"}" +
"document.dispatchEvent(event);" +
"})();");
2018-07-30 20:21:48 -07:00
} catch (JSONException e) {
throw new RuntimeException(e);
}
break;
case COMMAND_INJECT_JAVASCRIPT:
RNCWebView reactWebView = (RNCWebView) root;
reactWebView.evaluateJavascriptWithFallback(args.getString(0));
2018-07-30 20:21:48 -07:00
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;
2018-07-30 20:21:48 -07:00
}
}
@Override
public void onDropViewInstance(WebView webView) {
super.onDropViewInstance(webView);
((ThemedReactContext) webView.getContext()).removeLifecycleEventListener((RNCWebView) webView);
((RNCWebView) webView).cleanupCallbacksAndDestroy();
2018-07-30 20:21:48 -07:00
}
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;
2020-05-04 20:55:55 +03:00
private static final String REACT_CLASS = "InpStreamWithInjectedJS";
private static Map<Charset, String> script = new HashMap<>();
2020-05-04 20:55:55 +03:00
private int GREATER_THAN_SIGN = 62;
private int LESS_THAN_SIGN = 60;
private int SCRIPT_TAG_LENGTH = 7;
private boolean hasJS = false;
2020-05-04 20:55:55 +03:00
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>" + js + "</script>";
script.put(cs, jsScript);
this.pageIS = is;
}
}
2020-05-04 20:55:55 +03:00
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;
}
2020-05-04 20:55:55 +03:00
return nextByte;
}
private boolean checkHeadTag(int nextByte) {
int bufferLength = contentBuffer.length();
if (nextByte == GREATER_THAN_SIGN &&
bufferLength >= 6 &&
contentBuffer.substring(bufferLength - 6).equals("<head>")) {
Log.d(REACT_CLASS, "<head> 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("<script")) {
tagVectorIdx = 0;
Log.d(REACT_CLASS, "<script tag was found");
this.scriptIS = getScript(this.charset);
tagWasFound = true;
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}
@Override
public int read() throws IOException {
if ((scriptWasInjected || !hasJS) && !readFromTagVector) {
return pageIS.read();
} else if (!scriptWasInjected && tagWasFound) {
return readScript();
} else if (readFromTagVector) {
return readTag();
} else {
int nextByte = pageIS.read();
contentBuffer.append((char) nextByte);
if (checkHeadTag(nextByte)) {
return nextByte;
} else if (checkScriptTag(nextByte)) {
return scriptIS.read();
} else {
return nextByte;
}
}
}
}
protected class RNCWebViewClient extends WebViewClient {
protected static final String REACT_CLASS = "RNCWebViewClient";
protected boolean mLastLoadFailed = false;
protected @Nullable
ReadableArray mUrlPrefixesForDefaultIntent;
protected @Nullable String ignoreErrFailedForThisURL = null;
public void setIgnoreErrFailedForThisURL(@Nullable String url) {
ignoreErrFailedForThisURL = url;
}
@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScript();
emitFinishEvent(webView, url);
}
}
@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
mLastLoadFailed = false;
dispatchEvent(
webView,
new TopLoadingStartEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView webView, WebResourceRequest request) {
Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient");
WebResourceResponse response = RNCWebViewManager.this.shouldInterceptRequest(request, true, (RNCWebView)webView);
if (response != null) {
Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient -> return intercept response");
return response;
}
Log.d(REACT_CLASS, "shouldInterceptRequest / WebViewClient -> intercept response is nil, delegating up");
return super.shouldInterceptRequest(webView, request);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if (request == null || view == null) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
/*
* In order to follow redirects properly, we return null in interceptRequest().
* Doing this breaks the web3 injection on the resulting page, so we have to reload to
* make sure web3 is available.
* */
if (request.isForMainFrame() && request.isRedirect()) {
view.loadUrl(request.getUrl().toString());
return true;
}
}
/*
* API < 24: TODO: implement based on https://github.com/toshiapp/toshi-android-client/blob/f4840d3d24ff60223662eddddceca8586a1be8bb/app/src/main/java/com/toshi/view/activity/webView/ToshiWebClient.kt#L99
* */
final String url = request.getUrl().toString();
return this.shouldOverrideUrlLoading(view, url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
activeUrl = url;
dispatchEvent(
view,
new TopShouldStartLoadWithRequestEvent(
view.getId(),
createWebViewEvent(view, url)));
return true;
}
@Override
public void onReceivedError(
WebView webView,
int errorCode,
String description,
String failingUrl) {
if (ignoreErrFailedForThisURL != null
&& failingUrl.equals(ignoreErrFailedForThisURL)
&& errorCode == -1
&& description.equals("net::ERR_FAILED")) {
// This is a workaround for a bug in the WebView.
// See these chromium issues for more context:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1023678
// https://bugs.chromium.org/p/chromium/issues/detail?id=1050635
// This entire commit should be reverted once this bug is resolved in chromium.
setIgnoreErrFailedForThisURL(null);
return;
}
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));
}
2019-09-20 17:43:28 -07:00
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void onReceivedHttpError(
WebView webView,
WebResourceRequest request,
WebResourceResponse errorResponse) {
super.onReceivedHttpError(webView, request, errorResponse);
if (request.isForMainFrame()) {
WritableMap eventData = createWebViewEvent(webView, request.getUrl().toString());
eventData.putInt("statusCode", errorResponse.getStatusCode());
eventData.putString("description", errorResponse.getReasonPhrase());
dispatchEvent(
webView,
new TopHttpErrorEvent(webView.getId(), eventData));
}
}
protected void emitFinishEvent(WebView webView, String url) {
dispatchEvent(
webView,
new TopLoadingFinishEvent(
webView.getId(),
createWebViewEvent(webView, url)));
}
protected 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;
}
public void setUrlPrefixesForDefaultIntent(ReadableArray specialUrls) {
mUrlPrefixesForDefaultIntent = specialUrls;
}
}
protected static class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener {
protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER);
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
protected ReactContext mReactContext;
protected View mWebView;
protected View mVideoView;
protected WebChromeClient.CustomViewCallback mCustomViewCallback;
public RNCWebChromeClient(ReactContext reactContext, WebView webView) {
this.mReactContext = reactContext;
this.mWebView = webView;
}
@Override
public boolean onConsoleMessage(ConsoleMessage message) {
Log.i("StatusNativeLogs", "###js " + message.message() + " -- From line "
+ message.lineNumber() + " of "
+ message.sourceId());
if (ReactBuildConfig.DEBUG) {
return super.onConsoleMessage(message);
}
// Ignore console logs in non debug builds.
return true;
}
// Fix WebRTC permission request error.
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void onPermissionRequest(final PermissionRequest request) {
String[] requestedResources = request.getResources();
ArrayList<String> permissions = new ArrayList<>();
ArrayList<String> grantedPermissions = new ArrayList<String>();
for (int i = 0; i < requestedResources.length; i++) {
if (requestedResources[i].equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
permissions.add(Manifest.permission.RECORD_AUDIO);
} else if (requestedResources[i].equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
permissions.add(Manifest.permission.CAMERA);
}
// TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID.
}
for (int i = 0; i < permissions.size(); i++) {
if (ContextCompat.checkSelfPermission(mReactContext, permissions.get(i)) != PackageManager.PERMISSION_GRANTED) {
continue;
}
if (permissions.get(i).equals(Manifest.permission.RECORD_AUDIO)) {
grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);
} else if (permissions.get(i).equals(Manifest.permission.CAMERA)) {
grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);
}
}
if (grantedPermissions.isEmpty()) {
request.deny();
} else {
String[] grantedPermissionsArray = new String[grantedPermissions.size()];
grantedPermissionsArray = grantedPermissions.toArray(grantedPermissionsArray);
request.grant(grantedPermissionsArray);
}
}
@Override
public void onProgressChanged(WebView webView, int newProgress) {
super.onProgressChanged(webView, newProgress);
final String url = webView.getUrl();
if (
url != null
&& activeUrl != null
&& !url.equals(activeUrl)
) {
return;
}
WritableMap event = Arguments.createMap();
event.putDouble("target", webView.getId());
event.putString("title", webView.getTitle());
event.putString("url", url);
event.putBoolean("canGoBack", webView.canGoBack());
event.putBoolean("canGoForward", webView.canGoForward());
event.putDouble("progress", (float) newProgress / 100);
dispatchEvent(
webView,
new TopLoadingProgressEvent(
webView.getId(),
event));
}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
getModule(mReactContext).startPhotoPickerIntent(filePathCallback, "");
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptType);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
String[] acceptTypes = fileChooserParams.getAcceptTypes();
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
Intent intent = fileChooserParams.createIntent();
return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
}
@Override
public void onHostResume() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && mVideoView != null && mVideoView.getSystemUiVisibility() != FULLSCREEN_SYSTEM_UI_VISIBILITY) {
mVideoView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY);
}
}
@Override
public void onHostPause() { }
@Override
public void onHostDestroy() { }
protected ViewGroup getRootView() {
return (ViewGroup) mReactContext.getCurrentActivity().findViewById(android.R.id.content);
}
}
/**
* Subclass of {@link WebView} that implements {@link LifecycleEventListener} interface in order
* to call {@link WebView#destroy} on activity destroy event and also to clear the client
*/
protected static class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJS;
protected @Nullable String injectedJSBeforeContentLoaded;
protected boolean messagingEnabled = false;
protected @Nullable
String messagingModuleName;
protected @Nullable
RNCWebViewClient mRNCWebViewClient;
protected @Nullable
CatalystInstance mCatalystInstance;
protected boolean sendContentSizeChangeEvents = false;
private OnScrollDispatchHelper mOnScrollDispatchHelper;
protected boolean hasScrollEvent = false;
/**
* WebView must be created with an context of the current activity
* <p>
* Activity Context is required for creation of dialogs internally by WebView
* Reactive Native needed for access to ReactNative internal system functionality
*/
public RNCWebView(ThemedReactContext reactContext) {
super(reactContext);
}
public void setIgnoreErrFailedForThisURL(String url) {
mRNCWebViewClient.setIgnoreErrFailedForThisURL(url);
}
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
public void setHasScrollEvent(boolean hasScrollEvent) {
this.hasScrollEvent = hasScrollEvent;
}
@Override
public void onHostResume() {
// do nothing
}
@Override
public void onHostPause() {
// do nothing
}
@Override
public void onHostDestroy() {
cleanupCallbacksAndDestroy();
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if (sendContentSizeChangeEvents) {
dispatchEvent(
this,
new ContentSizeChangeEvent(
this.getId(),
w,
h
)
);
}
}
@Override
public void setWebViewClient(WebViewClient client) {
super.setWebViewClient(client);
if (client instanceof RNCWebViewClient) {
mRNCWebViewClient = (RNCWebViewClient) client;
}
}
public @Nullable
RNCWebViewClient getRNCWebViewClient() {
return mRNCWebViewClient;
}
public void setInjectedJavaScript(@Nullable String js) {
injectedJS = js;
}
2020-02-11 15:06:06 +02:00
public void setInjectedJavaScriptBeforeContentLoaded(@Nullable String js) {
injectedJSBeforeContentLoaded = js;
}
protected RNCWebViewBridge createRNCWebViewBridge(RNCWebView webView) {
return new RNCWebViewBridge(webView);
}
protected void createCatalystInstance() {
ReactContext reactContext = (ReactContext) this.getContext();
if (reactContext != null) {
mCatalystInstance = reactContext.getCatalystInstance();
}
}
@SuppressLint("AddJavascriptInterface")
public void setMessagingEnabled(boolean enabled) {
if (messagingEnabled == enabled) {
return;
}
messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
this.createCatalystInstance();
} else {
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
}
public void setMessagingModuleName(String moduleName) {
messagingModuleName = moduleName;
}
protected void evaluateJavascriptWithFallback(String script) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
evaluateJavascript(script, null);
return;
}
try {
loadUrl("javascript:" + URLEncoder.encode(script, "UTF-8"));
} catch (UnsupportedEncodingException e) {
// UTF-8 should always be supported
throw new RuntimeException(e);
}
}
public void callInjectedJavaScript() {
if (getSettings().getJavaScriptEnabled() &&
injectedJS != null &&
!TextUtils.isEmpty(injectedJS)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
2020-02-11 15:06:06 +02:00
//evaluateJavascriptWithFallback(injectedJS);
}
}
public void onMessage(String message) {
ReactContext reactContext = (ReactContext) this.getContext();
RNCWebView mContext = this;
if (mRNCWebViewClient != null) {
WebView webView = this;
webView.post(new Runnable() {
@Override
public void run() {
if (mRNCWebViewClient == null) {
return;
}
WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, webView.getUrl());
data.putString("data", message);
if (mCatalystInstance != null) {
mContext.sendDirectMessage(data);
} else {
dispatchEvent(webView, new TopMessageEvent(webView.getId(), data));
}
}
});
} else {
WritableMap eventData = Arguments.createMap();
eventData.putString("data", message);
if (mCatalystInstance != null) {
this.sendDirectMessage(eventData);
} else {
dispatchEvent(this, new TopMessageEvent(this.getId(), eventData));
}
}
}
protected void sendDirectMessage(WritableMap data) {
WritableNativeMap event = new WritableNativeMap();
event.putMap("nativeEvent", data);
WritableNativeArray params = new WritableNativeArray();
params.pushMap(event);
mCatalystInstance.callFunction(messagingModuleName, "onMessage", params);
}
protected void onScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (!hasScrollEvent) {
return;
}
if (mOnScrollDispatchHelper == null) {
mOnScrollDispatchHelper = new OnScrollDispatchHelper();
}
if (mOnScrollDispatchHelper.onScrollChanged(x, y)) {
ScrollEvent event = ScrollEvent.obtain(
this.getId(),
ScrollEventType.SCROLL,
x,
y,
mOnScrollDispatchHelper.getXFlingVelocity(),
mOnScrollDispatchHelper.getYFlingVelocity(),
this.computeHorizontalScrollRange(),
this.computeVerticalScrollRange(),
this.getWidth(),
this.getHeight());
dispatchEvent(this, event);
}
}
protected void cleanupCallbacksAndDestroy() {
setWebViewClient(null);
destroy();
}
protected class RNCWebViewBridge {
RNCWebView mContext;
RNCWebViewBridge(RNCWebView c) {
mContext = c;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
}
}
}
}