Creating a Synergy Contacts Package

When first introduced, Synergy set the standard for accessing and managing personal data. With HP webOS 2.0, third-party developers can now code their own Synergy connectors. This article presents the basics of creating and installing a third-party Synergy Contacts package. The app/service/accounts package extends and interacts with the resident webOS Contacts app and Account Manager service.

Note: This article assumes the reader has had some experience creating Mojo apps.

Integrating an app's contacts with those from the webOS Contacts app involves four steps:

  1. Extending the webOS db8 contacts kind.

  2. Installing an account template file the Account Manager service can read at start-up.

  3. Creating an account through the webOS Contacts app.

  4. Performing an initial sync of contacts upon account creation, then writing them to our extended kind.

After completing these steps, the contacts should appear in the webOS Contacts app.

Typically, a Synergy connector (contacts, calendar, email, etc.) creates a Synergy JavaScript service that connects to an outside data source for login and syncing. In addition, the service manages the caching of credentials and configuration data on the device. An account object on the device, in this case, serves as a proxy for a real provider account, such as one on Facebook, Google, or Linked-in. The Synergy service provides an account template containing, besides metadata, callbacks the Account Manager invokes when creating, deleting or modifying one of its account objects. The Synergy service also provides a callback to implement syncing with an outside data source.

This tutorial implements a bare-bones Synergy service that demonstrates interaction with the Account Manager service and syncing with an outside data source, in our case, Plaxo, an online address book and social networking site (www.plaxo.com).

This procedure demonstrates how to:

  • Create, package and install a combined app/service/accounts package

  • Configure db8 kinds with a service that the Configurator creates upon package installation

  • Create an account template file for installation with the package containing, besides metadata, callbacks the Account Manager service and Contacts app can invoke

  • Add a new account type in the webOS Contacts app for our new outside data source (Plaxo)

  • Perform an initial contact sync upon account creation

  • Modify a contact and re-sync it through the Contacts app

  • Troubleshoot and debug your app/service/accounts package

This tutorial is intended as a "Hello World" equivalent for implementing a Synergy connector. For brevity's sake, it does not include many of the features a full-blown app/service might typically include.

This procedure does not:

  • Implement two-way syncing. (You could implement this utilizing some combination of a db8 "watch" on device contact objects and the Mojo transport sync framework and utilities.)

  • Schedule syncing via the Activity Manager.

  • Implement extensive error-checking.

Syncing

This Synergy service implements a one-way sync from Plaxo to the device. The following tracking information will be kept in a single db8 data object:

  • Account ID
  • Date and time of last sync
  • An array of matching local db8 IDs and remote Plaxo IDs

During the initial sync, all contacts are downloaded. Subsequent syncs download new contacts and contacts updated since the last sync date/time. If a contact is updated, its current object in db8 is deleted and replaced with the new contact. Note that this implementation does not account for contacts deleted from Plaxo, yet continue to live in our db8 extended contacts kind.

Syncing in this example occurs initially when the account is created and, after that, when the user selects "Sync Now" in the Contacts app. Note that another option for syncing is to schedule it via the Activity Manager, which would periodically invoke our JavaScript service's "sync" routine.

In this section:

 


Prerequisites

  • Your development PC should have the webOS 2.0 (or higher) SDK installed.

  • If you are using a webOS device instead of the SDK's Emulator, then:

    • The device should have webOS 2.0 or higher installed.
    • You should have a USB cord to connect the device to your development PC.
    • The device should be charged and prepared.
    • You need a mechanism for logging into your device. The Tools section describes various mechanisms for doing this. Other options include using novaterm on the Mac. On a Windows PC, you can use putty (installed with the PDK) to launch a shell and log in to the device. See the Tools section for more information on using these utilities.
  • Go to www.plaxo.com, open an account, and create some sample contacts. For the purposes of this article, we have created an account with three sample contacts.

 


Terminology and Basic Concepts

