Media Store access using openFile() on Android 10

As of Android 10, things have changed slightly with how we access files that are contained outside of our application. This change of behaviour has come from the concept of scoped storage – which aims to improve the privacy of user files, adding more control to how files are accessed within the android system.

Even with these changes, our application still has read/write access by default to the files that it has created – regardless of whether those files are inside or outside of our applications directory on the device. When this is the case, we don’t require any extra permissions to access files – no change is required from our side here.

So what about when we want to access a file that belongs to another application? For example, maybe our user has edited a photo using VSCO or Instagram – that edited file will live inside of the corresponding app directory on the device. In our application the user may have the ability to add a photo from the device, for whatever purpose our application serves. Previously we may have access that file using a path to the file on the device (such as /sdcard/DCIM/some_external_file.png) – as of Android 10, this approach via a file path will no longer work. When this is attempted, an EACCESS (Permission Denied) error will be thrown as we are trying to access a file in a way that is no longer supported by the system. Even if we have requested the READ_EXTERNAL_STORAGE permission, the same fact holds true.

In this article I want to just cover the access of files that reside within the MediaStore (MediaStore.Images, MediaStore.Video or MediaStore.Audio). Provided that this is the case and we have the READ_EXTERNAL_STORAGE permission, then we can access the desired files by making use of the openFile() method which has been introduced in API level 29 (Android 10). We can use this new method to retrieve a reference to a ParcelFileDescriptor – this descriptor will give us everything we need to access and import the contents of the desired file.

When we call this method we need to pass in the Uri that we wish to retrieve, the mode which wish to use when opening the file (in this case, r for read) and a cancellation signal (which can be null if not required).

val fileDescriptor: ParcelFileDescriptor?
try {
    fileDescriptor = context.contentResolver.openFile(
        localMedia.mediaUri, "r", null)
} catch (e: FileNotFoundException) {
    // handle error
}

At this point we have this ParcelFileDescriptor reference, but what we want is an actual file so that we can do something with it within our application. In most cases we’ll likely be using the ACTION_GET_CONTENT approach to picking media content, so in this case we’re going to want to import a copy of the data into the cache directory of our application – this may not be the case in every situation, but having a copy of this file in our application cache gives us plenty of scope to process its contents. If your application needs are slightly different, it may be worth looking at the ACTION_OPEN_DOCUMENT approach.


We’re going to start here by using the FileDescriptor from our ParcelFileDescriptor reference to build a FileInputStream:

val input = FileInputStream(fileDescriptor.fileDescriptor)

Now that we have this input data, what we’re going to want to do is read the data and write it to a file which is stored in the temporary cache for our application. First we’re going to need a function that will take our input stream as the input data and give us back a ByteArray instance so that we can write this data to a file. We’ll write a short function (shown below) that does exactly this:

private fun readBinaryStream(
    stream: InputStream, 
    byteCount: Int
): ByteArray {
        val output = ByteArrayOutputStream()
        try {
            val buffer = ByteArray(if (byteCount > 0) byteCount else 4096)
            var read: Int
            while (stream.read(buffer).also { read = it } >= 0) {
                output.write(buffer, 0, read)
            }
        } catch (e: IOException) {
            e.printStackTrace()
        } finally {
            try {
                stream.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        return output.toByteArray()
    }

We won’t dive too much into this here, but we can see that we use a byteCount to determine the size of our byte array and then populate this with the contents of our input stream. We can then use this function providing our file descriptor, along with the size of the file. For this we access the getStatSize() method of our file descriptor – this is used to provide the size of our file, in bytes, so this is perfect for creating our byte array for the size of our file.

val input = FileInputStream(fileDescriptor.fileDescriptor)
...
val byteArray = readBinaryStream(input, 
                    fileDescriptor.statSize.toInt())

Now that we have our byte array, we’re going to want to write this to our cached version of this file for use within our application. For this, we can begin by generating a file in the cache directory of our application – this can be declared using the context.cacheDir reference, along with a generated filename of our choosing.

var cachedFile = File(context.cacheDir, someGeneratedFileName)

We can then write a short function that will take a byte array and write the content of it to the provided file. This function will take our file that is stored in our application cache, along with the byte array whose data we want to write to that file. All this function then does is essentially write that data and then return the success status of the operation.

private fun writeFile(cachedFile: File, data: ByteArray): Boolean {
        return try {
            var output: BufferedOutputStream? = null
            try {
                output = BufferedOutputStream(FileOutputStream(file))
                output.write(data)
                output.flush()
                true
            } finally {
                output?.close()
            }
        } catch (ex: Exception) {
            false
        }
    }

With that in place, we can now take the

val input = FileInputStream(fileDescriptor.fileDescriptor)
...
val byteArray = readBinaryStream(input, 
                    fileDescriptor.statSize.toInt())
val cachedFile = File(context.cacheDir, someGeneratedFileName)
val fileSaved = writeFile(cachedFile, byteArray)

And once that file has been saved, we can then access our cachedFile using its Uri. Because this file is within the cache directory of our application, there is no restriction to us accessing it – meaning we will not run into the same permission errors that we previously would have done when trying to access the original file via its Uri.


With this approach we can now access files from the MediaStore using the newly available openFile() method from the Android framework. With approaches used in our applications prior to Android 10 where we would have seen file access errors, we can now safely read files from the MediaStore and process them for use within our own applications.

Have you had to make changes with how you access media within Android 10, or are you currently working out how to approach these problems? If there are any questions on the above then please reach out and I’d be happy to help!

[twitter-follow screen_name=’hitherejoe’ show_count=’yes’]

Leave a Reply

Your email address will not be published. Required fields are marked *