Content Provider Implementation Patterns

The previous chapter focused on the concepts, classes, and methods behind content providers. This chapter more closely examines some implementations of content providers, organized into simple patterns.

Prerequisites

Understanding this chapter requires that you have read the preceding chapter, along with the chapter on permissions.

The Single-Table Database-Backed Content Provider

The simplest database-backed content provider is one that only attempts to expose a single table’s worth of data to consumers. The CallLog content provider works this way, for example.

Step #1: Create a Provider Class

We start off with a custom subclass of ContentProvider, named, cunningly enough, Provider. Here we need the database-style API methods: query(), insert(), update(), delete(), and getType().

onCreate()

Here is the onCreate() method for Provider, from the ContentProvider/ConstantsPlus sample application:

  @Override
  public boolean onCreate() {
    db=new DatabaseHelper(getContext());

    return(true);
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

While that does not seem all that special, the “magic” is in the private DatabaseHelper object, a fairly conventional SQLiteOpenHelper implementation:

package com.commonsware.android.constants;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteDatabase;
import android.hardware.SensorManager;

class DatabaseHelper extends SQLiteOpenHelper {
  private static final String DATABASE_NAME="constants.db";
  static final String TITLE="title";
  static final String VALUE="value";

  public DatabaseHelper(Context context) {
    super(context, DATABASE_NAME, null, 1);
  }
  
  @Override
  public void onCreate(SQLiteDatabase db) {
    Cursor c=db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='constants'", null);
    
    try {
      if (c.getCount()==0) {
        db.execSQL("CREATE TABLE constants (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL);");
        
        ContentValues cv=new ContentValues();
        
        cv.put(Provider.Constants.TITLE, "Gravity, Death Star I");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Earth");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_EARTH);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Jupiter");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_JUPITER);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Mars");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_MARS);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Mercury");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_MERCURY);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Moon");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_MOON);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Neptune");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_NEPTUNE);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Pluto");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_PLUTO);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Saturn");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_SATURN);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Sun");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_SUN);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, The Island");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_THE_ISLAND);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Uranus");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_URANUS);
        db.insert("constants", Provider.Constants.TITLE, cv);
        
        cv.put(Provider.Constants.TITLE, "Gravity, Venus");
        cv.put(Provider.Constants.VALUE, SensorManager.GRAVITY_VENUS);
        db.insert("constants", Provider.Constants.TITLE, cv);
      }
    }
    finally {
      c.close();
    }
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    android.util.Log.w("Constants", "Upgrading database, which will destroy all old data");
    db.execSQL("DROP TABLE IF EXISTS constants");
    onCreate(db);
  }
}
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/DatabaseHelper.java)

Note that we are creating the DatabaseHelper in onCreate() and are never closing it. That is because there is no onDestroy() (or equivalent) method in a ContentProvider. While we might be tempted to open and close the database on every operation, that will not work, as we cannot close the database and still hand back a live Cursor from the database. Hence, we leave it open and assume that SQLite’s transactional nature will ensure that our database is not corrupted when Android shuts down the ContentProvider.

query()

For SQLite-backed storage providers like this one, the query() method implementation should be largely boilerplate. Use a SQLiteQueryBuilder to convert the various parameters into a single SQL statement, then use query() on the builder to actually invoke the query and give you a Cursor back. The Cursor is what your query() method then returns.

