PhoneGap* compatible Contacts API Sample: Work with Device Contact Database

This article describes the design and implementation of the "PhoneGap compatible Contacts API" sample application. This application demonstrates how to use PhoneGap compatible Contacts API to to access a device's contact database.

The application supports displaying all contacts, displaying contact information for a single person, creating a new contact, editing a contact, saving a contact to the device's contact database, searching for all contacts satisfying a search criteria, filtering a list of contacts by name, sorting a list of contacts, deleting a single contact and duplicating a contact.

The application uses the following libraries: jQuery, jQuery Mobile (for styling).

The source code can be downloaded here.

Final Look

The application has 4 pages:

  1. The contact list page is the application's starting point. It contains the following UI elements:
    1. A contact list that lets the user select a contact.  When a contact is selected more information about the person will be displayed on a contact information page.
    2. An options button that opens a popup menu with options for advanced search, an editing contact, or changing the display and sorting preferences for the contact list.
    3. A text field that filters the contact list by name or a surname.
    4. A link to the sample application's license information.
  2. The contact information page lists all the information for a contact (i.e. the full name, a photo, a list of phone numbers, email addresses, etc.). The options button provides access to the following fuctionality:
    1. Create a content duplicate: Creates a duplicate contact and stores it in the contact database.
    2. Edit contact: opens the Edit Contact page with the current contact's information.
    3. Remove a contact: permanently removes the contact from the device contact database.
  3. Edit contact page provides a form to edit and save the contact's information. The form fields are generated and added dynamically (the contact field type chooser dialog is opened to choose the type of the field needed).
  4. The advanced search page allows the user to select a contact type, enter some search text, and view the filtered results. 

Screenshots of the Contact List and Contact Info pages.

Design Considerations and the Application Structure.

The application starts with the Contact List page. On contact list item press the work with the single contact starts. For this purpose the global variable activeContact of the class Contact is initialized (activeContact contains the full contact information received from the contacts database) and used further to provide interactions between Contact Information and Edit Contact pages and device contacts database in the case of saving changes in contact information. When the Contact list page resumes the active contact is removed as the work with the single contact finishes. 

The application uses the PhoneGap Contacts API to query for the contact information.

DisplayableContactList Object

The Contact List page content is managed by the DisplayableContactList object. The DisplayableContactList object is responsible for retrieving the content information it needs from the contact database, building a contact list in HTML and managing the display of the contact list. The display may change depending on user preferences and sorting options.  The HTML object built by this class is made of clickable contact list items and serve to open the contact info page. Below is the schematic declaration of the class along with its public methods:

// List of reduced representations of contacts, which are to be displayable in a contacts list
function DisplayableContactList(contListContainer) {
    ....
    var sortPref = SortPref.ASC;
    var dispPref = DispPref.NAME_FIRST;
    // Queries device contact database and redraws contacts list
    this.updateList = function(filter, filterField, additionalFieldsArray) { ... }
    // Adds the list of contacts to the page. Contacts, not satisfying the filter string, are invisible.
    // If a filter is empty, all contacts are displayed.
    this.drawContactList = function(filter) { ... }
    // Sets new contacts sorting preference and redraws contacts list
    this.setSortPref = function(newPref, filter) { ... }
    // Sets new contacts display preference and redraws contacts list
    this.setDispPref = function(newPref, filter) { ... }
    ...
} 

The object stores two parameters: the users sorting and display preferences.

The method updateList() asks for a list of contacts from the contact database. The query has three fields ("id", "name", "photos"), but additional fields may be provided with an array. The contact list is filtered only if the filter and the filter fields are provided. The filtering process is performed by the checkFilter() method which compares the filter string and the value in the filter field for a partial match. The updateList() method forms, sorts and draws the contact list.

Issues:

The query to the contact database is implemented according to the W3C specification which requeries the return fields and fields to which the search filter is applied to be the same. For example, if we want to return the "name" and "phoneNumbers" fields to be returned, but only for the contact with a specific id, the query would look like ["id", "name", "phoneNumbers"].  The return value may be one contact or a list of contacts.  For instance, along with the contacts which contain the phoneNumber partially matched with the required id. That is why in such cases the post-filtering is needed.

The methods drawContactList(filter), setSortPref(newPref, filter) and setDispPref(newPref, filter) can change the contacts display perferences without querying the database again.


Contact Information Page

