Using Android’s Content Providers to Manage App Data

Share this article

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.

Frequently Asked Questions (FAQs) about Using Android’s Content Providers to Manage App Data

What is a Content Provider in Android and why is it important?

A Content Provider in Android is a component that encapsulates data and provides it to applications. It acts as a central repository for data, allowing data sharing across different applications, thereby promoting data consistency. Content Providers are essential in Android as they handle data and allow it to be shared between applications, ensuring data security and privacy.

How do I create a Content Provider in Android?

Creating a Content Provider involves defining it in the AndroidManifest.xml file and implementing a subclass of the ContentProvider class. The subclass must implement six methods: onCreate(), query(), insert(), update(), delete(), and getType(). These methods allow the Content Provider to perform database operations.

What is the role of URIs in Content Providers?

URIs (Uniform Resource Identifiers) play a crucial role in Content Providers. They are used to identify data in a Content Provider. Each Content Provider has a unique URI, which is used to access its data. The URI defines what data to access and how to access it.

How does a Content Resolver work with a Content Provider?

A Content Resolver acts as an intermediary between the Content Provider and the application. It handles direct transactions with the Content Provider on behalf of the application. The Content Resolver uses the Content Provider’s URI to determine which Content Provider to access.

What is the difference between a Content Provider and a Database?

While both a Content Provider and a Database are used for data storage, they serve different purposes. A Database is a structured set of data. A Content Provider, on the other hand, is a component that encapsulates this data and provides methods to perform operations on this data. It can use a database for data storage.

How do I use a Content Provider to share data between applications?

To share data between applications, you need to create a Content Provider in the application that owns the data. Other applications can then access this data using a Content Resolver.

Can I use a Content Provider to store data locally?

Yes, a Content Provider can be used to store data locally. It can use various storage methods, including shared preferences, internal storage, external storage, and SQLite databases.

How do I secure my Content Provider?

You can secure your Content Provider by setting permissions in the AndroidManifest.xml file. You can specify who can access the Content Provider and what operations they can perform.

What is a CursorLoader and how is it used with a Content Provider?

A CursorLoader is a subclass of AsyncTaskLoader that queries a Content Provider and returns a Cursor. It offloads the task of querying a Content Provider to a separate thread, preventing blocking the UI thread.

How do I test my Content Provider?

You can test your Content Provider using Android’s testing frameworks, such as the AndroidJUnitRunner and the ProviderTestCase2 class. These frameworks provide methods to test the functionality of your Content Provider.

Aldo ZiflajAldo Ziflaj
View Author

Aldo is a Code-Lover and a student of Computer Engineering from Albania. His short-term goal is that of becoming a full-stack developer, focusing on Android, Ruby technologies and DevOps techniques.

androidchriswdata
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week