For example, here is query() from Provider:

  @Override
  public Cursor query(Uri url, String[] projection, String selection,
                      String[] selectionArgs, String sort) {
    SQLiteQueryBuilder qb=new SQLiteQueryBuilder();

    qb.setTables(TABLE);

    String orderBy;

    if (TextUtils.isEmpty(sort)) {
      orderBy=Constants.DEFAULT_SORT_ORDER;
    }
    else {
      orderBy=sort;
    }

    Cursor c=
        qb.query(db.getReadableDatabase(), projection, selection,
                 selectionArgs, null, null, orderBy);

    c.setNotificationUri(getContext().getContentResolver(), url);

    return(c);
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

We create a SQLiteQueryBuilder and pour the query details into the builder, notably the name of the table that we query against and the sort order (substituting in a default sort if the caller did not request one). When done, we use the query() method on the builder to get a Cursor for the results. We also tell the resulting Cursor what Uri was used to create it, for use with the content observer system.

insert()

Since this is a SQLite-backed content provider, once again, the implementation is mostly boilerplate: validate that all required values were supplied by the activity, merge your own notion of default values with the supplied data, and call insert() on the database to actually create the instance.

For example, here is insert() from Provider:

  @Override
  public Uri insert(Uri url, ContentValues initialValues) {
    long rowID=
        db.getWritableDatabase().insert(TABLE, Constants.TITLE,
                                        initialValues);

    if (rowID > 0) {
      Uri uri=
          ContentUris.withAppendedId(Provider.Constants.CONTENT_URI,
                                     rowID);
      getContext().getContentResolver().notifyChange(uri, null);

      return(uri);
    }

    throw new SQLException("Failed to insert row into " + url);
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

The pattern is the same as before: use the provider particulars plus the data to be inserted to actually do the insertion.

update()

Here is update() from Provider:

  @Override
  public int update(Uri url, ContentValues values, String where,
                    String[] whereArgs) {
    int count=
        db.getWritableDatabase()
          .update(TABLE, values, where, whereArgs);

    getContext().getContentResolver().notifyChange(url, null);

    return(count);
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

In this case, updates are always applied across the entire collection, though we could have a smarter implementation that supported updating a single instance via an instance Uri.

delete()

Similarly, here is delete() from Provider:

  @Override
  public int delete(Uri url, String where, String[] whereArgs) {
    int count=db.getWritableDatabase().delete(TABLE, where, whereArgs);

    getContext().getContentResolver().notifyChange(url, null);

    return(count);
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

This is almost a clone of the update() implementation described above.

getType()

The last method you need to implement is getType(). This takes a Uri and returns the MIME type associated with that Uri. The Uri could be a collection or an instance Uri; you need to determine which was provided and return the corresponding MIME type.

For example, here is getType() from Provider:

  @Override
  public String getType(Uri url) {
    if (isCollectionUri(url)) {
      return("vnd.android.cursor.dir/constant");
    }

    return("vnd.android.cursor.item/constant");
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

Step #2: Supply a Uri

You may wish to add a public static member… somewhere, containing the Uri for each collection your content provider supports, for use by your own application code. Typically, this is a public static final Uri put on the content provider class itself:

    public static final Uri CONTENT_URI=
        Uri.parse("content://com.commonsware.android.constants.Provider/constants");
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

You may wish to use the same namespace for the content Uri that you use for your Java classes, to reduce the chance of collision with others.

Bear in mind that if you intend for third parties to access your content provider, they will not have access to this public static data member, as your class is not in their project. Hence, you will need to publish the string representation of this Uri that they can hard-wire into their application.

Step #3: Declare the “Columns”

Remember those “columns” you referenced when you were using a content provider, in the previous chapter? Well, you may wish to publish public static values for those too for your own content provider.

Specifically, you may want a public static class implementing BaseColumns that contains your available column names, such as this example from Provider:

  public static final class Constants implements BaseColumns {
    public static final Uri CONTENT_URI=
        Uri.parse("content://com.commonsware.android.constants.Provider/constants");
    public static final String DEFAULT_SORT_ORDER="title";
    public static final String TITLE="title";
    public static final String VALUE="value";
  }
(from ContentProvider/ConstantsPlus/app/src/main/java/com/commonsware/android/constants/Provider.java)

Since we are using SQLite as a data store, the values for the column name constants should be the corresponding column names in the table, so you can just pass the projection (array of columns) to SQLite on a query(), or pass the ContentValues on an insert() or update().

Note that nothing in here stipulates the types of the properties. They could be strings, integers, or whatever. The biggest limitation is what a Cursor can provide access to via its property getters. The fact that there is nothing in code that enforces type safety means you should document the property types well, so people attempting to use your content provider know what they can expect.

Step #4: Update the Manifest

Finally, we need to add the provider to the AndroidManifest.xml file, by adding a <provider> element as a child of the <application> element:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.constants"
  android:versionCode="1"
  android:versionName="1.0">

  <supports-screens
    android:anyDensity="true"
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="true"/>

  <uses-sdk
    android:minSdkVersion="14"
    android:targetSdkVersion="18"/>

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name">
    <provider
      android:name=".Provider"
      android:authorities="com.commonsware.android.constants.Provider"
      android:exported="false"/>

    <activity
      android:name=".ConstantsBrowser"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>
(from ContentProvider/ConstantsPlus/app/src/main/AndroidManifest.xml)

The Local-File Content Provider

Implementing a content provider that supports serving up files based on Uri values is similar, and generally simpler, than creating a content provider for the database-style API. In this section, we will examine the ContentProvider/Files sample project. This project demonstrates a common use of the filesystem-style API: serving files from internal storage to third-party applications (who, by default, cannot read your internally-stored files).

Note that this sample project will only work on devices that have an application capable of viewing PDF files accessed via content:// Uri values.

The FileProvider Class

Our ContentProvider is named FileProvider. However, most of the logic is contained in an AbstractFileProvider that will be used for a handful of sample apps in this chapter. We will look at both of those classes, focusing first on the FileProvider.

onCreate()

We have an onCreate() method. In many cases, this would not be needed for this sort of provider. After all, there is no database to open. In this case, we use onCreate() to copy the file(s) out of assets into the app-local file store. In principle, this would allow our application code to modify these files as the user uses the app (versus the unmodifiable editions in assets/).

  @Override
  public boolean onCreate() {
    File f=new File(getContext().getFilesDir(), "test.pdf");

    if (!f.exists()) {
      AssetManager assets=getContext().getAssets();

      try {
        copy(assets.open("test.pdf"), f);
      }
      catch (IOException e) {
        Log.e("FileProvider", "Exception copying from assets", e);

        return(false);
      }
    }

    return(true);
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FileProvider.java)

This uses a static copy() method, inherited from AbstractFileProvider, that can copy an InputStream from an asset to a local File. We will take a peek at this later in this chapter.

openFile()

We need to implement openFile(), to return a ParcelFileDescriptor corresponding to the supplied Uri:

  @Override
  public ParcelFileDescriptor openFile(Uri uri, String mode)
    throws FileNotFoundException {
    File root=getContext().getFilesDir();
    File f=new File(root, uri.getPath()).getAbsoluteFile();

    if (!f.getPath().startsWith(root.getPath())) {
      throw new
        SecurityException("Resolved path jumped beyond root");
    }

    if (f.exists()) {
      return(ParcelFileDescriptor.open(f, parseMode(mode)));
    }

    throw new FileNotFoundException(uri.getPath());
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FileProvider.java)

We are passed in a *nix-style string mode, which will be a value like r for read access, wt for write access (and truncate the file), etc. In API Level 19+, ParcelFileDescriptor has a convenience method for converting such modes into the equivalent ParcelFileDescriptor flag values. For older devices, you can simply use the parseMode() code that Google added:

  // following is from ParcelFileDescriptor source code
  // Copyright (C) 2006 The Android Open Source Project
  // (even though this method was added much after 2006...)

  private static int parseMode(String mode) {
    final int modeBits;
    if ("r".equals(mode)) {
      modeBits=ParcelFileDescriptor.MODE_READ_ONLY;
    }
    else if ("w".equals(mode) || "wt".equals(mode)) {
      modeBits=
          ParcelFileDescriptor.MODE_WRITE_ONLY
              | ParcelFileDescriptor.MODE_CREATE
              | ParcelFileDescriptor.MODE_TRUNCATE;
    }
    else if ("wa".equals(mode)) {
      modeBits=
          ParcelFileDescriptor.MODE_WRITE_ONLY
              | ParcelFileDescriptor.MODE_CREATE
              | ParcelFileDescriptor.MODE_APPEND;
    }
    else if ("rw".equals(mode)) {
      modeBits=
          ParcelFileDescriptor.MODE_READ_WRITE
              | ParcelFileDescriptor.MODE_CREATE;
    }
    else if ("rwt".equals(mode)) {
      modeBits=
          ParcelFileDescriptor.MODE_READ_WRITE
              | ParcelFileDescriptor.MODE_CREATE
              | ParcelFileDescriptor.MODE_TRUNCATE;
    }
    else {
      throw new IllegalArgumentException("Bad mode '" + mode + "'");
    }
    return modeBits;
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FileProvider.java)

Our openFile() method then uses parseMode() in the call to the static open() method on ParcelFileDescriptor, which opens the file (with the desired access mode) and gives us our ParcelFileDescriptor back that we can return. If the file is not found, we can throw a FileNotFoundException to indicate that.

However, we also check to see that the File that we are trying to access is inside getFilesDir(), by comparing paths. A Uri can have .. path segments to move up directory levels. Using that with the File constructor means that a rogue Uri could move outside of our designated root directory (getFilesDir()), to perhaps try to access other data on our internal storage (e.g., databases). getAbsoluteFile() will net out any path-traversal segments (e.g., ..). If getAbsoluteFile() lies within getFilesDir(), we go ahead, otherwise we throw a SecurityException.

getDataLength()

AbstractFileProvider gives us a callback — getDataLength() — where we can indicate how big a file is, given its Uri. That information will be made available to clients consuming this stream. The default will be to indicate that the file size is unknown… and that usually works. However, if it is easy for you to determine the file size, do so, and it will increase the compatibility of your app with possible consumers.

In this case, determining the size of a local file is easy:

  @Override
  protected long getDataLength(Uri uri) {
    File f=new File(getContext().getFilesDir(), uri.getPath());

    return(f.length());
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FileProvider.java)

The AbstractFileProvider Class

AbstractFileProvider is designed to handle a lot of common boilerplate for streaming providers like the one provided in this sample.

getType()

Just as our database-style ContentProvider needed to implement getType() to provide a MIME type given a Uri, so too do our streaming providers. The difference is that a streaming provider usually wants to use “real” MIME types, values that third-party apps are likely to recognize. For example, a PDF file should use a MIME type of application/pdf, as that is what PDF viewing apps will expect.

Android has some convenience code for determining a likely MIME type. You can use MimeTypeMap to convert a file extension to a MIME type, or you can use guessContentTypeFromName() onURLConnection to get a MIME type for a URL. Both use the same underlying database — the difference is mostly a matter of whether you have a bare file extension already or not. So, the default implementation of getType() in AbstractFileProvider uses guessContentTypeFromName():

  @Override
  public String getType(Uri uri) {
    return(URLConnection.guessContentTypeFromName(uri.toString()));
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java)

If you know that your MIME type is unlikely to be recognized by Android (e.g., you invented your own), a subclass of AbstractFileProvider could handle those cases, chaining to the superclass for other Uri values.

insert(), update(), and delete()

ContentProvider itself is abstract, requiring us to implement a variety of methods to satisfy the compiler. Three of them — insert(), update(), and delete() — have no role in a pure-streaming ContentProvider, so AbstractFileProvider has stub implementations:

  @Override
  public Uri insert(Uri uri, ContentValues initialValues) {
    throw new RuntimeException("Operation not supported");
  }

  @Override
  public int update(Uri uri, ContentValues values, String where,
                    String[] whereArgs) {
    throw new RuntimeException("Operation not supported");
  }

  @Override
  public int delete(Uri uri, String where, String[] whereArgs) {
    throw new RuntimeException("Operation not supported");
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java)

A ContentProvider that supports both the database-style and streaming APIs will need real implementations of those methods for the database operations, perhaps throwing an Exception for requests to insert, update, or delete a Uri that represents a stream.

query() and getFileName()

We also need to implement query(). You can get by with having this be a stub similar to insert() and kin. However, for better compatibility, you should have a more robust query() implementation, as it will be used by ContentResolver to retrieve two pieces of metadata about a Uri:

query() will be called with a projection that contains either OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, or both. A streaming ContentProvider ideally supports returning a Cursor with this data. The AbstractFileProvider implementation of query() handles this for us:

abstract class AbstractFileProvider extends ContentProvider {
  private final static String[] OPENABLE_PROJECTION= {
      OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };

  @Override
  public Cursor query(Uri uri, String[] projection, String selection,
                      String[] selectionArgs, String sortOrder) {
    if (projection == null) {
      projection=OPENABLE_PROJECTION;
    }

    final MatrixCursor cursor=new MatrixCursor(projection, 1);

    MatrixCursor.RowBuilder b=cursor.newRow();

    for (String col : projection) {
      if (OpenableColumns.DISPLAY_NAME.equals(col)) {
        b.add(getFileName(uri));
      }
      else if (OpenableColumns.SIZE.equals(col)) {
        b.add(getDataLength(uri));
      }
      else { // unknown, so just add null
        b.add(null);
      }
    }

    return(new LegacyCompatCursorWrapper(cursor));
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java)

If the supplied projection is null, we assume that the caller wants the standard OpenableColumns; otherwise, we will use the supplied projection.

Our results will be packaged in a MatrixCursor. This amounts to a Cursor interface on a two-dimensional array, where you build up the rows in that array via a MatrixCursor.RowBuilder. In our case, there will only be one such row, for the relevant values for the file to be streamed in support of the requested Uri.

We iterate over the columns in the projection, calling out to getFileName() and getDataLength() methods for OpenableColumns.DISPLAY_NAME and OpenableColumns.SIZE respectively (and using null as the result for anything else). The default implementations of those methods return the last path segment of the Uri and AssetFileDescriptor.UNKNOWN_LENGTH, respectively:


  protected String getFileName(Uri uri) {
    return(uri.getLastPathSegment());
  }

  protected long getDataLength(Uri uri) {
    return(AssetFileDescriptor.UNKNOWN_LENGTH);
  }
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java)

Subclasses can override those as needed, as we saw with getDataLength() in the concrete FileProvider class.

However, query() does not return the MatrixCursor directly. Instead, it wraps it in a LegacyCompatCursorWrapper. This class comes from the CWAC-Provider project, from the author of this book. LegacyCompatCursorWrapper is designed to try to improve compatibility with clients that are expecting query() results to include a _DATA column, the way that MediaStore does. Poorly-written clients will crash if this column does not exist. LegacyCompatCursorWrapper wraps a Cursor and serves up an empty _DATA column for those clients that need one.

copy()

AbstractFileProvider also has a convenience copy() static method that copies an InputStream to a File, used from the FileProvider onCreate() method:

  static void copy(InputStream in, File dst)
                                                      throws IOException {
    FileOutputStream out=new FileOutputStream(dst);
    byte[] buf=new byte[1024];
    int len;

    while ((len=in.read(buf)) >= 0) {
      out.write(buf, 0, len);
    }

    in.close();
    out.close();
  }
}
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java)

The Manifest

Finally, we need to add the provider to the AndroidManifest.xml file, by adding a <provider> element as a child of the <application> element, as with any other content provider:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.commonsware.android.cp.files"
  android:versionCode="1"
  android:versionName="1.0">

  <supports-screens
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="true"/>

  <application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name">
    <activity
      android:name="FilesCPDemo"
      android:label="@string/app_name"
      android:theme="@android:style/Theme.Translucent.NoTitleBar">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <provider
      android:name=".FileProvider"
      android:authorities="com.commonsware.android.cp.files"
      android:exported="true"/>
  </application>

</manifest>
(from ContentProvider/Files/app/src/main/AndroidManifest.xml)

Note, however, that we have android:exported="true" set in our <provider> element. This means that this content provider can be accessed from third-party apps or other external processes (e.g., the media framework for playing back videos).

Using this Provider

The activity is fairly trivial, simply creating an ACTION_VIEW Intent on our PDF file and starting up an activity for it, then finishing itself:

package com.commonsware.android.cp.files;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

public class FilesCPDemo extends Activity {
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);
    
    startActivity(new Intent(Intent.ACTION_VIEW,
                             Uri.parse(FileProvider.CONTENT_URI
                                 + "test.pdf")));
    finish();
  }
}
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FilesCPDemo.java)

