diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java index 1995d87..85578fc 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java @@ -971,8 +971,7 @@ public class RNCWebViewManager extends SimpleViewManager { 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(mReactContext).startPhotoPickerIntent(filePathCallback, intent, acceptTypes, allowMultiple); + return getModule(mReactContext).startPhotoPickerIntent(filePathCallback, acceptTypes, allowMultiple); } @Override diff --git a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java index 7a82df5..c767621 100644 --- a/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java +++ b/android/src/main/java/com/reactnativecommunity/webview/RNCWebViewModule.java @@ -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 filePathCallbackLegacy; private ValueCallback 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 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 callback, final Intent intent, final String[] acceptTypes, final boolean allowMultiple) { + public boolean startPhotoPickerIntent(final ValueCallback callback, final String[] acceptTypes, final boolean allowMultiple) { filePathCallback = callback; ArrayList extraIntents = new ArrayList<>(); - if (! needsCameraPermission()) { + 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)) { - prefix = "image-"; - suffix = ".jpg"; - dir = Environment.DIRECTORY_PICTURES; - } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) { - prefix = "video-"; - suffix = ".mp4"; - dir = Environment.DIRECTORY_MOVIES; + switch (mimeType) { + case IMAGE: + prefix = "image-"; + suffix = ".jpg"; + dir = Environment.DIRECTORY_PICTURES; + 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); + outputFile = File.createTempFile(prefix, suffix, storageDir); } - File storageDir = getReactApplicationContext().getExternalFilesDir(null); - return File.createTempFile(filename, suffix, storageDir); + return outputFile; } - private Boolean isArrayEmpty(String[] arr) { + private Boolean noAcceptTypesSet(String[] types) { // 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] != null && arr[0].length() == 0); + + return types.length == 0 || (types.length == 1 && types[0] != null && types[0].length() == 0); } private PermissionAwareActivity getPermissionAwareActivity() {