Acting as a Content Provider Android

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 SQL ite Query Builder .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

Theinsert() 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 Content Resolver. 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 bereturned.

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 .android book. Track Point Provider 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.


All rights reserved © 2018 Wisdom IT Services India Pvt. Ltd DMCA.com Protection Status

Android Topics