Android Content URI: How to Get the File URI

In this blog post we are going to look at how we can pick file in Android without messing with Content URI. If you are just beginning with Android. You will notice that you will get a content:// URI after you do Intent.ACTION_OPEN_DOCUMENT. But this URI is useless if you want to lets say send file to server. To overcome this you have to either use a file picker library or manually check each type and try to get the actual location.

But there’s actually another much more easier and efficient way of doing this. But every way it has it’s disadvantages. We will look at it one by one. I’ll list when it might not be idle to use this approach.

Introduction to Storage Access Framework ( SAF )

Storage Access Framework or SAF is a newer approach to accessing the files in Android device. Not only local files but you can also access the online storage provider such as Google Drive.

It is an easy and centralized way to access everything in your device from multiple providers. Let us look at the image below.

Taken from Documentation

We have client that can either create or access the existing file from any one of the listed providers after doing Intent.ACTION_OPEN_DOCUMENT or Intent.ACTION_CREATE_DOCUMENT. We will be only looking at opening files in this post.

But SAF comes with its own challenge of not exposing the actual URI. Which is very good from security perspective as it limits the interactions. But how do we get the file to upload it somewhere or do something with it withing our own application ?

This post will exactly do that. We will pick file in Android and not worry about the Content URI. Let us quickly see what we are going to do before we actually do anything.

Our strategy to using SAF

Let us see what we can do to use SAF. We will follow the following steps to utilizing the framework.

  1. Open the system’s document picker
  2. Get the Content URI for the selected file(s)
  3. Copy the contents of file to cache directory
  4. Use the cached version of the file
  5. Clear the cache after the operation completes

Those are the steps to using the SAF. We will be following the listed approach to accomplish the task.

Limitations and disadvantages of this approach

It might not be ideal to always use this approach specially because we will be caching a copy to be used locally by our application.

Which will quickly grow the applications cache size. But if you think this will not be an issue with your current project. Then you can use it without any side effects at all.

Also this approach will not work if you want to directly modify the source file rather than a locally cached version. But it’s worth noting that this approach will download the file from remote server which might be advantageous in certain cases.

I was using this approach because I had to send some data via Rest API to remote server. Let us begin.

Opening system’s file picker

We can open the system’s UI for picking the document. Where we can either pick a single file or pick multiple files. We will look at both approaches. There is only a slight difference in picking a single file or multiple file.

We have this object which will help us show a system document picker. Briefly explained below.

object Picker {

    private const val FILE_TYPE = "*/*"
    private const val REQUEST_CODE = 1121

    fun showPicker(context: Activity) {

        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)

        // the type of file we want to pick; for now it's all
        intent.type = FILE_TYPE

        // allows us to pick multiple files
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        context.startActivityForResult(Intent.createChooser(intent, "Select file."), REQUEST_CODE)
    }

    fun onActivityResult(
        requestCode: Int,
        resultCode: Int,
        data: Intent?,
        onActionComplete: OnActionComplete
    ) {

        if (data == null) return // data is null we cannot do anything

        if (resultCode == RESULT_CANCELED) return // user cancelled the selection

        when (requestCode) {
            REQUEST_CODE -> {

                // user has selected single item
                val uri = data.data
                uri?.let {
                    println("DEBUG: $uri")
                    onActionComplete.onSuccess(uri = arrayListOf(it))
                    return
                }

                // user has selected multiple items
                val clipData = data.clipData
                clipData?.let {
                    val itemCount = it.itemCount
                    val uris = arrayListOf<Uri>()
                    for (index in 0 until itemCount) {
                        val itemUri = it.getItemAt(index).uri
                        println("DEBUG: $itemUri")
                        uris.add(itemUri)
                    }

                    onActionComplete.onSuccess(uri = uris)
                }

            }
        }
    }

    interface OnActionComplete {
        fun onSuccess(uri: List<Uri>)
        fun onFailure()
    }

}Code language: Kotlin (kotlin)

showPicker method will show the picker UI and the result will be passed on to onActivityResult from the Activity or Fragment‘s onActivityResult.

If you look closely you will see that there is OnActionComplete interface which has methods that will be invoked after the completion of the specific action.

Also Read : Interface in OOP: Guide to Polymorphism and Callbacks

TECHENUM

But how will you use the above object ? See this snippet for showing the file picker

mOpenFilePicker.setOnClickListener {
    Picker.showPicker(this)
}Code language: Kotlin (kotlin)

And for handling the result pass it from the onActivityResult callback.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    Picker.onActivityResult(requestCode, resultCode, data, this)
}Code language: Kotlin (kotlin)

We have successfully completed the selection action. Let us look at how we can copy the file locally in cache.

Caching the file from the URI

Look at this class named FileCache which has all the required methods to copy files from content:// URI.

