ContentProvider, FileProvider & URI Permissions
Secure data and file sharing between Android apps
Open interactive version (quiz + challenge)Real-world analogy
A ContentProvider is like a bank teller — you cannot walk into the vault yourself, but you can request data through the official window with the right ID. FileProvider is the bank's secure courier service — it delivers a sealed package to another party without revealing where the vault is.
What is it?
FileProvider.getUriForFile() turns a File into a shareable content:// URI with time-limited permissions — replacing unsafe file:// URIs between apps.
Real-world relevance
A camera app uses FileProvider to share a captured photo with a crop app — it generates a content:// URI, attaches FLAG_GRANT_READ_URI_PERMISSION to the Intent, and the crop app reads the file without ever knowing its path. A contacts app exposes data through ContentProvider so that a dialer or messaging app can query phone numbers by name.
Key points
- ContentProvider purpose — ContentProvider exposes structured data (usually from a SQLite database) to other apps via a standardized CRUD interface. It uses URI-based addressing: content://authority/table/id. The system routes queries through the provider, enforcing declared permissions. It is still the correct mechanism for sharing data with system components like the Contacts app or MediaStore.
- ContentResolver — the client side — Apps never talk to a ContentProvider directly. They use ContentResolver (obtained via context.contentResolver) to issue query(), insert(), update(), delete(), and openInputStream() calls. The system resolves the URI to the correct provider and routes the call, handling cross-process marshalling transparently.
- CRUD operations through ContentProvider — query() returns a Cursor. insert() returns the URI of the new row. update() and delete() return the number of affected rows. All four must be thread-safe because multiple apps can call them concurrently. SQLiteDatabase handles this internally, but custom providers must synchronize themselves.
- Declaring a ContentProvider — Declare in AndroidManifest with android:authorities (globally unique, typically your package name), android:exported (true if other apps need access), and android:readPermission / android:writePermission. Without exported=true and correct permissions, other apps cannot use it.
- FileProvider — secure file sharing — FileProvider is a special ContentProvider subclass that maps file paths to content:// URIs. It prevents exposing file:// URIs (which are blocked by FileUriExposedException since API 24) and instead grants time-limited, permission-scoped access to files. Essential for sharing photos, documents, and APKs with other apps or system intents.
- Configuring FileProvider — Declare FileProvider in Manifest with android:authorities, android:exported='false', and android:grantUriPermissions='true'. Provide an XML file-paths resource that maps logical names to real directory paths (e.g., files-path, cache-path, external-path). Never expose the raw file system path — use logical names only.
- URI permission grants — FLAG_GRANT_READ_URI_PERMISSION and FLAG_GRANT_WRITE_URI_PERMISSION are added to Intents when sharing a file. These grants are temporary and scoped to the receiving app's process. They expire when the receiving task stack is cleared. This is the correct way to give another app access to your private files without making them world-readable.
- getUriForFile() — the key FileProvider call — FileProvider.getUriForFile(context, authority, file) converts a File object to a content:// URI. You then set this URI on an Intent along with the URI permission flags. Never pass a File path as a String extra — it bypasses the permission system entirely.
- MediaStore — the system ContentProvider — MediaStore is Android's built-in ContentProvider for photos, videos, audio, and documents. In Android 10+ (scoped storage), your app can only access its own files and files the user explicitly picks via MediaStore queries or Storage Access Framework. Know how to query MediaStore for images and how to insert new media correctly.
- When ContentProvider is still the right choice in 2026 — System integrations (contacts, calendar, media), sharing structured data with third-party apps via a documented API, and search suggestions (SearchRecentSuggestionsProvider). For sharing within your own app across processes, prefer a database + ViewModel + repository pattern. ContentProvider adds complexity that is rarely justified for single-app use.
- CursorLoader deprecation awareness — CursorLoader was the recommended way to load ContentProvider data asynchronously in the Loader API era. It is now deprecated. Use viewModelScope + coroutines + contentResolver queries on a background dispatcher instead. Know this transition for legacy codebase discussions.
- Security pitfalls — Common bugs: setting exported=true on a ContentProvider without permissions (exposes all data to any app), using file:// URIs directly (causes crash on API 24+), not adding URI permission flags to the Intent (receiving app gets the URI but cannot read it). Mentioning these shows security awareness that impresses senior interviewers.
- Quick answer: what is Android FileProvider? — FileProvider is a special ContentProvider subclass from AndroidX Core that converts a File on disk into a content:// URI, so you can share it with another app or a system Intent (camera, share sheet, PDF viewer) without exposing the raw file path or granting broad file-system access. Declare it in the Manifest with android:exported='false', map allowed directories in an xml/file_paths.xml resource, then call FileProvider.getUriForFile().
- Minimal FileProvider setup — smallest working example — Three pieces, in order: the Manifest declaration, the file_paths.xml that whitelists a directory, and the Kotlin call that turns a File into a shareable URI. Miss the xml resource and getUriForFile() throws IllegalArgumentException at runtime — that error means the directory you're sharing from isn't listed below.
- Sharing multiple files at once — ACTION_SEND_MULTIPLE — A common gap in FileProvider tutorials: sharing more than one file requires ACTION_SEND_MULTIPLE with EXTRA_STREAM holding an ArrayList, not a single Uri. Every URI in the list must come from getUriForFile() and the grant flag applies to the whole list. Mixing MIME types (e.g. a photo and a PDF) means using type = "*/*" — a specific type like image/jpeg will make the chooser reject a non-matching file silently.
- Revoking a URI grant early with revokeUriPermission() — Grants normally expire when the receiving app's task is removed from recents, but that can be minutes or hours later — not good enough for a one-time share of a sensitive document. Call context.revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) as soon as you know the transfer is done (e.g. after an upload-complete callback) to close the grant immediately. This is the detail that separates 'I followed a tutorial' from 'I understand the permission lifecycle' in a senior interview.
- Fixing "Failed to find configured root" IllegalArgumentException — The single most common FileProvider crash. getUriForFile() throws IllegalArgumentException: Failed to find configured root for path when the File you pass lives OUTSIDE every directory whitelisted in file_paths.xml — not when the xml resource is simply missing. Checklist: (1) confirm the file's real path (context.cacheDir vs context.filesDir vs getExternalFilesDir() are different roots, each needs its own // tag); (2) the 'path' attribute must match the actual sub-directory you write into, not just "."; (3) after editing file_paths.xml, do a clean rebuild — the manifest merger sometimes caches the old meta-data resource pointer.
Code example
// FileProvider setup in AndroidManifest.xml (shown as comment)
// <provider
// android:name="androidx.core.content.FileProvider"
// android:authorities="com.example.app.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 (shown as comment)
// <paths>
// <cache-path name="camera_images" location="images/" />
// <files-path name="documents" location="docs/" />
// </paths>
// Sharing a file using FileProvider
fun sharePhoto(context: Context, photoFile: File) {
val photoUri: Uri = FileProvider.getUriForFile(
context,
"com.example.app.fileprovider",
photoFile
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "image/jpeg"
putExtra(Intent.EXTRA_STREAM, photoUri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, "Share Photo"))
}
// Launching camera and saving to FileProvider URI
fun launchCamera(activity: AppCompatActivity, launcher: ActivityResultLauncher<Uri>): Uri {
val imageFile = File(activity.cacheDir, "images/capture_${System.currentTimeMillis()}.jpg")
imageFile.parentFile?.mkdirs()
val photoUri = FileProvider.getUriForFile(
activity,
"com.example.app.fileprovider",
imageFile
)
launcher.launch(photoUri)
return photoUri
}
// Custom ContentProvider skeleton
class NotesProvider : ContentProvider() {
private lateinit var dbHelper: NotesDatabaseHelper
override fun onCreate(): Boolean {
dbHelper = NotesDatabaseHelper(context!!)
return true
}
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
val db = dbHelper.readableDatabase
val cursor = db.query("notes", projection, selection, selectionArgs, null, null, sortOrder)
cursor.setNotificationUri(context!!.contentResolver, uri)
return cursor
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
val db = dbHelper.writableDatabase
val id = db.insert("notes", null, values)
context!!.contentResolver.notifyChange(uri, null)
return ContentUris.withAppendedId(CONTENT_URI, id)
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = db.update("notes", values, selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val db = dbHelper.writableDatabase
val count = db.delete("notes", selection, selectionArgs)
context!!.contentResolver.notifyChange(uri, null)
return count
}
override fun getType(uri: Uri): String = "vnd.android.cursor.dir/vnd.com.example.notes"
companion object {
val CONTENT_URI: Uri = Uri.parse("content://com.example.notes.provider/notes")
}
}
// Querying MediaStore for images (scoped storage, API 29+)
fun getPhotosFromMediaStore(context: Context): List<Uri> {
val uris = mutableListOf<Uri>()
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME)
context.contentResolver.query(collection, projection, null, null, "${MediaStore.Images.Media.DATE_ADDED} DESC")?.use { cursor ->
val idCol = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idCol)
uris.add(ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id))
}
}
return uris
}Line-by-line walkthrough
- 1. FileProvider.getUriForFile() takes your real file path and maps it to a content:// URI using the file_paths.xml mapping — the receiving app never sees the real path.
- 2. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) tells the system to create a temporary permission grant tied to this Intent's recipient — without this, the URI is useless.
- 3. Intent.createChooser() wraps the share intent so the system shows the app picker — each app in the picker automatically receives the URI permission.
- 4. In launchCamera, imageFile.parentFile?.mkdirs() ensures the directory exists before FileProvider tries to map the path — missing directory causes FileNotFoundException.
- 5. In NotesProvider.query(), cursor.setNotificationUri() wires the cursor to receive updates when notifyChange() is called, enabling live UI updates for registered observers.
- 6. notifyChange() after insert/update/delete propagates the change notification to all active cursors pointing at this URI — critical for consistent UI state.
- 7. ContentUris.withAppendedId() constructs a URI like content://authority/notes/42 for the newly inserted row — the standard way to return an item URI from insert().
- 8. MediaStore query uses use{} block on the cursor — this is idiomatic Kotlin that auto-closes the cursor even if an exception occurs, preventing cursor leak.
- 9. getColumnIndexOrThrow() throws if the column does not exist rather than returning -1 silently — prefer this over getColumnIndex() to catch schema mismatches early.
- 10. ContentUris.withAppendedId on MediaStore.Images.Media.EXTERNAL_CONTENT_URI builds a direct content URI for the image that Glide, Coil, or ImageDecoder can load directly.
Spot the bug
// In AndroidManifest.xml
// <provider
// android:name="androidx.core.content.FileProvider"
// android:authorities="com.example.app.provider"
// android:exported="true"
// android:grantUriPermissions="true">
// <meta-data
// android:name="android.support.FILE_PROVIDER_PATHS"
// android:resource="@xml/file_paths" />
// </provider>
fun shareDocument(context: Context, file: File) {
val uri = FileProvider.getUriForFile(
context,
"com.example.app.provider",
file
)
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(uri, "application/pdf")
}
context.startActivity(intent)
}Need a hint?
There are two bugs: one is a critical security misconfiguration in the Manifest, and one causes the receiving app to be unable to open the file.
Show answer
Bug 1: android:exported='true' on a FileProvider is a critical security vulnerability. With exported=true, any app on the device can query your FileProvider directly without needing a URI permission grant, potentially accessing all files in the declared paths. FileProvider must always have android:exported='false'. Its sharing mechanism works through URI permission grants, not direct exports. Bug 2: The Intent does not include FLAG_GRANT_READ_URI_PERMISSION. Even though FileProvider generated a content:// URI, the receiving app (e.g., a PDF viewer) does not have permission to read it. setDataAndType() sets the URI and MIME type, but the permission grant flag must be added explicitly: addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION). Without this flag, the receiving app gets a SecurityException when trying to open the file.
Explain like I'm 5
Imagine your photos are locked in a safe. ContentProvider is a bank teller who can look things up in that safe for you using a special code number. FileProvider is like a secure messenger service — it takes a file from your safe and delivers it to another app in a sealed envelope with a temporary key, so the other app can open it but cannot keep the key forever.
Fun fact
The FileUriExposedException added in Android 7.0 (API 24) was a deliberate breaking change. Google enforced it because file:// URIs bypass all of Android's permission system — any app on the device with READ_EXTERNAL_STORAGE could read a file if it knew the path. ContentProvider URIs carry cryptographic tokens that only the system can validate, making them genuinely secure.
Hands-on challenge
Build a secure photo sharing flow: (1) Configure FileProvider in Manifest and file_paths.xml for cache directory. (2) Write a function that captures a photo using TakePicture ActivityResultContract and saves it to a FileProvider-managed cache path. (3) Write a function that shares the captured photo with FLAG_GRANT_READ_URI_PERMISSION. (4) Write a MediaStore query that returns all images taken in the last 7 days, sorted newest first. (5) Explain what happens to the URI permission grant when the receiving app's task is cleared from recents.
More resources
- ContentProvider basics — Android Developers (Android Docs)
- FileProvider — AndroidX Core (Android Docs)
- URI permissions — Android Developers (Android Docs)
- MediaStore access — scoped storage (Android Docs)
- Storage Access Framework overview (Android Docs)
- Sharing files with Intent.ACTION_SEND / ACTION_SEND_MULTIPLE (Android Docs)