package expo.modules.documentpicker import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.os.FileUtils import expo.modules.core.utilities.FileUtilities import expo.modules.kotlin.Promise import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.exception.toCodedException import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import org.apache.commons.io.FilenameUtils import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream private const val OPEN_DOCUMENT_CODE = 4137 class DocumentPickerModule : Module() { private val context: Context get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() private var pendingPromise: Promise? = null private var copyToCacheDirectory = true override fun definition() = ModuleDefinition { Name("ExpoDocumentPicker") AsyncFunction("getDocumentAsync") { options: DocumentPickerOptions, promise: Promise -> if (pendingPromise != null) { throw PickingInProgressException() } if (options.type.isEmpty()) { throw DocumentPickerOptionsEmptyListException() } pendingPromise = promise copyToCacheDirectory = options.copyToCacheDirectory val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, options.multiple) type = if (options.type.size > 1) { putExtra(Intent.EXTRA_MIME_TYPES, options.type.toTypedArray()) "*/*" } else { options.type[0] } } appContext.throwingActivity.startActivityForResult(intent, OPEN_DOCUMENT_CODE) } OnActivityResult { _, (requestCode, resultCode, intent) -> if (requestCode != OPEN_DOCUMENT_CODE || pendingPromise == null) { return@OnActivityResult } val promise = pendingPromise!! if (resultCode == Activity.RESULT_OK) { try { if (intent?.clipData != null) { handleMultipleSelection(intent) } else { handleSingleSelection(intent) } } catch (e: Exception) { promise.reject(e.toCodedException()) } } else { promise.resolve( DocumentPickerResult(canceled = true) ) } pendingPromise = null } } private fun copyDocumentToCacheDirectory(documentUri: Uri, name: String): Uri { val outputFilePath = FileUtilities.generateOutputPath( context.cacheDir, "DocumentPicker", FilenameUtils.getExtension(name) ) val outputFile = File(outputFilePath) context.contentResolver.openInputStream(documentUri).use { inputStream -> inputStream ?: throw FileNotFoundException("Inputstream for $documentUri was null.") FileOutputStream(outputFile).use { outputStream -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { FileUtils.copy(inputStream, outputStream) } else { inputStream.copyTo(outputStream) } } } return Uri.fromFile(outputFile) } private fun handleSingleSelection(intent: Intent?) { intent?.data?.let { uri -> val details = readDocumentDetails(uri) val result = DocumentPickerResult( assets = listOf(details) ) pendingPromise?.resolve(result) } ?: throw FailedToReadDocumentException() } private fun handleMultipleSelection(intent: Intent?) { val count = intent?.clipData?.itemCount ?: 0 val assets = mutableListOf() for (i in 0 until count) { val uri = intent?.clipData?.getItemAt(i)?.uri ?: throw FailedToReadDocumentException() val document = readDocumentDetails(uri) assets.add(document) } pendingPromise?.resolve(DocumentPickerResult(assets = assets)) } private fun readDocumentDetails(uri: Uri): DocumentInfo { val originalDocumentDetails = DocumentDetailsReader(context).read(uri) val details = if (!copyToCacheDirectory) { originalDocumentDetails } else { val copyPath = copyDocumentToCacheDirectory(uri, originalDocumentDetails.name) originalDocumentDetails.copy(uri = copyPath) } return details } }