It shouldn’t be too hard to understand what’s going on as I have commented major portions of the code.

class FileCache(
    private val mContext: Context
) {

    // content resolver
    private val contentResolver = mContext.contentResolver

    // to get the type of file
    private val mimeTypeMap = MimeTypeMap.getSingleton()

    private val mCacheLocation = mContext.cacheDir

    fun cacheThis(uris: List<Uri>) {
        executor.submit {
            uris.forEach { uri -> copyFromSource(uri) }
        }
    }

    /**
     * Copies the actual data from provided content provider.
     */
    private fun copyFromSource(uri: Uri) {

        val fileExtension: String = getFileExtension(uri) ?: kotlin.run {
            throw RuntimeException("Extension is null for $uri")
        }
        val fileName = queryName(uri) ?: getFileName(fileExtension)

        val inputStream = contentResolver.openInputStream(uri) ?: kotlin.run {
            throw RuntimeException("Cannot open for reading $uri")
        }
        val bufferedInputStream = BufferedInputStream(inputStream)

        // the file which will be the new cached file
        val outputFile = File(mCacheLocation, fileName)
        val bufferedOutputStream = BufferedOutputStream(FileOutputStream(outputFile))

        // this will hold the content for each iteration
        val buffer = ByteArray(DEFAULT_BUFFER_SIZE)

        var readBytes = 0 // will be -1 if reached the end of file

        while (true) {
            readBytes = bufferedInputStream.read(buffer)

            // check if the read was failure
            if (readBytes == -1) {
                bufferedOutputStream.flush()
                break
            }

            bufferedOutputStream.write(buffer)
            bufferedOutputStream.flush()
        }

        // close everything
        inputStream.close()
        bufferedInputStream.close()
        bufferedOutputStream.close()

    }

    private fun getFileExtension(uri: Uri): String? {
        return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri))
    }

    /**
     * Tries to get actual name of the file being copied.
     * This might be required in some of the cases where you might want to know the file name too.
     *
     * @param uri
     *
     */
    @SuppressLint("Recycle")
    private fun queryName(uri: Uri): String? {
        val returnCursor: Cursor = contentResolver.query(uri, null, null, null, null) ?: return null
        val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
        returnCursor.moveToFirst()
        val name: String = returnCursor.getString(nameIndex)
        returnCursor.close()
        return name
    }

    private fun getFileName(fileExtension: String): String {
        return "${System.currentTimeMillis().toString()}.$fileExtension"
    }

    /**
     * Remove everything that we have cached.
     * You might want to invoke this method before quiting the application.
     */
    fun removeAll() {
        mContext.cacheDir.deleteRecursively()
    }

    companion object {

        // base buffer size
        private const val BASE_BUFFER_SIZE = 1024

        // if you want to modify size use binary multiplier 2, 4, 6, 8
        private const val DEFAULT_BUFFER_SIZE = BASE_BUFFER_SIZE * 4

        private val executor = Executors.newSingleThreadExecutor()

    }

}Code language: Kotlin (kotlin)

This code will copy all the files from source to app’s mContext.cacheDir which can be queried later by listing files from there.

Let us finally see how we can combine everything in our MainActivity.

Also Read : Async Task in Android is Deprecated: There are Better Ways

TECHENUM

Wrapping everything up

Now that we have completed major portions of the code. Let us see how we can actually use it in our MainActivity.

The code for the design activity_main.xml.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.google.android.material.button.MaterialButton
        android:id="@+id/open_file_picker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:text="Open File Picker"
        app:layout_constraintBottom_toBottomOf="@+id/empty_cache"
        app:layout_constraintEnd_toEndOf="parent" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/empty_cache"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="16dp"
        android:text="Empty Cache"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

</androidx.constraintlayout.widget.ConstraintLayout>Code language: HTML, XML (xml)

And our final MainActivity looks like

class MainActivity : AppCompatActivity(), Picker.OnActionComplete {

    private lateinit var mOpenFilePicker: MaterialButton
    private lateinit var mEmptyCache: MaterialButton

    private val fc: FileCache by lazy {
        FileCache(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        mOpenFilePicker = findViewById(R.id.open_file_picker)
        mEmptyCache = findViewById(R.id.empty_cache)

        mOpenFilePicker.setOnClickListener {
            Picker.showPicker(this)
        }

        mEmptyCache.setOnClickListener {
            fc.removeAll()
        }
    }

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

    override fun onSuccess(uri: List<Uri>) {
        fc.cacheThis(uri)
    }

    override fun onFailure() {
    }

}Code language: Kotlin (kotlin)

Now that is all that we had to do. Everything should be available in the cache directory of the application.

What do you think about this approach to getting android content from content URI without converting it to file URI ?

Also Read : Kotlin Coroutine in Android : Understanding the Basics

TECHENUM

Related Posts