Here, we use a CONTENT_URI published by FileProvider as the basis for identifying the file:

  public static final Uri CONTENT_URI=
      Uri.parse("content://com.commonsware.android.cp.files/");
(from ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/FileProvider.java)

The Protected Provider

The problem with the preceding example is that any app on the device, if it knows the right Uri to ask for, will be able to access the file. This may be desired, but often times it will not be. Instead, you may want to specifically indicate which apps, at specific points in time, can view the file.

Particularly if your objective is to start a third-party app to work with that file, setting up this sort of security is not that difficult. To see how that works, we will walk through the ContentProvider/GrantUriPermissions sample project. This is a clone of the ContentProvider/Files project with this extra security added on.

The way the defense works is by using Android’s permission system. We will mark the ContentProvider as being not exported, then selectively grant that access to a specific Uri to the app that we want to view our file.

Step #1: Mark the Provider as Not Exported

Putting android:exported="false" on the <provider> element indicates that no app has the ability to make requests of your ContentProvider, except for specific cases where you authorize it:

    <provider
      android:name="FileProvider"
      android:authorities="com.commonsware.android.cp.files"
      android:exported="false"
      android:grantUriPermissions="false">
      <grant-uri-permission android:path="/test.pdf"/>
    </provider>
(from ContentProvider/GrantUriPermissions/app/src/main/AndroidManifest.xml)

