Zygote
Builder trackintermediate

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.

3 minAPI 21–37 · 17verified 2026-10-27

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#

  1. 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.
  2. Add a helper for URIs

    Use a UriMatcher to distinguish content://authority/people from content://authority/people/1. The matcher is how a provider routes a request.

    You should seeA function that builds item and directory URIs.
  3. 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)
  4. Extend ContentProvider

    Implement onCreate, query, insert, update, delete and getType. onCreate is where the database is opened; the other methods implement the CRUD contract.1

    You should seeThe six abstract methods compile.
  5. Back it with SQLite

    Open a SQLiteOpenHelper in onCreate and 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.
  6. Declare it in the manifest

    Add a <provider> element with android:authorities matching the contract, and set android:exported="true" so the second app can see it.

    You should seeThe provider is installed.
  7. Protect it

    Add android:readPermission and android:writePermission with 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.2

    You should seeThe second app fails without the permission.
  8. Query from the client

    Use contentResolver.query with the contract URI and read the cursor. Close the cursor; a leaked cursor is a leaked file descriptor.

    You should seeA cursor with rows.
  9. 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.
Query the provider from the shell
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 provider
Example 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#

  1. Add a second table and a join in query, exposed as a different URI path.
  2. Add getType and open a row in another app with an ACTION_VIEW intent.
  3. 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.

  1. docsContent provider | App data and filesdeveloper.android.comaccessed 2026-10-27↩
  2. docsManifest.permission | API referencedeveloper.android.comaccessed 2026-10-27↩
  3. docsData and file storage overview | App data and filesdeveloper.android.comaccessed 2026-10-27↩
  4. docsPermissions on Android | Privacydeveloper.android.comaccessed 2026-10-27↩
  5. docsSave data in a local database using Room | App data and filesdeveloper.android.comaccessed 2026-10-27↩