@react-native-documents/picker 9.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +80 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/com/reactnativedocumentpicker/CopyDestination.kt +12 -0
- package/android/src/main/java/com/reactnativedocumentpicker/DocumentMetadataBuilder.kt +79 -0
- package/android/src/main/java/com/reactnativedocumentpicker/FileOperations.kt +203 -0
- package/android/src/main/java/com/reactnativedocumentpicker/IntentFactory.kt +36 -0
- package/android/src/main/java/com/reactnativedocumentpicker/IsKnownTypeImpl.kt +40 -0
- package/android/src/main/java/com/reactnativedocumentpicker/MetadataGetter.kt +150 -0
- package/android/src/main/java/com/reactnativedocumentpicker/PickOptions.kt +63 -0
- package/android/src/main/java/com/reactnativedocumentpicker/PromiseWrapper.java +105 -0
- package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerModule.kt +352 -0
- package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerPackage.java +49 -0
- package/android/src/paper/java/com/reactnativedocumentpicker/NativeDocumentPickerSpec.java +69 -0
- package/ios/RCTConvert+RNDocumentPicker.h +8 -0
- package/ios/RCTConvert+RNDocumentPicker.mm +16 -0
- package/ios/RNDocumentPicker.h +19 -0
- package/ios/RNDocumentPicker.mm +128 -0
- package/ios/swift/DocPicker.swift +84 -0
- package/ios/swift/DocSaver.swift +41 -0
- package/ios/swift/DocumentMetadataBuilder.swift +69 -0
- package/ios/swift/FileOperations.swift +68 -0
- package/ios/swift/IsKnownTypeImpl.swift +42 -0
- package/ios/swift/LocalCopyResponse.swift +27 -0
- package/ios/swift/PickerBase.swift +78 -0
- package/ios/swift/PickerOptions.swift +44 -0
- package/ios/swift/PromiseSupport.swift +2 -0
- package/ios/swift/PromiseWrapper.swift +92 -0
- package/ios/swift/SaverOptions.swift +30 -0
- package/jest/build/jest/setup.js +70 -0
- package/jest/build/src/errors.js +47 -0
- package/jest/build/src/fileTypes.js +53 -0
- package/jest/build/src/index.js +22 -0
- package/jest/build/src/isKnownType.js +16 -0
- package/jest/build/src/keepLocalCopy.js +17 -0
- package/jest/build/src/pick.js +50 -0
- package/jest/build/src/pickDirectory.js +31 -0
- package/jest/build/src/release.js +22 -0
- package/jest/build/src/saveDocuments.js +40 -0
- package/jest/build/src/spec/NativeDocumentPicker.js +5 -0
- package/jest/build/src/types.js +4 -0
- package/jest/build/src/validateTypes.js +23 -0
- package/jest/build/tsconfig.tsbuildinfo +1 -0
- package/lib/commonjs/errors.js +53 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/fileTypes.js +84 -0
- package/lib/commonjs/fileTypes.js.map +1 -0
- package/lib/commonjs/index.js +74 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/isKnownType.js +27 -0
- package/lib/commonjs/isKnownType.js.map +1 -0
- package/lib/commonjs/keepLocalCopy.js +34 -0
- package/lib/commonjs/keepLocalCopy.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/pick.js +93 -0
- package/lib/commonjs/pick.js.map +1 -0
- package/lib/commonjs/pickDirectory.js +71 -0
- package/lib/commonjs/pickDirectory.js.map +1 -0
- package/lib/commonjs/release.js +31 -0
- package/lib/commonjs/release.js.map +1 -0
- package/lib/commonjs/saveDocuments.js +55 -0
- package/lib/commonjs/saveDocuments.js.map +1 -0
- package/lib/commonjs/spec/NativeDocumentPicker.js +16 -0
- package/lib/commonjs/spec/NativeDocumentPicker.js.map +1 -0
- package/lib/commonjs/types.js +37 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/validateTypes.js +29 -0
- package/lib/commonjs/validateTypes.js.map +1 -0
- package/lib/module/errors.js +48 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/fileTypes.js +81 -0
- package/lib/module/fileTypes.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/isKnownType.js +24 -0
- package/lib/module/isKnownType.js.map +1 -0
- package/lib/module/keepLocalCopy.js +31 -0
- package/lib/module/keepLocalCopy.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/pick.js +90 -0
- package/lib/module/pick.js.map +1 -0
- package/lib/module/pickDirectory.js +68 -0
- package/lib/module/pickDirectory.js.map +1 -0
- package/lib/module/release.js +26 -0
- package/lib/module/release.js.map +1 -0
- package/lib/module/saveDocuments.js +52 -0
- package/lib/module/saveDocuments.js.map +1 -0
- package/lib/module/spec/NativeDocumentPicker.js +13 -0
- package/lib/module/spec/NativeDocumentPicker.js.map +1 -0
- package/lib/module/types.js +33 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/validateTypes.js +24 -0
- package/lib/module/validateTypes.js.map +1 -0
- package/lib/typescript/errors.d.ts +40 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/fileTypes.d.ts +94 -0
- package/lib/typescript/fileTypes.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +13 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/isKnownType.d.ts +41 -0
- package/lib/typescript/isKnownType.d.ts.map +1 -0
- package/lib/typescript/keepLocalCopy.d.ts +46 -0
- package/lib/typescript/keepLocalCopy.d.ts.map +1 -0
- package/lib/typescript/pick.d.ts +84 -0
- package/lib/typescript/pick.d.ts.map +1 -0
- package/lib/typescript/pickDirectory.d.ts +62 -0
- package/lib/typescript/pickDirectory.d.ts.map +1 -0
- package/lib/typescript/release.d.ts +24 -0
- package/lib/typescript/release.d.ts.map +1 -0
- package/lib/typescript/saveDocuments.d.ts +55 -0
- package/lib/typescript/saveDocuments.d.ts.map +1 -0
- package/lib/typescript/spec/NativeDocumentPicker.d.ts +29 -0
- package/lib/typescript/spec/NativeDocumentPicker.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +95 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/validateTypes.d.ts +3 -0
- package/lib/typescript/validateTypes.d.ts.map +1 -0
- package/package.json +92 -0
- package/react-native-document-picker.podspec +30 -0
- package/src/errors.ts +49 -0
- package/src/fileTypes.ts +92 -0
- package/src/index.ts +47 -0
- package/src/isKnownType.ts +48 -0
- package/src/keepLocalCopy.ts +51 -0
- package/src/pick.ts +151 -0
- package/src/pickDirectory.ts +93 -0
- package/src/release.ts +36 -0
- package/src/saveDocuments.ts +99 -0
- package/src/spec/NativeDocumentPicker.ts +31 -0
- package/src/types.ts +119 -0
- package/src/validateTypes.ts +26 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
package com.reactnativedocumentpicker
|
|
3
|
+
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import com.facebook.react.bridge.ReadableArray
|
|
6
|
+
import com.facebook.react.bridge.ReadableMap
|
|
7
|
+
|
|
8
|
+
data class PickOptions(
|
|
9
|
+
private val mode: String?,
|
|
10
|
+
val mimeTypes: Array<String>,
|
|
11
|
+
val initialDirectoryUrl: String?,
|
|
12
|
+
val localOnly: Boolean,
|
|
13
|
+
val multiple: Boolean,
|
|
14
|
+
val requestLongTermAccess: Boolean,
|
|
15
|
+
val allowVirtualFiles: Boolean,
|
|
16
|
+
) {
|
|
17
|
+
val action: String
|
|
18
|
+
get() = if ("open" == mode) Intent.ACTION_OPEN_DOCUMENT else Intent.ACTION_GET_CONTENT
|
|
19
|
+
|
|
20
|
+
val intentFilterTypes: String get() {
|
|
21
|
+
return if (action == Intent.ACTION_OPEN_DOCUMENT) {
|
|
22
|
+
// https://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT
|
|
23
|
+
"*/*"
|
|
24
|
+
} else {
|
|
25
|
+
// https://stackoverflow.com/a/46074075/2070942
|
|
26
|
+
mimeTypes.joinToString("|")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fun parsePickOptions(readableMap: ReadableMap): PickOptions {
|
|
32
|
+
val mode = readableMap.getString("mode")
|
|
33
|
+
|
|
34
|
+
val mimeTypes = if (readableMap.hasKey("type") && !readableMap.isNull("type")) {
|
|
35
|
+
readableMap.getArray("type")?.let { readableArrayToStringArray(it) } ?: arrayOf("*/*")
|
|
36
|
+
} else {
|
|
37
|
+
arrayOf("*/*")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
val initialDirectoryUrl = if (readableMap.hasKey("initialDirectoryUrl")) readableMap.getString("initialDirectoryUrl") else null
|
|
41
|
+
val localOnly = readableMap.hasKey("localOnly") && readableMap.getBoolean("localOnly")
|
|
42
|
+
val multiple = readableMap.hasKey("allowMultiSelection") && readableMap.getBoolean("allowMultiSelection")
|
|
43
|
+
val requestLongTermAccess = readableMap.hasKey("requestLongTermAccess") && readableMap.getBoolean("requestLongTermAccess")
|
|
44
|
+
val allowVirtualFiles = readableMap.hasKey("allowVirtualFiles") && readableMap.getBoolean("allowVirtualFiles")
|
|
45
|
+
|
|
46
|
+
return PickOptions(
|
|
47
|
+
mode = mode,
|
|
48
|
+
mimeTypes = mimeTypes,
|
|
49
|
+
initialDirectoryUrl = initialDirectoryUrl,
|
|
50
|
+
localOnly = localOnly,
|
|
51
|
+
multiple = multiple,
|
|
52
|
+
requestLongTermAccess = requestLongTermAccess,
|
|
53
|
+
allowVirtualFiles = allowVirtualFiles,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
fun readableArrayToStringArray(readableArray: ReadableArray): Array<String> {
|
|
57
|
+
/**
|
|
58
|
+
* MIME type and Uri scheme matching in the
|
|
59
|
+
* Android framework is case-sensitive, unlike the formal RFC definitions.
|
|
60
|
+
* As a result, you should always write these elements with lower case letters,
|
|
61
|
+
* */
|
|
62
|
+
return readableArray.toArrayList().map { Intent.normalizeMimeType(it.toString())!! }.toTypedArray()
|
|
63
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
package com.reactnativedocumentpicker;
|
|
3
|
+
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
|
|
8
|
+
import com.facebook.react.bridge.Promise;
|
|
9
|
+
|
|
10
|
+
public class PromiseWrapper {
|
|
11
|
+
|
|
12
|
+
private Promise promise;
|
|
13
|
+
private String nameOfCallInProgress;
|
|
14
|
+
public static final String ASYNC_OP_IN_PROGRESS = "ASYNC_OP_IN_PROGRESS";
|
|
15
|
+
public static final String E_DOCUMENT_PICKER_CANCELED = "OPERATION_CANCELED";
|
|
16
|
+
private final String MODULE_NAME;
|
|
17
|
+
|
|
18
|
+
public PromiseWrapper(String moduleName) {
|
|
19
|
+
MODULE_NAME = moduleName;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public void setPromiseRejectingPrevious(Promise promise, @NonNull String fromCallsite) {
|
|
23
|
+
Promise previousPromise = this.promise;
|
|
24
|
+
if (previousPromise != null) {
|
|
25
|
+
rejectPreviousPromiseBecauseNewOneIsInProgress(previousPromise, fromCallsite);
|
|
26
|
+
}
|
|
27
|
+
this.promise = promise;
|
|
28
|
+
nameOfCallInProgress = fromCallsite;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public boolean trySetPromiseRejectingIncoming(Promise promise, @NonNull String fromCallsite) {
|
|
32
|
+
Promise previousPromise = this.promise;
|
|
33
|
+
if (previousPromise != null) {
|
|
34
|
+
rejectNewPromiseBecauseOldOneIsInProgress(promise, fromCallsite);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
this.promise = promise;
|
|
38
|
+
nameOfCallInProgress = fromCallsite;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public void resolve(Object value) {
|
|
43
|
+
Promise resolver = promise;
|
|
44
|
+
if (resolver == null) {
|
|
45
|
+
Log.e(MODULE_NAME, "cannot resolve promise because it's null");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resetMembers();
|
|
50
|
+
resolver.resolve(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public void reject(@NonNull String code, Exception e) {
|
|
54
|
+
String message = e.getLocalizedMessage() != null ? e.getLocalizedMessage() :
|
|
55
|
+
e.getMessage() != null ? e.getMessage() : "unknown error";
|
|
56
|
+
|
|
57
|
+
this.reject(code, message, e);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public void reject(Exception e) {
|
|
61
|
+
String message = e.getLocalizedMessage() != null ? e.getLocalizedMessage() :
|
|
62
|
+
e.getMessage() != null ? e.getMessage() : "unknown error";
|
|
63
|
+
|
|
64
|
+
this.reject(nameOfCallInProgress, message, e);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public void rejectAsUserCancelledOperation() {
|
|
68
|
+
this.reject(E_DOCUMENT_PICKER_CANCELED, "user canceled the document picker");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public void reject(@NonNull String code, @NonNull String message) {
|
|
72
|
+
reject(code, message, null);
|
|
73
|
+
}
|
|
74
|
+
public void reject(@NonNull String code, @NonNull String message, Exception e) {
|
|
75
|
+
Promise rejecter = promise;
|
|
76
|
+
if (rejecter == null) {
|
|
77
|
+
Log.e(MODULE_NAME, "cannot reject promise because it's null");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
resetMembers();
|
|
82
|
+
rejecter.reject(code, message, e);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public String getNameOfCallInProgress() {
|
|
86
|
+
return nameOfCallInProgress;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private void resetMembers() {
|
|
90
|
+
nameOfCallInProgress = null;
|
|
91
|
+
promise = null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
private void rejectPreviousPromiseBecauseNewOneIsInProgress(Promise promise, String requestedOperation) {
|
|
96
|
+
// TODO better message
|
|
97
|
+
promise.reject(ASYNC_OP_IN_PROGRESS, "Warning: previous promise did not settle and was overwritten. " +
|
|
98
|
+
"You've called \"" + requestedOperation + "\" while \"" + getNameOfCallInProgress() + "\" was already in progress and has not completed yet.");
|
|
99
|
+
}
|
|
100
|
+
private void rejectNewPromiseBecauseOldOneIsInProgress(Promise promise, String requestedOperation) {
|
|
101
|
+
// TODO better message
|
|
102
|
+
promise.reject(ASYNC_OP_IN_PROGRESS, "Warning: previous promise did not settle and you attempted to overwrite it. " +
|
|
103
|
+
"You've called \"" + requestedOperation + "\" while \"" + getNameOfCallInProgress() + "\" was already in progress and has not completed yet.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
package com.reactnativedocumentpicker
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Activity
|
|
5
|
+
import android.content.ActivityNotFoundException
|
|
6
|
+
import android.content.ClipData
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.provider.DocumentsContract
|
|
11
|
+
import android.util.Base64
|
|
12
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
13
|
+
import com.facebook.react.bridge.Arguments
|
|
14
|
+
import com.facebook.react.bridge.BaseActivityEventListener
|
|
15
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
16
|
+
import com.facebook.react.bridge.Promise
|
|
17
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
18
|
+
import com.facebook.react.bridge.ReactMethod
|
|
19
|
+
import com.facebook.react.bridge.ReadableArray
|
|
20
|
+
import com.facebook.react.bridge.ReadableMap
|
|
21
|
+
import com.facebook.react.bridge.WritableMap
|
|
22
|
+
import kotlinx.coroutines.CoroutineScope
|
|
23
|
+
import kotlinx.coroutines.Dispatchers
|
|
24
|
+
import kotlinx.coroutines.cancel
|
|
25
|
+
import kotlinx.coroutines.launch
|
|
26
|
+
|
|
27
|
+
class RNDocumentPickerModule(reactContext: ReactApplicationContext) :
|
|
28
|
+
NativeDocumentPickerSpec(reactContext), LifecycleEventListener {
|
|
29
|
+
private var currentPickOptions: PickOptions? = null
|
|
30
|
+
private var currentUriOfFileBeingExported: Uri? = null
|
|
31
|
+
private val promiseWrapper = PromiseWrapper(NAME)
|
|
32
|
+
private val pickedFilesUriMap = mutableMapOf<String, Uri>()
|
|
33
|
+
private val metadataGetter = MetadataGetter(pickedFilesUriMap)
|
|
34
|
+
private val fileOps = FileOperations(pickedFilesUriMap)
|
|
35
|
+
private val fileCopyingCoroutine = CoroutineScope(Dispatchers.IO)
|
|
36
|
+
|
|
37
|
+
private val activityEventListener: ActivityEventListener =
|
|
38
|
+
object : BaseActivityEventListener() {
|
|
39
|
+
override fun onActivityResult(
|
|
40
|
+
activity: Activity,
|
|
41
|
+
requestCode: Int,
|
|
42
|
+
resultCode: Int,
|
|
43
|
+
data: Intent?
|
|
44
|
+
) {
|
|
45
|
+
if (requestCode != PICK_FILES_REQUEST_CODE && requestCode != PICK_DIR_REQUEST_CODE && requestCode != SAVE_DOC_REQUEST_CODE) {
|
|
46
|
+
// we only handle the document picker library request codes
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
when (resultCode) {
|
|
51
|
+
Activity.RESULT_CANCELED -> promiseWrapper.rejectAsUserCancelledOperation()
|
|
52
|
+
(Activity.RESULT_OK) -> {
|
|
53
|
+
if (data == null) {
|
|
54
|
+
promiseWrapper.reject(E_INVALID_DATA_RETURNED, "Data from document picker is null")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
when (requestCode) {
|
|
58
|
+
PICK_FILES_REQUEST_CODE -> processFilePickerResult(data)
|
|
59
|
+
PICK_DIR_REQUEST_CODE -> processDirectoryPickerResult(data)
|
|
60
|
+
SAVE_DOC_REQUEST_CODE -> processSaveAsResult(data)
|
|
61
|
+
else -> promiseWrapper.reject(
|
|
62
|
+
"UNEXPECTED_ACTIVITY_RESULT", "Unknown activity result: $resultCode", null)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else ->
|
|
66
|
+
promiseWrapper.reject(
|
|
67
|
+
"UNEXPECTED_ACTIVITY_RESULT", "Unknown activity result: $resultCode", null)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
init {
|
|
73
|
+
reactContext.addActivityEventListener(activityEventListener)
|
|
74
|
+
reactContext.addLifecycleEventListener(this)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun invalidate() {
|
|
78
|
+
reactApplicationContext.removeActivityEventListener(activityEventListener)
|
|
79
|
+
// TODO verify this should be done (and order)
|
|
80
|
+
// reactApplicationContext.removeLifecycleEventListener(this)
|
|
81
|
+
super.invalidate()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@ReactMethod
|
|
85
|
+
override fun pick(opts: ReadableMap, promise: Promise) {
|
|
86
|
+
val currentActivity = currentActivity
|
|
87
|
+
|
|
88
|
+
if (currentActivity == null) {
|
|
89
|
+
rejectWithNullActivity(promise)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (!promiseWrapper.trySetPromiseRejectingIncoming(promise, "pick")) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
val options = parsePickOptions(opts)
|
|
96
|
+
currentPickOptions = options
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
val intent = IntentFactory.getPickIntent(options)
|
|
100
|
+
currentActivity.startActivityForResult(intent, PICK_FILES_REQUEST_CODE)
|
|
101
|
+
} catch (e: ActivityNotFoundException) {
|
|
102
|
+
promise.reject(UNABLE_TO_OPEN_FILE_TYPE, e)
|
|
103
|
+
} catch (e: Exception) {
|
|
104
|
+
promise.reject(E_OTHER_PRESENTING_ERROR, e)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun saveDocument(options: ReadableMap, promise: Promise) {
|
|
109
|
+
val currentActivity = currentActivity
|
|
110
|
+
if (currentActivity == null) {
|
|
111
|
+
rejectWithNullActivity(promise)
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
if (!promiseWrapper.trySetPromiseRejectingIncoming(promise, "saveDocuments")) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
val uri = Uri.parse(options.getArray("sourceUris")!!.getString(0))
|
|
120
|
+
currentUriOfFileBeingExported = uri
|
|
121
|
+
|
|
122
|
+
val mimeType = if (options.hasKey("mimeType")) options.getString("mimeType") else {
|
|
123
|
+
val contentResolver = reactApplicationContext.contentResolver
|
|
124
|
+
contentResolver.getType(uri) ?: throw IllegalStateException("MIME type could not be determined from the URI")
|
|
125
|
+
}
|
|
126
|
+
val suggestedTitle = if (options.hasKey("fileName")) options.getString("fileName") else null
|
|
127
|
+
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
|
128
|
+
addCategory(Intent.CATEGORY_OPENABLE)
|
|
129
|
+
type = mimeType
|
|
130
|
+
suggestedTitle?.let { putExtra(Intent.EXTRA_TITLE, it) }
|
|
131
|
+
|
|
132
|
+
// Optionally, specify a URI for the directory that should be opened in
|
|
133
|
+
// the system file picker before your app creates the document.
|
|
134
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.hasKey("initialUri")) {
|
|
135
|
+
putExtra(DocumentsContract.EXTRA_INITIAL_URI, options.getString("initialUri"))
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
currentActivity.startActivityForResult(intent, SAVE_DOC_REQUEST_CODE)
|
|
139
|
+
} catch (e: ActivityNotFoundException) {
|
|
140
|
+
promise.reject(UNABLE_TO_OPEN_FILE_TYPE, e)
|
|
141
|
+
} catch (e: Exception) {
|
|
142
|
+
promise.reject(E_OTHER_PRESENTING_ERROR, e)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@ReactMethod
|
|
147
|
+
override fun pickDirectory(opts: ReadableMap, promise: Promise) {
|
|
148
|
+
val currentActivity = currentActivity
|
|
149
|
+
if (currentActivity == null) {
|
|
150
|
+
rejectWithNullActivity(promise)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (!promiseWrapper.trySetPromiseRejectingIncoming(promise, "pickDirectory")) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
val options = parsePickOptions(opts)
|
|
157
|
+
currentPickOptions = options
|
|
158
|
+
try {
|
|
159
|
+
val intent =
|
|
160
|
+
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
|
161
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
|
162
|
+
options.initialDirectoryUrl != null) {
|
|
163
|
+
putExtra(
|
|
164
|
+
// TODO must be URI
|
|
165
|
+
DocumentsContract.EXTRA_INITIAL_URI,
|
|
166
|
+
options.initialDirectoryUrl)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// TODO option for extra task on stack?
|
|
170
|
+
currentActivity.startActivityForResult(intent, PICK_DIR_REQUEST_CODE)
|
|
171
|
+
} catch (e: ActivityNotFoundException) {
|
|
172
|
+
promise.reject(UNABLE_TO_OPEN_FILE_TYPE, e)
|
|
173
|
+
} catch (e: Exception) {
|
|
174
|
+
promise.reject(E_OTHER_PRESENTING_ERROR, e)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@ReactMethod
|
|
179
|
+
override fun keepLocalCopy(options: ReadableMap, promise: Promise) {
|
|
180
|
+
val filesToCopy = options.getArray("files")
|
|
181
|
+
val copyTo = options.getString("destination")
|
|
182
|
+
if (copyTo == null || filesToCopy == null) {
|
|
183
|
+
promise.reject("keepLocalCopy",
|
|
184
|
+
"You did not provide the correct options. Expected 'files' and 'destination', got: ${options.toHashMap().keys}"
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
fileCopyingCoroutine.launch {
|
|
188
|
+
val results = fileOps.copyFilesToLocalStorage(
|
|
189
|
+
reactApplicationContext,
|
|
190
|
+
filesToCopy,
|
|
191
|
+
CopyDestination.fromPath(copyTo),
|
|
192
|
+
)
|
|
193
|
+
promise.resolve(results)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
override fun isKnownType(kind: String, value: String): WritableMap {
|
|
199
|
+
return IsKnownTypeImpl.isKnownType(kind, value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
override fun releaseSecureAccess(uris: ReadableArray, promise: Promise) {
|
|
203
|
+
promise.resolve(null)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
override fun releaseLongTermAccess(uris: ReadableArray, promise: Promise) {
|
|
207
|
+
val contentResolver = reactApplicationContext.contentResolver
|
|
208
|
+
val results = Arguments.createArray()
|
|
209
|
+
for (i in 0 until uris.size()) {
|
|
210
|
+
val uriString = uris.getString(i)
|
|
211
|
+
val result = Arguments.createMap().apply {
|
|
212
|
+
putString("uri", uriString)
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
val uri = Uri.parse(uriString)
|
|
216
|
+
contentResolver.releasePersistableUriPermission(
|
|
217
|
+
uri,
|
|
218
|
+
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
219
|
+
)
|
|
220
|
+
// TODO clarify if this should be done
|
|
221
|
+
// pickedFilesUriMap.remove(uriString)
|
|
222
|
+
result.putString("status", "success")
|
|
223
|
+
} catch (e: Exception) {
|
|
224
|
+
result.putString("status", "error")
|
|
225
|
+
result.putString("errorMessage", e.message ?: "Unknown error")
|
|
226
|
+
}
|
|
227
|
+
results.pushMap(result)
|
|
228
|
+
}
|
|
229
|
+
promise.resolve(results)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@SuppressLint("WrongConstant") // in takePersistableUriPermission
|
|
233
|
+
private fun processDirectoryPickerResult(intent: Intent) {
|
|
234
|
+
val uri: Uri? = intent.data
|
|
235
|
+
val pickOptions = currentPickOptions
|
|
236
|
+
if (uri == null || pickOptions == null) {
|
|
237
|
+
promiseWrapper.reject(E_INVALID_DATA_RETURNED, "Data from document picker is null")
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
val map = Arguments.createMap().apply {
|
|
242
|
+
putString("uri", uri.toString())
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (pickOptions.requestLongTermAccess) {
|
|
246
|
+
// https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
|
|
247
|
+
// checking FLAG_GRANT_PERSISTABLE_URI_PERMISSION is not mentioned in the official docs
|
|
248
|
+
val takeFlags =
|
|
249
|
+
intent.flags and
|
|
250
|
+
(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
251
|
+
|
|
252
|
+
// TODO detect whether we have read or write permissions
|
|
253
|
+
// TODO use metadataBuilder too?
|
|
254
|
+
try {
|
|
255
|
+
reactApplicationContext.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
|
256
|
+
val encodedBookmark =
|
|
257
|
+
Base64.encodeToString(uri.toString().toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
|
258
|
+
map.putString("status", "success")
|
|
259
|
+
map.putString("bookmark", encodedBookmark)
|
|
260
|
+
} catch (e: Exception) {
|
|
261
|
+
val error =
|
|
262
|
+
e.localizedMessage ?: e.message ?: "Unknown error with takePersistableUriPermission"
|
|
263
|
+
map.putString("status", "error")
|
|
264
|
+
map.putString("bookmarkError", error)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
promiseWrapper.resolve(map)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override fun writeDocuments(options: ReadableMap, promise: Promise) {
|
|
271
|
+
fileCopyingCoroutine.launch {
|
|
272
|
+
try {
|
|
273
|
+
val targetUriString = if (options.hasKey("uri")) options.getString("uri") else null
|
|
274
|
+
|
|
275
|
+
val metadataBuilder = fileOps.writeDocumentImpl(currentUriOfFileBeingExported, targetUriString, reactApplicationContext)
|
|
276
|
+
metadataGetter.queryContentResolverMetadata(reactApplicationContext.contentResolver, metadataBuilder, reactApplicationContext)
|
|
277
|
+
|
|
278
|
+
val arrayWithSingleResult = Arguments.createArray().apply {
|
|
279
|
+
val resultMap = metadataBuilder.build()
|
|
280
|
+
pushMap(resultMap)
|
|
281
|
+
}
|
|
282
|
+
promise.resolve(arrayWithSingleResult)
|
|
283
|
+
} catch (e: Exception) {
|
|
284
|
+
promise.reject(e)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private fun processSaveAsResult(intent: Intent) {
|
|
290
|
+
val targetUri: Uri? = intent.data
|
|
291
|
+
if (targetUri != null) {
|
|
292
|
+
pickedFilesUriMap[targetUri.toString()] = targetUri
|
|
293
|
+
val map = Arguments.createMap().apply {
|
|
294
|
+
putString("uri", targetUri.toString())
|
|
295
|
+
}
|
|
296
|
+
promiseWrapper.resolve(map)
|
|
297
|
+
} else {
|
|
298
|
+
promiseWrapper.reject(E_INVALID_DATA_RETURNED, "Data from document picker is null")
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fun processFilePickerResult(intent: Intent) {
|
|
303
|
+
val singleFileUri: Uri? = intent.data
|
|
304
|
+
val multiSelectClipData: ClipData? = intent.clipData
|
|
305
|
+
|
|
306
|
+
val uris: List<Uri> =
|
|
307
|
+
when {
|
|
308
|
+
multiSelectClipData != null && multiSelectClipData.itemCount > 0 -> {
|
|
309
|
+
// multiple files selected
|
|
310
|
+
(0 until multiSelectClipData.itemCount).map { index ->
|
|
311
|
+
multiSelectClipData.getItemAt(index).uri
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
singleFileUri != null -> listOf(singleFileUri)
|
|
315
|
+
else -> emptyList()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
CoroutineScope(Dispatchers.IO).launch {
|
|
319
|
+
try {
|
|
320
|
+
val pickOptions = currentPickOptions
|
|
321
|
+
require(pickOptions != null)
|
|
322
|
+
val results =
|
|
323
|
+
metadataGetter.processPickedFileUris(reactApplicationContext, uris, pickOptions)
|
|
324
|
+
promiseWrapper.resolve(results)
|
|
325
|
+
} catch (e: Exception) {
|
|
326
|
+
promiseWrapper.reject(e)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
companion object {
|
|
332
|
+
fun rejectWithNullActivity(promise: Promise) {
|
|
333
|
+
promise.reject(PRESENTER_IS_NULL, PRESENTER_IS_NULL)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private const val PICK_FILES_REQUEST_CODE = 41
|
|
337
|
+
private const val PICK_DIR_REQUEST_CODE = 42
|
|
338
|
+
private const val SAVE_DOC_REQUEST_CODE = 43
|
|
339
|
+
private const val PRESENTER_IS_NULL = "NULL_PRESENTER"
|
|
340
|
+
private const val UNABLE_TO_OPEN_FILE_TYPE = "UNABLE_TO_OPEN_FILE_TYPE"
|
|
341
|
+
private const val E_OTHER_PRESENTING_ERROR = "OTHER_PRESENTING_ERROR"
|
|
342
|
+
private const val E_INVALID_DATA_RETURNED = "INVALID_DATA_RETURNED"
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
override fun onHostResume() {}
|
|
346
|
+
|
|
347
|
+
override fun onHostPause() {}
|
|
348
|
+
|
|
349
|
+
override fun onHostDestroy() {
|
|
350
|
+
fileCopyingCoroutine.cancel("host destroyed")
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
package com.reactnativedocumentpicker;
|
|
3
|
+
|
|
4
|
+
import androidx.annotation.NonNull;
|
|
5
|
+
import androidx.annotation.Nullable;
|
|
6
|
+
|
|
7
|
+
import com.facebook.react.TurboReactPackage;
|
|
8
|
+
import com.facebook.react.bridge.NativeModule;
|
|
9
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
10
|
+
import com.facebook.react.module.model.ReactModuleInfo;
|
|
11
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
12
|
+
|
|
13
|
+
import java.util.HashMap;
|
|
14
|
+
import java.util.Map;
|
|
15
|
+
|
|
16
|
+
public class RNDocumentPickerPackage extends TurboReactPackage {
|
|
17
|
+
|
|
18
|
+
@Nullable
|
|
19
|
+
@Override
|
|
20
|
+
public NativeModule getModule(String name, @NonNull ReactApplicationContext reactContext) {
|
|
21
|
+
if (name.equals(RNDocumentPickerModule.NAME)) {
|
|
22
|
+
return new RNDocumentPickerModule(reactContext);
|
|
23
|
+
} else {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@Override
|
|
29
|
+
public ReactModuleInfoProvider getReactModuleInfoProvider() {
|
|
30
|
+
return () -> {
|
|
31
|
+
boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
|
32
|
+
final Map<String, ReactModuleInfo> moduleInfos = new HashMap<>();
|
|
33
|
+
moduleInfos.put(
|
|
34
|
+
RNDocumentPickerModule.NAME,
|
|
35
|
+
// deprecated in RN 0.73
|
|
36
|
+
new ReactModuleInfo(
|
|
37
|
+
RNDocumentPickerModule.NAME,
|
|
38
|
+
RNDocumentPickerModule.NAME,
|
|
39
|
+
// "DocumentPickerModule",
|
|
40
|
+
false, // canOverrideExistingModule
|
|
41
|
+
false, // needsEagerInit
|
|
42
|
+
false, // hasConstants
|
|
43
|
+
false, // isCxxModule
|
|
44
|
+
isTurboModule // isTurboModule
|
|
45
|
+
));
|
|
46
|
+
return moduleInfos;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
|
|
4
|
+
*
|
|
5
|
+
* Then it was commited. It is here to support the old architecture.
|
|
6
|
+
* If you use the new architecture, this file won't be included and instead will be generated by the codegen.
|
|
7
|
+
*
|
|
8
|
+
* @generated by codegen project: GenerateModuleJavaSpec.js
|
|
9
|
+
*
|
|
10
|
+
* @nolint
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
package com.reactnativedocumentpicker;
|
|
14
|
+
|
|
15
|
+
import com.facebook.proguard.annotations.DoNotStrip;
|
|
16
|
+
import com.facebook.react.bridge.Promise;
|
|
17
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
18
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
19
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
20
|
+
import com.facebook.react.bridge.ReadableArray;
|
|
21
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
22
|
+
import com.facebook.react.bridge.WritableMap;
|
|
23
|
+
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
|
|
24
|
+
import javax.annotation.Nonnull;
|
|
25
|
+
|
|
26
|
+
public abstract class NativeDocumentPickerSpec extends ReactContextBaseJavaModule implements TurboModule {
|
|
27
|
+
public static final String NAME = "RNDocumentPicker";
|
|
28
|
+
|
|
29
|
+
public NativeDocumentPickerSpec(ReactApplicationContext reactContext) {
|
|
30
|
+
super(reactContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Override
|
|
34
|
+
public @Nonnull String getName() {
|
|
35
|
+
return NAME;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@ReactMethod
|
|
39
|
+
@DoNotStrip
|
|
40
|
+
public abstract void pick(ReadableMap options, Promise promise);
|
|
41
|
+
|
|
42
|
+
@ReactMethod
|
|
43
|
+
@DoNotStrip
|
|
44
|
+
public abstract void saveDocument(ReadableMap options, Promise promise);
|
|
45
|
+
|
|
46
|
+
@ReactMethod
|
|
47
|
+
@DoNotStrip
|
|
48
|
+
public abstract void writeDocuments(ReadableMap options, Promise promise);
|
|
49
|
+
|
|
50
|
+
@ReactMethod
|
|
51
|
+
@DoNotStrip
|
|
52
|
+
public abstract void pickDirectory(ReadableMap options, Promise promise);
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
@DoNotStrip
|
|
56
|
+
public abstract void keepLocalCopy(ReadableMap options, Promise promise);
|
|
57
|
+
|
|
58
|
+
@ReactMethod(isBlockingSynchronousMethod = true)
|
|
59
|
+
@DoNotStrip
|
|
60
|
+
public abstract WritableMap isKnownType(String kind, String value);
|
|
61
|
+
|
|
62
|
+
@ReactMethod
|
|
63
|
+
@DoNotStrip
|
|
64
|
+
public abstract void releaseSecureAccess(ReadableArray uris, Promise promise);
|
|
65
|
+
|
|
66
|
+
@ReactMethod
|
|
67
|
+
@DoNotStrip
|
|
68
|
+
public abstract void releaseLongTermAccess(ReadableArray uris, Promise promise);
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#import "RCTConvert+RNDocumentPicker.h"
|
|
2
|
+
|
|
3
|
+
@implementation RCTConvert (RNDocumentPicker)
|
|
4
|
+
|
|
5
|
+
RCT_ENUM_CONVERTER(
|
|
6
|
+
UIModalTransitionStyle,
|
|
7
|
+
(@{
|
|
8
|
+
@"coverVertical" : @(UIModalTransitionStyleCoverVertical),
|
|
9
|
+
@"flipHorizontal" : @(UIModalTransitionStyleFlipHorizontal),
|
|
10
|
+
@"crossDissolve" : @(UIModalTransitionStyleCrossDissolve),
|
|
11
|
+
@"partialCurl" : @(UIModalTransitionStylePartialCurl),
|
|
12
|
+
}),
|
|
13
|
+
UIModalTransitionStyleCoverVertical,
|
|
14
|
+
integerValue)
|
|
15
|
+
|
|
16
|
+
@end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// LICENSE: see License.md in the package root
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
5
|
+
#import <rndocumentpickerCGen/rndocumentpickerCGen.h>
|
|
6
|
+
#else
|
|
7
|
+
#import <React/RCTBridgeModule.h>
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
@interface RNDocumentPicker : NSObject <
|
|
11
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
12
|
+
NativeDocumentPickerSpec
|
|
13
|
+
#else
|
|
14
|
+
RCTBridgeModule
|
|
15
|
+
#endif
|
|
16
|
+
>
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@end
|