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; final String DEFAULT_MIME_TYPES = "*/*"; private ValueCallback filePathCallbackLegacy; private ValueCallback filePathCallback; private Uri outputFileUri; private DownloadManager.Request downloadRequest; 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; } // 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 (! needsCameraPermission()) { 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; } 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 = 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); outputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri); 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. , 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; } }