Mobile
Article
By Aldo Ziflaj

Using Android’s Content Providers to Manage App Data

By Aldo Ziflaj
Last chance to win! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

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);
    }

}
--ADVERTISEMENT--

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.

More:
Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the most important and interesting stories in tech. Straight to your inbox, daily.Is it good?