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
| Concept | Rule |
|---|---|
| Contract class | Define URIs, column names, and MIME types as constants |
UriMatcher | Route URIs to the correct query logic |
notifyChange | Call after mutations so observers (CursorLoader, ContentObserver) update |
FileProvider | Required for file sharing on Android 7+; never use file:// URIs |
| Binder thread | ContentProvider methods run on Binder threads, not main thread |