Skip to content

Storage Access Framework — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about Storage Access Framework. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

The Problem

You use ACTION_OPEN_DOCUMENT_TREE but lose access after the app restarts, or you can't read a file from a tree URI.

Wrong Approach ❌

// Requesting tree but not persisting the URI
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_TREE)

// In onActivityResult:
val treeUri = data?.data
// URI is not persisted — lost after reboot!

Output: User selects a folder, app restarts, and the URI is invalid.

Right Approach ✅

class DocumentManager(private val context: Context) {

    fun pickDirectory(activity: Activity, requestCode: Int) {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
            flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
                    Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        }
        activity.startActivityForResult(intent, requestCode)
    }

    fun persistUri(uri: Uri) {
        context.contentResolver.takePersistableUriPermission(
            uri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION or
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
        )
        // Save URI string to shared preferences
        savePreference("tree_uri", uri.toString())
    }

    fun getSavedTreeUri(): Uri? {
        val uriString = getPreference("tree_uri")
        return uriString?.let { Uri.parse(it) }
    }

    fun readDocument(uri: Uri): String? {
        return try {
            context.contentResolver.openInputStream(uri)?.use { input ->
                input.bufferedReader().readText()
            }
        } catch (e: Exception) {
            null
        }
    }

    fun listDocuments(treeUri: Uri): List<DocumentFile> {
        val treeDocument = DocumentFile.fromTreeUri(context, treeUri)
        return treeDocument?.listFiles()?.toList() ?: emptyList()
    }

    fun createDocument(treeUri: Uri, fileName: String, mimeType: String): Uri? {
        val treeDocument = DocumentFile.fromTreeUri(context, treeUri)
        return treeDocument?.createFile(mimeType, fileName)?.uri
    }

    fun createDirectory(treeUri: Uri, dirName: String): Uri? {
        val treeDocument = DocumentFile.fromTreeUri(context, treeUri)
        return treeDocument?.createDirectory(dirName)?.uri
    }

    fun deleteDocument(uri: Uri): Boolean {
        return DocumentFile.fromSingleUri(context, uri)?.delete() ?: false
    }

    fun pickSingleDocument(activity: Activity, requestCode: Int, mimeType: String = "*/*") {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
            addCategory(Intent.CATEGORY_OPENABLE)
            type = mimeType
        }
        activity.startActivityForResult(intent, requestCode)
    }

    // Read file metadata
    fun getFileInfo(uri: Uri): String? {
        val cursor = context.contentResolver.query(uri, null, null, null, null)
        return cursor?.use {
            if (it.moveToFirst()) {
                val name = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
                val size = it.getLong(it.getColumnIndexOrThrow(OpenableColumns.SIZE))
                "$name ($size bytes)"
            } else null
        }
    }
}

Output: Persistent document access across app restarts.

Prevention

  • Always call takePersistableUriPermission() for long-term access.
  • Save URI strings to DataStore/SharedPreferences.
  • Use DocumentFile wrapper for tree-based operations.
  • Use OpenableColumns for file metadata.
  • Check contentResolver.persistedUriPermissions for existing permissions.

Common Mistakes with storage saf document

  1. Using foldl instead of foldl' causing stack overflow on large lists
  2. Forgetting deriving (Show, Eq) on custom data types needed for debugging
  3. Placing the wildcard pattern first in case expressions, making all subsequent patterns unreachable

These mistakes appear frequently in real-world Android code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

### How long does a persistable URI permission last?

Until the user revokes it in system settings, or until the app is uninstalled. It survives device reboots and app updates.

### What is the difference between ACTION_OPEN_DOCUMENT and ACTION_OPEN_DOCUMENT_TREE?

ACTION_OPEN_DOCUMENT picks a single file. ACTION_OPEN_DOCUMENT_TREE picks a directory tree. Tree URIs allow read/write access to all files in the subtree.

### Can I use SAF without persistable permissions?

Yes, for one-time access. Without takePersistableUriPermission(), the permission lasts only until the Process is killed or device is rebooted.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro