Scoped storage
The model that replaced the free-for-all: app-specific directories, MediaStore collections, per-file grants, and the permissions that survived — plus the ones that did not.
Scoped storage is not one rule. It is a set of directories and APIs that replace a single broad permission with a model where an app gets its own space, an index of shared media, and explicit grants for everything else.
TL;DR#
- An app always has free access to its app-specific directories, internal and external.
- Shared media is reached through , which indexes photos, video and audio.
- Documents and downloads go through the Storage Access Framework or, from API 30Android 11, a broad read grant.
requestLegacyExternalStoragewas an opt-out in API 29Android 10; API 30Android 11 enforced the model for apps targeting 30.- The direction of travel is per-item consent: the photo picker and selected-photos access replace permission grants with item grants.
Mental model#
Think of a public library.
Every patron has a locker — the app-specific directory — that only they can open.
The catalogue is MediaStore: it lists what is on the shelves, and you can search it, but it does not give you the books.
To take a book out of the archive — documents, downloads — you need a librarian to fetch it and record the loan. That is the Storage Access Framework.
Where the analogy breaks#
A library's catalogue and shelves are separate. MediaStore is both: it is an index and the API through which files are opened, so "searching" and "reading" go through the same provider. And a URI grant is not a loan — it has no due date, and it can be revoked by the user at any time without the app being told until it tries to use it.
The big picture#
Text description of this diagram
A treemap of an app's storage access: its own files and cache directories, other apps' media through MediaStore, documents and downloads through the Storage Access Framework or the photo picker, and blocked paths including other apps' data and all-files access. The four groups show the shape of the scoped model: private space is free, media is indexed, documents are picked, everything else is blocked.
The four groups are also four different APIs. Choosing the wrong one is the most common scoped-storage mistake: an app that needs one document from the user does not need a storage permission at all.
Deep dive#
The app-specific directories#
Two locations, both free to use:
/data/user/0/<package>/files/ internal, private
/storage/emulated/0/Android/data/<package>/files/ external, app-specificThe internal one is removed on uninstall. The external one is also removed on uninstall, but is visible to other apps with special access (file managers, backup tools) and to the user over USB.
The distinction matters for what the user expects: a file the app manages for itself belongs in internal storage; a file the user might want to find in a file manager belongs in the external directory or in MediaStore.
MediaStore#
MediaStore is a content provider that indexes media on the shared volume. The collections are Images, Video,
Audio and Downloads.1
Querying:
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_TAKEN,
)
contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
"${MediaStore.Images.Media.DATE_TAKEN} DESC",
)?.use { cursor ->
// read rows
}The result is a set of content URIs, not file paths. That is deliberate: the URI is a capability, and the app does not need to know where the file lives.
Writing has an ownership rule. From API 29Android 10, an app can modify media it created; to modify another app's media it must request a write grant, and from API 30Android 11 the system may show a confirmation dialog.
The permissions that survived#
| Permission | Status |
|---|---|
READ_EXTERNAL_STORAGE | narrowed to media; effectively replaced by READ_MEDIA_* on 33+ |
WRITE_EXTERNAL_STORAGE | no longer grants broad write access; kept for compatibility |
READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO | the 33+ split permissions |
READ_MEDIA_VISUAL_USER_SELECTED | selected photos, API 34Android 14 |
MANAGE_EXTERNAL_STORAGE | all-files access, special, Play-reviewed |
The split into READ_MEDIA_* is significant: an app that only needs images no longer asks for audio. Each grant is
narrower, and the user sees a more specific prompt.
MANAGE_EXTERNAL_STORAGE is the exception, and it is deliberately hard to get.2 It is intended for file managers,
backup tools and antivirus apps; Play policy restricts its use, and the user grants it in a special settings screen
rather than a dialog.
The photo picker#
API 33Android 13 introduced the system photo picker. The app launches it, the user selects items, and the app receives URIs — with no storage permission at all.
Text description of this diagram
Three ways to get a photo in 2015, 2020 and 2024. In 2015 the app asked for READ_EXTERNAL_STORAGE and could then read any file. In 2020 it queried MediaStore and needed a grant only for files it did not create. In 2024 the system photo picker returns individual URIs, so no media permission is needed at all.
It was backported through Play services, which means it works on devices older than the API level that introduced it. That backport is one of the more consequential platform decisions in years: it let apps adopt the safer model without waiting for an OS update.
Selected photos#
API 34Android 14 added READ_MEDIA_VISUAL_USER_SELECTED: the user picks a subset of their photos and videos, and
the app can access those items, and only those, across sessions.
The app must handle three states: full access, partial access, and no access. That three-state model is the new normal for media permissions, and it is why a boolean "do I have permission" check is no longer sufficient.
The external storage migration#
Apps that had years of files in the shared volume needed a path. The platform provides:
MediaStore.Downloadsfor files the user downloads.- App-specific external directories for files the app owns.
- SAF tree grants for directories the user chooses.
An app migrating from legacy storage typically moves its files into its app-specific directory on first run, or adopts a SAF tree for a user-chosen folder.
What scoped storage does not fix#
It does not stop an app from reading files the user gives it. It does not prevent an app from uploading those files. And it does not remove files already written to shared storage by legacy apps. It bounds access, not use.
Why MediaStore is an index and an API at once#
MediaStore is a content provider whose database lists files, and whose openFile resolves them. The two halves can
disagree: a file written directly into a media directory exists on disk but may not appear in the index until a scan
runs.
That is why writing media through MediaStore rather than by path is not just a policy requirement but a correctness one: the insert registers the file, and the app receives a URI it can use immediately.
Scoped storage and file paths#
The model does not forbid paths entirely. An app can still use File APIs inside its own directories, and MediaStore
returns DATA columns on some releases for compatibility.
What it forbids is assuming a path works for another app's file. The URI is the capability; the path is an implementation detail that may or may not be resolvable.
The three-state media permission#
With selected photos, a media permission has three outcomes:
| State | What the app sees |
|---|---|
| Full | the whole library through MediaStore |
| Partial | only the user-selected items |
| None | nothing |
An app that checks a boolean will treat partial as full and show an incomplete gallery, or as none and ask again. The correct check is on the specific permission, and the UI should reflect what the user chose.
What the picker cost#
The picker is a separate process and a separate UI. That means:
- An extra activity transition, which is a visible latency the app cannot remove.
- No custom filtering beyond MIME type and a few options.
- No access to the library beyond the selection.
For an app whose core feature is browsing a photo library, the picker is insufficient and READ_MEDIA_IMAGES with a
justification is the right call. For an app that needs one avatar, the picker is strictly better.
Migration in practice#
An app moving from legacy storage typically:
- Writes new files to its app-specific directory.
- Migrates existing shared-volume files into MediaStore or the app-specific directory on first run.
- Uses SAF for any user-chosen folder it needs to keep.
The migration must be idempotent and must handle a partially migrated state, because it can be interrupted by a process kill.
What "all files access" really means#
MANAGE_EXTERNAL_STORAGE grants read and write to nearly the whole shared volume, including other apps' media and
downloads. It is the modern equivalent of the old WRITE_EXTERNAL_STORAGE, and it exists because a file manager
genuinely cannot function without it.
The platform's approach is to keep the permission available but make it expensive: a special settings screen rather than a dialog, a Play policy review, and a visible declaration in the store listing. The friction is the point.
Why MediaStore is a provider and not a directory#
MediaStore is a content provider whose database is the index, and whose openFile resolves the actual bytes. The
index and the bytes can be in different places, which is what lets the same API cover local files, cloud-backed
files and, on some devices, files on adopted storage.
The consequence for an app is that a MediaStore URI is the only reliable handle. A path may work today and stop working after a provider change.
The photo picker's backport#
The picker arrived in API 33, but it was backported through Play services so that it works on earlier releases. That is unusual: most platform features wait for the OS update.
The backport mattered because the storage model's safety depends on apps adopting the picker, and waiting for OS adoption would have taken years.
What a well-behaved storage app looks like#
- It uses its app-specific directories for its own data.
- It uses MediaStore for media it creates or edits.
- It uses the picker for one-off user selections.
- It uses a SAF tree when it needs a folder the user chooses.
- It asks for
MANAGE_EXTERNAL_STORAGEonly if its core function is file management. - It handles a revoked grant by falling back to the picker rather than crashing.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 3 | 1.5 Cupcake |
|
| 12 | 3.1 Honeycomb |
|
| 15 | 4.0.3 Ice Cream Sandwich |
|
| 19 | 4.4 KitKat |
|
| 24 | 7.0 Nougat |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 33 | 13 Tiramisu |
|
| 34 | 14 Upside Down Cake |
|
| 37 | 17 Cinnamon Bun |
|
Under the hood#
- MediaStore.javathe collections, columns and the insert/query contract
- StorageUserConnection.javaper-user storage and the FUSE mounts
- the MediaStore implementation and its access checks
- Environment.javathe directory constants and their deprecations
Curiosity corner#
- MediaStore is a provider, not a database you query directly. The index is maintained by MediaProvider, and a file written directly to the filesystem may not appear in it until a scan.
- The write-grant dialog is a real user decision. From Android 11, modifying another app's media can require explicit confirmation, which is why photo editors must handle a denial gracefully.
- The picker is a separate process. Your app does not see the library; it sees the result of a selection.
Try it#
adb shell content query --uri content://media/external/images/media --projection _id:_display_name:mime_typeExample output
Row: 0 _id=42, _display_name=IMG_20240101_120000.jpg, mime_type=image/jpegadb shell dumpsys package com.example.app | grep -A2 "runtime permissions"Example output
android.permission.READ_MEDIA_IMAGES: granted=true
android.permission.READ_MEDIA_VISUAL_USER_SELECTED: granted=trueadb shell ls -la /storage/emulated/0/Android/data/com.example.app/Example output
drwxrwx--x 2 u0_a123 u0_a123 4096 ... filesCheck yourself#
Check yourself
Why did WRITE_EXTERNAL_STORAGE become a privacy problem?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsStorage updates in Android 11developer.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 16 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 11developer.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 14developer.android.comaccessed 2026-10-27↩
- docsStorage overviewsource.android.comaccessed 2026-10-27↩