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.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 filePathCallbackLegacy; private ValueCallback 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 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 (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; } 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) { return types.isEmpty() || types.toLowerCase().contains("image"); } private Boolean acceptsImages(String[] types) { return isArrayEmpty(types) || arrayContainsString(types, "image"); } private Boolean acceptsVideo(String types) { return types.isEmpty() || types.toLowerCase().contains("video"); } private Boolean acceptsVideo(String[] types) { return isArrayEmpty(types) || arrayContainsString(types, "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}; } return types; } 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; } 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; } }; }