---
title: 'Content Providers'
source: 'https://academia.sh/en/courses/android-platform-and-tools/content-providers'
course: 'Android Platform and Tools'
language: en
updated: '2026-10-21T05:02:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tells the content provider, the content URI and the content resolver apart'
    - 'Measures what code the URI matcher turns a path into, and that a path with no match stays unmet'
    - 'Shows that a missing row is replaced by an empty cursor while a missing column stays unmet'
    - 'Verifies that the same query is fulfilled fully or stays unmet depending on the state of the caller'
---

# Content Providers

How a content provider resolves a content URI with a URI matcher, how an unknown path, a missing row and a missing column are fulfilled, and why a query without permission stays unmet.

A broadcast receiver reports an event; it carries no data. When data has to be shared with another app, the platform offers another app component: the content provider (`ContentProvider`). Contacts, calendar entries and media files are shared this way; if you want to open your own app's data to another app, you follow the same path.

A content provider is not reached directly like an object. The caller gives a **content URI**, the platform finds from the authority part of the URI which provider is responsible, and it forwards the request to that provider. The tool in the caller's hand is the content resolver (`ContentResolver`): it routes queries, inserts and deletes to the provider. Inside the provider, in turn, there is a **URI matcher** (`UriMatcher`) that turns the URI into a code.

The question of the lesson: when data is requested with a content URI, how does the platform **fulfill** the request? Three different points give three different answers: the matching of the path, the existence of the row and the column, and the permission of the caller.

```kotlin
// Fulfillment.kt — classifies how the platform fulfills a request
sealed interface Outcome {
    data class Given(val value: String) : Outcome
    data class Waiting(val on: String) : Outcome
    data class Refused(val reason: String) : Outcome
}

class Ask(val label: String, val wanted: String, val outcome: Outcome) {
    val fulfillment: String = when (outcome) {
        is Outcome.Waiting -> "pending"
        is Outcome.Refused -> "unmet"
        is Outcome.Given -> if (outcome.value == wanted) "full" else "substitute"
    }

    fun report() {
        val detail = when (outcome) {
            is Outcome.Given -> "given=${outcome.value}"
            is Outcome.Waiting -> "waiting-on=${outcome.on}"
            is Outcome.Refused -> "reason=${outcome.reason}"
        }
        println("$label: wanted=$wanted | $detail -> $fulfillment")
    }
}

fun tally(asks: List<Ask>) {
    for (name in listOf("full", "substitute", "pending", "unmet")) {
        println("$name: ${asks.count { it.fulfillment == name }}")
    }
}
```

## The Structure of a Content URI

A content URI is built from three parts: the `content://` scheme, the authority of the provider (`authority`) and the path. The number at the end of the path is most often the identifier of a row.

As an example, `content://com.example.library/books` points to all the books, and `content://com.example.library/books/7` points to the book whose identifier is 7.

The provider is declared in the manifest file with a `<provider>` element. The authority is given here; the `android:exported` and permission attributes are here too. Without this element in the manifest file the system cannot route the URI to any provider.

```xml
<application>
    <provider
        android:name=".BookProvider"
        android:authorities="com.example.library"
        android:exported="true"
        android:readPermission="com.example.library.READ_BOOKS" />
</application>
```

The code of the provider derives from the `ContentProvider` class and implements six methods: `onCreate`, `query`, `insert`, `update`, `delete` and `getType`. These methods, which the caller never calls directly, run through the content resolver.

```kotlin
// Platform code: a query reaching the provider
import android.content.ContentProvider
import android.net.Uri

class BookProvider : ContentProvider() {
    private val matcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI("com.example.library", "books", 1)
        addURI("com.example.library", "books/#", 2)
    }

    override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                       args: Array<String>?, order: String?): Cursor? =
        when (matcher.match(uri)) {
            1 -> database.listBooks(projection)
            2 -> database.findBook(uri.lastPathSegment, projection)
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
}
```

The matcher has two pattern words: `#` matches a number, and `*` matches any text. If there is no match, `match` returns `-1` (`NO_MATCH`) and the provider chooses to report this case with an exception. The person writing the provider makes this choice; the rule above is the common one, and the model of this lesson follows it.

## The Matching of the Path

The model builds the URI matcher and the query in two helper files. The first compares the path with the patterns and returns the matching code, or `-1` if there is no match. The second turns the code into a result.

