ContentProvider Fundamentals
What ContentProviders Do
A ContentProvider is an IPC (inter-process communication) mechanism. It exposes your app's data to other apps through a standard interface, regardless of whether the data lives in a database, file, or network.
Android system providers use this: Contacts, Media, Calendar, Downloads.
Anatomy of a ContentProvider
class NotesProvider : ContentProvider() {
private lateinit var dbHelper: NotesDbHelper
override fun onCreate(): Boolean {
dbHelper = NotesDbHelper(context!!)
return true
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
return when (uriMatcher.match(uri)) {
NOTES -> db.query("notes", projection, selection, selectionArgs, null, null, sortOrder)
NOTE_ID -> db.query("notes", projection, "id=?", arrayOf(uri.lastPathSegment), null, null, sortOrder)
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
require(uriMatcher.match(uri) == NOTES) { "Invalid URI for insert" }
val id = dbHelper.writableDatabase.insert("notes", null, values)
context?.contentResolver?.notifyChange(uri, null)
return ContentUris.withAppendedId(uri, id)
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
return when (uriMatcher.match(uri)) {
NOTES -> dbHelper.writableDatabase.update("notes", values, selection, selectionArgs)
NOTE_ID -> dbHelper.writableDatabase.update("notes", values, "id=?", arrayOf(uri.lastPathSegment))
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return when (uriMatcher.match(uri)) {
NOTES -> dbHelper.writableDatabase.delete("notes", selection, selectionArgs)
NOTE_ID -> dbHelper.writableDatabase.delete("notes", "id=?", arrayOf(uri.lastPathSegment))
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
override fun getType(uri: Uri): String = when (uriMatcher.match(uri)) {
NOTES -> "vnd.android.cursor.dir/vnd.example.notes"
NOTE_ID -> "vnd.android.cursor.item/vnd.example.notes"
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
}
URI Matching
companion object {
const val AUTHORITY = "com.example.provider"
val CONTENT_URI = Uri.parse("content://$AUTHORITY/notes")
private const val NOTES = 1
private const val NOTE_ID = 2
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
addURI(AUTHORITY, "notes", NOTES)
addURI(AUTHORITY, "notes/#", NOTE_ID)
}
}
Manifest Registration
<provider
android:name=".NotesProvider"
android:authorities="com.example.provider"
android:exported="false"
android:grantUriPermissions="true" />
Set exported="true" only when other apps need access. Use grantUriPermissions with Intent flags for selective access.
ContentResolver Client-Side Access
Querying an External Provider
class NotesRepository(private val context: Context) {
fun getAllNotes(): List<Note> {
val notes = mutableListOf<Note>()
context.contentResolver.query(
NotesProvider.CONTENT_URI,
arrayOf("id", "title", "content", "created_at"),
null,
null,
"created_at DESC"
)?.use { cursor ->
val idIdx = cursor.getColumnIndexOrThrow("id")
val titleIdx = cursor.getColumnIndexOrThrow("title")
val contentIdx = cursor.getColumnIndexOrThrow("content")
val dateIdx = cursor.getColumnIndexOrThrow("created_at")
while (cursor.moveToNext()) {
notes.add(Note(
id = cursor.getLong(idIdx),
title = cursor.getString(titleIdx),
content = cursor.getString(contentIdx),
createdAt = cursor.getLong(dateIdx)
))
}
}
return notes
}
}
The use block ensures the Cursor is closed even if an exception occurs.
Inserting and Updating
suspend fun insertNote(note: Note): Uri? = withContext(Dispatchers.IO) {
val values = ContentValues().apply {
put("title", note.title)
put("content", note.content)
put("created_at", note.createdAt)
}
context.contentResolver.insert(NotesProvider.CONTENT_URI, values)
}
suspend fun updateNote(id: Long, title: String): Int = withContext(Dispatchers.IO) {
val values = ContentValues().apply {
put("title", title)
}
val uri = ContentUris.withAppendedId(NotesProvider.CONTENT_URI, id)
context.contentResolver.update(uri, values, null, null)
}
Observing Changes with ContentObserver
class NotesObserver(private val onNotesChanged: () -> Unit) : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
onNotesChanged()
}
}
// Register
val observer = NotesObserver { refreshNotes() }
context.contentResolver.registerContentObserver(
NotesProvider.CONTENT_URI, true, observer
)
// Unregister in onDestroy
context.contentResolver.unregisterContentObserver(observer)
ContentObserver fires when data changes through the provider — useful for syncing UI across processes.
Security and Permission Control
Controlling Access
ContentProvider security has three layers:
1. exported flag — Whether other apps can reach the provider at all.
2. Permissions — Require callers to hold specific permissions.
<provider
android:name=".NotesProvider"
android:authorities="com.example.provider"
android:exported="true"
android:readPermission="com.example.READ_NOTES"
android:writePermission="com.example.WRITE_NOTES" />
<permission
android:name="com.example.READ_NOTES"
android:protectionLevel="normal" />
<permission
android:name="com.example.WRITE_NOTES"
android:protectionLevel="signature" />
signature permission means only apps signed with the same key as your app can write.
3. URI-level permissions — Grant temporary access to specific URIs.
// Sender: Grant URI permission
val intent = Intent(Intent.ACTION_VIEW).apply {
data = NotesProvider.CONTENT_URI
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
// Receiver: Access granted URI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent.data?.let { uri ->
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
// Read data from granted URI
}
}
}
FileProvider for Secure File Sharing
Instead of sharing raw file URIs (which crash on Android 7+), use FileProvider:
<provider
android:name=".provider.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, fileUri)
type = "application/pdf"
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
Best Practices
- Keep
exported="false"unless other apps genuinely need access - Use
signatureprotection for internal APIs - Prefer
query()with a specific projection — never passnullfor all columns - Always close Cursors with
use {}to avoid leaks
Quiz
1. What does the ContentProvider getType() method return?
2. Why should you always close a Cursor from ContentResolver.query()?
3. What is the correct way to grant temporary URI access to another app?
4. What is the purpose of notifyChange() after an insert/update/delete in ContentProvider?
Flashcards
Question
What is the role of UriMatcher in a ContentProvider?
Click to reveal answer
Answer
Maps incoming content URIs to integer constants so you can switch on them to route queries, inserts, updates, and deletes to the correct table or row.
Question
What does exported=false do for a ContentProvider?
Click to reveal answer
Answer
Prevents other apps from accessing the provider entirely. Only the app itself can use it.
Question
How does FileProvider differ from raw file URIs?
Click to reveal answer
Answer
FileProvider generates content:// URIs instead of file:// URIs, which are required for secure file sharing on Android 7+.
Question
What happens when you pass null for the projection parameter in query()?
Click to reveal answer
Answer
It returns all columns, which wastes bandwidth and memory. Always specify only the columns you need.
Revision Notes
Key Takeaways
- 1. ContentProvider exposes app data via a standard IPC interface using content URIs
- 2. Always close Cursors with use{} to prevent memory and connection leaks
- 3. Use FLAG_GRANT_READ_URI_PERMISSION for temporary access instead of exported=true
- 4. FileProvider generates content:// URIs required for secure file sharing on Android 7+
Interview Tips
- • Explain the difference between ContentProvider and Room — ContentProvider is for IPC, Room is for in-app database
- • Discuss why Android 7+ requires FileProvider instead of raw file:// URIs
- • Know how URI matching works with UriMatcher for list vs item queries
- • Be ready to describe when to use signature-level permissions vs normal permissions
Cheat Sheet
Content Providers Cheat Sheet
Provider Methods:
- query() — returns Cursor with matching rows
- insert() — returns URI of new row
- update() — returns number of rows affected
- delete() — returns number of rows deleted
- getType() — returns MIME type for URI
URI Patterns:
content://authority/path— list allcontent://authority/path/#— specific item
Security:
- exported: false = app-private
- readPermission / writePermission = require caller permissions
- FLAG_GRANT_READ_URI_PERMISSION = temporary URI access
Cursor Rules:
- Always close with use{}
- Never pass null projection
- Use getColumnIndexOrThrow() for safety