Before we begin, let's review some basic terms and concepts the reader should have at least passing familiarity with.

  • db8

    db8 is an addition to the webOS JavaScript Framework's current storage methods designed to meet the needs of robust, high-performance applications. db8 is a service -- com.palm.db -- available on the device bus that interfaces to an embedded JSON database. db8 stores kind objects and data objects. Kind objects define the owner, schema, and indexes for data objects. Once you create a kind object, you can then store data objects of that kind.

    In this procedure, we are going to extend the webOS Contacts kind and write contact data objects to it.

    See the db8 documentation for more information.

  • Account Manager

    The Account Manager service (com.palm.service.accounts) provides central account and credentials management on the device. Synergy connectors can use the Account service when interacting with external account providers*
    such as Facebook, Google, Yahoo, and LinkedIn. Providers give users
    capabilities
    * such as contacts, calendar, blogging, email, and so on.

    To interact with the Account Manager service, services need to register a template file - account-template.json - that is read at the Account Manager service start-up. This file contains callbacks and metadata that define your service's interaction with the Account Manager.

    Contacts must have an associated account ("accountId" field) to show up in the webOS Contacts app. Contact providers must have an account template to appear in the "Add an Account" menu of the Palm Contacts app.

    In this procedure, we are going to create an account template file the Account Manager can process. Then, we are going to create an account for our contacts in the Contacts app.

    See the Account Manager documentation for more information.

  • Activity Manager

    The Activity Manager service (com.palm.activitymanager) acts as a traffic cop for activities (apps, services, tasks, network flows, etc.) running on the device, balancing activity priorities and system resources to optimize the user's experience.

    While we will not cover it in this tutorial, a typical Synergy service would schedule periodic syncing using the Activity Manager.

    See the Activity Manager documentation for more information.

  • Configurator

    At boot, the Configurator initializes db8 kinds and permissions, file cache, and activities. It initializes them if they do not already exist. When your package is installed, the Configurator runs to create your configured db8 kinds and permissions.

    In this tutorial, as part of the service, you will create configuration files the Configurator will use to create db8 kinds on your service's behalf.

  • JavaScript Services

    Currently, webOS apps have access to a rich set of application and system services available on the public bus. With webOS 2, third-party developers can now "roll their own" services. Besides powering the new Synergy APIs, JavaScript services strengthen webOS support for background processing and add new capabilities such as low-level networking, filesystem access and binary data processing to the webOS technology stack.

    In our sample package, we are going to create, package and install a service -- com.palmdts.testacct.contacts.service -- that is going to do all our processing. The Contacts app and Account Manager will call our service for credentials validation and syncing. The Mojo application component of our package is there simply as a stub since, currently, installing a JavaScript service requires a Mojo app.

    Note: The service ID must begin with the app ID.

    For example:

    App ID : com.palmdts.testacct
    Service ID : com.palmdts.testacct.contacts.service
    
    

    Services do not run all the time, but launch when needed and terminate when not in use. You can specify how long a service runs between calls with the "acitivityTimeout" field in the service's "services.json" configuration file. If not defined, the default is 60 seconds.

    Debugging tip: If you change and re-install a service but it continues to behave as before, make sure the service is not still running during the re-installation. You can check if it is still running with "ps -aux", and "kill" it if it is.

  • Key Manager

    The Key Manager service (com.palm.keymanager) provides JavaScript applications with key management and cryptographic functionality. It makes this available on the device's public bus via a number of API calls. The Key Manager implements a key store, allowing apps a place to safely keep keys where they are readily available for cryptographic operations.

    We are going to encrypt and store the username and password for our Plaxo account using the Key Manager. See the Key Manager documentation for more information.

  • Foundations

    Foundations is a loadable framework of utility JavaScript APIs that both Mojo and JavaScript service applications can use. This tutorial utilizes Foundation APIs to interact with db8 and other services on the webOS message bus.

    See the Foundations API Reference for more information.

  • Futures

    A Future is a class -- part of the Foundations loadable framework -- that provides a mechanism for implementing asychronous callbacks. The advantage with using Futures is that it handles results and exceptions in a more flexible way than traditional callback mechanisms.

    Services are required to return a Future. For more information, see the section on Futures in the Foundations API Reference.

 


To Create a Synergy Contacts Package

Note that this procedure is being done on a Windows PC, but Mac users should have no problem doing the same on their machine.

Step 1. Create a folder for your package.

For example:

C:\SampleSynPackage

Step 2. Create package, application, accounts and service sub-directories.

You are going to need 4 sub-directories: one each for the app, service, accounts and package.

  1. Open a command prompt, go to c:\SampleSynPackage, and generate your stub app:
 c:\SampleSynPackage > palm-generate testapp

  1. Manually create the following sub-directories. (Note that currently, palm-generate is not set up to create these directories.)
 c:\SampleSynPackage\package
 c:\SampleSynPackage\service
 c:\SampleSynPackage\accounts

  1. Manually create the following \service and \accounts sub-directories.
 service\configuration
 service\configuration\db
 service\configuration\db\kinds
 accounts\images

Not including the testapp sub-directories, you should now have a directory structure that looks like this:

c:\SampleSynPackage
      \accounts 
          \images
       \package
       \service 
           \configuration
               \db
                   \kinds
            \testapp                              

Step 3. Create the files in Appendix A

You should now have a directory/file structure that looks like this:

       c:\SampleSynPackage
           \accounts
               account-template.json
               \images
                   plaxo32.png
           \package
               packageinfo.json
           \service 
               prologue.js
               sources.json
               services.json
               serviceEndPoints.js
               \configuration
                   \db
                       \kinds
                           com.palmdts.contact.testacct
                           com.palmdts.contact.transport
           \testapp
               appinfo.json
               framework-config.json
               icon.png
               index.html
               sources.json
               \images
               \stylesheets
                   testapp.css
               \app
                   \assistants
                       first-assistant.js
                       stage-assistant.js
                   \views
                       \first
                            first-scene.html
                    \models
                         helpers.js