With no other changes, if we tried to use the app, the third-party PDF viewer would crash when trying to read our PDF file from the Uri.

Step #2: Grant Access to the Uri

To allow third parties to get access only when we specify, we need to make a few more changes.

This <provider> element also has android:grantUriPermissions="false". That is the default value for this attribute, shown here purely for illustration purposes. It also has a <grant-uri-permissions> child element, listing the local path (within the ContentProvider) to our PDF file.

The <grant-uri-permissions> element (or elements, plural) allow us to override the permission requirement for certain pieces of content, granting access to that content on a per-request basis. There are three possibilities:

  1. If android:grantUriPermissions is true, then we will be able to grant access to any content within our provider
  2. If android:grantUriPermissions is false, but we have <grant-uri-permissions> sub-elements, we can only grant access to the content identified by the Uri paths specified in those sub-elements
  3. If android:grantUriPermissions is false, and we have no <grant-uri-permissions> sub-elements (the default case), we cannot grant access to any content within our provider

In this case, we specify that we will only grant access to /test.pdf. Since that is the only content in this provider, we could have the same net effect by setting android:grantUriPermissions to true.

Then, when we create an Intent used to interact with another component, we can include a flag indicating what permission we wish to grant:

package com.commonsware.android.cp.perms;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;

public class FilesCPDemo extends Activity {
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);

    Intent i=new Intent(Intent.ACTION_VIEW, Uri.parse(FileProvider.CONTENT_URI + "test.pdf"));

    i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    startActivity(i);
    finish();
  }
}
(from ContentProvider/GrantUriPermissions/app/src/main/java/com/commonsware/android/cp/perms/FilesCPDemo.java)

