Android content providers are an easy way to manage and expose data from an application, especially when backed by a database. You'll see a fast way to provide a specific, quick implementation of both a database and a content provider in this tutorial.
In this continuation from the last tutorial, Android Barometer Logger: Acquiring Sensor Data, we'll implement code for reading barometric sensor data on a regular basis and storing it in a persistent fashion. You'll learn to read the sensor data and how to schedule recurring events so the application, and its service, don't have to remain running.
Getting Started
This tutorial assumes you have a basic understanding of Android and Java, that you have all of the Android tools installed and working, and that you are comfortable with loading and testing applications on an Android device. We'll be using the barometer hardware sensor in this application. If you don't have a device with this sensor, you can substitute with another, similar sensor, for testing purposes, but the results or usefulness may not be equivalent.
You may have see database and content providers appear in some other tutorials; this one is presenting a fast, "quick-and-dirty" variant for the hobbyist or enthusiast wanting to quickly log some data in a structured database and display it later easily, all in an Android-friendly way.
Part A: Creating a Database
The fastest way to create a database on Android using SQLite is to use the SQLiteOpenHelper class. The data records we wish to log are very simple in structure: a timestamp and a value representing the pressure at that time.
Step 1: Determining the Schema
The schema for this database is simple. Beyond the two required data columns for the timestamp and pressure value, we'll also create a unique identifier--useful should we ever expand the schema or need to deal with the data on a row-by-row basis. Here is the schema definition for the SQLiteOpenHelper class:
static final String TABLE_NAME = "table_sensor_data"; static final String COL_ID = "_id"; static final String COL_VALUE = "value"; static final String COL_TIMESTAMP = "timestamp"; private static final String DB_SCHEMA = "CREATE TABLE " + TABLE_NAME + "(" + COL_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COL_TIMESTAMP + " INTEGER NOT NULL, " + COL_VALUE + " REAL " + ");";
SQLite can actually store timestamps in many ways and manipulate them internally. However, we may never need to manipulate them, so we can just store the value read from the sensor.
Step 2: Implementing the SQLiteOpenHelper Class
With the schema out of the way, the rest of the SQLiteOpenHelperclass is trivial:
public class SensorDataHelper extends SQLiteOpenHelper { private static final String DEBUG_TAG = "SensorDataHelper"; private static final String DATABASE_NAME = "sensor_data.db"; private static final int DATABASE_VERSION = 1; // schema // …. SensorDataHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DB_SCHEMA); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(DEBUG_TAG, "Warning: Dropping all tables; data migration not supported"); db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); onCreate(db); } }
Basically, we configure the database version and decide how we want to handle version changes and upgrades--in this case, we drop all data on an upgrade. We do not support data migration. However, you could easily add code here to migrate data on an upgrade as necessary.
Part B: Implementing a Simple Content Provider
Content providers are Android’s way of exposing structured data through Cursors while protecting the underlying data. Application data is queried through the content provider instead of accessed from the application database directly. Because this implementation is using a SQLiteOpenHelper, the task of creating a content provider is straightforward.
Step 1: Choosing What to Implement
While a fully functional content provider that handles all data operations like queries, inserts, updates, and deletes takes little more work than a partial one, for logging purposes all we really need to for input implement is the insert() method. Getting data out requires implementation of the query() method, which will also leverage the URIMatcher helper class. You can always go back and implement other features of the content provider later, should your requirements change. Most data logging, though, requires complete, unaltered records.
Step 2: Implementation Details
Let’s implement a content provider that works with our sensor logging plans. First, set up some useful constants and the URIMatcher to help us match URIs to the type of data they represent:
public class SensorDataProvider extends ContentProvider { private static final String DEBUG_TAG = "TutListProvider"; private SensorDataHelper sensorDataHelper; public static final int SENSORDATA = 100; public static final int SENSORDATA_ID = 110; private static final String AUTHORITY = "com.mamlambo.barologger.SensorDataProvider"; private static final String BASE_PATH = "sensordata"; public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + BASE_PATH); public static final String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/barolog"; public static final String CONTENT_TYPE = ContentResolver.CURSOR_DIR_BASE_TYPE + "/barolog"; private static final UriMatcher sURIMatcher = new UriMatcher( UriMatcher.NO_MATCH); static { sURIMatcher.addURI(AUTHORITY, BASE_PATH, SENSORDATA); sURIMatcher.addURI(AUTHORITY, BASE_PATH + "/#", SENSORDATA_ID); } //… } [sourcecode] A consumer of this content provider will refer to the CONTENT_URI value. The AUTHORITY is used by the provider entry in the manifest file. <hr> <h2><span>Step 3:</span> Implementation Insert and Query</h2> The insert() and query() methods are implemented using a simple mapping to helper methods from the SQLite database objects. [sourcecode language="java"] @Override public Uri insert(Uri uri, ContentValues values) { int uriType = sURIMatcher.match(uri); if (uriType != SENSORDATA) { throw new IllegalArgumentException("Invalid URI for insert"); } SQLiteDatabase sqlDB = getDatabase(true); try { long newID = sqlDB.insertOrThrow(SensorDataHelper.TABLE_NAME, null, values); if (newID > 0) { Uri newUri = ContentUris.withAppendedId(uri, newID); getContext().getContentResolver().notifyChange(uri, null); return newUri; } else { throw new SQLException("Failed to insert row into " + uri); } } catch (SQLiteConstraintException e) { Log.w(DEBUG_TAG, "Warning: Ignoring constraint failure."); } return null; }
Most of what you see here is error handling. The main call, insertOrThrow(), uses data directly from the parameters to the insert() method.
And now the query() method:
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(SensorDataHelper.TABLE_NAME); int uriType = sURIMatcher.match(uri); switch (uriType) { case SENSORDATA_ID: queryBuilder.appendWhere(SensorDataHelper.COL_ID + "=" + uri.getLastPathSegment()); break; case SENSORDATA: // no filter break; default: throw new IllegalArgumentException("Unknown URI"); } Cursor cursor = queryBuilder.query(getDatabase(false), projection, selection, selectionArgs, null, null, sortOrder); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; }
The only change to the query() method is adding the identifier to the where clause if the URI contains a path to a specific record. Otherwise, the parameters to the query() method are also passed as-is to the query() method call. Very convenient.
We've used a helper method to get the SQLiteDatabase object instance. This is so we only create the SQLiteOpenHelper object when absolutely necessary. This is recommended to improve overall performance.
private SQLiteDatabase getDatabase(boolean writable) { if (sensorDataHelper == null) { sensorDataHelper = new SensorDataHelper(getContext()); } return (writable ? sensorDataHelper.getWritableDatabase() : sensorDataHelper.getReadableDatabase()); }
Step 3: Updating the Manifest
Finally, a content provider needs to be registered as a provider in the application’s manifest file in the <application> tag, like so:
<provider android:name="com.mamlambo.barologger.SensorDataProvider" android:authorities="com.mamlambo.barologger.SensorDataProvider" > </provider> [sourcecode] The name field is the class name of the content provider and the authority should match the authority used in your content provider. <hr> <h2>Part C: Inserting Data</h2> Back in the AsyncTask of the Service we implemented in the last tutorial, we now need to actually insert the data into the content provider as new sensor readings are taken. The simplicity of this process may surprise you: [sourcecode language="java"] ContentValues values = new ContentValues(); values.put(SensorDataHelper.COL_VALUE, value); values.put(SensorDataHelper.COL_TIMESTAMP, timestamp); getContentResolver().insert(SensorDataProvider.CONTENT_URI, values);
This simplicity is one reason why we like the marginal overhead of implementing a content provider on top of a database.
Conclusion
In this tutorial, you have learned how to quickly create a simple application database and related content provider and use it for easily logging data--in this case the barometric pressure readings taken from the device at regular intervals.
As a challenge, implement this code on your own. Round out the implementation of the content provider by implementing the delete() and update() methods. Are they needed for your logger? Can leaving them out be used to enforce data integrity?
About the Authors
Mobile developers Lauren Darcey and Shane Conder have coauthored several books on Android development: an in-depth programming book entitled Android Wireless Application Development (now in it's third edition as a two-volume set) and Sams Teach Yourself Android Application Development in 24 Hours. When not writing, they spend their time developing mobile software at their company and providing consulting services. They can be reached at via email to [email protected], via their blog at androidbook.blogspot.com, and on Twitter @androidwireless.
Comments