diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 73590d2..2d72d6f 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1,13 @@
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java
new file mode 100644
index 0000000..0d7773d
--- /dev/null
+++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewFileProvider.java
@@ -0,0 +1,14 @@
+package com.reactnativecommunity.webview;
+
+import android.support.v4.content.FileProvider;
+
+/**
+ * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions.
+ *
+ * See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
+ */
+public class RNCWebViewFileProvider extends FileProvider {
+
+ // This class intentionally left blank.
+
+}
diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
index 3a7ae87..6e5cdd4 100644
--- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
+++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java
@@ -85,6 +85,7 @@ import org.json.JSONObject;
public class RNCWebViewManager extends SimpleViewManager {
protected static final String REACT_CLASS = "RNCWebView";
+ private RNCWebViewPackage aPackage;
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
@@ -427,6 +428,25 @@ public class RNCWebViewManager extends SimpleViewManager {
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
}
+
+ protected void openFileChooser(ValueCallback filePathCallback, String acceptType) {
+ getModule().startPhotoPickerIntent(filePathCallback, acceptType);
+ }
+ protected void openFileChooser(ValueCallback filePathCallback) {
+ getModule().startPhotoPickerIntent(filePathCallback, "");
+ }
+ protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) {
+ getModule().startPhotoPickerIntent(filePathCallback, acceptType);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) {
+ String[] acceptTypes = fileChooserParams.getAcceptTypes();
+ boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
+ Intent intent = fileChooserParams.createIntent();
+ return getModule().startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
+ }
});
reactContext.addLifecycleEventListener(webView);
mWebViewConfig.configWebView(webView);
@@ -747,4 +767,16 @@ public class RNCWebViewManager extends SimpleViewManager {
reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher();
eventDispatcher.dispatchEvent(event);
}
+
+ public RNCWebViewPackage getPackage() {
+ return this.aPackage;
+ }
+
+ public void setPackage(RNCWebViewPackage aPackage) {
+ this.aPackage = aPackage;
+ }
+
+ public RNCWebViewModule getModule() {
+ return this.aPackage.getModule();
+ }
}
diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java
index b93d992..2590d5b 100644
--- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java
+++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java
@@ -1,22 +1,309 @@
package com.reactnativecommunity.webview;
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.support.annotation.RequiresApi;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+
+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.bridge.Callback;
-public class RNCWebViewModule extends ReactContextBaseJavaModule {
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import static android.app.Activity.RESULT_OK;
+
+public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener {
private final ReactApplicationContext reactContext;
+ private RNCWebViewPackage aPackage;
+
+ private static final int PICKER = 1;
+ private static final int PICKER_LEGACY = 3;
+
+ private ValueCallback filePathCallbackLegacy;
+ private ValueCallback filePathCallback;
+ private Uri outputFileUri;
+
+ final String DEFAULT_MIME_TYPES = "*/*";
public RNCWebViewModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
+ reactContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "RNCWebView";
}
-}
\ No newline at end of file
+
+ @ReactMethod
+ public void isFileUploadSupported(final Promise promise) {
+ Boolean result = false;
+ int current = Build.VERSION.SDK_INT;
+ if (current >= Build.VERSION_CODES.LOLLIPOP) {
+ result = true;
+ }
+ if (current >= Build.VERSION_CODES.JELLY_BEAN && current <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ result = true;
+ }
+ promise.resolve(result);
+ }
+
+ public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
+
+ if (filePathCallback == null && filePathCallbackLegacy == null) {
+ return;
+ }
+
+ // 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 PICKER:
+ if (resultCode != RESULT_OK) {
+ if (filePathCallback != null) {
+ filePathCallback.onReceiveValue(null);
+ }
+ } else {
+ Uri result[] = this.getSelectedFiles(data, resultCode);
+ if (result != null) {
+ filePathCallback.onReceiveValue(result);
+ } else {
+ filePathCallback.onReceiveValue(new Uri[] { outputFileUri });
+ }
+ }
+ break;
+ case PICKER_LEGACY:
+ Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
+ filePathCallbackLegacy.onReceiveValue(result);
+ break;
+
+ }
+ filePathCallback = null;
+ filePathCallbackLegacy= null;
+ outputFileUri = null;
+ }
+
+ public void onNewIntent(Intent intent) {
+ }
+
+ private Uri[] getSelectedFiles(Intent data, int resultCode) {
+ if (data == null) {
+ return null;
+ }
+
+ // we have one file selected
+ if (data.getData() != null) {
+ if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return WebChromeClient.FileChooserParams.parseResult(resultCode, data);
+ } else {
+ 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;
+ }
+ return null;
+ }
+
+ public void startPhotoPickerIntent(ValueCallback filePathCallback, String acceptType) {
+ filePathCallbackLegacy = filePathCallback;
+
+ Intent fileChooserIntent = getFileChooserIntent(acceptType);
+ Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
+
+ ArrayList extraIntents = new ArrayList<>();
+ if (acceptsImages(acceptType)) {
+ extraIntents.add(getPhotoIntent());
+ }
+ if (acceptsVideo(acceptType)) {
+ extraIntents.add(getVideoIntent());
+ }
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
+
+ if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
+ getCurrentActivity().startActivityForResult(chooserIntent, PICKER_LEGACY);
+ } else {
+ Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ public boolean startPhotoPickerIntent(final ValueCallback callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
+ filePathCallback = callback;
+
+ ArrayList extraIntents = new ArrayList<>();
+ if (acceptsImages(acceptTypes)) {
+ extraIntents.add(getPhotoIntent());
+ }
+ if (acceptsVideo(acceptTypes)) {
+ extraIntents.add(getVideoIntent());
+ }
+
+ Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple);
+
+ Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
+ chooserIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent);
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
+
+ if (chooserIntent.resolveActivity(getCurrentActivity().getPackageManager()) != null) {
+ getCurrentActivity().startActivityForResult(chooserIntent, PICKER);
+ } else {
+ Log.w("RNCWebViewModule", "there is no Activity to handle this Intent");
+ }
+
+ return true;
+ }
+
+ public RNCWebViewPackage getPackage() {
+ return this.aPackage;
+ }
+
+ public void setPackage(RNCWebViewPackage aPackage) {
+ this.aPackage = aPackage;
+ }
+
+ private Intent getPhotoIntent() {
+ Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
+ return intent;
+ }
+
+ private Intent getVideoIntent() {
+ Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+ // @todo from experience, for Videos we get the data onActivityResult
+ // so there's no need to store the Uri
+ Uri outputVideoUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, outputVideoUri);
+ return intent;
+ }
+
+ private Intent getFileChooserIntent(String acceptTypes) {
+ String _acceptTypes = acceptTypes;
+ if (acceptTypes.isEmpty()) {
+ _acceptTypes = DEFAULT_MIME_TYPES;
+ }
+ 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("*/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
+ intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
+ return intent;
+ }
+
+ private Boolean acceptsImages(String types) {
+ return types.isEmpty() || types.toLowerCase().contains("image");
+ }
+ private Boolean acceptsImages(String[] types) {
+ return isArrayEmpty(types) || arrayContainsString(types, "image");
+ }
+
+ private Boolean acceptsVideo(String types) {
+ return types.isEmpty() || types.toLowerCase().contains("video");
+ }
+ private Boolean acceptsVideo(String[] types) {
+ return isArrayEmpty(types) || arrayContainsString(types, "video");
+ }
+
+ 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 (isArrayEmpty(types)) {
+ return new String[]{DEFAULT_MIME_TYPES};
+ }
+ return types;
+ }
+
+ private Uri getOutputUri(String intentType) {
+ File capturedFile = null;
+ try {
+ capturedFile = getCapturedFile(intentType);
+ } catch (IOException e) {
+ Log.e("CREATE FILE", "Error occurred while creating the File", e);
+ e.printStackTrace();
+ }
+
+ // 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 = getReactApplicationContext().getPackageName();
+ return FileProvider.getUriForFile(getReactApplicationContext(), packageName+".fileprovider", capturedFile);
+ }
+
+ private File getCapturedFile(String intentType) throws IOException {
+ String prefix = "";
+ String suffix = "";
+ String dir = "";
+ String filename = "";
+
+ if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
+ prefix = "image-";
+ suffix = ".jpg";
+ dir = Environment.DIRECTORY_PICTURES;
+ } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
+ prefix = "video-";
+ suffix = ".mp4";
+ dir = Environment.DIRECTORY_MOVIES;
+ }
+
+ filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
+
+ // 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);
+ return new File(storageDir, filename);
+ }
+
+ File storageDir = getReactApplicationContext().getExternalFilesDir(null);
+ return File.createTempFile(filename, suffix, storageDir);
+ }
+
+ private Boolean isArrayEmpty(String[] arr) {
+ // when our array returned from getAcceptTypes() has no values set from the webview
+ // i.e. , without any "accept" attr
+ // will be an array with one empty string element, afaik
+ return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0);
+ }
+}
diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java
index 60ee9f6..f8817c0 100644
--- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java
+++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewPackage.java
@@ -1,6 +1,7 @@
package com.reactnativecommunity.webview;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -10,10 +11,19 @@ import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.JavaScriptModule;
+
public class RNCWebViewPackage implements ReactPackage {
+
+ private RNCWebViewManager manager;
+ private RNCWebViewModule module;
+
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
- return Arrays.asList(new RNCWebViewModule(reactContext));
+ List modulesList = new ArrayList<>();
+ module = new RNCWebViewModule(reactContext);
+ module.setPackage(this);
+ modulesList.add(module);
+ return modulesList;
}
// Deprecated from RN 0.47
@@ -23,7 +33,12 @@ public class RNCWebViewPackage implements ReactPackage {
@Override
public List createViewManagers(ReactApplicationContext reactContext) {
- RNCWebViewManager viewManager = new RNCWebViewManager();
- return Arrays.asList(viewManager);
+ manager = new RNCWebViewManager();
+ manager.setPackage(this);
+ return Arrays.asList(manager);
}
-}
\ No newline at end of file
+
+ public RNCWebViewModule getModule() {
+ return module;
+ }
+}
diff --git a/android/src/main/res/xml/file_provider_paths.xml b/android/src/main/res/xml/file_provider_paths.xml
new file mode 100644
index 0000000..d04c4ca
--- /dev/null
+++ b/android/src/main/res/xml/file_provider_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/docs/Guide.md b/docs/Guide.md
index b9412e8..9f36585 100644
--- a/docs/Guide.md
+++ b/docs/Guide.md
@@ -50,3 +50,58 @@ class MyWeb extends Component {
}
```
+### Add support for File Upload
+
+##### iOS
+
+For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
+
+Photo capture:
+```
+NSCameraUsageDescription
+Take pictures for certain activities
+```
+
+Gallery selection:
+```
+NSPhotoLibraryUsageDescription
+Select pictures for certain activities
+```
+
+Video recording:
+```
+NSMicrophoneUsageDescription
+Need microphone access for recording videos
+```
+
+##### Android
+
+Add permission in AndroidManifest.xml:
+```xml
+
+ ......
+
+
+
+
+ ......
+
+```
+
+##### Check for File Upload support, with `static isFileUploadSupported()`
+
+File Upload using `` is not supported for Android 4.4 KitKat (see [details](https://github.com/delight-im/Android-AdvancedWebView/issues/4#issuecomment-70372146)):
+
+```
+import { WebView } from "react-native-webview";
+
+WebView.isFileUploadSupported().then(res => {
+ if (res === true) {
+ // file upload is supported
+ } else {
+ // not file upload support
+ }
+});
+
+```
+
diff --git a/js/WebView.android.js b/js/WebView.android.js
index 1dc7743..718b16c 100644
--- a/js/WebView.android.js
+++ b/js/WebView.android.js
@@ -19,7 +19,8 @@ import {
UIManager,
View,
Image,
- requireNativeComponent
+ requireNativeComponent,
+ NativeModules
} from 'react-native';
import invariant from 'fbjs/lib/invariant';
@@ -71,6 +72,11 @@ class WebView extends React.Component {
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
+ static isFileUploadSupported = async () => {
+ // native implementation should return "true" only for Android 5+
+ return NativeModules.RNCWebView.isFileUploadSupported();
+ }
+
state = {
viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE,
lastErrorEvent: null,
diff --git a/js/WebView.ios.js b/js/WebView.ios.js
index ae679dc..e442344 100644
--- a/js/WebView.ios.js
+++ b/js/WebView.ios.js
@@ -133,6 +133,11 @@ class WebView extends React.Component {
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
+ static isFileUploadSupported = async () => {
+ // no native implementation for iOS, depends only on permissions
+ return true;
+ }
+
state = {
viewState: this.props.startInLoadingState
? WebViewState.LOADING