Working with ContentProvider classes

Một phần của tài liệu Manning android in action 3rd (Trang 180 - 188)

A content provider in Android shares data between applications. Each application usu- ally runs in its own process. By default, applications can’t access the data and files of other applications. We explained earlier that you can make preferences and files avail- able across application boundaries with the correct permissions and if each application knows the context and path. This solution applies only to related applica- tions that already know details about one another. In contrast, with a content provider you can publish and expose a particular data type for other applications to query, add, update, and delete, and those applications don’t need to have any prior knowledge of paths, resources, or who provides the content.

The canonical content provider in Android is the contacts list, which provides names, addresses, and phone numbers. You can access this data from any application by using the correct URI and a series of methods provided by the Activity and ContentResolver classes to retrieve and store data. You’ll learn more about Content- Resolver as we explore provider details. One other data-related concept that a content provider offers is the Cursor, the same object we used previously to process SQLite database result sets.

In this section, you’ll build another application that implements its own content provider and includes a similar explorer-type Activity to manipulate that data.

NOTE For a review of content providers, please see chapter 1. You can also find a complete example of working with the Contacts content provider in chapter 15.

To begin, we’ll explore the syntax of URIs and the combinations and paths used to perform different types of operations with the ContentProvider and Content- Resolver classes.

5.4.1 Using an existing ContentProvider

Each ContentProvider class exposes a unique CONTENT_URI that identifies the con- tent type it’ll handle. This URI can query data in two forms, singular or plural, as shown in table 5.1.

Table 5.1 ContentProvider URI variations for different purposes

URI Purpose

content://food/ingredients/ Returns a List of all ingredients from the provider registered to handle content://food

content://food/meals/ Returns a List of all meals from the provider registered to handle content://food

content://food/meals/1 Returns or manipulates a single meal with ID 1 from the provider regis- tered to handle content://food

A provider can offer as many types of data as it likes. By using these formats, your application can either iterate through all the content offered by a provider or retrieve a specific datum of interest.

The Activity class has a managedQuery() method that makes calls into registered ContentProvider classes. When you create your own content provider in section 5.4.2, we’ll show you how a provider is registered with the platform. Each provider is required to advertise the CONTENT_URI it supports. To query the contacts provider, you have to know this URI and then get a Cursor by calling managedQuery(). When you have the Cursor, you can use it, as we showed you in listing 5.11.

A ContentProvider typically supplies all the details of the URI and the types it sup- ports as constants in a class. In the android.provider package, you can find classes that correspond to built-in Android content providers, such as the MediaStore. These classes have nested inner classes that represent types of data, such as Audio and Images. Within those classes are additional inner classes, with constants that represent fields or columns of data for each type. The values you need to query and manipulate data come from the inner classes for each type.

For additional information, see the android.provider package in the Javadocs, which lists all the built-in providers. Now that we’ve covered a bit about using a pro- vider, we’ll look at the other side of the coin—creating a content provider.

5.4.2 Creating a ContentProvider

In this section, you’ll build a provider that handles data responsibilities for a generic Widget object you’ll define. This simple object includes a name, type, and category; in a real application, you could represent any type of data.

Managed Cursor

To obtain a Cursor reference, you can also use the managedQuery method of the Activity class. The Activity automatically cleans up any managed Cursor objects when your Activity pauses and restarts them when it starts. If you just need to retrieve data within an Activity, you’ll want to use a managed Cursor, as opposed to a ContentResolver.

What if the content changes after the fact?

When you use a ContentProvider to make a query, you get only the current state of the data. The data could change after your call, so how do you stay up to date? To receive notifications when a Cursor changes, you can use the ContentObserver API. ContentObserver supports a set of callbacks that trigger when data changes.

The Cursor class provides register() and unregister() methods for Content- Observer objects.

To start, define a provider constants class that declares the CONTENT_URI and MIME_TYPE your provider will support. In addition, you can place the column names your provider will handle here.

DEFINING A CONTENT_URI AND MIME_TYPE

In the following listing, as a prerequisite to extending the ContentProvider class for a custom provider, we define necessary constants for our Widget type.

public final class Widget implements BaseColumns { public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";

public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";

public static final String MIME_ITEM = "vnd.msi.widget";

public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;

public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;

public static final String AUTHORITY = "com.msi.manning.chapter5.Widget";

public static final String PATH_SINGLE = "widgets/#";

public static final String PATH_MULTIPLE = "widgets";

public static final Uri CONTENT_URI =

Uri.parse("content://" + AUTHORITY + "/" + PATH_MULTIPLE);

public static final String DEFAULT_SORT_ORDER = "updated DESC";

public static final String NAME = "name";

public static final String TYPE = "type";

public static final String CATEGORY = "category";

public static final String CREATED = "created";

public static final String UPDATED = "updated";

}

In our Widget-related provider constants class, we first extend the BaseColumns class.

Now our class has a few base constants, such as _ID. Next, we define the MIME_TYPE prefix for a set of multiple items and a single item. By convention, vnd.android.

