Chapter    27

Exploring the Contacts API

In Chapters 25 and 26, we covered content providers and their close cousins, the loaders. We listed the benefits of exposing data through content provider abstraction. In a content provider abstraction, data is exposed as a series of URLs. These data URLs can be used to read, query, update, insert, and delete. These URLs and their corresponding cursors become the API for that content provider.

The Contacts API is one such content provider API for working with contact data. Contacts in Android are maintained in a database and exposed through a content provider whose authority is rooted at

content://com.android.contacts

The Android SDK documents the various URLs and the data they return using a set of Java interfaces and classes that are rooted at the Java package

android.provider.ContactsContract

You will see numerous classes whose parent context is ContactsContract; these are useful in querying, reading, updating, and inserting contacts into and from the content database. The primary documentation for using the Contacts API is available on the Android site at

https://developer.android.com/guide/topics/providers/contacts-provider.html

The primary API entry point ContactsContract is appropriately named because this class defines the contract between the clients of the contacts and the provider and protector of the contacts database.

This chapter explores this contract in a fair amount of detail but does not cover every nuance. The Contacts API is large and its tentacles far-reaching. However, when you approach the Contacts API, it will take a few weeks of research to realize that it is simple in its underlying structure. This is where we would like to contribute the most and explain these basics in the time it takes to read this chapter.

Android 4.0 has extended the idea of contacts to include a user profile, similar to a user profile in a social network. A user profile is a dedicated contact that represents the owner of the device. Most of the general contact-based concepts remain the same. We will cover how the Contacts API is extended to support a user profile.

Understanding Accounts

All contacts in Android work in the context of an account. What is an account? Well, for example, if you have your e-mail through Google, you are said to have an account with Google. If you set up yourself as a user of Facebook, you are said to have an account with Facebook. You will be able to set up these accounts through the “Accounts & sync” Settings option on the device. See the Android User’s Guide to get more details around accounts and how to set them up.

The contacts you manage are tied to a specific account. An account owns its set of contacts—or an account is said to be the parent of a contact. An account is identified by two strings: the account name and the account type. In case of Google, your account name is your e-mail user name at Gmail and your account type is com.google. The account type must be unique across the device. Your account name is unique within that account type. Together, an account type and an account name form an account, and only once the account is formed can a set of contacts be inserted for that account.

Enumerating Accounts

The Contacts API primarily deals with contacts that exist in various accounts. The mechanism of creating accounts is outside of the Contacts API, so explaining the ability to write your own account providers and how to sync the contacts within those accounts is outside the scope of this chapter. You can understand and benefit from this chapter without going into the details of how accounts get set up. However, when you want to add a contact or a list of contacts, you do need to know what accounts exist on the device. You can use the code in Listing 27-1 to enumerate the accounts and their properties (the account name and type). Code in Listing 27-1 lists the account name and type given a context variable such as an activity.

Listing 27-1. Code to Display a List of Accounts

public void listAccounts(Context ctx) {
    AccountManager am = AccountManager.get(ctx);
    Account[] accounts = am.getAccounts();
    for(Account ac: accounts) {
        String account_name=ac.name;
        String account_type = ac.type;
        Log.d("accountInfo", account_name + ":" + account_type);
    }
}

To run the code in Listing 27-1, the manifest file needs to ask for permission using the line in Listing 27-2.

Listing 27-2. Permission to Read Accounts

<uses-permission android:name="android.permission.GET_ACCOUNTS"/>

The code from Listing 27-1 will print something like the following:

Your-email-at-gmail:com.google

This assumes that you have only one account (Google) configured. If you have more than one account, all of those accounts will be listed in a similar manner.

Using the contacts application on the device you can add, edit, and delete contacts to any of your existing accounts.

Understanding Contacts

Contacts owned by an account are called raw contacts. A raw contact has a variable set of data elements (for example, e-mail address, phone number, name, and postal address). Android presents an aggregated view of raw contacts by listing only once any raw contacts that seems to match. These aggregated contacts form the set of contacts you see when you open the contacts application.

We will now examine how contacts and their related data are stored in various tables. Understanding these contact tables and their associated views is key to understanding the Contacts API.

Examining the Contacts SQLite Database

One way to understand and examine the contacts database tables is to download the contacts database from the device or the emulator and open it using one of the SQLite explorer tools.

To download the contacts database, use the File Explorer shown in Figure 30-17 and navigate to the following directory on your emulator:

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

Depending on the release, the database file name may differ slightly, but it should be called contacts.db, contacts2.db, or something similar. In 4.0, the contacts provider uses a similarly structured but separate database file called profile.db to hold the contacts related to the personal profile.

Understanding Raw Contacts

The contacts you see in the contacts application are called aggregated contacts. Underneath each aggregated contact lies a set of contacts called raw contacts. An aggregated contact is a view on a set of similar raw contacts.

The set of contacts belonging to an account are called raw contacts. Each raw contact points to the details of one person in the context of that account. This is in contrast to an aggregated contact, which crosses account boundaries and belongs to the device as a whole. This relationship between an account and its set of raw contacts is maintained in the raw contacts table. Listing 27-3 shows the structure of the raw contacts table in the contacts database.

Listing 27-3. Raw Contact Table Definition

CREATE TABLE raw_contacts
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
is_restricted        INTEGER DEFAULT 0,
account_name         STRING DEFAULT NULL,
account_type         STRING DEFAULT NULL,
sourceid             TEXT,
version              INTEGER NOT NULL DEFAULT 1,
dirty                INTEGER NOT NULL DEFAULT 0,
deleted              INTEGER NOT NULL DEFAULT 0,
contact_id           INTEGER REFERENCES contacts(_id),
aggregation_mode     INTEGER NOT NULL DEFAULT 0,
aggregation_needed   INTEGER NOT NULL DEFAULT 1,
custom_ringtone      TEXT
send_to_voicemail    INTEGER NOT NULL DEFAULT 0,
times_contacted      INTEGER NOT NULL DEFAULT 0,
last_time_contacted  INTEGER,
starred              INTEGER NOT NULL DEFAULT 0,
display_name         TEXT,
display_name_alt     TEXT,
display_name_source  INTEGER NOT NULL DEFAULT 0,
phonetic_name        TEXT,
phonetic_name_style  TEXT,
sort_key             TEXT COLLATE PHONEBOOK,
sort_key_alt         TEXT COLLATE PHONEBOOK,
name_verified        INTEGER NOT NULL DEFAULT 0,
contact_in_visible_group  INTEGER NOT NULL DEFAULT 0,
sync1 TEXT, sync2         TEXT, sync3 TEXT, sync4 TEXT )

As with most Android tables, the raw contacts table has the _ID column that uniquely identifies a raw contact. Together, the field’s account_name and account_type identify the account to which this contact (specifically, the raw contact) belongs. The sourceid field indicates how this raw contact is uniquely identified in the account.

The field contact_id refers to the aggregated contact that this raw contact is one of. An aggregated contact points to one or more similar contacts that are essentially the same person set up among multiple accounts.

The field display_name points to the display name of the contact. This is primarily a read-only field. It is set by triggers based on the data rows added in the data table (which is covered in the next subsection) for this raw contact.

