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:

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.

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.

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

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

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

 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.

Step 7. Modify a contact and re-sync.


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 manually start a service:

You can use "run-js-service" to start your service in a device shell 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>]}'

Another useful tool for testing services is ls-monitor, which lets you see traffic going over the webOS service bus, similar to a network sniffer that lets you observe HTTP traffic.

 


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:


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");

};