Zygote
Components & Intentsintermediate

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.

11 minAPI 1–37 · 17interactivequizverified 2026-10-27

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-permission and URI grants — not by your code.
  • Providers are created before the Application object, so onCreate must 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#

Loading diagram…
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
PartMeaning
schemealways content
authorityglobally unique; declared in the manifest, usually package-based
patha collection, e.g. notes
idone 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:

  1. android:exported — whether anyone outside the UID may talk to it at all.
  2. android:readPermission / android:writePermission — separate gates for reads and writes.
  3. android:path-permission — different permissions for different paths within the same provider.
  4. 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:

ProviderWhat it gives you
MediaStoreimages, video, audio, documents — the system media index
ContactsContractcontacts and call logs, permission-gated
CalendarContractcalendar events
DocumentsProviderthe Storage Access Framework's file tree
Settingssystem 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#

Storage changes10 releases
APIVersionWhat changed
31.5 Cupcake
  • android.widget video and media improvements
123.1 Honeycomb
  • ParcelFileDescriptor improvements
154.0.3 Ice Cream Sandwich
  • New calendar and media provider APIs
194.4 KitKat
  • Storage Access Framework
247.0 Nougat
  • FileProvider and StrictMode for file URLs
2910
  • Scoped storage with requestLegacyExternalStorage opt-out
3011
  • Scoped storage enforced for targeting apps
3313 Tiramisu
  • READ_MEDIA_IMAGES/VIDEO/AUDIO replace storage reads
3414 Upside Down Cake
  • READ_MEDIA_VISUAL_USER_SELECTED
3717 Cinnamon Bun
  • ACTION_PICK_CONTACTS gives session-scoped contact field access

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#

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.
  • ContentResolver is 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#

List the providers on a device

adb shell dumpsys activity providers | grep -E "authority|package"

Query a provider from the shell

adb shell content query --uri content://media/external/images/media --projection _id:_display_name

Check yourself#

Check yourself

1 / 12

You call `startActivity()` for an activity in another app. Which check does the system perform before delivering the intent?

0 answered · 0 correct

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.

  1. docsStorage updates in Android 11developer.android.comaccessed 2026-10-27↩
  2. docsBehavior changes: Apps targeting Android 13 or higherdeveloper.android.comaccessed 2026-10-27↩
  3. docsOpen files using the Storage Access Framework | App data and filesdeveloper.android.comaccessed 2026-10-27↩
  4. docsData access auditingdeveloper.android.comaccessed 2026-10-27↩
  5. docsContent provider | App data and filesdeveloper.android.comaccessed 2026-10-27↩
  6. docsPermissions on Android | Privacydeveloper.android.comaccessed 2026-10-27↩