The sync fields are used by the account to sync contacts between the device and the server-side account such as Google mail.

Although we have used SQLite tools to explore these fields, there is more than one way to discover these fields. The recommended way is to follow the class definitions as declared in the ContactsContract API. To explore the columns belonging to a raw contact, you can look at the class documentation for ContactsContract.RawContacts.

There are advantages and disadvantages to this approach. A significant advantage is that you get to know the fields published and acknowledged by the Android SDK. The database columns may get added or dropped without changing the public interface. So if you use the database columns directly, they may or may not be there. Instead, if you use the public definitions for these columns, you are safe between releases.

One disadvantage, however, is that the class documentation has many other constants interspersed with column names; we kind of got lost in figuring out what was what. These numerous class definitions give the impression that the API is complex when, in reality, 80% of the class documentation for the Contacts API is to define constants for these columns and the URIs to access these rows.

When we exercise the Contacts API in later sections, we will use the class-documentation-based constants instead of direct column names. However, we felt the direct exploration of the tables was the quickest way to help you understand the Contacts API.

Let’s talk next about how the data relating to a contact (such as e-mail and phone number) is stored.

Understanding the Contacts Data Table

As seen from the raw contact table definition, the raw contact (in an anticlimactic sense) is just an ID indicating what account it belongs to. Data pertaining to the contact is not in the raw contact table but saved in the data table. Each data element, such as e-mail and phone number, is stored as separate rows in the data table tied by the raw contact ID. The data table, whose definition is shown in Listing 27-4, contains 16 generic columns that can store any type of data element such as an e-mail.

Listing 27-4. Contact Data Table Definition

CREATE TABLE data
(_id              INTEGER PRIMARY KEY AUTOINCREMENT,
package_id        INTEGER REFERENCES package(_id),
mimetype_id       INTEGER REFERENCES mimetype(_id) NOT NULL,
raw_contact_id    INTEGER REFERENCES raw_contacts(_id) NOT NULL,
is_primary        INTEGER NOT NULL DEFAULT 0,
is_super_primary  INTEGER NOT NULL DEFAULT 0,
data_version      INTEGER NOT NULL DEFAULT 0,
data1 TEXT,data2 TEXT,data3 TEXT,data4 TEXT,data5 TEXT,
data6 TEXT,data7 TEXT,data8 TEXT,data9 TEXT,data10 TEXT,
data11 TEXT,data12 TEXT,data13 TEXT,data14 TEXT,data15 TEXT,
data_sync1 TEXT, data_sync2 TEXT, data_sync3 TEXT, data_sync4 TEXT )

The raw_contact_id points to the raw contact to which this data row belongs. The mimetype_id points to the MIME type entry indicating one of the types identified in the contact data types in Listing 27-4. The columns data1 through data15 are generic string-based tables that can store anything that is necessary based on the MIME type. The sync fields support contact syncing. The table that resolves the MIME type IDs is in Listing 27-5.

Listing 27-5. Contacts MIME Type Lookup Table Definition

CREATE TABLE mimetypes
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
mimetype TEXT NOT NULL)

As with the raw contacts table, you can discover the data table columns through the helper class documentation for ContactsContract.Data. Although you can figure out the columns from this class definition, you will not know what is stored in each of the generic columns from data1 through data15. To know this, you will need to see the class definitions for a number of classes under the namespace ContactsContract.CommonDataKinds.

Some examples of these classes follow:

  • ContactsContract.CommonDataKinds.Email
  • ContactsContract.CommonDataKinds.Phone

In fact, you will see one class for each of the predefined MIME types. These classes are as follows: Email, Event, GroupMembership, Identity, Im, Nickname, Note, Organization, Phone, Photo, Relation, SipAddress, StructuredName, StructuredPostal, Website. Ultimately, all the CommonDataKinds classes do is indicate which generic data fields (data1 through data15) are in use and what for.

Understanding Aggregated Contacts

Ultimately, a contact and its related data are unambiguously stored in the raw contacts table and the data table. An aggregated contact, on the other hand, is heuristic and could be ambiguous.

When there is a contact that is the same between multiple accounts, you may want to see one name instead of seeing the same or similar name repeated once for every account. Android addresses this by aggregating contacts into a read-only view. Android stores these aggregated contacts in a table called contacts. Android uses a number of triggers on the raw contact table and the data table to populate or change this aggregated contact table.

Before going into explaining the logic behind aggregation, let us show you the contact table definition (see Listing 27-6).

Listing 27-6. Aggregated Contact Table Definition

CREATE TABLE contacts
(_id                  INTEGER PRIMARY KEY AUTOINCREMENT,
name_raw_contact_id   INTEGER REFERENCES raw_contacts(_id),
photo_id              INTEGER REFERENCES data(_id),
custom_ringtone       TEXT,
send_to_voicemail     INTEGER NOT NULL DEFAULT 0,
times_contacted       INTEGER NOT NULL DEFAULT 0,
last_time_contacted   INTEGER,
starred               INTEGER NOT NULL DEFAULT 0,
in_visible_group      INTEGER NOT NULL DEFAULT 1,
has_phone_number      INTEGER NOT NULL DEFAULT 0,
lookup                TEXT,
status_update_id      INTEGER REFERENCES data(_id),
single_is_restricted  INTEGER NOT NULL DEFAULT 0)

No client directly updates this table. When a raw contact is added with its detail, Android searches other raw contacts to see if there are similar raw contacts. If there is one, it will use the aggregated contact ID of that raw contact as the aggregated contact ID of the new raw contact as well. No entry is made into the aggregated contact table. If none is found, it will create an aggregated contact and use that aggregated contact as the contact ID for that raw contact.

Android uses the following algorithm to determine which raw contacts are similar:

  1. The two raw contacts have matching names, both first and last.
  2. The words in the name are the same but vary in order: “first last” or “first, last” or “last, first.”
  3. The shorter versions of the names match, such as “Bob” for “Robert.”
  4. If one of the raw contacts has just a first or last name, this will trigger a search for other attributes, such as phone number or e-mail, and if the other attributes match, the contact will be aggregated.
  5. If one of the raw contacts is missing the name altogether, this will also trigger a search for other attributes as in step 4.

Because these rules are heuristic, some contacts may be aggregated unintentionally. The client applications need to provide a mechanism to separate the contacts in such a case. If you refer to the Android User’s Guide, you will see that the default contacts application allows you to separate contacts that are unintentionally merged.

You can also prevent the aggregation by setting the aggregation mode when you insert the raw contact. The aggregation modes are shown in Listing 27-7.

Listing 27-7. Aggregation Mode Constants

AGGREGATION_MODE_DEFAULT
AGGREGATION_MODE_DISABLED
AGGREGATION_MODE_SUSPENDED

The first option is obvious; it is how aggregation works.

The second option (disabled) keeps this raw contact out of aggregation. Even if it is aggregated already, Android will pull it out of aggregation and allocate a new aggregated contact ID dedicated to this raw contact.

The third option (suspended) indicates that even though the properties of the contact may change, which will make it invalid for the aggregation into that batch of contacts, it should be kept tied to that aggregated contact.

