Articles
Android ContentProvider Interview Questions: URIs, Permissions, and Safe Data Sharing
Prepare for Android ContentProvider interviews with URI routing, exact read grants, caller permissions, safe queries, custom calls, and file sharing.
A note-sharing app keeps its ContentProvider unexported, yet another app can read one shared note. Does that violate the intended access policy? Android ContentProvider interview questions test whether you can distinguish the provider's general exposure, a specific URI grant, and the operations the implementation actually permits.
Review the resource contract and authorization decision for every exposed provider operation. Use the authority to route the request, then establish the caller's rights. Parameterized SQL protects values; it does not decide which rows belong to that caller.
This guide uses official Android documentation for platform facts and original examples for the design decisions. The permission matrix, review exercise, and test cases are our preparation reasoning, not a recorded Android-device experiment. Start with Design Android MVVM API Architecture and trace one cross-app data request through its repository boundary.
What does a content URI identify?
Official fact: a content URI uses the content scheme, a provider authority, and a resource path. Android's provider creation guide describes routing requests and distinguishing collection URIs from individual-row URIs, commonly with UriMatcher.
Consider this original contract:
content://com.example.notes.provider/notes collection
content://com.example.notes.provider/notes/42 note 42
content://com.example.notes.provider/exports/7 export 7
The authority must match the registered provider. The path belongs to the provider's API design, not to the caller's filesystem. /notes/42 should not mean “open whatever local filename looks like 42.” A query parameter such as owner=alice also cannot serve as authenticated identity merely because the caller supplied it.
Write each route's allowed methods, MIME type, visible columns, and failure behavior so a reviewer can test the contract. For our example, /notes/42 permits a bounded read under the owner's access or a valid read grant. /notes is owner-only. /exports/7 is a separately controlled file resource. Unknown paths are rejected rather than falling through to an unrestricted table query.
Practice changing one path segment and explaining the resulting request. Explain how the matcher classifies that request, how the ID is validated, and how the resulting operation is authorized. Parsing 42 as a numeric ID proves its syntax, not that note 42 exists or that the caller may read it.
Also name the client boundary. The app uses ContentResolver to address the provider contract; a repository can expose domain objects to the rest of the app. That separation helps test URI behavior without making every UI component construct paths or handle cursor details itself.
How do exported, permissions, and URI grants differ?
Official fact: the manifest's provider element supports android:exported, general permissions, separate read/write permissions, and URI-grant configuration. A qualifying URI grant can provide access even when a provider is not exported. Read/write permission attributes can override the general permission for their respective operations. Prefer an explicit exported value rather than relying on version-dependent defaults. Provider manifest reference
Our original notes provider is exported="false", allows URI grants, and shares note 42 with one recipient using a read grant. It does not issue a write grant or a prefix grant. The owner app still controls its own writes. The following original authorization contract assumes no other permissions or grants:
| Caller and resource | Read note 42 | Read note 43 | Update note 42 | Custom export call |
|---|---|---|---|---|
| Owner app | Allow | Allow | Allow | Allow under owner policy |
| Recipient with exact read grant for note 42 | Allow | Deny | Deny | No permission inferred; check separately |
| Unrelated app | Deny | Deny | Deny | Deny |
“Unexported means nobody else can read anything” would fail this case. “The recipient can reach the provider, so it can edit notes” would also fail. Access has resource scope and operation scope. The illustration summarizes only note 42; the text matrix adds the neighboring-resource check.
Official fact: Android's secure file-sharing guide shows granting temporary read permission when sharing a content URI. The grant's lifetime and persistence depend on the mechanism used; a temporary grant is not a permanent account-level entitlement.
Test both the successful handoff and denial cases. Verify what happens after the relevant grant expires or is revoked. For this exercise, the expected response is denied access, not an accidental fallback to an exported collection query. Explain who issues the grant and why its scope is narrow enough for the sharing action.
Why are selection arguments insufficient for safe queries?
Official fact: Android recommends selection arguments rather than interpolating caller-controlled values into SQL. Its SQL injection guidance also treats exposed provider operations as an important boundary to review.
This original query sketch separates route validation from SQL value binding:
val noteId = ContentUris.parseId(uri)
requireAllowedProjection(projection)
requireAllowedSort(sortOrder)
// Authorization for this operation and resource must already be established.
return db.query(
"notes", projection, "_id = ?",
arrayOf(noteId.toString()), null, null, "_id ASC"
)
This is a review sketch, not a complete compiled provider. requireAllowedProjection and requireAllowedSort represent application-defined validation. The ID predicate is constructed by the provider, and the sort expression is fixed here. Do not paste the sketch into production without implementing the authorization and route checks it deliberately calls out.
Binding noteId avoids inserting its value into SQL syntax. It does not make arbitrary projection expressions, sort clauses, table names, or selection strings safe. Decide whether the external API accepts any of those inputs. A narrow provider can reject unsupported query shapes instead of attempting to sanitize a full SQL language.
Our shared-note route returns _id, title, and body. It never returns the internal account token or owner email. The owner's collection route may support a different projection, but a caller cannot obtain that broader result merely by asking for more columns on the granted note URI.
Now change note 42 to 43 while keeping the read grant unchanged. The implementation must deny the new resource even if the SQL query itself is valid. This distinguishes injection protection from authorization. Also review exception messages: a denial should not reveal private values or a detailed SQL statement to an untrusted caller.
An interviewer may propose accepting owner_id as a query argument. Ask whether it is a filter selected by an already authorized user or an attempt to supply identity. For an owner-only collection, derive the ownership rule from trusted caller context and the app's policy; do not trust the requested owner value as proof.
Who checks custom call and file access?
Official fact: the framework cannot infer whether a provider-defined call() method reads or writes data. Beyond basic access to the provider, that entry point must perform its own permission checks. ContentProvider call reference
Suppose the original provider adds call("export_all", ...). A read grant for one note is not an authorization design for exporting every note. Check the method's operation-specific policy, accepted arguments, returned fields, and resource scope. If external custom calls are unnecessary, leave that capability unsupported instead of inventing a broad convenience interface.
Be precise about what the framework does check. Do not claim that every app can invoke every custom method without restriction. The documented concern is that the framework cannot enforce the custom method's individual read/write semantics for you. Your method must establish the additional rights its work requires.
Official fact: AndroidX FileProvider supports content-URI file sharing through configured filesystem roots and grants. Android's FileProvider risk guidance warns against exposing broad paths or giving more access than the task needs.
For our export route, map a validated export ID to a file selected by the provider. Do not directly convert an arbitrary URI path or query parameter into a filesystem path. Review the allowed root, canonical path containment, requested mode, and permission scope. A caller with read access must not obtain a writable descriptor merely by requesting mode "rw".
If FileProvider already satisfies the file-sharing requirement, explain why a custom openFile() implementation would add review obligations. Conversely, a structured-note provider and a file provider can serve different contracts. Choosing one does not remove the need to test the actual operations the app exposes.
What threading and lifetime mistakes matter?
Official fact: provider methods other than onCreate() can be called concurrently, and long initialization work should be deferred rather than performed in onCreate(). Provider implementation guidance
The consequence for our design is shared-state discipline. A mutable field called currentCaller is a poor place to keep one request's identity: another request can overwrite it. Keep request-specific authorization and query inputs local. Protect genuinely shared caches or use an implementation whose concurrency contract is understood.
Do not equate “the provider is in another process” with “my UI call cannot block.” Consider where the client performs its resolver operation, how cancellation is handled, and how results enter the UI. A repository should make that boundary visible. This is an architectural recommendation; the article does not claim a device timing benchmark.
Official fact: a cursor is a resource that the client must close after use. Android's retrieve-data guide describes the query and result-access workflow. In Kotlin, an appropriate use scope can express the lifetime clearly.
Keep a database transaction's scope equally explicit. In our original exercise, a collection update changes several notes and then notifies consumers. Define what constitutes an atomic business operation, when notifications occur, and what an observer should requery. A notification is an invalidation signal, not a durable audit record of every intermediate mutation.
Avoid keeping an activity, a cursor, or a request-specific object indefinitely in the provider. Process recreation can discard in-memory state. Persistent notes and access policy need appropriate durable storage; the next process instance must not rely on a previous object's cache as the only record of authorization.
How would you test the sharing contract?
Use an original test matrix that changes one boundary at a time. An owner-side unit test alone cannot prove a cross-app manifest policy, so distinguish pure route/query tests from instrumented tests using a separate caller identity. This guide specifies expected outcomes; it does not claim those device tests were executed.
| Test | Expected result for this design | Mistake it detects |
|---|---|---|
| Recipient reads granted note 42 | Allowed columns only | Overly restrictive or broken sharing |
| Same recipient reads note 43 | Denial | Resource scope widened accidentally |
| Same recipient updates note 42 | Denial | Read grant treated as write access |
| Caller asks for an internal column | Rejection | Projection leaks private fields |
| Caller requests an unsupported sort expression | Rejection | SQL structure trusted as input |
| Caller invokes export-all custom method | Operation-specific denial | Custom method bypasses intended policy |
| Shared export requested in writable mode | Denial | File descriptor exceeds grant rights |
Add concurrent reads and writes to expose mutable request state, and a client that closes its cursor on both success and error. After a mutation, verify the intended observer requery and visible result. The acceptance condition is the whole contract: the right caller can perform the right operation on the right resource, while neighboring resources and extra capabilities remain inaccessible.