28

According to the docs file path access is granted in Android R:

Starting in Android 11, apps that have the READ_EXTERNAL_STORAGE permission can read a device's media files using direct file paths and native libraries. This new capability allows your app to work more smoothly with third-party media libraries.

The problem is that I can't get the file path from MediaStore, so how are we supposed to read a file path that we can't access/retrieve? Is there a way, I'm not aware of, that we can get the file path from MediaStore?


Furthermore, the docs say the following:

All Files Access

Some apps have a core use case that requires broad file access, such as file management or backup & restore operations. They can get All Files Access by doing the following:

  1. Declare the MANAGE_EXTERNAL_STORAGE permission.
  2. Direct users to a system settings page where they can enable the Allow access to manage all files option for your app.

This permission grants the following:

  • Read access and write access to all files within shared storage.
  • Access to the contents of the MediaStore.Files table.

But I do not need all file access, I only want the user to select a video from MediaStore and pass the file path to FFmpeg(it requires a file path). I know that I can no longer use the _data column to retrieve a file path.


Please note:

  • I know a Uri is returned from MediaStore and does not point to a file.
  • I know that I can copy the file to my application directory and pass that to FFmpeg, but I could do that before Android R.
  • I can not pass FileDescriptor to FFmpeg and I can not use /proc/self/fd/ (I get /proc/7828/fd/70: Permission denied when selecting a file from the SD Card), have a look at this issue.

So what am I supposed to do, am I missing something? What was meant with can read a device's media files using direct file paths and native libraries?

HB.
  • 4,116
  • 4
  • 29
  • 53

3 Answers3

18

