Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mighty-carrots-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-native-documents/picker": patch
---

chore(android): improve error handling when querying files metadata
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ enum class CopyDestination(val preset: String) {
DOCUMENT_DIRECTORY("documentDirectory");

companion object {
// keep values() for RN 73 compatibility
fun fromPath(path: String): CopyDestination = values().find { it.preset == path } ?: CACHES_DIRECTORY
fun fromPath(path: String): CopyDestination = entries.find { it.preset == path } ?: CACHES_DIRECTORY
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ class DocumentMetadataBuilder(forUri: Uri) {
openableMimeTypes?.let {
val arrayOfExtensionsAndMime = Arguments.createArray()
it.forEach { mimeType ->
val virtualFileDetails = Arguments.createMap()
val maybeExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)
virtualFileDetails.putString("mimeType", mimeType)
virtualFileDetails.putString("extension", maybeExtension)
val virtualFileDetails = Arguments.createMap().apply {
putString("mimeType", mimeType)
putString("extension", maybeExtension)
}
arrayOfExtensionsAndMime.pushMap(virtualFileDetails)
}
map.putArray("convertibleToMimeTypes", arrayOfExtensionsAndMime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ class FileOperations(private val uriMap: MutableMap<String, Uri>) {
}

val copyStreamToAnother: (InputStream, OutputStream) -> Long = { inputStream, outputStream ->
inputStream.use { input ->
outputStream.use { output ->
inputStream.use { _ ->
outputStream.use { _ ->
val bytesCopied = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
FileUtils.copy(inputStream, outputStream)
} else {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,35 @@ import android.webkit.MimeTypeMap
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap

class IsKnownTypeImpl {
companion object {
fun isKnownType(kind: String, value: String): WritableMap {
return when (kind) {
"mimeType" -> {
val extensionForMime = MimeTypeMap.getSingleton().getExtensionFromMimeType(value)
createMap(
isKnown = extensionForMime != null,
preferredFilenameExtension = extensionForMime,
mimeType = if (extensionForMime != null) value else null
)
}
"extension" -> {
val mimeForExtension = MimeTypeMap.getSingleton().getMimeTypeFromExtension(value)
createMap(
isKnown = mimeForExtension != null,
preferredFilenameExtension = if (mimeForExtension != null) value else null,
mimeType = mimeForExtension
)
}
else -> createMap(isKnown = false, preferredFilenameExtension = null, mimeType = null)
object IsKnownTypeImpl {
fun isKnownType(kind: String, value: String): WritableMap {
return when (kind) {
"mimeType" -> {
val extensionForMime = MimeTypeMap.getSingleton().getExtensionFromMimeType(value)
createMap(
isKnown = extensionForMime != null,
preferredFilenameExtension = extensionForMime,
mimeType = if (extensionForMime != null) value else null
)
}
"extension" -> {
val mimeForExtension = MimeTypeMap.getSingleton().getMimeTypeFromExtension(value)
createMap(
isKnown = mimeForExtension != null,
preferredFilenameExtension = if (mimeForExtension != null) value else null,
mimeType = mimeForExtension
)
}
else -> createMap(isKnown = false, preferredFilenameExtension = null, mimeType = null)
}
}

private fun createMap(isKnown: Boolean, preferredFilenameExtension: String?, mimeType: String?): WritableMap {
return Arguments.createMap().apply {
putNull("UTType")
putBoolean("isKnown", isKnown)
putString("preferredFilenameExtension", preferredFilenameExtension)
putString("mimeType", mimeType)
}
private fun createMap(isKnown: Boolean, preferredFilenameExtension: String?, mimeType: String?): WritableMap {
return Arguments.createMap().apply {
putNull("UTType")
putBoolean("isKnown", isKnown)
putString("preferredFilenameExtension", preferredFilenameExtension)
putString("mimeType", mimeType)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,39 @@ class MetadataGetter(private val uriMap: MutableMap<String, Uri>) {
contentResolver: ContentResolver,
metadataBuilder: DocumentMetadataBuilder,
couldBeVirtualFile: Boolean
) {
try {
queryContentResolverMetadataInternal(contentResolver, metadataBuilder, couldBeVirtualFile)
} catch (e: Exception) {
val suppressedSummary =
e.suppressed.joinToString(separator = "; ") { suppressed ->
"${suppressed.javaClass.simpleName}: ${suppressed.message ?: "no message"}"
}
metadataBuilder.metadataReadingError(
"Could not read file metadata: ${e.javaClass.simpleName}: ${e.message ?: "no message"}" +
(" (suppressed summary: [$suppressedSummary])")
)
}
}

private fun queryContentResolverMetadataInternal(
contentResolver: ContentResolver,
metadataBuilder: DocumentMetadataBuilder,
couldBeVirtualFile: Boolean
) {
val forUri = metadataBuilder.getUri()
val hasNoMime = !metadataBuilder.hasMime()

val projection = mutableListOf(
DocumentsContract.Document.COLUMN_MIME_TYPE,
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE,
).apply {
if (couldBeVirtualFile) {
add(DocumentsContract.Document.COLUMN_FLAGS)
}
if (hasNoMime) {
add(DocumentsContract.Document.COLUMN_MIME_TYPE)
}
}.toTypedArray()

contentResolver
Expand All @@ -97,59 +119,61 @@ class MetadataGetter(private val uriMap: MutableMap<String, Uri>) {
null
)
.use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
metadataBuilder.name(
getCursorValue(cursor, OpenableColumns.DISPLAY_NAME, String::class.java)
)

if (!metadataBuilder.hasMime()) {
metadataBuilder.mimeType(
getCursorValue(
cursor, DocumentsContract.Document.COLUMN_MIME_TYPE, String::class.java
)
if (cursor == null) {
metadataBuilder.metadataReadingError("Could not read file metadata because cursor was null. This is likely an issue with the underlying ContentProvider.")
return
}
if (!cursor.moveToFirst()) {
metadataBuilder.metadataReadingError("Could not read file metadata because cursor could not move to the first result row. This is likely an issue with the underlying ContentProvider. Row count: ${cursor.count}, columns: ${cursor.columnNames.joinToString(",")}")
return
}
metadataBuilder.name(
getCursorValue(cursor, OpenableColumns.DISPLAY_NAME, String::class.java)
)
metadataBuilder.size(getCursorValue(cursor, OpenableColumns.SIZE, Long::class.java))

if (hasNoMime) {
metadataBuilder.mimeType(
getCursorValue(
cursor, DocumentsContract.Document.COLUMN_MIME_TYPE, String::class.java
)
}
)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// https://developer.android.com/training/data-storage/shared/documents-files#open-virtual-file
val isVirtual =
if (couldBeVirtualFile) {
val cursorValue: Int =
getCursorValue(
cursor, DocumentsContract.Document.COLUMN_FLAGS, Int::class.java
)
?: 0
cursorValue and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
} else {
false
}
metadataBuilder.virtual(isVirtual)
}
metadataBuilder.size(getCursorValue(cursor, OpenableColumns.SIZE, Long::class.java))
} else {
// metadataBuilder only contains the uri, type and error in this unlikely case
// there's nothing more we can do
metadataBuilder.metadataReadingError("Could not read file metadata")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// https://developer.android.com/training/data-storage/shared/documents-files#open-virtual-file
val isVirtual =
if (couldBeVirtualFile) {
val cursorValue: Int =
getCursorValue(
cursor, DocumentsContract.Document.COLUMN_FLAGS, Int::class.java
)
?: 0
cursorValue and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
} else {
false
}
metadataBuilder.virtual(isVirtual)
}
}
}

@Suppress("UNCHECKED_CAST")
private fun <T> getCursorValue(cursor: Cursor, columnName: String, valueType: Class<T>): T? {
val columnIndex = cursor.getColumnIndex(columnName)
if (columnIndex != -1 && !cursor.isNull(columnIndex)) {
return runCatching {
when (valueType) {
String::class.java -> cursor.getString(columnIndex) as T
Int::class.java -> cursor.getInt(columnIndex) as T
Long::class.java -> cursor.getLong(columnIndex) as T
Double::class.java -> cursor.getDouble(columnIndex) as T
Float::class.java -> cursor.getFloat(columnIndex) as T
else -> null
}
// throw should not happen but if it does, we return null
}.getOrNull()
if (columnIndex == -1 || cursor.isNull(columnIndex)) {
return null
}
return null
return runCatching {
when (valueType) {
String::class.java -> cursor.getString(columnIndex) as T
Int::class.java -> cursor.getInt(columnIndex) as T
Long::class.java -> cursor.getLong(columnIndex) as T
Double::class.java -> cursor.getDouble(columnIndex) as T
Float::class.java -> cursor.getFloat(columnIndex) as T
else -> null
}
// throw should not happen but if it does, we return null
}.getOrNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
package com.reactnativedocumentpicker

import android.content.Intent
import android.os.Build
import android.provider.DocumentsContract
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap

Expand All @@ -14,6 +16,20 @@ data class PickOptions(
val requestLongTermAccess: Boolean,
val allowVirtualFiles: Boolean,
) {
constructor(readableMap: ReadableMap) : this(
mode = readableMap.getString("mode"),
mimeTypes = if (readableMap.hasKey("type") && !readableMap.isNull("type")) {
readableMap.getArray("type")?.let { readableArrayToStringArray(it) } ?: arrayOf("*/*")
} else {
arrayOf("*/*")
},
initialDirectoryUrl = if (readableMap.hasKey("initialDirectoryUrl")) readableMap.getString("initialDirectoryUrl") else null,
localOnly = readableMap.hasKey("localOnly") && readableMap.getBoolean("localOnly"),
multiple = readableMap.hasKey("allowMultiSelection") && readableMap.getBoolean("allowMultiSelection"),
requestLongTermAccess = readableMap.hasKey("requestLongTermAccess") && readableMap.getBoolean("requestLongTermAccess"),
allowVirtualFiles = readableMap.hasKey("allowVirtualFiles") && readableMap.getBoolean("allowVirtualFiles")
)

val action: String
get() = if ("open" == mode) Intent.ACTION_OPEN_DOCUMENT else Intent.ACTION_GET_CONTENT

Expand All @@ -26,33 +42,37 @@ data class PickOptions(
mimeTypes.joinToString("|")
}
}
}

fun parsePickOptions(readableMap: ReadableMap): PickOptions {
val mode = readableMap.getString("mode")
fun getPickIntent(): Intent {
// TODO option for extra task on stack?
// reminder - flags are for granting rights to others

val mimeTypes = if (readableMap.hasKey("type") && !readableMap.isNull("type")) {
readableMap.getArray("type")?.let { readableArrayToStringArray(it) } ?: arrayOf("*/*")
} else {
arrayOf("*/*")
}
return Intent(action).apply {
val types = mimeTypes

val initialDirectoryUrl = if (readableMap.hasKey("initialDirectoryUrl")) readableMap.getString("initialDirectoryUrl") else null
val localOnly = readableMap.hasKey("localOnly") && readableMap.getBoolean("localOnly")
val multiple = readableMap.hasKey("allowMultiSelection") && readableMap.getBoolean("allowMultiSelection")
val requestLongTermAccess = readableMap.hasKey("requestLongTermAccess") && readableMap.getBoolean("requestLongTermAccess")
val allowVirtualFiles = readableMap.hasKey("allowVirtualFiles") && readableMap.getBoolean("allowVirtualFiles")

return PickOptions(
mode = mode,
mimeTypes = mimeTypes,
initialDirectoryUrl = initialDirectoryUrl,
localOnly = localOnly,
multiple = multiple,
requestLongTermAccess = requestLongTermAccess,
allowVirtualFiles = allowVirtualFiles,
)
type =
if (types.size > 1) {
putExtra(Intent.EXTRA_MIME_TYPES, types)
intentFilterTypes
} else {
types[0]
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
initialDirectoryUrl != null
) {
// only works for ACTION_OPEN_DOCUMENT
// TODO must be URI
putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirectoryUrl)
}
if (!allowVirtualFiles) {
addCategory(Intent.CATEGORY_OPENABLE)
}
putExtra(Intent.EXTRA_LOCAL_ONLY, localOnly)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
}
}
}

fun readableArrayToStringArray(readableArray: ReadableArray): Array<String> {
/**
* MIME type and Uri scheme matching in the
Expand Down
Loading
Loading