In this revised version of our activity, we add FLAG_GRANT_READ_URI_PERMISSION to the Intent used with startActivity(). This will grant the activity that responds to our Intent read access to the specific Uri in the Intent, overriding the exported status. That is why, when you run this app on a device, the PDF viewer will still be able to view the file.

There is also FLAG_GRANT_WRITE_URI_PERMISSION for granting write access, not needed here, as our provider only supports read access.

While this is most commonly used with startActivity() (e.g., allowing a mail program limited access to your attachments provider), this can also be used with startService(), bindService(), and the various flavors of sending broadcasts (e.g., sendBroadcast()).

The Stream Provider

Sometimes, we want a provider that looks like the local-file provider from the preceding section… but we do not have a file. Instead, we have data in some other form, such as a byte array, or a String, or an InputStream. Writing that material to a file may be problematic, or even counterproductive.

For example, imagine an app that stores data on the user’s behalf in an encrypted fashion. One such file is a PDF, that the user would like to view. There are PDF viewers that can view files served via content:// Uri values, as the previous section demonstrated… but that assumes an unencrypted file. While we could decrypt the file, writing the decrypted results to another file, and serve the decrypted data to the PDF viewer, now we have a persistent decrypted version of the data. That opens a window of time when the data might be accessed by people with nefarious intent, which is something we are trying to avoid by using the encrypted store in the first place. Rather, it would be nice if we could decrypt the data on the fly and give that decrypted result to the PDF viewer. Of course, there are security risks intrinsic to that too — after all, we do not know what the PDF viewer might do with the unencrypted data — but it is at least an improvement.

The good news is that Android does support streaming options for openFile()-style ContentProvider implementations. However, as one might expect, they are not the simplest things to implement.

In this section, we will examine the ContentProvider/Pipe sample project. This is a near clone of the ContentProvider/Files sample from the preceding section. However, rather than simply handing the file to Android to serve as content, we will stream it in ourselves. In principle, as part of this streaming, we could be decrypting it from an encrypted state. Since this sample shares much code with the previous sample, we will focus solely on the changes here.

Note that this sample was inspired by the sample found at https://github.com/nandeeshwar/Pfd-Create-Pipe.

The Pipes

Starting with API Level 9, it is possible to create a pipe between two processes, from the Android SDK, via ParcelFileDescriptor. In the previous section, we saw how ParcelFileDescriptor could be used to open a local file and make that available to other processes — the createPipe() method gives us a pipe.

The “pipe” returned by createPipe() is a two-element array of ParcelFileDescriptor objects. The first element in the array represents the “read” end of the pipe. In our case, that is the end that should be used by a PDF viewer to read in the file contents. The second element of the array represents the “write” end of the pipe, which we will use to supply the file’s contents to the “read” end (and to the PDF viewer by extension).

The Revised openFile()

With that in mind, here is our revised openFile() method:

  @Override
  public ParcelFileDescriptor openFile(Uri uri, String mode)
    throws FileNotFoundException {
    ParcelFileDescriptor[] pipe=null;

    try {
      pipe=ParcelFileDescriptor.createPipe();
      AssetManager assets=getContext().getAssets();

      new TransferThread(assets.open(uri.getLastPathSegment()),
                       new AutoCloseOutputStream(pipe[1])).start();
    }
    catch (IOException e) {
      Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
      throw new FileNotFoundException("Could not open pipe for: "
          + uri.toString());
    }

    return(pipe[0]);
  }
(from ContentProvider/Pipe/app/src/main/java/com/commonsware/android/cp/pipe/PipeProvider.java)

We create our pipe via createPipe(), then get an InputStream on our PDF file stored as an asset — unlike the ContentProvider/Files sample, we do not need to copy the asset to a local file now. We then kick off a background thread, implemented in an inner class named TransferThread, to actually copy the data from the asset to the write end of the pipe.

Rather than supply TransferThread with a ParcelFileDescriptor for the write end of the pipe, we supply an OutputStream. Specifically, we pass in a ParcelFileDescriptor.AutoCloseOutputStream. This is an OutputStream that knows to close the ParcelFileDescriptor when we close the stream. Otherwise, it behaves like a fairly typical OutputStream.

The Transfer

TransferThread is a fairly conventional copy-data-from-stream-to-stream implementation:

  static class TransferThread extends Thread {
    InputStream in;
    OutputStream out;

    TransferThread(InputStream in, OutputStream out) {
      this.in=in;
      this.out=out;
    }

    @Override
    public void run() {
      byte[] buf=new byte[1024];
      int len;

      try {
        while ((len=in.read(buf)) >= 0) {
          out.write(buf, 0, len);
        }

        in.close();
        out.flush();
        out.close();
      }
      catch (IOException e) {
        Log.e(getClass().getSimpleName(),
              "Exception transferring file", e);
      }
    }
  }
(from ContentProvider/Pipe/app/src/main/java/com/commonsware/android/cp/pipe/PipeProvider.java)

Here, we read in data in 1KB blocks from the InputStream (our asset) and write the data to our OutputStream (obtained from the ParcelFileDescriptor).

The Results

Our activity logic has not substantially changed. We still create an ACTION_VIEW Intent on the content:// Uri from our provider, pointing to our test.pdf asset. Any PDF viewer capable of handling content:// Uri values will use a ContentResolver to open an InputStream for our Uri. In the ContentProvider/Files sample, that InputStream would receive the contents of the file directly from Android. In this new sample, that InputStream is reading in bytes off of our pipe, until such time as it has read in all the streamed data and we have closed the OutputStream.

Not every possible consumer of a Uri will be able to work with our stream, though. For example, MediaPlayer expects to be able to move forwards and backwards within the stream, and while that works for file-backed ParcelFileDescriptors, it does not work for those representing a pipe. Hence, MediaPlayer will crash when trying to use a Uri to a pipe-based stream, which is certainly unfortunate.

The author would like to thank Reuben Scratton for his assistance in tracking down this MediaPlayer limitation.

FileProvider

The Android Support package now contains its own implementation of a FileProvider that greatly simplifies serving files from internal or external storage to another app.

Here, we will see Google’s FileProvider in action via the ContentProvider/V4FileProvider sample project. This is a near clone of the ContentProvider/Pipe sample from the preceding section, just leveraging FileProvider to help us serve a file from internal storage.

The Rationale

The documentation for FileProvider states:

Apps should generally avoid sending raw filesystem paths across process boundaries, since the receiving app may not have the same access as the sender. Instead, apps should send Uri backed by a provider like FileProvider.

This is not just an issue for passing files from internal storage to other apps. On Android 4.2+ tablets, it could even be an issue for external storage, as each user account gets its own portion of external storage. There may be scenarios in which your app (associated with one user) winds up needing to pass the contents of a file on external storage to another app (associated with another user). Regular filesystem paths will not work in this case, as one user account cannot directly access another user account’s files, even on external storage.

The Sources of Files

Google’s FileProvider offers automatic serving of files from a few root points:

For each of these, you will be able to specify a specific subdirectory’s worth of files that should be served, if you do not want the entire directory’s contents published via FileProvider. You will also be able to specify an alias, which serves as the first path segment (after the authority in the content:// Uri) — FileProvider maps that path segment to a specific location of files to serve.

The Manifest Entry

The information about what files to serve comes in the form of an XML resource file. You can name the file whatever you like, but its content needs to be a root <paths> element, with a series of children for the different directories you wish to serve. Those directories will be denoted via child elements with specific names:

Note that the latter two require version 24.2.0 or higher of the support library, as they are fairly new.

For example, our sample project has a res/xml/provider_paths.xml file with the following contents:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="stuff" />
</paths>
(from ContentProvider/V4FileProvider/app/src/main/res/xml/provider_paths.xml)

Here, we are saying that we want to serve the contents of getFilesDir(), using a virtual root path of stuff. With an authority of com.commonsware.android.cp.v4file, this means that a Uri of content://com.commonsware.android.cp.v4file/stuff/test.pdf would serve up a test.pdf file in the getFilesDir() directory.

The optional path attribute of the <files-path>, etc. elements indicates a particular subdirectory, relative to the element-specific root, that should be used as the source of files. So, for example, had the provider_paths.xml file looked like:


<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="stuff" path="help/" />
</paths>

…then content://com.commonsware.android.cp.v4file/stuff/test.pdf would map to help/test.pdf inside of getFilesDir().

You then point to this XML resource from a <meta-data> element in the <provider> element in the manifest, teaching FileProvider what to serve. For example, our <provider> element in this sample app is:

    <provider
      android:name="LegacyCompatFileProvider"
      android:authorities="com.commonsware.android.cp.v4file"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
    </provider>
(from ContentProvider/V4FileProvider/app/src/main/AndroidManifest.xml)

Here, our android:name points to a LegacyCompatFileProvider class that we will examine shortly. We still provide the android:authorities value, along with any permission rules that we want. Beyond that, we have a <meta-data> element, with an android:name of android.support.FILE_PROVIDER_PATHS, that points to our XML resource with the path information.

You will also notice that our android:exported attribute is set to false. As it turns out, FLAG_GRANT_READ_URI_PERMISSION trumps the exported status of a provider. If you pass a Uri to an activity using FLAG_GRANT_READ_URI_PERMISSION, the activity will be able to read the contents of that Uri, even if the provider itself is not exported.

The Legacy Compatibility

LegacyCompatFileProvider is a simple subclass of FileProvider, one that overrides query() and wraps its Cursor in a LegacyCompatCursorWrapper to try to improve compability with ill-behaved clients:

package com.commonsware.android.cp.v4file;

import android.database.Cursor;
import android.net.Uri;
import android.support.v4.content.FileProvider;
import com.commonsware.cwac.provider.LegacyCompatCursorWrapper;

public class LegacyCompatFileProvider extends FileProvider {
  @Override
  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    return(new LegacyCompatCursorWrapper(super.query(uri, projection, selection, selectionArgs, sortOrder)));
  }
}
(from ContentProvider/V4FileProvider/app/src/main/java/com/commonsware/android/cp/v4file/LegacyCompatFileProvider.java)

The Usage

At this point, the provider is ready for use, insofar as we can specify Uri values like content://com.commonsware.android.cp.v4file/stuff/test.pdf and get results. Of course, we actually need to have files in our internal storage, and we need to use such a Uri.

Hence, our activity combines the unpack-the-file-from-assets logic from our own providers in earlier samples, plus starts up a PDF viewer on our designated test.pdf file:

package com.commonsware.android.cp.v4file;

import android.app.Activity;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Bundle;
import android.support.v4.content.FileProvider;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class FilesCPDemo extends Activity {
  private static final String AUTHORITY="com.commonsware.android.cp.v4file";
      
  @Override
  public void onCreate(Bundle state) {
    super.onCreate(state);

    File f=new File(getFilesDir(), "test.pdf");

    if (!f.exists()) {
      AssetManager assets=getAssets();

      try {
        copy(assets.open("test.pdf"), f);
      }
      catch (IOException e) {
        Log.e("FileProvider", "Exception copying from assets", e);
      }
    }

    Intent i=
        new Intent(Intent.ACTION_VIEW,
                   FileProvider.getUriForFile(this, AUTHORITY, f));

    i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    startActivity(i);
    finish();
  }

  static private void copy(InputStream in, File dst) throws IOException {
    FileOutputStream out=new FileOutputStream(dst);
    byte[] buf=new byte[1024];
    int len;

    while ((len=in.read(buf)) > 0) {
      out.write(buf, 0, len);
    }

    in.close();
    out.close();
  }
}
(from ContentProvider/V4FileProvider/app/src/main/java/com/commonsware/android/cp/v4file/FilesCPDemo.java)

FileProvider offers a handy getUriForFile() static helper method that will return a Uri for a given file, incorporating our specified content provider authority.

The result of running this activity is the same as the other file-serving provider samples from this chapter: a PDF viewer (if one is available) will display the test.pdf file.

StreamProvider

FileProvider is rather nice: you can serve up typical file-based content without having to roll your own implementation of ContentProvider and openFile(). However, it only supports a few sources of data.

The author of this book has written StreamProvider, a fork of FileProvider that adds support for serving content from assets and raw resources. Plus, through subclassing, you can readily serve up content from other sources as well. StreamProvider can be found in the CWAC-Provider project.

You can add this library to your Android Studio project much in the same way as you can other CWAC libraries: add the CWAC repository and request the dependency:


repositories {
    maven {
        url "https://s3.amazonaws.com/repo.commonsware.com"
    }
}

dependencies {
    implementation 'com.commonsware.cwac:provider:0.5.0'
}

Once you have added the CWAC-Provider dependency to your project, you use it much the same as you would use FileProvider:


<provider
  android:name="com.commonsware.cwac.provider.StreamProvider"
  android:authorities="..."
  android:exported="false"
  android:grantUriPermissions="true">
  <meta-data
    android:name="com.commonsware.cwac.provider.STREAM_PROVIDER_PATHS"
    android:resource="@xml/..."/>
  <meta-data
    android:name="com.commonsware.cwac.provider.USE_LEGACY_CURSOR_WRAPPER"
    android:value="true"/>
</provider>

Exporting and Usage Patterns

If your StreamProvider is exported, all of your streams will be considered read-only, regardless of any other configuration. Mostly, this mode is here for cases where you need a streaming provider and cannot grant Uri permissions (e.g., implementing a ChooserTargetService).

If your StreamProvider is not exported, and it has android:grantUriPermissions set, then you can control, on a per-Uri basis, which clients get access to your streams. This works identically to how FileProvider works. Whether a particular source of streams is read-only or read-write will depend on whether the stream is a file and your metadata configuration.

Wherever possible, elect to not export the provider and use FLAG_GRANT_READ_URI_PERMISSIONS or similar techniques to selectively grant access to your content.

Note that the exported-and-read-only rule is on a per-provider basis. If you have some content that needs to be published globally and others that are not:

Metadata Elements

Google’s FileProvider supports:

Each of those take a name attribute, indicating the first path segment of the Uri that should identify this particular source of files. For example, a name of foo would mean that content://your.authority.here/foo/... would look for a ... file in that particular element’s source of files.

Each of those optionally take a path attribute, indicating a subdirectory under the element-defined root to use as the source of files, rather than the root itself. So, a <files-path> with a path="stuff" attribute would serve files from the stuff/ subdirectory within getFilesDir(). Note that path can point to a file as well, to limit access to a single file rather than a directory. Note that path is required for <files-path>, so you do not accidentally serve everything under getFilesDir().

Also, each can optionally take a readOnly attribute. If this is set to true, then the files will be readable, but not writeable.

<external-files-path> also can take an optional dir attribute. If missing, the files are served from getExternalFilesDir(). If a valid value of dir is supplied, that value is passed into getExternalFilesDir(). As such, dir is limited to be one of the Environment.DIRECTORY_* constants:

However, you cannot have both <external-files-path> with no dir (indicating that you are serving from getExternalFilesDir(null)) and one or more <external-files-path> elements with dir values, as they will conflict.

StreamProvider adds support for:

Hence, StreamProvider is especially useful when you want to package some content — such as a PDF file for online help — that you want to serve from your app. Just drop the file in assets/ in your project, set up StreamProvider to serve up assets, and use an appropriate Intent with startActivity() to view that file.

In the case of <dir-path>, two attributes are required:

In the case of <external-public-path>, dir is required. It needs to be the string value of one of the Environment.DIRECTORY_* constants, listed above.

Assets and Gradle

For files you are looking to share from your app’s assets/, you will need to teach the build system to avoid compressing those files. While annoying, it helps StreamProvider be more compatible with various client apps.

To do this, add an aaptOptions closure to your android closure in your module’s build.gradle file. For example, you might have:


android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"

    aaptOptions {
        noCompress 'pdf', 'mp4', 'ogg'
    }
}

This would tell Gradle and the build system to not compress files ending in pdf, mp4, and ogg. For your own project, you would choose the file extensions of relevance for the content that you are looking to serve out of assets/.

I Can Haz Uri?

FileProvider has the static getUriForFile() convenience method, to build a Uri pointing to the FileProvider, given the File that you wish to serve.