```kotlin
// Matcher.kt — compares the path with the patterns, returns the code
fun matchUri(path: String, patterns: Map<String, Int>): Int {
    val parts = path.split("/")
    for ((pattern, code) in patterns) {
        val want = pattern.split("/")
        if (want.size != parts.size) continue
        val fits = want.zip(parts).all { (w, p) ->
            when (w) {
                "#" -> p.isNotEmpty() && p.all { it.isDigit() }
                "*" -> p.isNotEmpty()
                else -> w == p
            }
        }
        if (fits) return code
    }
    return -1
}
```

```kotlin
// Provider.kt — turns the matching code into a result
val patterns = mapOf("books" to 1, "books/#" to 2)
val rows = setOf(7, 9)

fun queryBooks(path: String): Outcome = when (matchUri(path, patterns)) {
    1 -> Outcome.Given("book-list")
    2 -> {
        val id = path.substringAfter("/").toInt()
        if (id in rows) Outcome.Given("book-row") else Outcome.Given("empty-cursor")
    }
    else -> Outcome.Refused("no-match")
}
```

`rows` is an input of the model: it says which identifiers exist in the table. The first measurement makes four requests: the list, an existing row, a path that is not defined at all, and an identifier that carries text instead of a number.

```kotlin
// Main.kt — URI matching
fun main() {
    val asks = listOf(
        Ask("list", "book-list", queryBooks("books")),
        Ask("row", "book-row", queryBooks("books/7")),
        Ask("unknown", "book-list", queryBooks("authors")),
        Ask("text-id", "book-row", queryBooks("books/abc")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
list: wanted=book-list | given=book-list -> full
row: wanted=book-row | given=book-row -> full
unknown: wanted=book-list | reason=no-match -> unmet
text-id: wanted=book-row | reason=no-match -> unmet
full: 2
substitute: 0
pending: 0
unmet: 2
```

The two requests that matched the two patterns were fulfilled in full. The two that did not match stayed unmet: the `authors` path is in no pattern, and `books/abc` does not fit the `#` pattern because the identifier is not a number. The second case is a subtle one: since the path starts with `books/`, the user expects a match, but the matcher compares the pattern part by part, not word by word, and `#` accepts only digits. The platform turns these requests away with an exception; in the model this is shown with `Refused`.

**AC1.** After a `books/#` match the model reads the identifier with `toInt`. The behavior with a number too long to fit was not measured. The real matcher does the same part-by-part comparison, but the order of precedence among patterns (exact text, then `#`, then `*`) was kept to two patterns in this lesson.

## A Missing Row and an Empty Path

A path may fit the pattern but the content may turn out empty. The book with identifier 8 is not in the table, but the `books/8` path fits the `books/#` pattern. In this case the provider does not throw an exception: it returns a cursor with zero rows. The caller did not get the row it wanted, but it did not get an error either.

Three requests are made. One asks for the existing row. One asks for the row that does not exist. The third gives a path in the form `books/`, whose last part is empty.

```kotlin
// Main.kt — a missing row and an empty part
fun main() {
    val asks = listOf(
        Ask("existing", "book-row", queryBooks("books/7")),
        Ask("missing", "book-row", queryBooks("books/8")),
        Ask("empty-part", "book-row", queryBooks("books/")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
existing: wanted=book-row | given=book-row -> full
missing: wanted=book-row | given=empty-cursor -> substitute
empty-part: wanted=book-row | reason=no-match -> unmet
full: 1
substitute: 1
pending: 0
unmet: 1
```

The existing row is full. The missing row became a **substitute**: the caller was expecting a book row, and the platform gave an empty cursor. Because the request did not turn into an error it is not unmet, but because what was wanted was not given it is not full either. The empty part, on the other hand, did not match: `#` does not count empty text as a number.

The practical counterpart of this distinction shows in the calling code. An empty cursor is a normal result and the code has to check for it. A matching failure is an exception, and if left unchecked it ends the app.

**AC2.** The model does not count an empty cursor as an error, because the provider itself throws no error. What happens if the caller does not check whether the cursor is empty was not measured.

## A Missing Column

A query carries not only the row but also which columns are wanted. The list of columns is called `projection`. If the provider rests on a SQL table, a column name that does not exist brings the query down and the caller gets an exception. If the provider filters the columns itself, the rule may differ; this lesson builds the case that rests on a SQL table, which is the most common.

