Android content providers

An Android app sometimes needs data produced or stored in another app. As you might expect, a database is used as the repository for the data, and (given the correct permissions) this data is available to any number of apps. The database used by Android is SQLite which, as the name implies, is a streamlined database that uses SQL as its language.

Rather than deal directly with the database, an app uses a ContentProvider as the interface between an app and the underlying data. To illustrate how ContentProviders are written and used, we’ll develop two simple apps that can exchange data. The first app has an interface that allows the user to enter a short text note which is then stored in a ContentProvider. The second app displays the notes in a ListView and allows the user to delete a note by clicking on it. We’ll look at the first app in this post and the second one in a future post.

We’ll begin with the main Activity called InsertDataActivity:

package growe.ex09contentproviderwriter;

import android.os.Bundle;
import android.app.Activity;
import android.content.ContentValues;
import android.view.Menu;
import android.view.View;
import android.widget.EditText;

public class InsertDataActivity extends Activity {
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_insert_data);
	}

	public void addEntryClick(View view)
	{
		EditText entryBox = (EditText) findViewById(R.id.newEntryEditText);
		String entryText = entryBox.getText().toString();
		ContentValues newEntry = new ContentValues();
		newEntry.put(EntriesContract.ENTRY_TEXT, entryText);
		getContentResolver().insert(EntriesContract.CONTENT_URI, newEntry);
	}
}

The view for this activity consists of an EditText box into which the user types the note and a button which adds the note, along with the date and time it was added, to the ContentProvider. The addEntryClick() method is the button’s event handler.

To understand lines 21 to 23 (which add the new note to the ContentProvider), we need to back up a bit and discuss how a ContentProvider works. A ContentProvider resides on an Android device at a particular location, which contains the Java package name within which the ContentProvider is defined. This package name is known as the authority of the ContentProvider. In this case, the package name is growe.ex09contentproviderwriter. The ContentProvider contains within it the SQLite database that holds the data, so once the app wishing to access the ContentProvider has found it using the authority, it also needs to send in the name of the database table it wishes to access.

However, the process of accessing a ContentProvider isn’t quite as simple as just retrieving the ContentProvider and then calling methods on it. Access to a ContentProvider is provided by yet another class called ContentResolver, which acts as a sort of lookup service for ContentProviders. A ContentResolver contains all the usual methods you’d expect for interacting with a database, such as insert(), delete(), query() and so on. Since the app we’re discussing only inserts items into the database, it’s the insert() method we’re interested in here. The insert() method we call on line 23 takes only 2 arguments. The first is the URI giving the address of the ContentProvider and the database table within which we want to insert the new data. The second argument contains the data to be inserted.

Both of these arguments probably look a bit cryptic here, so we’ll explain them one at a time. First, we have a parameter called EntriesContract.CONTENT_URI. We could have just used a bare String here, but it is more usual to define a Contract class which contains definitions of all the strings we need to interact with the ContentProvider. Here’s the EntriesContract class:

package growe.ex09acontentproviderviewer;

import android.net.Uri;

public final class EntriesContract {
	public static final String AUTHORITY = "growe.ex09contentproviderwriter";
	public static final Uri BASE_URI = Uri
			.parse("content://" + AUTHORITY + "/");
	public static final String ENTRIES_TABLE_NAME = "entrytable";
	public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_URI,
			ENTRIES_TABLE_NAME);

	public static final String _ID = "_id";
	public static final String ENTRY_DATETIME = "entryDateTime";
	public static final String ENTRY_TEXT = "entryText";
}

We define the BASE_URI which is the URI of the ContentProvider itself, without any reference to any particular database table within it. A URI for a ContentProvider always begins with content:// followed by the authority, followed by a slash.

The other strings define names we need to access the database itself. As we’ll see in a minute, the database contains a single table called ‘entrytable’, and this table has 3 columns: _id which is an autoincremented primary key, and a column for the date/time and one for the text itself.

The CONTENT_URI is built by joining the table name ‘entrytable’ onto the end of the BASE_URI, and it is this URI that we’ll use in all our interactions with the ContentProvider.

Now back to InsertDataActivity, and the second argument of the insert() method. Since we’re allowed only one argument to transmit all the data required for a new row in the database, you might wonder how we can send in values for each column in the row. The answer is that this argument is a ContentValues object, which is essentially a hash table of key-value pairs. We can use the put() method to add as many key-value pairs as we need. In this case, we need to add only one, since both the _id and the entryDateTime columns will be generated automatically when the row is added to the database (we’ll see how this is done when we discuss the ContentProvider below). On line 22, we add the entryText using the key EntriesContract.ENTRY_TEXT, which is the name of the column in the database in which the text is stored. If we had other columns to add, we just put in an additional put() call for each column.

That explains how we access and insert data into a ContentProvider, but we still haven’t created the ContentProvider, and it is here that most of the work lies. To write a custom ContentProvider, we need to inherit the ContentProvider abstract class and implement the required methods. We call our class EntriesContentProvider, and here’s the complete code:

