fix(android): Fix several Android file upload issues (#1302 by @hojason117)
[skip ci]
This commit is contained in:
parent
da31ab56f0
commit
89886c820d
|
@ -971,8 +971,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
|
|||
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
|
||||
String[] acceptTypes = fileChooserParams.getAcceptTypes();
|
||||
boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE;
|
||||
Intent intent = fileChooserParams.createIntent();
|
||||
return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple);
|
||||
return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -11,9 +11,11 @@ import android.os.Build;
|
|||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.ValueCallback;
|
||||
|
@ -42,11 +44,24 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
private static final int PICKER = 1;
|
||||
private static final int PICKER_LEGACY = 3;
|
||||
private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1;
|
||||
final String DEFAULT_MIME_TYPES = "*/*";
|
||||
private ValueCallback<Uri> filePathCallbackLegacy;
|
||||
private ValueCallback<Uri[]> filePathCallback;
|
||||
private Uri outputFileUri;
|
||||
private File outputImage;
|
||||
private File outputVideo;
|
||||
private DownloadManager.Request downloadRequest;
|
||||
|
||||
private enum MimeType {
|
||||
DEFAULT("*/*"),
|
||||
IMAGE("image"),
|
||||
VIDEO("video");
|
||||
|
||||
private final String value;
|
||||
|
||||
MimeType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private PermissionListener webviewFileDownloaderPermissionListener = new PermissionListener() {
|
||||
@Override
|
||||
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
|
@ -96,6 +111,16 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
return;
|
||||
}
|
||||
|
||||
boolean imageTaken = false;
|
||||
boolean videoTaken = false;
|
||||
|
||||
if (outputImage != null && outputImage.length() > 0) {
|
||||
imageTaken = true;
|
||||
}
|
||||
if (outputVideo != null && outputVideo.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
|
||||
|
@ -106,23 +131,42 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
filePathCallback.onReceiveValue(null);
|
||||
}
|
||||
} else {
|
||||
Uri result[] = this.getSelectedFiles(data, resultCode);
|
||||
if (result != null) {
|
||||
filePathCallback.onReceiveValue(result);
|
||||
if (imageTaken) {
|
||||
filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputImage)});
|
||||
} else if (videoTaken) {
|
||||
filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputVideo)});
|
||||
} else {
|
||||
filePathCallback.onReceiveValue(new Uri[]{outputFileUri});
|
||||
filePathCallback.onReceiveValue(this.getSelectedFiles(data, resultCode));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PICKER_LEGACY:
|
||||
Uri result = resultCode != Activity.RESULT_OK ? null : data == null ? outputFileUri : data.getData();
|
||||
filePathCallbackLegacy.onReceiveValue(result);
|
||||
if (resultCode != RESULT_OK) {
|
||||
filePathCallbackLegacy.onReceiveValue(null);
|
||||
} else {
|
||||
if (imageTaken) {
|
||||
filePathCallbackLegacy.onReceiveValue(getOutputUri(outputImage));
|
||||
} else if (videoTaken) {
|
||||
filePathCallbackLegacy.onReceiveValue(getOutputUri(outputVideo));
|
||||
} else {
|
||||
filePathCallbackLegacy.onReceiveValue(data.getData());
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
if (outputImage != null && !imageTaken) {
|
||||
outputImage.delete();
|
||||
}
|
||||
if (outputVideo != null && !videoTaken) {
|
||||
outputVideo.delete();
|
||||
}
|
||||
|
||||
filePathCallback = null;
|
||||
filePathCallbackLegacy = null;
|
||||
outputFileUri = null;
|
||||
outputImage = null;
|
||||
outputVideo = null;
|
||||
}
|
||||
|
||||
public void onNewIntent(Intent intent) {
|
||||
|
@ -133,15 +177,6 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
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();
|
||||
|
@ -151,6 +186,12 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -162,10 +203,16 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
|
||||
ArrayList<Parcelable> extraIntents = new ArrayList<>();
|
||||
if (acceptsImages(acceptType)) {
|
||||
extraIntents.add(getPhotoIntent());
|
||||
Intent photoIntent = getPhotoIntent();
|
||||
if (photoIntent != null) {
|
||||
extraIntents.add(photoIntent);
|
||||
}
|
||||
}
|
||||
if (acceptsVideo(acceptType)) {
|
||||
extraIntents.add(getVideoIntent());
|
||||
Intent videoIntent = getVideoIntent();
|
||||
if (videoIntent != null) {
|
||||
extraIntents.add(videoIntent);
|
||||
}
|
||||
}
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{}));
|
||||
|
||||
|
@ -177,16 +224,22 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) {
|
||||
public boolean startPhotoPickerIntent(final ValueCallback<Uri[]> callback, final String[] acceptTypes, final boolean allowMultiple) {
|
||||
filePathCallback = callback;
|
||||
|
||||
ArrayList<Parcelable> extraIntents = new ArrayList<>();
|
||||
if (!needsCameraPermission()) {
|
||||
if (acceptsImages(acceptTypes)) {
|
||||
extraIntents.add(getPhotoIntent());
|
||||
Intent photoIntent = getPhotoIntent();
|
||||
if (photoIntent != null) {
|
||||
extraIntents.add(photoIntent);
|
||||
}
|
||||
}
|
||||
if (acceptsVideo(acceptTypes)) {
|
||||
extraIntents.add(getVideoIntent());
|
||||
Intent videoIntent = getVideoIntent();
|
||||
if (videoIntent != null) {
|
||||
extraIntents.add(videoIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,23 +307,41 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
}
|
||||
|
||||
private Intent getPhotoIntent() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
outputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
|
||||
Intent intent = null;
|
||||
|
||||
try {
|
||||
outputImage = getCapturedFile(MimeType.IMAGE);
|
||||
Uri outputImageUri = getOutputUri(outputImage);
|
||||
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;
|
||||
}
|
||||
|
||||
private Intent getVideoIntent() {
|
||||
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
outputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
|
||||
Intent intent = null;
|
||||
|
||||
try {
|
||||
outputVideo = getCapturedFile(MimeType.VIDEO);
|
||||
Uri outputVideoUri = getOutputUri(outputVideo);
|
||||
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 = DEFAULT_MIME_TYPES;
|
||||
_acceptTypes = MimeType.DEFAULT.value;
|
||||
}
|
||||
if (acceptTypes.matches("\\.\\w+")) {
|
||||
_acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", ""));
|
||||
|
@ -284,7 +355,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
intent.setType(MimeType.DEFAULT.value);
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes));
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
||||
return intent;
|
||||
|
@ -295,25 +366,33 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
if (types.matches("\\.\\w+")) {
|
||||
mimeType = getMimeTypeFromExtension(types.replace(".", ""));
|
||||
}
|
||||
return mimeType.isEmpty() || mimeType.toLowerCase().contains("image");
|
||||
return mimeType.isEmpty() || mimeType.toLowerCase().contains(MimeType.IMAGE.value);
|
||||
}
|
||||
|
||||
private Boolean acceptsImages(String[] types) {
|
||||
String[] mimeTypes = getAcceptedMimeType(types);
|
||||
return isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "image");
|
||||
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("video");
|
||||
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 isArrayEmpty(mimeTypes) || arrayContainsString(mimeTypes, "video");
|
||||
return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value);
|
||||
}
|
||||
|
||||
private Boolean arrayContainsString(String[] array, String pattern) {
|
||||
|
@ -326,8 +405,8 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
}
|
||||
|
||||
private String[] getAcceptedMimeType(String[] types) {
|
||||
if (isArrayEmpty(types)) {
|
||||
return new String[]{DEFAULT_MIME_TYPES};
|
||||
if (noAcceptTypesSet(types)) {
|
||||
return new String[]{MimeType.DEFAULT.value};
|
||||
}
|
||||
String[] mimeTypes = new String[types.length];
|
||||
for (int i = 0; i < types.length; i++) {
|
||||
|
@ -355,15 +434,7 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
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();
|
||||
}
|
||||
|
||||
private 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);
|
||||
|
@ -374,41 +445,50 @@ public class RNCWebViewModule extends ReactContextBaseJavaModule implements Acti
|
|||
return FileProvider.getUriForFile(getReactApplicationContext(), packageName + ".fileprovider", capturedFile);
|
||||
}
|
||||
|
||||
private File getCapturedFile(String intentType) throws IOException {
|
||||
private File getCapturedFile(MimeType mimeType) throws IOException {
|
||||
String prefix = "";
|
||||
String suffix = "";
|
||||
String dir = "";
|
||||
String filename = "";
|
||||
|
||||
if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) {
|
||||
switch (mimeType) {
|
||||
case IMAGE:
|
||||
prefix = "image-";
|
||||
suffix = ".jpg";
|
||||
dir = Environment.DIRECTORY_PICTURES;
|
||||
} else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) {
|
||||
break;
|
||||
case VIDEO:
|
||||
prefix = "video-";
|
||||
suffix = ".mp4";
|
||||
dir = Environment.DIRECTORY_MOVIES;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
filename = prefix + String.valueOf(System.currentTimeMillis()) + suffix;
|
||||
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);
|
||||
return new File(storageDir, filename);
|
||||
}
|
||||
|
||||
outputFile = new File(storageDir, filename);
|
||||
} else {
|
||||
File storageDir = getReactApplicationContext().getExternalFilesDir(null);
|
||||
return File.createTempFile(filename, suffix, storageDir);
|
||||
outputFile = File.createTempFile(prefix, suffix, storageDir);
|
||||
}
|
||||
|
||||
private Boolean isArrayEmpty(String[] arr) {
|
||||
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 arr.length == 0 || (arr.length == 1 && arr[0] != null && arr[0].length() == 0);
|
||||
|
||||
return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0);
|
||||
}
|
||||
|
||||
private PermissionAwareActivity getPermissionAwareActivity() {
|
||||
|
|
Loading…
Reference in New Issue