The last point brings out the volatile dimension of the aggregated contact. Say you have a unique raw contact with a first name and a last name. Right now, it doesn’t match any other raw contact, so this unique raw contact gets its own allocation of an aggregated contact. The aggregated contact ID will be stored in the raw contact table against that raw contact row.

However, you go and change the last name of this raw contact, which makes it a match to another set of contacts that are aggregated. In that case, Android will remove the raw contact from this aggregated contact and move it to the other one, abandoning this single aggregated contact by itself. In this case, the ID of the aggregated contact becomes entirely abandoned, as it will not match anything in the future because it is just an ID without an underlying raw contact.

So an aggregated contact is volatile. There is not a significant value to hold on to this aggregated contact ID over time.

Android offers some respite from this predicament by providing a field called lookup in the aggregated contacts tables. This lookup field is an aggregation (concatenation) of the account and the unique ID of this raw contact in that account for each raw contact. This information is further codified so that it can be passed as a URL parameter to retrieve the latest aggregated contact ID. Android looks at the lookup key and sees which underlying raw contact IDs are there for this lookup key. It then uses a best-fit algorithm to return a suitable (or perhaps new) aggregated contact ID.

While we are explicitly examining the contacts database, let’s consider a couple of contact-related database views that are useful.

Exploring view_contacts

The first of these views is the view_contacts. Although there is a table that holds the aggregated contacts (contacts table), the API doesn’t expose the contacts table directly. Instead, it uses view_contacts as the target for reading the aggregated contacts. When you query based on the URI ContactsContract.Contacts.CONTENT_URI, the columns returned are based on this view view_contacts. The definition of view_contacts view is shown in Listing 27-8.

Listing 27-8. A View to Read Aggregated Contacts

CREATE VIEW view_contacts AS

SELECT contacts._id AS _id,
contacts.custom_ringtone                AS custom_ringtone,
name_raw_contact.display_name_source    AS display_name_source,
name_raw_contact.display_name           AS display_name,
name_raw_contact.display_name_alt       AS display_name_alt,
name_raw_contact.phonetic_name          AS phonetic_name,
name_raw_contact.phonetic_name_style    AS phonetic_name_style,
name_raw_contact.sort_key               AS sort_key,
name_raw_contact.sort_key_alt           AS sort_key_alt,
name_raw_contact.contact_in_visible_group AS in_visible_group,
has_phone_number,
lookup,
photo_id,
contacts.last_time_contacted           AS last_time_contacted,
contacts.send_to_voicemail             AS send_to_voicemail,
contacts.starred                       AS starred,
contacts.times_contacted               AS times_contacted, status_update_id

FROM contacts JOIN raw_contacts AS name_raw_contact
ON(name_raw_contact_id=name_raw_contact._id)

Notice that the view_contacts view combines the contacts table with the raw contact table based on the aggregated contact ID.

Exploring contact_entities_view

Another useful view is the contact_entities_view that combines the raw contacts table with the data table. This view allows us to retrieve all the data elements of a given raw contact one time, or even the data elements of multiple raw contacts belonging to the same aggregated contact. Listing 27-9 presents the definition of this view based on contact entities.

Listing 27-9. Contact Entities View

CREATE VIEW contact_entities_view AS

SELECT raw_contacts.account_name    AS account_name,
raw_contacts.account_type           AS account_type,
raw_contacts.sourceid               AS sourceid,
raw_contacts.version                AS version,
raw_contacts.dirty                  AS dirty,
raw_contacts.deleted                AS deleted,
raw_contacts.name_verified          AS name_verified,
package                             AS res_package,
contact_id,
raw_contacts.sync1                  AS sync1,
raw_contacts.sync2                  AS sync2,
raw_contacts.sync3                  AS sync3,
raw_contacts.sync4                  AS sync4,
mimetype, data1, data2, data3, data4, data5, data6, data7, data8,
data9, data10, data11, data12, data13, data14, data15,
data_sync1, data_sync2, data_sync3, data_sync4,

raw_contacts._id                    AS _id,

is_primary, is_super_primary,
data_version,
data._id                            AS data_id,
raw_contacts.starred                AS starred,
raw_contacts.is_restricted          AS is_restricted,
groups.sourceid                     AS group_sourceid

FROM raw_contacts LEFT OUTER JOIN data
   ON (data.raw_contact_id=raw_contacts._id)
LEFT OUTER JOIN packages
  ON (data.package_id=packages._id)
LEFT OUTER JOIN mimetypes
  ON (data.mimetype_id=mimetypes._id)
LEFT OUTER JOIN groups
  ON (mimetypes.mimetype='vnd.android.cursor.item/group_membership'
    AND groups._id=data.data1)

The URIs needed to access this view are available in the class ContactsContract.RawContactsEntity.

Working with the Contacts API

So far, we have explored the basic idea behind the Contacts API by exploring its tables and views. We will now present a number of code snippets that can be used to explore contacts. These snippets are taken from the sample application that is developed to support this chapter. Although the snippets are taken from the sample application, they are sufficient to aid the understanding of how the Contacts API work. You can download the full sample program using the project download URL at the end of this chapter.

Exploring Accounts

We will start our exercise by writing a program that can print out the list of accounts. We have already given the code snippets necessary to get a list of accounts. Consider the class AccountsFunctionTester in Listing 27-10.

Listing 27-10. AccountsFunctionTester That Prints Available Accounts

//Java class: AccountsFunctionTester.java
//Menu to invoke this: Accounts
//BaseTester is a supporting base class holding the parent activity
// and some reused common variables. See the source code if you are more curious.
public class AccountsFunctionTester extends BaseTester {
    private static String tag = "tc>";

    //IReportBack is a simple logging interface that writes log messages
    //to the main activity and also to the log.
    public AccountsFunctionTester(Context ctx, IReportBack target) {
        super(ctx, target);
    }
    public void testAccounts() {
        AccountManager am = AccountManager.get(this.mContext);
        Account[] accounts = am.getAccounts();
        for(Account ac: accounts) {
            String acname=ac.name;
            String actype = ac.type;
            this.mReportTo.reportBack(tag,acname + ":" + actype);
        }
    }
}

Note  As we present and explore the Java code necessary to work with contacts, you will see three variables repeatedly used in the presented source code:

mContext: A variable pointing to an activity

mReportTo: A variable implementing a logging interface (IReportBack—you can see this Java file in the downloadable project) that can be used to log messages to the test activity that is used for this chapter

Utils: A static class that encapsulates very simple utility methods

We have chosen not to list these classes here because they will distract you from understanding the core functionality of the Contacts API. You can examine these classes in the downloadable project.

All the code in this chapter uses an unmanaged query against the content provider. This is done by calling Activity.getContentResolver().query(). This is because we merely read the data and print out the results right away. If your goal instead is to use UI (through activities or fragments) as a target to display your contacts then read Chapter 27 on loaders. Loaders show the right way to display cursors from any content provider.

When you run the sample program that you can download for this chapter, you will see a main activity that appears with a number of menu options. The menu option “Accounts” will print the list of accounts available on the device.

Exploring Aggregated Contacts

Let’s see how we can explore aggregated contacts through code snippets. To read contacts, you need to request the following permission in the manifest file:

android.permission.READ_CONTACTS

As the functionality we are testing deals with content providers, URIs, and cursors, let’s look at some useful code snippets presented in Listing 27-11. (These code snippets are available either in utils.java or in some of the base classes derived from BaseTester in the chapter’s downloadable project.)

Listing 27-11. Getting a Cursor Given a URI and a where Clause

//Utils.java
//Retrieve a column from a cursor
public static String getColumnValue(Cursor cc, String cname) {
   int i = cc.getColumnIndex(cname);
   return cc.getString(i);
}
//See what columns are there  in a cursor
protected static String getCursorColumnNames(Cursor c) {
   int count = c.getColumnCount();
   StringBuffer cnamesBuffer = new StringBuffer();
    for (int i=0;i<count;i++) {
       String cname = c.getColumnName(i);
       cnamesBuffer.append(cname).append(';'),
    }
    return cnamesBuffer.toString();
}

//From URIFunctionTester.java, baseclass of some of the other testers
//Given a URI and a where clause return a cursor
protected Cursor getACursor(Uri uri,String clause) {
   Activity a = (Activity)this.mContext; //mContext coming from BaseTester
   return a.getContentResolver().query(uri, null, clause, null, null);
}

In this section, we are primarily exploring the cursor returned by aggregated contact URIs. Each row returned by the resulting contact cursor will have a number of fields. For our example, we are not interested in all the fields but only a few. You can abstract this out into another class called an AggregatedContact. Listing 27-12 shows this class.

Listing 27-12. An Object Definition for a Few Fields of an Aggregated Contact

//AggregatedContact.java
public class AggregatedContact {
    public String id;
    public String lookupUri;
    public String lookupKey;
    public String displayName;
    public void fillinFrom(Cursor c) {
        id = Utils.getColumnValue(c,"_ID");
        lookupKey = Utils.getColumnValue(c,ContactsContract.Contacts.LOOKUP_KEY);
        lookupUri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupKey;
        displayName = Utils.getColumnValue(c,ContactsContract.Contacts.DISPLAY_NAME);
    }
}

In Listing 27-12 we use the cursor to load up the fields that we are interested in.

Getting the Aggregated Contacts Cursor

Listing 27-13 shows how to retrieve a cursor that is a collection of aggregated contacts.

Listing 27-13. Getting a Cursor for All Aggregated Contacts

//Get a cursor of all contacts. Specify the where clause as null to indicate all rows.
//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts Cursor
private Cursor getContacts() {
    Uri uri = ContactsContract.Contacts.CONTENT_URI;
    //Specify ascending or descending way to sort names
    String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
                         + " COLLATE LOCALIZED ASC";
    Activity a = (Activity)this.mContext; //Local variable pointing to an activity
    return a.getContentResolver().query(uri, null, null, null, sortOrder);
}

The URI used to read all the contacts is ContactsContract.Contacts.CONTENT_URI. You can pass this URI to the query()function to retrieve a cursor. You can pass null as the column projection to receive all columns. Although this is not recommended in practice, in our case, it makes sense because we want to know about all the columns it returns. We have also used the display name of the contact as the sort order. Notice how, again, we have used ContactContract.Contacts to get the column name for the contact display name. If you were to print the field names from this cursor you will see the returned fields as those shown in Listing 27-14. Depending on the release the order may be different and more columns may be added. It is a good practice to explicitly specify a projection to the query clause; that way your code will work across releases.

Listing 27-14. Aggregated Contacts Content URI Cursor Columns

times_contacted; contact_status; custom_ringtone; has_phone_number; phonetic_name;
phonetic_name_style; contact_status_label; lookup; contact_status_icon; last_time_contacted;
display_name; sort_key_alt; in_visible_group; _id; starred; sort_key; display_name_alt;
contact_presence; display_name_source; contact_status_res_package; contact_status_ts;
photo_id; send_to_voicemail;

Reading Aggregated Contact Details

Now that we’ve explored the columns available with the contacts content URI, let’s pick a few columns and see what contact rows are available. We are interested in the following columns from a contact cursor: display name, lookup key, and lookup URI. We are considering these fields because we want to see what the lookup key and lookup key URI look like based on what is covered in the theory part of this chapter. Specifically, we are interested in firing off the lookup URI to see what type of a cursor it returns.

The function listContacts() in Listing 27-15 gets a contacts cursor and prints these three columns for each row of the cursor. Note that this listing is taken from a class that holds a local variable called mContext to indicate the activity and a local variable called mReportTo to be able to log any messages to the activity.

Listing 27-15. Printing the Lookup Keys for an Aggregated Contact

//Java class: AggregatedContactFunctionTester.java
//Menu item to invoke: Contacts
public void listContacts() {
    Cursor c = null;
    try {
        c = getContacts();
        int i = c.getColumnCount();
        this.mReportTo.reportBack(tag, "Number of columns:" + i);
        this.printLookupKeys(c);
    }
   finally { if (c!= null) c.close(); }
}
private void printLookupKeys(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        String name=this.getContactName(c);
        String lookupKey = this.getLookupKey(c);
        String luri = this.getLookupUri(lookupKey);
        this.mReportTo.reportBack(tag, name + ":" + lookupKey); //log
        this.mReportTo.reportBack(tag, name + ":" + luri); //log
    }
}
private String getLookupKey(Cursor cc) {
    int lookupkeyIndex = cc.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
    return cc.getString(lookupkeyIndex);
}
private String getContactName(Cursor cc){
    return Utils.getColumnValue(cc,ContactsContract.Contacts.DISPLAY_NAME);
}
private String getLookupUri(String lookupkey) {
    String luri = ContactsContract.Contacts.CONTENT_LOOKUP_URI + "/" + lookupkey;
    return luri;
}

Exploring the Lookup URI-Based Cursor

Now that we know how to extract lookup URIs for a given aggregated contact, let’s see what we can do with a lookup URI.

The function listLookupUriColumns() in Listing 27-16 will take the first contact from the list of all contacts and then formulate a lookup URI for that contact and fire off the URI to see what kind of a cursor it returns by printing the column names from that cursor.

Listing 27-16. Exploring the Lookup URI Cursor

//Class: AggregatedContactFunctionTester.java, Menu item to invoke: Single Contact Cursor
public void listLookupUriColumns() {
    Cursor c = null;
    try {
        c = getContacts();
        String firstContactLookupUri = getFirstLookupUri(c);
        printLookupUriColumns(firstContactLookupUri);
    }
    finally { if (c!= null) c.close(); }
}
private String getFirstLookupUri(Cursor c) {
    c.moveToFirst();
    if (c.isAfterLast()) {
        Log.d(tag,"No rows to get the first contact");
        return null;
    }
    String lookupKey = this.getLookupKey(c);
    return  this.getLookupUri(lookupKey);
}
public void printLookupUriColumns(String lookupuri) {
    Cursor c = null;
    try {
        c = getASingleContact(lookupuri);
        int i = c.getColumnCount();
        this.mReportTo.reportBack(tag, "Number of columns:" + i);
        int j = c.getCount();
        this.mReportTo.reportBack(tag, "Number of rows:" + j);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null)c.close(); }
}
// Use the lookup uri, retrieve a single aggregated contact
private Cursor getASingleContact(String lookupUri) {
    Activity a = (Activity)this.mContext;
    return a.getContentResolver().query(Uri.parse(lookupUri), null, null, null, null);
}