Step 4. Package and install your app/service/accounts package.

At the command line prompt, enter the following commands:

c:\SampleSynPackage> palm-package testapp service package accounts
c:\SampleSynPackage> palm-install com.palmdts.testacct_1.0.0_all.ipk 

Step 5. Verify your installation.

  • Was the app installed?

    Open a shell to the device and see if the following file exists:

 /media/cryptofs/apps/usr/palm/applications/com.palmdts.testacct

  • Was the service installed?

    See if the following directory exists.(Note that this directory contains all of your service files.)

 /media/cryptofs/apps/usr/palm/services/com.palmdts.testacct.contacts.service

  • Was the account template installed?

    Check that the following directory containing the account-template.json exists:

 /media/cryptofs/apps/usr/palm/accounts/com.palmdts.testacct

  • Was our extended db8 contacts kind created?

    On the device, make the following luna-send call and check that you get this result:

 luna-send -n 1  -a com.palmdts.testacct.contacts.service luna://com.palm.db/find '{"query":{"from":"com.palmdts.contact.testacct:1"}}'
 {"returnValue":true,"results":[]}

Step 6. Launch the Contacts app.

  • Go to: Contacts menu > Preferences & Accounts > Add an account

    You should see a screen with "Plaxo Contacts" listed:

  • Select "Plaxo Contacts"

    The login screen appears asking for username/password. Enter the username/password for your Plaxo account and select "Sign in".

    Expected behavior: Your "checkCredentials" Synergy service assistant function is called to validate the username/password over the cloud.

    You should see this screen:

  • Select "Create"

    Expected behavior: Your "onCreate" Synergy service function is called first, then your "onEnabled" function. The "onCreate" function saves username/password to encrypted storage. The "onEnabled" function downloads your contacts which you should now be able to view on the main Contacts screen:

    Note: To start over, you can delete the account in the Accounts app. This calls your "onDelete" function which performs all necessary clean-up of contacts, housekeeping information, and stored keys.

Step 7. Modify a contact and re-sync.

  • Go to Plaxo and modify one of your contacts.

    In this example, we will add "Sir" as a title for "Chester Fields".

  • Go to: Contacts menu > Preferences & Accounts and select "Sync Now".

    Expected behavior: Your Synergy service's "sync" function is called. Contacts updated since the last sync are downloaded and re-synced. You should see your changes on the main Contacts screen:


Troubleshooting and Debugging

Check for cut-and-paste errors