After asking a question on issuetracker, I've come to the following conclusions:

  • On Android R, the File restrictions that were added in Android Q is removed. So we can once again access File objects.
  • If you are targeting Android 10 > and you want to access/use file paths, you will have to add/keep the following in your manifest:

    android:requestLegacyExternalStorage="true"
    

    This is to ensure that file paths are working on Android 10(Q). On Android R this attribute will be ignored.

  • Don't use DATA column for inserting or updating into Media Store, use DISPLAY_NAME and RELATIVE_PATH, here is an example:

    ContentValues valuesvideos;
    valuesvideos = new ContentValues();
    valuesvideos.put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/" + "YourFolder");
    valuesvideos.put(MediaStore.Video.Media.TITLE, "SomeName");
    valuesvideos.put(MediaStore.Video.Media.DISPLAY_NAME, "SomeName");
    valuesvideos.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
    valuesvideos.put(MediaStore.Video.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
    valuesvideos.put(MediaStore.Video.Media.DATE_TAKEN, System.currentTimeMillis());
    valuesvideos.put(MediaStore.Video.Media.IS_PENDING, 1);
    ContentResolver resolver = getContentResolver();
    Uri collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
    Uri uriSavedVideo = resolver.insert(collection, valuesvideos);
    
  • You can no longer use the ACTION_OPEN_DOCUMENT_TREE or the ACTION_OPEN_DOCUMENT intent action to request that the user select individual files from Android/data/,Android/obb/and all sub-directories.

  • It is recommended to only use File objects when you need to perform "seeking", like when using FFmpeg, for example.
  • You can only use the data column to access files that are on the disk. You should handle I/O Exceptions accordingly.

If you want to access a File or want a file path from a Uri that was returned from MediaStore, I've created a library that handles all the exceptions you might get. This includes all files on the disk, internal and removable disk. When selecting a File from Dropbox, for example, the File will be copied to your applications directory where you have full access, the copied file path will then be returned.

HB.
  • 4,116
  • 4
  • 29
  • 53
  • Do you also have, perhaps, a sample on Github for the auto-finding of the media files , to list them? – android developer Jun 29 '20 at 22:33
  • @androiddeveloper "auto-finding of the media files" I'm not sure I understand what you are asking? – HB. Jun 30 '20 at 10:31
  • Aren't gallery apps and music apps finding their files using an API from the frameworks automatically, and that a special file (".nomedia") in each folder tells the framework to avoid scanning it? – android developer Jul 01 '20 at 07:28
  • @HB I use your library and test it for Android 11. pickiT.getPath return null. Issue has place when path resolved as media and getDataColumn() return null. Is cursor work for Android 11? May be I not understand anything? – Anton Stukov Nov 20 '20 at 06:25
  • @AntonStukov Look at the "read me", it explains how to use the library (you shouldn't use `pickiT.getPath` directly, the path will be returned in `PickiTonCompleteListener`). If you still can't get it working, open an issue on the library and fill in the issue template (include you log and how you implemented the library) - https://github.com/HBiSoft/PickiT#implementation – HB. Nov 20 '20 at 06:50
  • Could you implement something similar for Ionic? – Shinichi Kudo Feb 03 '21 at 15:51
  • Thanks a lot. Great job. No other solution worked for me except this library. – Prantik Mondal Sep 27 '21 at 06:21
10

If you are targeting to Android 11 API, you cannot directly get access to the file paths, as there are many restrictions in API 30(Android R). As scoped storage API was introduced in Android 10(API 29), the storage is now divided into scoped storage (private storage) and shared storage (public storage). Scoped storage is a kind you can only have access to the files that are created in your scoped storage directory(i.e. /Android/data/ or /Android/media/<your-package-name>). You cannot access files from shared storage (i.e. internal storage/external SD card storage etc.)

The shared storage is again further divided into Media and Download collection. Media collection stores Image, Audio and Video files. Download collection would take care of non-media files.

To learn in more details about scoped storage and shared storage refer this link: Scoped Storage in Android 10 & Android 11 .

If you are dealing with Media files (i.e. Images, Videos, Audio) you can get the file path by Using Media Store API that having support to API 30(Android 11). and If you are dealing with non-media files(i.e. documents and other files) you can get the file path by using file Uri.

Note: If you are using the file or Uri util classes (such as RealPathUtil, FilePathUtils etc.) to get the file path, here you can get the desired file path but you cannot read that file, as it will throw an exception of Read Access (as Permission denied) in Android 11, as you cannot read the files that are created by another application.

So to achieve this scenario of getting the file path in Android 11(API 30), It a recommended to copy the file into the cache directory of your application using File Uri and get the path of the file access from cache directory.

Here in my scenario I have used both APIs to get the file access in Android 11. To get the file path of the media files (i.e. Images, Videos, Audio), I've used the Media Store API (Refer this link: Media Store API Example - Access media files from shared storage ), and to get the file path of the non-media files (i.e. Documents and other files), I've used fileDescriptor.

File Descriptor Example: I have created the system dialog file picker to pick the file.

private fun openDocumentAction() {
    val mimetypes = arrayOf(
        "application/*",  //"audio/*",
        "font/*",  //"image/*",
        "message/*",
        "model/*",
        "multipart/*",
        "text/*"
    )
    // you can customize the mime types as per your choice.
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        //type = "application/pdf"    //only pdf files
        type = "*/*"
        putExtra(Intent.EXTRA_MIME_TYPES, mimetypes)
        addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        //putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, RC_SAF_NON_MEDIA)
}

And handled the result of file picker in onActivityResult method of the activity. Get the file URI at here.

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    when (requestCode) {
        
        RC_SAF_NON_MEDIA -> {
            //document selection by SAF(Storage Access Framework) for Android 11
            if (resultCode == RESULT_OK) {
                // The result data contains a URI for the document or directory that
                // the user selected.
                data?.data?.also { uri ->

                    //Permission needed if you want to retain access even after reboot
                    contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
                    // Perform operations on the document using its URI.
                   
                    val path = makeFileCopyInCacheDir(uri)
                    Log.e(localClassName, "onActivityResult: path ${path.toString()} ")
                   
                }
            }
        }
    }
}

Pass the file URI to the below method to get the file path. This method will create a file object at cache directory of your application and from that location you can easily get Read access to that file.

private fun makeFileCopyInCacheDir(contentUri :Uri) : String? {
    try {
        val filePathColumn = arrayOf(
            //Base File
            MediaStore.Files.FileColumns._ID,
            MediaStore.Files.FileColumns.TITLE,
            MediaStore.Files.FileColumns.DATA,
            MediaStore.Files.FileColumns.SIZE,
            MediaStore.Files.FileColumns.DATE_ADDED,
            MediaStore.Files.FileColumns.DISPLAY_NAME,
            //Normal File
            MediaStore.MediaColumns.DATA,
            MediaStore.MediaColumns.MIME_TYPE,
            MediaStore.MediaColumns.DISPLAY_NAME
        )
        //val contentUri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(mediaUrl))
        val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) }
        if (returnCursor!=null) {
            returnCursor.moveToFirst()
            val nameIndex = returnCursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
            val name = returnCursor.getString(nameIndex)
            val file = File(cacheDir, name)
            val inputStream = contentResolver.openInputStream(contentUri)
            val outputStream = FileOutputStream(file)
            var read = 0
            val maxBufferSize = 1 * 1024 * 1024
            val bytesAvailable = inputStream!!.available()

            //int bufferSize = 1024;
            val bufferSize = Math.min(bytesAvailable, maxBufferSize)
            val buffers = ByteArray(bufferSize)
            while (inputStream.read(buffers).also { read = it } != -1) {
                outputStream.write(buffers, 0, read)
            }
            inputStream.close()
            outputStream.close()
            Log.e("File Path", "Path " + file.path)
            Log.e("File Size", "Size " + file.length())
            return file.absolutePath
        }
    } catch (ex: Exception) {
        Log.e("Exception", ex.message!!)
    }
    return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() }
}

