Chapter    25

Exploring Android Persistence and Content Providers

There are a number of ways of saving state in the Android SDK. Some of these are 1) shared preferences, 2) internal files, 3) external files, 4) SQLite, 5) content providers, 6) O/R mapping tools, and 7) network storage in the cloud. We will briefly introduce each of these state-saving options first and then cover in detail managing application state using SQLite and content providers.

Saving State Using Shared Preferences

We have covered shared preferences in Chapter 11. Shared preferences are key/value-based XML files owned by your application. Android has a framework on top of this general persistence mechanism to display/update/retrieve preferences without writing a lot of code. This latter aspect is the main topic of Chapter 11.

Chapter 11 also touched briefly on how an application can store any type of data using the shared preference API in XML files. In this approach data is converted to a string representation first and then stored in the preferences key/value store. This approach can be used to store any arbitrary state of your application as long as it is small to medium in size.

The shared preference XML files are internal to the application on your device. This data is not directly available to other applications. End user cannot directly manipulate this data by mounting on to a USB port. This data is removed automatically when the application is removed.

From simple to moderate application persistence needs, you can take advantage of shared preferences by storing various trees of Java objects directly in a shared preference file. In a given preference file you can have a key point to a serialized Java object tree. You can also use multiple preference files for multiple Java object trees. We have used JSON/GSON library from google to do this conversion from Java objects to their equivalent JSON string values quite effectively. In this approach a Java object tree is streamed as a JSON string using the google GSON library. This tree is then stored as a value in a key/value pair of a preference file. Keep in mind that GSON and JSON conversion of a Java object may have some limitations. Read the GSON/JSON documentation to see how complex a Java object can get to make this approach work. We are fairly confident that for most data-based Java objects this will work.

Listing 25-1 has some sample code for how to save a Java tree using GSON/JSON and shared preferences.

Listing 25-1. Saving a Java Object Tree Using JSON in Shared Preferences XML Files

//Implementation of storeJSON for storing any object
public void storeJSON(Context context, Object anyObject) {

    //Get a GSON instance
    Gson gson = new Gson();

    //Convert Java object to a JSON string
    String jsonString = gson.toJson(anyObject);

    //See Chapter 11 for more details on how to get a shared preferences reference
    String filename = "somefilename.xml";
    int mode = Context.MODE_PRIVATE;
    SharedPreferences sp = context.getSharedPreferences(filename,mode);

    //Save the JSON string in the shared preferences
    SharedPreferences.Editor spe = sp.edit();
    spe.putString("json", jsonString);
    spe.commit();
}
//This code can then be used by a client like this:
//Create any data object with reasonable complexity
//Ex:  MainObject mo = MainObject.createTestMainObject();
//You can then call storeJSON(some-activity, mo) below

Listing 25-2 shows some sample code for how to retrieve a Java tree using GSON/JSON and shared preferences.

Listing 25-2. Reading a Java Object Tree Using JSON from Shared Preferences XML Files

public Object retrieveJSON(Context context, String filename, Class classRef) {
    int mode = Context.MODE_PRIVATE;
    SharedPreferences sp = context.getSharedPreferences(filename,mode);
    String jsonString = sp.getString("json", null);
    if (jsonString == null)    {
        throw new RuntimeException("Not able to read the preference");
    }
    Gson gson = new Gson();
    return gson.fromJson(jsonString, classRef);
}

//You can then do this in the client code
MainObject mo = (MainObject)retrieveJSON(context,"somefilename.xml", MainObject.class);
String compareResult = MainObject.checkTestMainObject(mo);
if (compareResult != null)    {
       throw new RuntimeException("Something is wrong. Objects don't match");
}

This code requires that you have the GSON Java library added to your project. This GSON-based approach is covered in detail in our companion book, Expert Android from Apress. This is also briefly documented online at http://androidbook.com/item/4438.

Saving State Using Internal Files

In Android, you can also use internal files to store the state of your application. These internal files are internal to the application on your device. This data is not directly available to other applications. End user cannot directly manipulate this data by mounting on to a USB port. This data is removed automatically when the application is removed.

Listing 25-3 shows sample code for how to save a Java tree using GSON/JSON and internal files.

Listing 25-3. Reading/Writing JSON Strings from/to an Android Internal File

private Object readFromInternalFile(Context appContext, String filename, Class classRef)
throws Exception
{
    FileInputStream fis = null;
    try {
        fis = appContext.openFileInput(filename);
        //Read the following string from the filestream fis
        String jsonString;

        Gson gson = new Gson();
        return gson.fromJson(jsonString, classRef);
}
    finally {
        // write code to closeStreamSilently(fis);
    }
}
private void saveToInternalFile(Context appContext, String filename, Object anyObject){
    Gson gson = new Gson();
    String jsonString = gson.toJson(anyObject);

    FileOutputStream fos = null;
    try {
        fos = appContext.openFileOutput(filename
                                ,Context.MODE_PRIVATE);
        fos.write(jsonString.getBytes());
    }
    finally    {
        // closeStreamSilently(fos);
    }
}

