Content Providers
Objectives
After this lesson, students will be able to:
- Describe what a content provider is
- Create a content provider and use it in the application
Preparation
Before this lesson, students should review the following lessons:
STUDENT PRE-WORK
Before this lesson, you should already:
- Read over lesson relating to SQLite and Threading
INSTRUCTOR PREP
Before this lesson, instructors will need to:
- Open and test the provided starter and solution code
Opening (10 mins)
What is a content provider?
Android provides multiple ways for different apps to communicate on the platform; in other words, an Android app can share its data with other apps or parts of the Android system! For example, an email app might need to query a person's contact details details in the phone book app.
In Android, the recommended way to share data is through content providers. A content provider is an owner of particular content, and it provides well-defined APIs to read, insert, update and delete that data. The content provider can internally use any place to store its data, like a local file, local database or some remote service. This flexibility of data storage is extremely useful, because it allows you to change where data is stored without modifying the overall logic of your app.
Before you start building a provider, decide if you need a content provider.
You'll need to build a content provider if you want to provide one or more of the following features:
- offer complex data or files to other applications
- allow users to copy complex data from your app into other apps
- provide custom search suggestions using the search framework
- Sync Adapters (this will be covered in a later lesson)
Today we are going to be working with our content provider within our own app. Tomorrow, we will show how content providers can be accessed externally by other apps.
Check: Ask the students, in pairs, to think of some use cases when their apps may need to use the data from other applications (ex. calendar provider, dictionary provider, contacts provider, MediaStore, Bookmarks); have students share out.
Introduction: URI and Registering Content Provider (10 mins)
Every content provider is associated with a content Uniform Resource Identifier (URI). A content URI is a URI that identifies data in a provider. It consists of:
- The scheme, which is always
content://
for a content URI - The authority, which is a unique string identifying the specific content provider
- The path, which identifies a particular record or collection of records managed by the provider
URI anatomy
Instructor Note: Draw out a table to show how the URI can match up against a specific row
Let's look at the URI, content://user_dictionary/words
:
- content:// — the scheme identifying this as a content URI
- user_dictionary — the authority of the system user dictionary provider
- words — the path corresponding to the “words” table
A Content Provider uses the path to choose the table to access. A provider usually has a path for each table it exposes. Many providers allow you to access a single row in a table by appending an ID value to the end of the URI.
For example, to retrieve a row with an ID of 4 from user dictionary, you can use this content URI:
content://user_dictionary/words/4
Check: Identify the components of a URI and what they are used for.
URI Matcher
To help you choose what action to take for an incoming content URI, the provider API includes the convenience class UriMatcher that maps content URI "patterns" to integer values.
A content URI pattern matches content URIs using wildcard characters:
*
: Matches a string of any valid characters of any length.#
: Matches a string of numeric characters of any length.
Demo: URI Matcher (5 mins)
As an example of designing and coding content URI handling, consider a provider with the authority com.example.app.provider
that recognizes the following content URIs pointing to tables:
content://com.example.app.provider/table1
: A table called table1.content://com.example.app.provider/table2/dataset1
: A table called dataset1.content://com.example.app.provider/table2/dataset2
: A table called dataset2.content://com.example.app.provider/table3
: A table called table3.
The provider also recognizes these content Uris if they have a row ID appended to them, as for example
content://com.example.app.provider/table3/1
for the row identified by 1 in table3.
The snippet below is the example of how a Uri is declared in the Java class and how UriMatcher works:
Instructor note: Ask students to take 30 seconds to pair up and, in their own words, explain what each line is doing. Have students share out.
Guided Practice: URI Matcher (5 mins)
In pairs, take some time to look at the examples and discuss what these Uris match:
"content://com.example.app.provider/*"
// for example: Matches any content URI in the provider.
"content://com.example.app.provider/table3/#"
"content://com.example.app.provider/table3/6"
"content://com.example.app.provider/table3/#"
"content://com.example.app.provider/table1/#/#"
Check: Review the answers to each of these with students in the last minute of this activity.
Demo: Creating a Content Provider (10 mins)
Check: Ask the students what file we have to modify to expose information for our app to other parts of Android.
When we create a content provider, we have to register it in the Manifest file.
<provider
android:name=".MyContentProvider"
android:authorities="com.example.database.provider.MyContentProvider" />
Instructor note: Point out that you will get familiar with authorities later in the lesson.
Now we need to create our actual Content Provider
public class MyContentProvider extends ContentProvider {
private static final String AUTHORITY = "com.example.database.provider.MyContentProvider";
private static final String PRODUCTS_TABLE = "products";
public static final Uri CONTENT_URI = Uri.parse("content://"
+ AUTHORITY + "/" + PRODUCTS_TABLE);
public static final int PRODUCTS = 1;
public static final int PRODUCTS_ID = 2;
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, PRODUCTS_TABLE, PRODUCTS);
sURIMatcher.addURI(AUTHORITY, PRODUCTS_TABLE + "/#", PRODUCTS_ID);
}
}
In the above example we configured our UriMatcher. The UriMatcher takes a given Uri, and returns a value if it matches a provided Uri pattern. For instance, this matcher returns a value of 1 when the URI references the entire products table, and a value of 2 when the URI references the ID of a specific row in the products table.
The method addURI()
maps an authority and path to an integer value. The UriMatcher instance (named sURIMatcher
) is now primed to return the value of PRODUCTS when just the products table is referenced in a URI and PRODUCTS_ID when the URI includes the ID of a specific row in the table.
Check: Ask the students how we could use the values PRODUCTS and PRODUCTS_ID if they were returned to us from a method.
Introduction: Implementing Methods, Content Resolver (10 mins)
A Content Resolver object is used to access data in a content provider. You don't directly access a Content Provider to retrieve its data. The Content Resolver object sends requests to the Content Provider implementation’s object. This makes the provider perform the requested actions and return results.
The abstract class ContentProvider defines six abstract methods that you must implement as part of your own concrete subclass. All of these methods except onCreate()
are called by a client application that is attempting to access your content provider.
- onCreate
- query
- insert
- update
- delete
- getType
Link to Content Provider Documentation
With a partner, take one minute to predict the method functionality of these six methods.
If you couldn't get all of them, that's ok! Let's talk about them together.
query()
This method retrieves data from your provider. You need to use the arguments to select the table to query, the rows and columns to return, and the sort order of the result. The method returns the data as a Cursor object. When called, this method is passed some or all of the following arguments:
- URI – The URI specifying the data source on which the query is to be performed. This can take the form of a general query with multiple results, or a specific query targeting the ID of a single table row.
- Projection – A row within a database table can comprise multiple columns of data. The projection argument is simply a String array containing the name for each of the columns that is to be returned in the result data set.
- Selection – The “where” element of the selection to be performed as part of the query. This argument controls what rows are selected from the specified database. For example, if the query was required to select only products named “Cat Food” then the selection string passed to the
query()
method would read productName = “Cat Food”. - Selection Args – Any additional arguments that need to be passed to the SQL query operation to perform the selection.
- Sort Order – The sort order for the selected rows.
insert()
This method inserts a new row into your provider. You need to use the arguments to select the destination table and to get the column values to use. It returns a content URI for the newly-inserted row.
update()
It updates existing rows in your provider. You need to use the arguments to select the table and rows to update and to get the updated column values. It returns the number of rows updated.
delete()
It deletes rows from your provider. You need to use the arguments to select the table and the rows to delete. The method returns the number of rows deleted.
getType()
It returns the MIME type corresponding to a content URI. For instance, it could return plain text, images, raw data, etc.
onCreate()
It initializes your provider. The Android system calls this method immediately after it creates your provider. Notice that your provider is not created until a ContentResolver object tries to access it.
Demo: Implementing Methods (15 mins)
Using the project from earlier, let's look at what each of these methods looks like in action.
onCreate()
The Android system calls onCreate()
when it starts up the provider. You should perform only fast-running initialization tasks in this method, and defer database creation and data loading until the provider actually receives a request for the data. If you do lengthy tasks in onCreate()
, you will slow down your provider's startup. In turn, this will slow down the response from the provider to other applications.
For example, if you are using a SQLite database, you can create a new SQLiteOpenHelper object in ContentProvider.onCreate()
, and then create the SQL tables the first time you open the database. To facilitate this, the first time you call getWritableDatabase()
, it automatically calls the SQLiteOpenHelper.onCreate()
method.
@Override
public boolean onCreate() {
myDB = new MyDBHandler(getContext(), null, null, 1);
return false;
}
Check: Why is it ok to create the database helper here?
insert()
Passed as arguments to the method are a URI specifying the destination of the insertion and a ContentValues object containing the data to be inserted. This method needs to perform the following tasks:
- Use the sUriMatcher to identify the URI type.
- Throw an exception if the URI is not valid.
- Obtain a reference to a writable instance of the underlying SQLite database.
- Perform a SQL insert operation to insert the data into the database table.
- Notify the corresponding content resolver that the database has been modified (we will see this used when we cover sync adapters).
- Return the URI of the newly added table row.
@Override
public Uri insert(Uri uri, ContentValues values) {
int uriType = sURIMatcher.match(uri);
long id = 0;
switch (uriType) {
case PRODUCTS:
id = myDB.addProduct(values);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(PRODUCTS_TABLE + "/" + id);
}
query()
When called, the query()
method is required to perform the following operations:
- Use the sUriMatcher to identify the Uri type.
- Throw an exception if the URI is not valid.
- Construct a SQL query based on the criteria passed to the method. For convenience, the SQLiteQueryBuilder class can be used in construction of the query.
- Execute the query operation on the database.
- Notify the content resolver of the operation.
- Return a Cursor object containing the results of the query.
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
int uriType = sURIMatcher.match(uri);
Cursor cursor = null;
switch (uriType) {
case PRODUCTS_ID:
cursor = myDB.findProductById(uri.getLastPathSegment());
break;
case PRODUCTS:
cursor = myDB.findProductByName(selection);
break;
default:
throw new IllegalArgumentException("Unknown URI");
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
Check: In your own words, describe what this code is doing.
Guided Practice: Completing the Content Provider (10 mins)
Now let's work through completing the update and delete methods. Take 5 minutes with a partner, and using the previous methods as an example, come up with the steps our methods need to complete.
update()
When called, the update()
method would typically perform the following steps:
- Use the sUriMatcher to identify the URI type.
- Throw an exception if the URI is not valid.
- Obtain a reference to a writable instance of the underlying SQLite database.
- Perform the appropriate update operation on the database depending on the selection criteria and the URI type.
- Notify the content resolver of the database change.
- Return a count of the number of rows that were changed as a result of the update operation.
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int uriType = sURIMatcher.match(uri);
int rowsUpdated = 0;
switch (uriType) {
case PRODUCTS:
rowsUpdated = myDB.updateProduct(values,selection,null);
break;
case PRODUCTS_ID:
String id = uri.getLastPathSegment();
rowsUpdated = myDB.updateProduct(values,null,id);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
Check: In your own words, describe what this code is doing.
delete()
When called, the delete()
method would typically perform the following steps:
- Use the sUriMatcher to identify the URI type.
- Throw an exception if the URI is not valid.
- Obtain a reference to a writable instance of the underlying SQLite database.
- Perform the appropriate delete operation on the database depending on the selection criteria and the Uri type.
- Notify the content resolver of the database change.
- Return the number of rows deleted as a result of the operation.
public int delete(Uri uri, String selection, String[] selectionArgs) {
int uriType = sURIMatcher.match(uri);
SQLiteDatabase sqlDB = myDB.getWritableDatabase();
int rowsDeleted = 0;
switch (uriType) {
case PRODUCTS:
rowsDeleted = myDB.deleteProductByName(selection);
break;
case PRODUCTS_ID:
String id = uri.getLastPathSegment();
rowsDeleted = myDB.deleteProductById(id);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsDeleted;
}
Check: In your own words, describe what this code is doing.
A typical delete()
method is, in many ways, very similar to the update()
method.
Demo: Using your Content Provider (5 mins)
Now that our Content Provider is complete, we need to actually use it. Let's implement the logic for our add button together.
Check: Ask the students what type of objects we use to access Content Providers.
By accessing our Content Resolver, we can call the Content Provider's methods to perform operations on our database.
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put("productname",);
values.put("quantity",);
cr.insert(CONTENT_URI,values);
Independent Practice: Refactor the code (10 mins)
Now that we had a look at all the methods the content provider is supposed to implement, you can run the project.
Note, there are 4 buttons:
- find - retrieves quantity of a product from the content provider when the user types its name in the edit text
- delete - removes the product when the user types its name in the edit text
- update - updates the quantity of the given product name
When performing delete and update, you need to show an error message if the product doesn't exist.
With a partner, complete the logic for the update, delete, and find buttons.
Check: Review the solution with students in the last 3 minutes of the activity.
Conclusion (5 mins)
- When do we need to use a content provider?
- Why do we need a URI?
- What are the main parts of a URI?
- What is a content resolver?
- What methods should be overridden when we create our custom content provider?