Scoped Storage Deep Dive — Part 2: How SAF Enables User-Controlled File Access
In the previous part, we explored why Android moved away from unrestricted filesystem-style access to shared storage and introduced Scoped…
Scoped Storage Deep Dive — Part 2: How SAF Enables User-Controlled File Access
In the previous part, we explored why Android moved away from unrestricted filesystem-style access to shared storage and introduced Scoped Storage as a more privacy-focused model.
Although the Storage Access Framework (SAF) was introduced much earlier in Android 4.4 (API 19), it became significantly more important as Android gradually restricted broad shared storage access previously granted through storage permissions.
SAF became one of the primary mechanisms Android uses for controlled, user-mediated file access.
The Core Idea Behind SAF
The central idea behind SAF is simple:
Applications should not freely explore a user’s shared storage. Instead, users should intentionally decide which files or folders an application can access.
Instead of accessing raw filesystem paths directly, applications request access through a system-controlled picker where the user explicitly chooses what to share.
Rather than returning filesystem paths, SAF returns content:// URIs, which are accessed through ContentResolver.
content://com.android.providers.downloads.documents/document/1234
This creates a more privacy-conscious model where file access becomes:
- explicit
- limited
- user-controlled
How SAF Works in Practice
1. Launching the System File Picker
SAF provides three main entry points depending on the use case:
A: Opening an Existing File — ACTION_OPEN_DOCUMENT
Use this when your app needs the user to select one or more existing files to read or edit. The system file picker appears, the user navigates and selects, and SAF returns a content:// URI for each selected file.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "application/pdf"))
}
filePickerLauncher.launch(intent)
B: Creating a New File — ACTION_CREATE_DOCUMENT
Use this when your app wants the user to save a new file to a location of their choice. The system picker asks the user where to save and what name to use. Your app provides a suggested filename and MIME type.
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "my_note.txt")
}
C: Accessing a Folder Tree — ACTION_OPEN_DOCUMENT_TREE
Use this when your app needs ongoing access to a whole directory and everything inside it. The user selects a folder, and SAF grants your app access to that folder and all its descendants.
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
2. Handling the Result
All SAF entry points return a content:// URI.
private val filePickerLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri: Uri? = result.data?.data
uri?.let {
// CRITICAL: Persist the permission
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
// Now work with the URI
handleSelectedFile(it)
}
}
}
3. Understanding the Permission Model
Temporary Access
When a user selects a file, Android automatically grants a temporary URI permission grant to that specific content URI. This allows the app to immediately read or write the file without requiring storage permissions.
Persistable Access
Temporary access can be lost when:
- the app process is killed
- the device restarts
If the URI is used again without persistence, access may fail with a SecurityException. To keep long-term access, you must call:
takePersistableUriPermission()
This converts temporary access into persistent access.
In cases where the file is immediately consumed (read/uploaded/copied), persistable permission is not necessary.
4. Working with the returned Content URIs
SAF does not return file paths. It returns content:// URIs, which must be handled using ContentResolver.
ContentResolver is the Android system component that provides a unified API for accessing data exposed via
content://URIs. It resolves the URI’s authority and routes the request to the appropriate ContentProvider (such as MediaStore, Contacts, or a DocumentsProvider used by SAF), then returns the requested data.
//Reading to a URI
contentResolver.openInputStream(uri).use { stream ->
val content = stream?.bufferedReader()?.readText()
}
//Writing to a URI
contentResolver.openFileDescriptor(uri, "wt").use { pfd ->
FileOutputStream(pfd.fileDescriptor).use { output ->
output.write("new content".toByteArray())
}
}
"wt"opens the file for writing and truncates existing content.
Working with Document Trees
When you use ACTION_OPEN_DOCUMENT_TREE, you get a tree URI like:
content://com.android.externalstorage.documents/tree/primary%3ADownloads
To access files inside the tree, you must build document URIs properly:
val documentId = DocumentsContract.getTreeDocumentId(treeUri)
val childUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
Key Difference from Legacy Pickers
Although ACTION_OPEN_DOCUMENT (SAF) and legacy pickers like ACTION_GET_CONTENT or ACTION_PICK both open a system UI and return a content:// URI, they differ in how access to that URI is handled.
Legacy pickers:
- return a URI intended for temporary use
- do not support persistable URI permissions
- access may not be valid after process death or later reuse
SAF:
- designed for long-term, user-granted access
- supports persistable permissions via
takePersistableUriPermission() - allows the system to maintain access grants across app restarts
In short, both return URIs, but SAF adds a structured and system-managed access model on top of the selection process.
What’s Next: MediaStore
SAF is ideal when the user selects specific files or folders.
But when an app needs to:
- display all images
- scan videos
- query audio files
SAF is not the right tool.
That’s where MediaStore becomes important, which we’ll explore in next part.
메타데이터
- post_id
- 97caa184a368
- slug
- scoped-storage-deep-dive-part-2-how-saf-enables-user-controlled-file-access-97caa184a368
- url
- https://medium.com/@sepidehAkbarinezhad/scoped-storage-deep-dive-part-2-how-saf-enables-user-controlled-file-access-97caa184a368
- canonical_url
- https://medium.com/@sepidehAkbarinezhad/scoped-storage-deep-dive-part-2-how-saf-enables-user-controlled-file-access-97caa184a368
- author_url
- https://medium.com/@sepidehAkbarinezhad
- status
- ok
- fetched_at
- 2026-06-22 05:41:33