As it turns out, it just returns a cursor (as in Listing 27-14) that is identical in columns for that of the aggregated contact cursor as in Listing 27-13, except that it has only one row pointing to the contact for which this is the lookup key. Also notice that we have used the following lookup URI definition:

ContactsContract.Contacts.CONTENT_LOOKUP_URI

You know from the discussion of the contact lookup URIs that each lookup URI represents a collection of raw contact identities that have been concatenated. That being the case, you might have expected the lookup URI to return a series of matching raw contacts. However, the test in Listing 27-16 is showing that it is not returning a cursor of raw contacts but instead a cursor of contacts.

Note  A lookup based on the contact lookup URI returns an aggregated contact and not a raw contact.

Another tidbit is that the lookup process for the aggregated contact based on the lookup URI is not linear or exact. This means Android will not look for an exact match of the lookup key. Instead, Android parses the lookup key into its constituent raw contacts and then finds the aggregated contact ID that matches the most of the raw contact records and returns that aggregated contact record.

One consequence of this is that no public mechanism is available to go from the lookup key to its constituent raw contacts. Instead, you have to find the contact ID for that lookup key and then fire off a raw contact URI for that contact ID to retrieve the corresponding raw contacts.

Here is another code snippet that shows what is returned from a cursor as an object instead of as a set of columns. The code in Listing 27-17 returns the first aggregated contact as an object.

Listing 27-17. Code Testing Aggregated Contacts

//Java class: AggregatedContactFunctionTester.java
protected AggregatedContact getFirstContact() {
    Cursor c=null;
    try {
        c = getContacts(); c.moveToFirst();
        if (c.isAfterLast()) {
            Log.d(tag,"No contacts");
            return null;
        }
        AggregatedContact firstcontact = new AggregatedContact();
        firstcontact.fillinFrom(c);
        return firstcontact;
    }
    finally { if (c!=null) c.close(); }
}

Exploring Raw Contacts

In Listing 27-18, the file RawContact.java, captures a few important fields from the raw contacts table cursor. (This file, like all other code snippets in this chapter, is available in the downloadable project for this chapter.)

Listing 27-18. Source code for RawContact.java

//Class: RawContact.java
public class RawContact  {
    public String rawContactId;
    public String aggregatedContactId;
    public String accountName;
    public String accountType;
    public String displayName;

    public void fillinFrom(Cursor c) {
        rawContactId = Utils.getColumnValue(c,"_ID");
        accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
        accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
        aggregatedContactId = Utils.getColumnValue(c,
                                        ContactsContract.RawContacts.CONTACT_ID);
        displayName = Utils.getColumnValue(c,"display_name");
    }
    public String toString() { //..prints the public fields. See the download project for details }
}//eof-class

Showing the Raw Contacts Cursor

As with the aggregated contact URIs, let’s first examine the nature of the raw contact URI and what it returns. The signature for the raw contact URI is defined as follows:

ContactsContract.RawContacts.CONTENT_URI

The function showRawContactsCursor() in Listing 27-19 prints the cursor columns for a raw contacts URI.

Listing 27-19. Exploring the Raw Contacts Cursor

//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts Cursor
public void showRawContactsCursor() {
    Cursor c = null;
    try {
        c = this.getACursor(ContactsContract.RawContacts.CONTENT_URI,null);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null) c.close(); }
}

Code in Listing 27-19 will show that the raw contact cursor has the fields shown in Listing 27-20 (this list seem to vary somewhat with each device).

Listing 27-20. Raw Contacts Cursor Fields

times_contacted; phonetic_name; phonetic_name_style; contact_id;version; last_time_contacted;
aggregation_mode; _id; name_verified; display_name_source; dirty; send_to_voicemail; account_type; custom_ringtone; sync4;sync3;sync2;sync1; deleted; account_name; display_name;
sort_key_alt; starred; sort_key; display_name_alt; sourceid;

Seeing the Data Returned by a Raw Contacts Cursor

Listing 27-21 shows the method showAllRawContacts(), which prints all the rows in the raw contacts cursor.

Listing 27-21. Displaying Raw Contacts

//Java class: RawContactFunctionTester.java; Menu item: All Raw Contacts
public void showAllRawContacts(){
    Cursor c = null;
    try {
        c = this.getACursor(getRawContactsUri(), null);
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
private void printRawContacts(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        RawContact rc = new RawContact();
        rc.fillinFrom(c);
        this.mReportTo.reportBack(tag, rc.toString()); //log
    }
}

Constraining Raw Contacts with a Corresponding Set of Aggregated Contacts

Using the columns of the cursor in Listing 27-20, let’s see if we can refine our query to retrieve the contacts for a given aggregated contact ID. The code in Listing 27-22 will look up the first aggregated contact and then issue a raw contact URI with a where clause specifying a value for the contact_id column.

Listing 27-22. Getting Raw Contacts for an Aggregated Contact

//Java class: RawContactFunctionTester.java; Menu item: Raw Contacts
public void showRawContactsForFirstAggregatedContact(){
    AggregatedContact ac = getFirstContact();
    Cursor c = null;
    try {
        c = this.getACursor(getRawContactsUri(), getClause(ac.id));
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
private String getClause(String contactId) {
    return "contact_id = " + contactId;
}

Exploring Raw Contact Data

Because a data row belonging to a raw contact contains a number of fields, we have created a Java class called ContactData.java, shown in Listing 27-23, to capture a representative set of the contact data, and not all fields.

Listing 27-23. Source code for ContactData.java

//ContactData.java
public class ContactData {
    public String rawContactId;
    public String aggregatedContactId;
    public String dataId;
    public String accountName;
    public String accountType;
    public String mimetype;
    public String data1;

    public void fillinFrom(Cursor c) {
        rawContactId = Utils.getColumnValue(c,"_ID");
        accountName = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_NAME);
        accountType = Utils.getColumnValue(c,ContactsContract.RawContacts.ACCOUNT_TYPE);
        aggregatedContactId =
               Utils.getColumnValue(c,ContactsContract.RawContacts.CONTACT_ID);
        mimetype = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.MIMETYPE);
        data1 = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA1);
        dataId = Utils.getColumnValue(c,ContactsContract.RawContactsEntity.DATA_ID);
    }
    public String toString()   {//just a concatenation of fields for logging }
}

Android uses a view called a RawContactEntity view to retrieve data from a raw contact table and the corresponding data tables as indicated in the section “contact_entities_view” in this chapter. The URI to access this view is in Listing 27-24.

Listing 27-24. Raw Entities Content URI

ContactsContract.RawContactsEntity.CONTENT_URI

Let’s see how this URI can be used to discover field names returned by this URI:

//Java class: ContactDataFunctionTester.java; Menu item: Contact Entity Cursor
public void showRawContactsEntityCursor(){
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,null);
        this.printCursorColumnNames(c);
    }
    finally { if (c!=null) c.close(); }
}

