Storage Access Framework — Complete Guide
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
DocumentFilewrapper for tree-based operations. - Use
OpenableColumnsfor file metadata. - Check
contentResolver.persistedUriPermissionsfor existing permissions.
Common Mistakes with storage saf document
- Using
foldlinstead offoldl'causing stack overflow on large lists - Forgetting
deriving (Show, Eq)on custom data types needed for debugging - 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
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro