Applied Example: MyWalks
In order to illustrate the usage of content providers, we will use an application that simulates a walk. We define a walk by one database table that contains descriptive rows. The walk table will then have a one-to-many relationship with location points: latitude and longitude.
The application can be downloaded here as an extractable zip file that contains the .apk file. It also contains the source, which is importable into eclipse. Follow these instructions to install and configure the application (assuming that Android is set up correctly:
- Navigate to the Android SDK's tools folder.
- Open a shell and start the emulator by typing adb emulator
- Open another shell in the same folder and type adb install MyWalks.apk
- Start the My Walk application by navigating to it via the Android UI; this will also set up the databases when first started.
Alternatively you can import and run the project from eclipse.
The database consists of a table called walk that has name/description/created/modified as columns. Another table will be used for the location points. A point consists of a latitude, a longitude, and the foreign key walk_id. You can view this data easily using the handy application 'SQLite database browser'.
Accessing Data Using URIs
In order to access the data, we need URIs. (URIs follow RFC 2396 syntax.) The idea behind the data access is similar to REST services, with the difference of using intents instead of http methods. For instance, you would use android.intent.action.VIEW instead of http method GET, android.intent.action.INSERT instead of POST or PUT, and so forth.
For our MyWalks application, we will use the following URIs:
content://com.informit.mywalks.Walks - provide a list for all saved walks content://com.informit.mywalks.Walks/1 - provide the data for a single walk content://com.informit.mywalks.Walks/1/points - provide the list of all points for a walk content://com.informit.mywalks.Walks/1/points/1 - provide the information for a single point of a walk (this will not be used in this application).
We begin by creating a class defining the database columns as follows (this is not mandatory but recommended for clarity):
public final class Walks { public static final class MyWalks implements BaseColumns { public static final URI CONTENT_URI = URI .parse("content://com.informit.mywalks.Walks/walk"); public static final String NAME = "name"; public static final String DESCRIPTION = "description"; public static final String CREATED_DATE = "created"; public static final String MODIFIED_DATE = "modified"; } public static final class MyPoints implements BaseColumns { public static final String _WALK_ID = "_walk_id"; public static final String LATITUDE = "latitude"; public static final String LONGITUDE = "longitude"; public static final String CREATED_DATE = "created"; } }
This class is used by the system as authority for the provider and defined in the AndroidManifest file (this value must be unique, thus defining the fully-qualified class name as authority is recommended):
<provider android:name=".WalksProvider" android:authorities="com.informit.mywalks.Walks"/>
Second, we create the implemented ContentProvider. The class is called WalkProvider and it extends the ContentProvider interface. The following methods have to be overridden:
query(Uri, String[], String, String[], String) which returns data to the caller insert(Uri, ContentValues) which inserts new data into the content provider update(Uri, ContentValues, String, String[]) which updates existing data in the content provider delete(Uri, String, String[]) which deletes data from the content provider getType(Uri) which returns the MIME type of data in the content provider
Know Where to Look Using a UriMatcher
In our application, we created 2 tables and we have 4 different URIs which resolve to the same content provider. In order for the provider to know which database to query, we will use a helper class called UriMatcher. The UriMatcher is straightforward. You declare different URIs and map them to a final static integer in order to stay consistent across the class. For instance, we have the following values:
private static final int WALK = 1; private static final int WALK_ID = 2; private static final int WALK_POINTS = 3; private static final int WALK_POINTS_ID = 4;
We then map the variables using the UriMatcher, as follows:
URI_MATCHER.addURI("com.informit.mywalks.Walks", "walk", WALK); URI_MATCHER.addURI("com.informit.mywalks.Walks", "walk/#", WALK_ID); URI_MATCHER.addURI("com.informit.mywalks.Walks", "walk/#/points", WALK_POINTS); URI_MATCHER.addURI("com.informit.mywalks.Walks", "walk/#/points/#", WALK_POINTS_ID);
URI_MATCHER is our instantiated UriMatcher. We used the wildcard # to represent the id; you could also use the wildcard * to match against the name of a place.
All overridden methods will have a URI passed as argument. Thus you can use the UriMatcher to ensure that the correct logic is used. The following code snippet, from the query method, illustrates its usage:
SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); switch (URI_MATCHER.match(uri)) { case WALK: qb.setTables("walks"); qb.setProjectionMap(WALK_LIST_PROJECTION_MAP); break; case WALK_ID: qb.setTables("walks"); qb.appendWhere("_id=" + uri.getPathSegments().get(1)); break; case WALK_POINTS: qb.setTables("points"); qb.appendWhere("_walk_id=" + uri.getPathSegments().get(1)); qb.setProjectionMap(POINT_LIST_PROJECTION_MAP); break; case WALK_POINTS_ID: qb.setTables("points"); qb.appendWhere("_walk_id=" + uri.getPathSegments().get(1)); qb.appendWhere("_id=" + uri.getPathSegments().get(3)); break; default: throw new IllegalArgumentException("Unknown URL " + uri); }
If you query the content provider with content://com.informit/walks/1/points, the third case will be used—thus set the table and its corresponding where clause for the walk_id. uri.getPathSegments().get(1) will give us "1" in the previous example. The URI and UriBuilder class contains several methods to help you create specific URIs.
The getType method is interesting mostly from a system-wide perspective. You would likely use the URI in your code whereas you would use types in the manifest. For instance, when we want to create a class that creates a new walk, we would create an activity and define it in the AndroidManifest, as follows:
<activity android:name=".MyWalksEdit" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.INSERT" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="vnd.android.cursor.dir/vnd.novacoda.walk" /> </intent-filter> </activity>
We could have declared an URI instead of the type here. However, it is logical to use a type because you can declare a type for a single walk (with an URI, it would be specific to a single id).
Generating Some Data
Now that we created the WalkProvider, we can add some data via our activities. Let looks at the MyWalksEdit.java file. As soon as the class is created, we create a new entry in the database:
mURI = getContentResolver().insert(intent.getData(), null);
As you can see, we use the ContentResolver given by the Activity interface. There is only one system-wide ContentResolver, which is responsible for mapping the correct content provider to the correct URI. The intent.getData() returns the URI that is being worked on. In this case, we are working on content://com.informit/walks.
We could have sent initial values using a ContentValues class instead of null. However, we will set the values later in our example. The insert function will return a new URI—in this case, something along the lines of content://com.informit/walks/3, depending where the new value is created in the database.
In order to hook into the newly created row, we will use a cursor that is very similar to a database cursor, as discussed previously. The cursor is returned while querying against the newly created row using the returned URI. We can then use Cursor's update methods to update columns of the URI and the commit function in order to save the new values into the database:
mCursor = managedQuery(mURI, null, null, null); ... mCursor.updateString(1, name.getText().toString()); mCursor.updateString(2, description.getText().toString()); mCursor.commitUpdates();