A contact information page is created when the user selects a contact from the contact list,. The showInfo(contactID, searchCriteria) method is called. This method generates the HTML with the contact information that will be added to the page.  Additionally, the activeContact is assigned the contact that was queried. To get the contact information to display the Contacts.find() method is called. The two parameters passed to this function are the id of the required contact and the search criteria which is passed to the Contacts.find() method as a filter. The search criteria should be the most descriptive one. Here the surname is used if it is present, then name if surname is unavailable, then contact id. When the list of contacts is received, there the single contact with the received id is searched.

index.html

        <!-- Contact Information page -->
        <div data-role="page" id="cont_info_page" data-theme="a">
            ....
            <div data-role="content">
                <div id="contact_info">
                    <!-- Content will be dynamically added -->
                </div>
            </div><!-- end: Content -->
        </div><!-- end: Contact Information page -->

contacts.js

// Forms the "Contact information" page
function showInfo(contactID, searchCriteria) {
    var contInfoContainer = getElement("contact_info");
    var contactFields = ["*"];
    var contactFindOptions = new ContactFindOptions();
    contactFindOptions.filter = searchCriteria;
    contactFindOptions.multiple = true;
    navigator.contacts.find(contactFields, contactSuccess, contactError, contactFindOptions);
    contInfoContainer.innerHTML = "";
    function contactSuccess(contacts) {
        contInfoContainer.innerHTML = "";
        for (var i = 0; i < contacts.length; i++) {
            if (contacts[i].id == contactID) {
                activeContact = contacts[i];
                var cNameSection = ...;                                // here the name section html string is formed from the contacts[i].name
                var cPhotoSection = ...;                                // here the photo section html string is formed from the contacts[i].photos
                var cPhoneNumbersSection = ...;                  // here the phone numbers section html string is formed from the contacts[i].phoneNumbers
                var cEmailsSection = "";                               // here the emails section html string is formed from the contacts[i].emails
                // then the IMs, urls addresses, organizations sections thml strings are formed out of the respective contact fields
                contInfoContainer.innerHTML +=  cPhotoSection + cNameSection + cPhoneNumbersSection + cEmailsSection + 
                                                cIMsSection + cUrlsSection + cAddressesSection + cOrganizationsSection + 
                                                cNoteSection;
                $.mobile.changePage("#cont_info_page", { transition: "pop" });
                break;
            }
        }
    }
    function contactError(contactError) {
        contInfoContainer.innerHTML = "Contacts are unavailable";
        $.mobile.changePage("#cont_info_page", { transition: "pop" });
    }
}

Removing Contact

The "Remove contact" operation uses the contact.remove(onSuccess, onError) Contact class method. On remove success the application updates the contact list and swicthes to the contact list page. The method is called on the Remove contact menu item click on the Contact Info page.

contacts.js

function onDeviceReady() {
    ....
    $("#remove_ok_button").bind ("click", onRemoveContact); 
    ....
}
// Called for an existing contact removing 
function onRemoveContact(e) {
    activeContact.remove(onSuccess, onError);
    function onSuccess() {
        alert("The contact was successfully removed");
        displContactList.updateList();
        $.mobile.changePage("#cont_list_page", { transition: "pop" }); 
    };
    function onError(contactError) {
        alert("Cannot remove contact: error " + contactError.code);
        $.mobile.changePage("#cont_info_page", { transition: "pop" }); 
    };
}

Cloning Contact

The contact is cloned using the contact.clone() function of the Contact class. It returnes the deep copy of the caller and this copy is saved with the Contact's save(onSuccess, onError) method to the devise database. The method is called on the "Create contact copy" menu item click on the Contact Info page.

contacts.js

// Called when Cordova is fully loaded (and calling to Cordova functions has become safe)
function onDeviceReady() {
   ....
   $("#clone_contact_button").bind ("click", onSaveContactCopy); 
   ....
}
// Called for an existing contact duplication
function onSaveContactCopy(e) {
    ...
    var newContact = activeContact.clone();
    newContact.name.familyName = newContact.name.familyName + " (copy)";
    newContact.save(onSuccess, onError);
    function onSuccess(contact) {
        alert("The contact duplicate " + contact.name.givenName + " " + contact.name.familyName + " was successfully created");
        displContactList.updateList();
    };
   function onError(contactError) {
        alert("The contact cannot be duplicated. Error: " + contactError.code);
    };
}

Editing Contact and New Contact Creation

The contact form on the edit contact page is used to edit or add a new contact. If the edit option is chosen, the form is filled with the activeContact properties. If the new contact option is chosen, an empty form is created. The form is created and managed by the Contact Form object.