package growe.ex09contentproviderwriter;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;

public class EntriesContentProvider extends ContentProvider {

	private static final String CREATE_LOCATION_TABLE = " CREATE TABLE " +
			EntriesContract.ENTRIES_TABLE_NAME +"(" +
			EntriesContract._ID
			+ " INTEGER PRIMARY KEY AUTOINCREMENT, "
			+ EntriesContract.ENTRY_TEXT + " TEXT NOT NULL, "
			+ EntriesContract.ENTRY_DATETIME + " DATETIME DEFAULT CURRENT_TIMESTAMP"
			+ ")";
	private static final String DB_NAME = "EntriesDB";
	private EntriesDatabaseHelper mDbHelper;
	private SQLiteDatabase database;

	protected static final class EntriesDatabaseHelper extends SQLiteOpenHelper
	{

		public EntriesDatabaseHelper(Context context) {
			super(context, DB_NAME, null, 1);
		}

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

		@Override
		public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		}

	}

	@Override
	public int delete(Uri arg0, String where, String[] whereArgs) {
		database.delete(EntriesContract.ENTRIES_TABLE_NAME, where, whereArgs);
		return 0;
	}

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

	@Override
	public Uri insert(Uri tableUri, ContentValues entry) {
		database = mDbHelper.getWritableDatabase();
		long newID = database.insert(EntriesContract.ENTRIES_TABLE_NAME, "", entry);
		if (newID > 0)
		{
			Uri newUri = ContentUris.withAppendedId(EntriesContract.CONTENT_URI, newID);
			return newUri;
		}
		throw new SQLException("Failed to add record into " + tableUri);
	}

	@Override
	public boolean onCreate() {
		mDbHelper = new EntriesDatabaseHelper(getContext());
		return true;
	}

	@Override
	public Cursor query(Uri arg0, String[] projection, String selection, String[] selectionArgs,
			String sortOrder) {
		SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
		queryBuilder.setTables(EntriesContract.ENTRIES_TABLE_NAME);
		Cursor cursor = queryBuilder.query(database, projection, selection,
				selectionArgs, null, null, sortOrder);
		cursor.setNotificationUri(getContext().getContentResolver(), arg0);
		return cursor;
	}

	@Override
	public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
		return 0;
	}

}

If you’ve done any SQL, much of the code here will be familiar. If not, you’ll just have to trust me.

The first thing we need to do if the application hasn’t been run before is create the database table. This is done using standard SQL, though in a bit of a roundabout way. The SQL for creating the table is the String on lines 16 to 22. We use the various Strings defined in EntriesContract to specify the table name and the names of the columns within it. The _id column is defined in the SQL as the primary key and autoincremented. The entryText column is defined as of type TEXT and not allowed to be null. SQLite’s SQL allows a DATETIME stamp to be added to an inserted row as shown on line 21, where we use the current time.

Where does the database actually get created? This is a bit involved. Android provides the abstract SQLiteOpenHelper class which can be used to create the database. We’ve inherited this class on line 27 and provided a constructor and the two required methods. In onCreate() (line 35) we call execSQL to create the table within the database db, but where does db itself get created?

The sequence of events is as follows. When the ContentProvider itself is created, its onCreate() method (line 69) is called. This creates the mDbHelper object, but doesn’t actually create the database (or access it, if it already exists). In fact, the database itself is not accessed until one of the accessing methods (insert(), query(), etc) is called. In the ContentProvider’s insert() method (line 57), we make a call to mDbHelper’s getWritableDatabase(). It is this call that checks to see if the database already exists and, if so, it returns the database object. If it doesn’t exist, the helper class then creates a database object and passes this to its own onCreate() method (line 35), which then creates the table within the database. As I said, it’s a bit involved, but it ensures that only one copy of the database exists.

Finally, we’ll look at the insert() method (we’ll leave query() and delete() to the next post, since they aren’t used in the first app). The ContentProvider’s insert() method (line 57) must call the database’s insert() method in turn (line 59) to do the actual insertion. The database insert() method takes the table name and a ContentValues object containing the column name-value pairs to be inserted. The second argument is to be used only in the event that we try to insert an empty row into the database (it’s called a ‘null column hack’), but since we won’t be doing that we’ll just set it to the empty string.

The ContentProvider insert() method should return a URI to the row that was just inserted. If we have an _id column, we can construct this by appending the _id to the CONTENT_URI.

One last task remains. We need to connect the ContentProvider class with the authority name. This is done in the AndroidManifest.xml file. Within the application tag, we insert the following:

        <provider android:name=".EntriesContentProvider"
            android:authorities="growe.ex09contentproviderwriter"
            android:exported="true"
            android:enabled="true">
        </provider>

That’s about it for inserting data into a ContentProvider’s database. We’ll examine querying and deleting in the next post.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Trackbacks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: