Loop/Architecture/Address Book

From MozillaWiki
Jump to: navigation, search

Introduction

The main means by which users are expected to initiate Loop calls on desktop is via the "Address Book" sidebar, which will contain a list of the user's contacts. For the initial versions of Loop, contacts will can be populated from several sources, via a one-way synchronization (that is, the data will be imported from a remote authority and updated from that remote authority, but cannot be changed by the Loop client). A necessary implication of this setup is that all contacts will need to track which data source they belong to, and be updated only from those sources.

Format

To allow for future integration with the Contacts API and/or potential integration with contact synchronization across devices (including Firefox OS devices), we will use objects with properties having the same structure as those used by mozContact. In theory, to store these in the Contacts API database, all that would be required would be passing in the object as the argument to the mozContact constructor, and then storing it via navigator.mozContacts.save(). For this reason, we will be converting all of the fields that make sense in mozContact, rather than only those used by Loop.

An example of this format is as follows; note that many of the fields are arrays of values rather than single values. Even though our importation will typically only populate these with a single value, we will be maintaining them as arrays to retain the properties described above.

{
  "id": "http://www.google.com/m8/feeds/contacts/example.user@gmail.com/base/v4t9vnaga983tng",
  "published": "2014-07-02T15:25:32.000Z",
  "updated": "2014-07-09T19:46:28.000Z",
  "bday": "1972-07-19T05:00:00.000Z",
  "adr": [
    {
      "countryName": "USA",
      "locality": "Dallas",
      "postalCode": "75201",
      "pref": true,
      "region": "TX",
      "streetAddress": "1234 Main St.",
      "type": [
        "home"
      ]
    }
  ],
  "email": [
    {
      "pref": true,
      "type": [
        "work"
      ],
      "value": "adam@example.com"
    }
  ],
  "tel": [
    {
      "pref": true,
      "type": [
        "work"
      ],
      "value": "+12145551234"
    }
  ],
  "name": [
    "Adam Roach"
  ],
  "honorificPrefix": [
    "Mr."
  ],
  "givenName": [
    "Adam"
  ],
  "additionalName": [
    "Boyd"
  ],
  "familyName": [
    "Roach"
  ],
  "honorificSuffix": [
    "Jr."
  ],
  "category": [
    "gmail"
  ],
  "org": [
    "Mozilla"
  ],
  "jobTitle": [
    "Principal Engineer"
  ],
  "note": [
    "Likes Mexican food"
  ]
}

We will be using the "category" field to keep track of the source from which the contact was imported. Currently defined values are:

  • gmail - Imported from Google Mail
  • local - Manually entered by the user


The remaining fields are derived from information retrieved during the importation process.

For the time being, we will not be using the following mozContacts fields:

  • anniversary
  • sex
  • genderIdentity
  • url
  • impp
  • phoneticGivenName
  • phoneticGamilyName
  • nickname
  • key

Storage

