I02 — Contacts-lite provider
Build a content provider that stores people, then read it from a second app through a content resolver. URIs, permissions and the query contract, all in one small project.
A content provider is the only first-class way to share structured data between apps. Building one shows you the URI contract, the CRUD surface and the permission model in a single place.
What you are building#
A PeopleProvider that stores a name and a phone number in SQLite and exposes them at
content://com.example.people/people. A second app queries it through a ContentResolver and shows
the rows in a list.
Steps#
- Create the contract
Define the authority, the table name, the URI and the column names in one place. Both the provider and the client use it; a typo in the authority is the most common cause of "no provider found".
You should seeA constants class with the authority, the path and the column names. - Add a helper for URIs
Use a
UriMatcherto distinguishcontent://authority/peoplefromcontent://authority/people/1. The matcher is how a provider routes a request.You should seeA function that builds item and directory URIs. - Extend ContentProvider
Implement
onCreate,query,insert,update,deleteandgetType.onCreateis where the database is opened; the other methods implement the CRUD contract.1You should seeThe six abstract methods compile. - Back it with SQLite
Open a
SQLiteOpenHelperinonCreateand implement the CRUD methods against it. The provider is a facade over the database, not a second storage engine.You should seeRows persist across restarts. - Declare it in the manifest
Add a
<provider>element withandroid:authoritiesmatching the contract, and setandroid:exported="true"so the second app can see it.You should seeThe provider is installed. - Protect it
Add
android:readPermissionandandroid:writePermissionwith a custom signature permission. Then grant the permission to the client in the client's manifest with<uses-permission>, and confirm that removing it makes the query fail.2You should seeThe second app fails without the permission. - Query from the client
Use
contentResolver.querywith the contract URI and read the cursor. Close the cursor; a leaked cursor is a leaked file descriptor.You should seeA cursor with rows. - Observe changes
Register a content observer on the URI and notify it in the provider's insert. That is how a provider pushes change notifications to readers.
You should seeThe client list updates after an insert.
The provider's routing starts with a matcher:
private val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "people", PEOPLE_DIR)
addURI(AUTHORITY, "people/#", PEOPLE_ITEM)
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?,
): Cursor = db.query(TABLE, projection, selection, selectionArgs, null, null, sortOrder)adb shell content query --uri content://com.example.people/people
adb shell content insert --uri content://com.example.people/people --bind name:s:Ada --bind phone:s:5550100
adb shell content query --uri content://com.example.people/people
adb shell dumpsys package com.example.peopleprovider | grep -i providerExample output
Illustrative — authority and columns depend on your project.Verification#
The shell content command is the independent check. If content query returns rows and the client
app does not, the problem is the client's permission or the authority string, not the provider. If
both fail, the provider is not installed or the authority does not match.
What you learned#
- A provider is a Binder service with a URI-based interface, not a database API.
- The authority is the identity. A mismatch is the first thing to check.
- Permissions are per-provider and per-operation, and a signature permission is the right default for data you only share with your own apps.
- Cursors are resources. Close them, and use a content observer for change notification.
Read Content providers for the contract, and Storage Access Framework for the document-based alternative.
Extend it#
- Add a second table and a join in
query, exposed as a different URI path. - Add
getTypeand open a row in another app with anACTION_VIEWintent. - Replace SQLite with Room and keep the provider as the facade; the contract should not change.
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.
- docsContent provider | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsManifest.permission | API referencedeveloper.android.comaccessed 2026-10-27↩
- docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsPermissions on Android | Privacydeveloper.android.comaccessed 2026-10-27↩
- docsSave data in a local database using Room | App data and filesdeveloper.android.comaccessed 2026-10-27↩