@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
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Vojtech Novak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
File without changes
@@ -0,0 +1,2 @@
1
+ #Sun Jan 12 23:27:41 PST 2025
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,80 @@
1
+ buildscript {
2
+ // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["DocumentPicker_kotlinVersion"]
4
+
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+
10
+ dependencies {
11
+ // noinspection DifferentKotlinGradleVersion
12
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13
+ }
14
+ }
15
+
16
+ def getExtOrIntegerDefault(name) {
17
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['ReactNativeDocumentPicker_' + name]).toInteger()
18
+ }
19
+
20
+ def isNewArchitectureEnabled() {
21
+ // To opt-in for the New Architecture, you can either:
22
+ // - Set `newArchEnabled` to true inside the `gradle.properties` file
23
+ // - Invoke gradle with `-newArchEnabled=true`
24
+ // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
25
+ return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
26
+ }
27
+
28
+ apply plugin: 'com.android.library'
29
+ apply plugin: "kotlin-android"
30
+
31
+ if (isNewArchitectureEnabled()) {
32
+ apply plugin: "com.facebook.react"
33
+ }
34
+
35
+
36
+ android {
37
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
38
+ if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
39
+ namespace "com.reactnativedocumentpicker"
40
+ }
41
+ compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
42
+
43
+ // Used to override the NDK path/version on internal CI or by allowing
44
+ // users to customize the NDK path/version from their root project (e.g. for M1 support)
45
+ if (rootProject.hasProperty("ndkPath")) {
46
+ ndkPath rootProject.ext.ndkPath
47
+ }
48
+ if (rootProject.hasProperty("ndkVersion")) {
49
+ ndkVersion rootProject.ext.ndkVersion
50
+ }
51
+
52
+ defaultConfig {
53
+ minSdkVersion getExtOrIntegerDefault('minSdkVersion')
54
+ targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
55
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
56
+ }
57
+
58
+ sourceSets.main {
59
+ java {
60
+ if (!isNewArchitectureEnabled()) {
61
+ srcDirs += 'src/paper/java'
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ google()
69
+ mavenLocal()
70
+ mavenCentral()
71
+ }
72
+
73
+ def kotlin_version = getExtOrIntegerDefault("kotlinVersion")
74
+
75
+ dependencies {
76
+ //noinspection GradleDynamicVersion
77
+ implementation 'com.facebook.react:react-native:+' // from node_modules
78
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
79
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
80
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
+ package="com.reactnativedocumentpicker">
3
+ </manifest>
@@ -0,0 +1,12 @@
1
+ // LICENSE: see License.md in the package root
2
+ package com.reactnativedocumentpicker
3
+
4
+ enum class CopyDestination(val preset: String) {
5
+ CACHES_DIRECTORY("cachesDirectory"),
6
+ DOCUMENT_DIRECTORY("documentDirectory");
7
+
8
+ companion object {
9
+ // keep values() for RN 73 compatibility
10
+ fun fromPath(path: String): CopyDestination = values().find { it.preset == path } ?: CACHES_DIRECTORY
11
+ }
12
+ }
@@ -0,0 +1,79 @@
1
+ // LICENSE: see License.md in the package root
2
+ package com.reactnativedocumentpicker
3
+
4
+ import android.net.Uri
5
+ import android.util.Base64
6
+ import android.webkit.MimeTypeMap
7
+ import com.facebook.react.bridge.Arguments
8
+ import com.facebook.react.bridge.ReadableMap
9
+
10
+ class DocumentMetadataBuilder(forUri: Uri) {
11
+ private val uri: Uri = forUri
12
+ private var name: String? = null
13
+ private var size: Long? = null
14
+ private var mimeType: String? = null
15
+ private var metadataError: String? = null
16
+ private var openableMimeTypes: Array<String>? = null
17
+ private var bookmark: String? = null
18
+ private var bookmarkError: String? = null
19
+ private var virtual: Boolean? = null
20
+
21
+ fun name(name: String?) = apply { this.name = name }
22
+
23
+ fun size(size: Long?) = apply { this.size = size }
24
+
25
+ fun mimeType(mimeType: String?) = apply { this.mimeType = mimeType }
26
+
27
+ fun metadataReadingError(error: String?) = apply { this.metadataError = error }
28
+
29
+ fun openableMimeTypes(openableMimeTypes: Array<String>?) = apply {
30
+ this.openableMimeTypes = openableMimeTypes
31
+ }
32
+
33
+ fun bookmark(bookmark: Uri) = apply { this.bookmark = bookmark.toString() }
34
+
35
+ fun bookmarkError(bookmarkError: String?) = apply { this.bookmarkError = bookmarkError }
36
+
37
+ fun virtual(virtual: Boolean) = apply { this.virtual = virtual }
38
+
39
+ fun build(): ReadableMap = createReadableMap()
40
+
41
+ fun hasMime() = mimeType != null
42
+
43
+ fun getUri() = uri
44
+
45
+ private fun createReadableMap(): ReadableMap {
46
+ val map = Arguments.createMap()
47
+ map.putString("name", name)
48
+ map.putString("uri", uri.toString())
49
+ size?.let { map.putDouble("size", it.toDouble()) } ?: map.putNull("size")
50
+ map.putString("type", mimeType?.lowercase())
51
+ map.putString("nativeType", mimeType?.lowercase())
52
+ openableMimeTypes?.let {
53
+ val arrayOfExtensionsAndMime = Arguments.createArray()
54
+ it.forEach { mimeType ->
55
+ val virtualFileDetails = Arguments.createMap()
56
+ val maybeExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
57
+ virtualFileDetails.putString("mimeType", mimeType)
58
+ virtualFileDetails.putString("extension", maybeExtension)
59
+ arrayOfExtensionsAndMime.pushMap(virtualFileDetails)
60
+ }
61
+ map.putArray("convertibleToMimeTypes", arrayOfExtensionsAndMime)
62
+ } ?: map.putNull("convertibleToMimeTypes")
63
+ map.putString("error", metadataError)
64
+ virtual?.let { map.putBoolean("isVirtual", it) } ?: map.putNull("isVirtual")
65
+
66
+ bookmark?.let {
67
+ // we're encoding so that we behave the same as the iOS implementation
68
+ val encodedBookmark = Base64.encodeToString(it.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
69
+ map.putString("bookmarkStatus", "success")
70
+ map.putString("bookmark", encodedBookmark)
71
+ }
72
+ ?: bookmarkError?.let {
73
+ map.putString("bookmarkStatus", "error")
74
+ map.putString("bookmarkError", it)
75
+ }
76
+
77
+ return map
78
+ }
79
+ }
@@ -0,0 +1,203 @@
1
+ package com.reactnativedocumentpicker
2
+
3
+ import android.content.ContentResolver
4
+ import android.content.Context
5
+ import android.net.Uri
6
+ import com.facebook.react.bridge.Arguments
7
+ import com.facebook.react.bridge.ReactApplicationContext
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.bridge.ReadableArray
10
+ import com.facebook.react.bridge.ReadableMap
11
+ import com.facebook.react.util.RNLog
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.withContext
14
+ import kotlinx.coroutines.async
15
+ import kotlinx.coroutines.awaitAll
16
+ import java.io.File
17
+ import java.io.FileNotFoundException
18
+ import java.io.FileOutputStream
19
+ import java.io.IOException
20
+ import java.io.InputStream
21
+ import java.nio.channels.Channels
22
+ import java.util.UUID
23
+
24
+ class FileOperations(private val uriMap: MutableMap<String, Uri>) {
25
+ suspend fun copyFilesToLocalStorage(
26
+ context: ReactContext,
27
+ filesToCopy: ReadableArray,
28
+ copyTo: CopyDestination,
29
+ ): ReadableArray =
30
+ withContext(Dispatchers.IO) {
31
+ /**
32
+ * export type LocalCopyResponse = | { status: 'success'; localUri: string; sourceUri: string } |
33
+ * { status: 'error'; copyError: string; sourceUri: string }
34
+ */
35
+ val destinationDir = getUniqueDir(context, copyTo)
36
+
37
+ val copyJobs = (0 until filesToCopy.size()).map { i ->
38
+ async {
39
+ val oneResult = Arguments.createMap()
40
+ val map = filesToCopy.getMap(i)
41
+
42
+ try {
43
+ val newFile = copySingleFile(map, context, destinationDir)
44
+ oneResult.merge(newFile)
45
+ } catch (e: Exception) {
46
+ val message: String = e.localizedMessage ?: e.message ?: "Unknown error"
47
+ oneResult.putString("status", "error")
48
+ oneResult.putString("copyError", message)
49
+ oneResult.putString("sourceUri", map.getString("uri"))
50
+ }
51
+ return@async oneResult
52
+ }
53
+ }
54
+
55
+ val results = Arguments.createArray()
56
+ copyJobs.awaitAll().forEach { result ->
57
+ results.pushMap(result)
58
+ }
59
+
60
+ return@withContext results
61
+ }
62
+
63
+ private fun copySingleFile(
64
+ map: ReadableMap,
65
+ context: ReactContext,
66
+ destinationDir: File
67
+ ): ReadableMap {
68
+ val sourceUriAsString: String = map.getString("uri") ?: throw IllegalArgumentException("URI is missing")
69
+ val fileName: String = map.getString("fileName") ?: throw IllegalArgumentException("fileName is missing")
70
+ val convertVirtualFileAsType = map.getString("convertVirtualFileToType")
71
+
72
+ val sourceUriInstance = uriMap[sourceUriAsString]
73
+ if (sourceUriInstance == null) {
74
+ RNLog.w(
75
+ context,
76
+ // https://developer.android.com/guide/components/intents-common#GetFile
77
+ "keepLocalCopy: You're trying to copy a file \"$fileName\" that wasn't picked with this module. " +
78
+ "This can lead to permission errors because the file reference is transient to your activity's current lifecycle. See https://developer.android.com/guide/components/intents-common#GetFile . " +
79
+ "Please use the result from the picker directly.")
80
+ }
81
+
82
+ val copiedFile =
83
+ copyFile(
84
+ context,
85
+ sourceUriInstance ?: Uri.parse(sourceUriAsString),
86
+ destinationDir,
87
+ fileName,
88
+ convertVirtualFileAsType)
89
+
90
+ val singleFileCopy = Arguments.createMap()
91
+ singleFileCopy.putString("status", "success")
92
+ // NOTE this url-encodes the path, consistent with the iOS implementation and with the response of not-copied files
93
+ singleFileCopy.putString("localUri", Uri.fromFile(copiedFile).toString())
94
+ singleFileCopy.putString("sourceUri", sourceUriAsString)
95
+ return singleFileCopy
96
+ }
97
+
98
+ private fun getUniqueDir(context: Context, copyTo: CopyDestination): File {
99
+ val baseDir =
100
+ if (copyTo == CopyDestination.DOCUMENT_DIRECTORY) context.filesDir else context.cacheDir
101
+
102
+ val randomDir = File(baseDir, UUID.randomUUID().toString())
103
+ val didCreateDir = randomDir.mkdir()
104
+ if (!didCreateDir) {
105
+ throw IOException("Failed to create directory at ${randomDir.absolutePath}")
106
+ }
107
+ return randomDir
108
+ }
109
+
110
+ private fun copyFile(
111
+ context: Context,
112
+ from: Uri,
113
+ destinationDir: File,
114
+ fileName: String,
115
+ convertVirtualFileAsType: String?
116
+ ): File {
117
+ val attemptedDestFile = File(destinationDir, fileName)
118
+ val destFileSafe = safeGetDestination(attemptedDestFile, destinationDir)
119
+
120
+ val copyStreamToFile: (InputStream?) -> Unit = { inputStream ->
121
+ if (inputStream == null) {
122
+ throw FileNotFoundException("No input stream was found for the source file")
123
+ }
124
+ FileOutputStream(destFileSafe).channel.use { destinationFileChannel ->
125
+ val inputChannel = Channels.newChannel(inputStream)
126
+ val size = destinationFileChannel.transferFrom(inputChannel, 0, Long.MAX_VALUE)
127
+ if (size == 0L) {
128
+ throw IOException("No data was copied to the destination file")
129
+ }
130
+ }
131
+ }
132
+
133
+ if (convertVirtualFileAsType == null) {
134
+ context.contentResolver.openInputStream(from).use(copyStreamToFile)
135
+ } else {
136
+ getInputStreamForVirtualFile(context.contentResolver, from, convertVirtualFileAsType)
137
+ .use(copyStreamToFile)
138
+ }
139
+
140
+ return destFileSafe
141
+ }
142
+
143
+ private fun getInputStreamForVirtualFile(
144
+ contentResolver: ContentResolver,
145
+ from: Uri,
146
+ convertVirtualFileAsType: String
147
+ ): InputStream? {
148
+ return contentResolver
149
+ .openTypedAssetFileDescriptor(from, convertVirtualFileAsType, null)
150
+ ?.createInputStream()
151
+ }
152
+
153
+ private fun safeGetDestination(destFile: File, expectedDir: File): File {
154
+ val canonicalPath = destFile.canonicalPath
155
+ if (!canonicalPath.startsWith(expectedDir.canonicalPath)) {
156
+ throw IllegalArgumentException(
157
+ "The copied file is attempting to write outside of the target directory.")
158
+ }
159
+ return destFile
160
+ }
161
+
162
+ fun writeDocumentImpl(sourceUri: Uri?, targetUriString: String?, context: ReactApplicationContext): DocumentMetadataBuilder {
163
+ if (sourceUri == null) {
164
+ throw IllegalArgumentException("The source URI is null. Call saveDocument() before writeDocument()")
165
+ }
166
+ val targetUri: Uri? = uriMap[targetUriString]
167
+
168
+ if (targetUri == null) {
169
+ RNLog.e(
170
+ context,
171
+ "writeDocument: You're trying to write from Uri \"$targetUriString\" that wasn't picked with this module. " +
172
+ "Please use the result from saveDocument()")
173
+ throw IllegalArgumentException("The provided URI is not known")
174
+ }
175
+
176
+ val metadataBuilder = DocumentMetadataBuilder(targetUri)
177
+
178
+ val contentResolver = context.contentResolver
179
+ val mimeFromUri = contentResolver.getType(targetUri)
180
+ metadataBuilder.mimeType(mimeFromUri)
181
+
182
+ // TODO https://gist.github.com/vonovak/73affb1a5b904ee165d9b5981d8dfe9a
183
+ contentResolver.openInputStream(sourceUri).use { inputStream ->
184
+ if (inputStream == null) {
185
+ metadataBuilder.metadataReadingError("No output stream found for source file")
186
+ } else {
187
+ contentResolver.openOutputStream(targetUri).use { outputStream ->
188
+ if (outputStream == null) {
189
+ metadataBuilder.metadataReadingError("No output stream found for destination file")
190
+ } else {
191
+ val bytesCopied = inputStream.copyTo(outputStream)
192
+ if (bytesCopied == 0L) {
193
+ metadataBuilder.metadataReadingError("No data was copied to the destination file")
194
+ }
195
+ outputStream.flush()
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ return metadataBuilder
202
+ }
203
+ }
@@ -0,0 +1,36 @@
1
+ package com.reactnativedocumentpicker
2
+
3
+ import android.content.Intent
4
+ import android.os.Build
5
+ import android.provider.DocumentsContract
6
+
7
+ object IntentFactory {
8
+ fun getPickIntent(options: PickOptions): Intent {
9
+ // TODO option for extra task on stack?
10
+ // reminder - flags are for granting rights to others
11
+
12
+ return Intent(options.action).apply {
13
+ val types = options.mimeTypes
14
+
15
+ type =
16
+ if (types.size > 1) {
17
+ putExtra(Intent.EXTRA_MIME_TYPES, types)
18
+ options.intentFilterTypes
19
+ } else {
20
+ types[0]
21
+ }
22
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
23
+ options.initialDirectoryUrl != null
24
+ ) {
25
+ // only works for ACTION_OPEN_DOCUMENT
26
+ // TODO must be URI
27
+ putExtra(DocumentsContract.EXTRA_INITIAL_URI, options.initialDirectoryUrl)
28
+ }
29
+ if (!options.allowVirtualFiles) {
30
+ addCategory(Intent.CATEGORY_OPENABLE)
31
+ }
32
+ putExtra(Intent.EXTRA_LOCAL_ONLY, options.localOnly)
33
+ putExtra(Intent.EXTRA_ALLOW_MULTIPLE, options.multiple)
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,40 @@
1
+ package com.reactnativedocumentpicker
2
+
3
+ import android.webkit.MimeTypeMap
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.WritableMap
6
+
7
+ class IsKnownTypeImpl {
8
+ companion object {
9
+ fun isKnownType(kind: String, value: String): WritableMap {
10
+ return when (kind) {
11
+ "mimeType" -> {
12
+ val extensionForMime = MimeTypeMap.getSingleton().getExtensionFromMimeType(value)
13
+ createMap(
14
+ isKnown = extensionForMime != null,
15
+ preferredFilenameExtension = extensionForMime,
16
+ mimeType = if (extensionForMime != null) value else null
17
+ )
18
+ }
19
+ "extension" -> {
20
+ val mimeForExtension = MimeTypeMap.getSingleton().getMimeTypeFromExtension(value)
21
+ createMap(
22
+ isKnown = mimeForExtension != null,
23
+ preferredFilenameExtension = if (mimeForExtension != null) value else null,
24
+ mimeType = mimeForExtension
25
+ )
26
+ }
27
+ else -> createMap(isKnown = false, preferredFilenameExtension = null, mimeType = null)
28
+ }
29
+ }
30
+
31
+ private fun createMap(isKnown: Boolean, preferredFilenameExtension: String?, mimeType: String?): WritableMap {
32
+ return Arguments.createMap().apply {
33
+ putNull("UTType")
34
+ putBoolean("isKnown", isKnown)
35
+ putString("preferredFilenameExtension", preferredFilenameExtension)
36
+ putString("mimeType", mimeType)
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,150 @@
1
+ // LICENSE: see License.md in the package root
2
+ package com.reactnativedocumentpicker
3
+
4
+ import android.content.ContentResolver
5
+ import android.content.Context
6
+ import android.content.Intent
7
+ import android.database.Cursor
8
+ import android.net.Uri
9
+ import android.os.Build
10
+ import android.provider.DocumentsContract
11
+ import android.provider.OpenableColumns
12
+ import com.facebook.react.bridge.Arguments
13
+ import com.facebook.react.bridge.ReadableArray
14
+ import kotlinx.coroutines.Dispatchers
15
+ import kotlinx.coroutines.withContext
16
+
17
+ class MetadataGetter(private val uriMap: MutableMap<String, Uri>) {
18
+
19
+ suspend fun processPickedFileUris(
20
+ context: Context,
21
+ uris: List<Uri>,
22
+ pickOptions: PickOptions
23
+ ): ReadableArray =
24
+ withContext(Dispatchers.IO) {
25
+ val results = Arguments.createArray()
26
+ for (uri in uris) {
27
+ val metadata = getMetadataForUri(context, uri, pickOptions)
28
+ uriMap[uri.toString()] = uri
29
+ results.pushMap(metadata.build())
30
+ }
31
+ results
32
+ }
33
+
34
+ private suspend fun getMetadataForUri(
35
+ context: Context,
36
+ sourceUri: Uri,
37
+ pickOptions: PickOptions,
38
+ ): DocumentMetadataBuilder =
39
+ withContext(Dispatchers.IO) {
40
+ val contentResolver = context.contentResolver
41
+ val metadataBuilder = DocumentMetadataBuilder(sourceUri)
42
+
43
+ val mimeFromUri = contentResolver.getType(sourceUri)
44
+ metadataBuilder.mimeType(mimeFromUri)
45
+
46
+ if (pickOptions.allowVirtualFiles) {
47
+ // https://developer.android.com/training/data-storage/shared/documents-files#open-virtual-file
48
+ val openableMimeTypes = contentResolver.getStreamTypes(sourceUri, "*/*")
49
+ metadataBuilder.openableMimeTypes(openableMimeTypes)
50
+ }
51
+
52
+ if (pickOptions.requestLongTermAccess) {
53
+ // https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
54
+ // checking FLAG_GRANT_PERSISTABLE_URI_PERMISSION is not mentioned in the official docs
55
+ val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
56
+
57
+ try {
58
+ context.contentResolver.takePersistableUriPermission(sourceUri, takeFlags)
59
+ metadataBuilder.bookmark(sourceUri)
60
+ } catch (e: Exception) {
61
+ metadataBuilder.bookmarkError(
62
+ e.localizedMessage
63
+ ?: e.message
64
+ ?: "Unknown error with takePersistableUriPermission")
65
+ }
66
+ }
67
+
68
+ queryContentResolverMetadata(contentResolver, metadataBuilder, context)
69
+
70
+ metadataBuilder
71
+ }
72
+
73
+ fun queryContentResolverMetadata(
74
+ contentResolver: ContentResolver,
75
+ metadataBuilder: DocumentMetadataBuilder,
76
+ context: Context
77
+ ) {
78
+ val forUri = metadataBuilder.getUri()
79
+ contentResolver
80
+ .query(
81
+ forUri,
82
+ arrayOf(
83
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
84
+ OpenableColumns.DISPLAY_NAME,
85
+ OpenableColumns.SIZE,
86
+ DocumentsContract.Document.COLUMN_FLAGS,
87
+ ),
88
+ null,
89
+ null,
90
+ null
91
+ )
92
+ .use { cursor ->
93
+ if (cursor != null && cursor.moveToFirst()) {
94
+ metadataBuilder.name(
95
+ getCursorValue(cursor, OpenableColumns.DISPLAY_NAME, String::class.java)
96
+ )
97
+
98
+ if (!metadataBuilder.hasMime()) {
99
+ metadataBuilder.mimeType(
100
+ getCursorValue(
101
+ cursor, DocumentsContract.Document.COLUMN_MIME_TYPE, String::class.java
102
+ )
103
+ )
104
+ }
105
+
106
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
107
+ // https://developer.android.com/training/data-storage/shared/documents-files#open-virtual-file
108
+ val isVirtual =
109
+ if (DocumentsContract.isDocumentUri(context, forUri)) {
110
+ val cursorValue: Int =
111
+ getCursorValue(
112
+ cursor, DocumentsContract.Document.COLUMN_FLAGS, Int::class.java
113
+ )
114
+ ?: 0
115
+ cursorValue and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
116
+ } else {
117
+ false
118
+ }
119
+ metadataBuilder.virtual(isVirtual)
120
+ }
121
+ metadataBuilder.size(getCursorValue(cursor, OpenableColumns.SIZE, Long::class.java))
122
+ } else {
123
+ // metadataBuilder only contains the uri, type and error in this unlikely case
124
+ // there's nothing more we can do
125
+ metadataBuilder.metadataReadingError("Could not read file metadata")
126
+ }
127
+ }
128
+ }
129
+
130
+ @Suppress("UNCHECKED_CAST")
131
+ private fun <T> getCursorValue(cursor: Cursor, columnName: String, valueType: Class<T>): T? {
132
+ val columnIndex = cursor.getColumnIndex(columnName)
133
+ if (columnIndex != -1 && !cursor.isNull(columnIndex)) {
134
+ return try {
135
+ when (valueType) {
136
+ String::class.java -> cursor.getString(columnIndex) as T
137
+ Int::class.java -> cursor.getInt(columnIndex) as T
138
+ Long::class.java -> cursor.getLong(columnIndex) as T
139
+ Double::class.java -> cursor.getDouble(columnIndex) as T
140
+ Float::class.java -> cursor.getFloat(columnIndex) as T
141
+ else -> null
142
+ }
143
+ } catch (e: Exception) {
144
+ // this should not happen but if it does, we return null
145
+ null
146
+ }
147
+ }
148
+ return null
149
+ }
150
+ }