diff --git a/README.md b/README.md
index cfc8931a..863bcc32 100644
--- a/README.md
+++ b/README.md
@@ -51,9 +51,9 @@ The type or types of documents to allow selection of. May be an array of types a
Defaults to `import`. If `mode` is set to `import` the document picker imports the file from outside to inside the sandbox, otherwise if `mode` is set to `open` the document picker opens the file right in place.
-##### [iOS only] `copyTo`:`"cachesDirectory" | "documentDirectory"`:
+##### [iOS and Android only] `copyTo`:`"cachesDirectory" | "documentDirectory"`:
-If specified, the picked file is copied to `NSCachesDirectory` / `NSDocumentDirectory` directory. The uri of the copy will be available in result's `fileCopyUri`. If copying the file fails (eg. due to lack of space), `fileCopyUri` will be the same as `uri`, and more details about the error will be available in `copyError` field in the result.
+If specified, the picked file is copied to `NSCachesDirectory` / `NSDocumentDirectory` (iOS) or `getCacheDir` / `getFilesDir` (Android). The uri of the copy will be available in result's `fileCopyUri`. If copying the file fails (eg. due to lack of space), `fileCopyUri` will be the same as `uri`, and more details about the error will be available in `copyError` field in the result.
This should help if you need to work with the file(s) later on, because by default, [the picked documents are temporary files. They remain available only until your application terminates](https://developer.apple.com/documentation/uikit/uidocumentpickerdelegate/2902364-documentpicker). This may impact performance for large files, so keep this in mind if you expect users to pick particularly large files and your app does not need immediate read access.
@@ -84,7 +84,7 @@ The URI representing the document picked by the user. _On iOS this will be a `fi
##### `fileCopyUri`:
-Same as `uri`, but has special meaning on iOS, if `copyTo` option is specified.
+Same as `uri`, but has special meaning if `copyTo` option is specified.
##### `type`:
diff --git a/android/src/main/java/io/github/elyx0/reactnativedocumentpicker/DocumentPickerModule.java b/android/src/main/java/io/github/elyx0/reactnativedocumentpicker/DocumentPickerModule.java
index 999153aa..69719712 100644
--- a/android/src/main/java/io/github/elyx0/reactnativedocumentpicker/DocumentPickerModule.java
+++ b/android/src/main/java/io/github/elyx0/reactnativedocumentpicker/DocumentPickerModule.java
@@ -4,6 +4,7 @@
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ContentResolver;
+import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
@@ -16,8 +17,10 @@
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.BaseActivityEventListener;
+import com.facebook.react.bridge.GuardedResultAsyncTask;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
@@ -25,6 +28,14 @@
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* @see android documentation
*/
@@ -42,9 +53,11 @@ public class DocumentPickerModule extends ReactContextBaseJavaModule {
private static final String OPTION_TYPE = "type";
private static final String OPTION_MULIPLE = "multiple";
+ private static final String OPTION_COPYTO = "copyTo";
private static final String FIELD_URI = "uri";
private static final String FIELD_FILE_COPY_URI = "fileCopyUri";
+ private static final String FIELD_COPY_ERROR = "copyError";
private static final String FIELD_NAME = "name";
private static final String FIELD_TYPE = "type";
private static final String FIELD_SIZE = "size";
@@ -55,7 +68,6 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode,
if (requestCode == READ_REQUEST_CODE) {
if (promise != null) {
onShowActivityResult(resultCode, data, promise);
- promise = null;
}
}
}
@@ -71,6 +83,7 @@ private String[] readableArrayToStringArray(ReadableArray readableArray) {
}
private Promise promise;
+ private String copyTo;
public DocumentPickerModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -91,14 +104,14 @@ public String getName() {
@ReactMethod
public void pick(ReadableMap args, Promise promise) {
Activity currentActivity = getCurrentActivity();
+ this.promise = promise;
+ this.copyTo = args.hasKey(OPTION_COPYTO) ? args.getString(OPTION_COPYTO) : null;
if (currentActivity == null) {
- promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist");
+ sendError(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist");
return;
}
- this.promise = promise;
-
try {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -129,18 +142,16 @@ public void pick(ReadableMap args, Promise promise) {
currentActivity.startActivityForResult(intent, READ_REQUEST_CODE, Bundle.EMPTY);
} catch (ActivityNotFoundException e) {
- this.promise.reject(E_UNABLE_TO_OPEN_FILE_TYPE, e.getLocalizedMessage());
- this.promise = null;
+ sendError(E_UNABLE_TO_OPEN_FILE_TYPE, e.getLocalizedMessage());
} catch (Exception e) {
e.printStackTrace();
- this.promise.reject(E_FAILED_TO_SHOW_PICKER, e.getLocalizedMessage());
- this.promise = null;
+ sendError(E_FAILED_TO_SHOW_PICKER, e.getLocalizedMessage());
}
}
public void onShowActivityResult(int resultCode, Intent data, Promise promise) {
if (resultCode == Activity.RESULT_CANCELED) {
- promise.reject(E_DOCUMENT_PICKER_CANCELED, "User canceled document picker");
+ sendError(E_DOCUMENT_PICKER_CANCELED, "User canceled document picker");
} else if (resultCode == Activity.RESULT_OK) {
Uri uri = null;
ClipData clipData = null;
@@ -151,62 +162,154 @@ public void onShowActivityResult(int resultCode, Intent data, Promise promise) {
}
try {
- WritableArray results = Arguments.createArray();
-
+ List uris = new ArrayList<>();
if (uri != null) {
- results.pushMap(getMetadata(uri));
+ uris.add(uri);
} 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);
- results.pushMap(getMetadata(item.getUri()));
+ uris.add(item.getUri());
}
} else {
- promise.reject(E_INVALID_DATA_RETURNED, "Invalid data returned by intent");
+ sendError(E_INVALID_DATA_RETURNED, "Invalid data returned by intent");
return;
}
- promise.resolve(results);
+ new ProcessDataTask(getReactApplicationContext(), uris, copyTo, promise).execute();
} catch (Exception e) {
- promise.reject(E_UNEXPECTED_EXCEPTION, e.getLocalizedMessage(), e);
+ sendError(E_UNEXPECTED_EXCEPTION, e.getLocalizedMessage(), e);
}
} else {
- promise.reject(E_UNKNOWN_ACTIVITY_RESULT, "Unknown activity result: " + resultCode);
+ sendError(E_UNKNOWN_ACTIVITY_RESULT, "Unknown activity result: " + resultCode);
}
}
- private WritableMap getMetadata(Uri uri) {
- WritableMap map = Arguments.createMap();
-
- map.putString(FIELD_URI, uri.toString());
- // TODO vonovak - FIELD_FILE_COPY_URI is implemented on iOS only (copyTo) settings
- map.putString(FIELD_FILE_COPY_URI, uri.toString());
+ private static class ProcessDataTask extends GuardedResultAsyncTask {
+ private final WeakReference weakContext;
+ private final List uris;
+ private final String copyTo;
+ private final Promise promise;
+
+ protected ProcessDataTask(ReactContext reactContext, List uris, String copyTo, Promise promise) {
+ super(reactContext.getExceptionHandler());
+ this.weakContext = new WeakReference<>(reactContext.getApplicationContext());
+ this.uris = uris;
+ this.copyTo = copyTo;
+ this.promise = promise;
+ }
- ContentResolver contentResolver = getReactApplicationContext().getContentResolver();
+ @Override
+ protected ReadableArray doInBackgroundGuarded() {
+ WritableArray results = Arguments.createArray();
+ for (Uri uri : uris) {
+ results.pushMap(getMetadata(uri));
+ }
+ return results;
+ }
- map.putString(FIELD_TYPE, contentResolver.getType(uri));
+ @Override
+ protected void onPostExecuteGuarded(ReadableArray readableArray) {
+ promise.resolve(readableArray);
+ }
- try (Cursor cursor = contentResolver.query(uri, null, null, null, null, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- int displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
- if (!cursor.isNull(displayNameIndex)) {
- map.putString(FIELD_NAME, cursor.getString(displayNameIndex));
+ private WritableMap getMetadata(Uri uri) {
+ Context context = weakContext.get();
+ if (context == null) {
+ return Arguments.createMap();
+ }
+ ContentResolver contentResolver = context.getContentResolver();
+ WritableMap map = Arguments.createMap();
+ map.putString(FIELD_URI, uri.toString());
+ map.putString(FIELD_TYPE, contentResolver.getType(uri));
+ try (Cursor cursor = contentResolver.query(uri, null, null, null, null, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ int displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (!cursor.isNull(displayNameIndex)) {
+ String fileName = cursor.getString(displayNameIndex);
+ map.putString(FIELD_NAME, fileName);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ int mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE);
+ if (!cursor.isNull(mimeIndex)) {
+ map.putString(FIELD_TYPE, cursor.getString(mimeIndex));
+ }
+ }
+ int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
+ if (!cursor.isNull(sizeIndex)) {
+ map.putInt(FIELD_SIZE, cursor.getInt(sizeIndex));
+ }
}
+ }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
- int mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE);
- if (!cursor.isNull(mimeIndex)) {
- map.putString(FIELD_TYPE, cursor.getString(mimeIndex));
- }
+ prepareFileUri(context, map, uri);
+ return map;
+ }
+
+ private void prepareFileUri(Context context, WritableMap map, Uri uri) {
+ if (copyTo != null) {
+ File dir = context.getCacheDir();
+ if (copyTo.equals("documentDirectory")) {
+ dir = context.getFilesDir();
+ }
+ String fileName = map.getString(FIELD_NAME);
+ if (fileName == null) {
+ fileName = System.currentTimeMillis() + "";
}
+ try {
+ File destFile = new File(dir, fileName);
+ String path = copyFile(context, uri, destFile);
+ map.putString(FIELD_FILE_COPY_URI, path);
+ } catch (IOException e) {
+ e.printStackTrace();
+ map.putString(FIELD_FILE_COPY_URI, uri.toString());
+ map.putString(FIELD_COPY_ERROR, e.getMessage());
+ }
+ } else {
+ map.putString(FIELD_FILE_COPY_URI, uri.toString());
+ }
+ }
- int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
- if (!cursor.isNull(sizeIndex)) {
- map.putInt(FIELD_SIZE, cursor.getInt(sizeIndex));
+ public static String copyFile(Context context, Uri uri, File destFile) throws IOException {
+ InputStream in = null;
+ FileOutputStream out = null;
+ try {
+ in = context.getContentResolver().openInputStream(uri);
+ if (in != null) {
+ out = new FileOutputStream(destFile);
+ byte[] buffer = new byte[1024];
+ while (in.read(buffer) > 0) {
+ out.write(buffer);
+ }
+ out.close();
+ in.close();
+ return destFile.getAbsolutePath();
+ } else {
+ throw new NullPointerException("Invalid input stream");
}
+ } catch (Exception e) {
+ try {
+ if (in != null) {
+ in.close();
+ }
+ if (out != null) {
+ out.close();
+ }
+ } catch (IOException ignored) {}
+ throw e;
}
}
+ }
- return map;
+ private void sendError(String code, String message) {
+ sendError(code, message, null);
+ }
+
+ private void sendError(String code, String message, Exception e) {
+ if (this.promise != null) {
+ Promise temp = this.promise;
+ this.promise = null;
+ temp.reject(code, message, e);
+ }
}
}