Note: You can use this method to get file path for both media files (Images, Videos, Audio) and non-media files (Documents and other files) as well. Just need to pass a file Uri.

Zoe
  • 27,060
  • 21
  • 118
  • 148
Rohan Shinde
  • 374
  • 1
  • 6
  • 16
  • your method contain several errors firstly in this line `val returnCursor = contentUri.let { contentResolver.query(it, filePathColumn, null, null, null) }` **Where is contentResolver var?** I got this `Unresolved reference: contentResolver` second in this line `val file = File(cacheDir, name)` also there's no var called `cacheDir` the next line complains from `contentResolver` also, next in `outputStream` I got this message `Overload resolution ambiguity. All these functions match.` and finally ` return contentUri.let { UriPathUtils().getRealPathFromURI(this, it).toString() }` – Dr Mido Oct 07 '22 at 07:57
  • `Unresolved reference: UriPathUtils` I think it's a library right? you should edit this method and fix the mentions errors! – Dr Mido Oct 07 '22 at 07:58
8

For getting path, i'm coping file with fileDescriptor to new path & i use that path.

Finding File Name:

private static String copyFileAndGetPath(Context context, Uri realUri, String id) {
    final String selection = "_id=?";
    final String[] selectionArgs = new String[]{id};
    String path = null;
    Cursor cursor = null;
    try {
        final String[] projection = {"_display_name"};
        cursor = context.getContentResolver().query(realUri, projection, selection, selectionArgs,
                null);
        cursor.moveToFirst();
        final String fileName = cursor.getString(cursor.getColumnIndexOrThrow("_display_name"));
        File file = new File(context.getCacheDir(), fileName);

        FileUtils.saveAnswerFileFromUri(realUri, file, context);
        path = file.getAbsolutePath();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null)
            cursor.close();
    }
    return path;
}

Copy With File Descriptor:

fun saveAnswerFileFromUri(uri: Uri, destFile: File?, context: Context) {
    try {
        val pfd: ParcelFileDescriptor =
            context.contentResolver.openFileDescriptor(uri, "r")!!
        if (pfd != null) {
            val fd: FileDescriptor = pfd.getFileDescriptor()
            val fileInputStream: InputStream = FileInputStream(fd)
            val fileOutputStream: OutputStream = FileOutputStream(destFile)
            val buffer = ByteArray(1024)
            var length: Int
            while (fileInputStream.read(buffer).also { length = it } > 0) {
                fileOutputStream.write(buffer, 0, length)
            }

            fileOutputStream.flush()
            fileInputStream.close()
            fileOutputStream.close()
            pfd.close()
        }
    } catch (e: IOException) {
        Timber.w(e)
    }

}
Hosein Haqiqian
  • 579
  • 9
  • 16
  • 5
    Few things to note with your answer. 1) Copying a file is not suitable for applications that work with larger files. Imagine having to copying a 2GB file. If you want to use that file, you will first have to wait for it to finish. 2) You are still using the `_data` column, which is deprecated, so you will have to add `requestLegacyExternalStorage` in your Manifest. 3) You are returning the path to early. You first have to wait for the file to be copied before returning the path. 4) The creation/copying of a file should be done on a background thread. – HB. Jun 15 '21 at 05:09
  • @HB. You are right, it's not a good soloution for large files. i use _data column for older android versions. _data column still is a better solution if it be available. i just copied the main solution. it should be done in background solution (coroutines can be used). thanks my friend :) – Hosein Haqiqian Jun 15 '21 at 06:48