Created
January 21, 2020 10:58
-
-
Save kukiron/21af4a26134e50a967131efd89242be3 to your computer and use it in GitHub Desktop.
Update of `FilePickerModule` from callback to promise implementation in `react-native-file-picker`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.filepicker; | |
import android.Manifest; | |
import android.annotation.TargetApi; | |
import android.app.Activity; | |
import android.app.AlertDialog; | |
import android.content.ActivityNotFoundException; | |
import android.content.ClipData; | |
import android.content.ContentUris; | |
import android.content.Context; | |
import android.content.DialogInterface; | |
import android.content.Intent; | |
import android.content.pm.PackageManager; | |
import android.database.Cursor; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.os.Environment; | |
import android.provider.DocumentsContract; | |
import android.provider.MediaStore; | |
import androidx.core.app.ActivityCompat; | |
import android.util.Log; | |
import android.widget.ArrayAdapter; | |
import com.facebook.react.bridge.ActivityEventListener; | |
import com.facebook.react.bridge.Arguments; | |
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.bridge.ReadableMap; | |
import com.facebook.react.bridge.ReadableMapKeySetIterator; | |
import com.facebook.react.bridge.WritableArray; | |
import com.facebook.react.bridge.WritableMap; | |
import com.facebook.react.modules.core.PermissionAwareActivity; | |
import com.facebook.react.modules.core.PermissionListener; | |
import org.apache.commons.io.FileUtils; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.io.InputStream; | |
import java.io.File; | |
import java.io.FileOutputStream; | |
import java.io.BufferedOutputStream; | |
import java.io.OutputStream; | |
public class FilePickerModule extends ReactContextBaseJavaModule implements ActivityEventListener { | |
static int REQUEST_LAUNCH_FILE_CHOOSER = 2; | |
private final ReactApplicationContext mReactContext; | |
private static final String E_ACTIVITY_DOES_NOT_EXIST = "ACTIVITY_DOES_NOT_EXIST"; | |
private static final String E_PERMISSION_DENIED = "PERMISSION_DENIED"; | |
private static final String E_UNABLE_TO_OPEN_FILE_TYPE = "UNABLE_TO_OPEN_FILE_TYPE"; | |
private static final String E_UNKNOWN_ACTIVITY_RESULT = "UNKNOWN_ACTIVITY_RESULT"; | |
private static final String E_INVALID_DATA_RETURNED = "INVALID_DATA_RETURNED"; | |
private static final String E_UNEXPECTED_EXCEPTION = "UNEXPECTED_EXCEPTION"; | |
private Promise mPromise; | |
public FilePickerModule(ReactApplicationContext reactContext) { | |
super(reactContext); | |
reactContext.addActivityEventListener(this); | |
mReactContext = reactContext; | |
} | |
@Override | |
public String getName() { | |
return "FilePickerManager"; | |
} | |
@ReactMethod | |
public void showFilePicker(final ReadableMap options, final Promise promise) { | |
final Activity currentActivity = getCurrentActivity(); | |
if (currentActivity == null) { | |
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist"); | |
return; | |
} | |
int readPermission = ActivityCompat.checkSelfPermission(currentActivity, Manifest.permission.READ_EXTERNAL_STORAGE); | |
int writePermission = ActivityCompat.checkSelfPermission(currentActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE); | |
if (writePermission != PackageManager.PERMISSION_GRANTED | |
|| readPermission != PackageManager.PERMISSION_GRANTED) { | |
String[] PERMISSIONS = { | |
Manifest.permission.READ_EXTERNAL_STORAGE, | |
Manifest.permission.WRITE_EXTERNAL_STORAGE, | |
}; | |
((PermissionAwareActivity) currentActivity).requestPermissions(PERMISSIONS, 1, new PermissionListener() { | |
@Override | |
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | |
if (requestCode == 1) { | |
int readPermission = ActivityCompat.checkSelfPermission(currentActivity, Manifest.permission.READ_EXTERNAL_STORAGE); | |
int writePermission = ActivityCompat.checkSelfPermission(currentActivity, Manifest.permission.WRITE_EXTERNAL_STORAGE); | |
if (writePermission != PackageManager.PERMISSION_GRANTED | |
|| readPermission != PackageManager.PERMISSION_GRANTED) { | |
// user rejected permission request | |
promise.reject(E_PERMISSION_DENIED, "User rejected permission request"); | |
return true; | |
} | |
// permissions available | |
launchFileChooser(options, promise); | |
return true; | |
} | |
return true; | |
} | |
}); | |
} else { | |
launchFileChooser(options, promise); | |
} | |
} | |
// NOTE: Currently not reentrant / doesn't support concurrent requests | |
@ReactMethod | |
public void launchFileChooser(final ReadableMap options, final Promise promise) { | |
int requestCode; | |
Intent libraryIntent; | |
Activity currentActivity = getCurrentActivity(); | |
String type = "*/*"; | |
boolean multiple = false; | |
if (options.hasKey("multiple")) { | |
multiple = options.getBoolean("multiple"); | |
} | |
if (options.hasKey("type")) { | |
String userRequestedType = options.getString("type"); | |
type = userRequestedType + "/*"; | |
} | |
if (currentActivity == null) { | |
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist"); | |
return; | |
} | |
if (multiple) { | |
REQUEST_LAUNCH_FILE_CHOOSER = 41; | |
requestCode = REQUEST_LAUNCH_FILE_CHOOSER; | |
} else { | |
requestCode = REQUEST_LAUNCH_FILE_CHOOSER; | |
} | |
libraryIntent = new Intent(Intent.ACTION_GET_CONTENT); | |
// libraryIntent.setType("audio/*"); | |
if (multiple) { | |
libraryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple); | |
} | |
libraryIntent.setType(type); | |
libraryIntent.addCategory(Intent.CATEGORY_OPENABLE); | |
if (libraryIntent.resolveActivity(mReactContext.getPackageManager()) == null) { | |
promise.reject(E_UNABLE_TO_OPEN_FILE_TYPE, "Cannot launch file library"); | |
return; | |
} | |
mPromise = promise; | |
try { | |
Intent intent = null; | |
if (multiple) { | |
intent = Intent.createChooser(libraryIntent, "Select a file"); | |
} else { | |
intent = Intent.createChooser(libraryIntent, "Select multiple files"); | |
} | |
currentActivity.startActivityForResult(intent, requestCode); | |
} catch (ActivityNotFoundException e) { | |
e.printStackTrace(); | |
} | |
} | |
// R.N > 33 | |
public void onActivityResult(final Activity activity, final int requestCode, | |
final int resultCode, final Intent data) { | |
onActivityResult(requestCode, resultCode, data); | |
} | |
public void onActivityResult(final int requestCode, final int resultCode, | |
final Intent data) { | |
//robustness code | |
if (mPromise == null || requestCode != REQUEST_LAUNCH_FILE_CHOOSER) { | |
return; | |
} | |
// user cancel | |
if (resultCode != Activity.RESULT_OK) { | |
mPromise.reject(E_UNKNOWN_ACTIVITY_RESULT, "Unknown activity result: " + resultCode); | |
return; | |
} | |
Uri uri = null; | |
ClipData clipData = null; | |
if (data != null) { | |
uri = data.getData(); | |
clipData = data.getClipData(); | |
} | |
try { | |
WritableArray results = Arguments.createArray(); | |
if (uri != null) { | |
WritableMap doc = getDataFromURI(uri); | |
mPromise.resolve(doc); | |
} else if (clipData != null && clipData.getItemCount() > 0) { | |
final int length = clipData.getItemCount(); | |
for (int i = 0; i < length; ++i) { | |
ClipData.Item item = clipData.getItemAt(i); | |
WritableMap doc = getDataFromURI(item.getUri()); | |
results.pushMap(doc); | |
} | |
mPromise.resolve(results); | |
} else { | |
mPromise.reject(E_INVALID_DATA_RETURNED, "Invalid data returned by intent"); | |
return; | |
} | |
} catch (Exception e) { | |
mPromise.reject(E_UNEXPECTED_EXCEPTION, e.getLocalizedMessage(), e); | |
return; | |
} | |
} | |
private WritableMap getDataFromURI(Uri uri) { | |
WritableMap map = Arguments.createMap(); | |
Activity currentActivity = getCurrentActivity(); | |
map.putString("uri", uri.toString()); | |
String path = null; | |
String readableSize = null; | |
Long size = null; | |
// final Uri uri = uri.toString(); | |
path = getPath(currentActivity, uri); | |
if (path != null) { | |
map.putString("path", path); | |
readableSize = getFileReadableSize(path); | |
size = getFileSize(path); | |
map.putString("readableSize", readableSize); | |
map.putInt("size", size.intValue()); | |
} else { | |
path = getFileFromUri(currentActivity, uri); | |
if (!path.equals("error")) { | |
readableSize = getFileReadableSize(path); | |
size = getFileSize(path); | |
map.putString("path", path); | |
map.putString("readableSize", readableSize); | |
map.putInt("size", size.intValue()); | |
} | |
} | |
map.putString("type", currentActivity.getContentResolver().getType(uri)); | |
map.putString("fileName", getFileNameFromUri(currentActivity, uri)); | |
return map; | |
} | |
@TargetApi(Build.VERSION_CODES.KITKAT) | |
public static String getPath(final Context context, final Uri uri) { | |
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; | |
// DocumentProvider | |
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { | |
// ExternalStorageProvider | |
if (isExternalStorageDocument(uri)) { | |
final String docId = DocumentsContract.getDocumentId(uri); | |
final String[] split = docId.split(":"); | |
final String type = split[0]; | |
if ("primary".equalsIgnoreCase(type)) { | |
return Environment.getExternalStorageDirectory() + "/" + split[1]; | |
} | |
// TODO handle non-primary volumes | |
} | |
// DownloadsProvider | |
else if (isDownloadsDocument(uri)) { | |
final String id = DocumentsContract.getDocumentId(uri); | |
final String[] split = id.split(":"); | |
final String type = split[0]; | |
if ("raw".equalsIgnoreCase(type)) { | |
return split[1]; | |
} else { | |
String prefix = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? "file:///" : "content://"; | |
final Uri contentUri = ContentUris.withAppendedId( | |
Uri.parse(prefix + "downloads/public_downloads"), Long.valueOf(id)); | |
return getDataColumn(context, contentUri, null, null); | |
} | |
} | |
// MediaProvider | |
else if (isMediaDocument(uri)) { | |
final String docId = DocumentsContract.getDocumentId(uri); | |
final String[] split = docId.split(":"); | |
final String type = split[0]; | |
Uri contentUri = null; | |
if ("image".equals(type)) { | |
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; | |
} else if ("video".equals(type)) { | |
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; | |
} else if ("audio".equals(type)) { | |
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; | |
} | |
final String selection = "_id=?"; | |
final String[] selectionArgs = new String[]{ | |
split[1] | |
}; | |
return getDataColumn(context, contentUri, selection, selectionArgs); | |
} | |
} | |
// MediaStore (and general) | |
else if ("content".equalsIgnoreCase(uri.getScheme())) { | |
// Return the remote address | |
if (isGooglePhotosUri(uri)) | |
return uri.getLastPathSegment(); | |
return getDataColumn(context, uri, null, null); | |
} | |
// File | |
else if ("file".equalsIgnoreCase(uri.getScheme())) { | |
return uri.getPath(); | |
} | |
return null; | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is ExternalStorageProvider. | |
*/ | |
public static boolean isExternalStorageDocument(Uri uri) { | |
return "com.android.externalstorage.documents".equals(uri.getAuthority()); | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is DownloadsProvider. | |
*/ | |
public static boolean isDownloadsDocument(Uri uri) { | |
return "com.android.providers.downloads.documents".equals(uri.getAuthority()); | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is MediaProvider. | |
*/ | |
public static boolean isMediaDocument(Uri uri) { | |
return "com.android.providers.media.documents".equals(uri.getAuthority()); | |
} | |
/** | |
* @param uri The Uri to check. | |
* @return Whether the Uri authority is Google Photos. | |
*/ | |
public static boolean isGooglePhotosUri(Uri uri) { | |
return "com.google.android.apps.photos.content".equals(uri.getAuthority()); | |
} | |
public static String getDataColumn(Context context, Uri uri, String selection, | |
String[] selectionArgs) { | |
Cursor cursor = null; | |
final String column = "_data"; | |
final String[] projection = { | |
column | |
}; | |
try { | |
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, | |
null); | |
if (cursor != null && cursor.moveToFirst()) { | |
final int column_index = cursor.getColumnIndexOrThrow(column); | |
return cursor.getString(column_index); | |
} | |
} catch (Exception e) { | |
Log.e("FilePickerModule", "Failed to get cursor, so return null for path", e); | |
} finally { | |
if (cursor != null) | |
cursor.close(); | |
} | |
return null; | |
} | |
private String getFileReadableSize(String path) { | |
File file = new File(path); | |
long size = FileUtils.sizeOf(file); | |
return FileUtils.byteCountToDisplaySize(size); | |
} | |
private Long getFileSize (String path) { | |
File file = new File(path); | |
return file.length(); | |
} | |
private String getFileFromUri(Activity activity, Uri uri) { | |
//If it can't get path of file, file is saved in cache, and obtain path from there | |
try { | |
String filePath = activity.getCacheDir().toString(); | |
String fileName = getFileNameFromUri(activity, uri); | |
String path = filePath + "/" + fileName; | |
if (!fileName.equals("error") && saveFileOnCache(path, activity, uri)) { | |
return path; | |
} else { | |
return "error"; | |
} | |
} catch (Exception e) { | |
//Log.d("FilePickerModule", "Error getFileFromStream"); | |
return "error"; | |
} | |
} | |
private String getFileNameFromUri(Activity activity, Uri uri) { | |
Cursor cursor = activity.getContentResolver().query(uri, null, null, null, null); | |
if (cursor != null && cursor.moveToFirst()) { | |
final int column_index = cursor.getColumnIndexOrThrow("_display_name"); | |
return cursor.getString(column_index); | |
} else { | |
return "error"; | |
} | |
} | |
private boolean saveFileOnCache(String path, Activity activity, Uri uri) { | |
//Log.d("FilePickerModule", "saveFileOnCache path: "+path); | |
try { | |
InputStream is = activity.getContentResolver().openInputStream(uri); | |
OutputStream stream = new BufferedOutputStream(new FileOutputStream(path)); | |
int bufferSize = 1024; | |
byte[] buffer = new byte[bufferSize]; | |
int len = 0; | |
while ((len = is.read(buffer)) != -1) { | |
stream.write(buffer, 0, len); | |
} | |
if (stream != null) | |
stream.close(); | |
//Log.d("FilePickerModule", "saveFileOnCache done!"); | |
return true; | |
} catch (Exception e) { | |
//Log.d("FilePickerModule", "saveFileOnCache error"); | |
return false; | |
} | |
} | |
// Required for RN 0.30+ modules than implement ActivityEventListener | |
public void onNewIntent(Intent intent) { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment