Skip to content

Commit 02afc3f

Browse files
r0b0t3dTuan Luong
andauthored
[Android][Google Drive] Download file from google drive then cache in local storage (#264)
* get real file path * update variable * add property to get file path from uri * fix bug could not get file from download folder * change getPath to usePath * Get data from google drive app * implement copyTo * update README * update index.d.ts * update code Co-authored-by: Tuan Luong <tuanluong@Tuans-MacBook-Pro.local>
1 parent 15976f0 commit 02afc3f

File tree

2 files changed

+145
-42
lines changed

2 files changed

+145
-42
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ The type or types of documents to allow selection of. May be an array of types a
5151

5252
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.
5353

54-
##### [iOS only] `copyTo`:`"cachesDirectory" | "documentDirectory"`:
54+
##### [iOS and Android only] `copyTo`:`"cachesDirectory" | "documentDirectory"`:
5555

56-
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.
56+
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.
5757

5858
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.
5959

@@ -84,7 +84,7 @@ The URI representing the document picked by the user. _On iOS this will be a `fi
8484

8585
##### `fileCopyUri`:
8686

87-
Same as `uri`, but has special meaning on iOS, if `copyTo` option is specified.
87+
Same as `uri`, but has special meaning if `copyTo` option is specified.
8888

8989
##### `type`:
9090

android/src/main/java/io/github/elyx0/reactnativedocumentpicker/DocumentPickerModule.java

Lines changed: 142 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.content.ActivityNotFoundException;
55
import android.content.ClipData;
66
import android.content.ContentResolver;
7+
import android.content.Context;
78
import android.content.Intent;
89
import android.database.Cursor;
910
import android.net.Uri;
@@ -16,15 +17,25 @@
1617
import com.facebook.react.bridge.ActivityEventListener;
1718
import com.facebook.react.bridge.Arguments;
1819
import com.facebook.react.bridge.BaseActivityEventListener;
20+
import com.facebook.react.bridge.GuardedResultAsyncTask;
1921
import com.facebook.react.bridge.Promise;
2022
import com.facebook.react.bridge.ReactApplicationContext;
23+
import com.facebook.react.bridge.ReactContext;
2124
import com.facebook.react.bridge.ReactContextBaseJavaModule;
2225
import com.facebook.react.bridge.ReactMethod;
2326
import com.facebook.react.bridge.ReadableArray;
2427
import com.facebook.react.bridge.ReadableMap;
2528
import com.facebook.react.bridge.WritableArray;
2629
import com.facebook.react.bridge.WritableMap;
2730

31+
import java.io.File;
32+
import java.io.FileOutputStream;
33+
import java.io.IOException;
34+
import java.io.InputStream;
35+
import java.lang.ref.WeakReference;
36+
import java.util.ArrayList;
37+
import java.util.List;
38+
2839
/**
2940
* @see <a href="https://developer.android.com/guide/topics/providers/document-provider.html">android documentation</a>
3041
*/
@@ -42,9 +53,11 @@ public class DocumentPickerModule extends ReactContextBaseJavaModule {
4253

4354
private static final String OPTION_TYPE = "type";
4455
private static final String OPTION_MULIPLE = "multiple";
56+
private static final String OPTION_COPYTO = "copyTo";
4557

4658
private static final String FIELD_URI = "uri";
4759
private static final String FIELD_FILE_COPY_URI = "fileCopyUri";
60+
private static final String FIELD_COPY_ERROR = "copyError";
4861
private static final String FIELD_NAME = "name";
4962
private static final String FIELD_TYPE = "type";
5063
private static final String FIELD_SIZE = "size";
@@ -55,7 +68,6 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode,
5568
if (requestCode == READ_REQUEST_CODE) {
5669
if (promise != null) {
5770
onShowActivityResult(resultCode, data, promise);
58-
promise = null;
5971
}
6072
}
6173
}
@@ -71,6 +83,7 @@ private String[] readableArrayToStringArray(ReadableArray readableArray) {
7183
}
7284

7385
private Promise promise;
86+
private String copyTo;
7487

7588
public DocumentPickerModule(ReactApplicationContext reactContext) {
7689
super(reactContext);
@@ -91,14 +104,14 @@ public String getName() {
91104
@ReactMethod
92105
public void pick(ReadableMap args, Promise promise) {
93106
Activity currentActivity = getCurrentActivity();
107+
this.promise = promise;
108+
this.copyTo = args.hasKey(OPTION_COPYTO) ? args.getString(OPTION_COPYTO) : null;
94109

95110
if (currentActivity == null) {
96-
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist");
111+
sendError(E_ACTIVITY_DOES_NOT_EXIST, "Current activity does not exist");
97112
return;
98113
}
99114

100-
this.promise = promise;
101-
102115
try {
103116
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
104117
intent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -129,18 +142,16 @@ public void pick(ReadableMap args, Promise promise) {
129142

130143
currentActivity.startActivityForResult(intent, READ_REQUEST_CODE, Bundle.EMPTY);
131144
} catch (ActivityNotFoundException e) {
132-
this.promise.reject(E_UNABLE_TO_OPEN_FILE_TYPE, e.getLocalizedMessage());
133-
this.promise = null;
145+
sendError(E_UNABLE_TO_OPEN_FILE_TYPE, e.getLocalizedMessage());
134146
} catch (Exception e) {
135147
e.printStackTrace();
136-
this.promise.reject(E_FAILED_TO_SHOW_PICKER, e.getLocalizedMessage());
137-
this.promise = null;
148+
sendError(E_FAILED_TO_SHOW_PICKER, e.getLocalizedMessage());
138149
}
139150
}
140151

141152
public void onShowActivityResult(int resultCode, Intent data, Promise promise) {
142153
if (resultCode == Activity.RESULT_CANCELED) {
143-
promise.reject(E_DOCUMENT_PICKER_CANCELED, "User canceled document picker");
154+
sendError(E_DOCUMENT_PICKER_CANCELED, "User canceled document picker");
144155
} else if (resultCode == Activity.RESULT_OK) {
145156
Uri uri = null;
146157
ClipData clipData = null;
@@ -151,62 +162,154 @@ public void onShowActivityResult(int resultCode, Intent data, Promise promise) {
151162
}
152163

153164
try {
154-
WritableArray results = Arguments.createArray();
155-
165+
List<Uri> uris = new ArrayList<>();
156166
if (uri != null) {
157-
results.pushMap(getMetadata(uri));
167+
uris.add(uri);
158168
} else if (clipData != null && clipData.getItemCount() > 0) {
159169
final int length = clipData.getItemCount();
160170
for (int i = 0; i < length; ++i) {
161171
ClipData.Item item = clipData.getItemAt(i);
162-
results.pushMap(getMetadata(item.getUri()));
172+
uris.add(item.getUri());
163173
}
164174
} else {
165-
promise.reject(E_INVALID_DATA_RETURNED, "Invalid data returned by intent");
175+
sendError(E_INVALID_DATA_RETURNED, "Invalid data returned by intent");
166176
return;
167177
}
168178

169-
promise.resolve(results);
179+
new ProcessDataTask(getReactApplicationContext(), uris, copyTo, promise).execute();
170180
} catch (Exception e) {
171-
promise.reject(E_UNEXPECTED_EXCEPTION, e.getLocalizedMessage(), e);
181+
sendError(E_UNEXPECTED_EXCEPTION, e.getLocalizedMessage(), e);
172182
}
173183
} else {
174-
promise.reject(E_UNKNOWN_ACTIVITY_RESULT, "Unknown activity result: " + resultCode);
184+
sendError(E_UNKNOWN_ACTIVITY_RESULT, "Unknown activity result: " + resultCode);
175185
}
176186
}
177187

178-
private WritableMap getMetadata(Uri uri) {
179-
WritableMap map = Arguments.createMap();
180-
181-
map.putString(FIELD_URI, uri.toString());
182-
// TODO vonovak - FIELD_FILE_COPY_URI is implemented on iOS only (copyTo) settings
183-
map.putString(FIELD_FILE_COPY_URI, uri.toString());
188+
private static class ProcessDataTask extends GuardedResultAsyncTask<ReadableArray> {
189+
private final WeakReference<Context> weakContext;
190+
private final List<Uri> uris;
191+
private final String copyTo;
192+
private final Promise promise;
193+
194+
protected ProcessDataTask(ReactContext reactContext, List<Uri> uris, String copyTo, Promise promise) {
195+
super(reactContext.getExceptionHandler());
196+
this.weakContext = new WeakReference<>(reactContext.getApplicationContext());
197+
this.uris = uris;
198+
this.copyTo = copyTo;
199+
this.promise = promise;
200+
}
184201

185-
ContentResolver contentResolver = getReactApplicationContext().getContentResolver();
202+
@Override
203+
protected ReadableArray doInBackgroundGuarded() {
204+
WritableArray results = Arguments.createArray();
205+
for (Uri uri : uris) {
206+
results.pushMap(getMetadata(uri));
207+
}
208+
return results;
209+
}
186210

187-
map.putString(FIELD_TYPE, contentResolver.getType(uri));
211+
@Override
212+
protected void onPostExecuteGuarded(ReadableArray readableArray) {
213+
promise.resolve(readableArray);
214+
}
188215

189-
try (Cursor cursor = contentResolver.query(uri, null, null, null, null, null)) {
190-
if (cursor != null && cursor.moveToFirst()) {
191-
int displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
192-
if (!cursor.isNull(displayNameIndex)) {
193-
map.putString(FIELD_NAME, cursor.getString(displayNameIndex));
216+
private WritableMap getMetadata(Uri uri) {
217+
Context context = weakContext.get();
218+
if (context == null) {
219+
return Arguments.createMap();
220+
}
221+
ContentResolver contentResolver = context.getContentResolver();
222+
WritableMap map = Arguments.createMap();
223+
map.putString(FIELD_URI, uri.toString());
224+
map.putString(FIELD_TYPE, contentResolver.getType(uri));
225+
try (Cursor cursor = contentResolver.query(uri, null, null, null, null, null)) {
226+
if (cursor != null && cursor.moveToFirst()) {
227+
int displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
228+
if (!cursor.isNull(displayNameIndex)) {
229+
String fileName = cursor.getString(displayNameIndex);
230+
map.putString(FIELD_NAME, fileName);
231+
}
232+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
233+
int mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE);
234+
if (!cursor.isNull(mimeIndex)) {
235+
map.putString(FIELD_TYPE, cursor.getString(mimeIndex));
236+
}
237+
}
238+
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
239+
if (!cursor.isNull(sizeIndex)) {
240+
map.putInt(FIELD_SIZE, cursor.getInt(sizeIndex));
241+
}
194242
}
243+
}
195244

196-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
197-
int mimeIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE);
198-
if (!cursor.isNull(mimeIndex)) {
199-
map.putString(FIELD_TYPE, cursor.getString(mimeIndex));
200-
}
245+
prepareFileUri(context, map, uri);
246+
return map;
247+
}
248+
249+
private void prepareFileUri(Context context, WritableMap map, Uri uri) {
250+
if (copyTo != null) {
251+
File dir = context.getCacheDir();
252+
if (copyTo.equals("documentDirectory")) {
253+
dir = context.getFilesDir();
254+
}
255+
String fileName = map.getString(FIELD_NAME);
256+
if (fileName == null) {
257+
fileName = System.currentTimeMillis() + "";
201258
}
259+
try {
260+
File destFile = new File(dir, fileName);
261+
String path = copyFile(context, uri, destFile);
262+
map.putString(FIELD_FILE_COPY_URI, path);
263+
} catch (IOException e) {
264+
e.printStackTrace();
265+
map.putString(FIELD_FILE_COPY_URI, uri.toString());
266+
map.putString(FIELD_COPY_ERROR, e.getMessage());
267+
}
268+
} else {
269+
map.putString(FIELD_FILE_COPY_URI, uri.toString());
270+
}
271+
}
202272

203-
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
204-
if (!cursor.isNull(sizeIndex)) {
205-
map.putInt(FIELD_SIZE, cursor.getInt(sizeIndex));
273+
public static String copyFile(Context context, Uri uri, File destFile) throws IOException {
274+
InputStream in = null;
275+
FileOutputStream out = null;
276+
try {
277+
in = context.getContentResolver().openInputStream(uri);
278+
if (in != null) {
279+
out = new FileOutputStream(destFile);
280+
byte[] buffer = new byte[1024];
281+
while (in.read(buffer) > 0) {
282+
out.write(buffer);
283+
}
284+
out.close();
285+
in.close();
286+
return destFile.getAbsolutePath();
287+
} else {
288+
throw new NullPointerException("Invalid input stream");
206289
}
290+
} catch (Exception e) {
291+
try {
292+
if (in != null) {
293+
in.close();
294+
}
295+
if (out != null) {
296+
out.close();
297+
}
298+
} catch (IOException ignored) {}
299+
throw e;
207300
}
208301
}
302+
}
209303

210-
return map;
304+
private void sendError(String code, String message) {
305+
sendError(code, message, null);
306+
}
307+
308+
private void sendError(String code, String message, Exception e) {
309+
if (this.promise != null) {
310+
Promise temp = this.promise;
311+
this.promise = null;
312+
temp.reject(code, message, e);
313+
}
211314
}
212315
}

0 commit comments

Comments
 (0)