When creating apps in Android, you have access to numerous data sources. In fact, Android comes with a built-in instance of SQLite, provides access data to local storage, gives direct access to remote resources over HTTP. In additions to these native data providers, you can setup your own application to be a data provider. Since Android does not allow applications to share data directly with one another, you can make your data accessible to (components in your application or) other applications using the ContentAPI. For instance, Android has a phonebook content provider which lets you search, add, modify contacts.
A ContentProvider
is a first-class Android component (like Activity, Service, etc). It is registered in the manifest file like any other high-level components. You can read about Android ContentProviders from Android’s website at:
- http://developer.android.com/reference/android/content/ContentProvider.html – API doc
- http://developer.android.com/guide/topics/providers/content-providers.html – Content Provider usage
The intent of this write up is to present a simple approach to create and work with your own ContentProvider. It is not meant to be an introduction to data storage in Android. If you have not used data storage in Android, visit the Android’s developer web site and look for data storage.
ContentProvider Overview
As mentioned above, one of the primary purposes for the use of ContentProvider is data sharing. Since databases and other internal data stores are application-scoped, there’s no way to share information between applications. A content provider lets you expose access to your application’s data in a structured and uniform manner.
ContentProdviders are considered data access objects (a software pattern). Their backing data store can be a database, data from local storage, data from a remote server over HTTP, or a custom data source. For this write up, I will use the SQLite database as the backing datastore for the sample content provider that will be be demonstrated. This will keep things simple since the API for SQLite maps nicely to the method used by the ContentProvider API.
The Example
How to Do It
- Define data model – figure out what will be in stored
- Create a Descriptor class – to help describe the data that you will work with. This is not part of any of the APIs. It is a class that I have used to help with with creation of providers.
- Create a Database Class – the database class is implemented as a
SQLiteOpenHelper
intended to help with creation/management of the database instance itself. - Define your ContentProvider – the content provider will use the classes created above to install the database, access, and manipulate the data in it. This is also the class that Activity classes will use to access the data.
Define the Data Model
ID
NAME
ADDRESS
CITY
ZIP
It’s good practice to define an ID column as an identifier for the data row. The ContentProvider’s URI mechanism uses the ID to refer to saved data entities.
Descriptor Class
- URI Authority – the authority portion of the URI representing this entity. In our example it is “com.favrestaurant.contentprovider”
- URI Matcher – this is an internal registry used to map a URI path (serviced by the ContentProvider) to an integer value.
- Entity Class – an inner static class that represents the entity to be managed by the ContentProvider. In our example, this class is called Restaurant. It exposes meta data such as the entity name, supported URIs, etc.
- Class EntityClass.Cols – the entity class provides an inner class called Cols. As you may have guessed, this class exposes the name of the columns to exposed by the ContentProvider for the entity.
public class ContentDescriptor { public static final String AUTHORITY = "demo.contentprovider.restaurant"; private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY); public static final UriMatcher URI_MATCHER = buildUriMatcher(); private ContentDescriptor(){}; private static UriMatcher buildUriMatcher() { final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); final String authority = AUTHORITY; matcher.addURI(authority, Restaurant.PATH, Restaurant.PATH_TOKEN); matcher.addURI(authority, Restaurant.PATH_FOR_ID, Restaurant.PATH_FOR_ID_TOKEN); return matcher; } public static class Restaurant { public static final String NAME = "restaurant"; public static final String PATH = "restaurants"; public static final int PATH_TOKEN = 100; public static final String PATH_FOR_ID = "restaurants/*"; public static final int PATH_FOR_ID_TOKEN = 200; public static final Uri CONTENT_URI = BASE_URI.buildUpon().appendPath(PATH).build(); public static final String CONTENT_TYPE_DIR = "vnd.android.cursor.dir/vnd.favrestaurant.app"; public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.favrestaurant.app"; public static class Cols { public static final String ID = BaseColumns._ID; // convention public static final String NAME = "restaurant_name"; public static final String ADDRESS = "restaurant_addr"; public static final String CITY = "restaurant_city"; public static final String STATE = "restaurant_state"; public static final String ZIP = "restaurant_zip"; } } }
What’s going on…
- The first thing to notice in the code above is the definition of variables
AUTHORITY
andBASE_URI
. Together these form the URI that identifies the ContentProvider. The URI is used by Android for registering the ContentProvider as part of the application. As you will see later, a ContentResolver class will locate and use the ContentProvider based on the provided URI. - Private method
buildMatcher()
creates an instance ofURIMatcher
for the ContentProvider. - Inner class
Restaurant
exposes meta data that defines the Restaurant entity managed by the associated ContentProvider. - Furthermore, inner class
Restaurant.Cols
define meta values for the columns associated with the Restaurant entity.
If none of this makes sense, read on to see how the Descriptor
class is used.
The Database Class (SQLiteOpenHelper)
ContentProvider
implementation is a database, we will use the SQLLite API here to define the database. The purpose of class RestaurantDatabase
is to create, install, and help manage the SQLLite database. The Android’s ContentProvider API (along with the ContentResolver class) uses this class to run DDL scripts to install and update the database. If you implement the onUpgrade()
method and change the version of the database, the database will be upgraded automatically next time the code is executed.
The one notable portion of the code below is its use of the ContentDescriptor
class (see defined above) to provide meta data the table and fields used in the database.
public class RestaurantDatabase extends SQLiteOpenHelper { private static final String DATABASE_NAME = "fav_restaurnt.db"; private static final int DATABASE_VERSION = 2; public RestaurantDatabase(Context ctx){ super(ctx, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE " + ContentDescriptor.Restaurant.NAME+ " ( " + ContentDescriptor.Restaurant.Cols.ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + ContentDescriptor.Restaurant.Cols.NAME + " TEXT NOT NULL, " + ContentDescriptor.Restaurant.Cols.ADDRESS + " TEXT , " + ContentDescriptor.Restaurant.Cols.CITY + " TEXT, " + ContentDescriptor.Restaurant.Cols.STATE + " TEXT, " + ContentDescriptor.Restaurant.Cols.ZIP + " TEXT, " + "UNIQUE (" + ContentDescriptor.Restaurant.Cols.ID + ") ON CONFLICT REPLACE)" ); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if(oldVersion < newVersion){ db.execSQL("DROP TABLE IF EXISTS " + ContentDescriptor.Restaurant.NAME); onCreate(db); } } }
The ContentProvider Class
ContentProvider
class with the logic for data access and update. The ContentProvider API exposes several methods for inserting, updating, querying, and deleting data. The example code only implements the insert()
and query()
methods. Note the usage of the ContentDescriptor
to provide naming and configuration meta data for the ContentProvider
.
public class RestaurantContentProvider extends ContentProvider { private RestaurantDatabase restaurantDb; @Override public boolean onCreate() { Context ctx = getContext(); restaurantDb = new RestaurantDatabase(ctx); return true; } @Override public String getType(Uri uri) { final int match = ContentDescriptor.URI_MATCHER.match(uri); switch(match){ case ContentDescriptor.Restaurant.PATH_TOKEN: return ContentDescriptor.Restaurant.CONTENT_TYPE_DIR; case ContentDescriptor.Restaurant.PATH_FOR_ID_TOKEN: return ContentDescriptor.Restaurant.CONTENT_ITEM_TYPE; default: throw new UnsupportedOperationException ("URI " + uri + " is not supported."); } } @Override public Uri insert(Uri uri, ContentValues values) { SQLiteDatabase db = restaurantDb.getWritableDatabase(); int token = ContentDescriptor.URI_MATCHER.match(uri); switch(token){ case ContentDescriptor.Restaurant.PATH_TOKEN:{ long id = db.insert(ContentDescriptor.Restaurant.NAME, null, values); getContext().getContentResolver().notifyChange(uri, null); return ContentDescriptor.Restaurant.CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); } default: { throw new UnsupportedOperationException("URI: " + uri + " not supported."); } } } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db = restaurantDb.getReadableDatabase(); final int match = ContentDescriptor.URI_MATCHER.match(uri); switch(match){ // retrieve restaurant list case ContentDescriptor.Restaurant.PATH_TOKEN:{ SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); builder.setTables(ContentDescriptor.Restaurant.NAME); return builder.query(db, null, null, null, null, null, null); } default: return null; } } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } }
What’s going on…
The onCreate()
method is called when the provider is instantiated (by theContentResolver
class). It is, in turn, used to bootstrap the database via theRestaurantDatabase
class (see above) instance db.- The
getType()
method uses theContentDescriptor.URI_MATCHER
(seeContentDescriptor
above) to lookup the MIME type for a given URI. - All of the data access & update methods (including
query()
,insert()
,update()
, anddelete()
) take a URI parameter. The URI provides hints such as the entity (and cardinality) being queried or updated. For instance, in our example, if the URI to passed to the query() method looks likecontent://com.favrestaurant.contentprovider/restaurants/*
the method will return all restaurant rows in the database. This is accomplished by using theContentDescriptor.URI_MATCHER
to determine how to process the URI.
Using the ContentProvider
Once you have all of your pieces in place, you can access the data exposed by the content provider using the ContentResolver
. There are certainly more robust ways to use to access data from a ContentProvier. This write up shows the simplest (non-production ready) way of doing it. You should investigate which way works best for your use (see http://developer.android.com/guide/topics/providers/content-providers.html).
public class FavRestaurantActivity extends Activity { TextView txtName; TextView txtAddr; TextView txtState; TextView txtCity; TextView txtZip; ContentResolver contentResolver; Cursor cur; SimpleCursorAdapter adapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); ... contentResolver = this.getContentResolver(); } @Override public void onStop() { super.onStop(); if(cur != null) cur.close(); } private void loadContent() { cur = this.getContentResolver().query(ContentDescriptor.Restaurant.CONTENT_URI, null, null, null, null); ... } private void saveContent(){ ContentValues val = new ContentValues(); val.put(ContentDescriptor.Restaurant.Cols.NAME, (this.txtName.getText() != null) ? this.txtName.getText().toString() : null); val.put(ContentDescriptor.Restaurant.Cols.ADDRESS, (this.txtAddr.getText() != null) ? this.txtAddr.getText().toString() : null); val.put(ContentDescriptor.Restaurant.Cols.CITY, (this.txtCity.getText() != null) ? this.txtCity.getText().toString() : null); val.put(ContentDescriptor.Restaurant.Cols.STATE, (this.txtState.getText() != null) ? this.txtState.getText().toString() : null); val.put(ContentDescriptor.Restaurant.Cols.ZIP, (this.txtZip.getText() != null) ? this.txtZip.getText().toString() : null); contentResolver.insert(ContentDescriptor.Restaurant.CONTENT_URI, val); loadContent(); } }
What is going on…
- First, let me point out that the code used above to access data from the
Activity
class is not optimal for production. You should optimize any io-bound code that has propensity to hang the UI by using an asynchronous task. Nevertheless, the code presented above uses an instance ofContentResolver
to access data managed by the backing ContentProvider. TheContentResolver
uses the URI value passed in to select the proper ContentProvider registered in the AndroidManifest.xml file (not shown). -
The
saveContent()
shows how you can use the Descriptor class to create an instance of ContentValues (from the ContentProvider API) to save data. Each column is mapped to its value using the ContentProvider to provide the column name.
Conclusion
This write up provides a guide for those of you, brave enough, to use the ContentProvider API directly. I have introduced the Descriptor class as a container to register meta data to describe the data element captured by the ContentProvier. The hope is to make using the ContentProvider API more organized and provide some structure when putting your own data access code together.
