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:
-
Extending the webOS db8 contacts kind.
-
Installing an account template file the Account Manager service can read at start-up.
-
Creating an account through the webOS Contacts app.
-
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
- Terminology and Basic Concepts
- To Create a Synergy Contacts Package
- Troubleshooting and Debugging
- Appendix A: Package/Service/Accounts/App files
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 useputty
(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 userscapabilities* 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.
- Open a command prompt, go to
c:\SampleSynPackage
, and generate your stub app:
c:\SampleSynPackage > palm-generate testapp
- 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
- 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 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:
-
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
-- Iftrue
, the command is callable on the public bus. -
globalized
- Iftrue
, 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"); };