Chapter 5
In This Chapter
A primer on databases
Database processing in Android
Sharing data using a content provider
In his introduction to Napalm & Silly Putty (Hyperion Books), George Carlin wrote, “For the next few hundred pages, I will be your content provider.” Carlin was poking fun at business-speak phrases and other phrases that seem artificially lofty or commercially sanitized. Little did he know that a few years later, the introduction to his book would compare him to an Android SDK component.
A database is a place to store lots of data. Nobody’s surprised about that. A database management system is a bunch of software for creating the data, finding the data, and doing other useful things with the data.
Until the mid-1970s, people didn’t agree on the best structure for storing data in a database. Some argued for hierarchical structures, whereas others swore that networked structures were the only way to go. But in the 1970s, Edgar Codd (working at IBM) published papers on relational structures for storing data. Since the mid-1980s, the relational database has been the all-around favorite.
A relational database is a bunch of tables. Like a table in this book, a database table has rows and columns. Each row represents an instance (a customer, an employee, an appointment, or whatever), and each column represents a property of some kind (such as the customer’s name, the employee’s salary, or the appointment’s time). Table 5-1 is a table in this book, but it might as well be a table in a relational database.
Table 5-1 Customers
Account Number |
Name |
Outstanding Balance |
Comment |
001 |
Boris Bleeper |
25.00 |
Valued, long-time customer |
002 |
Barry Burd |
454.21 |
Deadbeat |
003 |
Jane Q. Customer |
0.00 |
Valued, long-time customer |
A Java programmer might compare a database table to a Java class. Each instance is a row, and each public field is column. In fact, this similarity between tables and classes has been apparent to people for quite a while. Many software frameworks specialize in object-relational mapping (ORM), in which the software automatically manages the correspondence between Java objects and relational database tables.
A database management system (DBMS) stores database data and provides access to the data for administrators and users. A database administrator (DBA) is a person who keeps the DBMS software running. A user is a person who gets information from the database and (with the right privileges) modifies values stored in the database. A user might directly or indirectly add rows to a table, but a user doesn’t add columns to a table or change a table’s structure in any way.
A database management system uses sophisticated data structures and algorithms to efficiently store and retrieve data. So the data in a database seldom lives in a flat file.
Database management systems come from many different vendors, with many different price ranges and many different feature sets. The big commercial players are IBM (with its DB2 software), Microsoft (with its Access and SQL Server products), and Oracle (with its aptly named Oracle Database). Some popular noncommercial products include MySQL (owned by Oracle), PostgreSQL, and SQLite. Each Android device comes with SQLite software.
In general, you communicate with a database in the following way:
You query the database, asking for rows and columns matching criteria that you specify.
In response, the database management system hands you a cursor. A cursor is a minitable; it’s a table of the rows and columns that match your query. The database management system distills the information in the database in order to deliver the cursor to you.
Like a regular database table, a cursor consists of rows and columns. At any point in time, the cursor points to one of the rows in the table (or to the never-never land after the table’s last row).
Depending on your permissions, you can also create a table, modify the values in rows of the table, insert rows into the table, and do other things. The four major table operations go by the name CRUD, which stands for Create, Read, Update, and Delete.
The most common way of issuing commands to a DBMS is with SQL — the Structured Query Language. (Depending on your mood, you can pronounce the SQL acronym ess-kyoo-el or sequel.) An SQL statement looks something like this:
SELECT * FROM CUSTOMER_TABLE WHERE COMMENT='Deadbeat';
Each database management system has its own dialect of SQL, so the only way to study SQL in detail is to work exclusively with one DBMS. With Android’s SDK, you can add strings containing SQL commands to your code. But you can also call methods that compose SQL commands on your behalf.
With Android’s SDK, an app has two ways to access a database:
In the interest of full disclosure, I must write that content providers don’t work exclusively with databases. A content provider is a bridge between an app’s code and another app’s data. The other app’s data doesn’t have to be part of a database. But the content provider’s publicly exposed methods look like database calls. So to anyone living outside the provider’s app, the provider’s data looks like database data. (See Figure 5-3.) The provider creates an abstract database-like view of whatever data lives underneath it.
The rest of this chapter consists of two examples. The first example demonstrates an app creating its own SQLite database and making calls directly to that database. In the second example, an app makes data available using a content provider, and another app accesses the first app’s data through the provider.
As you compare the first and second examples, you’ll notice some striking similarities. The second example is very much like the first. To get the second example, I (figuratively) tear the first example in half, giving half of the first example’s code to the new content provider and giving the other half of the first example’s code to a brand-new app.
Listing 5-1 contains code to access an SQLite database. When the activity begins running, the code inserts data, then modifies the data, and then deletes the data. To keep things simple, I intentionally omit much of the fail-safe checking that database code normally has. I also have a dirt-simple user interface. The activity’s screen has only one widget — a text view for displaying data at various stages of processing.
Listing 5-1: Sending Commands to a Database
package com.allmycode.db1;
import android.app.Activity;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.widget.TextView;
public class MyActivity extends Activity {
TextView textView;
Cursor cursor = null;
DBHelper helper = null;
SQLiteDatabase db = null;
ContentValues values = null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
textView = (TextView) findViewById(R.id.textView1);
helper = new DBHelper(this, "simple_db", null, 1);
db = helper.getWritableDatabase();
values = new ContentValues();
values.put("name", "Barry");
values.put("amount", "100");
db.insert("simpletable", "", values);
values.clear();
values.put("name", "Harriet");
values.put("amount", "300");
db.insert("simpletable", "", values);
addToTextView();
values.clear();
values.put("amount", "500");
db.update("simpletable", values, "name='Barry'", null);
addToTextView();
db.delete("simpletable", "1", null);
addToTextView();
}
@Override
public void onDestroy() {
super.onDestroy();
helper.close();
}
void addToTextView() {
cursor =
db.rawQuery("SELECT * FROM simpletable;", null);
if (cursor != null && cursor.moveToFirst()) {
String name;
do {
String _id = cursor.getString(0);
name = cursor.getString(1);
int amount = cursor.getInt(2);
textView.append(_id + " " + name + " " + amount
+ "
");
} while (cursor.moveToNext());
}
textView.append("-----------
");
}
}
The first new and exciting statement in Listing 5-1 is the call to a DBHelper
constructor. My DBHelper
class (to be unveiled in Listing 5-2) extends the abstract android.database.sqlite.SQLiteOpenHelper
class. The purpose of such a class is to manage the creation and modification of an SQLite database. In particular, the code in Listing 5-1 uses the helper to grab hold of an actual database.
Listing 5-2 contains my DBHelper
code.
Listing 5-2: A Subclass of the SQLiteOpenHelper Class
package com.allmycode.db1;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class DBHelper extends SQLiteOpenHelper {
public DBHelper(Context context, String dbName,
SQLiteDatabase.CursorFactory factory, int version) {
super(context, dbName, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
String createString =
"CREATE TABLE IF NOT EXISTS simpletable "
+ "( _id INTEGER PRIMARY KEY AUTOINCREMENT, "
+ "name TEXT NOT NULL, "
+ "amount INTEGER NOT NULL);";
db.execSQL(createString);
}
@Override
public void onUpgrade(SQLiteDatabase db,
int oldVersion, int newVersion) {
String dropString =
"DROP TABLE IF EXISTS simpletable;";
db.execSQL(dropString);
aonCreate(db);
}
}
In Listing 5-2, I implement the parent class’s abstract methods (as I must). I also create a constructor that takes the lazy way out, passing all its parameters to the parent constructor.
The most important part of Listing 5-2 is the helper’s onCreate
method. You never call this method directly. Instead, Android calls the method on your behalf when you set up a helper the way I do in Listing 5-1.
Android hands the onCreate
method an SQLite database (a database belonging to the app in which the helper is located). That SQLite database (called db
in the onCreate
method’s parameter list) has an execSQL
method. Listing 5-2 calls the database’s execSQL
method, feeding the method an ordinary Java string. Luckily for me, this ordinary Java string happens to be an SQL command.
In Listing 5-2's onCreate
method, lots of good things happen without much fanfare. If the database doesn’t already exist, Android creates one. If the database doesn’t already have a table named simpletable
, SQLite creates one. If the database already exists and has a simpletable
, the onCreate
method doesn’t rock the boat.
SQL is more readable than some other languages, so with or without a thorough introduction, you can probably make sense of most of the SQL commands in this book. The SQL command midway through Listing 5-2, for example, says the following:
If the database doesn’t have a table named simpletable,
create simpletable with three columns, called
_id, name, and amount.
The _id column stores an integer value,
which serves to identify its row,
and is incremented by 1 for each newly added row,
The name column stores a string value
which cannot be null, and
The amount column stores an integer value
which cannot be null.
In Listing 5-1, after the call to getWritableDatabase
, the code performs some fairly commonplace operations — namely, inserting, updating, deleting, and querying.
Each call to db.insert
adds a row of values to the simpletable
. Each value is actually a name/value pair, the name being a database column name and the value being something to stuff into that column.
As a result of the calls to insert
in Listing 5-1, the activity’s screen contains the first two lines in the text view of Figure 5-4.
In Listing 5-1, the call to db.update
takes four parameters. The first two parameters — a table name and a set of values — are old hat. The update
method’s third parameter is part of an SQL WHERE
clause. A WHERE
clause tells SQLite which rows should be chosen for processing. For example, the WHERE
clause in the section “Databases: From the Stone Age to the Present Day” tells SQLite to select only those rows whose Comment column contains the value Deadbeat
.
For the third parameter in an update
method call, you supply a string containing the entire WHERE
clause, but you omit the word WHERE. So in Listing 5-1, the update
method call generates an SQL statement containing WHERE name = 'Barry'
. At this point in the game, it’s easy to become confused with nested single quotes and double quotes. The SQL command rules require a value such as 'Barry'
to be quoted, and a Java string must be double-quoted. If things become more complicated, you have to use escape sequences and other tricks.
String[] whereArgs = {"Barry"};
db.update("simpletable", values, "name=?", whereArgs);
The delete
method call in Listing 5-1 takes three parameters — a table name, a WHERE
clause, and a set of WHERE
arguments. The WHERE
clause is normally something like "name='Barry'"
. (Get rid of that deadbeat!) But in Listing 5-1, the WHERE
clause is "1"
— the SQL code for everything. In Listing 5-1, the delete
method removes every row in the database table.
After each change to the database, the code in Listing 5-1 adds text to the activity’s text view. To do this, the code executes a query. The database’s rawQuery
method takes two parameters — an SQL command string and a (possibly null) set of WHERE
arguments.
In Listing 5-1, the call to rawQuery
returns a cursor. The cursor.moveToFirst
call returns true
as long as the attempt to reach the cursor table’s first row is successful. (Failure typically means that the table has no rows.)
From that point on, the code in Listing 5-1 loops from row to row, moving the cursor to the next table row each time through the loop. The cursor.moveToNext
call returns false
when there’s no next row to move to.
Every time through the loop, the code uses the cursor to get the values in each of the table’s three columns. The columns' indices start at 0 and increase in the order in which I declare the columns in Listing 5-2. Notice how I call get
methods particular to the types of data in the database. Nothing good can happen if, for one column or another, I use a get
method with the wrong type.
As the code in Listing 5-1 takes its last breath, the activity’s onDestroy
method closes the helper instance. Doing so shuts down the entire database connection and frees up resources for use by other apps. As is the case with all onDestroy
methods, you should eschew my overly simple code. Before calling the helper’s close
method, make sure that the helper isn’t null
. Also include code to handle the possibility that the Android is temporarily destroying (and later re-creating) the activity. For some help with that, see Chapter 3 of this minibook.
A content provider is a gateway to an app’s data. Other apps approach the gateway as if it’s a database. But under the hood, the data can take many different forms.
This section’s example involves two apps — an app with a content provider and a separate client app. (Refer to Figures 5-2 and 5-3.) The client app’s code is in Listing 5-3.
Listing 5-3: Getting Data from a Content Provider
package a.b.c;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.CursorLoader;
import android.content.Loader;
import android.content.Loader.OnLoadCompleteListener;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends Activity {
ContentValues values = null;
ContentResolver resolver = null;
CursorLoader loader = null;
Button button, button2, button3, button4;
TextView textView;
Uri CONTENT_URI = Uri.parse
("content://com.allmycode.db/names_amounts");
Uri SILLY_URI = Uri.parse
("content://com.allmycode.db/silly_stuff");
@Override
public void onCreate(Bundle b) {
super.onCreate(b);
setContentView(R.layout.activity_main);
values = new ContentValues();
resolver = getContentResolver();
textView = (TextView) findViewById(R.id.textView);
button = (Button) findViewById(R.id.button);
button2 = (Button) findViewById(R.id.button2);
button3 = (Button) findViewById(R.id.button3);
button4 = (Button) findViewById(R.id.button4);
}
public void onButtonClick(View view) {
if (view == button) {
resolver.delete(CONTENT_URI, "1", null);
showStuffFrom(CONTENT_URI);
} else if (view == button2) {
values.clear();
values.put("name", "Sam");
values.put("amount", "100");
resolver.insert(CONTENT_URI, values);
values.clear();
values.put("name", "Jennie");
values.put("amount", "300");
resolver.insert(CONTENT_URI, values);
showStuffFrom(CONTENT_URI);
} else if (view == button3) {
values.clear();
values.put("amount", "500");
resolver.update(CONTENT_URI, values,
"name='Sam'", null);
showStuffFrom(CONTENT_URI);
} else if (view == button4) {
showStuffFrom(SILLY_URI);
}
}
void showStuffFrom(Uri uri) {
loader =
new CursorLoader(this, uri, null, "1", null, null);
loader.registerListener(42,
new MyOnLoadCompleteListener());
loader.startLoading();
}
class MyOnLoadCompleteListener implements
OnLoadCompleteListener<Cursor> {
@Override
public void onLoadComplete(Loader<Cursor> loader,
Cursor cursor) {
textView.setText("");
if (cursor != null && cursor.moveToFirst()) {
do {
String _id = cursor.getString(0);
String name = cursor.getString(1);
int amount = cursor.getInt(2);
textView.append(_id + " " + name + " " + amount
+ "
");
} while (cursor.moveToNext());
}
}
}
}
The activity in Listing 5-3 has four buttons:
If you click the first button, you empty the content provider of any and all names_amounts
data. (See Figure 5-5.)
If view == button
is true
, you call a delete
method. The method’s second parameter contains an SQL WHERE
clause. And (oddly enough) the number 1
happens to be a complete WHERE
clause. As a WHERE
clause, 1
means all rows.
If you click the second button, you add two rows to the content provider’s names_amounts
data. (See Figure 5-6.)
If you click the first button once, and then click the second button two times, the content provider has four rows of data (a Sam
row, a Jennie
row, and then another Sam
row and another Jennie
row).
If you click the third button, you change Sam’s amount to 500. (See Figure 5-7.)
The content provider might have more than one Sam
row. The value in every Sam
row changes to 500.
The content provider has at least two kinds of data — names_amounts
data and silly_stuff
data. If you click the fourth button, you display the content provider’s silly_stuff
data. (See Figure 5-8.)
There’s nothing mysterious about a content provider with two kinds of data. If you’re curious, look ahead to Listing 5-5.
In Listing 5-3, the client app has no direct communication with the provider’s database. Instead, the client app talks to the database through a content resolver.
A content resolver has methods to insert data, update data, delete data, and so on. Each content resolver method takes a URI — a reference to some data offered by a content provider. Here’s what happens when your code calls a content resolver method:
Android examines the URI’s scheme and finds the content
:
scheme.
The content:
scheme tells Android to look for a matching content provider.
Android compares the URI’s authority with the authorities in the intent filters of available content providers. (See Figure 5-9.)
A content provider must declare one or more authorities in its app’s AndroidManifest.xml
document. Listing 5-4 has the AndroidManifest.xml
document for this section’s example. Notice that in this example, the app containing the content provider has no activity. The app has no direct interface to the user.
Listing 5-4: A Content Provider’s XML Element
<manifest xmlns:android=
"http://schemas.android.com/apk/res/android"
package="com.allmycode.db">
<application android:allowBackup="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme">
<provider android:name=".MyContentProvider"
android:authorities="com.allmycode.db"
android:exported="true">
</provider>
</application>
</manifest>
If an android:authorities
attribute contains more than one authority, you separate authorities from one another using semicolons:
android:authorities="this.is.one;this.is.another"
So far, so good. Here’s what happens when Android finds a matching content provider:
Android hands the client’s database-like call (URI and all) to the matching content provider.
After Android hands the call to a content provider, the ball is in the content provider’s court.
To get data from a content provider, Listing 5-3 uses a cursor loader. The CursorLoader
constructor has six parameters. The parameters are as follows:
Context context
: In Listing 5-3, the context (this
) is the activity itself.
To read about Android’s Context
class, see Chapter 1 in this minibook.
Uri uri
: A reference to some data offered by a content provider.String[] projection
: A list of columns from the table. (If you pass null
, you’re saying “gimme' all the columns.”)String selection
: An SQL WHERE
clause. (See this chapter’s “Updating” section.)String[] selectionArgs
: The WHERE
string can include question marks. The question marks are placeholders for actual values that you place in the selectionArgs
array. (See this chapter’s “Updating” section.)String sortOrder
: The order in which the rows come from the table. (A SQL statement can have an ORDER BY
clause. Put that clause, minus the words ORDER BY
, in this parameter.) A null
value means “use the default order, whatever that is.”From the constructor call onward, the activity’s thread registers a callback listener (the MyOnLoadCompleteListener
) and calls the loader’s startLoading
method. This means that the parent thread (the code in Listing 5-3) doesn’t stop and wait for the startLoading()
call to return. Instead, the activity’s code continues on its merry way.
In the meantime, the cursor loader does its thing in a separate thread, and the callback listener (OnLoadCompleteListener
) listens for the cursor loader to get its results. When at last the loading of the data is complete (that is, the cursor is done being loaded), Android calls the onLoadComplete
method inside the OnLoadCompleteListener
instance. This onLoadComplete
method can do whatever you want done with the cursor’s data. (In Listing 5-3, the onLoadComplete
method displays data in your activity’s text view.)
Listing 5-5 contains a content provider for this section’s ongoing example.
Listing 5-5: Look: It’s a Content Provider
package com.allmycode.db;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
public class MyContentProvider extends ContentProvider {
public static final Uri CONTENT_URI = Uri
.parse("content://com.allmycode.db/names_amounts");
public static final Uri SILLY_URI = Uri
.parse("content://com.allmycode.db/silly_stuff");
private static final String SIMPLE_DB = "simple_db";
private static final String SIMPLETABLE = "simpletable";
DBHelper helper = null;
SQLiteDatabase db = null;
ContentValues values = null;
UriMatcher uriMatcher = null;
{
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("com.allmycode.db",
"names_amounts", 1);
uriMatcher.addURI("com.allmycode.db",
"silly_stuff", 2);
}
@Override
public boolean onCreate() {
try {
helper =
new DBHelper(getContext(), SIMPLE_DB, null, 1);
db = helper.getWritableDatabase();
values = new ContentValues();
return true;
} catch (Exception e) {
return false;
}
}
@Override
public Uri insert(Uri ure, ContentValues values) {
long id = db.insert(SIMPLETABLE, "", values);
return ContentUris.withAppendedId(CONTENT_URI, id);
}
@Override
public int update(Uri uri, ContentValues values,
String whereClause, String[] whereArgs) {
int numOfChangedRows =
db.update(SIMPLETABLE, values, whereClause,
whereArgs);
return numOfChangedRows;
}
@Override
public int delete(Uri uri, String whereClause,
String[] whereArgs) {
int numOfChangedRows =
db.delete(SIMPLETABLE, whereClause, whereArgs);
return numOfChangedRows;
}
@Override
public Cursor query(Uri uri, String[] columns,
String whereClause, String[] whereArgs,
String sortOrder) {
Cursor cursor = null;
int code = uriMatcher.match(uri);
if (code == 1) {
cursor =
db.query(SIMPLETABLE, columns, whereClause,
whereArgs, null, null, sortOrder);
} else if (code == 2) {
String[] columnNames = { "_id", "name", "amount" };
String[] rowValues = { "Table ", "4 ", "2" };
MatrixCursor matrixCursor =
new MatrixCursor(columnNames);
matrixCursor.addRow(rowValues);
cursor = matrixCursor;
}
return cursor;
}
@Override
public String getType(Uri uri) {
return null;
}
}
Listing 5-5 implements the six abstract methods declared in the android.content.ContentProvider
class. The implementation code bears a striking resemblance to some of the code in Listing 5-1.
onCreate
methods use DBHelper
(which extends Android’s SQLiteOpenHelper
class) to get a writable database.insert
, update
, and delete
methods.Both listings issue a query to the database.
Actually, Listing 5-1 uses the rawQuery
method and an SQL command string. In contrast, Listing 5-5 uses the query
method with a bunch of parameters. The difference has nothing to do with content providers. It’s just my whim in using different methods to illustrate different ways of issuing a query.
The bottom line is this: A content provider does with its app’s data what an ordinary activity does with its own app’s data.
Listing 5-5 has some features that I choose not to use in Listing 5-1. For example, Android’s update
and delete
methods return int
values. In Listing 5-1, I simply ignore the return values. But in Listing 5-5, I pass each method’s return value back to the client code. (Don’t congratulate me on my diligence. I pass on each value because the content provider’s abstract update
and delete
methods must have int
return values.)
Listing 5-5 also has some features that are unique to content providers. For example, to effectively dish out data, a content provider must manage URIs. Normally, a content provider examines each method’s incoming URI and uses the information to decide on its next move. In Listing 5-5, I keep things simple with only a minor bit of URI handling in the query
method.
In Listing 5-5, the query
method calls on a UriMatcher
instance to distinguish one path from another. As it’s defined near the start of Listing 5-5, the UriMatcher
instance returns 1
for the names_amounts
path and returns 2
for the silly_stuff
path.
A return value of 1
makes the query
method do its regular old database query. But a return value of 2
does something entirely different. To show that I can do it, I respond to a URI’s silly_stuff
path without consulting a real database. Instead, I use arrays to concoct something that looks like a simpletable
row. I squish the arrays into a MatrixCursor
(a cursor built from an array rather than a database), and I send the cursor back to the client.
The insert
method in Listing 5-5 returns a URI. What’s that all about? Each row associated with the content: scheme
has its own individual URI. For example, in a run of this section’s code, the first two row insertions have URIs content://com.allmycode.db/names_amounts/1
and content://com.allmycode.db/names_amounts/2
. The android.content.ContentUris
class’s withAppendedId
method fetches the URI for a particular database row. The client can use this row-specific URI to refer to one database row at a time.
35.170.81.33