← Back to list

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…

sepideh akbarinezhad · 2026-05-29 09:22 · 2 claps · 3.1 min read
#android-security #android-privacy #scoped-storage #saf #mediastore
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 🔒 · Cybersecurity 👗 · Fashion

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