It is very easy to make a cut-and-paste error when creating or modifying a large number of files. Given that a missing bracket, parentheses or comma can be fatal, it is recommended you run your code through a JavaScript checker (e.g. http://www.jslint.com) and your JSON files through a JSON validator (e.g. http://jsonformatter.curiousconcept.com/).

Even though the code has changed, the service continues to execute as before.

Sometimes the service keeps running for a period of time, even though the underlying code has changed. Check to see if it is still running with "ps -aux" and "kill" it if that is the case.

To open a shell and log in to the device:

Step 1. Open a command prompt.

  1. On Windows type:
putty -P 10022 root@localhost

  1. On the Mac OS X:
ssh -p 10022 root@localhost

Step 2. Press "Enter" at the password prompt.

Note: On Windows, you can also use "novacom" to open a shell:

novacom open tty:// = novaterm

To open a shell and log in to the Emulator:

Step 1. Open a command prompt.

  1. On Windows type:
putty -P 5522 localhost

  1. On the Mac OS X:
ssh -p 5522 localhost

Step 2. Enter "root" at the login prompt.

Step 3. Press "Enter" at the password prompt.

To manually start a service:

You can use "run-js-service" to start your service and see if it runs:

/media/cryptofs/apps/usr/palm/services# run-js-service /media/cryptofs/apps/usr/palm/services/com.palmdts.testacct.contacts.service

The "activityTimeout" field in "services.json" determines how long the service stays active without being called.

To monitor console messages in realtime:

Open a shell and run:

tail -f /var/log/messages

The "-f" option causes tail to display the last 10 lines of messages and append new lines to the display as they are added.

To show output for just your app:

tail -f /var/log/messages | grep <packageid> 

Possible useful luna-service commands

// List all accounts on the device
luna-send -n 1 -f palm://com.palm.service.accounts/listAccounts '{}'

// List Account templates supporting contacts capability
luna-send -n 1 -f palm://com.palm.service.accounts/listAccountTemplates '{"capability":"CONTACTS"}'

// Get extended contacts
luna-send -n 1  -a com.palmdts.testacct.contacts.service luna://com.palm.db/find '{"query":{"from":"com.palmdts.contact.testacct:1"}}'

// Delete objects the service creates
luna-send -n 1 -a com.palmdts.testacct.contacts.service luna://com.palm.db/del '{"ids":[<id>]}'

 


Appendix A: Package/Service/Accounts/App files

Package file

Path

package\
    packageinfo.json

Contents

{
  "id": "com.palmdts.testacct",
  "package_format_version": 2,
  "loc_name": "Palm Synergy Contact Demo",
  "version": "1.0.0",
  "vendor": "Palm",
  "vendorurl": "www.palm.com",
  "app": "com.palmdts.testacct",
  "services": ["com.palmdts.testacct.contacts.service"],
  "accounts": ["com.palmdts.testacct.contact"]
}

Notes

This file defines the package ID, app, services, and template data for the service and app package. Most of these fields should be familiar to those who have configured appinfo.json for Mojo apps.

The "services" and "accounts" fields define the service and account file we are creating. Once installed, the account-template.json becomes "com.palmdts.testacct.contact"


Account template file

Path

accounts\
   account-template.json

Contents

{
    "templateId": "com.palmdts.testacct.contact",
    "loc_name": "Plaxo Contacts",
    "readPermissions": ["com.palmdts.testacct.contacts.service"],
    "writePermissions": ["com.palmdts.testacct.contacts.service"],
    "validator": "palm://com.palmdts.testacct.contacts.service/checkCredentials",
    "onCapabiltiesChanged" : "palm://com.palmdts.testacct.contacts.service/onCapabiltiesChanged",       
    "onCredentialsChanged" : "palm://com.palmdts.testacct.contacts.service/onCredentialsChanged",   
    "loc_usernameLabel": "Email address",
    "icon": {"loc_32x32": "images/plaxo32.png"},    
    "capabilityProviders": [{
        "capability": "CONTACTS",
        "id"        : "com.palmdts.contacts.testacct",
        "onCreate"  : "palm://com.palmdts.testacct.contacts.service/onCreate",  
        "onEnabled" : "palm://com.palmdts.testacct.contacts.service/onEnabled", 
        "onDelete"  : "palm://com.palmdts.testacct.contacts.service/onDelete",
        "sync"      : "palm://com.palmdts.testacct.contacts.service/sync", 
        "loc_name"  : "Plaxo Contacts",
        "dbkinds": {  
                "contact": "com.palmdts.contact.testacct:1"
        }
    }]
}

Notes

This file is needed for interaction with the Account Manager service. Typically, this is provided by a Synergy service that connects to an outside data source for log in and syncing as well as managing the caching of credentials and configuration data on the device. An account object serves as a proxy for a real provider account, such as for Facebook.

For our example, we are going to implement one capability (CONTACTS), indicating the extended kind we are going to provide for this -- com.palmdts.contact.testacct:1.

See the Account Manager documentation for more explanation of these fields.

To provide an icon for the new account type, you should add the following "plaxo32.png" file to accounts\images:

This is going to used for your app in webOS Accounts and Contacts.

The Synergy service assistant functions are invoked by either the Account Manager service or the webOS Contacts app:


services.json

Path

service\
    services.json

Contents

{
   "id":"com.palmdts.testacct.contacts.service",
   "description":"Test Contact Service",
   "engine":"node",
   "activityTimeout":30,
   "services":[
      {
         "name":"com.palmdts.testacct.contacts.service",
         "description":"Test Contact",
         "globalized":false,
         "commands":[
            {
               "name":"checkCredentials",
               "assistant":"checkCredentialsAssistant",
               "public":true
            },
            {
               "name":"onCapabiltiesChanged",
               "assistant":"onCapabiltiesChangedAssistant",
               "public":true
            },  
            {
               "name":"onCredentialsChanged",
               "assistant":"onCredentialsChangedAssistant",
               "public":true
            },    
            {
               "name":"onCreate",
               "assistant":"onCreateAssistant",
               "public":true
            },    
            {
               "name":"onEnabled",
               "assistant":"onEnabledAssistant",
               "public":true
            },        
            {
               "name":"onDelete",
               "assistant":"onDeleteAssistant",
               "public":true
            },            
            {
               "name":"sync",
               "assistant":"syncAssistant",
               "public":true
            }                                                                    
         ]
      }
   ]
}

Notes

This file defines the service, its name, and the commands it provides on the webOS bus.

Fields of note:

  • id - Name used to call the service on the public bus.

  • engine - The runtime environment, in this case, node.js.

  • assistant -- The method invoked to implement the command.

  • public -- If true, the command is callable on the public bus.

  • globalized - If true, locale-dependent processing, such as the way names, addresses and phone numbers are parsed, is invoked. In general, services should try to be locale-agnostic as invoking this can add significant processing time.

  • activityTimeout -- How long, in seconds, the service continues to run between calls. Services do not run all the time, but launch when needed, and terminate when not in use. If not defined, this defaults to 60 seconds.


sources.json

Path

service\
    sources.json

Contents

[
   {
      "library":{
         "name":"foundations",
         "version":"1.0"
      }
   },
   {
      "source":"prologue.js"
   },
   {
      "source":"serviceEndPoints.js"
   }
]

Notes

The equivalent to that for Mojo apps: it declares what source files should be loaded into the current service. In this case, it loads the Foundations library, an initialization file (prologue.js), and a file implementing service commands (serviceEndPoints.js).


com.palmdts.contact.testacct

Path

service\
  configuration\
    db\
       kinds\
          com.palmdts.contact.testacct

Contents

{
    "id": "com.palmdts.contact.testacct:1",
    "owner": "com.palmdts.testacct.contacts.service",
    "sync": true,
    "indexes": [{ 
       "name": "accountId", 
       "props": [{ "name": "accountId"}]}], 
    "extends": ["com.palm.contact:1"]
}

Notes

When installed, the Configurator uses this file to create our extended contacts kind - com.palmdts.contact.testacct:1. Our service is the owner and we are creating one index on the accountId field.


com.palmdts.contact.transport

Path

service\
  configuration\
    db\
       kinds\
          com.palmdts.contact.transport

Contents

{
    "id": "com.palmdts.contact.transport:1",
    "owner": "com.palmdts.testacct.contacts.service",
    "indexes": [{ 
       "name": "lastSync", 
       "props": [{ "name": "lastSync"}]}
       ]
}

Notes

When installed, the Configurator uses this file to create the db8 kind we are going to store housekeeping information for syncing - accountId, last sync date/time and local id/remote id object pairs.

If you are creating a Mojo app component that is more than a stub and needs to access your kind data objects, then you need to create a "permissions" file for each of your kinds. This would be in a service/configuration/db/permissions folder and have the same name as your kind file. For instance, a permissions file for our extended contacts kind would have the path: service/configuration/db/permissions/com.palmdts.contact.testacct, and look like this:

[
    {
        "type": "db.kind",
        "object": "com.palmdts.contact.testacct:1",
        "caller": "com.palmdts.testacct",
        "operations": {
            "read": "allow",
            "create": "allow",
            "delete": "allow",
            "update": "allow"
        }
    }
]

This would give our Mojo app component -- com.palmdts.testacct -- total access to our extended contacts kind. See the db8 documentation on granting kind permissions for more information.


prologue.js

Path

service\prologue.js

Contents

//...
//... Load the Foundations library and create
//... short-hand references to some of its components.
//...
var Foundations = IMPORTS.foundations;
var DB = Foundations.Data.DB;
var Future = Foundations.Control.Future;
var PalmCall = Foundations.Comms.PalmCall;
var AjaxCall = Foundations.Comms.AjaxCall;

//..
//.. Returns the current date/time in the format Plaxo expects. 
//...Used in syncing.
//..
function calcSyncDateTime()
{
    // 
    // Get the current date/time and put it in the format Plaxo is expecting
    // i.e., "2005-01-01T00:00:00Z"
    //
    var d = new Date();
    var hour = d.getHours();
    var seconds = d.getSeconds();

    if (seconds < 10) seconds = "0"+seconds;
    if (hour < 10)  hour= "0"+hour;

    var syncDateTime = d.getFullYear() + "-" + (d.getMonth() + 1) + "-" + d.getDate() +"T"+hour+":"+d.getMinutes()+":"+seconds+"Z"; 
    return(syncDateTime);
}


//...
//...Base64 encode/decode functions. Plaxo expects Base64 encoding for username/password.
//...
/**
*  Base64 encode / decode
*  http://www.webtoolkit.info/
**/ 
var Base64 = {
    // private property
    _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
    // public method for encoding
    encode : function (input) {
        var output = "";
        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
        var i = 0;
        input = Base64._utf8_encode(input);
        while (i < input.length) {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);

            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;

            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } 
            else if (isNaN(chr3)) {
                enc4 = 64;
            }

            output = output +
            this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
            this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
        }
        return output;
    },

    // public method for decoding
    decode : function (input) {
        var output = "";
        var chr1, chr2, chr3;
        var enc1, enc2, enc3, enc4;
        var i = 0;

        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

        while (i < input.length) {

            enc1 = this._keyStr.indexOf(input.charAt(i++));
            enc2 = this._keyStr.indexOf(input.charAt(i++));
            enc3 = this._keyStr.indexOf(input.charAt(i++));
            enc4 = this._keyStr.indexOf(input.charAt(i++));

            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;

            output = output + String.fromCharCode(chr1);

            if (enc3 != 64) {
                output = output + String.fromCharCode(chr2);
            }
            if (enc4 != 64) {
                output = output + String.fromCharCode(chr3);
            }
        }
        output = Base64._utf8_decode(output);

        return output;
    },
    // private method for UTF-8 encoding
    _utf8_encode : function (string) {
        string = string.replace(/\r\n/g,"\n");
        var utftext = "";

        for (var n = 0; n < string.length; n++) {
             var c = string.charCodeAt(n);
             if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }
         }
         return utftext;
    },
    // private method for UTF-8 decoding
    _utf8_decode : function (utftext) {
        var string = "";
        var i = 0;
        var c = 0, c1 = 0, c2 = 0;

        while ( i < utftext.length ) {
            c = utftext.charCodeAt(i);
            if (c < 128) {
                string += String.fromCharCode(c);
                i++;
            }
            else if((c > 191) && (c < 224)) {
                c2 = utftext.charCodeAt(i+1);
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                i += 2;
            }
            else {
                c2 = utftext.charCodeAt(i+1);
                c3 = utftext.charCodeAt(i+2);
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }
        }
        return string;
    }
};

