Introducing ContentPal

Those who have worked with the Android address book or Android calendar know Android has this concept called “ContentProviders” to share data between apps. Among other things content providers provide access to the contact and calendar databases.

With URI addressing and database-like access ContentProviders are versatile components - which can also be quite a hassle to work with. The following code snippet is taken from Android’s ContactsProvider Documentation (comments have been removed for brevity). It shows the basic steps to create a new contact with display name, a phone number and an email address.

ArrayList<ContentProviderOperation> ops =
    new ArrayList<ContentProviderOperation>();

ContentProviderOperation.Builder op =
    ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, mSelectedAccount.getType())
    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, mSelectedAccount.getName());
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
    .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);
ops.add(op.build());

op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
    .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
    .withValue(ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
    .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
    .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);
op.withYieldAllowed(true);
ops.add(op.build());

getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);

This code is rather verbose and cumbersome. It’s hard to read and write and it’s easy to forget mandatory values or to pass the wrong value type.

Mastering back references can be a real challenge too (in this example it’s trivial because the batch contains only one contact).

In addition there are less obvious issue like the infamous TransactionTooLargeException which strikes when a transaction contains to much data to fit into the IPC transaction buffer of a process (which has a size of 1Mb). This limit can easily be hit when syncing an entire address book or calendar and putting too much data into one transaction. Unfortunately it’s not easy to estimate the size of a transaction, so most developers probably limit themselves to very small transaction sizes, at the expense of performance.

ContentPal to the rescue

ContentPal adds another layer on top of plain ContentProviderOperations. It aims to make it much easier and less error prone to work with Content Providers. The resulting code is easier to write and read and it is shorter, often even when inserting just a single row.

ContentPal manages back references automatically. It also tracks transaction sizes and commits enqueued content operations when the size grows beyond a certain limit.

Due to its design, ContentPal is easy to use and easy to extend. The first extension is called “ContactsPal”. ContactsPal implements the ContactsContract providing means to read and write contact data.

Just one more thing: ContentPal is not an ORM and it doesn’t intend to be one. In particular, it doesn’t model business logic entities, but database entities (tables and rows).

ContactsPal

ContactsPal adds many classes to model operations and entities for the contacts provider database. With ContactsPal the code to insert a new contact with display name, email and phone (just like the example above) can be rewritten to this:

ContentProviderClient client =
  getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
OperationsQueue operationsQueue = new BasicOperationsQueue(client);
operationsQueue.enqueue(
  new Yieldable(
    new InsertRawContactBatch(
      account,
      new DisplayNameData(name),
      new Typed(phoneType, new PhoneData(phone)),
      new Typed(emailType, new EmailData(email)))));

operationsQueue.flush();
client.release();

To get a better picture of ContentPal we’ll use a slightly more verbose form of the above operation. InsertRawContactBatch is a specialized class to insert contacts. It can be replaced with a few more generic OperationsBatch implementations like so (in fact that’s what happens under the hood when using InsertRawContactBatch):

ContentProviderClient client =
  getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
OperationsQueue operationsQueue = new BasicOperationsQueue(client);

RowSnapshot<ContactsContract.RawContacts> rawContact =
  new VirtualRowSnapshot<>(new AccountScoped<>(account, new RawContacts()));

mContactsQueue.enqueue(
  new Yieldable(
    new Joined(
      new SingletonBatch(
        new Put<>(rawContact)),
      new MultiInsertBatch<>(
        new RawContactData(rawContact),
        new DisplayNameData(name),
        new Typed(phoneType, new PhoneData(phone)),
        new Typed(emailType, new EmailData(email))))));
operationsQueue.flush();
client.release();

Breaking it down

We’ll use the latter version above to explain what exactly happens.

Setup

ContentPal uses a ContentProviderClient to perform Content Provider Operations. You can acquire one from a ContentResolver like this

ContentProviderClient client =
    getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);

Using a ContentProviderClient saves some overhead when executing multiple queries or batch operations because the provider doesn’t need to be resolved for every operation.

Next we create an OperationsQueue which uses the ContentProviderClient we just created. The OperationQueue will receive and commit all Operations.

OperationsQueue operationsQueue = new BasicOperationsQueue(client);

Now we’re ready to insert some database rows.

Insert the rawcontact row

To insert a new row we need to specify the table to insert to. We declare an AccountScoped RawContacts Table because we want to insert into a specific account. All entries added to this table will automatically be added to the given account. Tables are immutable (like most of the other classes in this library) and can be used to create as many rows as you wish.

new AccountScoped<>(account, new RawContacts())

Next we declare a new row in this table. We need to refer to this row later when we create the data rows. A VirtualRowSnapshot represents a “future row” which we can refer to in other operations later on. It can be used to update or delete the row which was inserted in an earlier operation or to add foreign keys to other rows which are inserted or updated later on.

RowSnapshot<ContactsContract.RawContacts> rawContact =
    new VirtualRowSnapshot<>(new AccountScoped<>(account, new RawContacts()));

In order to have this “future row” committed later on we create a Put operation with the RowSnapshot and add it to an OperationsBatch (since it’s the sole Operation in this batch we use a SingletonBatch).

new SingletonBatch(new Put<>(rawContact))

Insert the data rows

Each data row belongs to exactly one rawcontact. So when inserting data rows we need to add a reference to the rawcontact row they belong to. Also, the data rows should be inserted in the same database transaction like the rawcontact (to make sure the UI doesn’t see the empty contact).

An easy way to insert multiple rows which refer to the same foreign key is using a MultiInsertBatch. MultiInsertBatch takes an InsertOperation (which then serves as the prototype of the rows to create) and a couple of RowData objects which contain the data of each row to add.

The InsertOperation “prototype” looks like this:

new RawContactData(rawContact)

This is a special Operation which creates a new data row which refers to the given rawcontact.

It’s basically just short for

new Related<>(rawContact, ContactsContract.Data.RAW_CONTACT_ID, new Insert<>(new Data()))

To populate the data rows just add them to the MultiInsertBatch along with the prototype operation.

new MultiInsertBatch<>(
  new RawContactData(rawContact),
  new DisplayNameData(name),
  new Typed(phoneType, new PhoneData(phone)),
  new Typed(emailType, new EmailData(email)))

Note how the data sets for phone and email are composed from a base data class (i.e. PhoneData or EmailData) and a Typed decorator which sets the actual type. ContentPal (and ContacsPal) strictly follow the Single responsibility principle and complex structures are created by composition. For instance PhoneData only adds the absolute minimum of data, which is the data mime type and the phone number itself. The phone type is added by a decorator which is either Typed for predefined types or Custom for a custom label.

To ensure inserting the rawcontact and the data rows happens in the same transaction both OperationsBatches are joined using Joined.

new Joined(
  new SingletonBatch(
    new Put<>(rawContact)),
  new MultiInsertBatch<>(
    new RawContactData(rawContact),
    new DisplayNameData(name),
    new Typed(phoneType, new PhoneData(phone)),
    new Typed(emailType, new EmailData(email))))));

Finally the Yieladable decorator is added to allow the content provider to execute other pending operations after this transaction. It added for demonstration purposes (and because the old code above sets it too). For a single contact it’s not required.

Perform the operation

So far nothing has been sent to the ContactsProvider. We just declared the operation. In order to execute the operations, we enqueued them in the OperationsQueue.

operationsQueue.enqueue(
  new Yieldable(
    new Joined(
      new SingletonBatch(new Put<>(rawContact)),
      new MultiInsertBatch<>(
        new RawContactData(rawContact),
        new DisplayNameData(name),
        new Typed(phoneType, new PhoneData(phone)),
        new Typed(emailType, new EmailData(email))))));

Note that execution may or may not happen instantly. It depends on how much data has been enqueued so far and on the size of the new batch.

At this point we can enqueue more operations. BasicOperationsQueue will commit all enqueued operations when the transaction size grows beyond a certain limit or when flush() is called.

Other operations can refer to the same rawcontact which has been inserted earlier. ContentPal takes are of resolving references, even if the previous batch has already been committed.

Cleaning up

After all batches have been committed, we make sure all pending transactions are executed and release the ContentProviderClient.

operationsQueue.flush();
client.release();

Final words

ContactsPal already supports the most common operations on contacts like inserting, updating, deleting and reading rawcontacts and data entities. Other features like support for the user profile, groups, aggregation exceptions etc. are planned or already being worked on.

Also in development: CalendarPal.

If you’re interested in using or improving ContentPal or ContactsPal (or CalendarPal), drop us a line or fork the repository on GitHub.