This approach based on internal files and GSON is covered in detail in our companion book, Expert Android from Apress (http://www.apress.com/9781430249504). This is also briefly documented online at http://androidbook.com/item/4439.

Saving State Using External Files

In Android, external files are stored either on the SD card or on the device. These become public files that other apps including the user could see and read outside the context of your application. For many apps that want to manage their internal state these external files will unnecessarily pollute the public space.

Because the data you would represent as JSON is typically very specific to your application, it doesn’t make sense to make this available as external storage, which is typically used for music files, video files, or files that are commonly in a format that is understandable by other applications.

Because external storage such as an SD card can be in various states (available, not mounted, full, etc.), it is harder to program this for simple apps when the data is small enough. So we could not make a good case for now that the application state be maintained on external storage.

A hybrid approach may be meaningful if the application requires music and photos and those can go on the external storage while keeping the core state data in JSON and internal.

The android.os.Environment class and the android.content.Context class have a number of methods to read and write to external files and directories. We have not included code examples because the approach is very similar to internal files once you get access to these files through the andorid.content.Context.

Saving State Using SQLite

Android apps can use SQLite databases to store their state. SQLite is well integrated into the fabric of Android. If you want to store the internal state of your application robustly then this is probably the best approach. However, working with any relational database including SQLite has a lot of nuances. We will cover the essentials and nuances of using SQLite on Android a little later in the chapter.

Saving State Using O/R Mapping Libraries

O/R mapping stands for Object-to-Relational Mapping. A key difficulty with storing state in a relational database from a programming language like Java is the mismatch between Java object structures and relational structures of the database. One needs to map between the names, types, and relationships of fields as they are in the Java space and their equivalents in the database space. This mapping is error prone. You will see this when we cover the SQLite in detail later.

There is a need for simplifying this mapping of data between Java and SQL. This space in the industry is called O/R mapping. A few tools are now available to solve this in Android. It is beyond the scope of this book to cover the essentials of these O/R mapping tools. But we will name a couple of these tools and give their online references now.

Two key tools in this space are GreenDAO (http://greendao-orm.com/) and OrmLite (http://ormlite.com/). There are more appearing every year. So check often to see if the new ones are faster or easier. GreenDAO uses a code generation approach based on schema definitions. It is said to be three to four times faster than OrmLite. OrmLite fuses the schema definition with Java classes through annotations. The latter approach is easier programmatically. Also OrmLite works the same on any Java platform. However, possibly due to reflection used at run time, it can be slower, but I suspect is fast enough for most applications.

We predict that using one of these O/R mapping libraries is a key need to get your apps faster to the market. We recommend that you isolate the persistence services and start with OrmLite and then move to GreenDAO if your app gains enough traction or for moving to production from your prototype.

Saving State Using Content Providers

Android provides a higher-level abstraction on top of data stores based on URIs. Using this approach any application can read or store data using REST like URIs. This abstraction also allows applications to share their data through APIs based on URI strings. In this approach submitting a URI will give back a collection of rows and columns in a database cursor. A URI can also take a set of key/value pairs and persist them in a target database if permissions are granted. This is a general-purpose mechanism for interoperability of data between Android applications. We will cover this in greater detail later in the chapter. This is a preferred mechanism if your application has data that is valuable to be shared, created, or manipulated by other applications. For example many applications that deal with notes, documents, audio, or video implement their data as content providers. This is also the case with most of Android’s core data-related services.

Saving State Using Network Storage

Network storage comes into play when the data created or used by an application needs to be shared via a network by other users on either the same platform or different platforms like in a collaborative application. This back-end service facility utilized by mobile application is being called MBaaS (Mobile Back-end As A Service). Parse.com is an example of a MBAAS that provides back-end services such as user management, user logins, security, social, common network storage, server side business logic, and notifications.

Android also natively uses a concept called sync adapters to transfer data between the device and network servers. You can read more on sync adapters at http://developer.android.com/training/sync-adapters/index.html. This is a framework that uses asynchronous callbacks to optimize transfer of arbitrary amounts of data efficiently by scheduling and executing it at the most opportune moment. The framework sweats the detail and developers just provide the transfer code.

That concludes the overview of various ways to save state for Android mobile applications. We will cover now two of those approaches in detail: SQLite and content providers. We will start with the Android SQLite API.

Storing Data Directly Using SQLite

In this section we will explore in detail how to use SQLite effectively to manage Android application state. You will understand the extent of SQLite support in Android. We will show you the essential code snippets. We will show you best practices for using SQLite on Android. We will show you how best to load DDLs to create your database. We will show you a cleaner architectural pattern to abstract persistence services. We will show how to apply transactions through dynamic proxies. This section is a robust treatment of using SQLite on Android. We also have a sample program that you can download to see the complete working implementation. Let’s start with a quick overview of SQLite packages and classes in Android.

Summarizing Key SQLite Packages and Classes

Android supports SQLite through its Java package android.database.sqlite. Some of the key classes you will need to understand for effectively using the Android SQLite API are listed in Listing 25-4. Note that some of the classes are outside the android.database.sqlite package.

Listing 25-4. Key SQLite Java Classes in the Android SDK

android.database.sqlite.SQLiteDatabase
android.database.sqlite.SQLiteCursor
android.database.sqlite.SQLiteQueryBuilder
android.content.ContentValues
android.database.Cursor
android.database.SQLException
android.database.sqlite.SQLiteOpenHelper

Let’s talk about each of these packages and classes briefly.

SQLiteDatabase: SQLiteDatabase is a Java class that represents the database usually referring to a “.db” file on the file system. Using this object you can query, insert, update, or delete for a given table in that database. You can also execute a single arbitrary SQL statement. You can apply transactions. You can also use this object to define tables through DDLs (Data Definition Language). DDLs are statements that let you create database entities such as tables, views, indexes, etc. Typically there is a single instance of this object in your application representing your database.

SQLiteCursor: This Java class represents a collection of rows that are returned from an SQLiteDatabase. It also implements the android.database.Cursor interface. This object has methods to navigate the rows one at a time like a forward database cursor and retrieving the rows only as needed. This object can also jump forward or backward if needed like a random cursor by implementing windowing qualities. This is also the object you will use to read the column values for any current row.

SQLiteQueryBuilder: This is a helper Java class to construct an SQLite query string by incrementally specifying table names, column names, where clause, etc., as separate fields. This class has a number of set methods to gradually build up the query as opposed to specifying the entire SQL Query as a string. You can also use the query methods directly on the SQLiteDatabase class if your query is simple.

ContentValues: A Java object of this class holds a set of key/value pairs that are used by a number of SQLite classes to insert or update a row of data.

SQLException: Most Android SQLite database APIs throw this exception when there are errors.

SQLiteOpenHelper: This helper Java object provides access to an SQLiteDatabase by examining a few things: given a filename of the database, this object checks to see if that database is already installed and available. If it is available it checks to see if the version is the same. If the version is also the same it provides a reference to the SQLiteDatabase representing that database. If the version is different it provides a callback to migrate the database prior to providing a valid reference to the database. If the database file doesn’t exist then it provides a callback to create and populate the database. You will extend this base class and provide implementations to these various callbacks. You will see this shortly in the provided code snippets.

That is a quick summary of the key classes you use to save state of your application in an SQLite database. Let us now turn to key concepts in using SQLite for managing application state. Let’s start with creating a database.

Creating an SQLite Database

Creation of a database in Android is controlled through the SQLiteOpenHelper class. For each database in your application you will have a Java database object that is an instance of this class. This SQLiteOpenHelper object has a pair of get methods to get a reference to the read-optimized (configured for) or write-optimized (configured for) SQLiteDatabase object. Creating or getting access to your SQLite database object involves the following:

  1. Extending SQLiteOpenHelper and supplying the database name and version to the constructor of this derived class so that those values can be passed to the base class
  2. Overriding the onCreate(), onUpgrade(), and onDowngrade() methods from SQLiteOpenHelper. You get a call to onCreate() if this database is not there. You get a call to onUpgrade() if the version of the database is newer, and a call to onDowngrade() if the version of the database is older from the one that is on the device. You will use execute DDL statements in these methods to create or adjust your database. If your database is not new or has the same version, then neither of these callbacks will be invoked.
  3. Have a single static reference to this derived object. Call get methods on this object to get a reference to a copy of readable or writable database. Use these database references to perform CRUD operations and transactions.

Listing 25-5 is a code snippet that demonstrates how these steps are implemented in creating a database called “booksqlite.db”, a database to hold a single table of books and their detail.

Listing 25-5. Using SQLiteOpenHelper

// File reference in project: DirectAccessBookDBHelper.Java
/**
* A complete example of SQLiteOpenHelper demonstrating
* 1. How to create a databases
* 2. How to migrate a database
* 3. How to hold a static reference
* 4. How to give out read and write database references
*
* This class also can act as a DatabaseContext.IFactory to produce read and write
* database references. This aspect is not critical to understanding but included
* for advanced readers and for some material later in the chapter.
*/
public class DirectAccessBookDBHelper extends SQLiteOpenHelper
implements DatabaseContext.IFactory
{
    //there is one and only one of these database helpers
    //for this database for this entire application
    public static DirectAccessBookDBHelper m_self =
       new DirectAccessBookDBHelper(MyApplication.m_appContext);

    //Name of the database on the device
    private static final String DATABASE_NAME = "bookSQLite.db";

    //Name of the DDL file you want to load while creating a database
    private static final String CREATE_DATABASE_FILENAME = "create-book-db.sql";

    //Current version number of the database for the App to work
    private static final int DATABASE_VERSION = 1;

    //Just a logging tag
    private static final String TAG = "DirectAccessBookDBHelper";

    //Pass the database name and version to the base class
    //This is a non public constructor
    //Clients can just use m_self and not construct this object at all directly
    DirectAccessBookDBHelper(Context context) {
        super(context,DATABASE_NAME,null,DATABASE_VERSION);
        //Initialize anything else in your system that may need a
        //reference to this object.
        //Example: DatabaseContext.initialize(this);
    }
    @Override
    public void onCreate(SQLiteDatabase db)  {
       try {
          //No database exists. Load DDL from a file in the assets directory
          loadSQLFrom(this.CREATE_DATABASE_FILENAME,db);
       }
       catch(Throwable t)       {
          //Problem creating database
          throw new RuntimeException(t);
       }
    }
    //A function to load one SQL statement at a time using execSQL method
    private void loadSQLFrom(String assetFilename, SQLiteDatabase db)    {
       List<String> statements = getDDLStatementsFrom(assetFilename);
       for(String stmt: statements){
          Log.d(TAG,"Executing Statement:" + stmt);
          db.execSQL(stmt);
       }
    }
    //Optimize this function for robustness.
    //For now it assumes there are no comments in the file
    //the statements are separated by a semicolon
    private List<String> getDDLStatementsFrom(String assetFilename)  {
       ArrayList<String> l = new ArrayList<String>();
       String s = getStringFromAssetFile(assetFilename);
       for (String stmt: s.split(";"))   {
          //Add the stmt if it is a valid statement
          if (isValid(stmt)) {
             l.add(stmt);
          }
       }
       return l;
    }
    private boolean isValid(String s)    {
        //write logic here to see if it is null, empty etc.
        return true; //for now
    }
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {
       //Use old and new version numbers to run DDL statements
       //to upgrade the database
    }
    //Using your specific application object to remember the application context
    //Then using that application context to read assets
    private String getStringFromAssetFile(String filename)   {
       Context ctx = MyApplication.m_appContext;
       if ( ctx == null)   {
          throw new RuntimeException("Sorry your app context is null");
       }
       try   {
         AssetManager am = ctx.getAssets();
         InputStream is = am.open(filename);
         String s = convertStreamToString(is);
         is.close();
         return s;
       }
       catch (IOException x)    {
          throw new RuntimeException("Sorry not able to read filename:" + filename,x);
       }
    }
    //Optimize later. This may not be an efficient read
    private String convertStreamToString(InputStream is)  throws IOException   {
       ByteArrayOutputStream baos = new ByteArrayOutputStream();
       int i = is.read();
       while (i != -1)  {
          baos.write(i);
          i = is.read();
       }
       return baos.toString();
    }
   //Here are some examples of how to get access to readable and
   //writable databases. These methods will make sense once we get through the
   //the transactions applied through dynamic proxies
   /*
   public ReadDatabaseContext createReadableDatabase() {
      return new ReadDatabaseContext(this.getReadableDatabase());
   }
   public WriteDatabaseContext createWritableDatabase() {
      return new WriteDatabaseContext(this.getWritableDatabase());
   }
   */
}//eof-class DatabaseHelper
//Here is the code for MyApplication to remember the context
public class MyApplication extends Application {
   public final static String tag="MyApplication";
   public static volatile Context m_appContext = null;

   @Override
   public void onCreate() {
       super.onCreate();
          MyApplication.m_appContext = this.getApplicationContext();
   }
}
//assets/create-book-db.sql
CREATE TABLE t_books (id INTEGER PRIMARY KEY,
                name TEXT,  isbn TEXT, author TEXT,
                created_on INTEGER, created_by TEXT,
                last_updated_on INTEGER, last_updated_by TEXT
);

Defining a Database Through DDLs

In Listing 25-5, the DirectAccessBookDBHelper is a derived class of SQLiteOpenHelper that allows us to examine an existing database and see if it needs to be created or just migrated based on its version.

The method onCreate()is called only if this database does not exist on the device. Without the SQLiteOpenHelper we would have had to examine the physical location of this file and see if it exists. In other words SQLiteOpenHelper is really a thin wrapper that is saving us a number of “if-else” clauses to examine the database and do the necessary initialization: be it creating it or migrating it.

A number of examples for creating an Android database on the Internet use embedded DDL strings in Java code to create the tables needed. As DDL statements, strings in Java code are difficult to read and error prone. A better approach is to put these database creation scripts in a text file in the assets directory. Sample code in Listing 25-5 demonstrates how to read a text file from an assets directory of your application and use the execSQL() function available on the SQLiteDatabase to initialize the database.

A limitation of execSQL() is it can execute only one SQL statement at a time. That is why the code in Listing 25-5 reads the script file and parses it into a series of statements using a simple syntax. You may want to scour the Internet to see better parsing utilities to allow a better script file support. Another alternative, if it works for your case, is to have a schema class whose sole purpose is contain static public strings for your DDL as it alleviates the need for parsing files. We have links to some of these Java-based libraries in the online references provided at the end of this chapter. Especially, Java-based tools using ANTLR have a lot of promise for complex database setups.

The onCreate() function also wraps its execution in a transaction so that the executed database is consistent.

If you have a lot of scripts it is also possible to create the database entirely and keep it in the assets folder. During deployment if the database doesn’t exist you can just copy the file to its target location.

Migrating a Database

As stated the SQLiteOpenHelper recognizes version numbers and appropriately calls the onUpgrade() method to upgrade the database. Here also you may want to keep series of scripts in the assets folder that can alter the database appropriately depending on the differences in the version numbers. Keep in mind that the version number on the device may be smaller or larger than your target version. So you may need a set of scripts that are unique to each conversion sequence: Going from V1 to V3 or from V2 to V3 or V3 to V1. Going backwards may require either warnings or dynamic downloading of server-side conversions to an older version, as the source code of an older version of the app is unlikely to have the needed utilities to step down from a future version.

Inserting Rows

At its core, inserting a row with its column values into SQLiteDatabase is merely calling the insert-related methods on the SQLiteDatabase object. Pseudocode that explains this is shown in Listing 25-6.

Listing 25-6. Basics of Inserting a Row Using SQLiteDatabase

//Get a reference to the database object
//Depending on the framework you have there could be many ways of doing this
SQLiteDatatabase db = DirectAccessBookDBHelper.m_self.getReadableDatabase();
String tablename; //which table you want to insert the row into

//populate a structure with the needed columns and their values
ContentValues cv = new ContentValues();
cv.put(columnName1, column1Value); //etc.

//A column that could be null if 'cv' is empty if an empty row is needed
//Provide null if that behavior is not needed
String nullColumnName = null;

//Insert the row
long rowId = db.insertOrThrow(tablename, nullColumnName, cv);

This code is really simple. Inserting any Java object using this code is merely reading its attributes and putting those values into the ContentValues data set and just insert. As far as Android’s SQLite insert capabilities are concerned, that is all you need to know.

How best to structure to get to your Java objects and how to convert those values into the content values depends on your framework. This is a tedious process to do correctly. But again, this detail is not essential for the basic understanding of insert. You will need this level of rigor for most of your applications. You can skip this if you think this is complicated, but we are including it here as we feel you will need this level of rigor for most of your applications.

So getting the right column names and values for inserting rows requires some work and you typically need the following (irrespective of the framework you use):

  1. A Java object that typically represents the row in a database, for example, a Book object
  2. A table name to hold a set of books
  3. String names for the columns available in the Books table
  4. Finally, calling the insert method to persist the Book object as a row in the Books table

We will give code snippets (some in pseudocode fashion) for each of these needs. For actual code you can download the project for this chapter. Here are a couple of classes in Listing 25-7 that represent a Book object in Java code.

Listing 25-7. Ensuring Minimal Dependency Between Domain Objects and Persistence

// File reference in project: BaseEntity.Java
public class BaseEntity {
   private int id; //database identifier

   private String ownedAccount = null; //Multi-tenant if needed
   private String createdBy;
   private Date createdOn;
   private String lastUpdatedBy;
   private Date lastUpdatedOn;

   public BaseEntity(String ownedAccount, String createdBy, Date createdOn,
         String lastUpdatedBy, Date lastUpdatedOn, int id) {
      super();
      this.ownedAccount = ownedAccount;
      this.createdBy = createdBy;
      this.createdOn = createdOn;
      this.lastUpdatedBy = lastUpdatedBy;
      this.lastUpdatedOn = lastUpdatedOn;
      this.id = id;
   }
   //For persistence
   public BaseEntity(){}

   //Usual generated get/set methods
   //eliminated here for space. See the downloads
}
// File reference in project: Book.Java
public class Book extends BaseEntity
{
   //Key data fields
   //*************************************
   private String name;
   private String author;
   private String isbn;
   //*************************************

   public Book(String ownedAccount, String createdBy, Date createdOn,
         String lastUpdatedBy, Date lastUpdatedOn, String name,
         String author, String isbn) {
      super(ownedAccount, createdBy, createdOn, lastUpdatedBy, lastUpdatedOn,-1);
      this.name = name;
      this.author = author;
      this.isbn = isbn;
   }
   //To help with persistence
   public Book() {}
   //Generated methods get and set methods...
   //....
   //The following method is here for testing purposes
   //and also to see how a book object is typically created
   public static Book createAMockBook()  {
      String ownedAccount = "Account1";
      String createdBy = "satya";
      Date createdOn = Calendar.getInstance().getTime();
      String lastUpdatedBy = "satya";
      Date lastUpdatedOn = Calendar.getInstance().getTime();

      //See how many books I have and increment it by one
      //The following method returns a collection of books in the database
      //This is not essential for your understanding here
      //You will see this clarified when you read the section of transactions
      List<Book> books = Services.PersistenceServices.bookps.getAllBooks();
      int i = books.size();
      String name = String.format("Book %s",i);
      String author = "satya";
      String isbn   = "isbn-12344-" + i;

      return new Book(ownedAccount,createdBy,createdOn,
            lastUpdatedBy,lastUpdatedOn,
            name,author,isbn);
   }
}

This listing has two Java classes: a BaseEntity and a Book that extends the BaseEntity. Objects that look like a Book in Listing 25-7 are called domain objects. These are pure Java objects that can move around in the Java space of your program without being burdened by their behavior relating to persistence. However, who created these objects, when they were created, and such attributes are encapsulated in the BaseEntity so that all domain objects have this basic information.

Because the SQLite database methods require explicit column names for these objects that aspect is defined in a separate set of classes that describe the metadata for these objects. These supporting classes are given in Listing 25-8.

Listing 25-8. Defining Metadata for Domain Objects

// File reference in project: BaseEntitySQLiteSQLiteMetaData.Java
public class BaseEntitySQLiteSQLiteMetaData  {
   static public final String OWNED_ACCOUNT_COLNAME = "owned_account";
   static public final String CREATED_BY_COLNAME = "created_by";
   static public final String CREATED_ON_COLNAME = "created_on";
   static public final String LAST_UPDATED_ON = "last_updated_on";
   static public final String LAST_UPDATED_BY = "last_updated_by";
   static public final String ID_COLNAME = "id";
}
// File reference in project: BookSQLiteSQLiteMetaData.Java
public class BookSQLiteSQLiteMetaData extends BaseEntitySQLiteSQLiteMetaData {
   static public final String TABLE_NAME = "t_books";
   static public final String NAME = "name";
   static public final String AUTHOR = "author";
   static public final String ISBN = "isbn";
}

These two classes parallel their respective BaseEntity and Book object classes. You have to pay attention that the column names match to those in the database. So this need is fundamentally error prone. Unless you use an O/R mapping library and craft one of your own, this issue will remain and you have to test well. It is defining these classes explicitly by the programmer that is eliminated in the O/R mapping tools that we discussed earlier.

Now that we have a Java class to represent a book and its metadata definition, that tells us the table name and the fields we can proceed to write the Java code to save a book object in the database, as shown in Listing 25-9 (note that this is still pseudocode and use the download to see any missing details).

Listing 25-9. Using Android SQLite APIs to Insert a Row

// File reference in project: BookPSSQLite.Java
private long createBook(Book book) {
   //Get access to a read database
   SQLiteDatabase db = DirectAccessBookDBHelper.m_self.getWritableDatabase();

   //Fill fields from the book object into the content values
   ContentValues bcv = new ContentValues();
   //.... fill other fields example
   bcv.put(BookSQLiteSQLiteMetaData.NAME, book.getName());
   bcv.put(BookSQLiteSQLiteMetaData.ISBN, book.getIsbn());
   bcv.put(BookSQLiteSQLiteMetaData.AUTHOR, book.getAuthor());
   //.... fill other fields

   //if bcv is an empty set, then an empty row can possibly be inserted.
   //It is not the case for our book table. If it were though, the empty bcv
   //will result in an insert statement with no column names in it.
   //At least one column name is needed by SQL insert syntax.
   //It is one of these column names that goes below. For us this is not case so a null
   String nullColumnName = null;

    long rowId = db.insertOrThrow(BookSQLiteSQLiteMetaData.TABLE_NAME,
                      nullColumnName,
                      bcv);
    return rowId;
}

The logic in Listing 25-9 is quite simple. Get a reference to a Book object we want to save. Copy the field values from the book into a ContentValues key/value pair object. Use metadata classes to define the field names correctly. Use the filled-in ContentValues object and call the insert method. If we don’t do anything, the insert is encapsulated in an auto commit. We will talk about how to do transactions shortly, as the theory of it is a bit involved although the code is quite simple to write. The insert method returns the newly inserted primary key ID for this table. This convention of returning the primary key of the table comes from the underlying SQLite product documentation and is not Android specific.

The nullColumnName is related to the syntax of the SQL insert statement. If the row has ten columns but only two columns and their non-null values are indicated, then a new row is inserted with those two columns, and it is expected that the remaining eight columns will allow nulls. If you want a row with every column as null it is possible to issue an insert statement with no column names at all, matching the empty content values set. However, an insert statement with no column names is not allowed. So this parameter nullColumnName can contain one of the column names that could be null so that the insert statement syntax requirement is satisfied. The rest of the columns will be made null by the database internally when this row is inserted. Usually this column name is passed in as null because it is rare that we want to insert a row where every column is empty or null.

Updating Rows

Listing 25-10 is a sample pseudocode snippet (for full code see the download project) to show how to update a row in the database. Notice how Book object and BookSQLiteMetaData classes are used to minimize errors in specifying table names and column names. The approach is similar to the insert method.

Listing 25-10. Android SQLite API to Update a Record

// File reference in project: BookPSSQLite.Java
public void updateBook(Book book) {
   if (book.getId() < 0) {
      throw new SQLException("Book id is less than 0");
   }
   //Get access to a read database
   SQLiteDatabase db = DirectAccessBookDBHelper.m_self.getWritableDatabase();

   //Fill fields from the book object into the content values
   ContentValues bcv = new ContentValues();
   //.... fill other fields
   bcv.put(BookSQLiteSQLiteMetaData.NAME, book.getName());
   bcv.put(BookSQLiteSQLiteMetaData.ISBN, book.getIsbn());
   bcv.put(BookSQLiteSQLiteMetaData.AUTHOR, book.getAuthor());
   //.... fill other fields

   //You can do this
   String whereClause = String.format("%s = %s",BookSQLiteSQLiteMetaData.ID_COLNAME,book.getId());
   String whereClauseArgs = null;
   //Or the next 4 lines (this is preferred)
   String whereClause2 = BookSQLiteSQLiteMetaData.ID_COLNAME + " = ?";
   String[] whereClause2Args = new String[1];
   whereClause2Args[1] = Integer.toString(book.getId());

   int count = db.update(BookSQLiteSQLiteMetaData.TABLE_NAME, bcv, whereClause2, whereClause2Args);
   if (count == 0)   {
      throw new SQLException(
            String.format("Failed to update book for book id:%s",book.getId()));
   }
}

Deleting Rows

Listing 25-11 is an example of how to delete a row from the database.

Listing 25-11. Android SQLite API to Delete a Record

// File reference in project: BookPSSQLite.Java
public void deleteBook(int bookid){
   //Get access to a writable database
   SQLiteDatabase db = DirectAccessBookDBHelper.m_self.getWritableDatabase();

   String tname = BookSQLiteSQLiteMetaData.TABLE_NAME;
   String whereClause =
      String.format("%s = %s;",
         BookSQLiteSQLiteMetaData.ID_COLNAME,
         bookid);
   String[] whereClauseargs = null;
   int i = db.delete(tname,whereClause, whereClauseargs);
   if (i != 1)   {
      throw new RuntimeException("The number of deleted books is not 1 but:" + i);
   }
}

Reading Rows

Listing 25-12 shows pseudocode snippet (for full code see the download project) to read from SQLite using the SQLiteDatabase.query() method. This method returns a Cursor object, which you can use to retrieve each row.

Listing 25-12. Android SQLite API to Read Records

// File reference in project: BookPSSQLite.Java
public List<Book> getAllBooks()   {
   //Get access to a read database
   SQLiteDatabase db = DirectAccessBookDBHelper.m_self.getReadableDatabase();

   String tname = BookSQLiteSQLiteMetaData.TABLE_NAME;
   //Get column name array from the metadata class
   //(See the download how the column names are gathered)
   //(at the end of the day it is just a set of column names
   String[] colnames = BookSQLiteSQLiteMetaData.s_self.getColumnNames();

   //Selection
   String selection = null;       //all rows. Usually a where clause. exclude where part
   String[] selectionArgs = null; //use ?s if you need it

   String groupBy = null;     //sql group by clause: exclude group by part
   String having = null;      //similar
   String orderby = null;
   String limitClause = null; //max number of rows
   //db.query(tname, colnames)
   Cursor c = null;

   try {
      c = db.query(tname,colnames,selection,selectionArgs,groupBy,having,orderby,limitClause);
      //This may not be the optimal way to read data through a list
      //Directly pass the cursor back if your intent is to read these one row at a time
      List<Book> bookList = new ArrayList<Book>();
      for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
         Log.d(tag,"There are books");
         Book b = new Book();

         //..fill base fields the same way
         b.setName(c.getString(c.getColumnIndex(BookSQLiteMetaData.NAME)));
         b.setAuthor(c.getString(c.getColumnIndex(BookSQLiteMetaData.AUTHOR)));
         b.setIsbn(c.getString(c.getColumnIndex(BookSQLiteMetaData.ISBN)));
         //..fill other fields

         //Or you could delegate this work to the BookSQLiteMetaData object
         //as we have done in the sample downloadable project
         //Ex: BookSQLiteSQLiteMetaData.s_self.fillFields(c,b);

         bookList.add(b);
      }
      return bookList;
   }
   finally {
      if (c!= null) c.close();
   }
}

Here are a few facts about an Android cursor object:

  • A cursor is a collection of rows.
  • You need to use moveToFirst() before reading any data because the cursor starts off positioned before the first row.
  • You need to know the column names.
  • You need to know the column types.
  • All field-access methods are based on column number, so you must convert the column name to a column number first. Note that this lookup can be optimized. It’s more efficient to populate the column name array in order if you wish to fetch the values and then use explicit constant indices on the cursor.
  • The cursor is random (you can move forward and backward, and you can jump).
  • Because the cursor is random, you can ask it for a row count.

Applying Transactions

SQLite libraries on Android support transactions. The transaction methods are available on the SQLiteDatabase class. These methods are shown in pseudocode snippet (for full code see the download project) in Listing 25-13.

Listing 25-13. SQLite API for Transactions

// File reference in project: DBServicesProxyHandler.Java
public void doSomeUpdates() {
  SQLiteDatabase db;  //Get a reference to this database through helper
  db.beginTransaction();
  try {
    //...call a number of database methods
    db.setTransactionSuccessful();
  }
  finally {
    db.endTransaction();
  }
}

Summarizing SQLite

If you are a Java programmer with a few years of experience, what we have covered so far is sufficient to understand the SQLite API in Android. With the material covered so far, you know how to check for a database, create a database through DDL, insert rows, update rows, delete rows, or read using database cursors. We have also showed you the basic API for transactions. However, if you are not an experienced hand at Java, database transactions are tricky to implement correctly and efficiently. The next section will tell you an API-based pattern using Java dynamic proxies.

Doing Transactions Through Dynamic Proxies

You can visualize your mobile application as a collection of two bricks: An API brick and a UI brick. The API brick will have a series of stateless methods that provide logic and data to the UI brick. In this context the method in Listing 25-13 doSomeUpdates() is considered a reusable API by many parts of the UI or by other APIs. Because it is a reusable API the client decides whether something should be committed or not committed in that transaction. This means the API should not be dealing with transactions most of the time. It is very much like a stored procedure in a relational database. A stored procedure rarely does transactions directly. The container of the stored procedure decides to commit or not commit external to the stored procedure. The logic is this: if the stored procedure is invoked by itself then its output is committed at the stored procedure level. If the stored procedure is called by another stored procedure the commit waits until the main invoking stored procedure is complete.

It is better to use the same strategy for these APIs in your application to reduce the complexity in implementing the APIs. This is done by intercepting calls to all the APIs to make a determination if this is a direct call or being called by another API that is already being monitored for a transaction. There are a number of ways to intercept the API calls that need to be intercepted. This is also sometimes called Aspect-Oriented Programming or AOP. AOP needs sophisticated tooling to do. Java provides a less sophisticated but straightforward way to do this through dynamic proxies. A dynamic proxy is a facility in Java, based on Java reflection, that allows you to intercept calls to an underlying object without the object being aware of it. When a client calls the object through this proxy, the client thinks it is talking to the object directly. However, the proxy can choose to apply other aspects (like security, logging, transactions, etc.) before sending the call to the real object. The included project for this chapter provides a full implementation of a dynamic proxy that automatically applies the transactional aspects.

We will show you first what your API implementations look like once a dynamic proxy is in place. This will give you an idea of the simplicity of this approach to transactions first. Then you can see if you want to take this route and use dynamic proxies. As we present the code below note that we will be including only snippets or samples and not the entire code. Use the downloadable project for full details. We have annotated the download project with a lot of comments to help your understanding. With that caveat consider the API to work with Book-based objects.

Listing 25-14. API-Based Interfaces for Working with the Book Domain Object

// File reference in project: IBookPS.Java
public interface IBookPS {
   public int saveBook(Book book);
   public Book getBook(int bookid);
   public void updateBook(Book book);
   public void deleteBook(int bookid);
   public List<Book> getAllBooks();
}

This interface defines operations using Java-based objects. The letters “PS” at the end of IBookPS service indicates that this is a persistence service API for a book. Listing 25-15 shows an SQLite implementation for the IBookPS

Listing 25-15. Implementing the Book APIs Using SQLite

// File reference in project: BookPSSQLite.Java
// The missing classes in this code are in the download and not essential for
// exploring the idea.
// ASQLitePS is a class that contains reusable common methods like getting access
// to the read and write databases using the singleton database helper.
public class BookPSSQLite extends ASQLitePS implements IBookPS {
   private static String tag = "BookPSSQLite";
   @Override public int saveBook(Book book)   {
      //get the database
      //case: id does not exist in the book object
      if (book.getId() == -1)  {
         //id of the book doesn't exist so create it
         return (int)createBook(book);
      }
      //case: id exists in book object
      updateBook(book);
      return book.getId();
   }
   @Override public void deleteBook(int bookid){
      SQLiteDatabase db = getWriteDb();
      String tname = BookSQLiteSQLiteMetaData.TABLE_NAME;
      String whereClause =
         String.format("%s = %s;",
            BookSQLiteSQLiteMetaData.ID_COLNAME,
            bookid);
      String[] whereClauseargs = null;
      int i = db.delete(tname,whereClause, whereClauseargs);
      if (i != 1)  {
         throw new RuntimeException("The number of deleted books is not 1 but:" + i);
      }
   }
   private long createBook(Book book)   {
      //book doesn't exist
      //create it
      SQLiteDatabase db = getWriteDb();

      ContentValues bcv = this.getBookAsContentValuesForCreate(book);

      //I don't need to insert an empty row
      //usually any nullable column name goes here if I want to insert an empty row.
      String nullColumnNameHack = null;
      //Construct values from the Book object. SQLException is a runtime exception
      long rowId = db.insertOrThrow(BookSQLiteMetaData.TABLE_NAME, nullColumnNameHack, bcv);
      return rowId;
   }
   @Override  public void updateBook(Book book) {
      if (book.getId() < 0) {
         throw new SQLException("Book id is less than 0");
      }
      SQLiteDatabase db = getWriteDb();
      ContentValues bcv = this.getBookAsContentValuesForUpdate(book);
      String whereClause = String.format("%s = %s",BookSQLiteMetaData.ID_COLNAME,book.getId());
      whereArgs[0] = BookSQLiteMetaData.ID_COLNAME;
      whereArgs[1] = Integer.toString(book.getId());

      int count = db.update(BookSQLiteMetaData.TABLE_NAME, bcv, whereClause, null);
      if (count == 0)   {
         throw new SQLException(
               String.format("Failed to update book for book id:%s",book.getId()));
      }
   }
    private ContentValues getBookAsContentValuesForUpdate(Book book)  {
      ContentValues cv = new ContentValues();
      //Following code loads column values from book object to the cv
      //See the downloadable project for the mechanics of it
      BookSQLiteMetaData.s_self.fillUpdatableColumnValues(cv, book);
      return cv;
    }
    private ContentValues getBookAsContentValuesForCreate(Book book)  {
      ContentValues cv = new ContentValues();
      BookSQLiteMetaData.s_self.fillAllColumnValues(cv, book);
      return cv;
    }
    @Override   public List<Book> getAllBooks() {
      SQLiteDatabase db = getReadDb();
      String tname = BookSQLiteMetaData.TABLE_NAME;
      String[] colnames = BookSQLiteMetaData.s_self.getColumnNames();

      //Selection
      String selection = null; //all rows. Usually a where clause. exclude where part
      String[] selectionArgs = null; //use ?s if you need it

      String groupBy = null; //sql group by clause: exclude group by part
      String having = null; //similar
      String orderby = null;
      String limitClause = null; //max number of rows
      //db.query(tname, colnames)
      Cursor c = null;

      try {
         c = db.query(tname,colnames,selection,selectionArgs,groupBy,having,orderby,limitClause);
         //This may not be the optimal way to read data through a list
         //Directly pass the cursor back if your intent is to read these one row at a time
         List<Book> bookList = new ArrayList<Book>();
         for(c.moveToFirst();!c.isAfterLast();c.moveToNext())
         {
            Log.d(tag,"There are books");
            Book b = new Book();
            BookSQLiteMetaData.s_self.fillFields(c,b);
            bookList.add(b);
         }
         return bookList;
      }
      finally {
         if (c!= null) c.close();
      }
   }
   @Override   public Book getBook(int bookid) {
      SQLiteDatabase db = getReadDb();
      String tname = BookSQLiteMetaData.TABLE_NAME;
      String[] colnames = BookSQLiteMetaData.s_self.getColumnNames();

      //Selection
      String selection =
         String.format("%s = %s",
               BookSQLiteMetaData.ID_COLNAME,
               bookid);
      //all rows. Usually a where clause. exclude where part
      String[] selectionArgs = null; //use ?s if you need it

      String groupBy = null; //sql group by clause: exclude group by part
      String having = null; //similar
      String orderby = null;
      String limitClause = null; //max number of rows
      //db.query(tname, colnames)
      Cursor c = db.query(tname,colnames,selection,
                      selectionArgs,groupBy,having,orderby,limitClause);
      try    {
         if (c.isAfterLast()) {
            Log.d(tag,"No rows for id" + bookid);
            return null;
         }
         Book b = new Book();
         BookSQLiteMetaData.s_self.fillFields(c, b);
         return b;
      }
      finally {
         c.close();
      }
   }
}//eof-class

Notice how the implementation of the Book persistence API does not directly deal with the transactional aspects of these methods. Instead, the transactions are handled by Java dynamic proxy, which we will show shortly. Listing 25-16 shows how a client can see these APIs and invoke these persistence APIs indirectly (again, please refer to the download projects for classes that are referenced in this code but not listed here as they are not essential for understanding).

Listing 25-16. Client Access to API-Based Services

// File reference in project: SQLitePersistenceTester.Java
// BaseTester is just a helper class to provider common functionality
// it implements some logging and report back methods to the UI activity
public class SQLitePersistenceTester extends BaseTester {
   private static String tag = "SQLitePersistenceTester";
   //Services is a static class that provides access to persistence services
   //Services class provides visibility to the implementer of the IBookPS
   //It demonstrates how a client gets access to the namespace of services
   //You will shortly see what this class is. Understand the intent first.
   private IBookPS bookPersistenceService = Services.PersistenceServices.bookps;
   //IReportBack is a logging interface to report loggable events back to the UI
   //UI will then choose to log those events and also show on the activity screen.
   SQLitePersistenceTester(Context ctx, IReportBack target) {
      super(ctx, target,tag);
   }

   //Add a book whose id is one larger than the books
   //in the database
   public void addBook()  {
      Book book = Book.createAMockBook();
      int bookid = bookPersistenceService.saveBook(book);
      reportString(String.format("Inserted a book %s whose generated id now is %s"
            ,book.getName()
            ,bookid));
   }
   //Delete the last book
   public void removeBook()  {
      List<Book> bookList = bookPersistenceService.getAllBooks();
      if( bookList.size() <= 0)
      {
         reportString("There are no books that can be deleted");
         return;
      }
      reportString(String.format("There are %s books. First one will be deleted", bookList.size()));

      Book b = bookList.get(0);
      bookPersistenceService.deleteBook(b.getId());
      reportString(String.format("Book with id:%s successfully deleted", b.getId()));
   }

   //write the list of books so far to the screen
   public void showBooks()  {
      List<Book> bookList = bookPersistenceService.getAllBooks();
      reportString(String.format("Number of books:%s", bookList.size()));
      for(Book b: bookList) {
         reportString(String.format("id:%s name:%s author:%s isbn:%s"
               ,b.getId()
               ,b.getName()
               ,b.getAuthor()
               ,b.getIsbn()));
      }
   }

   //Count the number of books in the database
   private int getCount()  {
      List<Book> bookList = bookPersistenceService.getAllBooks();
      return bookList.size();
   }
}

In Listing 25-16, notice how simple it is to access the APIs through the static class Services. Of course we haven’t shown you the implementation of Services and also the dynamic proxy held by the static class Services. Listing 25-17 shows the source code for the static Services class in order to give you an idea of how this scheme works. The goal of many, if not all, of the listings in this chapter is to aid your understanding. For complete compilable source code we kindly request that you refer to the downloadable projects for this chapter.

Listing 25-17. Exposing APIs to Clients Through a Services Name Space

// File reference in project: Services.Java
/**
 * Allow a namespace for clients to discover various services
 * Usage: Services.persistenceServices.bookps.addBook(); etc.
 * Dynamic proxy will take care of transactions.
 * Dynamic proxy will take care of mock data.
 * Dynamic Proxy will allow more than one interface
 *   to apply the above aspects.
 */
public class Services {
   public static String tag = "Services";
   public static class PersistenceServices   {
      ////se this pointer during initialization
      public static IBookPS bookps = null;
      static {
         Services.init();
      }
   }
   //Although this method is empty, calling it
   //will trigger all static initialization code for this class
   public static void init() {}
   private static Object mainProxy;
   static    {
      //A utility class to compile all database-related initializations so far
      //Gets the database helper going.
      //See the download project how it uses the concepts presented so far to do this
      Database.initialize();

      //set up bookps
      ClassLoader cl = IBookPS.class.getClassLoader();
      //Add more interfaces as available
      Class[] variousServiceInterfaces = new Class[] { IBookPS.class };

      //Create a big object that can proxy all the related interfaces
      //for which similar common aspects are applied
      //In this cases it is android SQLite transactions
      mainProxy = Proxy.newProxyInstance(cl,
               variousServiceInterfaces, new DBServicesProxyHandler());

      //Preset the namespace for easy discovery
      PersistenceServices.bookps = (IBookPS)mainProxy;
   }
}

Notice how DBServicesProxyHandler is a proxy for the implementation of IBookPS. When called by clients, the DBServicesProxyHandler then calls the actual implementation for IBookPS. The actual implementation of IBookPS is shown in Listing 25-15. Let’s turn to the implementation of the dynamic proxy in Listing 25-18. Some of the code and classes referenced in Listing 25-18 are only available in the downloadable projects. However, that should not hinder the general understanding of the architecture of the dynamic proxy.

Listing 25-18. Java Dynamic Proxy to Wrap the SQLite API Implementations

// File reference in project: DBServicesProxyHandler.Java
/**
 * DBServicesProxyHandler: A class to externalize SQLite Transactions.
 * It is a dynamic proxy. See Services.Java to see how a reference to this is used.
 *
 * This proxy is capable of hosting multiple persistence interfaces.
 * Each interface may represent persistence aspects of a particular entity or a domain object
 * like a Book. Or the interface can be a composite interface dealing with multiple entities.
 *
 * It also uses ThreadLocals to pass the DatabaseContext
 * DatabaseContext holds a reference to the database that is on this thread
 * It also knows how to apply transactions to that database
 * It also knows if the current thread also has a running transaction
 * @See DatabaseContext
 *
 * DatabaseContext provides the SQLiteDatabase reference to
 * the implementation classes.
 *
 * Related classes
 * ****************
 * Services.Java : Client access to interfaces
 * IBookPS: Client interface to deal with persisting a Book
 * BookPSSQLite: SQLite Implementation of IBookPS
 *
 * DBServicesProxyHandler: This class that is a dynamic proxy
 * DatabaseContext: Holds a db reference for BookPSSQLite implementation
 * DirectAccessBookDBHelper: Android DBHelper to construct the database
 *
 */
public class DBServicesProxyHandler implements InvocationHandler {
   private BookPSSQLite bookServiceImpl = new BookPSSQLite();
   private static String tag = "DBServicesProxyHandler";
   DBServicesProxyHandler(){}
   public Object invoke(Object proxy, Method method, Object[] args)
         throws Throwable {
      logMethodSignature(method);
      String mname = method.getName();
      if (mname.startsWith("get")){
         return this.invokeForReads(method, args);
      }
      else {
         return this.invokeForWrites(method, args);
      }
   }
   private void logMethodSignature(Method method){
      String interfaceName = method.getDeclaringClass().getName();
      String mname = method.getName();
      Log.d(tag,String.format("%s : %s", interfaceName, mname));
   }
   private Object callDelegatedMethod(Method method, Object[] args)
   throws Throwable{
      return method.invoke(bookServiceImpl, args);
   }
   private Object invokeForReads(Method method, Object[] args) throws Throwable {
      //See comments above about DatabaseContext
      if (DatabaseContext.isItAlreadyInsideATransaction() == true){
         //It is already bound
         return invokeForReadsWithoutATransactionalWrap(method, args);
      }
      else {
         //A new transaction
         return invokeForReadsWithATransactionalWrap(method, args);
      }

   }
   private Object invokeForReadsWithATransactionalWrap(Method method, Object[] args)
   throws Throwable {
      try   {
         DatabaseContext.setReadableDatabaseContext();
         return callDelegatedMethod(method, args);
      }
      finally   {
         DatabaseContext.reset();
      }
   }
   private Object invokeForReadsWithoutATransactionalWrap(Method method, Object[] args)
   throws Throwable {
      return callDelegatedMethod(method, args);
   }
   private Object invokeForWrites(Method method, Object[] args) throws Throwable   {
      if (DatabaseContext.isItAlreadyInsideATransaction() == true) {
         //It is already bound
         return invokeForWritesWithoutATransactionalWrap(method, args);
      }
      else   {
         //A new transaction
         return invokeForWritesWithATransactionalWrap(method, args);
      }
   }
   private Object invokeForWritesWithATransactionalWrap(Method method, Object[] args)
   throws Throwable   {
      try   {
         DatabaseContext.setWritableDatabaseContext();
         DatabaseContext.beginTransaction();
         Object rtnObject = callDelegatedMethod(method, args);
         DatabaseContext.setTransactionSuccessful();
         return rtnObject;
      }
      finally   {
         try {
            DatabaseContext.endTransaction();
         }
         finally {
            DatabaseContext.reset();
         }
      }
   }
   private Object invokeForWritesWithoutATransactionalWrap(Method method, Object[] args)
   throws Throwable   {
      return callDelegatedMethod(method, args);
   }
}//eof-class

This code in Listing 25-18 is the dynamic proxy implementation. We have not included all the details but sufficient detail is here to understand how this dynamic proxy performs transactions in an automated aspect-oriented way. It examines the called method name through reflection to see if the method name starts with “get,” and if so then it assumes the method doesn’t need a transactional context. Otherwise it marks the current thread as a transactional context. At the return of the method it completes the transaction as successful. If there are other methods called in between, the dynamic proxy knows from the thread that there is a transaction in place and hence ignores that method from a transactional aspect perspective.

Now based on your need you may want to alter this protocol based on annotations or some other aspect of your interfaces, but you get the idea. This approach of separating APIs from your UI is good design and you can use any number of persistent stores without changing your client UI code. We strongly recommend that you adapt this approach irrespective of the persistence mechanism you use, including the O/R mapping tools.

Exploring Databases on the Emulator and Available Devices

As you use SQLite as your persistence mechanism either directly or through content providers (next section), you may want to examine the resulting database files on the device for debugging purposes.

The database files created by SQLite API are kept in the following directory:

/data/data/<fully-qualified-package-name>/databases

You can use Eclipse Android file explorer to locate the directory and copy the files to your local drive and use native SQLite tools provided by SQLite directly to see and manipulate that database.

You can also use tools provided both by Android and by SQLite to examine these databases. Many of these tools reside in the <android-sdk-install-directory> ools subdirectory; others are in <android-sdk-install-directory>platform-tools.

Some useful commands from these directories are

android list avd: To see a list of AVDs or emulators
emulator.exe @avdname: To start an emulator with a given name
adb.exe devices: To see the devices or emulators
adb shell: To open a shell on the emulator or device

You can use the following commands from an “adb shell.” These will work on the emulator but on a real device you will need root access.

ls /system/bin : To see available commands
ls -l /: Root level directories
ls /data/data/com.android.providers.contacts/databases: an example
ls -R /data/data/*/databases: To see all databases on the device or emulator

If there were a find command in the included Android Unix shell, you could look at all the *.db files. But there is no good way to do this with ls alone. The nearest thing you can do is this:

ls -R /data/data/*/databases

With this command, you will notice that the Android distribution has the databases shown in Listing 25-19 (depending on your release, this list may vary):

Listing 25-19. A Few Sample Databases

alarms.db
contacts.db
downloads.db
internal.db
settings.db
mmssms.db
telephony.db

You can invoke sqlite3 on one of these databases inside the adb shell by typing this:

sqlite3 /data/data/com.android.providers.contacts/databases/contacts.db

You can exit sqlite3 by typing this:

sqlite>.exit

Notice that the prompt for adb is # and the prompt for sqlite3 is sqlite>. These prompts could be different depending on the device. You can read about the various sqlite3 commands by visiting www.sqlite.org/sqlite.html. However, we will list a few important commands here so you don’t have to make a trip to the Web. You can see a list of tables by typing

sqlite> .tables

This command is a shortcut for querying on the sqlite_master table as shown in Listing 25-20 (format and structure of the resulting output may vary).

Listing 25-20. Using SQLite sqlite_master Table

SELECT name FROM sqlite_master
WHERE type IN ('table','view') AND name NOT LIKE 'sqlite_%'
UNION ALL
SELECT name FROM sqlite_temp_master
WHERE type IN ('table','view')
ORDER BY 1

The table sqlite_master is a master table that keeps track of tables and views in the SQLite database. The following command line displays a create statement for a table called people in contacts.db (assuming this database exists on your device):

.schema people

This is one way to get at the column names of a table in SQLite. This will also show the column data types. While working with content providers, you should note these column types because access methods depend on them. Also note that this may not be a practical way to see these databases as you may not have access to them on real devices. In that case you have to rely on the documentation provided by the content provider.

You can issue the following command from your OS command prompt to pull down the contacts.db file to the local file system:

adb pull /data/data/com.android.providers.contacts/databases/contacts.db É
c:/somelocaldir/contacts.db

The sample SQL statements in Listing 25-21 could help you navigate through an SQLite database quickly (alternatively you can use any third-party SQLite browser tool):

Listing 25-21. Sample SQL Code for SQLite

--Set the column headers to show in the tool
sqlite>.headers on

--select all rows from a table
select * from table1;

--count the number of rows in a table
select count(*) from table1;

--select a specific set of columns
select col1, col2 from table1;

--Select distinct values in a column
select distinct col1 from table1;

--counting the distinct values
select count(col1) from (select distinct col1 from table1);

--group by
select count(*), col1 from table1 group by col1;

--regular inner join
select * from table1 t1, table2 t2
where t1.col1 = t2.col1;

--left outer join
--Give me everything in t1 even though there are no rows in t2
select * from table t1 left outer join table2 t2
on t1.col1 = t2.col1
where ....

Exploring Content Providers

Earlier in the chapter we touched upon content providers to share data between applications. Content providers as stated are wrappers around a data store. The data stores could be local or remote. The data stores are usually SQLite databases on the local device.

To retrieve data from a content provider or save data into a content provider, you will use a set of REST-like URIs. For example, if you were to retrieve a set of books from a content provider that is an encapsulation of a book database, you might need to use a URI like this:

content://com.android.book.BookProvider/books

To retrieve a specific book from the book database (like say book 23), you might use a URI like this:

content://com.android.book.BookProvider/books/23

You will see in this chapter how these URIs translate to underlying database-access mechanisms. Any application with the appropriate access permissions on the device can make use of these URIs to access and manipulate data.

Exploring Android’s Built-in Providers

Android comes with a number of built-in content providers, which are documented in the SDK’s android.provider Java package. You can view the list of these providers here:

http://developer.android.com/reference/android/provider/package-summary.html

The providers include, for example, Contacts and Media Store. These SQLite databases typically have an extension of .db and are accessible only from the implementation package. Any access outside that package must go through the content-provider interface. You can use the previous section “Exploring Databases on the Emulator and Available Devices” to explore the database files created by built-in providers on the emulator. On real devices this is not feasible unless of course you have root access on the device.

Understanding the Structure of Content Provider URIs

Each content provider on a device is registered in the Android manifest file like a web site with a string identifier called an authority (akin to a domain name). Listing 25-22 has two examples of this registration:

Listing 25-22. Example of Registering a Provider

<!-- File reference in project: AndroidManifest.xml -->
<provider android:name="SomeProviderJavaClass"
        android:authorities="com.your-company.SomeProvider" />

<provider android:name="BookProvider"
   android:authorities="com.androidbook.provider.BookProvider"
/>

The unique authority string forms the basis of a set of URIs that this content provider offers. An Android content URI has the following structure:

content://<authority-name>/<path-segment1>/<path-segment2>/etc...

Here’s an example URI that identifies a book numbered 23 in a database of books:

content:// com.androidbook.provider.BookProvider/books/23

After content:, the URI contains the authority, which is used to locate the provider in the provider registry. In the preceding example, com.androidbook.provider.BookProvider is the authority portion of the URI.

/books/23 is the path section of the URI that is specific to each provider. The books and 23 portions of the path section are called path segments. It is the responsibility of the provider to document and interpret the path section and path segments of the URIs. Hence content providers provide these REST-like URLs to retrieve or manipulate data. For the preceding registration, the URI to identify a directory or a collection of books in the books database is

content:// com.androidbook.provider.BookProvider/books

The URI to identify a specific note is

content:// com.androidbook.provider.BookProvider/books/#

where # is the id of a particular note. Listing 25-23 shows additional examples of URIs that some data providers on Android accept:

Listing 25-23. Few Sample Android Content URLs

content://media/internal/images
content://media/external/images
content://contacts/people/
content://contacts/people/23

Notice how these providers’ media (content://media) and contacts (content://contacts) don’t have a fully qualified authority name. This is because providers offered by Android may not carry a fully qualified authority name.

Given these content URIs, a provider is expected to retrieve rows that the URIs represent. The provider is also expected to alter content at this URI using any of the state-change methods: insert, update, or delete.

Implementing Content Providers

Let’s fully understand content providers by implementing and using one. To write a content provider, you have to extend android.content.ContentProvider and implement the following key methods: query(), insert(), update(), delete(), and getType().

You’ll need to set up a number of things for implementing these methods. Implementing a content provider needs the following steps:

  1. Plan your database, URIs, column names, and so on, and create a metadata class that defines constants for all of these metadata elements.
  2. Extend the abstract class ContentProvider.
  3. Implement these methods: query, insert, update, delete, and getType.
  4. Register the provider in the manifest file.
  5. Use the content provider.

Planning a Database

To explore this topic, we’ll create a database similar to the one that we have used for the book collection that was used to illustrate the storing of data in SQLite directly. Note that to keep the databases from conflicting with each other some of the names may be different.

The book database contains only one table called books, and its columns are name, isbn, and author. These column names fall under metadata. You’ll define this sort of relevant metadata in a Java class. This metadata-bearing Java class BookProviderMetaData is shown in Listing 25-24.

Listing 25-24. Defining Metadata for Your Database

// File reference in project: BookProviderMetaData.Java
public class BookProviderMetaData {
    public static final String AUTHORITY = "com.androidbook.provider.BookProvider";

    public static final String DATABASE_NAME = "book.db";
    public static final int DATABASE_VERSION = 1;
    public static final String BOOKS_TABLE_NAME = "books";

    private BookProviderMetaData() {}

    //inner class describing BookTable
    public static final class BookTableMetaData implements BaseColumns  {
        private BookTableMetaData() {}
        public static final String TABLE_NAME = "books";

        //uri and MIME type definitions
        public static final Uri CONTENT_URI =
                          Uri.parse("content://" + AUTHORITY + "/books");
        public static final String CONTENT_TYPE =
                         "vnd.android.cursor.dir/vnd.androidbook.book";
        public static final String CONTENT_ITEM_TYPE =
                        "vnd.android.cursor.item/vnd.androidbook.book";

        public static final String DEFAULT_SORT_ORDER = "modified DESC";

        //Additional Columns start here.
        //string type
        public static final String BOOK_NAME = "name";
        //string type
        public static final String BOOK_ISBN = "isbn";
        //string type
        public static final String BOOK_AUTHOR = "author";
        //Integer from System.currentTimeMillis()
        public static final String CREATED_DATE = "created";
        //Integer from System.currentTimeMillis()
        public static final String MODIFIED_DATE = "modified";
    }
}

This BookProviderMetaData class starts by defining its authority to be com.androidbook.provider.BookProvider.

This class then proceeds to define its one table (books) as an inner BookTableMetaData class. The BookTableMetaData class then defines a URI for identifying a collection of books. Given the authority in the previous paragraph, the URI for a collection of books will look like this:

content://com.androidbook.provider.BookProvider/books

This URI is indicated by the constant

BookProviderMetaData.BookTableMetaData.CONTENT_URI

The BookTableMetaData class then proceeds to define the MIME types for a collection of books and a single book. The provider implementation will use these constants to return the MIME types for the incoming URIs. MIME types are similar to the MIME types defined by HTTP. As a guideline the primary MIME type for a collection of items returned through an Android cursor should always be vnd.android.cursor.dir, and the primary MIME type of a single item retrieved through an Android cursor should be vnd.android.cursor.item. You have more wiggle room when it comes to the subtype, as in vnd.androidbook.book in Listing 25-24.

BookTableMetaData then defines the set of columns for the book table: name, isbn, author, created (creation date), and modified (last-updated date).

The metadata class BookTableMetaData also inherits from the BaseColumns class that provides the standard _id field, which represents the row ID. With these metadata definitions in hand, we’re ready to tackle the provider implementation.

Extending ContentProvider

Implementing the BookProvider involves extending the ContentProvider class and overriding onCreate() to create the database and then implement the query, insert, update, delete, and getType methods.

A query method requires the set of columns it needs to return. This is similar to a select clause that requires column names along with their as counterparts (sometimes called synonyms). As a convention Android SDK uses a map object that it calls a projection map to represent these column names and their synonyms. We will need to set up this map so we can use it later in the query-method implementation. In the code for the provider implementation (see Listing 25-26), you will see this done upfront as part of projection map setup.

Most of the methods we’ll be implementing for the content provider contract take a URI as an input. Listing 25-25 shows book provider URI examples:

Listing 25-25. Examples of BookProvider Content URIs

Uri1: content://com.androidbook.provider.BookProvider/books
Uri2: content://com.androidbook.provider.BookProvider/books/12

The book provider needs to distinguish each of these URIs. BookProvider is a simple case. If our book provider had been housing more objects in addition to just books, then there would be more URIs to identify those additional objects.

The provider implementation needs a mechanism to distinguish one URI from the other; Android uses a class called UriMatcher for this purpose. So we need to set up this object with all our URI variations. You will see this code in Listing 25-26 right after we define the projection map. We’ll further explain the UriMatcher class in the section “Using UriMatcher to Figure Out the URIs.”

The code in Listing 25-26 then overrides the onCreate() method to facilitate the database creation. The database creation is identical to the database creation we have covered as part of using SQLite directly for internal persistence needs.

The source code in Listing 25-26 then implements the insert(), query(), update(), getType(), and delete() methods. The code for all of this is presented together in Listing 25-26, but we will explain each aspect in a separate subsection.

Listing 25-26. Implementing the BookProvider Content Provider

// File reference in project: BookProvider.Java
public class BookProvider extends ContentProvider
{
    //Logging helper tag. No significance to providers.
    private static final String TAG = "BookProvider";

    //Setup projection Map
    //Projection maps are similar to "as" (column alias) construct
    //in an sql statement where by you can rename the
    //columns.
    private static HashMap<String, String> sBooksProjectionMap;
    static
    {
        sBooksProjectionMap = new HashMap<String, String>();
        sBooksProjectionMap.put(BookTableMetaData._ID,
                                BookTableMetaData._ID);

        //name, isbn, author
        sBooksProjectionMap.put(BookTableMetaData.BOOK_NAME,
                                BookTableMetaData.BOOK_NAME);
        sBooksProjectionMap.put(BookTableMetaData.BOOK_ISBN,
                                BookTableMetaData.BOOK_ISBN);
        sBooksProjectionMap.put(BookTableMetaData.BOOK_AUTHOR,
                                BookTableMetaData.BOOK_AUTHOR);

        //created date, modified date
        sBooksProjectionMap.put(BookTableMetaData.CREATED_DATE,
                                BookTableMetaData.CREATED_DATE);
        sBooksProjectionMap.put(BookTableMetaData.MODIFIED_DATE,
                                BookTableMetaData.MODIFIED_DATE);
    }

    //Provide a mechanism to identify all the incoming uri patterns.
    private static final UriMatcher sUriMatcher;
    private static final int INCOMING_BOOK_COLLECTION_URI_INDICATOR = 1;
    private static final int INCOMING_SINGLE_BOOK_URI_INDICATOR = 2;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(BookProviderMetaData.AUTHORITY, "books",
                          INCOMING_BOOK_COLLECTION_URI_INDICATOR);
        sUriMatcher.addURI(BookProviderMetaData.AUTHORITY, "books/#",
                          INCOMING_SINGLE_BOOK_URI_INDICATOR);

    }
    // Setup/Create Database to use for the implementation
    private static class DatabaseHelper extends SQLiteOpenHelper {
        DatabaseHelper(Context context) {
            super(context,
                BookProviderMetaData.DATABASE_NAME,
                null,
                BookProviderMetaData.DATABASE_VERSION);
        }
        @Override
        public void onCreate(SQLiteDatabase db)     {
            Log.d(TAG,"inner oncreate called");
            db.execSQL("CREATE TABLE " + BookTableMetaData.TABLE_NAME + " ("
                    + BookTableMetaData._ID + " INTEGER PRIMARY KEY,"
                    + BookTableMetaData.BOOK_NAME + " TEXT,"
                    + BookTableMetaData.BOOK_ISBN + " TEXT,"
                    + BookTableMetaData.BOOK_AUTHOR + " TEXT,"
                    + BookTableMetaData.CREATED_DATE + " INTEGER,"
                    + BookTableMetaData.MODIFIED_DATE + " INTEGER"
                    + ");");
        }
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.d(TAG,"inner onupgrade called");
            Log.w(TAG, "Upgrading database from version "
                    + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " +
                     BookTableMetaData.TABLE_NAME);
            onCreate(db);
        }
    }//eof-inner DatabaseHelper class
    //This is initialized in the onCreate() method
    private DatabaseHelper mOpenHelper;

    //Component creation callback
    @Override
    public boolean onCreate()   {
        Log.d(TAG,"main onCreate called");
        mOpenHelper = new DatabaseHelper(getContext());
        return true;
    }

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

        switch (sUriMatcher.match(uri)) {
        case INCOMING_BOOK_COLLECTION_URI_INDICATOR:
            qb.setTables(BookTableMetaData.TABLE_NAME);
            qb.setProjectionMap(sBooksProjectionMap);
            break;

        case INCOMING_SINGLE_BOOK_URI_INDICATOR:
            qb.setTables(BookTableMetaData.TABLE_NAME);
            qb.setProjectionMap(sBooksProjectionMap);
            qb.appendWhere(BookTableMetaData._ID + "="
                        + uri.getPathSegments().get(1));
            break;

        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        // If no sort order is specified use the default
        String orderBy;
        if (TextUtils.isEmpty(sortOrder)) {
            orderBy = BookTableMetaData.DEFAULT_SORT_ORDER;
        } else {
            orderBy = sortOrder;
        }

        // Get the database and run the query
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        Cursor c = qb.query(db, projection, selection,
                   selectionArgs, null, null, orderBy);

        //example of getting a count
        int i = c.getCount();

        // Tell the cursor what uri to watch,
        // so it knows when its source data changes
        c.setNotificationUri(getContext().getContentResolver(), uri);
        return c;
    }
    @Override
    public String getType(Uri uri)    {
        switch (sUriMatcher.match(uri)) {
        case INCOMING_BOOK_COLLECTION_URI_INDICATOR:
            return BookTableMetaData.CONTENT_TYPE;
        case INCOMING_SINGLE_BOOK_URI_INDICATOR:
            return BookTableMetaData.CONTENT_ITEM_TYPE;
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
        }
    }
    @Override
    public Uri insert(Uri uri, ContentValues initialValues)  {
        // Validate the requested uri
        if (sUriMatcher.match(uri)
                != INCOMING_BOOK_COLLECTION_URI_INDICATOR) {
            throw new IllegalArgumentException("Unknown URI " + uri);
        }
        ContentValues values;
        if (initialValues != null) {
            values = new ContentValues(initialValues);
        } else {
            values = new ContentValues();
        }
        Long now = Long.valueOf(System.currentTimeMillis());
        // Make sure that the fields are all set
        if (values.containsKey(BookTableMetaData.CREATED_DATE) == false){
            values.put(BookTableMetaData.CREATED_DATE, now);
        }
        if (values.containsKey(BookTableMetaData.MODIFIED_DATE) == false) {
            values.put(BookTableMetaData.MODIFIED_DATE, now);
        }
        if (values.containsKey(BookTableMetaData.BOOK_NAME) == false) {
            throw new SQLException(
               "Failed to insert row because Book Name is needed " + uri);
        }
        if (values.containsKey(BookTableMetaData.BOOK_ISBN) == false) {
            values.put(BookTableMetaData.BOOK_ISBN, "Unknown ISBN");
        }
        if (values.containsKey(BookTableMetaData.BOOK_AUTHOR) == false) {
            values.put(BookTableMetaData.BOOK_ISBN, "Unknown Author");
        }

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        long rowId = db.insert(BookTableMetaData.TABLE_NAME,
                BookTableMetaData.BOOK_NAME, values);
        if (rowId > 0) {
            Uri insertedBookUri =
                ContentUris.withAppendedId(
                        BookTableMetaData.CONTENT_URI, rowId);
            getContext()
               .getContentResolver()
                    .notifyChange(insertedBookUri, null);

            return insertedBookUri;
        }
        throw new SQLException("Failed to insert row into " + uri);
    }
    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        switch (sUriMatcher.match(uri)) {
        case INCOMING_BOOK_COLLECTION_URI_INDICATOR:
            count = db.delete(BookTableMetaData.TABLE_NAME,
                    where, whereArgs);
            break;
        case INCOMING_SINGLE_BOOK_URI_INDICATOR:
            String rowId = uri.getPathSegments().get(1);
            count = db.delete(BookTableMetaData.TABLE_NAME,
                    BookTableMetaData._ID + "=" + rowId
                    + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),
                    whereArgs);
            break;
        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
    @Override
    public int update(Uri uri, ContentValues values,
            String where, String[] whereArgs)  {
        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        switch (sUriMatcher.match(uri)) {
        case INCOMING_BOOK_COLLECTION_URI_INDICATOR:
            count = db.update(BookTableMetaData.TABLE_NAME,
                    values, where, whereArgs);
            break;

        case INCOMING_SINGLE_BOOK_URI_INDICATOR:
            String rowId = uri.getPathSegments().get(1);
            count = db.update(BookTableMetaData.TABLE_NAME,
                    values, BookTableMetaData._ID + "=" + rowId
                    + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),
                    whereArgs);
            break;

        default:
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
}

Now, let’s analyze this code section by section.

Using UriMatcher to Figure Out the URIs

We’ve mentioned the UriMatcher class several times now; let’s look into it. Almost all methods in a content provider are overloaded with respect to the URI. For example, the same query() method is called whether you want to retrieve a single book or a list of books. It is up to the method to know which type of URI is being requested. Android’s UriMatcher utility class helps you identify the URI types.

Here’s how it works. You tell an instance of UriMatcher what kind of URI patterns to expect during its initialization. You will also associate a unique number with each pattern. Once these patterns are registered, you can then ask UriMatcher if the incoming URI matches a certain pattern.

As we’ve mentioned, our BookProvider content provider has two URI patterns: one for a collection of books and one for a single book. The code in Listing 25-26 registers both of these patterns using UriMatcher. It allocates 1 for a collection of books and 2 for a single book (the URI patterns themselves are defined in the metadata for the books table). You can see this in the static initialization of the variable sUriMatcher in Listing 25-26. You can then see how UriMatcher plays a part in the query() method implementation in distinguishing the URIs using the constants for each type of URI.

Using Projection Maps

A content provider acts like an intermediary between an abstract set of columns and a real set of columns in a database, yet these column sets could differ. While constructing queries, you must map between the where clause columns that a client specifies and the real database columns. You set up this projection map with the help of the SQLiteQueryBuilder class. You can see how this projection map variable sBooksProjectionMap is set for the BookProvider in Listing 25-26. You can also see in that listing how this variable sBooksProjectionMap is then used by the SQLiteQueryBuilder object.

Fulfilling MIME-Type Contracts

Let’s start with the getType() method in Listing 25-26. This method returns a MIME type for a given URI. This method, like many other methods of a content provider, is sensitive to the incoming URI. As a result, the first responsibility of the getType() method is to distinguish the type of the URI. Is it a collection of books or a single book? The code used the UriMatcher to decipher this URI type. Depending on this URI, the BookTableMetaData class has defined the MIME-type constants to return for each URI.

Implementing the Query Method

Like the other methods, the query method uses UriMatcher to identify the URI type. If the URI type is a single-item type, the method retrieves the book ID from the incoming URI by looking at the first segment returned by getPathSegments().

The query method then uses the projections that we created upfront in Listing 25-26 to identify the return columns. In the end, query returns the cursor to the caller. Throughout this process, the query method uses the SQLiteQueryBuilder object to formulate and execute the query.

While reading the data one can constrain the rows returned either using the URI or through explicit where clause arguments passed to the query method as inputs. In the BookProvider implementation of Listing 25-26 we used the approach of using the URI segments to retrieve the book ID to return the values for just that book.

Instead you can use the selection parameter and the selectionArgs parameter of the query() method to explicitly pass the where clause arguments. These arguments work just like the SQLiteDatabase.query() arguments in Listing 25-12, where “?” are used as placeholders for the values passed in the selectionArgs array.

Implementing the Insert Method

The insert method in a content provider is responsible for inserting a record into the underlying database and then returning a URI that point to the newly created record.

Like the other methods, insert uses UriMatcher to identify the URI type. The code first checks whether the URI indicates the proper collection-type URI. If not, the code throws an exception.

The code then validates the optional and mandatory column parameters. The code can substitute default values for some columns if they are missing.

Next, the code uses an SQLiteDatabase object to insert the new record and returns the newly inserted ID. In the end, the code constructs the new URI using the returned ID from the database.

Implementing the Update Method

The update method in a content provider is responsible for updating a record (or records) based on the column values passed in, as well as the where clause that is passed in. The update method then returns the number of rows updated in the process.

Like the other methods, update uses UriMatcher to identify the URI type. If the URI type is a collection, the where clause is passed through so it can affect as many records as possible. If the URI type is a single-record type, then the book ID is extracted from the URI and specified as an additional where clause. In the end, the code returns the number of records updated. Also notice how this notifyChange method enables you to announce to the world that the data at that URI has changed. Potentially, you can do the same in the insert method by saying that the collection of books data at URI “.../books” has changed when a record is inserted.

Implementing the Delete Method

The delete method in a content provider is responsible for deleting a record (or records) based on the where clause that is passed in. The delete method then returns the number of rows deleted in the process.

Like the other methods, delete uses UriMatcher to identify the URI type. If the URI type is a collection type, the where clause is passed through so you can delete as many records as possible. If the where clause is null, all records will be deleted. If the URI type is a single-record type, the book ID is extracted from the URI and specified as an additional where clause. In the end, the code returns the number of records deleted.

Registering the Provider

Finally, you must register the content provider in the Android.Manifest.xml file using the tag structure in Listing 25-27. A provider is a component and hence a sibling of the other components such as an activity and a receiver. So it is a sibling node to other activities in the Android manifest file.

Listing 25-27. Registering a Provider

<provider android:name=".BookProvider"
   android:authorities="com.androidbook.provider.BookProvider"/>

Exercising the Book Provider

Now that we have a book provider, we are going to show you sample code to exercise that provider. The sample code includes adding a book, removing a book, getting a count of the books, and finally displaying all the books.

Keep in mind that these are code extracts from the sample project and will not compile, because they require additional dependency files. However, we feel this sample code is sufficient in demonstrating the concepts we have explored.

At the end of this chapter, we have included a link to the downloadable sample project, which you can use in your Eclipse environment to compile and test.

Adding a Book

The code in Listing 25-28 inserts a new book into the book database.

Listing 25-28. Exercising a Provider Insert

// File reference in project:ProviderTester.Java
public void addBook(Context context) {
    String tag = "Exercise BookProvider";
    Log.d(tag,"Adding a book");
    ContentValues cv = new ContentValues();
    cv.put(BookProviderMetaData.BookTableMetaData.BOOK_NAME, "book1");
    cv.put(BookProviderMetaData.BookTableMetaData.BOOK_ISBN, "isbn-1");
    cv.put(BookProviderMetaData.BookTableMetaData.BOOK_AUTHOR, "author-1");

    ContentResolver cr = context.getContentResolver();
    Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;
    Log.d(tag,"book insert uri:" + uri);
    Uri insertedUri = cr.insert(uri, cv);
    Log.d(tag,"inserted uri:" + insertedUri);
}

Removing a Book

The code in Listing 25-29 deletes the last record from the book database.

Listing 25-29. Exercising a Provider Delete

// File reference in project:ProviderTester.Java
public void removeBook() {
   int firstBookId = this.getFirstBookId();
   if (firstBookId == -1) throw new SQLException("Book id is less than 0");
   ContentResolver cr = this.mContext.getContentResolver();
   Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;
   Uri delUri = Uri.withAppendedPath(uri, Integer.toString(firstBookId));
   reportString("Del Uri:" + delUri);
   cr.delete(delUri, null, null);
   this.reportString("Number of Books after the delete:" + getCount());
}

private int getFirstBookId() {
   Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;
   Activity a = (Activity)this.mContext;
   Cursor c = null;
   try   {
      c = a.getContentResolver().query(uri,
            null, //projection
            null, //selection string
            null, //selection args array of strings
            null); //sort order
      int numberOfRecords = c.getCount();
      if (numberOfRecords == 0) {
         return -1;
      }
      c.moveToFirst();
      int id = c.getInt(1); //id column
      return id;
   }
   finally   {
      if (c!= null) c.close();
   }
}

Displaying the List of Books

The code in Listing 25-30 retrieves all the records in the book database.

Listing 25-30. Displaying a List of Books

// File reference in project:ProviderTester.Java
public void showBooks() {
   Uri uri = BookProviderMetaData.BookTableMetaData.CONTENT_URI;
   Activity a = (Activity)this.mContext;
   Cursor c = null;
   try  {
      c = a.getContentResolver().query(uri,
            null, //projection
            null, //selection string
            null, //selection args array of strings
            null); //sort order
      int iid = c.getColumnIndex(BookProviderMetaData.BookTableMetaData._ID);
      int iname = c.getColumnIndex(BookProviderMetaData.BookTableMetaData.BOOK_NAME);
      int iisbn = c.getColumnIndex(BookProviderMetaData.BookTableMetaData.BOOK_ISBN);
      int iauthor = c.getColumnIndex(BookProviderMetaData.BookTableMetaData.BOOK_AUTHOR);

      //Report your indexes
      Log.d(tag, "name,isbn,author:" + iname + iisbn + iauthor);

      //walk through the rows based on indexes
      for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
         //Gather values
         String id = c.getString(iid);
         String name = c.getString(iname);
         String isbn = c.getString(iisbn);
         String author = c.getString(iauthor);

         //Report or log the row
         StringBuffer cbuf = new StringBuffer(id);
         cbuf.append(",").append(name);
         cbuf.append(",").append(isbn);
         cbuf.append(",").append(author);
         Log.d(tag,cbuf.toString());
      }

      //Report how many rows have been read
      int numberOfRecords = c.getCount();
      Log.d(tag,"Num of Records:" + numberOfRecords);
   }
   finally   {
      if (c!= null) c.close();
   }
}