serviceEndPoints.js

Path

service\
     serviceEndPoints.js

Contents

//***************************************************
// Validate contact username/password 
//***************************************************
var checkCredentialsAssistant = function(future) {};


checkCredentialsAssistant.prototype.run = function(future) {  

     var args = this.controller.args;  
     console.log("Test Service: checkCredentials args =" + JSON.stringify(args));

     //...Base64 encode our entered username and password
     var base64Auth = "Basic " + Base64.encode(args.username + ":" + args.password);

     //...Request contacts, which requires a username and password
     //...Ask for contacts updated in last second or so to minimize network traffic
     var syncURL = "http://www.plaxo.com/pdata/contacts?updatedSince=" + calcSyncDateTime();

     //...If request fails, the user is not valid
     AjaxCall.get(syncURL, {headers: {"Authorization":base64Auth, "Connection": "keep-alive"}}).then ( function(f2)
     {
        if (f2.result.status == 200 ) // 200 = Success
        {    
            //...Pass back credentials and config (username/password); config is passed to onCreate where
            //...we will save username/password in encrypted storage
            future.result = {returnValue: true, "credentials": {"common":{ "password" : args.password, "username":args.username}},
                                                "config": { "password" : args.password, "username":args.username} };
        }
        else   {
           future.result = {returnValue: false};
        }
     });    
};

//***************************************************
// Capabilites changed notification
//***************************************************
var onCapabilitiesChangedAssistant = function(future){};

// 
// Called when an account's capability providers changes. The new state of enabled 
// capability providers is passed in. This is useful for Synergy services that handle all syncing where 
// it is easier to do all re-syncing in one step rather than using multiple 'onEnabled' handlers.
//

onCapabilitiesChangedAssistant.prototype.run = function(future) { 
    var args = this.controller.args; 
    console.log("Test Service: onCapabilitiesChanged args =" + JSON.stringify(args));   
    future.result = {returnValue: true};
};

//***************************************************
// Credentials changed notification 
//***************************************************
var onCredentialsChangedAssistant = function(future){};
//
// Called when the user has entered new, valid credentials to replace existing invalid credentials. 
// This is the time to start syncing if you have been holding off due to bad credentials.
//
onCredentialsChangedAssistant.prototype.run = function(future) { 
    var args = this.controller.args; 
    console.log("Test Service: onCredentialsChanged args =" + JSON.stringify(args));    
    future.result = {returnValue: true};
};


//***************************************************
// Account created notification
//***************************************************
var onCreateAssistant = function(future){};

//
// The account has been created. Time to save the credentials contained in the "config" object
// that was emitted from the "checkCredentials" function.
//
onCreateAssistant.prototype.run = function(future) {  

    var args = this.controller.args;

    //...Username/password passed in "config" object
    var B64username = Base64.encode(args.config.username);
    var B64password = Base64.encode(args.config.password);

    var keystore1 = { "keyname":"AcctUsername", "keydata": B64username, "type": "AES", "nohide":true};
    var keystore2 = { "keyname":"AcctPassword", "keydata": B64password, "type": "AES", "nohide":true};

    //...Save encrypted username/password for syncing.
    PalmCall.call("palm://com.palm.keymanager/", "store", keystore1).then( function(f) 
    {
        if (f.result.returnValue === true)
        {
            PalmCall.call("palm://com.palm.keymanager/", "store", keystore2).then( function(f2) 
           {
              future.result = f2.result;
           });
        }
        else   {
           future.result = f.result;
        }
    });
};

//***************************************************
// Account deleted notification
//***************************************************
var onDeleteAssistant = function(future){};

//
// Account deleted - Synergy service should delete account and config information here.
//

onDeleteAssistant.prototype.run = function(future) { 


    //..Create query to delete contacts from our extended kind associated with this account
    var args = this.controller.args;
    var q ={ "query":{ "from":"com.palmdts.contact.testacct:1", "where":[{"prop":"accountId","op":"=","val":args.accountId}] }};

    //...Delete contacts from our extended kind
    PalmCall.call("palm://com.palm.db/", "del", q).then( function(f) 
    {
        if (f.result.returnValue === true)
        {
           //..Delete our housekeeping/sync data
           var q2 = {"query":{"from":"com.palmdts.contact.transport:1"}};
           PalmCall.call("palm://com.palm.db/", "del", q2).then( function(f1) 
           {
              if (f1.result.returnValue === true)
              {
                 //...Delete our account username/password from key store
                 PalmCall.call("palm://com.palm.keymanager/", "remove", {"keyname" : "AcctUsername"}).then( function(f2) 
                 {
                    if (f2.result.returnValue === true)
                    {
                       PalmCall.call("palm://com.palm.keymanager/", "remove", {"keyname" : "AcctPassword"}).then( function(f3) 
                       {
                          future.result = f3.result;
                       });
                    }
                    else   {
                       future.result = f2.result;
                    }
                 });   
              }
              else   {
                 future.result = f1.result;
              }
           });
        }
        else   {
           future.result = f.result;
        }
    });     
};

