diff --git a/.gitignore b/.gitignore
index fd4f2b06..0ad831ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules
.DS_Store
+android/build
diff --git a/android/build.gradle b/android/build.gradle
index dfec22af..30036377 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -11,12 +11,12 @@ buildscript {
apply plugin: 'com.android.library'
android {
- compileSdkVersion 23
- buildToolsVersion "23.0.1"
+ compileSdkVersion 27
+ buildToolsVersion "27.0.3"
defaultConfig {
minSdkVersion 16
- targetSdkVersion 23
+ targetSdkVersion 27
versionCode 1
versionName "1.0"
}
@@ -30,5 +30,6 @@ repositories {
}
dependencies {
- compile 'com.facebook.react:react-native:0.19.+'
+ compile "com.android.support:appcompat-v7:27.0.3"
+ compile "com.facebook.react:react-native:+"
}
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 84054d58..005fb34a 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,4 +1,6 @@
-
+
+
+
diff --git a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/AndroidWebViewModule.java b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/AndroidWebViewModule.java
new file mode 100644
index 00000000..bcadafab
--- /dev/null
+++ b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/AndroidWebViewModule.java
@@ -0,0 +1,261 @@
+package com.github.alinz.reactnativewebviewbridge;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.DownloadManager;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.support.v4.content.ContextCompat;
+import android.util.Log;
+import android.webkit.ValueCallback;
+import android.widget.Toast;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ActivityEventListener;
+import com.facebook.react.common.annotations.VisibleForTesting;
+import com.facebook.react.modules.core.PermissionAwareActivity;
+import com.facebook.react.modules.core.PermissionListener;
+
+import android.webkit.JavascriptInterface;
+import android.webkit.WebView;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.ReactContext;
+import com.facebook.react.bridge.WritableMap;
+import com.facebook.react.modules.core.DeviceEventManagerModule;
+import com.facebook.react.uimanager.events.RCTEventEmitter;
+
+
+public class AndroidWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
+ private ValueCallback mUploadMessage;
+ private ValueCallback mUploadCallbackAboveL;
+ private DownloadManager.Request downloadRequest;
+ private static final int FILE_CHOOSER_PERMISSION_REQUEST = 1;
+ private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 2;
+
+ @VisibleForTesting
+ public static final String REACT_CLASS = "AndroidWebViewModule";
+
+ private WebView webView;
+ private ReactApplicationContext reactContext;
+
+ public void JavascriptBridge(WebView webView) {
+ this.webView = webView;
+ }
+
+ @JavascriptInterface
+ public void send(String message) {
+ WritableMap event = Arguments.createMap();
+ event.putString("message", message);
+ ReactContext reactContext = (ReactContext) this.webView.getContext();
+ reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(
+ this.webView.getId(),
+ "topChange",
+ event);
+ }
+
+ public AndroidWebViewModule(ReactApplicationContext context){
+ super(context);
+ context.addActivityEventListener(this);
+ }
+
+ private WebViewBridgePackage aPackage;
+
+ public void setPackage(WebViewBridgePackage aPackage) {
+ this.aPackage = aPackage;
+ }
+
+ public WebViewBridgePackage getPackage() {
+ return this.aPackage;
+ }
+
+ public void setContext( ReactApplicationContext context )
+ {
+ this.reactContext = context;
+ }
+
+ public ReactApplicationContext getContext()
+ {
+ return this.reactContext;
+ }
+
+ @Override
+ public String getName(){
+ return REACT_CLASS;
+ }
+
+ @SuppressWarnings("unused")
+ public Activity getActivity() {
+ return getCurrentActivity();
+ }
+
+ public void setUploadMessage(ValueCallback uploadMessage) {
+ mUploadMessage = uploadMessage;
+ }
+
+ public void setmUploadCallbackAboveL(ValueCallback mUploadCallbackAboveL) {
+ this.mUploadCallbackAboveL = mUploadCallbackAboveL;
+ }
+
+ public void setDownloadRequest(DownloadManager.Request request) {
+ this.downloadRequest = request;
+ }
+
+ @Override
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+ // super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == 1) {
+ if (null == mUploadMessage && null == mUploadCallbackAboveL){
+ return;
+ }
+ Uri result = data == null || resultCode != Activity.RESULT_OK ? null : data.getData();
+ if (mUploadCallbackAboveL != null) {
+ onActivityResultAboveL(requestCode, resultCode, data);
+ } else if (mUploadMessage != null) {
+ mUploadMessage.onReceiveValue(result);
+ mUploadMessage = null;
+ }
+ }
+ }
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
+ if (requestCode != 1 || mUploadCallbackAboveL == null) {
+ return;
+ }
+ Uri[] results = null;
+ if (resultCode == Activity.RESULT_OK) {
+ if (data != null) {
+ String dataString = data.getDataString();
+ ClipData clipData = data.getClipData();
+ if (clipData != null) {
+ results = new Uri[clipData.getItemCount()];
+ for (int i = 0; i < clipData.getItemCount(); i++) {
+ ClipData.Item item = clipData.getItemAt(i);
+ results[i] = item.getUri();
+ }
+ }
+ if (dataString != null)
+ results = new Uri[]{Uri.parse(dataString)};
+ }
+ }
+ mUploadCallbackAboveL.onReceiveValue(results);
+ mUploadCallbackAboveL = null;
+ }
+
+ public void openFileChooserView(){
+ try {
+ Intent openableFileIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ openableFileIntent.addCategory(Intent.CATEGORY_OPENABLE);
+ openableFileIntent.setType("*/*");
+
+ final Intent chooserIntent = Intent.createChooser(openableFileIntent, "Choose File");
+ getActivity().startActivityForResult(chooserIntent, 1);
+ } catch (Exception e) {
+ Log.d("customwebview", e.toString());
+ }
+ }
+
+ public void downloadFile() {
+ DownloadManager dm = (DownloadManager) getActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
+ String downloadMessage = "Downloading";
+
+ dm.enqueue(this.downloadRequest);
+
+ Toast.makeText(getActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
+ }
+
+ // NB: parts of the permission management are adapted, with significant modification, from
+ // https://lakshinkarunaratne.wordpress.com/2018/03/11/enhancing-the-react-native-webview-part-2-supporting-file-downloads-in-android/
+
+ private PermissionAwareActivity getPermissionAwareActivity() {
+ Activity activity = 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 PermissionListener webviewFileChooserPermissionListener = new PermissionListener() {
+ @Override
+ public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case FILE_CHOOSER_PERMISSION_REQUEST: {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ if (mUploadCallbackAboveL != null){
+ openFileChooserView();
+ }
+ } else {
+ Toast.makeText(getActivity().getApplicationContext(), "Cannot upload files as permission was denied. Please provide permission to access storage, in order to upload files.", Toast.LENGTH_LONG).show();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ private PermissionListener webviewFileDownloaderPermissionListener = 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(downloadRequest != null){
+ downloadFile();
+ }
+ } else {
+ Toast.makeText(getActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ public boolean grantFileChooserPermissions() {
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
+ return true;
+ }
+ boolean result = true;
+ if (ContextCompat.checkSelfPermission(this.getActivity(),Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ result = false;
+ }
+
+ if(!result){
+ PermissionAwareActivity activity = getPermissionAwareActivity();
+ activity.requestPermissions(new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE }, FILE_CHOOSER_PERMISSION_REQUEST, webviewFileChooserPermissionListener);
+ }
+ return result;
+ }
+
+ public boolean grantFileDownloaderPermissions() {
+ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
+ return true;
+ }
+ boolean result = true;
+ if (ContextCompat.checkSelfPermission(this.getActivity(),Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ result = false;
+ }
+
+ if(!result){
+ PermissionAwareActivity activity = getPermissionAwareActivity();
+ activity.requestPermissions(new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, FILE_DOWNLOAD_PERMISSION_REQUEST, webviewFileDownloaderPermissionListener);
+ }
+ return result;
+ }
+
+ public void onNewIntent(Intent intent) {}
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java
index c1250163..0e85de24 100644
--- a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java
+++ b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgeManager.java
@@ -6,17 +6,52 @@
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.webview.ReactWebViewManager;
import com.facebook.react.uimanager.annotations.ReactProp;
+import com.facebook.react.bridge.ReactApplicationContext;
import java.util.ArrayList;
import java.util.Map;
import javax.annotation.Nullable;
+
+import android.app.Activity;
+import android.app.DownloadManager;
+import android.content.Context;
+import android.os.Environment;
+import android.webkit.URLUtil;
+import android.widget.Toast;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.CookieManager;
+import android.webkit.DownloadListener;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import java.nio.charset.StandardCharsets;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+
+import com.github.alinz.reactnativewebviewbridge.AndroidWebViewModule;
+
public class WebViewBridgeManager extends ReactWebViewManager {
+ private Activity mActivity = null;
+ private WebViewBridgePackage aPackage;
+
private static final String REACT_CLASS = "RCTWebViewBridge";
public static final int COMMAND_SEND_TO_BRIDGE = 101;
+ public void setPackage(WebViewBridgePackage aPackage){
+ this.aPackage = aPackage;
+ }
+
+ public WebViewBridgePackage getPackage(){
+ return this.aPackage;
+ }
+
@Override
public String getName() {
return REACT_CLASS;
@@ -35,9 +70,105 @@ Map getCommandsMap() {
@Override
protected WebView createViewInstance(ThemedReactContext reactContext) {
- WebView root = super.createViewInstance(reactContext);
- root.addJavascriptInterface(new JavascriptBridge(root), "WebViewBridge");
- return root;
+ WebView view = super.createViewInstance(reactContext);
+ view.addJavascriptInterface(new JavascriptBridge(view), "WebViewBridge");
+
+ //Now do our own setWebChromeClient, patching in file chooser support
+ final AndroidWebViewModule module = this.aPackage.getModule();
+
+ view.setWebChromeClient(new WebChromeClient(){
+
+ public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
+ module.setUploadMessage(uploadMsg);
+ module.openFileChooserView();
+
+ }
+
+ public boolean onJsConfirm (WebView view, String url, String message, JsResult result){
+ return true;
+ }
+
+ public boolean onJsPrompt (WebView view, String url, String message, String defaultValue, JsPromptResult result){
+ return true;
+ }
+
+ // For Android < 3.0
+ public void openFileChooser(ValueCallback uploadMsg) {
+ module.setUploadMessage(uploadMsg);
+ module.openFileChooserView();
+ }
+
+ // For Android > 4.1.1
+ public void openFileChooser(ValueCallback uploadMsg, String acceptType, String capture) {
+ module.setUploadMessage(uploadMsg);
+ module.openFileChooserView();
+ }
+
+ // For Android > 5.0
+ public boolean onShowFileChooser (WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
+ Log.d("customwebview", "onShowFileChooser");
+
+ module.setmUploadCallbackAboveL(filePathCallback);
+ if (module.grantFileChooserPermissions()) {
+ module.openFileChooserView();
+ } else {
+ Toast.makeText(module.getActivity().getApplicationContext(), "Cannot upload files as permission was denied. Please provide permission to access storage, in order to upload files.", Toast.LENGTH_LONG).show();
+ }
+ return true;
+ }
+ });
+
+ view.setDownloadListener(new DownloadListener() {
+ public void onDownloadStart(String url, String userAgent,
+ String contentDisposition, String mimetype,
+ long contentLength) {
+
+ DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
+
+ //Try to extract filename from contentDisposition, otherwise guess using URLUtil
+ String fileName = "";
+ try {
+ fileName = contentDisposition.replaceFirst("(?i)^.*filename=\"?([^\"]+)\"?.*$", "$1");
+ fileName = URLDecoder.decode(fileName, "UTF-8");
+ } catch (Exception e) {
+ System.out.println("Error extracting filename from contentDisposition: " + e);
+ System.out.println("Falling back to URLUtil.guessFileName");
+ fileName = URLUtil.guessFileName(url,contentDisposition,mimetype);
+ }
+ String downloadMessage = "Downloading " + fileName;
+
+ //Attempt to add cookie, if it exists
+ URL urlObj = null;
+ try {
+ urlObj = new URL(url);
+ String baseUrl = urlObj.getProtocol() + "://" + urlObj.getHost();
+ String cookie = CookieManager.getInstance().getCookie(baseUrl);
+ request.addRequestHeader("Cookie", cookie);
+ System.out.println("Got cookie for DownloadManager: " + cookie);
+ } catch (MalformedURLException e) {
+ System.out.println("Error getting cookie for DownloadManager: " + e.toString());
+ e.printStackTrace();
+ }
+
+ //Finish setting up request
+ request.addRequestHeader("User-Agent", userAgent);
+ request.setTitle(fileName);
+ request.setDescription(downloadMessage);
+ request.allowScanningByMediaScanner();
+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+ request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
+
+ module.setDownloadRequest(request);
+
+ if (module.grantFileDownloaderPermissions()) {
+ module.downloadFile();
+ } else {
+ Toast.makeText(module.getActivity().getApplicationContext(), "Cannot download files as permission was denied. Please provide permission to write to storage, in order to download files.", Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+
+ return view;
}
@Override
diff --git a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgePackage.java b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgePackage.java
index 1e189c4d..27a61180 100644
--- a/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgePackage.java
+++ b/android/src/main/java/com/github/alinz/reactnativewebviewbridge/WebViewBridgePackage.java
@@ -9,21 +9,36 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Collections;
+
public class WebViewBridgePackage implements ReactPackage {
- @Override
- public List createNativeModules(ReactApplicationContext reactApplicationContext) {
- return new ArrayList<>();
+ private WebViewBridgeManager manager;
+ private AndroidWebViewModule module;
+
+ @Override public List createNativeModules( ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+ module = new AndroidWebViewModule(reactContext);
+ module.setPackage(this);
+ modules.add(module);
+ return modules;
+ }
+
+ public WebViewBridgeManager getManager(){
+ return manager;
+ }
+
+ public AndroidWebViewModule getModule(){
+ return module;
}
@Override
public List createViewManagers(ReactApplicationContext reactApplicationContext) {
- return Arrays.asList(
- new WebViewBridgeManager()
- );
+ manager = new WebViewBridgeManager();
+ manager.setPackage(this);
+ return Arrays.asList(manager);
}
- @Override
public List> createJSModules() {
return Arrays.asList();
}