Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / SharePoint

Android Content Provider for Beginners

5.00/5 (1 vote)
19 Sep 2015CPOL12 min read 10.9K  
In this post I will provide a tutorial for Android Content Provider targeted at new Android developers. This post is a continuation of my post on SQLite. In that post I ended with a SQLite database created with one table.

In this post I will provide a tutorial for Android Content Provider targeted at new Android developers. This post is a continuation of my post on SQLite. In that post I ended with a SQLite database created with one table. In this tutorial I will show how to implement CRUD operations (create, read, update & delete) using Content Provider.

Just as a reminder, here is we left off with the blog post on SQLite database

public class DatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "simple_note_app.db";
    private static final int DATABASE_VERSION = 1;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_NOTE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("some sql statement to do something");
    }

    private static final String CREATE_TABLE_NOTE = "create table note"
            + "("
            + "_id" + " integer primary key autoincrement, "
            + "title" + " text not null, "
            + "content" + " text not null, "
            + "modified_time" + " integer not null, "
            + "created_time" + " integer not null " + ")";
}

And What Exactly Is Content Provider

Content Provider contrary to what the name suggests, does not have any content of its own to give you; and also there is no way for you to ask Content Provider directly for content. To get content from Content Provider you have to contact the ContentResolver, and for Content Provider to provider anything back to you, it has to get that from a data source such as SQLite Database.

Then who exactly is this  Content Provider and what does it do? Content Provider is a necessary middle man! And why do I need a middle man you may ask? – the answer is to save you from yourself and open up doors of potential opportunity for sharing your data with your other apps .  This last part (about sharing data with other apps) is the selling point of Content Provider so lets us examine that with an analogy.

Suppose you are building a point of sale Android app, it is ideal that an app like that will have a database table for Products, Customers & Transactions. Lets say that your client decides that he want other apps in the device to have access to the Products in this app – how are you going to implement this? You obviously do not want to expose the Customer and Transaction tables, just the Products table – Content Provider to the rescue.

Using Content Provider you can provide data read/write access to the Products table only. That means that any app in the device with the correct address (URI) and specifies the correct columns to get back (projection) can perform CRUD operations against the Product table of your app using the Content Provider. The Content Provider will then translate those external calls into appropriate SQL statements and execute those against your database and return the result back to the calling app if any result is returned.

These external apps cannot execute direct SQL. You maybe wondering is facilitating access for sharing data with external apps – is that all that Content Provider does. Well yes and more. You can use Content Provider for your app internally and doing so opens up the door for other opportunities such as:

  1. Loader Framework – this makes it easy to perform asynchronous data read from your database. It works best but not exclusively with Content Provider.
  2. Abstraction – Content Provider provides an extra level of abstraction over your data source, this becomes important if that underlying data source changes, then you have to only make those changes within the content provider and not hunting down all the areas of your app that access data.
  3. AsyncQueryHandler – can be used to perform asynchronous query against your SQLite database using the Content Provider via the Content Resolver. It is an abstraction over an abstracted abstraction.  The AsyncQueryHandler has CRUD methods that corresponds to the CRUD methods of the ContentResolver which maps to the CRUD methods of the Content Provider which calls the corresponding methods in the database. If you are a new developer, you may this class to use, you can accomplish the same asynchronous operation with AsynchTask and IntentService.

Content Provider, therefore is a non convenient wrapper around an underlying data source mostly SQLite database but could be a RESTful service. The primary use case for Content Provider is for data that need to be accessed by the other apps in the device such as the Contacts in phone, the Dictionary for spelling etc. Content Provider can be used exclusively internally and to do that you have to set the exported flag to false in the manifest. The Content Provider does not speak, until it is spoken to and no one can speak directly to the Content Provider, they must first speak to the Content Resolver.

And Who is the ContentResolver

In any given Android device there could be more than one app that is shipped with Content Provider. Lets say you build your app with Content Provider and I build my app with Content Provider and a user install both of our app. The user also install eighteen other apps that is shipped with Content Provider. That makes twenty apps with Content Provider as the interface for their data access. If three of these apps have set their export flag to false that makes seventeen of those apps as potentially callable from other apps.

If another developer wants to develop an app that will call into any of those seventeen apps, how does this developer know what end point to call? The answer is through the manifest, each of those apps that have data that is potentially accessible by other apps must publish a URI in their manifest that contains a unique Authority in the device.

For our Simple Note App, the Authority will look like this 

"com.okason.simpleonotesapp.data.provider";
  and then their is something called the PATH which is a Content Provider’s equivalent of table and our base path for the Notes table in the Content Provider could look this 
private static final String BASE_PATH_NOTE = "notes";
  and with that the URI for the Content Provider for our SimpleNoteApp should look like this

public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH_NOTE);

And this could be published in the Manifest like this

<provider
            android:name=".data.NoteContentProvider"
            android:authorities="com.okason.simplenotesapp.data.provider"
            android:exported="false"
            android:multiprocess="true" />