//*****************************************************************************
// Capability enabled notification - called when capability enabled or disabled
//*****************************************************************************
var onEnabledAssistant = function(future){};

//
// Synergy service got 'onEnabled' message. When enabled, a sync should be started and future syncs scheduled.
// Otherwise, syncing should be disabled and associated data deleted.
// Account-wide configuration should remain and only be deleted when onDelete is called.
// 

onEnabledAssistant.prototype.run = function(future) {  



    var args = this.controller.args;

    if (args.enabled === true) 
    {
        //...Save initial sync-tracking info. Set "lastSync" to a value that returns all records the first-time
        var acctId = args.accountId;
        var ids = [];
        var syncRec = { "objects":[{ _kind: "com.palmdts.contact.transport:1", "lastSync":"2005-01-01T00:00:00Z", "accountId":acctId, "remLocIds":ids}]};
        PalmCall.call("palm://com.palm.db/", "put", syncRec).then( function(f) 
        {
            if (f.result.returnValue === true)
            {
               PalmCall.call("palm://com.palmdts.testacct.contacts.service/", "sync", {}).then( function(f2) 
               { 
                  // 
                  // Here you could schedule additional syncing via the Activity Manager.
                  //
                  future.result = f2.result;
               });
            }
            else {
               future.result = f.result;
            }
        });
    }
    else {
       // Disable scheduled syncing and delete associated data.
    }

    future.result = {returnValue: true};    
};


//***************************************************
// Sync function
//***************************************************
var syncAssistant = function(future){};

syncAssistant.prototype.run = function(future) { 

        var args = this.controller.args;

        var username = "";
        var password = "";

        //..Retrieve our saved username/password
        PalmCall.call("palm://com.palm.keymanager/", "fetchKey", {"keyname" : "AcctUsername"}).then( function(f) 
        {
           if (f.result.returnValue === true)
           {
              username = Base64.decode(f.result.keydata);
              PalmCall.call("palm://com.palm.keymanager/", "fetchKey", {"keyname" : "AcctPassword"}).then( function(f1) 
              {
                  if (f1.result.returnValue === true)
                  {
                     password = Base64.decode(f1.result.keydata);

                     //..Format Plaxo authentication
                     var base64Auth = "Basic " + Base64.encode(username + ":" + password);
                     var syncURL = "http://www.plaxo.com/pdata/contacts?updatedSince=";

                     //..Get our sync-tracking information saved previously in a db8 object
                     var q = {"query":{"from":"com.palmdts.contact.transport:1"}};
                     PalmCall.call("palm://com.palm.db/", "find", q).then( function(f2) 
                     {
                        if (f2.result.returnValue === true)
                        {
                           var id        = f2.result.results[0]._id; 
                           var accountId = f2.result.results[0].accountId;     
                           var remLocIds = f2.result.results[0].remLocIds;  // local id/remote id pairs
                           var lastSync  = f2.result.results[0].lastSync;   // date/time since last sync


                           syncURL = syncURL + lastSync + "&fields=%40all&sortBy=id&sortOrder=ascending";

                           console.log("Test Service: syncURL="+syncURL +"\n");

                           //...Get our updated or new contacts from Plaxo
                           AjaxCall.get(syncURL, {headers: {"Authorization":base64Auth, "Connection": "keep-alive"}}).then ( function(f3)
                           {
                               if (f3.result.status === 200 ) // 200 = Success
                               {
                                   //... Turn JSON text into JSON object, Yes, eval is evil.
                                  var results =  eval('(' + f3.result.responseText + ')');

                                  if (results.totalResults <= 0)  { // Return if no new or updated records.
                                     future.result = f3.result;
                                  }

                                  console.log("Test Service: results=" + JSON.stringify(results.entry));

                                  //...Add necessary fields for our extended contacts.
                                  //...Collect all remote ids into array to check if they already exist in db8
                                  var remIds =[];
                                  for (i=0; i < results.totalResults; i++)
                                  {
                                     results.entry[i].accountId = accountId;
                                     results.entry[i]._kind = "com.palmdts.contact.testacct:1";
                                     remIds.push(results.entry[i].id);  
                                  }

                                  //...Find all returned contacts that are already in db8
                                  var delIds = [];
                                  for (i=0; i < remIds.length; i++)
                                  {
                                     var found = false;
                                     for (j=0; j < remLocIds.length && !found; j++)
                                     {
                                        //...Does remote id match one we are storing
                                        if (remIds[i] == remLocIds[j].remId)
                                        { 
                                           delIds.push(remLocIds[j].locId); // Save for deletion
                                           remLocIds.splice(j, 1);  // Remove from our local record-keeping
                                           found = true;
                                        }
                                      }
                                   } 

                                  //...Delete all contacts that have been updated. Note that empty array still returns true
                                  delObjs = {"ids":delIds};
                                  PalmCall.call("palm://com.palm.db/",  "del", delObjs).then( function(f4) 
                                  {
                                     if (f4.result.returnValue === true)
                                     {
                                        //...Save our updated or new contacts
                                        var newContactObjects = {"objects":results.entry};

                                        //..Write new or updated contacts
                                        PalmCall.call("palm://com.palm.db/",  "put", newContactObjects).then( function(f5) 
                                        {
                                           if (f5.result.returnValue === true)
                                           {
                                               var idObj = {};

                                               //...Create objects containing assoc. remote ids and local ids for local record-keeping
                                               for (i=0; i < f5.result.results.length; i++)
                                               {
                                                  idObj = {"locId": f5.result.results[i].id, "remId":remIds[i]};
                                                  remLocIds.push(idObj);  
                                               }

                                               var lastSyncDateTime = calcSyncDateTime(); // Get date/time of this sync                    
                                               var syncRec = { "objects": [{ "_id":id, "lastSync":lastSyncDateTime, "remLocIds": remLocIds}]};

                                               //...Update our sync-tracking info
                                               PalmCall.call("palm://com.palm.db/",  "merge", syncRec).then( function(f6) 
                                               {
                                                  future.result = f6.result; 
                                               });
                                          }
                                          else   {
                                             future.result = f5.result;  // "put" of new contacts failure
                                          }   
                                        });                           
                                     }
                                     else   {
                                        future.result = f4.result; // "del" of updated contacts failure
                                     }
                                  });   // del objs   
                              }
                              else   {
                                  future.result = f3.result;  // Ajax Call failure
                              }       
                         }); 
                     }
                     else   {
                        future.result = f2.result;  // Failure to "get" local sync-tracking info
                     }           
                 });         
               }
               else {
                     future.result = f1.result;  // Failure to get account pwd from Key Manager
               }
           });
        }
        else   {
              future.result = f.result;  // Failure to get account username from Key Manager
        }
     });  
}; 

