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 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; 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.module.annotations.ReactModule; 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 java.util.Arrays; import static android.app.Activity.RESULT_OK; @ReactModule(name = RNCWebViewModule.MODULE_NAME) public class RNCWebViewModule extends ReactContextBaseJavaModule implements ActivityEventListener { public static final String MODULE_NAME = "RNCWebView"; private static final int PICKER = 1; private static final int PICKER_LEGACY = 3; private static final int FILE_DOWNLOAD_PERMISSION_REQUEST = 1; private ValueCallback filePathCallbackLegacy; private ValueCallback filePathCallback; 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) { 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; } }; public RNCWebViewModule(ReactApplicationContext reactContext) { super(reactContext); reactContext.addActivityEventListener(this); } @Override public String getName() { return MODULE_NAME; } @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; } 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 switch (requestCode) { case PICKER: if (resultCode != RESULT_OK) { if (filePathCallback != null) { filePathCallback.onReceiveValue(null); } } else { if (imageTaken) { filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputImage)}); } else if (videoTaken) { filePathCallback.onReceiveValue(new Uri[]{getOutputUri(outputVideo)}); } else { filePathCallback.onReceiveValue(this.getSelectedFiles(data, resultCode)); } } break; case PICKER_LEGACY: 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; outputImage = null; outputVideo = null; } public void onNewIntent(Intent intent) { } private Uri[] getSelectedFiles(Intent data, int resultCode) { if (data == null) { 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; } // 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; } 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)) { Intent photoIntent = getPhotoIntent(); if (photoIntent != null) { extraIntents.add(photoIntent); } } if (acceptsVideo(acceptType)) { Intent videoIntent = getVideoIntent(); if (videoIntent != null) { extraIntents.add(videoIntent); } } 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 String[] acceptTypes, final boolean allowMultiple) { filePathCallback = callback; ArrayList extraIntents = new ArrayList<>(); if (!needsCameraPermission()) { if (acceptsImages(acceptTypes)) { Intent photoIntent = getPhotoIntent(); if (photoIntent != null) { extraIntents.add(photoIntent); } } if (acceptsVideo(acceptTypes)) { Intent videoIntent = getVideoIntent(); if (videoIntent != null) { extraIntents.add(videoIntent); } } } 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; } protected boolean needsCameraPermission() { boolean needed = false; PackageManager packageManager = getCurrentActivity().getPackageManager(); try { String[] requestedPermissions = packageManager.getPackageInfo(getReactApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) && ContextCompat.checkSelfPermission(getCurrentActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { needed = true; } } catch (PackageManager.NameNotFoundException e) { needed = true; } return needed; } private Intent getPhotoIntent() { 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 = 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 = MimeType.DEFAULT.value; } 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(MimeType.DEFAULT.value); 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(MimeType.IMAGE.value); } private Boolean acceptsImages(String[] types) { String[] mimeTypes = getAcceptedMimeType(types); 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(MimeType.VIDEO.value); } private Boolean acceptsVideo(String[] types) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return false; } String[] mimeTypes = getAcceptedMimeType(types); return arrayContainsString(mimeTypes, MimeType.DEFAULT.value) || arrayContainsString(mimeTypes, MimeType.VIDEO.value); } 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 (noAcceptTypesSet(types)) { return new String[]{MimeType.DEFAULT.value}; } 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(".", "")); if(mimeType != null) { mimeTypes[i] = mimeType; } else { mimeTypes[i] = t; } } 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(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); } // 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(MimeType mimeType) throws IOException { String prefix = ""; String suffix = ""; String dir = ""; 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; } 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); outputFile = new File(storageDir, filename); } else { File storageDir = getReactApplicationContext().getExternalFilesDir(null); outputFile = File.createTempFile(prefix, suffix, storageDir); } return outputFile; } 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 types.length == 0 || (types.length == 1 && types[0] != null && types[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; } }