The contacts are stored in a single IndexedDB table, called "contacts". This requires bug 1028187 to land, so that we have a useable origin for IndexedDB to use. (This will be updated by the behavior proposed in bug 1033587, but this won't happen for a while).

The key for the table will be "id".

Importing

In order to avoid running into cross-origin issues in retrieving contacts, the retrieval will be performed in privileged code. This means that the window.mozLoop API will need new functions added to it in order to initiate contact import.

See bug 972000 for further information and a proof-of-concept patch.

The function for starting an import will be of the form:

function startImport(importId, serviceParameters, newContactCallback, doneCallback)
  • importId: A unique ID passed back to the caller in callbacks. This allows the caller to disambiguate between import operations in the case that there are multiple operations going on at the same time.
  • serviceParameters: An object defining the service to import from. This must contain a "service" parameter. Certain services may require additional parameters (e.g., a generic CardDAV importer may require a username and password).
  • newContactCallback: A callback of the form function(importId, contact, progress, total), called whenever a new contact is imported. Note that this contact may already exist in the database (that is, it may already have an entry with a matching "id" field), in which case the new contact should replace the existing entry.
    • importId: The import ID passed in when startImport was called.
    • contact: The contact in the format described above
    • progress: The number of contacts that have been imported from this operation
    • total: The total number of contacts in this current import operation. Set to '-1' if unknown.
  • doneCallback: A callback of the form function(importId, error)
    • importId: The import ID passed in when startImport was called.
    • error: If the import was successful, set to "null"; otherwise, an Error object with information about the failure.

For example, importing gmail contacts would be initiated using something along the lines of:

window.mozLoop.startImport(importId, {service: "gmail"}, addContactToDatabase, handleImportErrorOrCompletion);

Local Contacts

The the Loop MVP UX spec defines the experience for adding a user manually to the address book. This is currently intended to be a minimal set of data, populating only the following fields:

  • id: UUID generated at the time the user is added
  • name[0]: Text from user-entered "Name" field
  • email[0].value: Text from user-entered "Email" field
  • email[0].type[0]: set to "other"
  • email[0].pref: set to true
  • phone[0].value: Text from user-entered "Phone" field
  • phone[0].type[0]: set to "other"
  • phone[0].pref: set to true
  • category[0]: set to "local"

Currently, the plan is to leave the photo field blank for user-entered contacts; this may be revisited in the future (potentially leveraging Gravatar icons where appropriate).

Gmail

The current plan is to use the Google Contacts API, v3 to retrieve contacts from users' gmail accounts. The behavior described here is modeled on the Gaia Gmail Connector code.

Functionally, the steps involved here are:

  1. Retrieve OAUTH2 token for user (see "Authorizing requests to the Google Contacts API service" in the Google Contacts API)
  2. Retrieve the group id for the "Contacts" group from https://www.google.com/m8/feeds/groups/default/full/ (see "Retreiving all Contact Groups").
  3. Retrieve the list of users in the "Contacts" group from https://www.google.com/m8/feeds/contacts/default/full/?max-results=maxResults&group=groupId, where groupId is the value retrieved in the previous step. The value of maxResults should be large enough to accommodate users' address books -- the mobile implementation uses 10,000.
  4. For each contact in the result:
    • Translate the result into a contact object according the the table below. Note that some data may require manipulation to convert between the gmail format and the field described in mozContacts.
    • Fire a callback to allow the UI to update. The callback should include the number of total contacts being imported, the number of contacts already imported (from this import), and the "id" field of the contact that was just imported.

Note that the iteration in the final step may take substantial processing. The code should take steps to yield the processor between processing each contact, so as to not impact system responsiveness (e.g. with Tasks).

destination source (see Google Data Elements)
id id
published published
updated updated
name[0] name/fullName
honorificPrefix[0] name/namePrefix
givenName[0] name/givenName
additionalName[0] name/additionalName
familyName[0] name/familyName
honorificSuffix[0] name/nameSuffix
email[x].type[0] email @rel
email[x].pref email @primary
email[x].value email @email
adr[x].type[0] structuredPostalAddress @rel
adr[x].pref structuredPostalAddress @primary
adr[x].streetAddress structuredPostalAddress/street (or structuredPostalAddress/pobox)
adr[x].locality structuredPostalAddress/city
adr[x].region structuredPostalAddress/region
adr[x].postalCode structuredPostalAddress/postcode
adr[x].countryName structuredPostalAddress/country
tel[x].type[0] phoneNumber @rel
tel[x].pref phoneNumber @primary
tel[x].value phoneNumber @uri (or text value)
org[0] organization/orgName
jobTitle[0] organization/orgTitle
bday birthday/when
note[0] content
photo[0] link[rel='http://schemas.google.com/contacts/2008/rel#photo'] -- Note; need to retrieve and convert to blob
category[0] hardcoded to "gmail"

UX

See the Loop MVP UX spec (Note: at the time of writing this, the contact import UX flow is not yet specified)

The value of category[0] in each user record will be used to determine a couple of different aspects of user experience:

  1. When set to "gmail", the "gmail" icon shown in the UX mock-ups will be displayed
  2. When set to "local," the UX will allow selection of the contact for editing and deletion.