androidengineers.Book a session

Core Android Components Deep Dive

ContentProviders: Contracts & IPC

article25 minHard

ContentProvider is Android's standard mechanism for structured data sharing across processes. It's also the foundation of MediaStore, ContactsContract, and FileProvider. Understanding it is essential for any system-level Android work.

ContentProvider as a Binder Service

Under the hood, a ContentProvider is a Binder service. Calls to ContentResolver cross the Binder boundary into your provider's process. The provider runs on a Binder thread pool thread — not the main thread — and callers block until it returns.

Client (any process)
    └── ContentResolver.query(uri, ...)
            └── Binder IPC
                    └── YourContentProvider.query(uri, ...)  [Binder thread]

URI Scheme

Every provider interaction uses a content:// URI:

content://com.example.provider/articles/42
         ──────────────────── ──────── ──
         authority             path     id
// Define URIs in a Contract class — a public API for your provider
object ArticleContract {
    const val AUTHORITY = "com.example.provider"
    val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")

    object Articles {
        val URI: Uri = Uri.withAppendedPath(BASE_URI, "articles")
        const val ID = "_id"
        const val TITLE = "title"
        const val BODY = "body"

        fun buildUri(id: Long) = ContentUris.withAppendedId(URI, id)
    }
}

Implementing a ContentProvider

class ArticleProvider : ContentProvider() {
    private lateinit var db: SQLiteDatabase

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI(ArticleContract.AUTHORITY, "articles", 1)     // all articles
        addURI(ArticleContract.AUTHORITY, "articles/#", 2)   // single article
    }

    override fun onCreate(): Boolean {
        db = ArticleDbHelper(context!!).writableDatabase
        return true
    }

    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        val qb = SQLiteQueryBuilder().apply {
            tables = "articles"
            if (uriMatcher.match(uri) == 2) {
                appendWhere("_id = ${uri.lastPathSegment}")
            }
        }
        return qb.query(db, projection, selection, selectionArgs, null, null, sortOrder)
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val id = db.insert("articles", null, values)
        context?.contentResolver?.notifyChange(uri, null)
        return ContentUris.withAppendedId(ArticleContract.Articles.URI, id)
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?,
                        selectionArgs: Array<String>?): Int {
        val count = db.update("articles", values, selection, selectionArgs)
        if (count > 0) context?.contentResolver?.notifyChange(uri, null)
        return count
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        val count = db.delete("articles", selection, selectionArgs)
        if (count > 0) context?.contentResolver?.notifyChange(uri, null)
        return count
    }

    override fun getType(uri: Uri): String = when (uriMatcher.match(uri)) {
        1 -> "vnd.android.cursor.dir/vnd.com.example.article"
        2 -> "vnd.android.cursor.item/vnd.com.example.article"
        else -> throw IllegalArgumentException("Unknown URI: $uri")
    }
}

FileProvider: Secure File Sharing

Direct file:// URIs are blocked on Android 7+ (throws FileUriExposedException). Use FileProvider to expose files via content:// URIs with temporary permissions.

<!-- AndroidManifest.xml -->
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>
<!-- res/xml/file_paths.xml -->
<paths>
    <cache-path name="shared_images" path="images/" />
</paths>
// Share a file
val file = File(context.cacheDir, "images/photo.jpg")
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)

val intent = Intent(Intent.ACTION_SEND).apply {
    type = "image/jpeg"
    putExtra(Intent.EXTRA_STREAM, uri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)  // grant temporary read access
}
startActivity(Intent.createChooser(intent, "Share image"))

Key Takeaways

ConceptRule
Contract classDefine URIs, column names, and MIME types as constants
UriMatcherRoute URIs to the correct query logic
notifyChangeCall after mutations so observers (CursorLoader, ContentObserver) update
FileProviderRequired for file sharing on Android 7+; never use file:// URIs
Binder threadContentProvider methods run on Binder threads, not main thread

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
ContentProviders: Contracts & IPC | Android System Design | Android Engineers