cursor.dir represents multiple items, and vnd.android.cursor.item represents a single item. We can then define a specific MIME item and combine it with the single and multiple paths to create two MIME_TYPE representations.

After we have the MIME details out of the way, we define the authority B and path for both single and multiple items that will be used in the CONTENT_URI that callers pass in to use our provider. Callers will ultimately start from the multiple-item URI, so we publish this one C.

After taking care of all the other details, we define column names that represent the variables in our Widget object, which correspond to fields in the database table we’ll use. Callers will use these constants to get and set specific fields. Now we’re on to the next part of the process, extending ContentProvider.

Listing 5.12 WidgetProvider constants, including columns and URI

Define authority

B

Define ultimate CONTENT_URI C

EXTENDING CONTENTPROVIDER

The following listing shows the beginning of our ContentProvider implementation class, WidgetProvider. In this part of the class, we do some housekeeping relating to the database we’ll use and the URI we’re supporting.

public class WidgetProvider extends ContentProvider { private static final String CLASSNAME =

WidgetProvider.class.getSimpleName();

private static final int WIDGETS = 1;

private static final int WIDGET = 2;

public static final String DB_NAME = "widgets_db";

public static final String DB_TABLE = "widget";

public static final int DB_VERSION = 1;

private static UriMatcher URI_MATCHER = null;

private static HashMap<String, String> PROJECTION_MAP;

private SQLiteDatabase db;

static {

WidgetProvider.URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);

WidgetProvider.URI_MATCHER.addURI(Widget.AUTHORITY, Widget.PATH_MULTIPLE, WidgetProvider.WIDGETS);

WidgetProvider.URI_MATCHER.addURI(Widget.AUTHORITY, Widget.PATH_SINGLE, WidgetProvider.WIDGET);

WidgetProvider.PROJECTION_MAP = new HashMap<String, String>();

WidgetProvider.PROJECTION_MAP.put(BaseColumns._ID, "_id");

WidgetProvider.PROJECTION_MAP.put(Widget.NAME, "name");

WidgetProvider.PROJECTION_MAP.put(Widget.TYPE, "type");

WidgetProvider.PROJECTION_MAP.put(Widget.CATEGORY, "category");

WidgetProvider.PROJECTION_MAP.put(Widget.CREATED, "created");

WidgetProvider.PROJECTION_MAP.put(Widget.UPDATED, "updated");

}

private static class DBOpenHelper extends SQLiteOpenHelper { private static final String DB_CREATE = "CREATE TABLE "

+ WidgetProvider.DB_TABLE

+ " (_id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL,"

+ "type TEXT, category TEXT, updated INTEGER, created"

+ "INTEGER);";

public DBOpenHelper(Context context) {

super(context, WidgetProvider.DB_NAME, null, WidgetProvider.DB_VERSION);

}

@Override

public void onCreate(SQLiteDatabase db) { try {

db.execSQL(DBOpenHelper.DB_CREATE);

} catch (SQLException e) { // log and or handle }

}

@Override

public void onOpen(SQLiteDatabase db) { }

Listing 5.13 The first portion of the WidgetProviderContentProvider

Define database constants

B

Use

SQLiteDatabase reference

C

Create and D

open database

@Override

public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

db.execSQL("DROP TABLE IF EXISTS "

+ WidgetProvider.DB_TABLE);

onCreate(db);

} }

@Override

public boolean onCreate() {

DBOpenHelper dbHelper = new DBOpenHelper(getContext());

db = dbHelper.getWritableDatabase();

if (db == null) { return false;

} else {

return true;

} }

@Override

public String getType(Uri uri) {

switch (WidgetProvider.URI_MATCHER.match(uri)) { case WIDGETS:

return Widget.MIME_TYPE_MULTIPLE;

case WIDGET:

return Widget.MIME_TYPE_SINGLE;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

} }

Our provider extends ContentProvider, which defines the methods we’ll need to implement. We use several database-related constants to define the database name and table we’ll use B. After that, we include a UriMatcher, which we’ll use to match types, and a projection Map for field names.

We include a reference to a SQLiteDatabase object; we’ll use this to store and retrieve the data that our provider handles C. We create, open, or upgrade the data- base using a SQLiteOpenHelper in an inner class D. We’ve used this helper pattern before, when we worked directly with the database in listing 5.10. In the onCreate() method, the open helper sets up the database E.

After our setup-related steps, we come to the first method ContentProvider requires us to implement, getType() F. The provider uses this method to resolve each passed-in URI to determine whether it’s supported. If it is, the method checks which type of data the current call is requesting. The data might be a single item or the entire set.

Next, we need to cover the remaining required methods to satisfy the Content- Provider contract. These methods, shown in the following listing, correspond to the CRUD-related activities: query, insert, update, and delete.

Override onCreate

E

Implement getType method

F

@Override

public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,

