Creating a Synergy Contacts Package

When first introduced, Synergy set the standard for accessing and managing personal data. With HP webOS 2.0, 3rd-party developers can now code their own Synergy connectors. This article presents the basics of creating and installing a 3rd-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 4 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 (http://www.plaxo.com).

This procedure demonstrates how to:

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:

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:

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


Terminology and Basic Concepts

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


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.

  1. Create a folder for your package.

    For example:

    C:\SampleSynPackage
    
  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:, and generate your stub app:

      c:\SampleSynPackage > palm-generate testapp
      
    2. Manually create the following sub-directories:

             c:\SampleSynPackage\package
             c:\SampleSynPackage\service
             c:\SampleSynPackage\accounts
      

      Note that currently, palm-generate is not set up to create these directories.

    3. Manually create the following and 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                              
    
  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                                                                                                                                                                                                                                                                                                                                                           
    
  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 
    
  5. Verify your installation.

    1. 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
      
    2. Was the service installed?

      See if the following directory exists:

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

      Note that this directory contains all of your service files.

    3. Was the account template installed?

      Check that the following directory exists:

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

      This directory contains the account-template.json file.

    4. Was our extended db8 contacts kind created?

      On the device, make the following luna-send call:

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

      You should see the following response:

      {"returnValue":true,"results":[]}
      
  6. Launch the Contacts app.

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

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

    2. 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:

    3. 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.

  7. Modify a contact and re-sync.

    1. Go to Plaxo and modify one of your contacts.

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

    2. 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:

  1. Open a command prompt.

  2. On Windows type:

        putty -P 10022 root@localhost
    
  3. On the Mac OS X:

        ssh -p 10022 root@localhost
    
  4. Press "Enter" at the password prompt.

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:

  1. Open a command prompt.

  2. On Windows type:

        putty -P 5522 localhost
    
  3. On the Mac OS X:

        ssh -p 5522 localhost
    
  4. Enter "root" at the login prompt.

  5. 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:

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

  1. Package file

    File Contents Notes
    package\
        packageinfo.json
    
    {
      "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"]
    }
    

    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".

  2. Account template file

    File Contents Notes
    accounts\
       account-template.json
    
    {
        "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"
            }
        }]
    }
    

    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:

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

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



  3. Service files

    File Contents Notes
    service\
        services.json
    
    {
       "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
                }                                                                    
             ]
          }
       ]
    }
    

    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.



    File Contents Notes
    service\
        sources.json
    
    [
       {
          "library":{
             "name":"foundations",
             "version":"1.0"
          }
       },
       {
          "source":"prologue.js"
       },
       {
          "source":"serviceEndPoints.js"
       }
    ]
    

    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).

    service\
      configuration\
        db\
           kinds\
              com.palmdts.contact.testacct
    
    {
        "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"]
    }
    

    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.

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

    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.

    Note: 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\kinds\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\kinds\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.

    service.js
    //...
    //... 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;
        }
    };
    


    service.js
    //***************************************************
    // 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 onEnabled 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
            }
         });  
    }; 
    

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

  4. 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.

    File Contents
    testapp\
       sources.json
    
    [
        {   "source": "app/assistants/stage-assistant.js" },
        {
            "scenes": "first",
            "source": "app/assistants/first-assistant.js"
        },
        {   "source": "app/models/helpers.js" }
    ]
    
    testapp\
       appinfo.json
    
    {
        "id": "com.palmdts.testacct",
        "version": "1.0.0",
        "vendor": "HP Palm",
        "type": "web",
        "main": "index.html",
        "title": "Synergy Contacts",
        "icon": "icon.png"
    }
    
    testapp\
      app\
        models\
           helpers.js
    
    // 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; };
    • Modify testapp.html:

      testapp.html
      <!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>
      
      
    • Create testapp-scene.html:

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

      testapp-scene.html
      <!--Output area for log messages-->
      <div class="palm-body-text">
          <div id="targOutput">
      
          </div>
      </div>
      
      
    • Create testapp-assistant.js:

      testapp-assistant.js
      function FirstAssistant() {};
      
      FirstAssistant.prototype.setup = function() {
      
         logData(this.controller, "THIS IS ONLY A STUB. USE THE CONTACTS APP FOR ALL IMPLEMENTATION");
      };
      
    • Modify testapp-assistant.js:

      testapp-assistant.js
      function StageAssistant() {
          /* this is the creator function for your stage assistant object */
      };
      
      StageAssistant.prototype.setup = function() {
          this.controller.pushScene("first");
      
      };