chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
@@ -0,0 +1,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.reactnativecommunity.webview">
<queries>
<intent>
<action android:name="org.chromium.intent.action.PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.IS_READY_TO_PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS"/>
</intent>
</queries>
<application>
<provider
android:name=".RNCWebViewFileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,26 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="org.chromium.intent.action.PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.IS_READY_TO_PAY"/>
</intent>
<intent>
<action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS"/>
</intent>
</queries>
<application>
<provider
android:name=".RNCWebViewFileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths" />
</provider>
</application>
</manifest>
@@ -0,0 +1,11 @@
package com.reactnativecommunity.webview;
class RNCBasicAuthCredential {
String username;
String password;
RNCBasicAuthCredential(String username, String password) {
this.username = username;
this.password = password;
}
}
@@ -0,0 +1,372 @@
package com.reactnativecommunity.webview;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Message;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions;
import android.webkit.PermissionRequest;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.build.ReactBuildConfig;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import com.facebook.react.uimanager.UIManagerHelper;
import com.reactnativecommunity.webview.events.TopLoadingProgressEvent;
import com.reactnativecommunity.webview.events.TopOpenWindowEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RNCWebChromeClient extends WebChromeClient implements LifecycleEventListener {
protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
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 static final int COMMON_PERMISSION_REQUEST = 3;
protected RNCWebView mWebView;
protected View mVideoView;
protected WebChromeClient.CustomViewCallback mCustomViewCallback;
/*
* - Permissions -
* As native permissions are asynchronously handled by the PermissionListener, many fields have
* to be stored to send permissions results to the webview
*/
// Webview camera & audio permission callback
protected PermissionRequest permissionRequest;
// Webview camera & audio permission already granted
protected List<String> grantedPermissions;
// Webview geolocation permission callback
protected GeolocationPermissions.Callback geolocationPermissionCallback;
// Webview geolocation permission origin callback
protected String geolocationPermissionOrigin;
// true if native permissions dialog is shown, false otherwise
protected boolean permissionsRequestShown = false;
// Pending Android permissions for the next request
protected List<String> pendingPermissions = new ArrayList<>();
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
protected boolean mAllowsProtectedMedia = false;
protected boolean mHasOnOpenWindowEvent = false;
public RNCWebChromeClient(RNCWebView webView) {
this.mWebView = webView;
}
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
final WebView newWebView = new WebView(view.getContext());
if(mHasOnOpenWindowEvent) {
newWebView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading (WebView subview, String url) {
WritableMap event = Arguments.createMap();
event.putString("targetUrl", url);
((RNCWebView) view).dispatchEvent(
view,
new TopOpenWindowEvent(RNCWebViewWrapper.getReactTagFromWebView(view), event)
);
return true;
}
});
}
final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(newWebView);
resultMsg.sendToTarget();
return true;
}
@Override
public boolean onConsoleMessage(ConsoleMessage message) {
if (ReactBuildConfig.DEBUG) {
return super.onConsoleMessage(message);
}
// Ignore console logs in non debug builds.
return true;
}
@Override
public void onProgressChanged(WebView webView, int newProgress) {
super.onProgressChanged(webView, newProgress);
final String url = webView.getUrl();
if (progressChangedFilter.isWaitingForCommandLoadUrl()) {
return;
}
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
WritableMap event = Arguments.createMap();
event.putDouble("target", reactTag);
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);
UIManagerHelper.getEventDispatcherForReactTag(this.mWebView.getThemedReactContext(), reactTag).dispatchEvent(new TopLoadingProgressEvent(reactTag, event));
}
@Override
public void onPermissionRequest(final PermissionRequest request) {
grantedPermissions = new ArrayList<>();
ArrayList<String> requestedAndroidPermissions = new ArrayList<>();
for (String requestedResource : request.getResources()) {
String androidPermission = null;
if (requestedResource.equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
androidPermission = Manifest.permission.RECORD_AUDIO;
} else if (requestedResource.equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
androidPermission = Manifest.permission.CAMERA;
} else if(requestedResource.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) {
if (mAllowsProtectedMedia) {
grantedPermissions.add(requestedResource);
} else {
/**
* Legacy handling (Kept in case it was working under some conditions (given Android version or something))
*
* Try to ask user to grant permission using Activity.requestPermissions
*
* Find more details here: https://github.com/react-native-webview/react-native-webview/pull/2732
*/
androidPermission = PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID;
} }
// TODO: RESOURCE_MIDI_SYSEX, RESOURCE_PROTECTED_MEDIA_ID.
if (androidPermission != null) {
if (ContextCompat.checkSelfPermission(this.mWebView.getThemedReactContext(), androidPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(requestedResource);
} else {
requestedAndroidPermissions.add(androidPermission);
}
}
}
// If all the permissions are already granted, send the response to the WebView synchronously
if (requestedAndroidPermissions.isEmpty()) {
request.grant(grantedPermissions.toArray(new String[0]));
grantedPermissions = null;
return;
}
// Otherwise, ask to Android System for native permissions asynchronously
this.permissionRequest = request;
requestPermissions(requestedAndroidPermissions);
}
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
if (ContextCompat.checkSelfPermission(this.mWebView.getThemedReactContext(), Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
/*
* Keep the trace of callback and origin for the async permission request
*/
geolocationPermissionCallback = callback;
geolocationPermissionOrigin = origin;
requestPermissions(Collections.singletonList(Manifest.permission.ACCESS_FINE_LOCATION));
} else {
callback.invoke(origin, true, false);
}
}
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = this.mWebView.getThemedReactContext().getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
private synchronized void requestPermissions(List<String> permissions) {
/*
* If permissions request dialog is displayed on the screen and another request is sent to the
* activity, the last permission asked is skipped. As a work-around, we use pendingPermissions
* to store next required permissions.
*/
if (permissionsRequestShown) {
pendingPermissions.addAll(permissions);
return;
}
PermissionAwareActivity activity = getPermissionAwareActivity();
permissionsRequestShown = true;
activity.requestPermissions(
permissions.toArray(new String[0]),
COMMON_PERMISSION_REQUEST,
webviewPermissionsListener
);
// Pending permissions have been sent, the list can be cleared
pendingPermissions.clear();
}
private PermissionListener webviewPermissionsListener = (requestCode, permissions, grantResults) -> {
permissionsRequestShown = false;
/*
* As a "pending requests" approach is used, requestCode cannot help to define if the request
* came from geolocation or camera/audio. This is why shouldAnswerToPermissionRequest is used
*/
boolean shouldAnswerToPermissionRequest = false;
for (int i = 0; i < permissions.length; i++) {
String permission = permissions[i];
boolean granted = grantResults[i] == PackageManager.PERMISSION_GRANTED;
if (permission.equals(Manifest.permission.ACCESS_FINE_LOCATION)
&& geolocationPermissionCallback != null
&& geolocationPermissionOrigin != null) {
if (granted) {
geolocationPermissionCallback.invoke(geolocationPermissionOrigin, true, false);
} else {
geolocationPermissionCallback.invoke(geolocationPermissionOrigin, false, false);
}
geolocationPermissionCallback = null;
geolocationPermissionOrigin = null;
}
if (permission.equals(Manifest.permission.RECORD_AUDIO)) {
if (granted && grantedPermissions != null) {
grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);
}
shouldAnswerToPermissionRequest = true;
}
if (permission.equals(Manifest.permission.CAMERA)) {
if (granted && grantedPermissions != null) {
grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);
}
shouldAnswerToPermissionRequest = true;
}
if (permission.equals(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID)) {
if (granted && grantedPermissions != null) {
grantedPermissions.add(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID);
}
shouldAnswerToPermissionRequest = true;
}
}
if (shouldAnswerToPermissionRequest
&& permissionRequest != null
&& grantedPermissions != null) {
permissionRequest.grant(grantedPermissions.toArray(new String[0]));
permissionRequest = null;
grantedPermissions = null;
}
if (!pendingPermissions.isEmpty()) {
requestPermissions(pendingPermissions);
return false;
}
return true;
};
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType) {
this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptType);
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback) {
this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, "");
}
protected void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture) {
this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptType);
}
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
String[] acceptTypes = fileChooserParams.getAcceptTypes();
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
return this.mWebView.getThemedReactContext().getNativeModule(RNCWebViewModule.class).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple, fileChooserParams.isCaptureEnabled());
}
@Override
public void onHostResume() {
if (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 this.mWebView.getThemedReactContext().getCurrentActivity().findViewById(android.R.id.content);
}
public void setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) {
progressChangedFilter = filter;
}
/**
* Set whether or not protected media should be allowed
* /!\ Setting this to false won't revoke permission already granted to the current webpage.
* In order to do so, you'd need to reload the page /!\
*/
public void setAllowsProtectedMedia(boolean enabled) {
mAllowsProtectedMedia = enabled;
}
public void setHasOnOpenWindowEvent(boolean hasEvent) {
mHasOnOpenWindowEvent = hasEvent;
}
}
@@ -0,0 +1,472 @@
package com.reactnativecommunity.webview;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.net.Uri;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.JavaScriptReplyProxy;
import androidx.webkit.WebMessageCompat;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewFeature;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.CatalystInstance;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.ContentSizeChangeEvent;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.views.scroll.OnScrollDispatchHelper;
import com.facebook.react.views.scroll.ScrollEvent;
import com.facebook.react.views.scroll.ScrollEventType;
import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent;
import com.reactnativecommunity.webview.events.TopMessageEvent;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class RNCWebView extends WebView implements LifecycleEventListener {
protected @Nullable
String injectedJS;
protected @Nullable
String injectedJSBeforeContentLoaded;
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebView";
protected @Nullable
RNCWebViewBridge fallbackBridge;
protected @Nullable
WebViewCompat.WebMessageListener bridgeListener = null;
/**
* android.webkit.WebChromeClient fundamentally does not support JS injection into frames other
* than the main frame, so these two properties are mostly here just for parity with iOS & macOS.
*/
protected boolean injectedJavaScriptForMainFrameOnly = true;
protected boolean injectedJavaScriptBeforeContentLoadedForMainFrameOnly = true;
protected boolean messagingEnabled = false;
protected @Nullable
String messagingModuleName;
protected @Nullable
RNCWebViewMessagingModule mMessagingJSModule;
protected @Nullable
RNCWebViewClient mRNCWebViewClient;
protected boolean sendContentSizeChangeEvents = false;
private OnScrollDispatchHelper mOnScrollDispatchHelper;
protected boolean hasScrollEvent = false;
protected boolean nestedScrollEnabled = false;
protected ProgressChangedFilter progressChangedFilter;
/**
* 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);
mMessagingJSModule = ((ThemedReactContext) this.getContext()).getReactApplicationContext().getJSModule(RNCWebViewMessagingModule.class);
progressChangedFilter = new ProgressChangedFilter();
}
public void setIgnoreErrFailedForThisURL(String url) {
mRNCWebViewClient.setIgnoreErrFailedForThisURL(url);
}
public void setBasicAuthCredential(RNCBasicAuthCredential credential) {
mRNCWebViewClient.setBasicAuthCredential(credential);
}
public void setSendContentSizeChangeEvents(boolean sendContentSizeChangeEvents) {
this.sendContentSizeChangeEvents = sendContentSizeChangeEvents;
}
public void setHasScrollEvent(boolean hasScrollEvent) {
this.hasScrollEvent = hasScrollEvent;
}
public void setNestedScrollEnabled(boolean nestedScrollEnabled) {
this.nestedScrollEnabled = nestedScrollEnabled;
}
@Override
public void onHostResume() {
// do nothing
}
@Override
public void onHostPause() {
// do nothing
}
@Override
public void onHostDestroy() {
cleanupCallbacksAndDestroy();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (this.nestedScrollEnabled) {
requestDisallowInterceptTouchEvent(true);
}
return super.onTouchEvent(event);
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
if (sendContentSizeChangeEvents) {
dispatchEvent(
this,
new ContentSizeChangeEvent(
RNCWebViewWrapper.getReactTagFromWebView(this),
w,
h
)
);
}
}
protected @Nullable
List<Map<String, String>> menuCustomItems;
public void setMenuCustomItems(List<Map<String, String>> menuCustomItems) {
this.menuCustomItems = menuCustomItems;
}
@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
if(menuCustomItems == null ){
return super.startActionMode(callback, type);
}
return super.startActionMode(new ActionMode.Callback2() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
for (int i = 0; i < menuCustomItems.size(); i++) {
menu.add(Menu.NONE, i, i, (menuCustomItems.get(i)).get("label"));
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
WritableMap wMap = Arguments.createMap();
RNCWebView.this.evaluateJavascript(
"(function(){return {selection: window.getSelection().toString()} })()",
new ValueCallback<String>() {
@Override
public void onReceiveValue(String selectionJson) {
Map<String, String> menuItemMap = menuCustomItems.get(item.getItemId());
wMap.putString("label", menuItemMap.get("label"));
wMap.putString("key", menuItemMap.get("key"));
String selectionText = "";
try {
selectionText = new JSONObject(selectionJson).getString("selection");
} catch (JSONException ignored) {}
wMap.putString("selectedText", selectionText);
dispatchEvent(RNCWebView.this, new TopCustomMenuSelectionEvent(RNCWebViewWrapper.getReactTagFromWebView(RNCWebView.this), wMap));
mode.finish();
}
}
);
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
mode = null;
}
@Override
public void onGetContentRect (ActionMode mode,
View view,
Rect outRect){
if (callback instanceof ActionMode.Callback2) {
((ActionMode.Callback2) callback).onGetContentRect(mode, view, outRect);
} else {
super.onGetContentRect(mode, view, outRect);
}
}
}, type);
}
@Override
public void setWebViewClient(WebViewClient client) {
super.setWebViewClient(client);
if (client instanceof RNCWebViewClient) {
mRNCWebViewClient = (RNCWebViewClient) client;
mRNCWebViewClient.setProgressChangedFilter(progressChangedFilter);
}
}
WebChromeClient mWebChromeClient;
@Override
public void setWebChromeClient(WebChromeClient client) {
this.mWebChromeClient = client;
super.setWebChromeClient(client);
if (client instanceof RNCWebChromeClient) {
((RNCWebChromeClient) client).setProgressChangedFilter(progressChangedFilter);
}
}
public WebChromeClient getWebChromeClient() {
return this.mWebChromeClient;
}
public @Nullable
RNCWebViewClient getRNCWebViewClient() {
return mRNCWebViewClient;
}
public boolean getMessagingEnabled() {
return this.messagingEnabled;
}
protected void createRNCWebViewBridge(RNCWebView webView) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)){
if (this.bridgeListener == null) {
this.bridgeListener = new WebViewCompat.WebMessageListener() {
@Override
public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy) {
RNCWebView.this.onMessage(message.getData(), sourceOrigin.toString());
}
};
WebViewCompat.addWebMessageListener(
webView,
JAVASCRIPT_INTERFACE,
Set.of("*"),
this.bridgeListener
);
}
} else {
if (fallbackBridge == null) {
fallbackBridge = new RNCWebViewBridge(webView);
addJavascriptInterface(fallbackBridge, JAVASCRIPT_INTERFACE);
}
}
injectJavascriptObject();
}
private void injectJavascriptObject() {
if (getSettings().getJavaScriptEnabled()) {
String js = "(function(){\n" +
" window." + JAVASCRIPT_INTERFACE + " = window." + JAVASCRIPT_INTERFACE + " || {};\n" +
" window." + JAVASCRIPT_INTERFACE + ".injectedObjectJson = function () { return " + (injectedJavaScriptObject == null ? null : ("`" + injectedJavaScriptObject + "`")) + "; };\n" +
"})();";
evaluateJavascriptWithFallback(js);
}
}
@SuppressLint("AddJavascriptInterface")
public void setMessagingEnabled(boolean enabled) {
if (messagingEnabled == enabled) {
return;
}
messagingEnabled = enabled;
if (enabled) {
createRNCWebViewBridge(this);
}
}
protected void evaluateJavascriptWithFallback(String script) {
evaluateJavascript(script, null);
}
public void callInjectedJavaScript() {
if (getSettings().getJavaScriptEnabled() &&
injectedJS != null &&
!TextUtils.isEmpty(injectedJS)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJS + ";\n})();");
injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
}
}
public void callInjectedJavaScriptBeforeContentLoaded() {
if (getSettings().getJavaScriptEnabled() &&
injectedJSBeforeContentLoaded != null &&
!TextUtils.isEmpty(injectedJSBeforeContentLoaded)) {
evaluateJavascriptWithFallback("(function() {\n" + injectedJSBeforeContentLoaded + ";\n})();");
injectJavascriptObject(); // re-inject the Javascript object in case it has been overwritten.
}
}
protected String injectedJavaScriptObject = null;
public void setInjectedJavaScriptObject(String obj) {
this.injectedJavaScriptObject = obj;
injectJavascriptObject();
}
public void onMessage(String message, String sourceUrl) {
ThemedReactContext reactContext = getThemedReactContext();
RNCWebView mWebView = this;
if (mRNCWebViewClient != null) {
WebView webView = this;
webView.post(new Runnable() {
@Override
public void run() {
if (mRNCWebViewClient == null) {
return;
}
WritableMap data = mRNCWebViewClient.createWebViewEvent(webView, sourceUrl);
data.putString("data", message);
if (mMessagingJSModule != null) {
dispatchDirectMessage(data);
} else {
dispatchEvent(webView, new TopMessageEvent(RNCWebViewWrapper.getReactTagFromWebView(webView), data));
}
}
});
} else {
WritableMap eventData = Arguments.createMap();
eventData.putString("data", message);
if (mMessagingJSModule != null) {
dispatchDirectMessage(eventData);
} else {
dispatchEvent(this, new TopMessageEvent(RNCWebViewWrapper.getReactTagFromWebView(this), eventData));
}
}
}
protected void dispatchDirectMessage(WritableMap data) {
WritableNativeMap event = new WritableNativeMap();
event.putMap("nativeEvent", data);
event.putString("messagingModuleName", messagingModuleName);
mMessagingJSModule.onMessage(event);
}
protected boolean dispatchDirectShouldStartLoadWithRequest(WritableMap data) {
WritableNativeMap event = new WritableNativeMap();
event.putMap("nativeEvent", data);
event.putString("messagingModuleName", messagingModuleName);
mMessagingJSModule.onShouldStartLoadWithRequest(event);
return true;
}
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(
RNCWebViewWrapper.getReactTagFromWebView(this),
ScrollEventType.SCROLL,
x,
y,
mOnScrollDispatchHelper.getXFlingVelocity(),
mOnScrollDispatchHelper.getYFlingVelocity(),
this.computeHorizontalScrollRange(),
this.computeVerticalScrollRange(),
this.getWidth(),
this.getHeight());
dispatchEvent(this, event);
}
}
protected void dispatchEvent(WebView webView, Event event) {
ThemedReactContext reactContext = getThemedReactContext();
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag).dispatchEvent(event);
}
protected void cleanupCallbacksAndDestroy() {
setWebViewClient(null);
destroy();
}
@Override
public void destroy() {
if (mWebChromeClient != null) {
mWebChromeClient.onHideCustomView();
}
super.destroy();
}
public ThemedReactContext getThemedReactContext() {
return (ThemedReactContext) this.getContext();
}
public ReactApplicationContext getReactApplicationContext() {
return this.getThemedReactContext().getReactApplicationContext();
}
protected class RNCWebViewBridge {
private String TAG = "RNCWebViewBridge";
RNCWebView mWebView;
RNCWebViewBridge(RNCWebView c) {
mWebView = c;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
if (mWebView.getMessagingEnabled()) {
// Post to main thread because `mWebView.getUrl()` requires to be executed on main.
mWebView.post(() -> mWebView.onMessage(message, mWebView.getUrl()));
} else {
FLog.w(TAG, "ReactNativeWebView.postMessage method was called but messaging is disabled. Pass an onMessage handler to the WebView.");
}
}
}
protected static class ProgressChangedFilter {
private boolean waitingForCommandLoadUrl = false;
public void setWaitingForCommandLoadUrl(boolean isWaiting) {
waitingForCommandLoadUrl = isWaiting;
}
public boolean isWaitingForCommandLoadUrl() {
return waitingForCommandLoadUrl;
}
}
}
@@ -0,0 +1,328 @@
package com.reactnativecommunity.webview;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.webkit.HttpAuthHandler;
import android.webkit.RenderProcessGoneDetail;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.util.Pair;
import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingFinishEvent;
import com.reactnativecommunity.webview.events.TopLoadingStartEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import java.util.concurrent.atomic.AtomicReference;
public class RNCWebViewClient extends WebViewClient {
private static String TAG = "RNCWebViewClient";
protected static final int SHOULD_OVERRIDE_URL_LOADING_TIMEOUT = 250;
protected boolean mLastLoadFailed = false;
protected RNCWebView.ProgressChangedFilter progressChangedFilter = null;
protected @Nullable String ignoreErrFailedForThisURL = null;
protected @Nullable RNCBasicAuthCredential basicAuthCredential = null;
public void setIgnoreErrFailedForThisURL(@Nullable String url) {
ignoreErrFailedForThisURL = url;
}
public void setBasicAuthCredential(@Nullable RNCBasicAuthCredential credential) {
basicAuthCredential = credential;
}
@Override
public void onPageFinished(WebView webView, String url) {
super.onPageFinished(webView, url);
String cookies = CookieManager.getInstance().getCookie(url);
if (cookies != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
CookieManager.getInstance().flush();
}else {
CookieSyncManager.getInstance().sync();
}
}
if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScript();
emitFinishEvent(webView, url);
}
}
@Override
public void doUpdateVisitedHistory (WebView webView, String url, boolean isReload) {
super.doUpdateVisitedHistory(webView, url, isReload);
((RNCWebView) webView).dispatchEvent(
webView,
new TopLoadingStartEvent(
RNCWebViewWrapper.getReactTagFromWebView(webView),
createWebViewEvent(webView, url)));
}
@Override
public void onPageStarted(WebView webView, String url, Bitmap favicon) {
super.onPageStarted(webView, url, favicon);
mLastLoadFailed = false;
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScriptBeforeContentLoaded();
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
final RNCWebView rncWebView = (RNCWebView) view;
final boolean isJsDebugging = rncWebView.getReactApplicationContext().getJavaScriptContextHolder().get() == 0;
if (!isJsDebugging && rncWebView.mMessagingJSModule != null) {
final Pair<Double, AtomicReference<RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState>> lock = RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.getNewLock();
final double lockIdentifier = lock.first;
final AtomicReference<RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState> lockObject = lock.second;
final WritableMap event = createWebViewEvent(view, url);
event.putDouble("lockIdentifier", lockIdentifier);
rncWebView.dispatchDirectShouldStartLoadWithRequest(event);
try {
assert lockObject != null;
synchronized (lockObject) {
final long startTime = SystemClock.elapsedRealtime();
while (lockObject.get() == RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.UNDECIDED) {
if (SystemClock.elapsedRealtime() - startTime > SHOULD_OVERRIDE_URL_LOADING_TIMEOUT) {
FLog.w(TAG, "Did not receive response to shouldOverrideUrlLoading in time, defaulting to allow loading.");
RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
return false;
}
lockObject.wait(SHOULD_OVERRIDE_URL_LOADING_TIMEOUT);
}
}
} catch (InterruptedException e) {
FLog.e(TAG, "shouldOverrideUrlLoading was interrupted while waiting for result.", e);
RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
return false;
}
final boolean shouldOverride = lockObject.get() == RNCWebViewModuleImpl.ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE;
RNCWebViewModuleImpl.shouldOverrideUrlLoadingLock.removeLock(lockIdentifier);
return shouldOverride;
} else {
FLog.w(TAG, "Couldn't use blocking synchronous call for onShouldStartLoadWithRequest due to debugging or missing Catalyst instance, falling back to old event-and-load.");
progressChangedFilter.setWaitingForCommandLoadUrl(true);
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(view);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) view.getContext(), reactTag).dispatchEvent(new TopShouldStartLoadWithRequestEvent(
reactTag,
createWebViewEvent(view, url)));
return true;
}
}
@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
final String url = request.getUrl().toString();
return this.shouldOverrideUrlLoading(view, url);
}
@Override
public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {
if (basicAuthCredential != null) {
handler.proceed(basicAuthCredential.username, basicAuthCredential.password);
return;
}
super.onReceivedHttpAuthRequest(view, handler, host, realm);
}
@Override
public void onReceivedSslError(final WebView webView, final SslErrorHandler handler, final SslError error) {
// onReceivedSslError is called for most requests, per Android docs: https://developer.android.com/reference/android/webkit/WebViewClient#onReceivedSslError(android.webkit.WebView,%2520android.webkit.SslErrorHandler,%2520android.net.http.SslError)
// WebView.getUrl() will return the top-level window URL.
// If a top-level navigation triggers this error handler, the top-level URL will be the failing URL (not the URL of the currently-rendered page).
// This is desired behavior. We later use these values to determine whether the request is a top-level navigation or a subresource request.
String topWindowUrl = webView.getUrl();
String failingUrl = error.getUrl();
// Cancel request after obtaining top-level URL.
// If request is cancelled before obtaining top-level URL, undesired behavior may occur.
// Undesired behavior: Return value of WebView.getUrl() may be the current URL instead of the failing URL.
handler.cancel();
if (!topWindowUrl.equalsIgnoreCase(failingUrl)) {
// If error is not due to top-level navigation, then do not call onReceivedError()
Log.w(TAG, "Resource blocked from loading due to SSL error. Blocked URL: "+failingUrl);
return;
}
int code = error.getPrimaryError();
String description = "";
String descriptionPrefix = "SSL error: ";
// https://developer.android.com/reference/android/net/http/SslError.html
switch (code) {
case SslError.SSL_DATE_INVALID:
description = "The date of the certificate is invalid";
break;
case SslError.SSL_EXPIRED:
description = "The certificate has expired";
break;
case SslError.SSL_IDMISMATCH:
description = "Hostname mismatch";
break;
case SslError.SSL_INVALID:
description = "A generic error occurred";
break;
case SslError.SSL_NOTYETVALID:
description = "The certificate is not yet valid";
break;
case SslError.SSL_UNTRUSTED:
description = "The certificate authority is not trusted";
break;
default:
description = "Unknown SSL Error";
break;
}
description = descriptionPrefix + description;
this.onReceivedError(
webView,
code,
description,
failingUrl
);
}
@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);
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopLoadingErrorEvent(reactTag, eventData));
}
@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());
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopHttpErrorEvent(reactTag, eventData));
}
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) {
// WebViewClient.onRenderProcessGone was added in O.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
super.onRenderProcessGone(webView, detail);
if(detail.didCrash()){
Log.e(TAG, "The WebView rendering process crashed.");
}
else{
Log.w(TAG, "The WebView rendering process was killed by the system.");
}
// if webView is null, we cannot return any event
// since the view is already dead/disposed
// still prevent the app crash by returning true.
if(webView == null){
return true;
}
WritableMap event = createWebViewEvent(webView, webView.getUrl());
event.putBoolean("didCrash", detail.didCrash());
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopRenderProcessGoneEvent(reactTag, event));
// returning false would crash the app.
return true;
}
protected void emitFinishEvent(WebView webView, String url) {
int reactTag = RNCWebViewWrapper.getReactTagFromWebView(webView);
UIManagerHelper.getEventDispatcherForReactTag((ReactContext) webView.getContext(), reactTag).dispatchEvent(new TopLoadingFinishEvent(reactTag, createWebViewEvent(webView, url)));
}
protected WritableMap createWebViewEvent(WebView webView, String url) {
WritableMap event = Arguments.createMap();
event.putDouble("target", RNCWebViewWrapper.getReactTagFromWebView(webView));
// 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 setProgressChangedFilter(RNCWebView.ProgressChangedFilter filter) {
progressChangedFilter = filter;
}
}
@@ -0,0 +1,11 @@
package com.reactnativecommunity.webview;
import android.webkit.WebView;
/**
* Implement this interface in order to config your {@link WebView}. An instance of that
* implementation will have to be given as a constructor argument to {@link RNCWebViewManager}.
*/
public interface RNCWebViewConfig {
void configWebView(WebView webView);
}
@@ -0,0 +1,14 @@
package com.reactnativecommunity.webview;
import androidx.core.content.FileProvider;
/**
* Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
* <p>
* See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
*/
public class RNCWebViewFileProvider extends FileProvider {
// This class intentionally left blank.
}
@@ -0,0 +1,725 @@
package com.reactnativecommunity.webview
import android.app.DownloadManager
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.webkit.CookieManager
import android.webkit.DownloadListener
import android.webkit.WebSettings
import android.webkit.WebView
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.MapBuilder
import com.facebook.react.common.build.ReactBuildConfig
import com.facebook.react.uimanager.ThemedReactContext
import org.json.JSONException
import org.json.JSONObject
import java.io.UnsupportedEncodingException
import java.net.MalformedURLException
import java.net.URL
import java.util.Locale
val invalidCharRegex = "[\\\\/%\"]".toRegex()
class RNCWebViewManagerImpl(private val newArch: Boolean = false) {
companion object {
const val NAME = "RNCWebView"
}
private val TAG = "RNCWebViewManagerImpl"
private var mWebViewConfig: RNCWebViewConfig = RNCWebViewConfig { webView: WebView? -> }
private var mAllowsFullscreenVideo = false
private var mAllowsProtectedMedia = false
private var mDownloadingMessage: String? = null
private var mLackPermissionToDownloadMessage: String? = null
private var mHasOnOpenWindowEvent = false
private var mPendingSource: ReadableMap? = null
private var mUserAgent: String? = null
private var mUserAgentWithApplicationName: String? = null
private val HTML_ENCODING = "UTF-8"
private val HTML_MIME_TYPE = "text/html"
private val HTTP_METHOD_POST = "POST"
// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
private val BLANK_URL = "about:blank"
private val DEFAULT_DOWNLOADING_MESSAGE = "Downloading"
private val DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE =
"Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files."
fun createRNCWebViewInstance(context: ThemedReactContext): RNCWebView {
return RNCWebView(context)
}
fun createViewInstance(context: ThemedReactContext): RNCWebViewWrapper {
val webView = createRNCWebViewInstance(context)
return createViewInstance(context, webView);
}
fun createViewInstance(context: ThemedReactContext, webView: RNCWebView): RNCWebViewWrapper {
setupWebChromeClient(webView)
context.addLifecycleEventListener(webView)
mWebViewConfig.configWebView(webView)
val settings = webView.settings
settings.builtInZoomControls = true
settings.displayZoomControls = false
settings.domStorageEnabled = true
settings.setSupportMultipleWindows(true)
settings.allowFileAccess = false
settings.allowContentAccess = false
settings.allowFileAccessFromFileURLs = false
settings.allowUniversalAccessFromFileURLs = false
settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
// Fixes broken full-screen modals/galleries due to body height being 0.
webView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
if (ReactBuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true)
}
webView.setDownloadListener(DownloadListener { url, userAgent, contentDisposition, mimetype, contentLength ->
webView.setIgnoreErrFailedForThisURL(url)
val module = webView.reactApplicationContext.getNativeModule(RNCWebViewModule::class.java) ?: return@DownloadListener
val request: DownloadManager.Request = try {
DownloadManager.Request(Uri.parse(url))
} catch (e: IllegalArgumentException) {
Log.w(TAG, "Unsupported URI, aborting download", e)
return@DownloadListener
}
var fileName = URLUtil.guessFileName(url, contentDisposition, mimetype)
// Sanitize filename by replacing invalid characters with "_"
fileName = fileName.replace(invalidCharRegex, "_")
val downloadMessage = "Downloading $fileName"
//Attempt to add cookie, if it exists
var urlObj: URL? = null
try {
urlObj = URL(url)
val baseUrl = urlObj.protocol + "://" + urlObj.host
val cookie = CookieManager.getInstance().getCookie(baseUrl)
request.addRequestHeader("Cookie", cookie)
} catch (e: MalformedURLException) {
Log.w(TAG, "Error getting cookie for DownloadManager", e)
}
//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(
getDownloadingMessageOrDefault(),
getLackPermissionToDownloadMessageOrDefault()
)
) {
module.downloadFile(
getDownloadingMessageOrDefault()
)
}
})
return RNCWebViewWrapper(context, webView)
}
private fun setupWebChromeClient(
webView: RNCWebView,
) {
val activity = webView.themedReactContext.currentActivity
if (mAllowsFullscreenVideo && activity != null) {
val initialRequestedOrientation = activity.requestedOrientation
val webChromeClient: RNCWebChromeClient =
object : RNCWebChromeClient(webView) {
override fun getDefaultVideoPoster(): Bitmap? {
return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
}
override fun onShowCustomView(view: View, callback: CustomViewCallback) {
if (mVideoView != null) {
callback.onCustomViewHidden()
return
}
mVideoView = view
mCustomViewCallback = callback
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
mVideoView.systemUiVisibility = FULLSCREEN_SYSTEM_UI_VISIBILITY
activity.window.setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
)
mVideoView.setBackgroundColor(Color.BLACK)
// Since RN's Modals interfere with the View hierarchy
// we will decide which View to hide if the hierarchy
// does not match (i.e., the WebView is within a Modal)
// NOTE: We could use `mWebView.getRootView()` instead of `getRootView()`
// but that breaks the Modal's styles and layout, so we need this to render
// in the main View hierarchy regardless
val rootView = rootView
rootView.addView(mVideoView, FULLSCREEN_LAYOUT_PARAMS)
// Different root views, we are in a Modal
if (rootView.rootView !== mWebView.rootView) {
mWebView.rootView.visibility = View.GONE
} else {
// Same view hierarchy (no Modal), just hide the WebView then
mWebView.visibility = View.GONE
}
mWebView.themedReactContext.addLifecycleEventListener(this)
}
override fun onHideCustomView() {
if (mVideoView == null) {
return
}
// Same logic as above
val rootView = rootView
if (rootView.rootView !== mWebView.rootView) {
mWebView.rootView.visibility = View.VISIBLE
} else {
// Same view hierarchy (no Modal)
mWebView.visibility = View.VISIBLE
}
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
rootView.removeView(mVideoView)
mCustomViewCallback.onCustomViewHidden()
mVideoView = null
mCustomViewCallback = null
activity.requestedOrientation = initialRequestedOrientation
mWebView.themedReactContext.removeLifecycleEventListener(this)
}
}
webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia);
webChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.webChromeClient = webChromeClient
} else {
var webChromeClient = webView.webChromeClient as RNCWebChromeClient?
webChromeClient?.onHideCustomView()
webChromeClient = object : RNCWebChromeClient(webView) {
override fun getDefaultVideoPoster(): Bitmap? {
return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888)
}
}
webChromeClient.setAllowsProtectedMedia(mAllowsProtectedMedia);
webChromeClient.setHasOnOpenWindowEvent(mHasOnOpenWindowEvent);
webView.webChromeClient = webChromeClient
}
}
fun setUserAgent(viewWrapper: RNCWebViewWrapper, userAgent: String?) {
mUserAgent = userAgent
setUserAgentString(viewWrapper)
}
fun setApplicationNameForUserAgent(viewWrapper: RNCWebViewWrapper, applicationName: String?) {
when {
applicationName != null -> {
val defaultUserAgent = WebSettings.getDefaultUserAgent(viewWrapper.webView.context)
mUserAgentWithApplicationName = "$defaultUserAgent $applicationName"
}
else -> {
mUserAgentWithApplicationName = null
}
}
setUserAgentString(viewWrapper)
}
private fun setUserAgentString(viewWrapper: RNCWebViewWrapper) {
val view = viewWrapper.webView
when {
mUserAgent != null -> {
view.settings.userAgentString = mUserAgent
}
mUserAgentWithApplicationName != null -> {
view.settings.userAgentString = mUserAgentWithApplicationName
}
else -> {
view.settings.userAgentString = WebSettings.getDefaultUserAgent(view.context)
}
}
}
fun setBasicAuthCredential(viewWrapper: RNCWebViewWrapper, credential: ReadableMap?) {
var basicAuthCredential: RNCBasicAuthCredential? = null
if (credential != null) {
if (credential.hasKey("username") && credential.hasKey("password")) {
val username = credential.getString("username")
val password = credential.getString("password")
basicAuthCredential = RNCBasicAuthCredential(username, password)
}
}
viewWrapper.webView.setBasicAuthCredential(basicAuthCredential)
}
fun onAfterUpdateTransaction(viewWrapper: RNCWebViewWrapper) {
mPendingSource?.let { source ->
loadSource(viewWrapper, source)
}
mPendingSource = null
}
fun onDropViewInstance(viewWrapper: RNCWebViewWrapper) {
val webView = viewWrapper.webView
webView.themedReactContext.removeLifecycleEventListener(webView)
webView.cleanupCallbacksAndDestroy()
webView.mWebChromeClient = null
}
val COMMAND_GO_BACK = 1
val COMMAND_GO_FORWARD = 2
val COMMAND_RELOAD = 3
val COMMAND_STOP_LOADING = 4
val COMMAND_POST_MESSAGE = 5
val COMMAND_INJECT_JAVASCRIPT = 6
val COMMAND_LOAD_URL = 7
val COMMAND_FOCUS = 8
// android commands
val COMMAND_CLEAR_FORM_DATA = 1000
val COMMAND_CLEAR_CACHE = 1001
val COMMAND_CLEAR_HISTORY = 1002
fun getCommandsMap(): Map<String, Int>? {
return MapBuilder.builder<String, Int>()
.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()
}
fun receiveCommand(viewWrapper: RNCWebViewWrapper, commandId: String, args: ReadableArray) {
val webView = viewWrapper.webView
when (commandId) {
"goBack" -> webView.goBack()
"goForward" -> webView.goForward()
"reload" -> webView.reload()
"stopLoading" -> webView.stopLoading()
"postMessage" -> try {
val eventInitDict = JSONObject()
eventInitDict.put("data", args.getString(0))
webView.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 (e: JSONException) {
throw RuntimeException(e)
}
"injectJavaScript" -> webView.evaluateJavascriptWithFallback(args.getString(0))
"loadUrl" -> {
val url = args?.getString(0) ?: throw RuntimeException("Arguments for loading an url are null!")
webView.progressChangedFilter.setWaitingForCommandLoadUrl(false)
webView.loadUrl(url)
}
"requestFocus" -> webView.requestFocus()
"clearFormData" -> webView.clearFormData()
"clearCache" -> {
val includeDiskFiles = args != null && args.getBoolean(0)
webView.clearCache(includeDiskFiles)
}
"clearHistory" -> webView.clearHistory()
}
}
fun setMixedContentMode(viewWrapper: RNCWebViewWrapper, mixedContentMode: String?) {
val view = viewWrapper.webView
if (mixedContentMode == null || "never" == mixedContentMode) {
view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
} else if ("always" == mixedContentMode) {
view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
} else if ("compatibility" == mixedContentMode) {
view.settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
}
}
fun setAllowUniversalAccessFromFileURLs(viewWrapper: RNCWebViewWrapper, allow: Boolean) {
viewWrapper.webView.settings.allowUniversalAccessFromFileURLs = allow
}
private fun getDownloadingMessageOrDefault(): String? {
return mDownloadingMessage ?: DEFAULT_DOWNLOADING_MESSAGE
}
private fun getLackPermissionToDownloadMessageOrDefault(): String? {
return mLackPermissionToDownloadMessage
?: DEFAULT_LACK_PERMISSION_TO_DOWNLOAD_MESSAGE
}
fun setSource(viewWrapper: RNCWebViewWrapper, source: ReadableMap?) {
mPendingSource = source
}
private fun loadSource(viewWrapper: RNCWebViewWrapper, source: ReadableMap?) {
val view = viewWrapper.webView
if (source != null) {
if (source.hasKey("html")) {
val html = source.getString("html")
val baseUrl = if (source.hasKey("baseUrl")) source.getString("baseUrl") else ""
view.loadDataWithBaseURL(
baseUrl,
html!!,
HTML_MIME_TYPE,
HTML_ENCODING,
null
)
return
}
if (source.hasKey("uri")) {
val url = source.getString("uri")
val previousUrl = view.url
if (previousUrl != null && previousUrl == url) {
return
}
if (source.hasKey("method")) {
val method = source.getString("method")
if (method.equals(HTTP_METHOD_POST, ignoreCase = true)) {
var postData: ByteArray? = null
if (source.hasKey("body")) {
val body = source.getString("body")
postData = try {
body!!.toByteArray(charset("UTF-8"))
} catch (e: UnsupportedEncodingException) {
body!!.toByteArray()
}
}
if (postData == null) {
postData = ByteArray(0)
}
view.postUrl(url!!, postData)
return
}
}
val headerMap = HashMap<String, String?>()
if (source.hasKey("headers")) {
if (newArch) {
val headerArray = source.getArray("headers");
for (header in headerArray!!.toArrayList()) {
val headerCasted = header as HashMap<String, String>
val name = headerCasted.get("name") ?: ""
val value = headerCasted.get("value") ?: ""
if ("user-agent" == name.lowercase(Locale.ENGLISH)) {
view.settings.userAgentString = value
} else {
headerMap[name] = value
}
}
} else {
val headers = source.getMap("headers")
val iter = headers!!.keySetIterator()
while (iter.hasNextKey()) {
val key = iter.nextKey()
if ("user-agent" == key.lowercase(Locale.ENGLISH)) {
view.settings.userAgentString = headers.getString(key)
} else {
headerMap[key] = headers.getString(key)
}
}
}
}
view.loadUrl(url!!, headerMap)
return
}
}
view.loadUrl(BLANK_URL)
}
fun setMessagingModuleName(viewWrapper: RNCWebViewWrapper, value: String?) {
val view = viewWrapper.webView
view.messagingModuleName = value
}
fun setCacheEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
view.settings.cacheMode = if (enabled) WebSettings.LOAD_DEFAULT else WebSettings.LOAD_NO_CACHE
}
fun setIncognito(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
// Don't do anything when incognito is disabled
if (!enabled) {
return;
}
// Remove all previous cookies
CookieManager.getInstance().removeAllCookies(null);
// Disable caching
view.settings.cacheMode = WebSettings.LOAD_NO_CACHE
view.clearHistory();
view.clearCache(true);
// No form data or autofill enabled
view.clearFormData();
view.settings.savePassword = false;
view.settings.saveFormData = false;
}
fun setInjectedJavaScript(viewWrapper: RNCWebViewWrapper, injectedJavaScript: String?) {
val view = viewWrapper.webView
view.injectedJS = injectedJavaScript
}
fun setInjectedJavaScriptBeforeContentLoaded(viewWrapper: RNCWebViewWrapper, value: String?) {
val view = viewWrapper.webView
view.injectedJSBeforeContentLoaded = value
}
fun setInjectedJavaScriptForMainFrameOnly(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.injectedJavaScriptForMainFrameOnly = value
}
fun setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.injectedJavaScriptBeforeContentLoadedForMainFrameOnly = value
}
fun setInjectedJavaScriptObject(viewWrapper: RNCWebViewWrapper, value: String?) {
val view = viewWrapper.webView
view.setInjectedJavaScriptObject(value)
}
fun setJavaScriptCanOpenWindowsAutomatically(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.javaScriptCanOpenWindowsAutomatically = value
}
fun setShowsVerticalScrollIndicator(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.isVerticalScrollBarEnabled = value
}
fun setShowsHorizontalScrollIndicator(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.isHorizontalScrollBarEnabled = value
}
fun setMessagingEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.setMessagingEnabled(value)
}
fun setMediaPlaybackRequiresUserAction(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.mediaPlaybackRequiresUserGesture = value
}
fun setHasOnScroll(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.setHasScrollEvent(value)
}
fun setJavaScriptEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
view.settings.javaScriptEnabled = enabled
}
fun setAllowFileAccess(viewWrapper: RNCWebViewWrapper, allowFileAccess: Boolean) {
val view = viewWrapper.webView
view.settings.allowFileAccess = allowFileAccess;
}
fun setAllowFileAccessFromFileURLs(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.allowFileAccessFromFileURLs = value;
}
fun setAllowsFullscreenVideo(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
mAllowsFullscreenVideo = value
setupWebChromeClient(view)
}
fun setAndroidLayerType(viewWrapper: RNCWebViewWrapper, layerTypeString: String?) {
val view = viewWrapper.webView
val layerType = when (layerTypeString) {
"hardware" -> View.LAYER_TYPE_HARDWARE
"software" -> View.LAYER_TYPE_SOFTWARE
else -> View.LAYER_TYPE_NONE
}
view.setLayerType(layerType, null)
}
fun setCacheMode(viewWrapper: RNCWebViewWrapper, cacheModeString: String?) {
val view = viewWrapper.webView
view.settings.cacheMode = when (cacheModeString) {
"LOAD_CACHE_ONLY" -> WebSettings.LOAD_CACHE_ONLY
"LOAD_CACHE_ELSE_NETWORK" -> WebSettings.LOAD_CACHE_ELSE_NETWORK
"LOAD_NO_CACHE" -> WebSettings.LOAD_NO_CACHE
"LOAD_DEFAULT" -> WebSettings.LOAD_DEFAULT
else -> WebSettings.LOAD_DEFAULT
}
}
fun setDomStorageEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.domStorageEnabled = value
}
fun setDownloadingMessage(value: String?) {
mDownloadingMessage = value
}
fun setForceDarkOn(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
// Only Android 10+ support dark mode
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
val forceDarkMode =
if (enabled) WebSettingsCompat.FORCE_DARK_ON else WebSettingsCompat.FORCE_DARK_OFF
WebSettingsCompat.setForceDark(view.settings, forceDarkMode)
}
// Set how WebView content should be darkened.
// PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING: checks for the "color-scheme" <meta> tag.
// If present, it uses media queries. If absent, it applies user-agent (automatic)
// More information about Force Dark Strategy can be found here:
// https://developer.android.com/reference/androidx/webkit/WebSettingsCompat#setForceDarkStrategy(android.webkit.WebSettings)
if (enabled && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
WebSettingsCompat.setForceDarkStrategy(
view.settings,
WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
)
}
}
}
fun setGeolocationEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.setGeolocationEnabled(value)
}
fun setLackPermissionToDownloadMessage(value: String?) {
mLackPermissionToDownloadMessage = value
}
fun setHasOnOpenWindowEvent(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
mHasOnOpenWindowEvent = value
setupWebChromeClient(view)
}
fun setMinimumFontSize(viewWrapper: RNCWebViewWrapper, value: Int) {
val view = viewWrapper.webView
view.settings.minimumFontSize = value
}
fun setAllowsProtectedMedia(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
// This variable is used to keep consistency
// in case a new WebChromeClient is created
// (eg. when mAllowsFullScreenVideo changes)
mAllowsProtectedMedia = enabled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val client = view.webChromeClient
if (client != null && client is RNCWebChromeClient) {
client.setAllowsProtectedMedia(enabled)
}
}
}
fun setMenuCustomItems(viewWrapper: RNCWebViewWrapper, value: ReadableArray?) {
val view = viewWrapper.webView
when (value) {
null -> view.setMenuCustomItems(null)
else -> view.setMenuCustomItems(value.toArrayList() as List<Map<String, String>>)
}
}
fun setNestedScrollEnabled(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.nestedScrollEnabled = value
}
fun setOverScrollMode(viewWrapper: RNCWebViewWrapper, overScrollModeString: String?) {
val view = viewWrapper.webView
view.overScrollMode = when (overScrollModeString) {
"never" -> View.OVER_SCROLL_NEVER
"content" -> View.OVER_SCROLL_IF_CONTENT_SCROLLS
"always" -> View.OVER_SCROLL_ALWAYS
else -> View.OVER_SCROLL_ALWAYS
}
}
fun setSaveFormDataDisabled(viewWrapper: RNCWebViewWrapper, disabled: Boolean) {
val view = viewWrapper.webView
view.settings.saveFormData = !disabled
}
fun setScalesPageToFit(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.loadWithOverviewMode = value
view.settings.useWideViewPort = value
}
fun setSetBuiltInZoomControls(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.builtInZoomControls = value
}
fun setSetDisplayZoomControls(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.displayZoomControls = value
}
fun setSetSupportMultipleWindows(viewWrapper: RNCWebViewWrapper, value: Boolean) {
val view = viewWrapper.webView
view.settings.setSupportMultipleWindows(value)
}
fun setTextZoom(viewWrapper: RNCWebViewWrapper, value: Int) {
val view = viewWrapper.webView
view.settings.textZoom = value
}
fun setThirdPartyCookiesEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
CookieManager.getInstance().setAcceptThirdPartyCookies(view, enabled)
}
fun setWebviewDebuggingEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
RNCWebView.setWebContentsDebuggingEnabled(enabled)
}
fun setPaymentRequestEnabled(viewWrapper: RNCWebViewWrapper, enabled: Boolean) {
val view = viewWrapper.webView
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
WebSettingsCompat.setPaymentRequestEnabled(view.settings, enabled)
}
}
}
@@ -0,0 +1,9 @@
package com.reactnativecommunity.webview
import com.facebook.react.bridge.JavaScriptModule
import com.facebook.react.bridge.WritableMap
internal interface RNCWebViewMessagingModule : JavaScriptModule {
fun onShouldStartLoadWithRequest(event: WritableMap)
fun onMessage(event: WritableMap)
}
@@ -0,0 +1,554 @@
package com.reactnativecommunity.webview;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.core.util.Pair;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.widget.Toast;
import com.facebook.common.activitylistener.ActivityListenerManager;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.PermissionAwareActivity;
import com.facebook.react.modules.core.PermissionListener;
import java.io.File;
import java.io.IOException;
import java.lang.SecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicReference;
import static android.app.Activity.RESULT_OK;
public class RNCWebViewModuleImpl implements ActivityEventListener {
public static final String NAME = "RNCWebViewModule";
public static final int PICKER = 1;
public static final int PICKER_LEGACY = 3;
public static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
final private ReactApplicationContext mContext;
private DownloadManager.Request mDownloadRequest;
private ValueCallback<Uri> mFilePathCallbackLegacy;
private ValueCallback<Uri[]> mFilePathCallback;
private File mOutputImage;
private File mOutputVideo;
public RNCWebViewModuleImpl(ReactApplicationContext context) {
mContext = context;
context.addActivityEventListener(this);
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (mFilePathCallback == null && mFilePathCallbackLegacy == null) {
return;
}
boolean imageTaken = false;
boolean videoTaken = false;
if (mOutputImage != null && mOutputImage.length() > 0) {
imageTaken = true;
}
if (mOutputVideo != null && mOutputVideo.length() > 0) {
videoTaken = true;
}
// based off of which button was pressed, we get an activity result and a file
// the camera activity doesn't properly return the filename* (I think?) so we use
// this filename instead
switch (requestCode) {
case RNCWebViewModuleImpl.PICKER:
if (resultCode != RESULT_OK) {
if (mFilePathCallback != null) {
mFilePathCallback.onReceiveValue(null);
}
} else {
if (imageTaken) {
mFilePathCallback.onReceiveValue(new Uri[]{getOutputUri(mOutputImage)});
} else if (videoTaken) {
mFilePathCallback.onReceiveValue(new Uri[]{getOutputUri(mOutputVideo)});
} else {
mFilePathCallback.onReceiveValue(getSelectedFiles(data, resultCode));
}
}
break;
case RNCWebViewModuleImpl.PICKER_LEGACY:
if (resultCode != RESULT_OK) {
mFilePathCallbackLegacy.onReceiveValue(null);
} else {
if (imageTaken) {
mFilePathCallbackLegacy.onReceiveValue(getOutputUri(mOutputImage));
} else if (videoTaken) {
mFilePathCallbackLegacy.onReceiveValue(getOutputUri(mOutputVideo));
} else {
mFilePathCallbackLegacy.onReceiveValue(data.getData());
}
}
break;
}
if (mOutputImage != null && !imageTaken) {
mOutputImage.delete();
}
if (mOutputVideo != null && !videoTaken) {
mOutputVideo.delete();
}
mFilePathCallback = null;
mFilePathCallbackLegacy = null;
mOutputImage = null;
mOutputVideo = null;
}
@Override
public void onNewIntent(Intent intent) {
}
protected static class ShouldOverrideUrlLoadingLock {
protected enum ShouldOverrideCallbackState {
UNDECIDED,
SHOULD_OVERRIDE,
DO_NOT_OVERRIDE,
}
private double nextLockIdentifier = 1;
private final HashMap<Double, AtomicReference<ShouldOverrideCallbackState>> shouldOverrideLocks = new HashMap<>();
public synchronized Pair<Double, AtomicReference<ShouldOverrideCallbackState>> getNewLock() {
final double lockIdentifier = nextLockIdentifier++;
final AtomicReference<ShouldOverrideCallbackState> shouldOverride = new AtomicReference<>(ShouldOverrideCallbackState.UNDECIDED);
shouldOverrideLocks.put(lockIdentifier, shouldOverride);
return new Pair<>(lockIdentifier, shouldOverride);
}
@Nullable
public synchronized AtomicReference<ShouldOverrideCallbackState> getLock(Double lockIdentifier) {
return shouldOverrideLocks.get(lockIdentifier);
}
public synchronized void removeLock(Double lockIdentifier) {
shouldOverrideLocks.remove(lockIdentifier);
}
}
protected static final ShouldOverrideUrlLoadingLock shouldOverrideUrlLoadingLock = new ShouldOverrideUrlLoadingLock();
private enum MimeType {
DEFAULT("*/*"),
IMAGE("image"),
VIDEO("video");
private final String value;
MimeType(String value) {
this.value = value;
}
}
private PermissionListener getWebviewFileDownloaderPermissionListener(String downloadingMessage, String lackPermissionToDownloadMessage) {
return new PermissionListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case FILE_DOWNLOAD_PERMISSION_REQUEST: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (mDownloadRequest != null) {
downloadFile(downloadingMessage);
}
} else {
Toast.makeText(mContext, lackPermissionToDownloadMessage, Toast.LENGTH_LONG).show();
}
return true;
}
}
return false;
}
};
}
public boolean isFileUploadSupported() {
return true;
}
public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockIdentifier) {
final AtomicReference<ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState> lockObject = shouldOverrideUrlLoadingLock.getLock(lockIdentifier);
if (lockObject != null) {
synchronized (lockObject) {
lockObject.set(shouldStart ? ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.DO_NOT_OVERRIDE : ShouldOverrideUrlLoadingLock.ShouldOverrideCallbackState.SHOULD_OVERRIDE);
lockObject.notify();
}
}
}
public Uri[] getSelectedFiles(Intent data, int resultCode) {
if (data == null) {
return null;
}
// we have multiple files selected
if (data.getClipData() != null) {
final int numSelectedFiles = data.getClipData().getItemCount();
Uri[] result = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
result[i] = data.getClipData().getItemAt(i).getUri();
}
return result;
}
// we have one file selected
if (data.getData() != null && resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
}
return null;
}
public void startPhotoPickerIntent(String acceptType, ValueCallback<Uri> callback) {
mFilePathCallbackLegacy = callback;
Activity activity = mContext.getCurrentActivity();
Intent fileChooserIntent = getFileChooserIntent(acceptType);
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
ArrayList<Parcelable> extraIntents = new ArrayList<>();
if (acceptsImages(acceptType)) {
Intent photoIntent = getPhotoIntent();
if (photoIntent != null) {
extraIntents.add(photoIntent);
}
}
if (acceptsVideo(acceptType)) {
Intent videoIntent = getVideoIntent();
if (videoIntent != null) {
extraIntents.add(videoIntent);
}
}
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) {
activity.startActivityForResult(chooserIntent, PICKER_LEGACY);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
}
public boolean startPhotoPickerIntent(final String[] acceptTypes, final boolean allowMultiple, final ValueCallback<Uri[]> callback, final boolean isCaptureEnabled) {
mFilePathCallback = callback;
Activity activity = mContext.getCurrentActivity();
ArrayList<Parcelable> extraIntents = new ArrayList<>();
Intent photoIntent = null;
if (!needsCameraPermission()) {
if (acceptsImages(acceptTypes)) {
photoIntent = getPhotoIntent();
if (photoIntent != null) {
extraIntents.add(photoIntent);
}
}
if (acceptsVideo(acceptTypes)) {
Intent videoIntent = getVideoIntent();
if (videoIntent != null) {
extraIntents.add(videoIntent);
}
}
}
Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
if (isCaptureEnabled) {
chooserIntent = photoIntent;
} else {
Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
}
if (chooserIntent != null) {
if (chooserIntent.resolveActivity(activity.getPackageManager()) != null) {
activity.startActivityForResult(chooserIntent, PICKER);
} else {
Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
}
} else {
Log.w("RNCWebViewModule", "there is no Camera permission");
}
return true;
}
public void setDownloadRequest(DownloadManager.Request request) {
mDownloadRequest = request;
}
public void downloadFile(String downloadingMessage) {
DownloadManager dm = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
try {
dm.enqueue(mDownloadRequest);
} catch (IllegalArgumentException | SecurityException e) {
Log.w("RNCWebViewModule", "Unsupported URI, aborting download", e);
return;
}
Toast.makeText(mContext, downloadingMessage, Toast.LENGTH_LONG).show();
}
public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) {
Activity activity = mContext.getCurrentActivity();
// Permission not required for Android Q and above
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
return true;
}
boolean result = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
if (!result && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PermissionAwareActivity PAactivity = getPermissionAwareActivity();
PAactivity.requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, FILE_DOWNLOAD_PERMISSION_REQUEST, getWebviewFileDownloaderPermissionListener(downloadingMessage, lackPermissionToDownloadMessage));
}
return result;
}
protected boolean needsCameraPermission() {
Activity activity = mContext.getCurrentActivity();
boolean needed = false;
PackageManager packageManager = activity.getPackageManager();
try {
String[] requestedPermissions = packageManager.getPackageInfo(activity.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions;
if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA)
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
needed = true;
}
} catch (PackageManager.NameNotFoundException e) {
needed = true;
}
return needed;
}
public Intent getPhotoIntent() {
Intent intent = null;
try {
mOutputImage = getCapturedFile(MimeType.IMAGE);
Uri outputImageUri = getOutputUri(mOutputImage);
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputImageUri);
} catch (IOException | IllegalArgumentException e) {
Log.e("CREATE FILE", "Error occurred while creating the File", e);
e.printStackTrace();
}
return intent;
}
public Intent getVideoIntent() {
Intent intent = null;
try {
mOutputVideo = getCapturedFile(MimeType.VIDEO);
Uri outputVideoUri = getOutputUri(mOutputVideo);
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
} catch (IOException | IllegalArgumentException e) {
Log.e("CREATE FILE", "Error occurred while creating the File", e);
e.printStackTrace();
}
return intent;
}
private Intent getFileChooserIntent(String acceptTypes) {
String _acceptTypes = acceptTypes;
if (acceptTypes.isEmpty()) {
_acceptTypes = MimeType.DEFAULT.value;
}
if (acceptTypes.matches("\\.\\w+")) {
_acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", ""));
}
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(_acceptTypes);
return intent;
}
private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType(MimeType.DEFAULT.value);
intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
return intent;
}
private Boolean acceptsImages(String types) {
String mimeType = types;
if (types.matches("\\.\\w+")) {
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
}
return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value);
}
private Boolean acceptsImages(String[] types) {
String[] mimeTypes = getAcceptedMimeType(types);
return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.IMAGE.value);
}
private Boolean acceptsVideo(String types) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
String mimeType = types;
if (types.matches("\\.\\w+")) {
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
}
return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.VIDEO.value);
}
private Boolean acceptsVideo(String[] types) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false;
}
String[] mimeTypes = getAcceptedMimeType(types);
return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value);
}
private Boolean arrayContainsString(String[] array, String pattern) {
for (String content : array) {
if (content.contains(pattern)) {
return true;
}
}
return false;
}
private String[] getAcceptedMimeType(String[] types) {
if (noAcceptTypesSet(types)) {
return new String[]{MimeType.DEFAULT.value};
}
String[] mimeTypes = new String[types.length];
for (int i = 0; i < types.length; i++) {
String t = types[i];
// convert file extensions to mime types
if (t.matches("\\.\\w+")) {
String mimeType = getMimeTypeFromExtension(t.replace(".", ""));
if(mimeType != null) {
mimeTypes[i] = mimeType;
} else {
mimeTypes[i] = t;
}
} else {
mimeTypes[i] = t;
}
}
return mimeTypes;
}
private String getMimeTypeFromExtension(String extension) {
String type = null;
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}
public Uri getOutputUri(File capturedFile) {
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return Uri.fromFile(capturedFile);
}
// for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions
String packageName = mContext.getPackageName();
return FileProvider.getUriForFile(mContext, packageName + ".fileprovider", capturedFile);
}
public File getCapturedFile(MimeType mimeType) throws IOException {
String prefix = "";
String suffix = "";
String dir = "";
switch (mimeType) {
case IMAGE:
prefix = "image-";
suffix = ".jpg";
dir = Environment.DIRECTORY_PICTURES;
break;
case VIDEO:
prefix = "video-";
suffix = ".mp4";
dir = Environment.DIRECTORY_MOVIES;
break;
default:
break;
}
String filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
File outputFile = null;
// for versions below 6.0 (23) we use the old File creation & permissions model
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// only this Directory works on all tested Android versions
// ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21)
File storageDir = Environment.getExternalStoragePublicDirectory(dir);
outputFile = new File(storageDir, filename);
} else {
File storageDir = mContext.getExternalFilesDir(null);
outputFile = File.createTempFile(prefix, suffix, storageDir);
}
return outputFile;
}
private Boolean noAcceptTypesSet(String[] types) {
// when our array returned from getAcceptTypes() has no values set from the webview
// i.e. <input type="file" />, without any "accept" attr
// will be an array with one empty string element, afaik
return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0);
}
private PermissionAwareActivity getPermissionAwareActivity() {
Activity activity = mContext.getCurrentActivity();
if (activity == null) {
throw new IllegalStateException("Tried to use permissions API while not attached to an Activity.");
} else if (!(activity instanceof PermissionAwareActivity)) {
throw new IllegalStateException("Tried to use permissions API but the host Activity doesn't implement PermissionAwareActivity.");
}
return (PermissionAwareActivity) activity;
}
}
@@ -0,0 +1,56 @@
package com.reactnativecommunity.webview;
import androidx.annotation.Nullable;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RNCWebViewPackage extends TurboReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new RNCWebViewManager());
return viewManagers;
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return () -> {
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
moduleInfos.put(
RNCWebViewModuleImpl.NAME,
new ReactModuleInfo(
RNCWebViewModuleImpl.NAME,
RNCWebViewModuleImpl.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
isTurboModule // isTurboModule
));
return moduleInfos;
};
}
@Nullable
@Override
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(RNCWebViewModuleImpl.NAME)) {
return new RNCWebViewModule(reactContext);
} else {
return null;
}
}
}
@@ -0,0 +1,39 @@
package com.reactnativecommunity.webview
import android.content.Context
import android.graphics.Color
import android.view.View
import android.webkit.WebView
import android.widget.FrameLayout
/**
* A [FrameLayout] container to hold the [RNCWebView].
* We need this to prevent WebView crash when the WebView is out of viewport and
* [com.facebook.react.views.view.ReactViewGroup] clips the canvas.
* The WebView will then create an empty offscreen surface and NPE.
*/
class RNCWebViewWrapper(context: Context, webView: RNCWebView) : FrameLayout(context) {
init {
// We make the WebView as transparent on top of the container,
// and let React Native sets background color for the container.
webView.setBackgroundColor(Color.TRANSPARENT)
addView(webView)
}
val webView: RNCWebView = getChildAt(0) as RNCWebView
companion object {
/**
* A helper to get react tag id by given WebView
*/
@JvmStatic
fun getReactTagFromWebView(webView: WebView): Int {
// It is expected that the webView is enclosed by [RNCWebViewWrapper] as the first child.
// Therefore, it must have a parent, and the parent ID is the reactTag.
// In exceptional cases, such as receiving WebView messaging after the view has been unmounted,
// the WebView will not have a parent.
// In this case, we simply return -1 to indicate that it was not found.
return (webView.parent as? View)?.id ?: -1
}
}
}
@@ -0,0 +1,179 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* The source code is obtained from the Android SDK Sources (API level 31),
* and modified by UNIDY2002 <UNIDY2002@outlook.com>.
*
* Change list:
* - Remove all unused class members except guessFileName,
* CONTENT_DISPOSITION_PATTERN and parseContentDisposition
* - Improve CONTENT_DISPOSITION_PATTERN and parseContentDisposition to add
* support for the "filename*" parameter in content disposition
*/
package com.reactnativecommunity.webview;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class URLUtil {
/**
* Guesses canonical filename that a download would have, using
* the URL and contentDisposition. File extension, if not defined,
* is added based on the mimetype
* @param url Url to the content
* @param contentDisposition Content-Disposition HTTP header or {@code null}
* @param mimeType Mime-type of the content or {@code null}
*
* @return suggested filename
*/
public static final String guessFileName(
String url,
@Nullable String contentDisposition,
@Nullable String mimeType) {
String filename = null;
String extension = null;
// If we couldn't do anything with the hint, move toward the content disposition
if (filename == null && contentDisposition != null) {
filename = parseContentDisposition(contentDisposition);
if (filename != null) {
int index = filename.lastIndexOf('/') + 1;
if (index > 0) {
filename = filename.substring(index);
}
}
}
// If all the other http-related approaches failed, use the plain uri
if (filename == null) {
String decodedUrl = Uri.decode(url);
if (decodedUrl != null) {
int queryIndex = decodedUrl.indexOf('?');
// If there is a query string strip it, same as desktop browsers
if (queryIndex > 0) {
decodedUrl = decodedUrl.substring(0, queryIndex);
}
if (!decodedUrl.endsWith("/")) {
int index = decodedUrl.lastIndexOf('/') + 1;
if (index > 0) {
filename = decodedUrl.substring(index);
}
}
}
}
// Finally, if couldn't get filename from URI, get a generic filename
if (filename == null) {
filename = "downloadfile";
}
// Split filename between base and extension
// Add an extension if filename does not have one
int dotIndex = filename.indexOf('.');
if (dotIndex < 0) {
if (mimeType != null) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (extension != null) {
extension = "." + extension;
}
}
if (extension == null) {
if (mimeType != null && mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) {
if (mimeType.equalsIgnoreCase("text/html")) {
extension = ".html";
} else {
extension = ".txt";
}
} else {
extension = ".bin";
}
}
} else {
if (mimeType != null) {
// Compare the last segment of the extension against the mime type.
// If there's a mismatch, discard the entire extension.
int lastDotIndex = filename.lastIndexOf('.');
String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
filename.substring(lastDotIndex + 1));
if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if (extension != null) {
extension = "." + extension;
}
}
}
if (extension == null) {
extension = filename.substring(dotIndex);
}
filename = filename.substring(0, dotIndex);
}
return filename + extension;
}
/** Regex used to parse content-disposition headers */
private static final Pattern CONTENT_DISPOSITION_PATTERN =
Pattern.compile("attachment(?:;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1)?(?:;\\s*filename\\s*\\*\\s*=\\s*([^']*)'[^']*'([^']*))?\\s*$",
Pattern.CASE_INSENSITIVE);
/**
* Parse the Content-Disposition HTTP Header. The format of the header
* is defined here: <a href="https://www.rfc-editor.org/rfc/rfc6266">RFC 6266</a>
* This header provides a filename for content that is going to be
* downloaded to the file system. We only support the attachment type.
*/
static String parseContentDisposition(String contentDisposition) {
try {
// The regex attempts to match the following pattern:
// attachment; filename="(Group 2)"; filename*=(Group 3)'(lang)'(Group 4)
// Group 4 refers to the percent-encoded filename, and the charset
// is specified in Group 3.
// Group 2 is the fallback filename.
// Group 1 refers to the quotation marks around Group 2.
//
// Test cases can be found at http://test.greenbytes.de/tech/tc2231/
// Examples can be found at https://www.rfc-editor.org/rfc/rfc6266#section-5
// There are a few known limitations:
// - any Content Disposition value that does not have parameters
// arranged in the order of "attachment...filename...filename*"
// or contains extra parameters shall fail to be parsed
// - any filename that contains " shall fail to be parsed
Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
if (m.find()) {
if (m.group(3) != null && m.group(4) != null) {
try {
return URLDecoder.decode(m.group(4), m.group(3).isEmpty() ? "UTF-8" : m.group(3));
} catch (UnsupportedEncodingException e) {
// Skip the ext-parameter as the encoding is unsupported
}
}
return m.group(2);
}
} catch (IllegalStateException ex) {
// This function is defined as returning null when it can't parse the header
}
return null;
}
}
@@ -0,0 +1,24 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when there is a loading progress event.
*/
class TopCustomMenuSelectionEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopCustomMenuSelectionEvent>(viewId) {
companion object {
const val EVENT_NAME = "topCustomMenuSelection"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when a http error is received from the server.
*/
class TopHttpErrorEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopHttpErrorEvent>(viewId) {
companion object {
const val EVENT_NAME = "topHttpError"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when there is an error in loading.
*/
class TopLoadingErrorEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingErrorEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingError"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,24 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when loading is completed.
*/
class TopLoadingFinishEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingFinishEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingFinish"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,24 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when there is a loading progress event.
*/
class TopLoadingProgressEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingProgressEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingProgress"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when loading has started
*/
class TopLoadingStartEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopLoadingStartEvent>(viewId) {
companion object {
const val EVENT_NAME = "topLoadingStart"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,24 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when there is an error in loading.
*/
class TopMessageEvent(viewId: Int, private val mEventData: WritableMap) : Event<TopMessageEvent>(viewId) {
companion object {
const val EVENT_NAME = "topMessage"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) {
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, mEventData)
}
}
@@ -0,0 +1,25 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when the WebView opens a new Window (i.e: target=_blank)
*/
class TopOpenWindowEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopOpenWindowEvent>(viewId) {
companion object {
const val EVENT_NAME = "topOpenWindow"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,26 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when the WebView's process has crashed or
was killed by the OS.
*/
class TopRenderProcessGoneEvent(viewId: Int, private val mEventData: WritableMap) :
Event<TopRenderProcessGoneEvent>(viewId) {
companion object {
const val EVENT_NAME = "topRenderProcessGone"
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)
}
@@ -0,0 +1,29 @@
package com.reactnativecommunity.webview.events
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.events.RCTEventEmitter
/**
* Event emitted when shouldOverrideUrlLoading is called
*/
class TopShouldStartLoadWithRequestEvent(viewId: Int, private val mData: WritableMap) : Event<TopShouldStartLoadWithRequestEvent>(viewId) {
companion object {
const val EVENT_NAME = "topShouldStartLoadWithRequest"
}
init {
mData.putString("navigationType", "other")
// Android does not raise shouldOverrideUrlLoading for inner frames
mData.putBoolean("isTopFrame", true)
}
override fun getEventName(): String = EVENT_NAME
override fun canCoalesce(): Boolean = false
override fun getCoalescingKey(): Short = 0
override fun dispatch(rctEventEmitter: RCTEventEmitter) =
rctEventEmitter.receiveEvent(viewTag, EVENT_NAME, mData)
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="shared"
path="." />
</paths>
@@ -0,0 +1,562 @@
package com.reactnativecommunity.webview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.RNCWebViewManagerDelegate;
import com.facebook.react.viewmanagers.RNCWebViewManagerInterface;
import com.facebook.react.views.scroll.ScrollEventType;
import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent;
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
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.TopOpenWindowEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import android.webkit.WebChromeClient;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Map;
@ReactModule(name = RNCWebViewManagerImpl.NAME)
public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper>
implements RNCWebViewManagerInterface<RNCWebViewWrapper> {
private final ViewManagerDelegate<RNCWebViewWrapper> mDelegate;
private final RNCWebViewManagerImpl mRNCWebViewManagerImpl;
public RNCWebViewManager() {
mDelegate = new RNCWebViewManagerDelegate<>(this);
mRNCWebViewManagerImpl = new RNCWebViewManagerImpl(true);
}
@Nullable
@Override
protected ViewManagerDelegate<RNCWebViewWrapper> getDelegate() {
return mDelegate;
}
@NonNull
@Override
public String getName() {
return RNCWebViewManagerImpl.NAME;
}
@NonNull
@Override
protected RNCWebViewWrapper createViewInstance(@NonNull ThemedReactContext context) {
return mRNCWebViewManagerImpl.createViewInstance(context);
}
@Override
@ReactProp(name = "allowFileAccess")
public void setAllowFileAccess(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowFileAccess(view, value);
}
@Override
@ReactProp(name = "allowFileAccessFromFileURLs")
public void setAllowFileAccessFromFileURLs(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowFileAccessFromFileURLs(view, value);
}
@Override
@ReactProp(name = "allowUniversalAccessFromFileURLs")
public void setAllowUniversalAccessFromFileURLs(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowUniversalAccessFromFileURLs(view, value);
}
@Override
@ReactProp(name = "allowsFullscreenVideo")
public void setAllowsFullscreenVideo(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsFullscreenVideo(view, value);
}
@Override
@ReactProp(name = "allowsProtectedMedia")
public void setAllowsProtectedMedia(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsProtectedMedia(view, value);
}
@Override
@ReactProp(name = "androidLayerType")
public void setAndroidLayerType(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setAndroidLayerType(view, value);
}
@Override
@ReactProp(name = "applicationNameForUserAgent")
public void setApplicationNameForUserAgent(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setApplicationNameForUserAgent(view, value);
}
@Override
@ReactProp(name = "basicAuthCredential")
public void setBasicAuthCredential(RNCWebViewWrapper view, @Nullable ReadableMap value) {
mRNCWebViewManagerImpl.setBasicAuthCredential(view, value);
}
@Override
@ReactProp(name = "cacheEnabled")
public void setCacheEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setCacheEnabled(view, value);
}
@Override
@ReactProp(name = "cacheMode")
public void setCacheMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setCacheMode(view, value);
}
@Override
@ReactProp(name = "domStorageEnabled")
public void setDomStorageEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setDomStorageEnabled(view, value);
}
@Override
@ReactProp(name = "downloadingMessage")
public void setDownloadingMessage(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setDownloadingMessage(value);
}
@Override
@ReactProp(name = "forceDarkOn")
public void setForceDarkOn(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setForceDarkOn(view, value);
}
@Override
@ReactProp(name = "geolocationEnabled")
public void setGeolocationEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setGeolocationEnabled(view, value);
}
@Override
@ReactProp(name = "hasOnScroll")
public void setHasOnScroll(RNCWebViewWrapper view, boolean hasScrollEvent) {
mRNCWebViewManagerImpl.setHasOnScroll(view, hasScrollEvent);
}
@Override
@ReactProp(name = "incognito")
public void setIncognito(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setIncognito(view, value);
}
@Override
@ReactProp(name = "injectedJavaScript")
public void setInjectedJavaScript(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScript(view, value);
}
@Override
@ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
public void setInjectedJavaScriptBeforeContentLoaded(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoaded(view, value);
}
@Override
@ReactProp(name = "injectedJavaScriptForMainFrameOnly")
public void setInjectedJavaScriptForMainFrameOnly(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptForMainFrameOnly(view, value);
}
@Override
@ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly")
public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(view, value);
}
@ReactProp(name = "injectedJavaScriptObject")
public void setInjectedJavaScriptObject(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptObject(view, value);
}
@Override
@ReactProp(name = "javaScriptCanOpenWindowsAutomatically")
public void setJavaScriptCanOpenWindowsAutomatically(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setJavaScriptCanOpenWindowsAutomatically(view, value);
}
@ReactProp(name = "javaScriptEnabled")
public void setJavaScriptEnabled(RNCWebViewWrapper view, boolean enabled) {
mRNCWebViewManagerImpl.setJavaScriptEnabled(view, enabled);
}
@Override
@ReactProp(name = "lackPermissionToDownloadMessage")
public void setLackPermissionToDownloadMessage(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setLackPermissionToDownloadMessage(value);
}
@Override
@ReactProp(name = "hasOnOpenWindowEvent")
public void setHasOnOpenWindowEvent(RNCWebViewWrapper view, boolean hasEvent) {
mRNCWebViewManagerImpl.setHasOnOpenWindowEvent(view, hasEvent);
}
@Override
@ReactProp(name = "mediaPlaybackRequiresUserAction")
public void setMediaPlaybackRequiresUserAction(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setMediaPlaybackRequiresUserAction(view, value);
}
@Override
@ReactProp(name = "menuItems")
public void setMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {
mRNCWebViewManagerImpl.setMenuCustomItems(view, items);
}
@Override
@ReactProp(name = "suppressMenuItems")
public void setSuppressMenuItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {}
@Override
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setMessagingEnabled(view, value);
}
@Override
@ReactProp(name = "messagingModuleName")
public void setMessagingModuleName(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMessagingModuleName(view, value);
}
@Override
@ReactProp(name = "minimumFontSize")
public void setMinimumFontSize(RNCWebViewWrapper view, int value) {
mRNCWebViewManagerImpl.setMinimumFontSize(view, value);
}
@Override
@ReactProp(name = "mixedContentMode")
public void setMixedContentMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMixedContentMode(view, value);
}
@Override
@ReactProp(name = "nestedScrollEnabled")
public void setNestedScrollEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setNestedScrollEnabled(view, value);
}
@Override
@ReactProp(name = "overScrollMode")
public void setOverScrollMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setOverScrollMode(view, value);
}
@Override
@ReactProp(name = "saveFormDataDisabled")
public void setSaveFormDataDisabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSaveFormDataDisabled(view, value);
}
@Override
@ReactProp(name = "scalesPageToFit")
public void setScalesPageToFit(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setScalesPageToFit(view, value);
}
@Override
@ReactProp(name = "setBuiltInZoomControls")
public void setSetBuiltInZoomControls(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetBuiltInZoomControls(view, value);
}
@Override
@ReactProp(name = "setDisplayZoomControls")
public void setSetDisplayZoomControls(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetDisplayZoomControls(view, value);
}
@Override
@ReactProp(name = "setSupportMultipleWindows")
public void setSetSupportMultipleWindows(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetSupportMultipleWindows(view, value);
}
@Override
@ReactProp(name = "showsHorizontalScrollIndicator")
public void setShowsHorizontalScrollIndicator(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setShowsHorizontalScrollIndicator(view, value);
}
@Override
@ReactProp(name = "showsVerticalScrollIndicator")
public void setShowsVerticalScrollIndicator(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setShowsVerticalScrollIndicator(view, value);
}
@Override
@ReactProp(name = "newSource")
public void setNewSource(RNCWebViewWrapper view, @Nullable ReadableMap value) {
mRNCWebViewManagerImpl.setSource(view, value);
}
@Override
@ReactProp(name = "textZoom")
public void setTextZoom(RNCWebViewWrapper view, int value) {
mRNCWebViewManagerImpl.setTextZoom(view, value);
}
@Override
@ReactProp(name = "thirdPartyCookiesEnabled")
public void setThirdPartyCookiesEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setThirdPartyCookiesEnabled(view, value);
}
@Override
@ReactProp(name = "webviewDebuggingEnabled")
public void setWebviewDebuggingEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value);
}
@Override
@ReactProp(name = "paymentRequestEnabled")
public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
}
/* iOS PROPS - no implemented here */
@Override
public void setAllowingReadAccessToURL(RNCWebViewWrapper view, @Nullable String value) {}
@Override
public void setAllowsBackForwardNavigationGestures(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAllowsInlineMediaPlayback(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAllowsPictureInPictureMediaPlayback(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAllowsAirPlayForMediaPlayback(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAllowsLinkPreview(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAutomaticallyAdjustContentInsets(RNCWebViewWrapper view, boolean value) {}
@Override
public void setAutoManageStatusBarEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setBounces(RNCWebViewWrapper view, boolean value) {}
@Override
public void setContentInset(RNCWebViewWrapper view, @Nullable ReadableMap value) {}
@Override
public void setContentInsetAdjustmentBehavior(RNCWebViewWrapper view, @Nullable String value) {}
@Override
public void setContentMode(RNCWebViewWrapper view, @Nullable String value) {}
@Override
public void setDataDetectorTypes(RNCWebViewWrapper view, @Nullable ReadableArray value) {}
@Override
public void setDecelerationRate(RNCWebViewWrapper view, double value) {}
@Override
public void setDirectionalLockEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setEnableApplePay(RNCWebViewWrapper view, boolean value) {}
@Override
public void setHideKeyboardAccessoryView(RNCWebViewWrapper view, boolean value) {}
@Override
public void setKeyboardDisplayRequiresUserAction(RNCWebViewWrapper view, boolean value) {}
@Override
public void setPagingEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setPullToRefreshEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setRefreshControlLightMode(RNCWebViewWrapper view, boolean value) {}
@Override
public void setIndicatorStyle(RNCWebViewWrapper view, @Nullable String value) {}
@Override
public void setScrollEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setSharedCookiesEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setUseSharedProcessPool(RNCWebViewWrapper view, boolean value) {}
@Override
public void setLimitsNavigationsToAppBoundDomains(RNCWebViewWrapper view, boolean value) {}
@Override
public void setTextInteractionEnabled(RNCWebViewWrapper view, boolean value) {}
@Override
public void setHasOnFileDownload(RNCWebViewWrapper view, boolean value) {}
@Override
public void setMediaCapturePermissionGrantType(RNCWebViewWrapper view, @Nullable String value) {}
@Override
public void setFraudulentWebsiteWarningEnabled(RNCWebViewWrapper view, boolean value) {}
/* !iOS PROPS - no implemented here */
@Override
@ReactProp(name = "userAgent")
public void setUserAgent(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setUserAgent(view, value);
}
@Override
public void goBack(RNCWebViewWrapper view) {
view.getWebView().goBack();
}
@Override
public void goForward(RNCWebViewWrapper view) {
view.getWebView().goForward();
}
@Override
public void reload(RNCWebViewWrapper view) {
view.getWebView().reload();
}
@Override
public void stopLoading(RNCWebViewWrapper view) {
view.getWebView().stopLoading();
}
@Override
public void injectJavaScript(RNCWebViewWrapper view, String javascript) {
view.getWebView().evaluateJavascriptWithFallback(javascript);
}
@Override
public void requestFocus(RNCWebViewWrapper view) {
view.requestFocus();
}
@Override
public void postMessage(RNCWebViewWrapper view, String data) {
try {
JSONObject eventInitDict = new JSONObject();
eventInitDict.put("data", data);
view.getWebView().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);
}
}
@Override
public void loadUrl(RNCWebViewWrapper view, String url) {
view.getWebView().loadUrl(url);
}
@Override
public void clearFormData(RNCWebViewWrapper view) {
view.getWebView().clearFormData();
}
@Override
public void clearCache(RNCWebViewWrapper view, boolean includeDiskFiles) {
view.getWebView().clearCache(includeDiskFiles);
}
@Override
public void clearHistory(RNCWebViewWrapper view) {
view.getWebView().clearHistory();
}
@Override
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebViewWrapper view) {
// Do not register default touch emitter and let WebView implementation handle touches
view.getWebView().setWebViewClient(new RNCWebViewClient());
}
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
Map<String, Object> export = super.getExportedCustomDirectEventTypeConstants();
if (export == null) {
export = MapBuilder.newHashMap();
}
// Default events but adding them here explicitly for clarity
export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart"));
export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish"));
export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError"));
export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage"));
// !Default events but adding them here explicitly for clarity
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"));
export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
export.put(TopCustomMenuSelectionEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomMenuSelection"));
export.put(TopOpenWindowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOpenWindow"));
return export;
}
@Override
public @Nullable
Map<String, Integer> getCommandsMap() {
return mRNCWebViewManagerImpl.getCommandsMap();
}
@Override
public void receiveCommand(@NonNull RNCWebViewWrapper reactWebView, String commandId, @Nullable ReadableArray args) {
super.receiveCommand(reactWebView, commandId, args);
}
@Override
protected void onAfterUpdateTransaction(@NonNull RNCWebViewWrapper view) {
super.onAfterUpdateTransaction(view);
mRNCWebViewManagerImpl.onAfterUpdateTransaction(view);
}
@Override
public void onDropViewInstance(@NonNull RNCWebViewWrapper view) {
mRNCWebViewManagerImpl.onDropViewInstance(view);
super.onDropViewInstance(view);
}
}
@@ -0,0 +1,57 @@
package com.reactnativecommunity.webview;
import android.app.DownloadManager;
import android.net.Uri;
import android.webkit.ValueCallback;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
@ReactModule(name = RNCWebViewModuleImpl.NAME)
public class RNCWebViewModule extends NativeRNCWebViewModuleSpec {
final private RNCWebViewModuleImpl mRNCWebViewModuleImpl;
public RNCWebViewModule(ReactApplicationContext reactContext) {
super(reactContext);
mRNCWebViewModuleImpl = new RNCWebViewModuleImpl(reactContext);
}
@Override
public void isFileUploadSupported(final Promise promise) {
promise.resolve(mRNCWebViewModuleImpl.isFileUploadSupported());
}
@Override
public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockIdentifier) {
mRNCWebViewModuleImpl.shouldStartLoadWithLockIdentifier(shouldStart, lockIdentifier);
}
public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptType, filePathCallback);
}
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final String[] acceptTypes, final boolean allowMultiple, final boolean isCaptureEnabled) {
return mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptTypes, allowMultiple, callback, isCaptureEnabled);
}
public void setDownloadRequest(DownloadManager.Request request) {
mRNCWebViewModuleImpl.setDownloadRequest(request);
}
public void downloadFile(String downloadingMessage) {
mRNCWebViewModuleImpl.downloadFile(downloadingMessage);
}
public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) {
return mRNCWebViewModuleImpl.grantFileDownloaderPermissions(downloadingMessage, lackPermissionToDownloadMessage);
}
@NonNull
@Override
public String getName() {
return RNCWebViewModuleImpl.NAME;
}
}
@@ -0,0 +1,333 @@
package com.reactnativecommunity.webview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.scroll.ScrollEventType;
import com.reactnativecommunity.webview.events.TopCustomMenuSelectionEvent;
import com.reactnativecommunity.webview.events.TopHttpErrorEvent;
import com.reactnativecommunity.webview.events.TopLoadingErrorEvent;
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.TopOpenWindowEvent;
import com.reactnativecommunity.webview.events.TopRenderProcessGoneEvent;
import com.reactnativecommunity.webview.events.TopShouldStartLoadWithRequestEvent;
import java.util.Map;
public class RNCWebViewManager extends ViewGroupManager<RNCWebViewWrapper> {
private final RNCWebViewManagerImpl mRNCWebViewManagerImpl;
public RNCWebViewManager() {
mRNCWebViewManagerImpl = new RNCWebViewManagerImpl();
}
@Override
public String getName() {
return RNCWebViewManagerImpl.NAME;
}
@Override
public RNCWebViewWrapper createViewInstance(ThemedReactContext context) {
return mRNCWebViewManagerImpl.createViewInstance(context);
}
public RNCWebViewWrapper createViewInstance(ThemedReactContext context, RNCWebView view) {
return mRNCWebViewManagerImpl.createViewInstance(context, view);
}
@ReactProp(name = "allowFileAccess")
public void setAllowFileAccess(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowFileAccess(view, value);
}
@ReactProp(name = "allowFileAccessFromFileURLs")
public void setAllowFileAccessFromFileURLs(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowFileAccessFromFileURLs(view, value);
}
@ReactProp(name = "allowUniversalAccessFromFileURLs")
public void setAllowUniversalAccessFromFileURLs(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowUniversalAccessFromFileURLs(view, value);
}
@ReactProp(name = "allowsFullscreenVideo")
public void setAllowsFullscreenVideo(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsFullscreenVideo(view, value);
}
@ReactProp(name = "allowsProtectedMedia")
public void setAllowsProtectedMedia(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setAllowsProtectedMedia(view, value);
}
@ReactProp(name = "androidLayerType")
public void setAndroidLayerType(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setAndroidLayerType(view, value);
}
@ReactProp(name = "applicationNameForUserAgent")
public void setApplicationNameForUserAgent(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setApplicationNameForUserAgent(view, value);
}
@ReactProp(name = "basicAuthCredential")
public void setBasicAuthCredential(RNCWebViewWrapper view, @Nullable ReadableMap value) {
mRNCWebViewManagerImpl.setBasicAuthCredential(view, value);
}
@ReactProp(name = "cacheEnabled")
public void setCacheEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setCacheEnabled(view, value);
}
@ReactProp(name = "cacheMode")
public void setCacheMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setCacheMode(view, value);
}
@ReactProp(name = "domStorageEnabled")
public void setDomStorageEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setDomStorageEnabled(view, value);
}
@ReactProp(name = "downloadingMessage")
public void setDownloadingMessage(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setDownloadingMessage(value);
}
@ReactProp(name = "forceDarkOn")
public void setForceDarkOn(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setForceDarkOn(view, value);
}
@ReactProp(name = "geolocationEnabled")
public void setGeolocationEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setGeolocationEnabled(view, value);
}
@ReactProp(name = "hasOnScroll")
public void setHasOnScroll(RNCWebViewWrapper view, boolean hasScrollEvent) {
mRNCWebViewManagerImpl.setHasOnScroll(view, hasScrollEvent);
}
@ReactProp(name = "incognito")
public void setIncognito(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setIncognito(view, value);
}
@ReactProp(name = "injectedJavaScript")
public void setInjectedJavaScript(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScript(view, value);
}
@ReactProp(name = "injectedJavaScriptBeforeContentLoaded")
public void setInjectedJavaScriptBeforeContentLoaded(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoaded(view, value);
}
@ReactProp(name = "injectedJavaScriptForMainFrameOnly")
public void setInjectedJavaScriptForMainFrameOnly(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptForMainFrameOnly(view, value);
}
@ReactProp(name = "injectedJavaScriptBeforeContentLoadedForMainFrameOnly")
public void setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptBeforeContentLoadedForMainFrameOnly(view, value);
}
@ReactProp(name = "injectedJavaScriptObject")
public void setInjectedJavaScriptObject(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setInjectedJavaScriptObject(view, value);
}
@ReactProp(name = "javaScriptCanOpenWindowsAutomatically")
public void setJavaScriptCanOpenWindowsAutomatically(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setJavaScriptCanOpenWindowsAutomatically(view, value);
}
@ReactProp(name = "javaScriptEnabled")
public void setJavaScriptEnabled(RNCWebViewWrapper view, boolean enabled) {
mRNCWebViewManagerImpl.setJavaScriptEnabled(view, enabled);
}
@ReactProp(name = "lackPermissionToDownloadMessage")
public void setLackPermissionToDownloadMessage(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setLackPermissionToDownloadMessage(value);
}
@ReactProp(name = "hasOnOpenWindowEvent")
public void setHasOnOpenWindowEvent(RNCWebViewWrapper view, boolean hasEvent) {
mRNCWebViewManagerImpl.setHasOnOpenWindowEvent(view, hasEvent);
}
@ReactProp(name = "mediaPlaybackRequiresUserAction")
public void setMediaPlaybackRequiresUserAction(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setMediaPlaybackRequiresUserAction(view, value);
}
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setMessagingEnabled(view, value);
}
@ReactProp(name = "menuItems")
public void setMenuCustomItems(RNCWebViewWrapper view, @Nullable ReadableArray items) {
mRNCWebViewManagerImpl.setMenuCustomItems(view, items);
}
@ReactProp(name = "messagingModuleName")
public void setMessagingModuleName(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMessagingModuleName(view, value);
}
@ReactProp(name = "minimumFontSize")
public void setMinimumFontSize(RNCWebViewWrapper view, int value) {
mRNCWebViewManagerImpl.setMinimumFontSize(view, value);
}
@ReactProp(name = "mixedContentMode")
public void setMixedContentMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setMixedContentMode(view, value);
}
@ReactProp(name = "nestedScrollEnabled")
public void setNestedScrollEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setNestedScrollEnabled(view, value);
}
@ReactProp(name = "overScrollMode")
public void setOverScrollMode(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setOverScrollMode(view, value);
}
@ReactProp(name = "saveFormDataDisabled")
public void setSaveFormDataDisabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSaveFormDataDisabled(view, value);
}
@ReactProp(name = "scalesPageToFit")
public void setScalesPageToFit(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setScalesPageToFit(view, value);
}
@ReactProp(name = "setBuiltInZoomControls")
public void setSetBuiltInZoomControls(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetBuiltInZoomControls(view, value);
}
@ReactProp(name = "setDisplayZoomControls")
public void setSetDisplayZoomControls(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetDisplayZoomControls(view, value);
}
@ReactProp(name = "setSupportMultipleWindows")
public void setSetSupportMultipleWindows(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setSetSupportMultipleWindows(view, value);
}
@ReactProp(name = "showsHorizontalScrollIndicator")
public void setShowsHorizontalScrollIndicator(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setShowsHorizontalScrollIndicator(view, value);
}
@ReactProp(name = "showsVerticalScrollIndicator")
public void setShowsVerticalScrollIndicator(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setShowsVerticalScrollIndicator(view, value);
}
@ReactProp(name = "source")
public void setSource(RNCWebViewWrapper view, @Nullable ReadableMap value) {
mRNCWebViewManagerImpl.setSource(view, value);
}
@ReactProp(name = "textZoom")
public void setTextZoom(RNCWebViewWrapper view, int value) {
mRNCWebViewManagerImpl.setTextZoom(view, value);
}
@ReactProp(name = "thirdPartyCookiesEnabled")
public void setThirdPartyCookiesEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setThirdPartyCookiesEnabled(view, value);
}
@ReactProp(name = "webviewDebuggingEnabled")
public void setWebviewDebuggingEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setWebviewDebuggingEnabled(view, value);
}
@ReactProp(name = "userAgent")
public void setUserAgent(RNCWebViewWrapper view, @Nullable String value) {
mRNCWebViewManagerImpl.setUserAgent(view, value);
}
@ReactProp(name = "paymentRequestEnabled")
public void setPaymentRequestEnabled(RNCWebViewWrapper view, boolean value) {
mRNCWebViewManagerImpl.setPaymentRequestEnabled(view, value);
}
@Override
protected void addEventEmitters(@NonNull ThemedReactContext reactContext, RNCWebViewWrapper viewWrapper) {
// Do not register default touch emitter and let WebView implementation handle touches
viewWrapper.getWebView().setWebViewClient(new RNCWebViewClient());
}
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
Map<String, Object> export = super.getExportedCustomDirectEventTypeConstants();
if (export == null) {
export = MapBuilder.newHashMap();
}
// Default events but adding them here explicitly for clarity
export.put(TopLoadingStartEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingStart"));
export.put(TopLoadingFinishEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingFinish"));
export.put(TopLoadingErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingError"));
export.put(TopMessageEvent.EVENT_NAME, MapBuilder.of("registrationName", "onMessage"));
// !Default events but adding them here explicitly for clarity
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"));
export.put(TopRenderProcessGoneEvent.EVENT_NAME, MapBuilder.of("registrationName", "onRenderProcessGone"));
export.put(TopCustomMenuSelectionEvent.EVENT_NAME, MapBuilder.of("registrationName", "onCustomMenuSelection"));
export.put(TopOpenWindowEvent.EVENT_NAME, MapBuilder.of("registrationName", "onOpenWindow"));
return export;
}
@Override
public @Nullable
Map<String, Integer> getCommandsMap() {
return mRNCWebViewManagerImpl.getCommandsMap();
}
@Override
public void receiveCommand(@NonNull RNCWebViewWrapper reactWebView, String commandId, @Nullable ReadableArray args) {
mRNCWebViewManagerImpl.receiveCommand(reactWebView, commandId, args);
super.receiveCommand(reactWebView, commandId, args);
}
@Override
protected void onAfterUpdateTransaction(@NonNull RNCWebViewWrapper view) {
super.onAfterUpdateTransaction(view);
mRNCWebViewManagerImpl.onAfterUpdateTransaction(view);
}
@Override
public void onDropViewInstance(@NonNull RNCWebViewWrapper view) {
mRNCWebViewManagerImpl.onDropViewInstance(view);
super.onDropViewInstance(view);
}
}
@@ -0,0 +1,59 @@
package com.reactnativecommunity.webview;
import android.app.DownloadManager;
import android.net.Uri;
import androidx.annotation.NonNull;
import android.webkit.ValueCallback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
@ReactModule(name = RNCWebViewModuleImpl.NAME)
public class RNCWebViewModule extends ReactContextBaseJavaModule {
final private RNCWebViewModuleImpl mRNCWebViewModuleImpl;
public RNCWebViewModule(ReactApplicationContext reactContext) {
super(reactContext);
mRNCWebViewModuleImpl = new RNCWebViewModuleImpl(reactContext);
}
@ReactMethod
public void isFileUploadSupported(final Promise promise) {
promise.resolve(mRNCWebViewModuleImpl.isFileUploadSupported());
}
@ReactMethod
public void shouldStartLoadWithLockIdentifier(boolean shouldStart, double lockIdentifier) {
mRNCWebViewModuleImpl.shouldStartLoadWithLockIdentifier(shouldStart, lockIdentifier);
}
public void startPhotoPickerIntent(ValueCallback<Uri> filePathCallback, String acceptType) {
mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptType, filePathCallback);
}
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final String[] acceptTypes, final boolean allowMultiple, final boolean isCaptureEnabled) {
return mRNCWebViewModuleImpl.startPhotoPickerIntent(acceptTypes, allowMultiple, callback, isCaptureEnabled);
}
public void setDownloadRequest(DownloadManager.Request request) {
mRNCWebViewModuleImpl.setDownloadRequest(request);
}
public void downloadFile(String downloadingMessage) {
mRNCWebViewModuleImpl.downloadFile(downloadingMessage);
}
public boolean grantFileDownloaderPermissions(String downloadingMessage, String lackPermissionToDownloadMessage) {
return mRNCWebViewModuleImpl.grantFileDownloaderPermissions(downloadingMessage, lackPermissionToDownloadMessage);
}
@NonNull
@Override
public String getName() {
return RNCWebViewModuleImpl.NAME;
}
}