@@ -81,7 +81,7 @@ object FileUtils {
8181 uri,
8282 DocumentsContract .getTreeDocumentId(uri)
8383 )
84- val dirPath = getFullPathFromTreeUri(uri)
84+ val dirPath = getFullPathFromTreeUri(uri, activity )
8585 if (dirPath != null ) {
8686 finishWithSuccess(dirPath)
8787 } else {
@@ -131,6 +131,14 @@ object FileUtils {
131131 }
132132 }
133133
134+ /* *
135+ * Creates and launches an intent for the given file type.
136+ *
137+ * This method is responsible for creating the appropriate intent based on the [type] of file
138+ * that is requested to be picked.
139+ *
140+ * This may be either a directory, a regular file, or a gallery pick.
141+ */
134142 fun FilePickerDelegate.startFileExplorer () {
135143 val intent: Intent
136144
@@ -142,7 +150,8 @@ object FileUtils {
142150 if (type == " dir" ) {
143151 intent = Intent (Intent .ACTION_OPEN_DOCUMENT_TREE )
144152 } else {
145- if (type != " */*" ) {
153+ if (type == " image/*" ) {
154+ // Use ACTION_PICK for images to allow using the Gallery app, which provides a better UX for image selection.
146155 intent = Intent (Intent .ACTION_PICK )
147156 val uri = (Environment .getExternalStorageDirectory().path + File .separator).toUri()
148157 intent.setDataAndType(uri, type)
@@ -159,7 +168,12 @@ object FileUtils {
159168 intent.putExtra(Intent .EXTRA_MIME_TYPES , allowedExtensions)
160169 }
161170 } else {
162- intent = Intent (Intent .ACTION_GET_CONTENT ).apply {
171+ // Use ACTION_OPEN_DOCUMENT to allow selecting files from any document provider (SAF).
172+ // We prefer ACTION_OPEN_DOCUMENT over ACTION_GET_CONTENT because it offers persistent
173+ // access to the files via URI permissions, which is crucial for some use cases
174+ // (e.g. caching, repeated access). ACTION_GET_CONTENT is more suitable for
175+ // "importing" content and might not provide a persistent URI.
176+ intent = Intent (Intent .ACTION_OPEN_DOCUMENT ).apply {
163177 addCategory(Intent .CATEGORY_OPENABLE )
164178 type = this @startFileExplorer.type
165179 if (! allowedExtensions.isNullOrEmpty()) {
@@ -186,6 +200,16 @@ object FileUtils {
186200 }
187201 }
188202
203+ /* *
204+ * Called by the plugin to start a new file explorer activity.
205+ *
206+ * @param type The file types that will be selectable.
207+ * @param isMultipleSelection Whether multiple files can be selected.
208+ * @param withData Whether the file data should be loaded into memory.
209+ * @param allowedExtensions The allowed file extensions for custom file types.
210+ * @param compressionQuality The compression quality for images.
211+ * @param result The MethodChannel result to send the file picking result to.
212+ */
189213 fun FilePickerDelegate?.startFileExplorer (
190214 type : String? ,
191215 isMultipleSelection : Boolean? ,
@@ -413,7 +437,7 @@ object FileUtils {
413437 try {
414438 context.contentResolver.openInputStream(originalImageUri).use { imageStream ->
415439 val compressFormat = getCompressFormat(context, originalImageUri)
416- val compressedFile = createImageFile(context, originalImageUri, compressFormat)
440+ val compressedFile = createImageFile(context, compressFormat)
417441 val originalBitmap = BitmapFactory .decodeStream(imageStream)
418442 // Compress and save the image
419443 val fileOutputStream = FileOutputStream (compressedFile)
@@ -429,7 +453,7 @@ object FileUtils {
429453 }
430454
431455 @Throws(IOException ::class )
432- private fun createImageFile (context : Context , uri : Uri , compressFormat : Bitmap .CompressFormat ): File {
456+ private fun createImageFile (context : Context , compressFormat : Bitmap .CompressFormat ): File {
433457 val timeStamp = SimpleDateFormat (" yyyyMMdd_HHmmss" , Locale .getDefault()).format(Date ())
434458 val imageFileName = " IMAGE_" + timeStamp + " _"
435459 val storageDir = context.cacheDir
@@ -557,7 +581,7 @@ object FileUtils {
557581 }
558582
559583 @JvmStatic
560- fun getFullPathFromTreeUri (treeUri : Uri ? ): String? {
584+ fun getFullPathFromTreeUri (treeUri : Uri ? , context : Context ): String? {
561585 if (treeUri == null ) {
562586 return null
563587 }
@@ -569,6 +593,14 @@ object FileUtils {
569593 Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DOWNLOADS ).path
570594 if (docId == " downloads" ) {
571595 return extPath
596+ } else if (docId.matches(" ^ms[df]:.*" .toRegex())) {
597+ // Handle "msf:" (Media Store File) and "msd:" (Media Store Directory) prefixes.
598+ // These are commonly seen on Android 10+ (API 29+) when selecting files from the
599+ // "Downloads" category in the system picker.
600+ // Note that this does not happen on all devices.
601+ // Example URI: content://com.android.providers.downloads.documents/document/msf:1000000033
602+ val fileName = getFileName(treeUri, context)
603+ return " $extPath /$fileName "
572604 } else if (docId.startsWith(" raw:" )) {
573605 return docId.split(" :" .toRegex()).dropLastWhile { it.isEmpty() }
574606 .toTypedArray()[1 ]
@@ -580,13 +612,13 @@ object FileUtils {
580612 var volumePath = getPathFromTreeUri(treeUri)
581613
582614 if (volumePath.endsWith(File .separator)) {
583- volumePath = volumePath.substring( 0 , volumePath.length - 1 )
615+ volumePath = volumePath.dropLast( 1 )
584616 }
585617
586618 var documentPath = getDocumentPathFromTreeUri(treeUri)
587619
588620 if (documentPath.endsWith(File .separator)) {
589- documentPath = documentPath.substring( 0 , documentPath.length - 1 )
621+ documentPath = documentPath.dropLast( 1 )
590622 }
591623 return if (documentPath.isNotEmpty()) {
592624 if (volumePath.endsWith(documentPath)) {
@@ -623,4 +655,4 @@ object FileUtils {
623655
624656 file.delete()
625657 }
626- }
658+ }
0 commit comments