Notice that the method of retrieving the books from a content provider is very similar to retrieving data from an SQLite database. In Listing 25-30 we have used the query() method from a ContentResolver object. After using the cursor object we have closed the cursor.

Instead if you were passing this cursor object to a UI component that is in the Activity then this cursor object needs to be managed as the activity follows its life cycle. Prior to Honeycomb, there was a method called managedQuery() on the Activity to do this automatically, which has since been deprecated in favor of CursorLoader.

When a query is thus managed through managedQuery(), the activity can call methods on the cursor to place it into proper state. For example, the activity will call deactivate() on the cursor when it is stopped and later calls requery() when it is started. The cursor will be closed when the activity is destroyed. You can choose to call stopManagingCursor() on that cursor if you want to control the behavior of the cursor yourself. Because the activity closes the cursor, don’t close a managed cursor. If your intention is to read all the rows one time and close the cursor, then use the query() method of the ContentResolver as opposed to the Activity.managedQuery() method and explicitly close the cursor.

Since Honeycomb, the cursor reads are wrapped into a more general approach called “Loaders,” which allow you to read data in an asynchronous thread through callbacks exposed to fragments or activities. This is the recommended and preferred method. We will cover this approach in the next chapter, Chapter 26, on Loaders.

You have seen how we have used update APIs on a content providers. These update operations can be inefficient if done one by one through a content provider. In Chapter 27 we will cover how these individual update operations can be sent as a batch to a content provider for efficiency reasons.

Resources

Here are some additional Android resources that can help you with the topics covered in this chapter:

Summary

This chapter has covered a lot of aspects about a vital need of your applications: persistence. We have given you a plethora of options available in Android for persistence and how to choose an appropriate option. We have covered how to use SQLite for internal persistence needs in significant detail. We have shown you an industrial-strength API pattern for persistence using SQLite which can be extended to any persistence implementation. Importantly, this pattern showed you how to externalize transactions to keep your persistence code simple. We have then covered what content providers are, the nature of content URIs, MIME types, how to use SQLite to construct providers that respond to URIs, how to write a new content provider, and how to access an existing content provider.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.107.32