The code in Listing 27-24 prints out the list of columns shown in Listing 27-25. So the columns in Listing 27-25 are the columns that are returned by the raw contacts entity cursor. There may be additional columns depending on vendor-specific implementations.

Listing 27-25. Contact Entities Cursor Columns

data_version; contact_id; version; data12;data11;data10; mimetype; res_package;
_id; data15;data14;data13; name_verified; is_restricted; is_super_primary; data_sync1;dirty;data_sync3;data_sync2; data_sync4;account_type;data1;sync4;sync3;
data4;sync2;data5;sync1; data2;data3;data8;data9; deleted; group_sourceid; data6;data7;
account_name; data_id; starred; sourceid; is_primary;

Once you know this set of columns, you can filter the result set of this cursor by formulating a proper where clause. However, you want to use the ContactsContract Java class to use the definitions for these column names. For example, in Listing 27-26 we retrieve the data elements pertaining to contact IDs 3, 4, and 5.

Listing 27-26. Displaying Data Elements from RawContactsEntity

//Java class: ContactDataFunctionTester.java; Menu item: Contact Data
public void showRawContactsData(){
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,"contact_id in (3,4,5)");
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}
protected void printRawContactsData(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        ContactData dataRecord = new ContactData();
        dataRecord.fillinFrom(c);
        this.mReportTo.reportBack(tag, dataRecord.toString());
    }
}

Code in Listing 27-26 will print such things as name, e-mail address, and MIME type as defined in the ContactData object in Listing 27-23.

Adding a Contact with Its Details

Let’s look at a code snippet to add a contact with name, e-mail, and phone number. To write to contacts, you need the following permission in the manifest file:

android.permission.WRITE_CONTACTS

Code in Listing 27-27 adds a raw contact followed by adding two data rows (name and phone number) for that contact.

Listing 27-27. Adding a Contact

