mirror of
https://github.com/status-im/react-native-webview.git
synced 2026-08-31 11:41:08 +00:00
The current implementation doesn't support a list of file extensions in the accept attribute (e.g. `<input type="file" accept=".jpg, .png">`) while that is a valid value per the HTML spec. I've updated the implementation to convert any file extensions to mime types before we set the type(s) on the intent to rectify the issue. In addition I've updated the `acceptsImages` and `acceptsVideo` methods to handle file extensions as well.
415 lines
14 KiB
Java
415 lines
14 KiB
Java
|
|
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 android.support.annotation.RequiresApi;
|
|
import android.support.v4.content.ContextCompat;
|
|
import android.support.v4.content.FileProvider;
|
|
import android.util.Log;
|
|
import android.webkit.MimeTypeMap;
|
|
import android.webkit.ValueCallback;
|
|
import android.webkit.WebChromeClient;
|
|
import android.widget.Toast;
|
|
|
|
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.modules.core.PermissionAwareActivity;
|
|
import com.facebook.react.modules.core.PermissionListener;
|
|
|
|
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<Uri> filePathCallbackLegacy;
|
|
private ValueCallback<Uri[]> filePathCallback;
|
|
private Uri outputFileUri;
|
|
|
|
private DownloadManager.Request downloadRequest;
|
|
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
|
|
|
|
final String DEFAULT_MIME_TYPES = "*/*";
|
|
|
|
public RNCWebViewModule(ReactApplicationContext reactContext) {
|
|
super(reactContext);
|
|
this.reactContext = reactContext;
|
|
reactContext.addActivityEventListener(this);
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
return "RNCWebView";
|
|
}
|
|
|
|
@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<Uri> filePathCallback, String acceptType) {
|
|
filePathCallbackLegacy = filePathCallback;
|
|
|
|
Intent fileChooserIntent = getFileChooserIntent(acceptType);
|
|
Intent chooserIntent = Intent.createChooser(fileChooserIntent, "");
|
|
|
|
ArrayList<Parcelable> 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<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
|
|
filePathCallback = callback;
|
|
|
|
ArrayList<Parcelable> 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 void setDownloadRequest(DownloadManager.Request request) {
|
|
this.downloadRequest = request;
|
|
}
|
|
|
|
public void downloadFile() {
|
|
DownloadManager dm = (DownloadManager) getCurrentActivity().getBaseContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
|
String downloadMessage = "Downloading";
|
|
|
|
dm.enqueue(this.downloadRequest);
|
|
|
|
Toast.makeText(getCurrentActivity().getApplicationContext(), downloadMessage, Toast.LENGTH_LONG).show();
|
|
}
|
|
|
|
public boolean grantFileDownloaderPermissions() {
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
return true;
|
|
}
|
|
|
|
boolean result = true;
|
|
if (ContextCompat.checkSelfPermission(getCurrentActivity(), 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 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;
|
|
}
|
|
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("*/*");
|
|
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("image");
|
|
}
|
|
private Boolean acceptsImages(String[] types) {
|
|
String[] mimeTypes = getAcceptedMimeType(types);
|
|
return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image");
|
|
}
|
|
|
|
private Boolean acceptsVideo(String types) {
|
|
String mimeType = types;
|
|
if (types.matches("\\.\\w+")) {
|
|
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
|
|
}
|
|
return mimeType.isEmpty() || mimeType.toLowerCase().contains("video");
|
|
}
|
|
private Boolean acceptsVideo(String[] types) {
|
|
String[] mimeTypes = getAcceptedMimeType(types);
|
|
return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "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};
|
|
}
|
|
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(".", ""));
|
|
mimeTypes[i] = mimeType;
|
|
} else {
|
|
mimeTypes[i] = t;
|
|
}
|
|
}
|
|
return mimeTypes;
|
|
}
|
|
|
|
private String getMimeTypeFromExtension(String extension) {
|
|
String type = null;
|
|
if (extension != null) {
|
|
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
|
}
|
|
return type;
|
|
}
|
|
|
|
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. <input type="file" />, 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);
|
|
}
|
|
|
|
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 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(getCurrentActivity().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;
|
|
}
|
|
};
|
|
}
|