```kotlin
// Main.kt — the column list
val known = setOf("_id", "title", "author")

fun queryColumns(wanted: List<String>): Outcome {
    val missing = wanted.filter { it !in known }
    return if (missing.isEmpty()) {
        Outcome.Given(wanted.joinToString(","))
    } else {
        Outcome.Refused("column-missing")
    }
}

fun columnAsk(label: String, wanted: List<String>): Ask =
    Ask(label, wanted.joinToString(","), queryColumns(wanted))

fun main() {
    val asks = listOf(
        columnAsk("all-columns", listOf("title", "author")),
        columnAsk("missing-column", listOf("title", "isbn")),
        columnAsk("with-id", listOf("_id", "title", "author")),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
}
```

```text
all-columns: wanted=title,author | given=title,author -> full
missing-column: wanted=title,isbn | reason=column-missing -> unmet
with-id: wanted=_id,title,author | given=_id,title,author -> full
full: 2
substitute: 0
pending: 0
unmet: 1
```

Two requests came with known columns and were fulfilled in full. The third asked for the `isbn` column; it is not in the table and the request stayed unmet. The platform does not find the row here and skip the missing column: if even one column is unknown, the whole query is rejected. A partial result is not a partial substitute; the rule at work is all or nothing.

**AC3.** The `known` set is an input of the model. How column names are validated during a query in reality, the special name of the identifier column (`_id`) and multi-table join queries were not measured.

## The Bounding Measurement

The same content URI, the same query: only the state of the caller changes. If the provider is protected with `android:readPermission`, a caller without the permission cannot reach the query. The system checks the permission before it enters the provider; so the matching of the path or the existence of the row is no longer asked. Without permission, a correct path changes nothing.

There is one more path: the party that wrote the provider can give a temporary permission for a single URI (`grantUriPermission`). This permission covers only that URI, and does not pass to the other rows in the provider.

```kotlin
// Main.kt — the same query, three callers
fun access(path: String, caller: String, allowed: Set<String>): Outcome = when {
    caller == "permitted" -> queryBooks(path)
    path in allowed -> queryBooks(path)
    else -> Outcome.Refused("no-permission")
}

fun main() {
    val allowed = setOf("books/7")
    val asks = listOf(
        Ask("permitted", "book-row", access("books/7", "permitted", allowed)),
        Ask("uri-granted", "book-row", access("books/7", "other", allowed)),
        Ask("other-row", "book-row", access("books/9", "other", allowed)),
    )
    for (ask in asks) {
        ask.report()
    }
    tally(asks)
    println(asks.map { it.fulfillment }.distinct().sorted())
}
```

```text
permitted: wanted=book-row | given=book-row -> full
uri-granted: wanted=book-row | given=book-row -> full
other-row: wanted=book-row | reason=no-permission -> unmet
full: 2
substitute: 0
pending: 0
unmet: 1
[full, unmet]
```

The caller who holds the permission was fulfilled in full. The caller who has no permission but was given a temporary URI permission for `books/7` got the same row in the same way, in full. When the same caller asked for the `books/9` row it stayed unmet: the URI permission applies only to the URI it was given for. The last line shows that there are two different fulfillment values; it is not who the caller is but whether the URI is in the permission set that decides.

The fulfillment measured in this lesson is **full**: a request that matches, exists and is permitted gets the requested data from the content provider exactly. When fulfillment breaks, the place that breaks is always the same kind: the match, the row, the column or the permission. Which one broke is read from the result the caller received.

**AC4.** The model builds permission with two values: the existence of the provider's permission and the individual URI permissions. The write permission (`writePermission`), path-based permissions (`path-permission`) and loss of permission during an operation were not measured.

## Summary

- This lesson's criterion: **fulfillment**. A content provider offers its data with a content URI; the content resolver is on the side of the client, and the URI matcher is on the side of the provider. This lesson's fulfillment: **full**; a matching, existing, permitted request receives the data as it is.
- A matching path was fulfilled in full and a non-matching path stayed unmet: 2 full and 2 unmet. An identifier that is not a number, such as `books/abc`, does not fit the `#` pattern.
- A missing row is not an error: an empty cursor returns as a **substitute**. A missing column leaves the whole query unmet.
- Permission is checked before entering the provider. A temporary URI permission covers only the URI it was given for.
- The rule gives the fulfillment, not the label: an empty cursor and a matching failure fall into different categories.

## Next Step

You called a content provider with a URI. The general way of calling another app component, in turn, is the intent. The next lesson, Intents and Intent Filters, measures which app component an intent reaches, and the case where more than one app fulfills the same intent.