//Java class: AddContactFunctionTester.java; Menu item: Add Contact
public void addContact(){
    long rawContactId = insertRawContact();
    this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
    insertName(rawContactId);
    insertPhoneNumber(rawContactId);
    showRawContactsDataForRawContact(rawContactId);
}
private long insertRawContact(){
    ContentValues cv = new ContentValues();
    cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
    cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id -- ");
    Uri rawContactUri =
        this.mContext.getContentResolver()
             .insert(RawContacts.CONTENT_URI, cv);
    long rawContactId = ContentUris.parseId(rawContactUri);
    return rawContactId;
}
private void insertName(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    cv.put(StructuredName.DISPLAY_NAME,"John Doe_" + rawContactId);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertPhoneNumber(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    cv.put(Phone.NUMBER,"123 123 " + rawContactId);
    cv.put(Phone.TYPE,Phone.TYPE_HOME);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void showRawContactsDataForRawContact(long rawContactId) {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.CONTENT_URI;
        c = this.getACursor(uri,"_id = " + rawContactId);
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}

Code in Listing 27-27 does the following:

  1. Adds a new raw contact for a predefined account using the account’s name and type, represented by the method insertRawContact(). Notice how it uses the URI RawContact.CONTENT_URI.
  2. Takes the raw contact ID from step 1 and inserts a name record using the insertName() method in the data table. Notice how it uses the URI Data.CONTENT_URI.
  3. Takes the raw contact ID from step 1 and inserts a phone number record using the insertPhoneNumber() method in the data table. Being a data row, it uses Data.CONTENT_URI as the URI.

Listing 27-27 also demonstrates the column aliases used in inserting records. Notice how constants like Phone.TYPE and Phone.NUMBER point to the generic data table column names data1 and data2.

Controlling Aggregation of Contacts

Clients that update or insert contacts do not explicitly change the contacts table. The contacts table is updated by triggers that look into the raw contact table and raw contact data table.

Raw contacts that get added or changed, in turn, affect the aggregated contacts in the contacts table. However, you may not want to allow two contacts to be aggregated.

You can control the aggregation behavior of a raw contact by setting the aggregation mode when that contract is created. As you can see from the raw contact table columns in Listing 27-20, the raw contact table contains a field called aggregation_mode. Values for the aggregation mode are shown in Listing 27-7 and explained in the section “Aggregated Contacts.”

You can also keep two contacts always apart by inserting rows into a table called agg_exceptions. The URIs needed to insert into this table are defined in the Java class ContactsContract.AggregationExceptions. The table structure of agg_exceptions is shown in Listing 27-28.

Listing 27-28. Aggregate Exceptions Table Definition

CREATE TABLE agg_exceptions
(_id INTEGER PRIMARY KEY AUTOINCREMENT,
type INTEGER NOT NULL,
raw_contact_id1 INTEGER REFERENCES raw_contacts(_id),
raw_contact_id2 INTEGER REFERENCES raw_contacts(_id))

The type column in Listing 27-28 holds one of the integer constants in Listing 27-29.

Listing 27-29. Aggregation Types in the Aggregation Exception Table

ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER
ContactsContract.AggregationExceptions.TYPE_KEEP_SEPARATE
ContactsContract.AggregationExceptions.TYPE_AUTOMATIC

TYPE_KEEP_TOGETHER says the two raw contacts should never be broken apart. TYPE_KEEP_SEPARATE says that these raw contacts should never be joined. TYPE_AUTOMATIC says to use the default algorithm to aggregate contacts.

The URI you will use to insert, read, and update this table is defined as

ContactsContract.AggregationExceptions.CONTENT_URI

Constants for field definitions to work with this table are also available in the Java class ContactsContract.AggregationExceptions.

Understanding Personal Profile

A personal profile, introduced in API 14, is like a contact, except there is only one personal profile contact. That is the singular you, on your device.

However, as an implementation detail, all data pertaining to the singular personal profile contact is maintained in a separate database called profile.db. Our research shows that this database has a structure identical to contacts2.db. This means you already know what relevant tables are available and what the columns of each table are.

Being a single contact, the aggregation is straightforward. Every raw contact that is added to the personal profile is expected to belong to the singular aggregated contact. If one doesn’t exist, then a new aggregated contact is created and placed in the new raw contact. If one exists, that contact ID is used as the aggregated contact ID for the raw contact.

The Android SDK uses the same base class ContactsContract to define the necessary URIs to read/update/delete/add raw contacts to the personal profile. These URIs parallel their counterparts but with the string "PROFILE" somewhere in them. Listing 27-30 shows a few of these URIs.

Listing 27-30. Profile-Based URIs Introduced in 4.0

//Relates to profile aggregated contact
ContactsContract.Profile.CONTENT_URI

//Relates to profile based raw contact
ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI

//Relates to profile based raw contact + profile based data table
ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

Listing 27-30 shows we have separate URIs when dealing with aggregated contact and a raw contact. However, there isn’t a corresponding personal profile URI for the Data table. The same Data URI, Data.CONTENT_URI, is applicable to both regular contact data and also the profile contact data.

Also note that the same content provider serves the needs of both the personal profile and regular contacts. Internally, this content provider knows based on the raw contact ID if the data URI belongs to the profile data or the regular contact data.

Let’s look next at code snippets to read and add contact data to the personal profile. You will need the permissions from Listing 27-31 to read from and write to the profile data.

Listing 27-31. Permissions Reading/Writing Profile Data

<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>

Reading Profile Raw Contacts

Let’s use the following URI to read the raw contacts that belong to the personal profile:

ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI

Listing 27-32 shows how to read profile raw contact entries.

Listing 27-32. Showing All Profile Raw Contacts

//Java class: ProfileRawContactFunctionTester.java; Menu item: PRaw Contacts
//In the download this method is named showAllRawContacts
//It is expanded here for clarity.
public void showAllRawProfileContacts() {
    Cursor c = null;
    try {
        String whereClause = null;
        c = this.getACursor(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI,
            whereClause);
        this.printRawContacts(c);
    }
    finally { if (c!=null) c.close(); }
}
//In the download this method is named printRawContacts
//It is expanded here for clarity.
private void printRawProfileContacts(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        RawContact rc = new RawContact();
        rc.fillinFrom(c);
        this.mReportTo.reportBack(tag, rc.toString());
    }
}

Notice that once we retrieve the cursor, the data it contains matches the RawContact that we defined earlier for a regular raw contact.

Reading Profile Contact Data

Let’s use the following URI to read the various data elements (such as e-mail, MIME type, and so on) of raw contacts that belong to the personal profile:

ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

Notice how we are using a similar view as in the case of regular contacts. The RawContactEntity is a join between raw contacts and the data rows belonging to that raw contact. We will see one row for each data element such as name, e-mail, MIME type, and so on.

Listing 27-33 shows the code snippet to read profile raw contact entries.

Listing 27-33. Showing Data Elements for Profile Contacts

//Java class: ProfileContactDataFunctionTester.java; Menu item: all p raw contacts
public void showProfileRawContactsData() {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
        String whereClause = null;
        c = this.getACursor(uri,whereClause);
        this.printProfileRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}
protected void printProfileRawContactsData(Cursor c) {
    for(c.moveToFirst();!c.isAfterLast();c.moveToNext()) {
        ContactData dataRecord = new ContactData();
        dataRecord.fillinFrom(c);
        this.mReportTo.reportBack(tag, dataRecord.toString());
    }
}

Notice that once we retrieve the cursor, the data it contains matches the ContactData object (Listing 27-23) that we defined earlier for a regular raw contact data element.

Adding Data to the Personal Profile

Let’s use the following URI to add a raw contact to a personal profile:

ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI

We will also add a few data elements such as a phone number and a nickname to that raw contact so they appear in the details of your personal profile on the device. Listing 27-34 shows the code snippet.

Listing 27-34. Adding a Profile Raw Contact

//Java class: AddProfileContactFunctionTester.java; Menu item: all p raw contacts
//In the source code you won't see the word "profile" in the following method names
//It is added here to add clarity as the whole class is not included
public void addProfileContact() {
    long rawContactId = insertProfileRawContact();
    this.mReportTo.reportBack(tag, "RawcontactId:" + rawContactId);
    insertProfileNickName(rawContactId);
    insertProfilePhoneNumber(rawContactId);
    showProfileRawContactsDataForRawContact(rawContactId);
}
private void insertProfileNickName(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    //cv.put(Data.IS_USER_PROFILE, "1");
    cv.put(Data.MIMETYPE, CommonDataKinds.Nickname.CONTENT_ITEM_TYPE);
    cv.put(CommonDataKinds.Nickname.NAME,"PJohn Nickname_" + rawContactId);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private void insertProfilePhoneNumber(long rawContactId) {
    ContentValues cv = new ContentValues();
    cv.put(Data.RAW_CONTACT_ID, rawContactId);
    cv.put(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    cv.put(Phone.NUMBER,"P123 123 " + rawContactId);
    cv.put(Phone.TYPE,Phone.TYPE_HOME);
    this.mContext.getContentResolver().insert(Data.CONTENT_URI, cv);
}
private long insertProfileRawContact() {
    ContentValues cv = new ContentValues();
    cv.put(RawContacts.ACCOUNT_TYPE, "com.google");
    cv.put(RawContacts.ACCOUNT_NAME, "--use your gmail id --");
    Uri rawContactUri =
        this.mContext.getContentResolver()
             .insert(ContactsContract.Profile.CONTENT_RAW_CONTACTS_URI, cv);
    long rawContactId = ContentUris.parseId(rawContactUri);
    return rawContactId;
}
private void showProfileRawContactsDataForRawContact(long rawContactId) {
    Cursor c = null;
    try {
        Uri uri = ContactsContract.RawContactsEntity.PROFILE_CONTENT_URI;
        c = this.getACursor(uri,"_id = " + rawContactId);
        this.printRawContactsData(c);
    }
    finally { if (c!=null) c.close(); }
}

The code in Listing 27-34 parallels the code we used to add a regular contact and its details (Listing 27-27). Although we have used a profile-specific URI to add a raw contact, we have used the same Data.CONTENT_URI to add the individual data elements.

Note the following commented-out code in Listing 27-34:

//cv.put(Data.IS_USER_PROFILE, "1");

Because Data.CONTENT_URI is not specific to the profile, how does the underlying content provider know whether to insert this data into a regular raw contact or a personal profile raw contact? We thought that specifying a column called IS_USER_PROFILE would help the content provider. Apparently not. This new column is available primarily for read purposes. Your inserts will fail if you specify this during inserts. The only conclusion then is that the content provider is relying on the raw contact ID to see whether that raw contact came from profile.db or contacts2.db.

Role of Sync Adapters

So far, we have mainly talked about manipulating the contacts on the device. However, accounts and their contacts on Android work hand in hand with server-based contacts. For example, if you have created a Google account on your Android phone, the Google account will pull your Gmail contacts and make them available on the device. To do this syncing Android provides a synchronization framework which does most of the groundwork as long as you write a conforming Sync adapter. Android’s synchronization framework takes care of network availability, optional authentication, and scheduling.

Implementing a sync adapter involves implementing a service by extending the SDK class AbstractThreadedSyncAdapter and doing the work in the method onPerformSync(). Work involved in this method is to load data from servers and update the contacts using the Contacts API that is discussed in this chapter. Then, a sync-adapter resource file (XML) needs to be created on the device that will describe how this service is tied to the account that needs to be synched.

Outside of this basic understanding, due to space limitations we have not covered the syncing API in this edition of the book. Android SDK documentation has some documentation and samples.

Synchronization of contacts has impacts on deleting contacts on the device. When you delete a contact using the aggregated contact URI, it will delete all its corresponding raw contacts and the data elements of each of those raw contacts. However, Android will only mark them as deleted on the device and expects the background sync to actually sync with the server and then delete the contacts permanently from the device. This cascading of deletes also happens at the raw contact level where the corresponding data elements of that raw contact are deleted.

Using Batch Operations to Optimize ContentProvider Updates

While covering content providers in Chapter 26 we indicated that we would cover the batch operations in this chapter.

Reconsider how a raw contact and its associated data elements are created earlier in the chapter. Notice the multiple commands we need to send to the contacts provider to insert a raw contact. First we have to insert raw contact. Then use that ID to insert multiple data elements belonging to that raw contact. Each of these inserts is a separate command sent to the content provider independently.

There are two issues when we send these multiple commands sequentially. The first issue is that the content provider is not aware that they belong to a single commit unit. The second issue is that it will take longer to update the content provider database as each transaction is committed by itself.

These two issues are addressed by the batch update API available for any content provider including the contact provider.

Idea of Batching Content Provider Updates

In the batching approach, each content provider update operation is encapsulated in an object called “ContentProviderOperation” along with the URI and all the necessary key/value pairs to perform that operation. Then you gather these operations into a list object. You then tell the content resolver to send the entire batch or list of commands to the content provider at the same time. Because the content provider knows these commands are in a batch, it applies the transactions appropriately either at the end or so often based on hints.

If an operation indicates that a transaction can be applied at the end of that operation, then the operations completed thus far will be committed. This allows you to sub-batch long updates of many rows in to smaller set of sub-rows. You can also indicate in an operation that one of the columns to be updated needs to use the key returned by an indexed previous operation. We will present now some sample code showing how these ideas work.

Listing 27-35 shows an example of creating a list object to hold a list of operations.

Listing 27-35. A Container for Content Provider Operations

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

Let us see now how to construct the individual operations to be added to that list in Listing 27-36.

Listing 27-36. Batching ContentProviderOperations

ContentProviderOperation.Builder op = ContentProviderOperation.newInsert(a content URI);
op.withValue(key, value);
//...more of these
ContentProviderOperation op1 = op.build();
ops.add(op1);

The key class is ContentProviderOperation and its corresponding Builder object. In the example here we are using the insert operation. For the rest of the methods see the class reference. Once we have a builder along with its associated content URI, we tell the builder to add set of key/value pairs that go along with that content URI. Once finished adding all the key/value pairs we produce the ContentProviderOperation from the builder and add it to the list. We then ask the content resolver to apply the batch of operations using the code in Listing 27-37.

Listing 27-37. Using a Content Resolved to Apply the Batch of Operations

activity.getContentResolver().applyBatch(contentProviderAuthority, ops);

In Listing 27-37 the argument contentProviderAuthority is the authority string pointing the content provider and the argument ops is the list of operations that should be applied as a batch to that content provider. This is an example of adding a series of update operations as a single transaction. Let us see now how to provide commit hints to so that commit operations can be done on smaller subsets from the given batch.

Batching Commits by Yielding

One problem with committing a large batch of commands as a single transaction is that this work can block other operations on the database. To help with this and also to help with too much work to be committed in a single transaction you can instruct an operation to yield. When the content provider recognizes the yield parameter on an operation it commits the work done and pauses to yield for other processes to run.

Notice how in the code in Listing 27-38 one of the operations is set to allow yield.

Listing 27-38. Using Yield in a ContentProviderOperation

ContentProviderOperation.Builder operationBuilder =
      ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
//...more of these key/value pairs when you have them
ContentProviderOperation op1 = operationBuilder.build();

//... Add More operations

//Mark the next operation as yield allowed
operationBuilder = ContentProviderOperation.newInsert(a content URI);
operationBuilder.withValue(key, value);
operationBuilder.withYieldAllowed(true); //it is ok to commit
ContentProviderOperation operationWithYield = operationBuilder.build();
ops.add(operationWithYield);

//... Add More operations and yield points as needed

//Finally apply the list of operations
activity.getContentResolver().apply(contentProviderAuthority, ops);

Using Back References

For one of the operations above you can use a back reference as shown in Listing 27-39.

Listing 27-39. Using a Back Reference in a ContentProviderOperation

//Take the key coming out of op1 and add it as the value
int indexOfTheOperationWhoseKeyYouNeed = 0;
op.withValueBackReference(mykey, indexOfTheOperationWhoseKeyYouNeed);

Code in Listing 27-39 is asking the content provider to run the operation indicated by list index indexOfTheOperationWhoseKeyYouNeed and take its generated primary key and use it as a value for the column that is set on the target operation. This is how you take the insert from raw contact and use its primary key as the key value for the data items belonging to that raw contact.

Optimistic Locking

In optimistic locking, you first apply the transactions without locking the underlying repository and see if any updates have been made since you know its value before. If so, cancel the transaction and retry it.

To make this in the batch mode, the API offers a type of operation called an assert query. In this type of operation the content provider makes the query and compares the values of the retrieved cursor for either the count or the values of certain keys. If they don’t match, it rolls back the transaction and raises an exception breaking the code flow. See this demonstrated in the code shown in Listing 27-40.

Listing 27-40. Using Optimistic Locking through newAssertQuery

try {
  //Read a raw contact for a particular raw contact id
  ContentProviderOperation.Builder assertOpBuilder =
              ContentProviderOperation.newAssertQuery(rawContactUri);
  //Make sure there is only one raw contact with that details
  assertOpBuilder.withExpectedCount(1);
  //Make sure the version column matches with you started with
  //If not throw an exception. We chose to compare the version number
  //column (field) in the raw contacts table to assert.
  assertOpBuilder.withValue(SyncColumns.VERSION, mVersion);
  //get this operation and add it to the operations list at the end
  //Apply the batch ...
  activityInstance.getContentResolver().applyBatch(...);
}
//for this or other exceptions
catch (OperationApplicationException e) {
  //The batch is already cancelled
  //Tell the user the update failed
  //Show the user the new details and repeat the process
}

Reusing the Contact Provider UI

Contact provider capability in Android also defines a set of intents that can be used to reuse the UI available in the contacts application.

There are three kinds of intents. There is a set of intents that the contact provider fires based on the events taking place in the content provider UI application. For example, the intent INVITE_CONTACT is fired when the user clicks the “invite to the network” button on a contact in the contact application. An application can register for this event and read the contact details.

There is another set of intents that are used when the contact provider acts as a search provider for your custom activities. Using this facility you can search for a contact in your custom application through search suggestions.

There is another set of intents that external applications can fire to reuse the UI that is provided by the contact application. You can use these intents to pick from a list of contacts, or from a list of phone numbers, or from a list of addresses, or from a list of e-mails. You can also use these intents to update a contact or create a contact using the UI provided the Android application.

These intents are documented in the class reference for ContactsContract.Intents.

Using Group Features

Contacts API provides the contracts shown in Listing 27-41 to work with the Group Features of contacts

Listing 27-41. Group Contact Contracts

ContactsContract.Groups
ContactsContract.CommonDataKinds.GroupMembership

The groups table holds things like name of the group, notes about that group, and some group level counts of the membership. The groups a raw contact belongs to are kept in the data tables.

Using Photo Features

You can explore the photo-related information for a contact using the class contract shown in Listing 27-42.

Listing 27-42. Contact Photo Contracts

ContactsContract.Contacts.Photo
ContactsContract.RawContacts.DisplayPhoto

The class documentation for these contracts has sample code that describes how to use these features.

References

Here are additional resources for the topics covered in this chapter:

Summary

In this chapter, we have covered the following: the nature of the Contacts API, exploring the contacts database, exploring the Contacts API URIs and their cursors, reading and adding contacts, aggregating raw contacts, the relationship between the personal profile and contacts, and reading and adding contacts to a personal profile. We have also briefly covered batching provider operations, using the contact provider as a search provider for contacts.

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

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