2011年5月16日月曜日

AndroidStudyMemo=Acting as a Content Provider

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 件のコメント:

コメントを投稿