From f7495a60b35154fdcfba5faa20d7075e70f4e3bd Mon Sep 17 00:00:00 2001 From: elhardoum Date: Fri, 1 Feb 2019 14:53:24 +0000 Subject: [PATCH 1/2] added file upload support from @Oblongmana / react-native-webview-file-upload-android --- .gitignore | 1 + android/build.gradle | 13 +- android/src/main/AndroidManifest.xml | 4 +- .../AndroidWebViewModule.java | 261 ++++++++++++++++++ .../WebViewBridgeManager.java | 137 ++++++++- .../WebViewBridgePackage.java | 29 +- 6 files changed, 428 insertions(+), 17 deletions(-) create mode 100644 android/src/main/java/com/github/alinz/reactnativewebviewbridge/AndroidWebViewModule.java 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..82fcc438 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -11,14 +11,14 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 23 - buildToolsVersion "23.0.1" + compileSdkVersion 27 + buildToolsVersion "27.0.3" defaultConfig { minSdkVersion 16 - targetSdkVersion 23 - versionCode 1 - versionName "1.0" + targetSdkVersion 27 + versionCode 3 + versionName "0.3.0" } lintOptions { abortOnError false @@ -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(); } From 29904f8f3a7a48db21859e8e21d48a3492c45ae5 Mon Sep 17 00:00:00 2001 From: elhardoum Date: Fri, 1 Feb 2019 15:00:53 +0000 Subject: [PATCH 2/2] maintain author build version --- android/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 82fcc438..30036377 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -17,8 +17,8 @@ android { defaultConfig { minSdkVersion 16 targetSdkVersion 27 - versionCode 3 - versionName "0.3.0" + versionCode 1 + versionName "1.0" } lintOptions { abortOnError false