The history of Android storage
From a real SD card that any app could read to a virtualised filesystem with per-app views: how WRITE_EXTERNAL_STORAGE became a privacy disaster, and how Android unwound it.
Early Android had a simple storage model: there was internal storage, private to each app, and there was external storage, shared by everyone. That simplicity became a privacy problem, and the last decade of Android storage work has been the slow, painful process of undoing it.
TL;DR#
- Internal storage was always per-app and private. External storage was a shared volume, historically a real SD card.
WRITE_EXTERNAL_STORAGEgave an app access to essentially every user file. It was the most over-granted permission on the platform.- As SD cards disappeared, Android emulated external storage on the same flash — first with a kernel filesystem, later with FUSE.
- The shared model was replaced in stages: scoped storage in API 29Android 10, enforced in API 30Android 11, and refined by the photo picker in API 33Android 13.
- Each stage moved the boundary from capability (you may write files) to per-item consent (you may read this file).
Mental model#
Think of a shared office fridge with no labels.
Everyone's lunch went in, and everyone could take anything. It worked while there were five people. With fifty, the fridge became a free-for-all: things disappeared, allergies were ignored, and nobody could tell whose food was whose.
The fix was not a bigger fridge. It was labelled shelves, a per-person shelf, and a rule that you can only take from someone else's shelf with their permission.
Where the analogy breaks#
A fridge is physical and the shelves are obvious. In a filesystem, an app's "shelf" is a directory whose path changes between Android versions, whose permissions are enforced by the kernel, and which on modern Android is presented through a userspace filesystem that can show different views to different apps. None of that is visible from the path.
The big picture#
Text description of this diagram
A treemap of what an app can read and write in shared storage, split into four groups: its own directories, other apps' media, documents and downloads, and blocked or special paths. Own directories are always writable without permission. Other apps' media needs MediaStore or a picker. Documents need the Storage Access Framework or a broad grant. Other apps' data and all-files access are blocked or require a special permission.
The treemap's four groups are the modern model: private, media, documents, blocked. The historical story is how the middle two moved out of the first.
Deep dive#
The two storages#
The original model, which still shapes the API names:
- Internal storage is the app's private directory on the data partition. No permission needed; no other app can read it.
- External storage is a shared volume. Historically this was a physical SD card; it was world-readable with the right permission, and it survived app uninstall.
The names persisted long after the physical distinction did. A phone with no SD card still has "external storage" — it is just a directory on the same flash, presented as a separate volume.
The permission problem#
READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE were normal permissions: granted at install time, with no runtime
dialog. An app that asked for write access could read every photo, every document and every other app's files on the
shared volume.
The consequences were predictable:
- Photo and file-manager apps read everything, whether or not they needed to.
- An app could modify another app's files.
- Uninstalling an app did not remove what it had written to shared storage.
- There was no record of what had been read.
By the time Android 6.0 made permissions runtime-granted, the damage was structural: the permission was too broad to be meaningful, so users granted it without thinking.
Emulating the SD card#
When manufacturers stopped shipping SD cards, Android kept the /sdcard path by emulating it on the data
partition. The first implementations used a kernel filesystem, sdcardfs, which presented per-app views of the same
underlying directory and enforced UID-based access in the kernel.1
sdcardfs was an out-of-tree Android addition: efficient, but another kernel patch to maintain. Android 11 replaced
it with FUSE, a userspace filesystem that could implement the same per-app views with far more flexibility — and,
crucially, could implement scoped storage's rules, which a simple UID check could not.
The move to FUSE cost performance; the platform mitigated it with FUSE passthrough, which lets reads and writes bypass the userspace daemon for files the app is allowed to access. The trade was deliberate: policy in userspace is easier to change than policy in the kernel.
Scoped storage#
API 29Android 10 introduced scoped storage: an app gets its own external directory freely, and accesses other
apps' media through MediaStore. The first release allowed requestLegacyExternalStorage as an opt-out, because too
many apps would have broken.
API 30Android 11 enforced it for apps targeting 30. The opt-out stopped working. From that point, the model was: private directory, MediaStore for media, SAF for documents.2
The photo picker and per-item consent#
API 33Android 13 added the system photo picker: the user selects specific photos and the app receives URIs for those items only. API 34Android 14 added selected-photos access, where the user grants access to a chosen subset of their library that the app can re-read.
The arc is now complete: from "this app may read all storage" to "this app may read these three photos". Each step reduced the scope of a grant and increased the visibility of what was granted.
Why it took so long#
Storage is the most compatibility-sensitive part of the platform. A file manager genuinely needs broad access; a backup tool needs to enumerate; a photo editor needs to write back to the original. A model that works for all three without opening the door for everyone is hard, and the platform iterated in public for years.
What an SD card actually was#
A removable SD card was a block device shared by every app. It had no per-app quota, no ownership model and no journaling requirement. An app could write a file and another app could read it, by path, with no grant.
That model was convenient for the user — the card worked in a laptop — and catastrophic for isolation. Deleting an app did not delete its files; uninstalling a photo app left its cache in the shared volume forever.
Emulation changed the storage, not the API#
When phones stopped shipping removable cards, Android kept the /sdcard path by emulating the volume on the data
partition. Apps kept working because the path did not change; only the device behind it did.
The emulation layer had to present a per-app view: the same path, but with different contents depending on which app was asking. That is a policy requirement a plain filesystem cannot express, which is why it needed a special filesystem in the kernel and, later, a userspace daemon.
The performance tax#
sdcardfs was fast because it was in the kernel. FUSE is slower because every file operation is a round trip to a
userspace process.
FUSE passthrough recovers most of the loss by allowing direct access to files the app is permitted to open, so the common case does not pay the round trip. The design goal was to keep the policy in userspace while keeping the hot path in the kernel.
What the transition cost developers#
Every storage migration broke some apps. The scoped-storage transition was especially hard because:
- Apps had built features around arbitrary paths, which no longer worked.
- File managers genuinely needed broad access, which required a special permission.
- The opt-out made it possible to ignore the change, which delayed the migration rather than avoiding it.
The lesson the platform drew was to stage the change over multiple releases, and to provide a migration path
(MediaStore.Downloads, SAF trees, app-specific directories) before enforcing it.
The compatibility surface#
Every storage change has a compatibility cost, and the platform has learned to stage them:
- Provide a migration API before enforcing.
- Allow an opt-out for one release.
- Enforce for new target SDKs first.
- Backport the safe alternative (the picker) so apps do not have to wait.
The scoped-storage transition used all four, and it still took three releases. The lesson is that storage is the slowest part of the platform to change, because every app touches it.
What the user sees#
For the user, the changes are mostly invisible until something breaks: an app that can no longer find its files, or a picker that appears where a permission dialog used to.
The visible improvement is in Settings, where the app's access is enumerated as items and folders rather than as a capability. That is the privacy model becoming legible.
The remaining gap#
Storage is bounded now, but use is not. An app with a grant can still upload what it reads. The platform's answer to that is the data-access auditing and the privacy dashboard, which log access rather than preventing it.
The two mechanisms are complementary: scoped storage decides what is reachable, auditing makes what is reached visible.
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#
- the volume daemon that mounts and manages storage
- StorageVolume.javathe public description of a volume
- StorageUserConnection.javaper-user storage mounting
- sdcard.cppthe FUSE daemon that implements the emulated volume
Curiosity corner#
- The path
/sdcardstill exists on phones with no SD card. It is a compatibility alias for the emulated volume, and apps that hardcoded it kept working long after the hardware disappeared. - An app's external directory is deleted on uninstall, while files it wrote to the shared volume historically were not — one of the concrete privacy problems scoped storage fixed.
- FUSE is slower than a kernel filesystem, which is why passthrough exists. Every storage redesign since has had to answer the performance question.
Try it#
adb shell sm list-volumes allExample output
emulated;0 mounted
public:179,1 mountedadb shell dumpsys mount | grep -i "volume|path" | head -20Example output
Internal storage: /data
External storage: /storage/emulated/0adb shell run-as com.example.app ls -la /data/data/com.example.app/Example output
/data/user/0/com.example.app/files
/storage/emulated/0/Android/data/com.example.app/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 overviewsource.android.comaccessed 2026-10-27↩
- docsStorage updates in Android 11developer.android.comaccessed 2026-10-27↩
- docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docs10 storagedeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 11developer.android.comaccessed 2026-10-27↩
- docsEROFS filesystemsource.android.comaccessed 2026-10-27↩
- docsPerubahan perilaku: aplikasi yang menargetkan API level 28+developer.android.comaccessed 2026-10-27↩