String sortOrder) {

SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();

String orderBy = null;

switch (WidgetProvider.URI_MATCHER.match(uri)) { case WIDGETS:

queryBuilder.setTables(WidgetProvider.DB_TABLE);

queryBuilder.setProjectionMap(WidgetProvider.PROJECTION_MAP);

break;

case WIDGET:

queryBuilder.setTables(WidgetProvider.DB_TABLE);

queryBuilder.appendWhere("_id="

+ uri.getPathSegments().get(1));

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

if (TextUtils.isEmpty(sortOrder)) { orderBy = Widget.DEFAULT_SORT_ORDER;

} else {

orderBy = sortOrder;

}

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

c.setNotificationUri(

getContext().getContentResolver(), uri);

return c;

}

@Override

public Uri insert(Uri uri, ContentValues initialValues) { long rowId = 0L;

ContentValues values = null;

if (initialValues != null) {

values = new ContentValues(initialValues);

} else {

values = new ContentValues();

}

if (WidgetProvider.URI_MATCHER.match(uri) !=

WidgetProvider.WIDGETS) {

throw new IllegalArgumentException("Unknown URI " + uri);

}

Long now = System.currentTimeMillis();

. . . omit defaulting of values for brevity

rowId = db.insert(WidgetProvider.DB_TABLE, "widget_hack", values);

if (rowId > 0) {

Uri result = ContentUris.withAppendedId(Widget.CONTENT_URI, rowId);

getContext().getContentResolver().

notifyChange(result, null);

Listing 5.14 The second portion of the WidgetProviderContentProvider

B

Use query builder

Set up query based on URI

C

Perform query to get Cursor

D

Set notification URI on Cursor

E

Use ContentValues in insert method

F

Call database insert

G

Get URI to return

H

Notify listeners data was inserted

I

return result;

}

throw new SQLException("Failed to insert row into " + uri);

}

@Override

public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

int count = 0;

switch (WidgetProvider.URI_MATCHER.match(uri)) { case WIDGETS:

count = db.update(WidgetProvider.DB_TABLE, values, selection, selectionArgs);

break;

case WIDGET:

String segment = uri.getPathSegments().get(1);

String where = "";

if (!TextUtils.isEmpty(selection)) { where = " AND (" + selection + ")";

}

count = db.update(WidgetProvider.DB_TABLE, values, "_id=" + segment + where, selectionArgs);

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

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

return count;

}

@Override

public int delete(

Uri uri, String selection, String[] selectionArgs) { int count;

switch (WidgetProvider.URI_MATCHER.match(uri)) { case WIDGETS:

count = db.delete(WidgetProvider.DB_TABLE, selection, selectionArgs);

break;

case WIDGET:

String segment = uri.getPathSegments().get(1);

String where = "";

if (!TextUtils.isEmpty(selection)) { where = " AND (" + selection + ")";

}

count = db.delete(WidgetProvider.DB_TABLE, "_id=" + segment + where, selectionArgs);

break;

default:

throw new IllegalArgumentException("Unknown URI " + uri);

}

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

return count;

} }

The last part of our WidgetProvider class shows how to implement the Content- Provider methods. First, we use a SQLQueryBuilder inside the query() method to

Provide update method

J

Provide delete method

1)

append the projection map passed in B and any SQL clauses, along with the correct URI based on our matcher C, before we make the actual query and get a handle on a Cursor to return D.

At the end of the query() method, we use the setNotificationUri() method to watch the returned URI for changes E. This event-based mechanism keeps track of when Cursor data items change, regardless of who changes them.

Next, you see the insert() method, where we validate the passed-in Content- Values object and populate it with default values, if the values aren’t present F. After we have the values, we call the database insert() method G and get the resulting URI to return with the appended ID of the new record H. After the insert is complete, we use another notification system, this time for ContentResolver. Because we’ve made a data change, we inform the ContentResolver what happened so that any reg- istered listeners can be updated I.

After completing the insert() method, we come to the update() J and delete() 1) methods. These methods repeat many of the previous concepts. First, they match the URI passed in to a single element or the set, and then they call the respective update() and delete() methods on the database object. Again, at the end of these methods, we notify listeners that the data has changed.

Implementing the needed provider methods completes our class. After we register this provider with the platform, any application can use it to query, insert, update, or delete data. Registration occurs in the application manifest, which we’ll look at next.

PROVIDER MANIFESTS

Content providers must be defined in an application manifest file and installed on the platform so the platform can learn that they’re available and what data types they offer. The following listing shows the manifest for our provider.

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.msi.manning.chapter5.widget">

<application android:icon="@drawable/icon"

android:label="@string/app_short_name">

<activity android:name=".WidgetExplorer"

android:label="@string/app_name">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name=

"android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

<provider android:name="WidgetProvider"

android:authorities=

"com.msi.manning.chapter5.Widget" />

</application>

</manifest>

Listing 5.15 WidgetProvider AndroidManifest.xml file

Declare provider’s authority

B

The <provider> element B defines the class that implements the provider and associ- ates a particular authority with that class.

A completed project that supports inserting, retrieving, updating, and deleting records rounds out our exploration of using and building ContentProvider classes.

And with that, we’ve also now demonstrated the ways to locally store and retrieve data on the Android platform.

Một phần của tài liệu Manning android in action 3rd (Trang 180 - 188)

Tải bản đầy đủ (PDF)

(662 trang)