The PhoneGap Contacts API provides applications with an interface that can be used to create, locate, edit, copy, and delete contact records from the device’s native Contacts application. The API is not proprietary; instead, it’s an implementation of the W3C’s Contacts API (www.w3.org/TR/2011/WD-contacts-api-20110616/). This API interfaces with the native Contacts APIs provided by the mobile platform, and because of the way the internal API views contact information, there are quite a few quirks that manifest themselves across mobile device platforms.
Example Applications
Two sample applications have been created to help illustrate the features of the Contacts API. Example 15-1 illustrates how to create a new contact within a PhoneGap application, and Example 15-2 shows how to use the Contacts API’s search capabilities to locate a contact in an application.
Because of the length of the applications, it was not possible to include the application source code in this chapter. Relevant portions of the application code and screen shots of the application in action are shown within the chapter, but to see the completed application code, you will need to point your browser of choice to the book’s web site at www.phonegapessentials.com and look for the example project files in the Code section of the site.
Creating a contact from within a PhoneGap application is pretty straightforward; simply execute the following code:
var contact = navigator.contacts.create();
When the call to navigator.contacts.create
completes, the contact
object exists that contains nothing more than a representation of the different fields that define a contact as would be rendered within the device’s native Contacts application. At this point, the contact
object doesn’t contain any information about the contact; all you have is an object that can be populated with contact information and saved to the Contacts application’s database. You must manually save any changes to the contact using the navigator.contacts.save
method described later in the chapter.
Note
One of the most common problems people encounter with the PhoneGap Contacts API stems from their failure to save their changes to a contact once they’ve made them. Many a developer has created a contact, set the appropriate properties for the contact, and then scratched their head when the changes are found to have not been written to the device’s contacts database. Be sure to call navigator.contacts.save
when you’ve completed making changes to a contact’s properties.
The call to navigator.contacts.create
is one of the few synchronous API calls implemented by PhoneGap. Instead of using callback functions to register success or failure of an API call as illustrated in other chapters, the call simply creates a contact
object in memory and returns; there’s no need for callback functions here. Once an application has a contact
object to work with, the application must populate the contact fields defined within the object and then save the contact to complete the process.
Defined within the contact object are a group of strings and objects that specify different aspects of the contact, as shown in the following list:
• id
: A unique identifier for the contact; this variable is assigned a unique value during the call to navigator.contacts.create
.
• displayName
: The name of the contact; on most devices, this is the name that is displayed in contact lists and address picker dialogs. Unfortunately, this field is not supported on all platforms (such as iOS).
• name
: An object defining the different components of a contact’s name, such as given name, family name, middle name, and so on.
• nickname
: A casual name for the contact.
• phoneNumbers
: An array containing the contact’s phone numbers.
• emails
: An array containing the contact’s email addresses.
• addresses
: An array containing the contact’s physical addresses (home, business, and so on).
• ims
: An array containing the contact’s instant messaging (IM) addresses.
• organizations
: An array containing the organizations the contact is associated with.
• birthday
: The contact’s birthday.
• note
: A variable used to contain text-based notes related to the contact.
• photos
: An array containing photos of the contact.
• categories
: An array containing user-defined categories associated with the contact.
• urls
: An array containing the web addresses associated with the contact.
The name object includes the string values shown in the following list:
• formatted
: The contact’s complete name.
• familyName
: The contact’s family name.
• givenName
: The contact’s given name.
• middleName
: The contact’s middle name.
• honorificPrefix
: The prefix associated with the contact (examples: Dr., Mr., or Mrs.).
• honorificSuffix
: The suffix associated with the contact (example: Ph.D.).
When you look at these lists, you may notice that the contact’s name components are represented in different places in the object. The displayName
and nickname
values are associated with the contact, while everything else is associated with the contact.name
object. I expected that all name components would be associated with the name object, but for some reason they’re not.
Many of the other components of the contact
object are simple arrays representing multiple values of the same type. The addresses
array is a two-dimensional array of ContactAddress
objects consisting of the following values:
• pref
: Boolean value that defines whether the entry is the default address for the contact
• type
: A string value defining the type of address being defined such as home or work
• formatted
: The full address formatted for display
• streetAddress
: The full street address
• locality
: The city or locality associated with this address
• region
: The state or region associated with this address
• postalCode
: The ZIP or postal code associated with this address
• country
: The country associated with this address
The organizations
array is a two-dimensional array of ContactOrganization
objects consisting of the following values:
• pref
: Boolean value that defines whether the entry is the preferred or default organization for the contact
• type
: A string value defining the type of organization being defined such as home or work
• name
: The name of the organization
• department
: The department where the contact works
• title
: The contact’s title within the organization
The contact’s phoneNumbers, emails
, and ims
values are all populated the same way, using an array of values shown in the following list:
• type
: A string value defining the type of value being defined such as home or work
• value
: The contact value such as phone number or email address
• pref
: Boolean value that defines whether the entry is the default entry for this type of contact method
Some smartphone platforms are picky about the values assigned to the type
property. Even though PhoneGap will accept anything for this value, your target mobile device might not display the values unless the right values are assigned here. My testing has shown that it’s best to use standard values like home, work
, and mobile
for type
.
Now that you have a good understanding of the contact properties, let’s take a look at an example of how all of this is implemented in code. In the Example 15-1 sample application (available from the www.phonegapessentials.com web site), the application retrieves contact information from an external source (in this case external .js
files) and allows the application user to add information about a selected user to the local contacts database. The following is an example of the contact object for one of the contacts used in the application:
{
"FullName": "Michael Palin",
"LastName": "Palin",
"FirstName": "Michael",
"EmailAddress": "[email protected]",
"OfficePhone": "330.123.4567",
"MobilePhone": "330.987.6543"
}
Note
I made up that email address for Michael Palin; he may have an email address, but I definitely don’t know what it is. It’s probably best not to send any email messages to that address; there’s no telling where it would go or what would happen.
In the application, the contact information shown is assigned to the contactInfo
object and passed to the following function so the contact’s information can be added to the local contacts database:
function addContact(contactInfo) {
//Create a new contact object
var contact = navigator.contacts.create();
//Populate the contact object with values
contact.displayName = contactInfo.FullName;
contact.nickname = contactInfo.FullName;
//Populate the Contact's Name entries
var tmpName = new ContactName();
tmpName.givenName = contactInfo.FirstName;
tmpName.familyName = contactInfo.LastName;
tmpName.formatted = contactInfo.FullName;
//Then add the name object to the contact object
contact.name = tmpName;
//Populate Phone Number Entries by creating and populating
//an array of phone number information
var phoneNums = [2];
phoneNums[0] = new ContactField('work',
contactInfo.OfficePhone, false);
phoneNums[1] = new ContactField('mobile',
contactInfo.MobilePhone, true);
contact.phoneNumbers = phoneNums;
//Populate Email Address the same way that you did the
//phone numbers
var emailAddresses = [1];
emailAddresses[0] = new ContactField('home',
contactInfo.EmailAddress, true);
contact.emails = emailAddresses;
// save the contact object to the device's contact database
contact.save(onContactSaveSuccess, onContactSaveError);
}
You can also create a contact object and populate it at the same time by passing in a properly formatted contact object to the create
method, as shown here:
var contact = navigator.contacts.create({displayName:
'Michael Palin', nickname: 'Mike', name: {givenName:
'Michael', familyName: 'Palin'}});
contact.save(onContactSaveSuccess, onContactSaveError);
There are some quirks in the way each individual mobile device platform stores the contact information passed to the contact
object. You will need to refer to the PhoneGap documentation for specifics about these quirks since they’re likely to change over time. For some examples, though, the BlackBerry platform doesn’t support the displayName
field directly, so the value is stored in the user1
field and the nickname
field returns null. On iOS, the displayName
field isn’t directly supported, but different values may be returned depending on what values are defined for the contact.
As mentioned previously, the changes you make to a contact’s properties will not be written to the device’s contacts database until you execute the following code:
contact.save(onContactSaveSuccess, onContactSaveError);
In this example, I’m passing in two functions: an onContactSaveSuccess
function that is executed after the contact
has been successfully saved to the contacts database and an onContactSaveError
function that is executed if there’s an error writing to the database.
The onContactSaveSuccess
function is quite simple; it just lets the user know that the save completed successfully, as shown in the following example:
function onContactSaveSuccess() {
alert(contactInfo.FullName + " was successfully saved to the
device contacts database");
}
If there’s an error saving the contact, the onContactSaveError
function is called, and an error object is passed to the function that allows an application to understand the nature of the error and react according to the needs of the application. In the following example, the code displays a different error message for the user depending on the nature of the nature of the error encountered by the application.
function onContactSaveError(e) {
var msgText;
//Now build a message string based upon the error
//returned by the API
switch(e.code) {
case ContactError.UNKNOWN_ERROR:
msgText = "An Unknown Error was reported while saving
the contact.";
break;
case ContactError.INVALID_ARGUMENT_ERROR:
msgText = "An invalid argument was used with the Contact
API.";
break;
case ContactError.TIMEOUT_ERROR:
msgText = "Timeout Error.";
break;
case ContactError.PENDING_OPERATION_ERROR:
msgText = "Pending Operation Error.";
break;
case ContactError.IO_ERROR:
msgText = "IO Error.";
break;
case ContactError.NOT_SUPPORTED_ERROR:
msgText = "Not Supported Error.";
break;
case ContactError.PERMISSION_DENIED_ERROR:
msgText = "Permission Denied Error.";
break;
default:
//Create a generic response, just in case the
// switch fails
msgText = "Unknown Error (" + e.code + ")";
}
//Now tell the user what happened
navigator.notification.alert(msgText, null,
"Contact Save Error");
}
In your applications, you will likely want to do something more substantive when an error occurs. For some of the errors, there’s not much the application can do except to perhaps try again later. The INVALID_ARGUMENT_ERROR
is most likely caused by a coding error and would likely not appear in a properly tested application (unless PhoneGap changed the Contacts API options behind the scenes).
The PERMISSION_DENIED_ERROR
is important and would affect users depending on how they answer the security prompt they receive on BlackBerry and Android devices when they install new applications. On each platform, you must configure the application project with a list of the APIs used by the application. If your application doesn’t properly identify that it uses the Contacts API, the device may block access to the API. On BlackBerry and Android, the user is prompted to allow access to the different APIs used by the application; if the user doesn’t allow access to the Contacts API when installing the application, the application will not function properly and will likely return the permission denied error. To configure your projects with the appropriate permissions, refer to the documentation that accompanies the mobile platform development tools you are using.
When working with PhoneGap Build (described in Chapter 9), the build service takes care of configuring each development environment for you. To enable access to the Contacts API on Android, add the following line to the project’s config.xml
file, and be sure to include it with the project files uploaded to the PhoneGap Build service:
<feature name="http://api.phonegap.com/1.0/contacts" />
This enables the READ_CONTACTS, WRITE_CONTACTS
, and GET_ACCOUNTS
permissions on Android.
For a BlackBerry WebWorks application, you must include the following line in the config.xml
file:
<feature id="blackberry.pim.Contact" />
The following listing shows a completed PhoneGap Build config.xml
for this application:
<?xml version="1.0" encoding="UTF-8"?>
<widget xmlns = "http://www.w3.org/ns/widgets"
xmlns:gap = "http://phonegap.com/ns/1.0"
id = "com.phonegapessentials.ex151"
version = "1.0.0">
<name>Example 15-1</name>
<description>An example application that uses the PhoneGap
Contacts API</description>
<author href="http://johnwargo.com"
email="[email protected]">John M. Wargo</author>
<gap:platforms>
<gap:platform name="android" minVersion="2.1" />
<gap:platform name="webos" />
<gap:platform name="symbian.wrt" />
<gap:platform name="blackberry" project="widgets"/>
</gap:platforms>
<feature name="http://api.phonegap.com/1.0/contacts" />
<feature id="blackberry.pim.Contact" />
</widget>
One of the quirks of the Android platform is that an application must have a Google account configured on the device in order to access contact information from a PhoneGap application. I discovered this issue when I was testing on a device that I’d done a complete security wipe on before testing the application. Without a Google account defined in the Android Accounts and Sync area of Settings, the application returned an Unknown Error whenever it tried to write a contact to the contacts database. As soon as I configured the device for my Gmail account, the error went away. If you are using the Android emulator, you will need to use an emulator based upon the Google APIs, not the default SDK.
Let’s take a look at the application in action. Figure 15-1 shows the Example 15-1 application running on a BlackBerry Torch simulator. It starts by showing a list of contacts from an external data source.
When the user selects a contact, the application opens a page that displays detailed information about the contact, as shown in Figure 15-2.
When the user clicks the Add Contact button, the application adds the selected contact to the contacts database and displays the confirmation dialog shown in Figure 15-3.
Opening the BlackBerry Contacts application and searching for and then opening the new contact will show a screen similar to the one shown in Figure 15-4.
Figure 15-5 shows the results of the same activity on an Android smartphone.
Figure 15-6 shows the results of the same activity on an Apple iPhone.
With each of these examples, the native contacts application makes the phone number, email address, and other electronic contact fields clickable so the user can click an item and initiate contact through the selected option.
Another useful feature of the Contacts API is the ability to search the device’s local contacts database for contacts. To initiate a search, an application should execute the following code:
navigator.contacts.find(contactFields, onContactSearchSuccess,
onContactSearchError, searchOptions);
The contactFields
parameter passed to the find
method defines the list of contact field names whose values will be included in the search results. In some cases, you may want the search function to return all fields, in which case you would use the following:
contactFields = ['*'];
In other cases, you might want to limit your search results to a limited number of contact fields, as shown in the following example:
contactFields = ['displayName', 'name', 'nickname'];
The name
entry refers to the name
object described in the previous section and includes familyName, givenName, middleName
, and more.
As you build your applications, you may think you can just use the displayName
field and catch most contact names. The problem with this approach is that displayName
is not supported on iOS, so if you rely upon that field name for searches targeted at iOS devices, the search will likely not return any values.
The onContactSearchSuccess
and onContactSearchError
functions passed as parameters to find
are standard callback functions you’ve seen in other examples in this book. The onContactSearchSuccess
function is executed when the search succeeds, and the onContactSearchError
function is executed if an error is encountered performing the search. You’ll learn more about these functions later.
The searchOptions
parameter is an object defining options used to control how the search is performed. It consists of two values, filter
and multiple
, as shown in the following example:
var searchOptions = { filter : searchStr, multiple : true };
The filter
value defines the search string used when searching the device’s contacts database, and the multiple
value is a Boolean value that defines whether the application should return multiple results or return a value as soon as a single item that matches the search criteria is found.
Let’s take a look at an example application that puts all of this to use. In the Example 15-2 application (available for download from www.phonegapessentials.com), the application displays the simple form shown in Figure 15-7. In the application, I used the jQuery (www.jquery.com) and jQuery Mobile (www.jquerymobile.com) frameworks to provide the application with a more professional-looking interface without having to write a bunch of interface code myself.
On the form, the search field is defined using the following HTML markup:
<input type="search" id="editSearch" />
The search scope picker is defined using the following markup:
<select id="searchScope" name="searchScope">
<option>All</option>
<option>Name</option>
<option>Address</option>
<option>Notes</option>
</select>
The code behind the Search Contacts button is defined in the following function:
function searchContacts() {
//Get the search string from the page
var searchStr =
document.getElementById("editSearch").value;
//Figure out which search option is selected
var searchScope =
document.getElementById("searchScope").selectedIndex;
//Then populate searchOptions with the list of fields being
//searched
var contactFields = [];
switch(searchScope) {
case 1:
//Return just name fields
contactFields = ['displayName', 'name', 'nickname'];
break;
case 2:
//Return address fields
contactFields = ['name', 'streetAddress', 'locality',
'region', 'postalCode', 'country'];
break;
case 3:
//Return name and contents of the Notes field
contactFields = ['name', 'note'];
break;
default:
//return all contact fields
contactFields = ['*'];
}
//Populate the search options object
var searchOptions = { filter : searchStr, multiple : true };
//Execute the search
navigator.contacts.find(contactFields,
onContactSearchSuccess, onContactSearchError, searchOptions);
}
In the function, the code grabs the value from the search field and the selected value from the search scope picker. Using those values, it defines the values for the contactFields
variable based upon which picker option was selected and passes the search string in the filter
value in the searchOptions
object.
That’s it—that’s all an application has to do to search the local contacts database. The onContactSearchError
function is the same as the onContactSaveError
function described in the previous section. When the search completes, the find
method calls the onContactSearchSuccess
function, which is shown next. The function essentially builds an on-screen list of the search results (displaying the name of the contact if it can be determined).
function onContactSearchSuccess(contacts) {
// alert("onContactSearchSuccess");
//Populate the contact list element of the contact list page
var i, len, theList;
//Store the contact data in our global variable so the
//other functions have something to work with
contactList = contacts;
//Did we get any results from the search?
len = contacts.length;
if(len > 0) {
theList = '<ul data-role="listview">';
for( i = 0, len; i < len; i += 1) {
//on iOS displayName isn't supported, so we can't
//use it
if(contacts[i].displayName == null) {
theList += '<li><a onclick="showContact(' + i +
'),">' + contacts[i].name.familyName + ", " +
contacts[i].name.givenName + '</a></li>';
} else {
theList += '<li><a onclick="showContact(' + i +
'),">' + contacts[i].displayName + '</a></li>';
}
}
theList += '</ul>';
$('#contacts').html(theList);
//Then switch to the Contact Details page
$.mobile.changePage("#contactList", "slide", false, true);
} else {
navigator.notification.alert('Search returned 0 results',
null, 'Contact Search'),
}
}
In onContactSearchSuccess
, the function is passed an array containing contact entries for each of the contact records containing the search string. The code first checks to see whether the array has any values; if not, a message is displayed to the user letting them know that there are no search results. If there are results, the function loops through the results array and creates an unordered list containing the name of each contact included in the search results. In the application, the unordered list is assigned a data-role
attribute of listview
, which is used by jQuery Mobile to render the interactive list shown in Figure 15-8. In this case, there’s only one result, but if there had been more, the list would scroll down the length of the screen as needed.
Looking at the function, you may have noticed that when creating the list view, the code first checks to see whether displayName
is null
and uses the contact’s givenName
and familyName
to create the list entry. This is because displayName
is not supported on iOS, so I had to find another way to ensure that something would display in the list.
if(contacts[i].displayName == null) {
theList += '<li><a onclick="showContact(' + i + '),">' +
contacts[i].name.familyName + ", " +
contacts[i].name.givenName + '</a></li>';
} else {
theList += '<li><a onclick="showContact(' + i + '),">' +
contacts[i].displayName + '</a></li>';
}
Each entry in the list view has an onclick
event defined that causes the showContact
function to be executed when the user selects a contact; passed to showContact
is the index for the selected contact. The function retrieves information about the selected contact and displays a page similar to the one shown in Figure 15-9.
As you can see from the figure, the application displays some common field values at the top of the page and lists all of the contact field values below the horizontal rule. I built the application this way to help me validate what was returned by the search function. When using the application, as you select different search scopes, you can easily see what contact fields are (and aren’t) returned to the application when performing a search. Here’s the code that generates the field/value list:
//Show all of the contact fields
dt = "<hr />";
for(myKey in contact) {
dt += "Contact[" + myKey + "] = " + contact[myKey] + "<br />";
}
$('#detailContent').html(dt);
To make a clone of an existing contact, an application simply calls the clone
method, as shown in the following example:
var contact2 = contact1.clone();
The contact
object points to an existing contact obtained by creating a new contact via a call to navigator.contacts.create
, as described in the beginning of the chapter, or retrieved by searching the local device contacts database using the find
method, as described in the previous section.
Once the application has the cloned copy of the original contact, it can manipulate the properties of the clone, changing whatever properties are appropriate for the application and then calling navigator.contacts.save
to write the updates to the contacts database. When the clone is created, the cloned contact object exists solely in memory and must be written to disk for any changes to be maintained.
To remove an existing contact, an application simply calls the remove
method, as shown in the following example:
contact.remove(onContactRemoveSuccess, onContactRemoveError);
The contact
object points to an existing contact obtained by creating a new contact via a call to navigator.contacts.create
, as described in the beginning of the chapter, or retrieved by searching the local device contacts database using the find
method, as described in an earlier section.
The onContactRemoveSuccess
and onContactRemoveError
parameters passed to the call to remove
are callback functions that are executed by the remove
method. The onContactRemoveSuccess
function is executed after the contact has been successfully removed, while the onContactRemoveError
function is the same as the onContactSaveError
function described in the first section of this chapter.