Access files and documents on Android

Introduction

I recently had to migrate legacy code in Fulguris Web Browser away from accessing files directly and instead use so called document's URIs.
It's a task that was pushed on me by Google as they were enforcing scoped storage from Android 11 and restricting access to Google Play for non compliant applications. Take a look at that Google Play policy update notice.

Requirement

I needed ways to open an existing document and create a new document to address Fulguris' bookmarks import and export use cases.
Fulguris legacy code would simply use the File APIs which are severely restricted by the scoped storage policies mentioned above.
Here is the corresponding Fulguris issue for reference.

Challenges

You would think showing a file selection and a file creation dialog should be straight forward on any platform in 2021. However I found it frustrating to gather the knowledge needed in the whirlwind of such coding tips you can find online. Many articles and samples are outdated or misleading. Also I was not exactly sure what to look for and how to tell apart proper solutions from clumsy ones. As a veteran of the smartphone wars I can draw from years of experience in coding on various embedded platforms but I'm still far from an Android expert as I was fighting for the loosing side, namely Symbian OS and Nokia. In retrospect things could have been a lot easier for me if I had come across that Storage Access Framework documentation before writing this article.

Code

Below are the solutions I came up with. While I'm fairly confident they work fine and are in line with Android's latest recommendation at time of writing, I'm also sure they could be improved one way or another. Feel free to login on the forum and join the discussion. During implementation I found it useful to look at the AOSP DocumentsUI source code which is basically Android's default file explorer. From there you can notably extract those magic constants defining defaults document providers.

Open document dialog

Here is an Android developer documentation sample showing how to launch that open document dialog. Unfortunately it is using some deprecated API and does not illustrate what to do with the resulting URI. Typically you should use ContentResolver to access the provided URI data through an input stream. See this article on how to consume URIs.
It should look like that in Kotlin, assuming this code is part of an activity or fragment class for context access:

Kotlin:
/**
* Starts bookmarks import workflow by showing file selection dialog.
*/
private fun showImportBookmarksDialog() {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "*/*" /* That's needed for some reason, crashes otherwise */
          // List all file types you want the user to be able to select
          putExtra(
            Intent.EXTRA_MIME_TYPES, arrayOf(
                "text/html", // .html
                "text/plain" // .txt
            )
        )
    }
    bookmarkImportFilePicker.launch(intent)
    // See bookmarkImportFilePicker declaration below for result handler
}

// Assuming you have context access as a fragment or an activity
val bookmarkImportFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {
        // Using content resolver to get an input stream from selected URI
        // See:  https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
        result.data?.data?.let{ uri ->
            context?.contentResolver?.openInputStream(uri).let { inputStream ->
                val mimeType = context?.contentResolver?.getType(uri)
                // TODO: do your stuff like check the MIME type and read from that input stream
        }
    }
}


Create document dialog

Here is how to ask user for a file to create using Kotlin:
Kotlin:
/**
* Start bookmarks export workflow by showing file creation dialog.
*/
private fun showExportBookmarksDialog() {
    //TODO: specify default path
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "text/plain" // Specify type of newly created document

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

            // Those magic constants were taken from AOSP DocumentsUI source code:
            val AUTHORITY_DOWNLOADS = "com.android.providers.downloads.documents";
            val ROOT_ID_DOWNLOADS = "downloads";

            // It's actually best not to specify the initial directory URI it seems as it then uses the last one used
            // Specifying anything other than download would not really work anyway
            val uri = DocumentsContract.buildDocumentUri(AUTHORITY_DOWNLOADS, ROOT_ID_DOWNLOADS)
            putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri)
        }

        var timeStamp = ""
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            val dateFormat = SimpleDateFormat("-yyyy-MM-dd-(HH:mm:ss)", Locale.US)
            timeStamp = dateFormat.format(Date())
        }
    // Specify default file name, user can change it.
    // If that file already exists a numbered suffix is automatically generated and appended to the file name between brackets.
    // That is a neat feature as it guarantees no file will be overwritten.
    putExtra(Intent.EXTRA_TITLE, "FulgurisBookmarks$timeStamp.txt")
    }
    bookmarkExportFilePicker.launch(intent)
    // See bookmarkExportFilePicker declaration below for result handler
}

//
val bookmarkExportFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
        result: ActivityResult ->
    if (result.resultCode == Activity.RESULT_OK) {

        // Using content resolver to get an input stream from selected URI
        // See:  https://commonsware.com/blog/2016/03/15/how-consume-content-uri.html
        result.data?.data?.let{ uri ->
            context?.contentResolver?.openOutputStream(uri)?.let { outputStream ->
            // TODO: Write your stuff into that output stream
        }
    }
}

Open issues

  • How to reliably point DocumentsUI to a specific folder?
  • Which URI construction method is suppose to build those documents URI with colon?
  • Create a sample application.

Resources

 
Last edited:
Top