Lesson 56 of 83 intermediate

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?

ContentProvider is Android's structured data-sharing mechanism between apps, built on a URI-addressed CRUD interface backed by SQLite or any other data source. FileProvider is a ContentProvider subclass that securely exposes files via content:// URIs with time-limited URI permission grants, replacing the dangerous file:// URI pattern. Together they form Android's inter-app data exchange system.

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

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. 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. 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. 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. 4. In launchCamera, imageFile.parentFile?.mkdirs() ensures the directory exists before FileProvider tries to map the path — missing directory causes FileNotFoundException.
  5. 5. In NotesProvider.query(), cursor.setNotificationUri() wires the cursor to receive updates when notifyChange() is called, enabling live UI updates for registered observers.
  6. 6. notifyChange() after insert/update/delete propagates the change notification to all active cursors pointing at this URI — critical for consistent UI state.
  7. 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. 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. 9. getColumnIndexOrThrow() throws if the column does not exist rather than returning -1 silently — prefer this over getColumnIndex() to catch schema mismatches early.
  10. 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

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery