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.
Understanding this chapter requires that you have read the preceding chapter, along with the chapter on permissions.
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.
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()
.
Here is the onCreate()
method for Provider
, from the
ContentProvider/ConstantsPlus
sample application:
@Override
public boolean onCreate() {
db=new DatabaseHelper(getContext());
return(true);
}
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);
}
}
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
.
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);
}
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.
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);
}
The pattern is the same as before: use the provider particulars plus the data to be inserted to actually do the insertion.
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);
}
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
.
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);
}
This is almost a clone of the update()
implementation described
above.
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");
}
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");
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.
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";
}
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.
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>
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.
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
.
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);
}
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.
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());
}
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;
}
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
.
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());
}
AbstractFileProvider
is designed to handle a lot of common boilerplate
for streaming providers like the one provided in this sample.
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()));
}
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.
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");
}
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.
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
:
Uri
, should we need a
human-readable name? After all, a ContentProvider
Uri
does not have to
represent a human-readable path, and so the last segment of that Uri
could
be a cryptic string of hex digits or something, not a filename.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));
}
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);
}
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.
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();
}
}
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>
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).
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();
}
}
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/");
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.
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>
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
.
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:
android:grantUriPermissions
is true
, then we will be able to grant
access to any content within our providerandroid: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-elementsandroid:grantUriPermissions
is false
, and we have no
<grant-uri-permissions>
sub-elements (the default case), we cannot grant access
to any content within our providerIn 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();
}
}
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()
).
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.
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).
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]);
}
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
.
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);
}
}
}
Here, we read in data in 1KB blocks from the InputStream
(our asset) and write the data
to our OutputStream
(obtained from the ParcelFileDescriptor
).
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 ParcelFileDescriptor
s, 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.
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 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.
Google’s FileProvider
offers automatic serving of files from a few root points:
getFilesDir()
(i.e., the standard portion of internal storage for your app)getCacheDir()
(i.e., internal storage, but files that the OS can purge if
needed to free up disk space)Environment.getExternalStorageDirectory()
(i.e., the root of external storage)getExternalFilesDir(null)
and getExternalCacheDir()
(i.e., unique directories
on external storage for your app)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 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:
<files-path>
for getFilesDir()
<cache-path>
for getCacheDir()
<external-path>
for Environment.getExternalStorageDirectory()
<external-files-path>
for getExternalFilesDir(null)
<external-cache-path>
for getExternalCachePath()
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>
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>
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.
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)));
}
}
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();
}
}
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.
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
:
<paths>
root element, containing
one or more elements describing what you want the provider to servecom.commonsware.cwac.provider.StreamProvider
as a <provider>
to your manifest, under your own android:authority
, with a
<meta-data>
element (with a name of
com.commonsware.cwac.provider.STREAM_PROVIDER_PATHS
), pointing to that
XML metadata
<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>
USE_LEGACY_CURSOR_WRAPPER
<meta-data>
element,
shown in the above example, to automatically add in LegacyCompatCursorWrapper
support, described elsewhere in this chapterFLAG_GRANT_READ_URI_PERMISSION
and FLAG_GRANT_WRITE_URI_PERMISSION
in Intent
objects you use to have third parties use the files
the StreamProvider
serves, to allow those apps selective, temporary
access to the fileIf 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:
StreamProvider
and one <provider>
element for one set of content,
with one authority and android:exported
settingStreamProvider
and have a separate <provider>
element for the
other set of content, with a separate authority and android:exported
settingGoogle’s FileProvider
supports:
<files-path>
for serving files from your app’s getFilesDir()
<external-path>
for serving files from
Environment.getExternalStorageDirectory()
<cache-path>
for serving files from your app’s getCacheDir()
<external-files-path>
for serving files from getExternalFilesDir()
<external-cache-path>
for serving files from getExternalCacheDir()
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:
Alarms
DCIM
Documents
Download
Movies
Music
Notifications
Pictures
Podcasts
Ringtones
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:
<raw-resource>
for serving a particular raw resource, where the path
is the name of the raw resource (without file extension)<asset>
for serving files from assets/
<dir-path>
, for serving files from locations identified by getDir()
<external-public-path>
, for serving files from locations identified by
Environment.getExternalStoragePublicDirectory()
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:
dir
, which indicates what directory to serve (this is passed into getDir()
)path
, which serves its normal role, to determine what to serve from the
directory identified by dir
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.
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/
.
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:
File
; no Context
is necessaryFile
(the
way FileProvider
does), StreamProvider
just returns null
, indicating
that the File
you requested is not one that the StreamProvider
is configured to serveSo, 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.
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.
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.
In your subclass, you have three options for changing the
Uri
prefix used by StreamProvider
:
buildUriPrefix()
and return your own generated
String
getUriPrefix()
and return your constantgetUriPrefix()
and
return null
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:
Context
, should you need one (though do not assume it is
any particular sort of Context
)database-path
)name
attribute, which all of these need to
have, as that is how we determine which StreamStrategy
handles
this requestpath
attribute, which can be null
HashMap
of all attributes, in case you wish to have some
custom onesEither 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
.
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.
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
.
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.
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.
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.