@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.
Files changed (139) hide show
  1. package/LICENSE.md +21 -0
  2. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  4. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  5. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  6. package/android/.gradle/8.9/gc.properties +0 -0
  7. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  8. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  9. package/android/.gradle/vcs-1/gc.properties +0 -0
  10. package/android/build.gradle +80 -0
  11. package/android/src/main/AndroidManifest.xml +3 -0
  12. package/android/src/main/java/com/reactnativedocumentpicker/CopyDestination.kt +12 -0
  13. package/android/src/main/java/com/reactnativedocumentpicker/DocumentMetadataBuilder.kt +79 -0
  14. package/android/src/main/java/com/reactnativedocumentpicker/FileOperations.kt +203 -0
  15. package/android/src/main/java/com/reactnativedocumentpicker/IntentFactory.kt +36 -0
  16. package/android/src/main/java/com/reactnativedocumentpicker/IsKnownTypeImpl.kt +40 -0
  17. package/android/src/main/java/com/reactnativedocumentpicker/MetadataGetter.kt +150 -0
  18. package/android/src/main/java/com/reactnativedocumentpicker/PickOptions.kt +63 -0
  19. package/android/src/main/java/com/reactnativedocumentpicker/PromiseWrapper.java +105 -0
  20. package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerModule.kt +352 -0
  21. package/android/src/main/java/com/reactnativedocumentpicker/RNDocumentPickerPackage.java +49 -0
  22. package/android/src/paper/java/com/reactnativedocumentpicker/NativeDocumentPickerSpec.java +69 -0
  23. package/ios/RCTConvert+RNDocumentPicker.h +8 -0
  24. package/ios/RCTConvert+RNDocumentPicker.mm +16 -0
  25. package/ios/RNDocumentPicker.h +19 -0
  26. package/ios/RNDocumentPicker.mm +128 -0
  27. package/ios/swift/DocPicker.swift +84 -0
  28. package/ios/swift/DocSaver.swift +41 -0
  29. package/ios/swift/DocumentMetadataBuilder.swift +69 -0
  30. package/ios/swift/FileOperations.swift +68 -0
  31. package/ios/swift/IsKnownTypeImpl.swift +42 -0
  32. package/ios/swift/LocalCopyResponse.swift +27 -0
  33. package/ios/swift/PickerBase.swift +78 -0
  34. package/ios/swift/PickerOptions.swift +44 -0
  35. package/ios/swift/PromiseSupport.swift +2 -0
  36. package/ios/swift/PromiseWrapper.swift +92 -0
  37. package/ios/swift/SaverOptions.swift +30 -0
  38. package/jest/build/jest/setup.js +70 -0
  39. package/jest/build/src/errors.js +47 -0
  40. package/jest/build/src/fileTypes.js +53 -0
  41. package/jest/build/src/index.js +22 -0
  42. package/jest/build/src/isKnownType.js +16 -0
  43. package/jest/build/src/keepLocalCopy.js +17 -0
  44. package/jest/build/src/pick.js +50 -0
  45. package/jest/build/src/pickDirectory.js +31 -0
  46. package/jest/build/src/release.js +22 -0
  47. package/jest/build/src/saveDocuments.js +40 -0
  48. package/jest/build/src/spec/NativeDocumentPicker.js +5 -0
  49. package/jest/build/src/types.js +4 -0
  50. package/jest/build/src/validateTypes.js +23 -0
  51. package/jest/build/tsconfig.tsbuildinfo +1 -0
  52. package/lib/commonjs/errors.js +53 -0
  53. package/lib/commonjs/errors.js.map +1 -0
  54. package/lib/commonjs/fileTypes.js +84 -0
  55. package/lib/commonjs/fileTypes.js.map +1 -0
  56. package/lib/commonjs/index.js +74 -0
  57. package/lib/commonjs/index.js.map +1 -0
  58. package/lib/commonjs/isKnownType.js +27 -0
  59. package/lib/commonjs/isKnownType.js.map +1 -0
  60. package/lib/commonjs/keepLocalCopy.js +34 -0
  61. package/lib/commonjs/keepLocalCopy.js.map +1 -0
  62. package/lib/commonjs/package.json +1 -0
  63. package/lib/commonjs/pick.js +93 -0
  64. package/lib/commonjs/pick.js.map +1 -0
  65. package/lib/commonjs/pickDirectory.js +71 -0
  66. package/lib/commonjs/pickDirectory.js.map +1 -0
  67. package/lib/commonjs/release.js +31 -0
  68. package/lib/commonjs/release.js.map +1 -0
  69. package/lib/commonjs/saveDocuments.js +55 -0
  70. package/lib/commonjs/saveDocuments.js.map +1 -0
  71. package/lib/commonjs/spec/NativeDocumentPicker.js +16 -0
  72. package/lib/commonjs/spec/NativeDocumentPicker.js.map +1 -0
  73. package/lib/commonjs/types.js +37 -0
  74. package/lib/commonjs/types.js.map +1 -0
  75. package/lib/commonjs/validateTypes.js +29 -0
  76. package/lib/commonjs/validateTypes.js.map +1 -0
  77. package/lib/module/errors.js +48 -0
  78. package/lib/module/errors.js.map +1 -0
  79. package/lib/module/fileTypes.js +81 -0
  80. package/lib/module/fileTypes.js.map +1 -0
  81. package/lib/module/index.js +13 -0
  82. package/lib/module/index.js.map +1 -0
  83. package/lib/module/isKnownType.js +24 -0
  84. package/lib/module/isKnownType.js.map +1 -0
  85. package/lib/module/keepLocalCopy.js +31 -0
  86. package/lib/module/keepLocalCopy.js.map +1 -0
  87. package/lib/module/package.json +1 -0
  88. package/lib/module/pick.js +90 -0
  89. package/lib/module/pick.js.map +1 -0
  90. package/lib/module/pickDirectory.js +68 -0
  91. package/lib/module/pickDirectory.js.map +1 -0
  92. package/lib/module/release.js +26 -0
  93. package/lib/module/release.js.map +1 -0
  94. package/lib/module/saveDocuments.js +52 -0
  95. package/lib/module/saveDocuments.js.map +1 -0
  96. package/lib/module/spec/NativeDocumentPicker.js +13 -0
  97. package/lib/module/spec/NativeDocumentPicker.js.map +1 -0
  98. package/lib/module/types.js +33 -0
  99. package/lib/module/types.js.map +1 -0
  100. package/lib/module/validateTypes.js +24 -0
  101. package/lib/module/validateTypes.js.map +1 -0
  102. package/lib/typescript/errors.d.ts +40 -0
  103. package/lib/typescript/errors.d.ts.map +1 -0
  104. package/lib/typescript/fileTypes.d.ts +94 -0
  105. package/lib/typescript/fileTypes.d.ts.map +1 -0
  106. package/lib/typescript/index.d.ts +13 -0
  107. package/lib/typescript/index.d.ts.map +1 -0
  108. package/lib/typescript/isKnownType.d.ts +41 -0
  109. package/lib/typescript/isKnownType.d.ts.map +1 -0
  110. package/lib/typescript/keepLocalCopy.d.ts +46 -0
  111. package/lib/typescript/keepLocalCopy.d.ts.map +1 -0
  112. package/lib/typescript/pick.d.ts +84 -0
  113. package/lib/typescript/pick.d.ts.map +1 -0
  114. package/lib/typescript/pickDirectory.d.ts +62 -0
  115. package/lib/typescript/pickDirectory.d.ts.map +1 -0
  116. package/lib/typescript/release.d.ts +24 -0
  117. package/lib/typescript/release.d.ts.map +1 -0
  118. package/lib/typescript/saveDocuments.d.ts +55 -0
  119. package/lib/typescript/saveDocuments.d.ts.map +1 -0
  120. package/lib/typescript/spec/NativeDocumentPicker.d.ts +29 -0
  121. package/lib/typescript/spec/NativeDocumentPicker.d.ts.map +1 -0
  122. package/lib/typescript/types.d.ts +95 -0
  123. package/lib/typescript/types.d.ts.map +1 -0
  124. package/lib/typescript/validateTypes.d.ts +3 -0
  125. package/lib/typescript/validateTypes.d.ts.map +1 -0
  126. package/package.json +92 -0
  127. package/react-native-document-picker.podspec +30 -0
  128. package/src/errors.ts +49 -0
  129. package/src/fileTypes.ts +92 -0
  130. package/src/index.ts +47 -0
  131. package/src/isKnownType.ts +48 -0
  132. package/src/keepLocalCopy.ts +51 -0
  133. package/src/pick.ts +151 -0
  134. package/src/pickDirectory.ts +93 -0
  135. package/src/release.ts +36 -0
  136. package/src/saveDocuments.ts +99 -0
  137. package/src/spec/NativeDocumentPicker.ts +31 -0
  138. package/src/types.ts +119 -0
  139. 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,8 @@
1
+ #import <React/RCTConvert.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ @interface RCTConvert(RNDocumentPicker)
5
+
6
+ + (UIModalTransitionStyle)UIModalTransitionStyle:(NSString*)value;
7
+
8
+ @end
@@ -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