Do you have data in your application? Can another application do something interesting
with that data? To share the information within your application with other applications,
you need to make the application a content provider by providing the standardized content
provider interface for other applications; then you must register your application as a
content provider within the Android manifest file.The most straightforward way to make
an application a content provider is to store the information you want to share in a
SQLite database.
One example is a content provider for GPS track points.This content provider enables
users of it to query for points and store points.The data for each point contains a time
stamp, the latitude and longitude, and the elevation.
Implementing a Content Provider Interface
Implementing a content provider interface is relatively straightforward.The following
code shows the basic interface that an application needs to implement to become a content
provider, requiring implementations of five important methods:
public class TrackPointProvider extends ContentProvider {
public int delete(Uri uri,
String selection, String[] selectionArgs) {
return 0;
}
public String getType(Uri uri) {
return null;
}
public Uri insert(Uri uri, ContentValues values) {
return null;
}
public boolean onCreate() {
return false;
}
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
return null;
}
public int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
return 0;
}
}
Defining the Data URI
The provider application needs to define a base URI that other applications will use to access
this content provider.This must be in the form of a public static final Uri
named CONTENT_URI, and it must start with content://.The URI must be unique.The
best practice for this naming is to use the fully qualified class name of the content provider.
Here,we have created a URI name for our GPS track point provider book example:
public static final Uri CONTENT_URI =
Uri.parse("content://com.androidbook.TrackPointProvider");
Defining Data Columns
The user of the content provider needs to know what columns the content provider has
available to it. In this case, the columns used are timestamp, latitude and longitude, and the
elevation.We also include a column for the record number, which is called _id.
public final static String _ID = "_id";
public final static String TIMESTAMP = "timestamp";
public final static String LATITUDE = "latitude";
public final static String LONGITUDE = "longitude";
public final static String ELEVATION = "elevation";
Users of the content provider use these same strings.A content provider for data such as
this often stores the data within a SQLite database. If this is the case, matching these
columns' names to the database column names simplifies the code.
Implementing Important Content Provider Methods
This section shows example implementations of each of the methods that are used by the
system to call this content provider when another application wants to use it.The system,
in this case, is the ContentResolver interface that was used indirectly in the previous section
when built-in content providers were used.
Some of these methods can make use of a helper class provided by the Android SDK,
UriMatcher, which is used to match incoming Uri values to patterns that help speed up
development.The use of UriMatcher is described and then used in the implementation of
these methods.
Implementing the query() Method
Let's start with a sample query implementation.Any query implementation needs to return
a Cursor object. One convenient way to get a Cursor object is to return the Cursor
from the underlying SQLite database that many content providers use. In fact, the interface
to ContentProvider.query() is compatible with the
SQLiteQueryBuilder.query() call.This example uses it to quickly build the query and
return a Cursor object.
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs,
String sortOrder) {
SQLiteQueryBuilder qBuilder = new SQLiteQueryBuilder();
qBuilder.setTables(TrackPointDatabase.TRACKPOINTS_TABLE);
if ((sURIMatcher.match(uri)) == TRACKPOINT_ID) {
qBuilder.appendWhere("_id=" + uri.getLastPathSegment());
}
Cursor resultCursor = qBuilder.query(mDB
.getReadableDatabase(), projection,
selection, selectionArgs, null, null,
sortOrder, null);
resultCursor.setNotificationUri(getContext()
.getContentResolver(), uri);
return resultCursor;
}
First, the code gets an instance of a SQLiteQueryBuilder object, which builds up a query
with some method calls.Then, the setTables() method configures which table in the
database is used.The UriMatcher class checks to see which specific rows are requested.
UriMatcher is discussed in greater detail later.
Next, the actual query is called.The content provider query has fewer specifications than
the SQLite query, so the parameters are passed through and the rest is ignored.The instance
of the SQLite database is read-only. Because this is only a query for data, it's acceptable.
Finally, the Cursor needs to know if the source data has changed.This is done by a call
to the setNotificationUri() method telling it which URI to watch for data changes.
The call to the application's query() method might be called from multiple threads, as it
calls to update(), so it's possible the data can change after the Cursor is returned. Doing
this keeps the data synchronized.
Exploring the UriMatcher Class
The UriMatcher class is a helper class for pattern matching on the URIs that are passed
to this content provider. It is used frequently in the implementations of the content
provider functions that must be implemented. Here is the UriMatcher used in these sample
implementations:
public static final String AUTHORITY =
"com.androidbook.TrackPointProvider"
private static final int TRACKPOINTS = 1;
private static final int TRACKPOINT_ID = 10;
private static final UriMatcher sURIMatcher =
new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, "points", TRACKPOINTS);
sURIMatcher.addURI(AUTHORITY, "points/#", TRACKPOINT_ID);
}
First, arbitrary numeric values are defined to identify each different pattern. Next, a static
UriMatcher instance is created for use.The code parameter that the constructor wants is
merely the value to return when there is no match.A value for this is provided for use
within the UriMatcher class itself.
Next, the URI values are added to the matcher with their corresponding identifiers.
The URIs are broken up in to the authority portion, defined in AUTHORITY, and the path
portion, which is passed in as a literal string.The path can contain patterns, such as the
"#" symbol to indicate a number.The "*" symbol is used as a wildcard to match anything.
Implementing the insert() Method
The insert() method is used for adding data to the content provider. Here is a sample
implementation of the insert() method:
public Uri insert(Uri uri, ContentValues values) {
int match = sURIMatcher.match(uri);
if (match != TRACKPOINTS) {
throw new IllegalArgumentException(
"Unknown or Invalid URI " + uri);
}
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
long newID = sqlDB.
insert(TrackPointDatabase.TRACKPOINTS_TABLE, null, values);
if (newID > 0) {
Uri newUri = ContentUris.withAppendedId(uri, newID);
getContext()
.getContentResolver().notifyChange(newUri, null);
return newUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
The Uri is first validated to make sure it's one where inserting makes sense.A Uri targeting
a particular row would not, for instance. Next, a writeable database object instance is
retrieved. Using this, the database insert() method is called on the table defined by the
incoming Uri and with the values passed in. At this point, no error checking is performed
on the values. Instead, the underlying database implementation throws exceptions that can
be handled by the user of the content provider.
If the insert was successful, a Uri is created for notifying the system of a change to the
underlying data via a call to the notifyChange() method of the ContentResolver.
Otherwise, an exception is thrown.
Implementing the update() Method
The update() method is used to modify an existing row of data. It has elements similar
to the insert() and query() methods.The update is applied to a particular selection
defined by the incoming Uri.
public int update(Uri uri, ContentValues values,
String selection, String[] selectionArgs) {
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
int match = sURIMatcher.match(uri);
int rowsAffected;
switch (match) {
case TRACKPOINTS:
rowsAffected = sqlDB.update(
TrackPointDatabase.TRACKPOINTS_TABLE,
values, selection, selectionArgs);
break;
case TRACKPOINT_ID:
String id = uri.getLastPathSegment();
if (TextUtils.isEmpty(selection)) {
rowsAffected = sqlDB.update(
TrackPointDatabase.TRACKPOINTS_TABLE,
values, _ID + "=" + id, null);
} else {
rowsAffected = sqlDB.update(
TrackPointDatabase.TRACKPOINTS_TABLE,
values, selection + " and " + _ID + "="
+ id, selectionArgs);
}
break;
default:
throw new IllegalArgumentException(
"Unknown or Invalid URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
In this block of code, a writable SQLiteDatabase instance is retrieved and the Uri type
the user passed in is determined with a call to the match() method of the UriMatcher.
No checking of values or parameters is performed here. However, to block updates to a
specific Uri, such as a Uri affecting multiple rows or a match on TRACKPOINT_ID,
java.lang.UnsupportedOperationException can be thrown to indicate this. In this
example, though, trust is placed in the user of this content provider.
After calling the appropriate update() method, the system is notified of the change to
the URI with a call to the notifyChange() method.This tells any observers of the URI
that data has possibly changed. Finally, the affected number of rows is returned, which is
information conveniently returned from the call to the update() method.
Implementing the delete() Method
Now it's time to clean up the database.The following is a sample implementation of the
delete() method. It doesn't check to see if the user might be deleting more data than
they should.You also notice that this is similar to the update() method.
public int delete(Uri uri, String selection, String[] selectionArgs) {
int match = sURIMatcher.match(uri);
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
int rowsAffected = 0;
switch (match) {
case TRACKPOINTS:
rowsAffected = sqlDB.delete(
TrackPointDatabase.TRACKPOINTS_TABLE,
selection, selectionArgs);
break;
case TRACKPOINT_ID:
String id = uri.getLastPathSegment();
if (TextUtils.isEmpty(selection)) {
rowsAffected =
sqlDB.delete(TrackPointDatabase.TRACKPOINTS_TABLE,
_ID+"="+id, null);
} else {
rowsAffected =
sqlDB.delete(TrackPointDatabase.TRACKPOINTS_TABLE,
selection + " and " +_ID+"="+id, selectionArgs);
}
break;
default:
throw new IllegalArgumentException(
"Unknown or Invalid URI " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected;
}
Again, a writable database instance is retrieved and the Uri type is determined using the
match method of UriMatcher. If the result is a directory Uri, the delete is called with the
selection the user passed in. However, if the result is a specific row, the row index is used
to further limit the delete, with or without the selection.Allowing this without a specific
selection enables deletion of a specified identifier without having to also know exactly
where it came from.
As before, the system is then notified of this change with a call to the notifyChange()
method of ContentResolver. Also as before, the number of affect rows is returned, which
we stored after the call to the delete() method.
Implementing the getType() Method
The last method to implement is the getType() method.The purpose of this method is
to return the MIME type for a particular Uri that is passed in. It does not need to return
MIME types for specific columns of data.
public static final String CONTENT_ITEM_TYPE =
ContentResolver.CURSOR_ITEM_BASE_TYPE +
"/track-points";
public static final String CONTENT_TYPE =
ContentResolver.CURSOR_DIR_BASE_TYPE +
"/track-points";
public String getType(Uri uri) {
int matchType = sURIMatcher.match(uri);
switch (matchType) {
case TRACKPOINTS:
return CONTENT_TYPE;
case TRACKPOINT_ID:
return CONTENT_ITEM_TYPE;
default:
throw new
IllegalArgumentException("Unknown or Invalid URI "
+ uri);
}
}
To start, a couple of MIME types are defined.The Android SDK provides some guideline
values for single items and directories of items, which are used here.The corresponding
string for each is vnd.android.cursor.item and vnd.android.cursor.dir, respectively.
Finally, the match() method is used to determine the type of the provided Uri so that the
appropriate MIME type can be returned.
Updating the Manifest File
Finally, you need to update your application's AndroidManifest.xml file so that it reflects
that a content provider interface is exposed to the rest of the system. Here, the class name
and the authorities, or what might considered the domain of the content:// URI, need
to be set. For instance, content://com.androidbook.TrackPointProvider is the base
URI used in this content provider example, which means the authority is
com.androidbook.TrackPointProvider.The following XML shows an example of this:
<provider
android:authorities="com.androidbook.gpx.TrackPointProvider"
android:multiprocess="true"
android:name="com.androidbook.gpx.TrackPointProvider"
</provider>
The value of multiprocess is set to true because the data does not need to be
synchronized between multiple running versions of this content provider. It's possible that
two or more applications might access a content provider at the same time, so proper
synchronization might be necessary.
0 件のコメント:
コメントを投稿