Using Android’s Content Providers to Manage App Data
In my last article, I created an Android Todo App, which is a more advanced version of the classic ‘Hello World’ application for mobile development. In that article, I introduced building views for Android, assigning methods (functions) to events such as button clicks, and using the embedded SQLite database to store, retrieve and delete data (tasks).
In that tutorial, I had to write SQL queries to achieve this, but there is a simpler way, Content Providers.
Providing Content for Android
Content providers are a simpler way to manage the data stored in the embedded SQLite database. They are a standard interface that connects data in one process with code running in another process. They might seem hard to understand or implement, they are not. In this tutorial, I will show you how to create your own content provider.
You don’t need to develop your own provider if you don’t intend to share your data with other applications. However, you do need your own provider to provide custom search suggestions in your own application. You also need your own provider if you want to copy and paste complex data or files from your application to other applications.
To start following the tutorial, download the current repo of the project from GitHub and import it into your IDE.
Before writing the content provider class, we have to add some code to the TaskContract
class. Add the code below the existing variable declarations in the class:
public static final String AUTHORITY = "com.example.TodoList.mytasks";
public static final Uri CONTENT_URI = Uri.parse("content://"+AUTHORITY+"/"+ TABLE);
public static final int TASKS_LIST = 1;
public static final int TASKS_ITEM = 2;
public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/example.tasksDB/"+TABLE;
public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/example/tasksDB" + TABLE;
The code is nothing but a list of static
constants (final
variables) containing necessary information. Firstly, I chose an authority for the content provider. Normally, this would be .provider
, but you can choose anything as long as you are sure it won’t conflict any other app.
After the authority, a content URI is set to content://com.example.TodoList.mytasks/tasks
(with tasks
being the TABLE
). This content URI should always start with content://
and is used to access the data in the table. If you will use more than one table, you can use the same structure as above, though you have to change the table name.
CONTENT_TYPE
and CONTENT_TYPE_ITEM
are used to identify if a URI requested points to a directory (i.e. table) or to an item (i.e. record in the table).
We also need to add the content provider to the AndroidManifest.xml file, add the following code:
<provider
android:authorities="com.example.TodoList.mytasks"
android:name=".db.TaskProvider" />
Just before the closing application
tag.
Now that the contract is ready, we can create the content provider.
Creating a provider
A content provider is a simple Java class that extends the ContentProvider
class and implements its methods.
Our content provider will be called TaskProvider
, and will be placed in the db
package. To do this, create a new class file inside the db
folder called TaskProvider.java
and add the following code:
package com.example.TodoList.db;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
public class TaskProvider extends ContentProvider{
private SQLiteDatabase db;
private TaskDBHelper taskDBHelper;
public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
uriMatcher.addURI(TaskContract.AUTHORITY,TaskContract.TABLE,TaskContract.TASKS_LIST);
uriMatcher.addURI(TaskContract.AUTHORITY,TaskContract.TABLE+"/#",TaskContract.TASKS_ITEM);
}
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] strings, String s, String[] strings2, String s2) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
return null;
}
@Override
public int delete(Uri uri, String s, String[] strings) {
return 0;
}
@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
return 0;
}
}
At this moment, this class doesn’t do much. It is a content provider, but it doesn’t provide any content, since the methods are not yet implemented. All it does is create a UriMatcher
instance which will then be used to check if the URI accessed is valid or not. In a static block two URIs are added to the UriMatcher
, the URI corresponding to the table and the URI corresponding to the record.
Let’s implement the onCreate()
method. The Android system calls this method immediately after it creates the provider. This means that it should be kept as simple as possible, since we don’t want the system to make too many calculations when the provider is created. Put this code into the onCreate()
method in the class we just created:
@Override
public boolean onCreate() {
boolean ret = true;
taskDBHelper = new TaskDBHelper(getContext());
db = taskDBHelper.getWritableDatabase();
if (db == null) {
ret = false;
}
if (db.isReadOnly()) {
db.close();
db = null;
ret = false;
}
return ret;
}
In this method, the TaskDBHelper
class is used to create a helper object which creates the database if it does not already exist. This method would return false if the provider won’t be loaded because the database is not accessible, otherwise would return true. This simple code is more than enough to create our Content Provider.
Now let’s add the query()
functionality, which is used to retrieve the data stored in the database and return a Cursor
instance:
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TaskContract.TABLE);
switch (uriMatcher.match(uri)) {
case TaskContract.TASKS_LIST:
break;
case TaskContract.TASKS_ITEM:
qb.appendWhere(TaskContract.Columns._ID + " = "+ uri.getLastPathSegment());
break;
default:
throw new IllegalArgumentException("Invalid URI: " + uri);
}
Cursor cursor = qb.query(db,projection,selection,selectionArgs,null,null,null);
return cursor;
}
The query()
method should return a Cursor
instance, or throw an exception if there is any problem with querying.
In the method above, a SQLiteQueryBuilder
instance is used to help creating the query. If the object requested (by the URI) is a record (an item, not a list/table), the SQLiteQueryBuilder
uses the appendWhere()
method to add the WHERE
clause to the query. In the end, a Cursor
instance is created by executing the query created by query builder, which is then returned by the method. If the URI requested is invalid, an IllegalArgumentException
is thrown.
After adding the query()
method, we are going to implement the insert()
method, which adds some values stored in a ContentValues
instance as a new record in a table:
@Override
public Uri insert(Uri uri, ContentValues contentValues) {
if (uriMatcher.match(uri) != TaskContract.TASKS_LIST) {
throw new IllegalArgumentException("Invalid URI: "+uri);
}
long id = db.insert(TaskContract.TABLE,null,contentValues);
if (id>0) {
return ContentUris.withAppendedId(uri,id);
}
throw new SQLException("Error inserting into table: "+TaskContract.TABLE);
}
This method receives two arguments, the URI where the records are going to be inserted and the values that are going to make up the record. If the URI doesn’t matches the URI of the table, then an IllegalArgumentException
is thrown. If the URI is correct, then the insert()
method of a SQLiteDatabase
instance is used to insert the data into the proper table. This insert()
method returns the URI of the new record. This is done if the id
is greater than 0 (which means that the record is added at the table), and the new URI is created by using the withAppendedId()
method. If the id
is not greater than 0, it means the record is not stored, so an SQLException
is thrown.
Now let’s move on to another method, the update()
method. This method is used to change (i.e. update) an existing record of a given table in the SQLite database:
@Override
public int update(Uri uri, ContentValues contentValues, String s, String[] strings) {
int updated = 0;
switch (uriMatcher.match(uri)) {
case TaskContract.TASKS_LIST:
db.update(TaskContract.TABLE,contentValues,s,strings);
break;
case TaskContract.TASKS_ITEM:
String where = TaskContract.Columns._ID + " = " + uri.getLastPathSegment();
if (!s.isEmpty()) {
where += " AND "+s;
}
updated = db.update(TaskContract.TABLE,contentValues,where,strings);
break;
default:
throw new IllegalArgumentException("Invalid URI: "+uri);
}
return updated;
}
This method takes four parameters:
Uri uri
: The URI to query. It can be the URI of a single record, or the URI of a table.ContentValues contentValues
: A set of key-value pairs, with the column name as a key and the values to update as values.String s
: A selection to match the rows which are going to be updated.String[] strings
: The arguments of the above rows.
Using this method, we initially check if the URI corresponds to a table or to a record. If it corresponds to a table, we call the update()
method to an SQLiteDatabase
instance, passing the same parameters as our method (except for the URI, which is changed to the table name).
If the URI corresponds to a record, we have to change the third parameter to match the id required to the ID of the record in the database, and add any selection that the user required.
Finally, the update()
record is called to the SQLiteDatabase
instance, passing the table name, content values, our where clause, and the last argument.
This method would return the number of rows updated, or throw an IllegalArgumentException
if the URI passed is not correct.
The next method is delete()
. You will notice that this method is not that different from the update()
method implemented above:
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int deleted = 0;
switch (uriMatcher.match(uri)) {
case TaskContract.TASKS_LIST:
db.delete(TaskContract.TABLE,selection,selectionArgs);
break;
case TaskContract.TASKS_ITEM:
String where = TaskContract.Columns._ID + " = " + uri.getLastPathSegment();
if (!selection.isEmpty()) {
where += " AND "+selection;
}
deleted = db.delete(TaskContract.TABLE,where,selectionArgs);
break;
default:
throw new IllegalArgumentException("Invalid URI: "+uri);
}
return deleted;
}
Just like the update()
method, the delete()
method checks if the URI passed corresponds to a table and if so, it deletes the table. If it corresponds to a record, it deletes that record from the table. In the latter case, we make sure to add a clause for the id to the selection
parameter.
As above, the delete()
method would return the number of deleted rows, or throw an IllegalArgumentException
if the URI is invalid.
To finalize our TaskProvider
class, we have to implement the last method, the getType()
method. This method will tell if an URI passed corresponds to a table, to a record, or is invalid. This method is simply:
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case TaskContract.TASKS_LIST:
return TaskContract.CONTENT_TYPE;
case TaskContract.TASKS_ITEM:
return TaskContract.CONTENT_ITEM_TYPE;
default:
throw new IllegalArgumentException("Invalid URI: "+uri);
}
}
Use your own provider
In order to use our new Content Provider, we need to open MainActivity.java
and change some of its code.
Go to the updateUI()
method and we will change the method of querying the data from the database. Remove this piece of code:
helper = new TaskDBHelper(MainActivity.this);
SQLiteDatabase sqlDB = helper.getReadableDatabase();
Cursor cursor = sqlDB.query(TaskContract.TABLE,
new String[]{TaskContract.Columns._ID, TaskContract.Columns.TASK},
null, null, null, null, null);
And replace it with:
Uri uri = TaskContract.CONTENT_URI;
Cursor cursor = this.getContentResolver().query(uri,null,null,null,null);
For deleting tasks, remove the following code from onDoneButtonClick()
method:
String sql = String.format("DELETE FROM %s WHERE %s = '%s'",
TaskContract.TABLE,
TaskContract.Columns.TASK,
task);
helper = new TaskDBHelper(MainActivity.this);
SQLiteDatabase sqlDB = helper.getWritableDatabase();
sqlDB.execSQL(sql);
And replace it with this:
Uri uri = TaskContract.CONTENT_URI;
this.getContentResolver().delete(uri,
TaskContract.Columns.TASK + "=?",
new String[]{task});
Now you can delete tasks using the content provider created, without needing any SQL queries.
Finally, we need to insert new tasks using the insert()
method we created. Find the onOptionsItemSelected()
method and replace following code:
helper = new TaskDBHelper(MainActivity.this);
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.clear();
values.put(TaskContract.Columns.TASK,task);
db.insertWithOnConflict(TaskContract.TABLE,null,values,SQLiteDatabase.CONFLICT_IGNORE);
With:
helper = new TaskDBHelper(MainActivity.this);
SQLiteDatabase db = helper.getWritableDatabase();
ContentValues values = new ContentValues();
values.clear();
values.put(TaskContract.Columns.TASK,task);
Uri uri = TaskContract.CONTENT_URI;
getApplicationContext().getContentResolver().insert(uri,values);
Now the values will be added to the database using the insert()
method we created earlier.
Conclusion
As you can see, Content Providers are not hard, especially after trying them in a few projects.
I hope this guide gave you a good insight on Android Content Providers, their implementation and their usage. You can find the source code of this tutorial on Github.