Skip to content
intermediate Phase 7 · Data & Persistence

Content Providers

Share data between apps using Content Providers and ContentResolver.

40m
0 problems
Topic Progress 0%

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 signature protection for internal APIs
  • Prefer query() with a specific projection — never pass null for all columns
  • Always close Cursors with use {} to avoid leaks

Quiz

1. What does the ContentProvider getType() method return?

Question 1 options

2. Why should you always close a Cursor from ContentResolver.query()?

Question 2 options

3. What is the correct way to grant temporary URI access to another app?

Question 3 options

4. What is the purpose of notifyChange() after an insert/update/delete in ContentProvider?

Question 4 options

Flashcards

Question

What is the role of UriMatcher in a ContentProvider?

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?

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?

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()?

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 all
  • content://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