Content Providers (Java)

Room and SQLiteOpenHelper both manage data inside a single app. A ContentProvider is Android's mechanism for sharing structured data between apps, behind a stable, permission-checked interface. Most apps — including RecipeBox — never need to write a custom ContentProvider unless they are deliberately exposing a reusable data source for other apps to consume. But understanding the concept matters anyway, because you will regularly consume system providers, such as the device's Contacts or the photo/video MediaStore.

The Four Methods a ContentProvider Implements

MethodPurpose
query()Returns a Cursor of matching rows
insert()Adds a new row, returns its new Uri
update()Modifies existing rows matching a selection
delete()Removes rows matching a selection

These four methods intentionally mirror the SQLite operations from earlier lessons — a ContentProvider is often just a permission-checked wrapper around a database like the one built with SQLiteOpenHelper or Room.

Anatomy of a content:// URI

content://com.example.recipebox.provider/recipes/42 ^authority^ ^path^ ^id^
  • Authority — uniquely identifies which provider to talk to (usually the app's package name).
  • Path — identifies which table or data type within that provider (recipes).
  • Id (optional) — identifies one specific row (42).
This is exactly why URIs like content://media/external/images/media (MediaStore) or content://com.android.contacts/contacts (Contacts) look the way they do — authority, then path, then optional id.

Querying the System MediaStore

Suppose RecipeBox wants to let the user pick an existing photo from their device to attach as a recipe photo, rather than taking a new one. That means querying the system's MediaStore ContentProvider for images already on the device.

// Querying MediaStore for the device's images
Uri imagesUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME };

Cursor cursor = getContentResolver().query(
    imagesUri, projection, null, null,
    MediaStore.Images.Media.DATE_ADDED + " DESC"
);

if (cursor != null) {
    while (cursor.moveToNext()) {
        long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
        Uri photoUri = ContentUris.withAppendedId(imagesUri, id);
        // photoUri can now be shown in an ImageView or offered as a recipe photo choice
    }
    cursor.close();
}
Explanation:
  • getContentResolver() is the entry point for talking to any ContentProvider, system or custom.
  • RecipeBox never touches the Gallery app's database directly — it goes through the well-defined MediaStore contract.
  • ContentUris.withAppendedId() builds the per-image URI from the table URI plus its id column.

FileProvider: Sharing a File Safely

The far more common real-world need is the opposite direction: RecipeBox wants to share one of its own private files — a recipe photo saved in app-specific storage — with another app (say, a messaging app, so the user can send a recipe photo to a friend). Handing out a raw file:// path doesn't work on modern Android and is a security risk even where it does — the other app would have direct filesystem access it has no business having. FileProvider is a special, ready-made ContentProvider (you don't write query/insert/update/delete yourself) built exactly for this: it hands out a scoped content:// URI instead.

Step 1 — declare it in AndroidManifest.xml:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.recipebox.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

Step 2 — declare which folders are shareable in res/xml/file_paths.xml:

<paths>
    <external-files-path
        name="recipe_photos"
        path="recipe_photos/" />
</paths>

Step 3 — generate a shareable Uri and grant temporary access:

// Sharing a recipe photo file with another app
File photoFile = new File(context.getExternalFilesDir("recipe_photos"), "recipe_12.jpg");

Uri shareUri = FileProvider.getUriForFile(
    context,
    "com.example.recipebox.fileprovider",
    photoFile
);

Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, shareUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

startActivity(Intent.createChooser(shareIntent, "Share recipe photo"));
Explanation:
  • FileProvider.getUriForFile() turns a raw File into a safe, temporary content:// URI.
  • FLAG_GRANT_READ_URI_PERMISSION grants the receiving app read access to just that one file, only for as long as it needs it — not blanket access to RecipeBox's storage.
  • This is the standard, modern way to share a file between apps — you will see it again with camera intents and print/export features.
ContentProviders explain how apps expose queryable data across process boundaries — which is also the foundation the next lesson builds on. Cursor & Loaders looks at how Cursor results were historically kept in sync with the UI lifecycle, and why that older Loader API has since been replaced by ViewModel + LiveData.