Content providers
A provider puts your data behind a URI, with permissions enforced by the system rather than by your code. It is the oldest IPC surface in Android and still the one with the strongest access control.
Every photo you have ever taken is reachable through a URI. Not a file path — a URI that a provider resolves, with a permission check in front of it. That indirection is what lets the gallery, the camera, a chat app and a cloud backup service share data without any of them trusting the others.
TL;DR#
- A exposes data as URIs. Clients use a , never the provider object.
- URIs have an authority (who), a path (what), and optionally an ID (which row).
- Permissions are enforced by the framework —
readPermission,writePermission,path-permissionand URI grants — not by your code. - Providers are created before the Application object, so
onCreatemust be fast. - Modern apps mostly consume providers (, ) rather than write their own; a Room database is usually the better choice behind your own app.
Mental model#
A provider is a records office.
You do not walk into the archive; you go to the counter and hand over a reference number. The clerk checks your badge, fetches the folder, and gives you exactly the folder you asked for. You never see the shelves, and you cannot take a folder you are not cleared for.
Where the analogy breaks#
The records office is a single clerk who also serves other counters in the building, and the
building opens the office before it opens anything else. Provider onCreate runs before the
Application's, because components that start with the process may need data immediately. That
ordering is unusual enough to cause real bugs when a provider assumes the app is fully initialised.
The big picture#
Text description of this diagram
A sequence diagram of the component start path. The system resolves the target component, checks export and permission, and creates a process through Zygote if needed. Within the new process, ActivityThread binds the Application and then installs content providers — providers are created before the Application object, which is why a provider's onCreate must not depend on application-level initialisation. Client calls then arrive over Binder on a binder thread, not on the main thread.
That ordering — providers before Application.onCreate — is the single most surprising fact about
providers, and it is why so many apps install their dependency graph in a provider rather than in
Application. AndroidX Startup does exactly that.
Deep dive#
The URI contract#
content://com.example.notes.provider/notes/42
└──────┘ └─────────────┬──────────────┘└──┬──┘└┬┘
scheme authority path id| Part | Meaning |
|---|---|
| scheme | always content |
| authority | globally unique; declared in the manifest, usually package-based |
| path | a collection, e.g. notes |
| id | one row, e.g. 42 |
The provider parses that URI into a UriMatcher code and dispatches:
class NotesProvider : ContentProvider() {
private lateinit var db: NotesDatabase
private val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "notes", NOTES)
addURI(AUTHORITY, "notes/#", NOTE_ID)
}
override fun onCreate(): Boolean {
// Runs before Application.onCreate. Keep it cheap.
db = NotesDatabase.get(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor = when (matcher.match(uri)) {
NOTES -> db.notesDao().queryAllCursor()
NOTE_ID -> db.notesDao().queryCursor(uri.lastPathSegment!!.toLong())
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
override fun getType(uri: Uri): String = when (matcher.match(uri)) {
NOTES -> "vnd.android.cursor.dir/vnd.$AUTHORITY.note"
NOTE_ID -> "vnd.android.cursor.item/vnd.$AUTHORITY.note"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
// insert, update, delete, and the abstract members omitted for brevity
}The getType return value is not cosmetic. It is how an implicit ACTION_VIEW intent with a
content:// URI gets a MIME type, which is how the system knows which apps can open it.
Permission model#
Four mechanisms, in increasing specificity:
android:exported— whether anyone outside the UID may talk to it at all.android:readPermission/android:writePermission— separate gates for reads and writes.android:path-permission— different permissions for different paths within the same provider.- URI permission grants — a temporary, per-URI grant handed to another app at runtime.
<provider
android:name=".NotesProvider"
android:authorities="com.example.notes.provider"
android:exported="true"
android:readPermission="com.example.notes.permission.READ_NOTES"
android:writePermission="com.example.notes.permission.WRITE_NOTES">
<path-permission
android:pathPrefix="/public"
android:readPermission="android.permission.INTERNET" />
</provider>The fourth mechanism is the one that matters for sharing. FLAG_GRANT_READ_URI_PERMISSION on an
intent gives the receiver access to that one URI, for as long as the receiving activity lives:
val share = Intent(Intent.ACTION_SEND).apply {
type = "image/png"
putExtra(Intent.EXTRA_STREAM, photoUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(share, null))This is the correct way to share a file. Copying the file to external storage and sending a file://
URI is both a permission hole and, since Android 7, blocked by FileUriExposedException.
Observing changes#
Providers can notify observers when data changes, which is how a list screen refreshes when another app (or another part of yours) writes:
class NotesViewModel(
private val resolver: ContentResolver,
) : ViewModel() {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
val notes: StateFlow<List<Note>> = _notes.asStateFlow()
private val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
reload()
}
}
init {
resolver.registerContentObserver(NOTES_URI, true, observer)
reload()
}
override fun onCleared() {
resolver.unregisterContentObserver(observer)
}
private fun reload() {
// query and map into _notes
}
}Providers on modern Android can also register with setNotificationUri on a cursor, which lets the
system wake the cursor when the underlying data changes. It is the mechanism RecyclerView adapters
have used since the CursorAdapter era.
Providers in the real world#
Most apps do not write one. They consume them:
| Provider | What it gives you |
|---|---|
MediaStore | images, video, audio, documents — the system media index |
ContactsContract | contacts and call logs, permission-gated |
CalendarContract | calendar events |
DocumentsProvider | the Storage Access Framework's file tree |
Settings | system settings values, mostly read-only |
Since Android 11, scoped storage means the media providers are the only way to reach other apps'
media without a broad storage permission.1 Since Android 13, the
granular media permissions (READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO) replaced
READ_EXTERNAL_STORAGE for that purpose.2
The Storage Access Framework#
DocumentsProvider is the provider type behind the system file picker. An app that implements one can
appear in the picker as a storage location — that is how cloud drives integrate.
val open = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
uri ?: return@registerForActivityResult
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION,
)
viewModel.open(uri)
}takePersistableUriPermission is what makes a grant survive a reboot: without it, the URI is readable
only for the lifetime of the receiving activity.3
Call, and the escape hatch#
Not every operation is CRUD. Providers expose a call method for provider-specific commands:
val bundle = contentResolver.call(
NOTES_URI,
"rebuildIndex",
null,
null,
)It is the provider equivalent of an RPC method, and it goes through the same permission checks as a query. MediaStore and the contacts provider use it for operations that do not map onto rows — a scan, a rebuild, a refresh.
The reason it exists: a provider's query/insert/update/delete surface is fixed by the abstract
class, and adding a method would break binary compatibility. call is the extension point that let the
API evolve without changing the base class.
Cross-process cost, measured#
Every provider call is a Binder transaction, and a transaction has a fixed cost even when it returns nothing. Three patterns dominate real provider performance:
Batch writes. A hundred individual insert calls are a hundred transactions. A ContentProviderOperation
batch is one:
val ops = notes.map { note ->
ContentProviderOperation.newInsert(NOTES_URI)
.withValue(COL_TITLE, note.title)
.build()
}
contentResolver.applyBatch(AUTHORITY, ArrayList(ops))Query with a projection. SELECT * on a table with a large blob column can push the transaction
past the per-process Binder transaction budget. Asking for the three columns the UI needs avoids it.
Do not hold a cursor across a frame. A Cursor is a window into another process. Reading from it
on the main thread can block on a Binder call, which is exactly what StrictMode catches.
Performance and threading#
Provider calls are synchronous Binder calls. query returns a Cursor that is a window onto data
that may live in another process, so reading from it can block. Rules that follow:
- Never query a provider on the main thread in a production app.
- Close cursors (
use { }) — leaked cursors leak file descriptors in the provider's process. - Keep a projection: asking for all columns makes the transaction bigger and can exceed the Binder buffer for wide rows.
- Batch writes in a transaction inside the provider; each Binder call has fixed overhead.
Provider versus database#
A common design question: if the data is only used inside one app, should it be a provider at all?
Usually not. A ContentProvider exists to expose data across a UID boundary with framework-enforced
permissions. Inside one app, a Room database is simpler, faster and has a better type system. The
provider adds a Binder hop, a URI layer and a permission model that nothing is using.
The exceptions are real: a provider is worth writing when another app must read the data, when the
data must appear in the system's media or document index, or when a DocumentsProvider is the point.
Those are cases where the boundary is the feature.
Auditability#
Android 11 added data access auditing, which lets a provider record who read what and when, and
surface it to AppOpsManager. It is aimed at sensitive data — location, contacts, media — and is how
the system can show the privacy indicators for camera and microphone.4
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 |
|
The provider API itself has been stable since API 1. What changed is which data is reachable through it: scoped storage moved everything into providers, and the picker APIs moved from "permission to the library" to "access to chosen items".
Under the hood#
- ContentProvider.javathe provider base class and its URI/permission contract
- ContentResolver.javathe client side, including URI grant handling
- ContentProviderHelper.javasystem_server's provider lifecycle and publishing
- UriMatcher.javathe URI-to-code matcher every provider uses
Curiosity corner#
- A provider can be created before your Application. That is why dependency-injection frameworks
and AndroidX Startup initialise from a provider rather than from
Application.onCreate. ContentResolveris per-process, the provider is not. Two apps querying the same provider share the provider's process, not their own.- The contacts provider is one of the oldest APIs in Android and still one of the most heavily permission-gated.
Try it#
adb shell dumpsys activity providers | grep -E "authority|package"
adb shell content query --uri content://media/external/images/media --projection _id:_display_name
Check yourself#
Check yourself
You call `startActivity()` for an activity in another app. Which check does the system perform before delivering the intent?
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 13 or higherdeveloper.android.comaccessed 2026-10-27↩
- docsOpen files using the Storage Access Framework | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsData access auditingdeveloper.android.comaccessed 2026-10-27↩
- docsContent provider | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsPermissions on Android | Privacydeveloper.android.comaccessed 2026-10-27↩