Notes

This file implements the service commands. We use the Foundations PalmCall and AjaxCall to call services on the message bus and return a Future.


Application files

Mojo app developers should be familiar with the app files below. See the in-code comments for what we are specifically doing here. Refer to the Mojo documentation for a general explanation of these files.

Note that the application here is merely a stub -- all implementation and functionality takes place through the Contacts app. Services, for the time being, are required to have an application component.


sources.json

Path

testapp\
   sources.json

Contents

[
    {   "source": "app/assistants/stage-assistant.js" },
    {
        "scenes": "first",
        "source": "app/assistants/first-assistant.js"
    },
    {   "source": "app/models/helpers.js" }
]

appinfo.json

Path

testapp\
   appinfo.json

Contents

{
    "id": "com.palmdts.testacct",
    "version": "1.0.0",
    "vendor": "HP Palm",
    "type": "web",
    "main": "index.html",
    "title": "Synergy Contacts",
    "icon": "icon.png"
}

helpers.js

Path

testapp\
  app\
    models\
       helpers.js

Contents

// Simple logging to app screen - requires target HTML element with id of "targOutput"
var logData = function(controller, logInfo) {
    this.targOutput = controller.get("targOutput");
    this.targOutput.innerHTML =  logInfo + "
" + this.targOutput.innerHTML; };

index.html

Path

testapp\index.html

Contents

<!DOCTYPE html>
<html>
<head>
    <title>account.app</title>

    <!-- Include JS for loading Foundation libraries-->   
    <script src="/usr/palm/frameworks/mojo/mojo.js" type="text/javascript" x-mojo-version="1"></script>
    <script src="/usr/palm/frameworks/mojoloader.js" type="text/javascript"></script>

    <!-- application stylesheet should come in after the one loaded by the framework -->
    <link href="stylesheets/accountapp.css" media="screen" rel="stylesheet" type="text/css">
</head>
</html>

first-scene.html

Path

testapp\app\views\first\first-scene.html

Note that under \\views, you need to create a \\first sub-directory:

Contents

<!--Output area for log messages-->
<div class="palm-body-text">
    <div id="targOutput">

    </div>
</div>

first-assistant.js

Note that you have to create this file.

Path

testapp\app\assistants\first-assistant.js

Contents

function FirstAssistant() {};

FirstAssistant.prototype.setup = function() {

   logData(this.controller, "THIS IS ONLY A STUB. USE THE CONTACTS APP FOR ALL IMPLEMENTATION");
};

stage-assistant.js

Path

  testapp\app\assistants\stage-assistant.js

Contents

function StageAssistant() {
    /* this is the creator function for your stage assistant object */
};

StageAssistant.prototype.setup = function() {
    this.controller.pushScene("first");

};