@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
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.
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
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,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
|
+
}
|