contacts.js

// Called when Cordova is fully loaded (and calling to Cordova functions has become safe)
function onDeviceReady() {
    .....
    $("#edit_contact_button").bind ("click", onEditContact);
    $("#new_contact_ok_nav").bind ("click", onSaveContact);
    .....
}
// Called for a new contact creation
function onNewContact(e) {
    contactForm.buildContactForm(null);  
    $.mobile.changePage("#edit_contact_page", { transition: "pop" });
    .....
}
// Called for an existing contact editing 
function onEditContact(e) {
    contactForm.buildContactForm(activeContact);  
    $.mobile.changePage("#edit_contact_page", { transition: "pop" });
}

ContactForm Object 

The contact editing form is generated dynamically. Each Contact property corresponds to the single form section. If the property has a simple nature, the corresponding section is made visible or invisible depending of the presence of the property. If a property represents an array, the required number of subsections each of which corresponds to the array item can be attached to the corresponding section (if the section does not have any subsection attached, it is also invisible). Sections / subsections are attached on the "Add item" form button click (the contactForm.addItem() method is called). The subsection (of section if it doesn't have any subsections) may be removed by the remove() method. On adding item the dialog with the item type (e.g. phone number, email, etc.) chooser is fixed with fixChooser() method and opened. fixChooser() disables the select options corresponding to the form sections that should not have any more subsections.

Sections and subsections are defined as strings for simplicity: they are initialized in the defineSectionPatterns() method. 

To construct and to initialize the contact form the buildContactForm() is used: it forms the form section sequence and adds it to the form root, and then queries contacts database to fill the form if the activeContact is not null.

the methods like getNames(), getNicknames(), getPhoneNumbers() and other functions return appropriate sets of user's input from the form required to save a contact to the device. Below the schematic code follows. 

index.html

<!-- Edit contact page -->
      <div data-role="page" id="edit_contact_page" data-theme="a">
            <div data-role="header">   .....   </div>
            <div data-role="content">
                    <form name="new_contact_form" id="new_contact_form">
                            <!-- Form is created dynamically -->
                    </form>
            </div>
            <div data-role="footer" >   .....   </div>
</div><!-- end: Edit contact page -->

contacts.form.js
// The object representing an "Edit contact" form
function ContactForm() {
    var formSectionPatterns = defineSectionPatterns();
    // Appends a form section corresponding to the itemName (e.g. phone number input, etc.) to the form.
    // itemName should correcpond to a Contact class property name.
    this.addItem = function(itemName) { ... }
    ....
    // Forms the "edit contact" form (and fills the form if the contact info is profided)
    this.buildContactForm = function(contact) {
        var contactPropBatch = getElement("new_contact_form");
        .....
        contactPropBatch.innerHTML = formSectionPatterns.nameSec + formSectionPatterns.phonesSec + 
                                     formSectionPatterns.emailsSec + formSectionPatterns.addressesSec +
                                     formSectionPatterns.imsSec + formSectionPatterns.organizationsSec +
                                     formSectionPatterns.noteSec + formSectionPatterns.photoSec + 
                                     formSectionPatterns.urlsSec +
                                     "<a id='addNewItem_button' href='#item_chooser_page' data-rel='dialog' data-role='button' data-icon='plus' onclick='fixChooser();'>Add detail</a>";
        if (contact != null) {
            if (contact.name != null) {
                $("#given_name_input").val(contact.name.givenName);
                $("#family_input").val(contact.name.familyName);
                $("#middle_input").val(contact.name.middleName);
                $("#honorificPrefix_input").val(contact.name.honorificPrefix);
                $("#honorificSuffix_input").val(contact.name.honorificSuffix);
                $("#nickname_input").val(contact.nickname);
            }

            if (contact.phoneNumbers && (contact.phoneNumbers.length > 0)) {
                var phoneSec = getElement("phonesSec");
                buildSection(phoneSec, "phoneNumber", contact.phoneNumbers.length);
                for (var i = 0; i < contact.phoneNumbers.length; i++) {
                    getElement("phoneNumbersType_input_" + i).value = contact.phoneNumbers[i].type;
                    getElement("phoneNumbers_input_" + i).value = contact.phoneNumbers[i].value;
                }
            }

            .....                       // further form content filling
        }
        $("#new_contact_form").trigger("create");        // form refreshing (required for jQuery Mobile)
    }
    // Adds the necessary (subsectionNumber) number of subsections to the specified form section
    function buildSection(section, subsectionName, subsectionNumber) { ..... }
    // Returns the object containing all html strings defining the form parts
    function defineSectionPatterns() { ..... }
    // Returns the DOM element (form subsection) with the appropriate unique item ids 
    function getFormSection(propertyName, i) { ..... }
    // Returns the contact name information entered by user
    this.getNames = function() { .... }
    // Returns the contact nickname information entered by user
    this.getNickname = function() { ..... }
    // Returns the contact phone numbers information entered by user
    this.getPhoneNumbers = function() { ..... }
    // Returns the contact emails information entered by user
    this.getEmails = function() { ...... }
    // Returns the contact ims information entered by user
    this.getIMs = function() { ..... }
    // Returns the contact URLs information entered by user
    this.getURLs = function() { ...... }
    // Returns the contact photo information entered by user
    this.getPhotos = function() { ..... }
    // Returns the contact address information entered by user
    this.getAddresses = function() { ..... }
    // Returns the contact organization information entered by user
    this.getOrganizations = function() { ..... }
    // Returns the contact note information entered by user
    this.getNote = function() { ...... }
}
// Removes the subsection from the form. Is to be triggered by the remove button click event.
// The remove button is to be contained by the subsection to remove.
function remove(e) { ..... }
// Disables the "Add subsection" chooser items which are to be single and are already displayed into the form  
function fixChooser() { ...... }

Saving Contact

On saving a new or edited contact the onSaveContact(e) method is called. It requests the contact data from the Contact form and saves it to the new contact (if a new contact is created with the navigator.contacts.create() method) or updates the activeContact (if it is the contact edition process, here the active contact is the edited contact).

Note:

On Android, if updating the contact fields, like phone number or email, it is important to update the properties of each contact field that already exist instead of replacing them with new field instances. In the latter case, the fields will be added to existing ones instead of replacing them. This happens because each phoneNumber, for example, has an id which is compared against the Android contacts database. While saving a contact if the id exists the field modification is performed. If no id is set for the phoneNumber it is added to the contact phone number set. That's why, for example, the best way to remove a phoneNumber now is to set it's properties to "" (source: Q&As section here).

// Called for saving a contact having been edited
function onSaveContact(e) {
    var toSave;
    if (activeContact) {
        toSave = activeContact;
    } else {
        toSave = navigator.contacts.create();
    }
    // name saving:
    initName();
    // nickname saving:
    toSave.nickname = contactForm.getNickname();
    // phones saving:
    initContactFieldVals("phoneNumbers", contactForm.getPhoneNumbers());
    ....                   // here follows the same procedure for emails, ims, urls, addresses, organizations saving
    // note saving:
    var note = contactForm.getNote();
    if (!isEmptyOrBlank(note)) {
        toSave.note = note;
    }
    // photo saving:
    initContactFieldVals("photos", contactForm.getPhotos());
    toSave.save(onSuccess,onError);


    function onSuccess(contact) {
        alert("The contact was successfully saved");
        displContactList.updateList();
        $.mobile.changePage("#cont_list_page", { transition: "pop" });
    };


    function onError(contactError) {
        alert("The contact cannot be saved: error; " + contactError.code);
    };
    
    function initName() {
        ...                   // receiving the name data to save
    };
    
    function initContactFieldVals(contactFieldName, contactFieldValsArray) {
        if (!toSave[contactFieldName] && (contactFieldValsArray.length > 0)) {
            toSave[contactFieldName] = [];
        } 
        for (var i = 0; i < contactFieldValsArray.length; i++) {
            if (!toSave[contactFieldName][i]) {
                if (contactFieldName == "addresses") {
                    toSave[contactFieldName][i] = new ContactAddress();
                } else if (contactFieldName == "organizations") {
                    toSave[contactFieldName][i] = new ContactOrganization();
                } else {
                    toSave[contactFieldName][i] = new ContactField();
                }
            }
            for (var key in contactFieldValsArray[i]) {
                toSave[contactFieldName][i][key] = contactFieldValsArray[i][key];
            }
        }
        if (toSave[contactFieldName]) {
            for (var i = contactFieldValsArray.length; i < toSave[contactFieldName].length; i++) {
                if (toSave[contactFieldName][i]) {
                    for (var key in toSave[contactFieldName][i]) {
                        if (key.toLowerCase() != "id") {
                            toSave[contactFieldName][i][key] = "";
                        }
                    }
                }
            }
        }
    };
}

Getting Photos from Device

To get photo from the device for the purpose of setting it as a contact avatar the PhoneGap Camera API is used. The Camera.getPicture(onCaptureSuccess, onCaptureError, [ cameraOptions ]) methos retrieves the photo from the device saved photo albums and it is called with constant parameters (please, see the API reference for futher explanation). If the picture is successfully chosen, it is set as the source of the picture to the Edit Contact form, otherwise the picture source is not changed: 

// Selects photo from the Photo Album
function selectPhoto(e) {
    navigator.camera.getPicture(onCaptureSuccess, 
                                onCaptureError, 
                                { quality : 50, 
                                  destinationType : Camera.DestinationType.FILE_URI, 
                                  sourceType : Camera.PictureSourceType.PHOTOLIBRARY, 
                                  allowEdit : false, 
                                  encodingType : Camera.EncodingType.JPEG,
                                  targetWidth : 100,
                                  targetHeight : 100,
                                  mediaType: Camera.MediaType.PICTURE,
                                  saveToPhotoAlbum : false,
                                  correctOrientation: true
                                });
    // Sets contact photo URL
    function onCaptureSuccess(imageData) {
        getElement("photo_loc_input").src = imageData;
    }
    function onCaptureError(message) { }
}

Note:

Ripple and real devices treat picture urls differently. Currently removing contact image works fine with Ripple, but fails to remove a contact picture on any real device.

Advanced Search

The advance search is started on the "searchby_chooser_ok_button" press and is managed by the Advanced Search Chooser page which contains the serch field select and the search string input: 

        <!-- Advanced Search Chooser page -->
        <div data-role="page" id="searchby_chooser_page" data-theme="a">
            <div data-role="header" data-position="fixed">
                <h3>Advanced search</h3>
            </div>
            <div data-role="content">
                <div>
                    <label for="searchby_chooser_input">Choose a search criteria:</label>
                    <select id='searchby_chooser_input' name='searchby_chooser_input' data-mini='true'>
                        <option value='name'>Name</option>
                        <option value='nickname'>Nickname</option>
                        <option value='phoneNumbers' selected='selected'>Phone</option>
                        <option value='emails'>Email</option>
                        <option value='ims'>IM</option>
                        <option value='urls'>URL</option>
                        <option value='addresses'>Address</option>
                        <option value='organizations'>Organization</option>
                    </select>
                    <label for="search_val_input">Enter the value to search for:</label>
                    <input id="search_val_input" name="search_val_input" type="text" />
                    <div style="width: 100%; margin: 0 auto; margin-left: auto; margin-right: auto; align: center; text-align: center;">
                        <a href="#" id="searchby_chooser_ok_button" data-role="button" data-theme="b" data-inline="true">Go!</a>
                    </div>
                </div>
            </div><!-- end: Content -->
        </div><!-- end: Advanced Search Chooser page -->

After the search field choosing and the search string entering the device contact database is queried by the searchByCriteria() method, which calls the DisplayableContactList object to form the list satisfying the search criteria with the additional field (chosen by the user) to search in. It also adds the "Show full list" button to return to the full contact list.

function onLoad() {
    ....
    $("#searchby_chooser_ok_button").bind ("click", searchByCriteria); 
    ....
}
// Shows the list of contacts satisfying the advanced search criteria input
function searchByCriteria() {
    $.mobile.changePage("#cont_list_page", { transition : "pop" });
    var searchCriteria = getElement("searchby_chooser_input").options[getElement("searchby_chooser_input").selectedIndex].value;
    var searchVal = getElement("search_val_input").value;
    getElement("search_val_input").value = "";
    var addF = [];
    addF.push(searchCriteria);
    displContactList.updateList(searchVal, searchCriteria, addF);
    var div = getElement("full_list_button_container");
    if (div) {
        div.innerHTML = '<a href="#" id="full_list_button" data-role="button" data-mini="true" class="ui-btn-left" data-theme="c" >Show full list</a>';
        $("#full_list_button").bind("click", function() { displContactList.updateList(); div.innerHTML = ""; });
        $("#full_list_button").button();
    }
}

Devices Tested

  • Mobile devices:
    • Apple iPad* 2 tablet (iOS 5.1.1)
    • Sony Ericsson* Go smartphone (Android 2.3.7)
    • Samsung Galaxy Tab* 2 tablet (Android 4.0.3)