And with that, the issue of discover-ability is solved. The address for our Content Provider is now published and it could be accessed with that address if we set the export flag to true. The next questions then is what methods are available to be called from outside of our app? Going back to the analogy of seventeen apps with publicly accessible Content Provider, how would a developer know which methods are available in each of these seventeen apps?  That is where the Content Resolver comes to the rescue.

The ContentResolver is the one that keeps a record of the methods that are available in each Content Provider so you don’t have to worry about it.  That means that there could only be one ContentResolver in the device. So if an app ships with a Content Provider it is expected to have implemented the basic four CRUD methods (create, read, update & delete). The ContentResolver have methods that matches these Content Provider CRUD methods.

So if you want to create an object in the database of say AppA you call the insert() method of ContentResolver. Since the ContentResolver is the one that stores those URIs we published in the manifest and it will use it to locate the Content Provider of AppA and ask it to do a create() operation. The ContentResolver does not have a say on how any individual Content Provider implements the methods they implement. It just expects them to have an implementation of the basic four CRUD methods. Here is an example of how to create a Note record in our app using ContentResolver.

public long add(Note note) {
        ContentValues values = new ContentValues();
        values.put(Constants.COLUMN_TITLE, note.getTitle());
        values.put(Constants.COLUMN_CONTENT, note.getContent());
        values.put(Constants.COLUMN_CREATED_TIME, System.currentTimeMillis());       
        Uri result = mContext.getContentResolver().insert(NoteContentProvider.CONTENT_URI, values);
        long id = Long.parseLong(result.getLastPathSegment());
        return id;
    }

We will examine what the above code is doing in the next post when we use what we have learned about ContentResolver, ContentProvider & SQLite database to create Notepad app.

Create New Content Provider

Follow the steps below to create a Content Provider and in the next post, we will use the Content Provider we create below to complete a Notepad app. These steps below assumes that you have complete the steps in the SQLite tutorial.

Step 1:  In the data package add a class called NoteContentProvider.java this will be a basic Java class like this.

public class NoteContentProvider {
}

Step 2:  Extend this class from the ContentProvider class, and once you do you will get a squiggly red line. Use the Android Studio quick fix (Control + Alt) in Windows to implement the methods. Once you do, your NoteContentProvider class will now look like this:

public class NoteContentProvider extends ContentProvider{
    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

Step 3:  Connect to the database – remember that Content Provide in of itself does not have a data store so we need to give it a data store to work with which is our database.  Create an instance variable called

private DatabaseHelper dbHelper;
  and then in the oncreate method instantiate that variable like this

@Override
    public boolean onCreate() {
        dbHelper = new DatabaseHelper(getContext());
        return false;
    }

Step 4:  Create Constants – our SQLite database contains only one table at the moment called notes. We will use a constant to represent that table in the Content Provider as a PATH. Add the constant 

private static final String BASE_PATH_NOTE = "notes";
  to the top of your class.

There are essentially to ways this table can be called, the call can be for the whole table or the call can be for a single row in the table. We will use two set of integers to represent this two possibilities. Add the following constants to your class.

private static final int NOTE = 100;
    private static final int NOTES = 101;

We talked about Authority being a unique address for our Content Provider. A common practice is to use your package name (aka domain name) in reverse and add the path to the Content Provider. Add this constant to your class using your own package name.

private static final String AUTHORITY = "com.okason.simplenotesapp.data.provider";

Now combine your Authority and your base path to form an absolute unique path to the Notes table. Add this next constant to your class

public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH_NOTE);

Notice that we have to make the CONTENT_URI public because this what we pass to the the ContentResolver.

Remember the ContentResolver is not aware of the internal implementation of our Content Provider class, so whenever we receive a call from the ContentResolver we have perform an internal check to see if the call was for a the whole table or for a single row in the table. The way we determine if the call is for a specific row on the table is if the calls contains any qualifier after the table name such as /authority/path/table/2 and if the calls is the format of say /authority/path/table then we know it is a query for the table.

Performing this checking in Content Provider speak is called matching and the Android Framework has a convenient class for us to perform this matching called the UriMatcher. Add the following code to your class , the “#” represents any number after the table indicating a table row.

private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    static {
        URI_MATCHER.addURI(AUTHORITY, BASE_PATH_NOTE, NOTES);
        URI_MATCHER.addURI(AUTHORITY, BASE_PATH_NOTE + "/#", NOTE);
        
    }

Step 5: Add Projection – a projection in this instance is a String array of the columns that we want to select when you run a query. For a simple app you probably want to the whole column. If you have not done so already. Add a package to the root of your project called “utility”. And under the utility package add a file named Constants.java. This is the place you define your constants that you need to access from more than one file. The projection array is definitely something we will access from more than one place, so in that Constants.java file add the following to your class. The constants in the COLUMNS array are the same constants that you defined if you followed the tutorial on SQLite.