StreamProvider has a similar getUriForFile() method, with three key differences:

  1. It only takes the authority string and the File; no Context is necessary
  2. It only works for files, not assets or raw resources
  3. Rather than throwing an exception for an unrecognized File (the way FileProvider does), StreamProvider just returns null, indicating that the File you requested is not one that the StreamProvider is configured to serve

So, you can call StreamProvider.getUriForFile(AUTHORITY, f), for some String for your AUTHORITY and some File (here named f) to get a Uri pointing to that file, for the purposes of using that Uri in an Intent, etc.

Uri Prefixes

Activities that support ACTION_SEND through an appropriate <intent-filter> are likely to have a flaw: they probably do not validate the Uri being supplied via EXTRA_STREAM. The “surreptitious sharing” attack takes advantage of this, tricking the app into sharing its own content. While the researchers who reported this flaw focused on file: Uri schemes, content: is also vulnerable, if your provider’s Uri values are predictable.

To help defeat this attack, StreamProvider automatically adds a per-install UUID to each Uri. So, instead of:

content://your.authority.here/something/and/a/relative/path.xml

the Uri will be something like:

content://your.authority.here/9b80af30-4507-4f34-956a-3b47e4a7f27f/something/and/a/relative/path.xml

By using a UUID unique for this installation of your app, it makes your Uri values dependent upon the device. This makes it more difficult for attackers to hand you a valid Uri to your own content to send somewhere that you might not want.

On the flip side, this makes constructing your own Uri values a bit more difficult. For files, you can use the getUriForFile() method. For assets and raw resources, you can call the static getUriPrefix() method to get the prefix that is being used, and add that to your Uri, such as by using a Uri.Builder:


PROVIDER
  .buildUpon()
  .appendPath(StreamProvider.getUriPrefix(AUTHORITY))
  .appendPath(path)
  .build()

getUriPrefix() takes the authority string of your StreamProvider and returns the prefix… or null, if by subclassing StreamProvider, you disabled this prefix.

Extending StreamProvider

You are welcome to create subclasses of StreamProvider, to extend its capabilities for things that you may want to do in your app. For example, the instrumentation tests for StreamProvider demonstrate creating a subclass that supports serving database files, via a custom <database-path> element in the metadata.

By and large, you just create a subclass of StreamProvider and use it in your <provider> element. Of importance are the hooks in StreamProvider to allow subclasses to change critical behavior.

Customizing the Uri Prefix

In your subclass, you have three options for changing the Uri prefix used by StreamProvider:

Supporting Other Stream Locations

You may have content located in directories other than what StreamProvider supports out of the box, such as the path for SQLite databases. To handle that, you can add support for new XML elements in the <paths> element (e.g., <database-path] for serving up databases).

To do this, in your StreamProvider subclass, override buildStrategy() and return a StreamStrategy implementation that is configured for your scenario. For files located in unusual spots, LocalPathStrategy should work.

In the library’s androidTest/ source set, you will find a DatabaseProvider that, at the time of this writing, looks like this:


public class DatabaseProvider extends StreamProvider {
  private static final String TAG="database-path";
  
  @Override
  protected StreamStrategy buildStrategy(Context context,
                                         String tag, String name,
                                         String path, boolean readOnly,
                                         HashMap<String, String> attrs)
    throws IOException {
    if (TAG.equals(tag)) {
      return(new LocalPathStrategy(name,
        context.getDatabasePath(path)));
    }

    return(super.buildStrategy(context, tag, name, path, attrs));
  }
}

The parameters to buildStrategy() are:

Either return your own StreamStrategy instance based off of this information or chain to the superclass’ implementation, so StreamProvider can handle the stock tags.

If your provider is intrinsically read-only (i.e., it is impossible to modify the content), you can ignore the readOnly flag. If, however, your content could be modified, and you support modification, please honor the readOnly flag and block modifications/deletions when that is set to true.

Supporting Other Stream Strategies

You may have content located in things that are not files, such as BLOB columns in a database. In theory, you can create a custom StreamStrategy implementation that handles this. However, this has not been tried much, and so there are likely to be some gaps in the implementation.

That being said, you can examine the built-in strategies (e.g., AssetStrategy, LocalPathStrategy) and their superclasses (e.g., AbstractPipeStrategy) to see how to implement strategies.

Adding Columns to query()

You may wish to add other columns in response to a query() call, beyond the OpenableColumns that StreamProvider handles itself and the _DATA and MIME_TYPE columns added by LegacyCompatCursorWrapper.

To do that, override getValueForQueryColumn() in your StreamProvider subclass. This is supplied the Uri of the content and the name of the column requested by the client. You can return an Object suitable for stuffing into a MatrixCursor to send back – typically, this will be a String, int, or long.

Totally Overhauling Uri Handling

StreamProvider itself holds onto a CompositeStreamStrategy, delegating all operations to it. If you wish to extend CompositeStreamStrategy and do things differently, also override buildCompositeStrategy() on your StreamProvider subclass, to return the instance of the CompositeStreamStrategy that you want the StreamProvider to use.

Overriding Standard Methods

You can override standard ContentProvider methods (e.g., getType()) if needed.

Alternatively, you can override the methods on a StreamStrategy, then use that alternative StreamStrategy implementation in your buildStrategy() method.

Adding Support for insert() and update()

By default, none of the StreamStrategy implementations support insert() or update(). However, your custom StreamStrategy can, whether you are extending one of the stock strategy classes or are implementing your own from scratch.

First, override canInsert() and/or canUpdate(), returning true for those operations you do support. Then, you can override insert() and update(), which have the same method signatures on StreamStrategy as they do on ContentProvider. There, you can do what you wish.