public static final String[] COLUMNS = {
            Constants.COLUMN_ID,
            Constants.COLUMN_TITLE,
            Constants.COLUMN_CONTENT,
            Constants.COLUMN_CREATED_TIME            
    };

Step 6: Optional – check the projection that was passed to you. Remember the Content Provider is not called directly, the calls goes to the ContentResolver which is unaware of your database schema so it is possible that a non valid selection of columns can be passed to you.  I like to include this simple check to check the projection that is passed to me. Add this code your Content Provider class:

private void checkColumns(String[] projection) {
        if (projection != null) {
            HashSet<String> request = new HashSet<String>(Arrays.asList(projection));
            HashSet<String> available = new HashSet<String>(Arrays.asList(Constants.COLUMNS));
            if (!available.containsAll(request)) {
                throw new IllegalArgumentException("Unknown columns in projection");
            }
        }
    }

Step 6: Implement Query method – it is in the query method and the other methods that you translate the generic parameters that the ContentResolver called the Content Provider with into meaningful syntax that the underlying data store will understand. Update your query method like this

@Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
        checkColumns(projection);

        int type = URI_MATCHER.match(uri);
        switch (type){
            case NOTE:
                //there not to do if the query is for the table
                break;
            case NOTES:
                queryBuilder.appendWhere(Constants.COLUMN_ID + " = " + uri.getLastPathSegment());
                break;            
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

Here first, we used the checkColumns() we created to check the projection that was passed to us to make sure that they are matches columns in our database. Then we do a little switch statement to check if this query is for a table or a row in the table. If it is for a table we do nothing, if it is for a row in the table then we use SQLiteQueryBuilder which is yet another convenient method in the android.database.sqlite package to add a where clause to the query before running the query against our database.

Cursor

In SQL a Statement is a logic expressed as String (with valid syntax) that you run against a database and you do not expect a result. A query is a statement where you expect a result. This result could be zero or more in number. In Android, this result (which is a list of rows or data set) is wrapped in a Cursor. This Cursor object has helper method for you to iterate over it in the same way you can move cursor in your computer screen around.  If you want to go to the first item in the data set, you use a method called moveToFirst() and so one. To learn more about Cursor click here.

Step 7  – Implement Insert method – here we just check to see if the insert is for a single row which it  likely is, we just manually append the id of the row when we call the database.

@Override
    public Uri insert(Uri uri, ContentValues values) {
        int type = URI_MATCHER.match(uri);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Long id;
        switch (type){
            case NOTES:
                id = db.insert(Constants.NOTES_TABLE, null, values);
                break;            
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return Uri.parse(BASE_PATH_NOTE + "/" + id);
    }

Step 8: Implement  Delete method – with the delete method, we just want to apply some caution so we not dump the whole table. If the call is for a single row, we want to append a where clause using the id of the row which is stored in a Constants.java file as COLUMN_ID. If the call is for the whole table then we just passed whatever is in the selection args to the database.

@Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int type = URI_MATCHER.match(uri);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int affectedRows;
        switch (type) {
            case NOTES:
                affectedRows = db.delete(Constants.NOTES_TABLE, selection, selectionArgs);
                break;

            case NOTE:
                String id = uri.getLastPathSegment();
                if (TextUtils.isEmpty(selection)) {
                    affectedRows = db.delete(Constants.NOTES_TABLE, Constants.COLUMN_ID + "=" + id, null);
                } else {
                    affectedRows = db.delete(Constants.NOTES_TABLE, Constants.COLUMN_ID + "=" + id + " and " + selection, selectionArgs);
                }
                break;

            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return affectedRows;
    }

Step 9: Implement Update method – same with the delete method, we want to apply some caution, again we use switch statement to apply the correct where clauses before calling the database. Update your update method as follows:

@Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int type = URI_MATCHER.match(uri);
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int affectedRows;
        switch (type) {
            case NOTES:
                affectedRows = db.update(Constants.NOTES_TABLE, values, selection, selectionArgs);
                break;

            case NOTE:
                String id = uri.getLastPathSegment();
                if (TextUtils.isEmpty(selection)) {
                    affectedRows = db.update(Constants.NOTES_TABLE, values, Constants.COLUMN_ID + "=" + id, null);
                } else {
                    affectedRows = db.update(Constants.NOTES_TABLE, values, Constants.COLUMN_ID + "=" + id + " and " + selection, selectionArgs);
                }
                break;

            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);
        return affectedRows;
    }

Summary

This tutorial is an excerpt from my upcoming book Android SQLite for Beginners.  In this tutorial I provided an overview of Android Content Provider in a way that anyone getting started with Android development can grap. Content Provider has a lot of benefits but requires getting used to. In the next tutorial I will use what we have covered in this post and in the post on Android SQLite to create complete a Simple Android Note Taking app.

Happy Coding

 

The post Android Content Provider for Beginners appeared first on Val Okafor.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)