You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by pm...@apache.org on 2012/03/22 21:43:06 UTC

[1/7] git commit: [CB-280] Improve layout of cordova-js scripts

Updated Branches:
  refs/heads/master 3b06e03e6 -> e50b7ef68


[CB-280] Improve layout of cordova-js scripts

https://issues.apache.org/jira/browse/CB-280

This was basically a refactoring of the lib subdirectories,
without changing any of the code IN the lib subdirectories.

Other changes:

- build/packager.js

modified to build based on the new lib subdirectories

- Jakefile

modified to call a new method in build/packager

- README.md

updated file/directory locations

- test/runner.js

changed the runner to run off a new 'test' platform,
instead of using an on-the-fly platform it pieced
together when the tests run.

also changed the static() URL bits for the
browser-based test - it was pulling in more
than it needed

- test/suite.html

changed the urls of the jasmine files, based
on the updates to the static() URL bits in the
runner.js
-
- lib/common/exec.js
- lib/common/platform.js

stub files that will be replaced with platform-specific
ones


Project: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/commit/e50b7ef6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/tree/e50b7ef6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/diff/e50b7ef6

Branch: refs/heads/master
Commit: e50b7ef686e865cc8b6bb7caa5e05292de4148a6
Parents: 3b06e03
Author: Patrick Mueller <pm...@apache.org>
Authored: Thu Mar 22 16:28:17 2012 -0400
Committer: Patrick Mueller <pm...@apache.org>
Committed: Thu Mar 22 16:28:17 2012 -0400

----------------------------------------------------------------------
 Jakefile                                           |   13 +-
 README.md                                          |   72 ++--
 build/packager.js                                  |  279 ++++++-----
 lib/android/exec.js                                |   82 +++
 lib/android/platform.js                            |  131 +++++
 lib/android/plugin/android/app.js                  |   71 +++
 lib/android/plugin/android/callback.js             |   85 ++++
 lib/android/plugin/android/device.js               |   92 ++++
 lib/android/plugin/android/polling.js              |   33 ++
 lib/android/plugin/android/storage.js              |  377 ++++++++++++++
 lib/blackberry/exec.js                             |   56 +++
 lib/blackberry/platform.js                         |  179 +++++++
 lib/blackberry/plugin/blackberry/Contact.js        |  386 +++++++++++++++
 lib/blackberry/plugin/blackberry/ContactUtils.js   |  353 +++++++++++++
 lib/blackberry/plugin/blackberry/DirectoryEntry.js |  239 +++++++++
 lib/blackberry/plugin/blackberry/Entry.js          |   87 ++++
 lib/blackberry/plugin/blackberry/app.js            |   51 ++
 lib/blackberry/plugin/blackberry/contacts.js       |   62 +++
 lib/blackberry/plugin/blackberry/device.js         |   23 +
 lib/blackberry/plugin/blackberry/manager.js        |   87 ++++
 lib/blackberry/plugin/blackberry/notification.js   |   53 ++
 lib/bootstrap.js                                   |   69 ---
 lib/bootstrap/errgen.js                            |   20 -
 lib/bootstrap/playbook.js                          |    1 -
 lib/builder.js                                     |   86 ----
 lib/channel.js                                     |  207 --------
 lib/common/builder.js                              |   86 ++++
 lib/common/channel.js                              |  207 ++++++++
 lib/common/common.js                               |  180 +++++++
 lib/common/exec.js                                 |    1 +
 lib/common/platform.js                             |    1 +
 lib/common/plugin/Acceleration.js                  |    8 +
 lib/common/plugin/Camera.js                        |   84 ++++
 lib/common/plugin/CameraConstants.js               |   20 +
 lib/common/plugin/CaptureAudioOptions.js           |   13 +
 lib/common/plugin/CaptureError.js                  |   19 +
 lib/common/plugin/CaptureImageOptions.js           |   11 +
 lib/common/plugin/CaptureVideoOptions.js           |   13 +
 lib/common/plugin/CompassError.js                  |   13 +
 lib/common/plugin/CompassHeading.js                |    8 +
 lib/common/plugin/ConfigurationData.js             |   15 +
 lib/common/plugin/Connection.js                    |   12 +
 lib/common/plugin/Contact.js                       |  177 +++++++
 lib/common/plugin/ContactAddress.js                |   25 +
 lib/common/plugin/ContactError.js                  |   21 +
 lib/common/plugin/ContactField.js                  |   16 +
 lib/common/plugin/ContactFindOptions.js            |   13 +
 lib/common/plugin/ContactName.js                   |   20 +
 lib/common/plugin/ContactOrganization.js           |   23 +
 lib/common/plugin/Coordinates.js                   |   43 ++
 lib/common/plugin/DirectoryEntry.js                |   80 +++
 lib/common/plugin/DirectoryReader.js               |   41 ++
 lib/common/plugin/Entry.js                         |  198 ++++++++
 lib/common/plugin/File.js                          |   18 +
 lib/common/plugin/FileEntry.js                     |   63 +++
 lib/common/plugin/FileError.js                     |   25 +
 lib/common/plugin/FileReader.js                    |  249 ++++++++++
 lib/common/plugin/FileSystem.js                    |   17 +
 lib/common/plugin/FileTransfer.js                  |   68 +++
 lib/common/plugin/FileTransferError.js             |   13 +
 lib/common/plugin/FileUploadOptions.js             |   16 +
 lib/common/plugin/FileUploadResult.js              |   11 +
 lib/common/plugin/FileWriter.js                    |  253 ++++++++++
 lib/common/plugin/Flags.js                         |   15 +
 lib/common/plugin/LocalFileSystem.js               |   13 +
 lib/common/plugin/Media.js                         |  187 +++++++
 lib/common/plugin/MediaError.js                    |   16 +
 lib/common/plugin/MediaFile.js                     |   56 +++
 lib/common/plugin/MediaFileData.js                 |   18 +
 lib/common/plugin/Metadata.js                      |   10 +
 lib/common/plugin/Position.js                      |    8 +
 lib/common/plugin/PositionError.js                 |   17 +
 lib/common/plugin/ProgressEvent.js                 |   46 ++
 lib/common/plugin/accelerometer.js                 |   95 ++++
 lib/common/plugin/battery.js                       |   87 ++++
 lib/common/plugin/capture.js                       |   72 +++
 lib/common/plugin/compass.js                       |   90 ++++
 lib/common/plugin/contacts.js                      |   57 +++
 lib/common/plugin/geolocation.js                   |   94 ++++
 lib/common/plugin/network.js                       |   59 +++
 lib/common/plugin/notification.js                  |   56 +++
 lib/common/plugin/requestFileSystem.js             |   40 ++
 lib/common/plugin/resolveLocalFileSystemURI.js     |   41 ++
 lib/common/utils.js                                |   94 ++++
 lib/errgen/exec.js                                 |  107 ++++
 lib/errgen/platform.js                             |   28 +
 lib/errgen/plugin/errgen/device.js                 |   42 ++
 lib/exec/android.js                                |   82 ---
 lib/exec/blackberry.js                             |   56 ---
 lib/exec/errgen.js                                 |  107 ----
 lib/exec/ios.js                                    |   93 ----
 lib/exec/playbook.js                               |   56 ---
 lib/exec/test.js                                   |    1 -
 lib/exec/wp7.js                                    |   57 ---
 lib/ios/exec.js                                    |   93 ++++
 lib/ios/platform.js                                |   41 ++
 lib/ios/plugin/ios/Entry.js                        |    7 +
 lib/ios/plugin/ios/FileReader.js                   |   87 ++++
 lib/ios/plugin/ios/console.js                      |  102 ++++
 lib/ios/plugin/ios/device.js                       |   30 ++
 lib/ios/plugin/ios/nativecomm.js                   |   10 +
 lib/ios/plugin/ios/notification.js                 |    7 +
 lib/platform/android.js                            |  131 -----
 lib/platform/blackberry.js                         |  179 -------
 lib/platform/common.js                             |  180 -------
 lib/platform/errgen.js                             |   28 -
 lib/platform/ios.js                                |   41 --
 lib/platform/playbook.js                           |   16 -
 lib/platform/wp7.js                                |   16 -
 lib/playbook/exec.js                               |   56 +++
 lib/playbook/platform.js                           |   16 +
 lib/playbook/plugin/playbook/device.js             |   23 +
 lib/playbook/plugin/playbook/manager.js            |  321 ++++++++++++
 lib/plugin/Acceleration.js                         |    8 -
 lib/plugin/Camera.js                               |   84 ----
 lib/plugin/CameraConstants.js                      |   20 -
 lib/plugin/CaptureAudioOptions.js                  |   13 -
 lib/plugin/CaptureError.js                         |   19 -
 lib/plugin/CaptureImageOptions.js                  |   11 -
 lib/plugin/CaptureVideoOptions.js                  |   13 -
 lib/plugin/CompassError.js                         |   13 -
 lib/plugin/CompassHeading.js                       |    8 -
 lib/plugin/ConfigurationData.js                    |   15 -
 lib/plugin/Connection.js                           |   12 -
 lib/plugin/Contact.js                              |  177 -------
 lib/plugin/ContactAddress.js                       |   25 -
 lib/plugin/ContactError.js                         |   21 -
 lib/plugin/ContactField.js                         |   16 -
 lib/plugin/ContactFindOptions.js                   |   13 -
 lib/plugin/ContactName.js                          |   20 -
 lib/plugin/ContactOrganization.js                  |   23 -
 lib/plugin/Coordinates.js                          |   43 --
 lib/plugin/DirectoryEntry.js                       |   80 ---
 lib/plugin/DirectoryReader.js                      |   41 --
 lib/plugin/Entry.js                                |  198 --------
 lib/plugin/File.js                                 |   18 -
 lib/plugin/FileEntry.js                            |   63 ---
 lib/plugin/FileError.js                            |   25 -
 lib/plugin/FileReader.js                           |  249 ----------
 lib/plugin/FileSystem.js                           |   17 -
 lib/plugin/FileTransfer.js                         |   68 ---
 lib/plugin/FileTransferError.js                    |   13 -
 lib/plugin/FileUploadOptions.js                    |   16 -
 lib/plugin/FileUploadResult.js                     |   11 -
 lib/plugin/FileWriter.js                           |  253 ----------
 lib/plugin/Flags.js                                |   15 -
 lib/plugin/LocalFileSystem.js                      |   13 -
 lib/plugin/Media.js                                |  187 -------
 lib/plugin/MediaError.js                           |   16 -
 lib/plugin/MediaFile.js                            |   56 ---
 lib/plugin/MediaFileData.js                        |   18 -
 lib/plugin/Metadata.js                             |   10 -
 lib/plugin/Position.js                             |    8 -
 lib/plugin/PositionError.js                        |   17 -
 lib/plugin/ProgressEvent.js                        |   46 --
 lib/plugin/accelerometer.js                        |   95 ----
 lib/plugin/android/app.js                          |   71 ---
 lib/plugin/android/callback.js                     |   85 ----
 lib/plugin/android/device.js                       |   92 ----
 lib/plugin/android/polling.js                      |   33 --
 lib/plugin/android/storage.js                      |  377 --------------
 lib/plugin/battery.js                              |   87 ----
 lib/plugin/blackberry/Contact.js                   |  386 ---------------
 lib/plugin/blackberry/ContactUtils.js              |  353 -------------
 lib/plugin/blackberry/DirectoryEntry.js            |  239 ---------
 lib/plugin/blackberry/Entry.js                     |   87 ----
 lib/plugin/blackberry/app.js                       |   51 --
 lib/plugin/blackberry/contacts.js                  |   62 ---
 lib/plugin/blackberry/device.js                    |   23 -
 lib/plugin/blackberry/manager.js                   |   87 ----
 lib/plugin/blackberry/notification.js              |   53 --
 lib/plugin/capture.js                              |   72 ---
 lib/plugin/compass.js                              |   90 ----
 lib/plugin/contacts.js                             |   57 ---
 lib/plugin/errgen/device.js                        |   42 --
 lib/plugin/geolocation.js                          |   94 ----
 lib/plugin/ios/Entry.js                            |    7 -
 lib/plugin/ios/FileReader.js                       |   87 ----
 lib/plugin/ios/console.js                          |  102 ----
 lib/plugin/ios/device.js                           |   30 --
 lib/plugin/ios/nativecomm.js                       |   10 -
 lib/plugin/ios/notification.js                     |    7 -
 lib/plugin/network.js                              |   59 ---
 lib/plugin/notification.js                         |   56 ---
 lib/plugin/playbook/device.js                      |   23 -
 lib/plugin/playbook/manager.js                     |  321 ------------
 lib/plugin/requestFileSystem.js                    |   40 --
 lib/plugin/resolveLocalFileSystemURI.js            |   41 --
 lib/plugin/webworks/manager.js                     |   14 -
 lib/plugin/wp7/device.js                           |   26 -
 lib/require.js                                     |   43 --
 lib/scripts/bootstrap-errgen.js                    |   20 +
 lib/scripts/bootstrap-playbook.js                  |    1 +
 lib/scripts/bootstrap.js                           |   69 +++
 lib/scripts/require.js                             |   43 ++
 lib/test/exec.js                                   |    1 +
 lib/utils.js                                       |   94 ----
 lib/webworks/plugin/webworks/manager.js            |   14 +
 lib/wp7/exec.js                                    |   57 +++
 lib/wp7/platform.js                                |   16 +
 lib/wp7/plugin/wp7/device.js                       |   26 +
 test/runner.js                                     |   25 +-
 test/suite.html                                    |    8 +-
 203 files changed, 7150 insertions(+), 7077 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/Jakefile
----------------------------------------------------------------------
diff --git a/Jakefile b/Jakefile
index af05af6..f8cd07a 100644
--- a/Jakefile
+++ b/Jakefile
@@ -22,12 +22,13 @@ task('build', ['clean'], function () {
 
     var packager = require("./build/packager");
 
-    packager.write("blackberry");
-    packager.write("playbook");
-    packager.write("ios");
-    packager.write("wp7");
-    packager.write("android");
-    packager.write("errgen");
+    packager.generate("blackberry");
+    packager.generate("playbook");
+    packager.generate("ios");
+    packager.generate("wp7");
+    packager.generate("android");
+    packager.generate("errgen");
+    packager.generate("test");
 
 });
 

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index 5dd4e92..b8ca0dc 100644
--- a/README.md
+++ b/README.md
@@ -5,45 +5,57 @@ A unified JavaScript layer for [Apache Cordova](http://incubator.apache.org/proj
     cordova-js
       |
       |-build/
-      | Will contain any build modules (currently nothing here as it is all hacked
-      | into the JakeFile)
+      | Will contain any build modules (currently nothing here as it is all
+      | hacked into the JakeFile)
       |
       |-lib
-      |  |-bootstrap.js
-      |  | Code to bootstrap the Cordova platform, inject APIs and fire events
-      |  |
-      |  |-builder.js
-      |  | Injects in our classes onto window and navigator (or wherever else is needed)
-      |  |
-      |  |-channel.js
-      |  | A pub/sub implementation to handle custom framework events 
-      |  |
       |  |-cordova.js
       |  | Common Cordova stuff such as callback handling and
       |  | window/document add/removeEventListener hijacking 
       |  | 
-      |  |-require.js
-      |  | Our own module definition and require implementation. 
+      |  |-common/
+      |  | Contains the common-across-platforms base modules
+      |  |
+      |  |-common/builder.js
+      |  | Injects in our classes onto window and navigator (or wherever else 
+      |  | is needed)
+      |  |
+      |  |-common/channel.js
+      |  | A pub/sub implementation to handle custom framework events 
       |  |
-      |  |-utils.js
+      |  |-common/common.js
+      |  | Common locations to add Cordova objects to browser globals.
+      |  |
+      |  |-common/exec.js
+      |  | Stub for platform's specific version of exec.js
+      |  |
+      |  |-common/platform.js
+      |  | Stub for platform's specific version of platform.js
+      |  |
+      |  |-common/utils.js
       |  | General purpose JS utility stuff: closures, uuids, object
       |  | cloning, extending prototypes
       |  |
-      |  |-exec/
-      |  | Contains the platform specific definitions of the exec method
+      |  |-common/plugin
+      |  | Contains the common-across-platforms plugin modules
       |  |
-      |  |-platform/
-      |  | Definitions of each platform that help us describe where
-      |  | and what to put on the window object, and what to run to
-      |  | initialize the platform. A common set of globals are also
-      |  | defined (common.js)
+      |  |-scripts/
+      |  | Contains non-module JavaScript source that gets added to the
+      |  | resulting cordova.<platform>.js files closures, uuids, object
+      |  |
+      |  |-scripts/bootstrap.js
+      |  | Code to bootstrap the Cordova platform, inject APIs and fire events
       |  |
-      |  |-plugin/
-      |  |  | All API definitions as plugins, ones common to all
-      |  |  | platforms reside at the top level...
-      |  |  `-<platform>
-      |  |    ... and platform-specific ones reside in their respective
-      |  |    folders
+      |  |-scripts/require.js
+      |  | Our own module definition and require implementation. 
+      |  |
+      |  |-<platform>/
+      |  | Contains the platform-specific base modules.
+      |  |
+      |  |-<platform>/plugin
+      |  | Contains the platform-specific plugin modules.
+
+The way the resulting `cordova.<platform>.js` files will be built is by combining the scripts in the `lib/scripts` directory with modules from the `lib/common` and `lib/<platform>` directories.  For cases where there is the same named module in `lib/common` and `lib/<platform>`, the `lib/<platform>` version wins.  For instance, every `lib/<platform>` includes an `exec.js`, and there is also a version in `lib/common`, so the `lib/<platform>` version will always be used.  In fact, the `lib/common` one will throw errors, so if you build a new platform and forget `exec.js`, the resulting `cordova.<platform>.js` file will also throw errors.
 
 # Building
 
@@ -77,13 +89,13 @@ This will run the `build` and `test` tasks by default. All of the available task
 
 The `build/packager.js` tool is a node.js script that concatenates all of the core Cordova plugins in this repository into a `cordova.<platform>.js` file under the `pkg/` folder. It also wraps the plugins with a RequireJS-compatible module syntax that works in both browser and node environments. We end up with a cordova.js file that wraps each Cordova plugin into its own module.
 
-Cordova defines a `channel` module under `lib/channel.js`, which is a publish/subscribe implementation that the project uses for event management.
+Cordova defines a `channel` module under `lib/common/channel.js`, which is a publish/subscribe implementation that the project uses for event management.
 
-The Cordova native-to-webview bridge is initialized in `lib/bootstrap.js`. This file attaches the `boot` function to the `channel.onNativeReady` event - fired by native with a call to:
+The Cordova native-to-webview bridge is initialized in `lib/scripts/bootstrap.js`. This file attaches the `boot` function to the `channel.onNativeReady` event - fired by native with a call to:
 
     cordova.require('cordova/channel).onNativeReady.fire()
 
-The `boot` method does all the work.  First, it grabs the common platform definition (under `lib/platform/common.js`) and injects all of the objects defined there onto `window` and other global namespaces. Next, it grabs all of the platform-specific object definitions (as defined under `lib/platform/<platform>.js`) and overrides those onto `window`. Finally, it calls the platform-specific `initialize` function (located in the platform definition). At this point, Cordova is fully initialized and ready to roll. Last thing we do is wait for the `DOMContentLoaded` event to fire to make sure the page has loaded properly. Once that is done, Cordova fires the `deviceready` event where you can safely attach functions that consume the Cordova APIs.
+The `boot` method does all the work.  First, it grabs the common platform definition (under `lib/common/common.js`) and injects all of the objects defined there onto `window` and other global namespaces. Next, it grabs all of the platform-specific object definitions (as defined under `lib/<platform>/platform.js`) and overrides those onto `window`. Finally, it calls the platform-specific `initialize` function (located in the platform definition). At this point, Cordova is fully initialized and ready to roll. Last thing we do is wait for the `DOMContentLoaded` event to fire to make sure the page has loaded properly. Once that is done, Cordova fires the `deviceready` event where you can safely attach functions that consume the Cordova APIs.
 
 # Testing
 

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/build/packager.js
----------------------------------------------------------------------
diff --git a/build/packager.js b/build/packager.js
index 39a9704..6569020 100644
--- a/build/packager.js
+++ b/build/packager.js
@@ -1,135 +1,184 @@
-var util = require('util'),
-    debug = false,
-    fs = require('fs');
-
-// (Recursively) list contents of a directory
-function walk(dir, doRecursive) {
-    var results = [];
-    try {
-        var list = fs.readdirSync(dir);
-        for (var i = 0, l = list.length; i < l; i++) {
-            var file = list[i];
-            file = dir + '/' + file;
-            var stat = fs.statSync(file);
-            if (stat && doRecursive && stat.isDirectory()) {
-                results = results.concat(walk(file,doRecursive));
-            } else {
-                if (list[i] != ".DS_Store") {
-                    results.push(file);
-                }
-            }
-        }
-    } catch (e) {
-        //do nothing
-    }
-    return results;
-}
-
-// Simply inline includes the specified file(s) with an optional transform function.
-function include(files, transform) {
-    files = files.map ? files : [files];
-    return files.map(function (file) {
-        try {
-            var str = fs.readFileSync(file, "utf-8") + "\n";
-            str = transform ? transform(str, file) : str;
-            str = debug ? "try {" + str + "} catch (e) { alert('" + file + ":' + e);}" : str;
-            return str;
-        } catch (e) {
-            //do nothing
-        }
-    }).join('\n');
+var fs    = require('fs')
+var util  = require('util')
+var path  = require('path')
+
+var packager = module.exports
+
+//------------------------------------------------------------------------------
+packager.generate = function(platform) {
+    var time = new Date().valueOf()
+    
+    var libraryRelease = packager.bundle(platform, false)
+    var libraryDebug   = packager.bundle(platform, true)
+    
+    time = new Date().valueOf() - time
+    
+    var outFile
+    
+    outFile = path.join('pkg', 'cordova.' + platform + '.js')
+    fs.writeFileSync(outFile, libraryRelease, 'utf8')
+    
+    outFile = path.join('pkg', 'cordova.' + platform + '-debug.js')
+    fs.writeFileSync(outFile, libraryDebug, 'utf8')
+    
+    console.log('generated platform: ' + platform + ' in ' + time + 'ms')
 }
 
-// Includes the specified file(s) with optional overriding id
-// Wraps the specified file(s) in a define statement which implicitly
-// creates a closure as well.
-function drop(files, id) {
-    return include(files, function(file, path) {
-        var define_id = (typeof id != 'undefined' && id.length > 0 ? id : path.replace(/lib\//, "cordova/").replace(/\.js$/, ''));
-        return "define('" + define_id + "', function(require, exports, module) {\n" + file + "});\n";
-    });
-}
-
-module.exports = {
-    modules: function (platform) {
-        var baseFiles = [
-                "lib/utils.js",
-                "lib/builder.js"
-            ],
-            platformFiles = walk('lib/plugin/' + platform, true),
-            output = "";
-
-        //HACK: ummm .... we really need to figure out this webworks common file stuff
-        if (platform === "blackberry" || platform === "playbook") {
-            platformFiles = platformFiles.concat(walk('lib/plugin/webworks', true));
-        }
-
-        //include all common platform files that are under lib/plugin
-        baseFiles = baseFiles.concat(walk('lib/plugin'));
-
-        //include require
-        output += include("lib/require.js");
-
-        //include channel
-        output += drop('lib/channel.js', 'cordova/channel');
+//------------------------------------------------------------------------------
+packager.bundle = function(platform, debug) {
+    var modules = collectFiles('lib/common')
+    var scripts = collectFiles('lib/scripts')
+    
+    modules[''] = 'lib/cordova.js'
+    
+    if (['playbook', 'blackberry'].indexOf(platform) > -1) {
+        copyProps(modules, collectFiles(path.join('lib', 'webworks')))
+    }
+    
+    copyProps(modules, collectFiles(path.join('lib', platform)))
+
+    var output = []
+
+    // write header     
+    output.push('/*\n' + getContents('LICENSE-for-js-file.txt') + '\n*/')
+    output.push('\n;(function() {\n')
+    
+    // write initial scripts
+    if (!scripts['require']) {
+        throw new Error("didn't find a script for 'require'")
+    }
+    
+    writeScript(output, scripts['require'], debug)
+
+    // write modules
+    var moduleIds = Object.keys(modules)
+    moduleIds.sort()
+    
+    for (var i=0; i<moduleIds.length; i++) {
+        var moduleId = moduleIds[i]
+        
+        writeModule(output, modules[moduleId], moduleId, debug)
+    }
 
-        //include cordova
-        output += drop('lib/cordova.js', 'cordova');
+    output.push("\nwindow.cordova = require('cordova');\n")
 
-        //include exec
-        output += drop('lib/exec/' + platform + '.js', 'cordova/exec');
+    // write final scripts
+    if (!scripts['bootstrap']) {
+        throw new Error("didn't find a script for 'bootstrap'")
+    }
+    
+    writeScript(output, scripts['bootstrap'], debug)
+    
+    var bootstrapPlatform = 'bootstrap-' + platform
+    if (scripts[bootstrapPlatform]) {
+        writeScript(output, scripts[bootstrapPlatform], debug)
+    }
 
-        //include common platform defn 
-        output += drop('lib/platform/common.js', 'cordova/common');
+    // write trailer
+    output.push('\n})();')
 
-        //include platform defn
-        output += drop('lib/platform/' + platform + '.js', 'cordova/platform');
+    return output.join('\n')
+}
 
-        //include common modules
-        output += drop(baseFiles);
+//------------------------------------------------------------------------------
+var CollectedFiles = {}
 
-        //include platform specific modules
-        output += drop(platformFiles);
+function collectFiles(dir, id) {
+    if (!id) id = ''
+    
+    if (CollectedFiles[dir]) {
+        return copyProps({}, CollectedFiles[dir])
+    }
 
-        return output;
-    },
+    var result = {}    
+    
+    var entries = fs.readdirSync(dir)
+    
+    entries = entries.filter(function(entry) {
+        if (entry.match(/\.js$/)) return true
+        
+        var stat = fs.statSync(path.join(dir, entry))
+        if (stat.isDirectory())  return true
+    })
 
-    bundle: function (platform) {
-        console.log("building platform: " + platform);
+    entries.forEach(function(entry) {
+        var moduleId = path.join(id,  entry)
+        var fileName = path.join(dir, entry)
         
-        var output = "";
+        var stat = fs.statSync(fileName)
+        if (stat.isDirectory()) {
+            copyProps(result, collectFiles(fileName, moduleId))
+        }
+        else {
+            moduleId         = getModuleId(moduleId)
+            result[moduleId] = fileName
+        }
+    })
+    
+    CollectedFiles[dir] = result
+    
+    return copyProps({}, result)
+}
 
-        //include LICENSE
-        output += include("LICENSE-for-js-file.txt", function (file) {
-            return "/*\n" + file + "*/\n";
-        });
+//------------------------------------------------------------------------------
+function writeScript(oFile, fileName, debug) {
+    var contents = getContents(fileName, 'utf8')
+    
+    writeContents(oFile, fileName, contents, debug)    
+}
 
-        // wrap the entire thing in one more closure
-        // closure closure closure
-        output += "(function() {\n";
+//------------------------------------------------------------------------------
+function writeModule(oFile, fileName, moduleId, debug) {
+    var contents = '\n' + getContents(fileName, 'utf8') + '\n'
+    
+    moduleId = path.join('cordova', moduleId)
+    
+    var signature = 'function(require, exports, module)'
+    
+    contents = 'define("' + moduleId + '", ' + signature + ' {' + contents + '})\n'
+
+    writeContents(oFile, fileName, contents, debug)    
+}
 
-        //include modules
-        output += this.modules(platform);
+//------------------------------------------------------------------------------
+var FileContents = {}
 
-        // HACK: this gets done in bootstrap.js anyways, once native side is ready + domcontentloaded is fired.
-        // TODO: Do we need it?
-        output += "window.cordova = require('cordova');\n"; 
+function getContents(file) {
+    if (!FileContents.hasOwnProperty(file)) {
+        FileContents[file] = fs.readFileSync(file, 'utf8')
+    }
+    
+    return FileContents[file]
+}
 
-        //include bootstrap
-        output += include('lib/bootstrap.js');
+//------------------------------------------------------------------------------
+function writeContents(oFile, fileName, contents, debug) {
+    
+    if (debug) {
+        contents += '\n//@ sourceURL=' + fileName
+        
+        contents = 'eval(' + JSON.stringify(contents) + ')'
+    }
+    
+    else {
+        contents = '// file: ' + fileName + '\n' + contents    
+    }
 
-        // TODO/HACK: we don't need platform-specific bootstrap.
-        // those can go into the init function inside the platform/*.js
-        // files
-        output += include('lib/bootstrap/' + platform + '.js');
+    oFile.push(contents)
+}
 
-        // closing the closure har har
-        output += "})();";
+//------------------------------------------------------------------------------
+function getModuleId(fileName) {
+    return fileName.match(/(.*)\.js$/)[1]
+}
 
-        return output;
-    },
-    write: function (platform) {
-        var output = this.bundle(platform);
-        fs.writeFileSync(__dirname + "/../pkg/cordova." + platform + ".js", output);
+//------------------------------------------------------------------------------
+function copyProps(target, source) {
+    for (var key in source) {
+        if (!source.hasOwnProperty(key)) continue
+        
+        target[key] = source[key]
     }
-};
+    
+    return target
+}

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/exec.js
----------------------------------------------------------------------
diff --git a/lib/android/exec.js b/lib/android/exec.js
new file mode 100644
index 0000000..b3410bb
--- /dev/null
+++ b/lib/android/exec.js
@@ -0,0 +1,82 @@
+/**
+ * Execute a cordova command.  It is up to the native side whether this action
+ * is synchronous or asynchronous.  The native side can return:
+ *      Synchronous: PluginResult object as a JSON string
+ *      Asynchrounous: Empty string ""
+ * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
+ * depending upon the result of the action.
+ *
+ * @param {Function} success    The success callback
+ * @param {Function} fail       The fail callback
+ * @param {String} service      The name of the service to use
+ * @param {String} action       Action to be run in cordova
+ * @param {String[]} [args]     Zero or more arguments to pass to the method
+ */
+var cordova = require('cordova');
+
+module.exports = function(success, fail, service, action, args) {
+  try {
+    var callbackId = service + cordova.callbackId++;
+    if (success || fail) {
+        cordova.callbacks[callbackId] = {success:success, fail:fail};
+    }
+
+    var r = prompt(JSON.stringify(args), "gap:"+JSON.stringify([service, action, callbackId, true]));
+
+    // If a result was returned
+    if (r.length > 0) {
+        eval("var v="+r+";");
+
+        // If status is OK, then return value back to caller
+        if (v.status === cordova.callbackStatus.OK) {
+
+            // If there is a success callback, then call it now with
+            // returned value
+            if (success) {
+                try {
+                    success(v.message);
+                } catch (e) {
+                    console.log("Error in success callback: " + callbackId  + " = " + e);
+                }
+
+                // Clear callback if not expecting any more results
+                if (!v.keepCallback) {
+                    delete cordova.callbacks[callbackId];
+                }
+            }
+            return v.message;
+        }
+
+        // If no result
+        else if (v.status === cordova.callbackStatus.NO_RESULT) {
+            // Clear callback if not expecting any more results
+            if (!v.keepCallback) {
+                delete cordova.callbacks[callbackId];
+            }
+        }
+
+        // If error, then display error
+        else {
+            console.log("Error: Status="+v.status+" Message="+v.message);
+
+            // If there is a fail callback, then call it now with returned value
+            if (fail) {
+                try {
+                    fail(v.message);
+                }
+                catch (e1) {
+                    console.log("Error in error callback: "+callbackId+" = "+e1);
+                }
+
+                // Clear callback if not expecting any more results
+                if (!v.keepCallback) {
+                    delete cordova.callbacks[callbackId];
+                }
+            }
+            return null;
+        }
+    }
+  } catch (e2) {
+    console.log("Error: "+e2);
+  }
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/platform.js
----------------------------------------------------------------------
diff --git a/lib/android/platform.js b/lib/android/platform.js
new file mode 100644
index 0000000..6c71820
--- /dev/null
+++ b/lib/android/platform.js
@@ -0,0 +1,131 @@
+module.exports = {
+    id: "android",
+    initialize:function() {
+        var channel = require("cordova/channel"),
+            cordova = require('cordova'),
+            callback = require('cordova/plugin/android/callback'),
+            polling = require('cordova/plugin/android/polling'),
+            exec = require('cordova/exec');
+
+        channel.onDestroy.subscribe(function() {
+            cordova.shuttingDown = true;
+        });
+
+        // Start listening for XHR callbacks
+        // Figure out which bridge approach will work on this Android
+        // device: polling or XHR-based callbacks
+        setTimeout(function() {
+            if (cordova.UsePolling) {
+                polling();
+            }
+            else {
+                var isPolling = prompt("usePolling", "gap_callbackServer:");
+                cordova.UsePolling = isPolling;
+                if (isPolling == "true") {
+                    cordova.UsePolling = true;
+                    polling();
+                } else {
+                    cordova.UsePolling = false;
+                    callback();
+                }
+            }
+        }, 1);
+
+        // Inject a listener for the backbutton on the document.
+        var backButtonChannel = cordova.addDocumentEventHandler('backbutton', {
+            onSubscribe:function() {
+                // If we just attached the first handler, let native know we need to override the back button.
+                if (this.numHandlers === 1) {
+                    exec(null, null, "App", "overrideBackbutton", [true]);
+                }
+            },
+            onUnsubscribe:function() {
+                // If we just detached the last handler, let native know we no longer override the back button.
+                if (this.numHandlers === 0) {
+                    exec(null, null, "App", "overrideBackbutton", [false]);
+                }
+            }
+        });
+
+        // Add hardware MENU and SEARCH button handlers
+        cordova.addDocumentEventHandler('menubutton');
+        cordova.addDocumentEventHandler('searchbutton');
+
+        // Figure out if we need to shim-in localStorage and WebSQL
+        // support from the native side.
+        var storage = require('cordova/plugin/android/storage');
+
+        // First patch WebSQL if necessary
+        if (typeof window.openDatabase == 'undefined') {
+            // Not defined, create an openDatabase function for all to use!
+            window.openDatabase = storage.openDatabase;
+        } else {
+            // Defined, but some Android devices will throw a SECURITY_ERR -
+            // so we wrap the whole thing in a try-catch and shim in our own
+            // if the device has Android bug 16175.
+            var originalOpenDatabase = window.openDatabase;
+            window.openDatabase = function(name, version, desc, size) {
+                var db = null;
+                try {
+                    db = originalOpenDatabase(name, version, desc, size);
+                } 
+                catch (ex) {
+                    db = null;
+                }
+
+                if (db === null) {
+                    return storage.openDatabase(name, version, desc, size);
+                }
+                else {
+                    return db;
+                }
+              
+            };
+        }
+
+        // Patch localStorage if necessary
+        if (typeof window.localStorage == 'undefined' || window.localStorage === null) {
+            window.localStorage = new storage.CupCakeLocalStorage();
+        }
+
+        // Let native code know we are all done on the JS side.
+        // Native code will then un-hide the WebView.
+        channel.join(function() {
+            prompt("", "gap_init:");
+        }, [channel.onCordovaReady]);
+    },
+    objects: {
+        cordova: {
+            children: {
+                JSCallback:{
+                    path:"cordova/plugin/android/callback"
+                },
+                JSCallbackPolling:{
+                    path:"cordova/plugin/android/polling"
+                }
+            }
+        },
+        navigator: {
+            children: {
+                app:{
+                    path: "cordova/plugin/android/app"
+                }
+            }
+        },
+        device:{
+            path: "cordova/plugin/android/device"
+        },
+        File: { // exists natively on Android WebView, override
+            path: "cordova/plugin/File"
+        },
+        FileReader: { // exists natively on Android WebView, override
+            path: "cordova/plugin/FileReader"
+        },
+        FileError: { //exists natively on Android WebView on Android 4.x
+            path: "cordova/plugin/FileError"
+        },
+        MediaError: { // exists natively on Android WebView on Android 4.x
+            path: "cordova/plugin/MediaError"
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/plugin/android/app.js
----------------------------------------------------------------------
diff --git a/lib/android/plugin/android/app.js b/lib/android/plugin/android/app.js
new file mode 100644
index 0000000..f858829
--- /dev/null
+++ b/lib/android/plugin/android/app.js
@@ -0,0 +1,71 @@
+var exec = require('cordova/exec');
+
+module.exports = {
+  /**
+   * Clear the resource cache.
+   */
+  clearCache:function() {
+    exec(null, null, "App", "clearCache", []);
+  },
+
+  /**
+   * Load the url into the webview or into new browser instance.
+   *
+   * @param url           The URL to load
+   * @param props         Properties that can be passed in to the activity:
+   *      wait: int                           => wait msec before loading URL
+   *      loadingDialog: "Title,Message"      => display a native loading dialog
+   *      loadUrlTimeoutValue: int            => time in msec to wait before triggering a timeout error
+   *      clearHistory: boolean              => clear webview history (default=false)
+   *      openExternal: boolean              => open in a new browser (default=false)
+   *
+   * Example:
+   *      navigator.app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
+   */
+  loadUrl:function(url, props) {
+    exec(null, null, "App", "loadUrl", [url, props]);
+  },
+
+  /**
+   * Cancel loadUrl that is waiting to be loaded.
+   */
+  cancelLoadUrl:function() {
+    exec(null, null, "App", "cancelLoadUrl", []);
+  },
+
+  /**
+   * Clear web history in this web view.
+   * Instead of BACK button loading the previous web page, it will exit the app.
+   */
+  clearHistory:function() {
+    exec(null, null, "App", "clearHistory", []);
+  },
+
+  /**
+   * Go to previous page displayed.
+   * This is the same as pressing the backbutton on Android device.
+   */
+  backHistory:function() {
+    exec(null, null, "App", "backHistory", []);
+  },
+
+  /**
+   * Override the default behavior of the Android back button.
+   * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
+   *
+   * Note: The user should not have to call this method.  Instead, when the user
+   *       registers for the "backbutton" event, this is automatically done.
+   *
+   * @param override		T=override, F=cancel override
+   */
+  overrideBackbutton:function(override) {
+    exec(null, null, "App", "overrideBackbutton", [override]);
+  },
+
+  /**
+   * Exit and terminate the application.
+   */
+  exitApp:function() {
+    return exec(null, null, "App", "exitApp", []);
+  } 
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/plugin/android/callback.js
----------------------------------------------------------------------
diff --git a/lib/android/plugin/android/callback.js b/lib/android/plugin/android/callback.js
new file mode 100644
index 0000000..d5768a7
--- /dev/null
+++ b/lib/android/plugin/android/callback.js
@@ -0,0 +1,85 @@
+var port = null,
+    token = null,
+    cordova = require('cordova'),
+    polling = require('cordova/plugin/android/polling'),
+    callback = function() {
+      // Exit if shutting down app
+      if (cordova.shuttingDown) {
+          return;
+      }
+
+      // If polling flag was changed, start using polling from now on
+      if (cordova.UsePolling) {
+          polling();
+          return;
+      }
+
+      var xmlhttp = new XMLHttpRequest();
+
+      // Callback function when XMLHttpRequest is ready
+      xmlhttp.onreadystatechange=function(){
+          if(xmlhttp.readyState === 4){
+
+              // Exit if shutting down app
+              if (cordova.shuttingDown) {
+                  return;
+              }
+
+              // If callback has JavaScript statement to execute
+              if (xmlhttp.status === 200) {
+
+                  // Need to url decode the response
+                  var msg = decodeURIComponent(xmlhttp.responseText);
+                  setTimeout(function() {
+                      try {
+                          var t = eval(msg);
+                      }
+                      catch (e) {
+                          // If we're getting an error here, seeing the message will help in debugging
+                          console.log("JSCallback: Message from Server: " + msg);
+                          console.log("JSCallback Error: "+e);
+                      }
+                  }, 1);
+                  setTimeout(callback, 1);
+              }
+
+              // If callback ping (used to keep XHR request from timing out)
+              else if (xmlhttp.status === 404) {
+                  setTimeout(callback, 10);
+              }
+
+              // If security error
+              else if (xmlhttp.status === 403) {
+                  console.log("JSCallback Error: Invalid token.  Stopping callbacks.");
+              }
+
+              // If server is stopping
+              else if (xmlhttp.status === 503) {
+                  console.log("JSCallback Server Closed: Stopping callbacks.");
+              }
+
+              // If request wasn't GET
+              else if (xmlhttp.status === 400) {
+                  console.log("JSCallback Error: Bad request.  Stopping callbacks.");
+              }
+
+              // If error, revert to polling
+              else {
+                  console.log("JSCallback Error: Request failed.");
+                  cordova.UsePolling = true;
+                  polling();
+              }
+          }
+      };
+
+      if (port === null) {
+          port = prompt("getPort", "gap_callbackServer:");
+      }
+      if (token === null) {
+          token = prompt("getToken", "gap_callbackServer:");
+      }
+      xmlhttp.open("GET", "http://127.0.0.1:"+port+"/"+token , true);
+      xmlhttp.send();
+};
+
+module.exports = callback;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/plugin/android/device.js
----------------------------------------------------------------------
diff --git a/lib/android/plugin/android/device.js b/lib/android/plugin/android/device.js
new file mode 100644
index 0000000..1defbbe
--- /dev/null
+++ b/lib/android/plugin/android/device.js
@@ -0,0 +1,92 @@
+var channel = require('cordova/channel'),
+    exec = require('cordova/exec');
+
+/**
+ * This represents the mobile device, and provides properties for inspecting the model, version, UUID of the
+ * phone, etc.
+ * @constructor
+ */
+function Device() {
+    this.available = false;
+    this.platform = null;
+    this.version = null;
+    this.name = null;
+    this.uuid = null;
+    this.cordova = null;
+
+    var me = this;
+    this.getInfo(
+        function(info) {
+            me.available = true;
+            me.platform = info.platform;
+            me.version = info.version;
+            me.name = info.name;
+            me.uuid = info.uuid;
+            me.cordova = info.cordova;
+            channel.onCordovaInfoReady.fire();
+        },
+        function(e) {
+            me.available = false;
+            console.log("Error initializing Cordova: " + e);
+            alert("Error initializing Cordova: "+e);
+        });
+}
+
+/**
+ * Get device info
+ *
+ * @param {Function} successCallback The function to call when the heading data is available
+ * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL)
+ */
+Device.prototype.getInfo = function(successCallback, errorCallback) {
+
+    // successCallback required
+    if (typeof successCallback !== "function") {
+        console.log("Device Error: successCallback is not a function");
+        return;
+    }
+
+    // errorCallback optional
+    if (errorCallback && (typeof errorCallback !== "function")) {
+        console.log("Device Error: errorCallback is not a function");
+        return;
+    }
+
+    // Get info
+    exec(successCallback, errorCallback, "Device", "getDeviceInfo", []);
+};
+
+/*
+ * DEPRECATED
+ * This is only for Android.
+ *
+ * You must explicitly override the back button.
+ */
+Device.prototype.overrideBackButton = function() {
+	console.log("Device.overrideBackButton() is deprecated.  Use App.overrideBackbutton(true).");
+	navigator.app.overrideBackbutton(true);
+};
+
+/*
+ * DEPRECATED
+ * This is only for Android.
+ *
+ * This resets the back button to the default behaviour
+ */
+Device.prototype.resetBackButton = function() {
+	console.log("Device.resetBackButton() is deprecated.  Use App.overrideBackbutton(false).");
+	navigator.app.overrideBackbutton(false);
+};
+
+/*
+ * DEPRECATED
+ * This is only for Android.
+ *
+ * This terminates the activity!
+ */
+Device.prototype.exitApp = function() {
+	console.log("Device.exitApp() is deprecated.  Use App.exitApp().");
+	navigator.app.exitApp();
+};
+
+module.exports = new Device();

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/plugin/android/polling.js
----------------------------------------------------------------------
diff --git a/lib/android/plugin/android/polling.js b/lib/android/plugin/android/polling.js
new file mode 100644
index 0000000..36b8e86
--- /dev/null
+++ b/lib/android/plugin/android/polling.js
@@ -0,0 +1,33 @@
+var cordova = require('cordova'),
+    period = 50,
+    polling = function() {
+      // Exit if shutting down app
+      if (cordova.shuttingDown) {
+          return;
+      }
+
+      // If polling flag was changed, stop using polling from now on and switch to XHR server / callback
+      if (!cordova.UsePolling) {
+          require('cordova/plugin/android/callback')();
+          return;
+      }
+
+      var msg = prompt("", "gap_poll:");
+      if (msg) {
+          setTimeout(function() {
+              try {
+                  var t = eval(""+msg);
+              }
+              catch (e) {
+                  console.log("JSCallbackPolling: Message from Server: " + msg);
+                  console.log("JSCallbackPolling Error: "+e);
+              }
+          }, 1);
+          setTimeout(polling, 1);
+      }
+      else {
+          setTimeout(polling, period);
+      }
+};
+
+module.exports = polling;

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/android/plugin/android/storage.js
----------------------------------------------------------------------
diff --git a/lib/android/plugin/android/storage.js b/lib/android/plugin/android/storage.js
new file mode 100644
index 0000000..000612b
--- /dev/null
+++ b/lib/android/plugin/android/storage.js
@@ -0,0 +1,377 @@
+var utils = require('cordova/utils'),
+    exec = require('cordova/exec');
+    channel = require('cordova/channel');
+
+var queryQueue = {};
+
+/**
+ * SQL result set object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Rows = function() {
+    this.resultSet = [];    // results array
+    this.length = 0;        // number of rows
+};
+
+/**
+ * Get item from SQL result set
+ *
+ * @param row           The row number to return
+ * @return              The row object
+ */
+DroidDB_Rows.prototype.item = function(row) {
+    return this.resultSet[row];
+};
+
+/**
+ * SQL result set that is returned to user.
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Result = function() {
+    this.rows = new DroidDB_Rows();
+};
+
+/**
+ * Callback from native code when query is complete.
+ * PRIVATE METHOD
+ *
+ * @param id   Query id
+ */
+function completeQuery(id, data) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+
+                // Save query results
+                var r = new DroidDB_Result();
+                r.rows.resultSet = data;
+                r.rows.length = data.length;
+                try {
+                    if (typeof query.successCallback === 'function') {
+                        query.successCallback(query.tx, r);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user success callback: "+ex);
+                }
+
+                tx.queryComplete(id);
+            }
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * Callback from native code when query fails
+ * PRIVATE METHOD
+ *
+ * @param reason            Error message
+ * @param id                Query id
+ */
+function failQuery(reason, id) {
+    var query = queryQueue[id];
+    if (query) {
+        try {
+            delete queryQueue[id];
+
+            // Get transaction
+            var tx = query.tx;
+
+            // If transaction hasn't failed
+            // Note: We ignore all query results if previous query
+            //       in the same transaction failed.
+            if (tx && tx.queryList[id]) {
+                tx.queryList = {};
+
+                try {
+                    if (typeof query.errorCallback === 'function') {
+                        query.errorCallback(query.tx, reason);
+                    }
+                } catch (ex) {
+                    console.log("executeSql error calling user error callback: "+ex);
+                }
+
+                tx.queryFailed(id, reason);
+            }
+
+        } catch (e) {
+            console.log("executeSql error: "+e);
+        }
+    }
+}
+
+/**
+ * SQL query object
+ * PRIVATE METHOD
+ *
+ * @constructor
+ * @param tx                The transaction object that this query belongs to
+ */
+var DroidDB_Query = function(tx) {
+
+    // Set the id of the query
+    this.id = utils.createUUID();
+
+    // Add this query to the queue
+    queryQueue[this.id] = this;
+
+    // Init result
+    this.resultSet = [];
+
+    // Set transaction that this query belongs to
+    this.tx = tx;
+
+    // Add this query to transaction list
+    this.tx.queryList[this.id] = this;
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+};
+
+/**
+ * Transaction object
+ * PRIVATE METHOD
+ * @constructor
+ */
+var DroidDB_Tx = function() {
+
+    // Set the id of the transaction
+    this.id = utils.createUUID();
+
+    // Callbacks
+    this.successCallback = null;
+    this.errorCallback = null;
+
+    // Query list
+    this.queryList = {};
+};
+
+/**
+ * Mark query in transaction as complete.
+ * If all queries are complete, call the user's transaction success callback.
+ *
+ * @param id                Query id
+ */
+DroidDB_Tx.prototype.queryComplete = function(id) {
+    delete this.queryList[id];
+
+    // If no more outstanding queries, then fire transaction success
+    if (this.successCallback) {
+        var count = 0;
+        var i;
+        for (i in this.queryList) {
+            if (this.queryList.hasOwnProperty(i)) {
+                count++;
+            }
+        }
+        if (count === 0) {
+            try {
+                this.successCallback();
+            } catch(e) {
+                console.log("Transaction error calling user success callback: " + e);
+            }
+        }
+    }
+};
+
+/**
+ * Mark query in transaction as failed.
+ *
+ * @param id                Query id
+ * @param reason            Error message
+ */
+DroidDB_Tx.prototype.queryFailed = function(id, reason) {
+
+    // The sql queries in this transaction have already been run, since
+    // we really don't have a real transaction implemented in native code.
+    // However, the user callbacks for the remaining sql queries in transaction
+    // will not be called.
+    this.queryList = {};
+
+    if (this.errorCallback) {
+        try {
+            this.errorCallback(reason);
+        } catch(e) {
+            console.log("Transaction error calling user error callback: " + e);
+        }
+    }
+};
+
+/**
+ * Execute SQL statement
+ *
+ * @param sql                   SQL statement to execute
+ * @param params                Statement parameters
+ * @param successCallback       Success callback
+ * @param errorCallback         Error callback
+ */
+DroidDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCallback) {
+
+    // Init params array
+    if (typeof params === 'undefined') {
+        params = [];
+    }
+
+    // Create query and add to queue
+    var query = new DroidDB_Query(this);
+    queryQueue[query.id] = query;
+
+    // Save callbacks
+    query.successCallback = successCallback;
+    query.errorCallback = errorCallback;
+
+    // Call native code
+    exec(null, null, "Storage", "executeSql", [sql, params, query.id]);
+};
+
+var DatabaseShell = function() {
+};
+
+/**
+ * Start a transaction.
+ * Does not support rollback in event of failure.
+ *
+ * @param process {Function}            The transaction function
+ * @param successCallback {Function}
+ * @param errorCallback {Function}
+ */
+DatabaseShell.prototype.transaction = function(process, errorCallback, successCallback) {
+    var tx = new DroidDB_Tx();
+    tx.successCallback = successCallback;
+    tx.errorCallback = errorCallback;
+    try {
+        process(tx);
+    } catch (e) {
+        console.log("Transaction error: "+e);
+        if (tx.errorCallback) {
+            try {
+                tx.errorCallback(e);
+            } catch (ex) {
+                console.log("Transaction error calling user error callback: "+e);
+            }
+        }
+    }
+};
+
+/**
+ * Open database
+ *
+ * @param name              Database name
+ * @param version           Database version
+ * @param display_name      Database display name
+ * @param size              Database size in bytes
+ * @return                  Database object
+ */
+var DroidDB_openDatabase = function(name, version, display_name, size) {
+    exec(null, null, "Storage", "openDatabase", [name, version, display_name, size]);
+    var db = new DatabaseShell();
+    return db;
+};
+
+/**
+ * For browsers with no localStorage we emulate it with SQLite. Follows the w3c api.
+ * TODO: Do similar for sessionStorage.
+ * @constructor
+ */
+var CupcakeLocalStorage = function() {
+    channel.waitForInitialization("cupcakeStorage");
+
+    try {
+
+      this.db = openDatabase('localStorage', '1.0', 'localStorage', 2621440);
+      var storage = {};
+      this.length = 0;
+      function setLength (length) {
+        this.length = length;
+        localStorage.length = length;
+      }
+      this.db.transaction(
+        function (transaction) {
+            var i;
+          transaction.executeSql('CREATE TABLE IF NOT EXISTS storage (id NVARCHAR(40) PRIMARY KEY, body NVARCHAR(255))');
+          transaction.executeSql('SELECT * FROM storage', [], function(tx, result) {
+            for(var i = 0; i < result.rows.length; i++) {
+              storage[result.rows.item(i)['id']] =  result.rows.item(i)['body'];
+            }
+            setLength(result.rows.length);
+            channel.initializationComplete("cupcakeStorage");
+          });
+
+        },
+        function (err) {
+          alert(err.message);
+        }
+      );
+      this.setItem = function(key, val) {
+        if (typeof(storage[key])=='undefined') {
+          this.length++;
+        }
+        storage[key] = val;
+        this.db.transaction(
+          function (transaction) {
+            transaction.executeSql('CREATE TABLE IF NOT EXISTS storage (id NVARCHAR(40) PRIMARY KEY, body NVARCHAR(255))');
+            transaction.executeSql('REPLACE INTO storage (id, body) values(?,?)', [key,val]);
+          }
+        );
+      };
+      this.getItem = function(key) {
+        return storage[key];
+      };
+      this.removeItem = function(key) {
+        delete storage[key];
+        this.length--;
+        this.db.transaction(
+          function (transaction) {
+            transaction.executeSql('CREATE TABLE IF NOT EXISTS storage (id NVARCHAR(40) PRIMARY KEY, body NVARCHAR(255))');
+            transaction.executeSql('DELETE FROM storage where id=?', [key]);
+          }
+        );
+      };
+      this.clear = function() {
+        storage = {};
+        this.length = 0;
+        this.db.transaction(
+          function (transaction) {
+            transaction.executeSql('CREATE TABLE IF NOT EXISTS storage (id NVARCHAR(40) PRIMARY KEY, body NVARCHAR(255))');
+            transaction.executeSql('DELETE FROM storage', []);
+          }
+        );
+      };
+      this.key = function(index) {
+        var i = 0;
+        for (var j in storage) {
+          if (i==index) {
+            return j;
+          } else {
+            i++;
+          }
+        }
+        return null;
+      };
+
+    } catch(e) {
+      alert("Database error "+e+".");
+        return;
+    }
+};
+
+module.exports = {
+  openDatabase:DroidDB_openDatabase,
+  CupcakeLocalStorage:CupcakeLocalStorage,
+  failQuery:failQuery,
+  completeQuery:completeQuery
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/blackberry/exec.js
----------------------------------------------------------------------
diff --git a/lib/blackberry/exec.js b/lib/blackberry/exec.js
new file mode 100644
index 0000000..a5fb81f
--- /dev/null
+++ b/lib/blackberry/exec.js
@@ -0,0 +1,56 @@
+/**
+ * Execute a cordova command.  It is up to the native side whether this action
+ * is synchronous or asynchronous.  The native side can return:
+ *      Synchronous: PluginResult object as a JSON string
+ *      Asynchrounous: Empty string ""
+ * If async, the native side will cordova.callbackSuccess or cordova.callbackError,
+ * depending upon the result of the action.
+ *
+ * @param {Function} success    The success callback
+ * @param {Function} fail       The fail callback
+ * @param {String} service      The name of the service to use
+ * @param {String} action       Action to be run in cordova
+ * @param {String[]} [args]     Zero or more arguments to pass to the method
+ */
+var blackberry = require('cordova/plugin/blackberry/manager'),
+    cordova = require('cordova');
+
+module.exports = function(success, fail, service, action, args) {
+    try {
+        var v = blackberry.exec(success, fail, service, action, args);
+
+        // If status is OK, then return value back to caller
+        if (v.status == cordova.callbackStatus.OK) {
+
+            // If there is a success callback, then call it now with returned value
+            if (success) {
+                try {
+                    success(v.message);
+                }
+                catch (e) {
+                    console.log("Error in success callback: "+ service + "." + action + " = "+e);
+                }
+
+            }
+            return v.message;
+        } else if (v.status == cordova.callbackStatus.NO_RESULT) {
+
+        } else {
+            // If error, then display error
+            console.log("Error: " + service + "." + action + " Status="+v.status+" Message="+v.message);
+
+            // If there is a fail callback, then call it now with returned value
+            if (fail) {
+                try {
+                    fail(v.message);
+                }
+                catch (e) {
+                    console.log("Error in error callback: " + service + "." + action + " = "+e);
+                }
+            }
+            return null;
+        }
+    } catch (e) {
+        alert("Error: "+e);
+    }
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/blackberry/platform.js
----------------------------------------------------------------------
diff --git a/lib/blackberry/platform.js b/lib/blackberry/platform.js
new file mode 100644
index 0000000..d98f0e6
--- /dev/null
+++ b/lib/blackberry/platform.js
@@ -0,0 +1,179 @@
+module.exports = {
+    id: "blackberry",
+    initialize:function() {
+        var cordova = require('cordova'),
+            exec = require('cordova/exec'),
+            channel = require('cordova/channel'),
+            blackberryManager = require('cordova/plugin/blackberry/manager'),
+            app = require('cordova/plugin/blackberry/app');
+
+        // BB OS 5 does not define window.console.
+        if (typeof window.console === 'undefined') {
+            window.console = {};
+        }
+
+        // Override console.log with native logging ability.
+        // BB OS 7 devices define console.log for use with web inspector
+        // debugging. If console.log is already defined, invoke it in addition
+        // to native logging.
+        var origLog = window.console.log;
+        window.console.log = function(msg) {
+            if (typeof origLog === 'function') {
+                origLog.call(window.console, msg);
+            }
+            org.apache.cordova.Logger.log(''+msg);
+        };
+
+        // Mapping of button events to BlackBerry key identifier.
+        var buttonMapping = {
+            'backbutton'         : blackberry.system.event.KEY_BACK,
+            'conveniencebutton1' : blackberry.system.event.KEY_CONVENIENCE_1,
+            'conveniencebutton2' : blackberry.system.event.KEY_CONVENIENCE_2,
+            'endcallbutton'      : blackberry.system.event.KEY_ENDCALL,
+            'menubutton'         : blackberry.system.event.KEY_MENU,
+            'startcallbutton'    : blackberry.system.event.KEY_STARTCALL,
+            'volumedownbutton'   : blackberry.system.event.KEY_VOLUMEDOWN,
+            'volumeupbutton'     : blackberry.system.event.KEY_VOLUMEUP
+        };
+
+        // Generates a function which fires the specified event.
+        var fireEvent = function(event) {
+            return function() {
+                cordova.fireDocumentEvent(event, null);
+            };
+        };
+
+        var eventHandler = function(event) {
+            return { onSubscribe : function() {
+                // If we just attached the first handler, let native know we
+                // need to override the back button.
+                if (this.numHandlers === 1) {
+                    blackberry.system.event.onHardwareKey(
+                            buttonMapping[event], fireEvent(event));
+                }
+            },
+            onUnsubscribe : function() {
+                // If we just detached the last handler, let native know we
+                // no longer override the back button.
+                if (this.numHandlers === 0) {
+                    blackberry.system.event.onHardwareKey(
+                            buttonMapping[event], null);
+                }
+            }};
+        };
+
+        // Inject listeners for buttons on the document.
+        for (var button in buttonMapping) {
+            if (buttonMapping.hasOwnProperty(button)) {
+                cordova.addDocumentEventHandler(button, eventHandler(button));
+            }
+        }
+
+        // Fires off necessary code to pause/resume app
+        var resume = function() {
+            cordova.fireDocumentEvent('resume');
+            blackberryManager.resume();
+        };
+        var pause = function() {
+            cordova.fireDocumentEvent('pause');
+            blackberryManager.pause();
+        };
+
+        /************************************************
+         * Patch up the generic pause/resume listeners. *
+         ************************************************/
+
+        // Unsubscribe handler - turns off native backlight change
+        // listener
+        var onUnsubscribe = function() {
+            if (channel.onResume.numHandlers === 0 && channel.onPause.numHandlers === 0) {
+                exec(null, null, 'App', 'ignoreBacklight', []);
+            }
+        };
+
+        // Native backlight detection win/fail callbacks
+        var backlightWin = function(isOn) {
+            if (isOn === true) {
+                resume();
+            } else {
+                pause();
+            }
+        };
+        var backlightFail = function(e) {
+            console.log("Error detecting backlight on/off.");
+        };
+
+        // Override stock resume and pause listeners so we can trigger
+        // some native methods during attach/remove
+        channel.onResume = cordova.addDocumentEventHandler('resume', {
+            onSubscribe:function() {
+                // If we just attached the first handler and there are
+                // no pause handlers, start the backlight system
+                // listener on the native side.
+                if (channel.onResume.numHandlers === 1 && channel.onPause.numHandlers === 0) {
+                    exec(backlightWin, backlightFail, "App", "detectBacklight", []);
+                }
+            },
+            onUnsubscribe:onUnsubscribe
+        });
+        channel.onPause = cordova.addDocumentEventHandler('pause', {
+            onSubscribe:function() {
+                // If we just attached the first handler and there are
+                // no resume handlers, start the backlight system
+                // listener on the native side.
+                if (channel.onResume.numHandlers === 0 && channel.onPause.numHandlers === 1) {
+                    exec(backlightWin, backlightFail, "App", "detectBacklight", []);
+                }
+            },
+            onUnsubscribe:onUnsubscribe
+        });
+
+        // Fire resume event when application brought to foreground.
+        blackberry.app.event.onForeground(resume);
+
+        // Fire pause event when application sent to background.
+        blackberry.app.event.onBackground(pause);
+
+        // Trap BlackBerry WebWorks exit. Allow plugins to clean up before exiting.
+        blackberry.app.event.onExit(app.exitApp);
+    },
+    objects: {
+        navigator: {
+            children: {
+                app: {
+                    path: "cordova/plugin/blackberry/app"
+                }
+            }
+        },
+        device: {
+            path: "cordova/plugin/blackberry/device"
+        },
+        File: { // exists natively on BlackBerry OS 7, override
+            path: "cordova/plugin/File"
+        }
+    },
+    merges: {
+        navigator: {
+            children: {
+                contacts: {
+                    path: 'cordova/plugin/blackberry/contacts'
+                },
+                device: {
+                    path: 'cordova/plugin/blackberry/device'
+                },
+                notification: {
+                    path: 'cordova/plugin/blackberry/notification'
+                }
+            }
+        },
+        Contact: {
+            path: 'cordova/plugin/blackberry/Contact'
+        },
+        DirectoryEntry: {
+            path: 'cordova/plugin/blackberry/DirectoryEntry'
+        },
+        Entry: {
+            path: 'cordova/plugin/blackberry/Entry'
+        }
+    }
+};

http://git-wip-us.apache.org/repos/asf/incubator-cordova-js/blob/e50b7ef6/lib/blackberry/plugin/blackberry/Contact.js
----------------------------------------------------------------------
diff --git a/lib/blackberry/plugin/blackberry/Contact.js b/lib/blackberry/plugin/blackberry/Contact.js
new file mode 100644
index 0000000..bbc0937
--- /dev/null
+++ b/lib/blackberry/plugin/blackberry/Contact.js
@@ -0,0 +1,386 @@
+var ContactError = require('cordova/plugin/ContactError'),
+    ContactUtils = require('cordova/plugin/blackberry/ContactUtils'),
+    exec = require('cordova/exec');
+
+// ------------------
+// Utility functions
+// ------------------
+
+/**
+ * Retrieves a BlackBerry contact from the device by unique id.
+ *
+ * @param uid
+ *            Unique id of the contact on the device
+ * @return {blackberry.pim.Contact} BlackBerry contact or null if contact with
+ *         specified id is not found
+ */
+var findByUniqueId = function(uid) {
+    if (!uid) {
+        return null;
+    }
+    var bbContacts = blackberry.pim.Contact
+            .find(new blackberry.find.FilterExpression("uid", "==", uid));
+    return bbContacts[0] || null;
+};
+
+/**
+ * Creates a BlackBerry contact object from the W3C Contact object and persists
+ * it to device storage.
+ *
+ * @param {Contact}
+ *            contact The contact to save
+ * @return a new contact object with all properties set
+ */
+var saveToDevice = function(contact) {
+
+    if (!contact) {
+        return;
+    }
+
+    var bbContact = null;
+    var update = false;
+
+    // if the underlying BlackBerry contact already exists, retrieve it for
+    // update
+    if (contact.id) {
+        // we must attempt to retrieve the BlackBerry contact from the device
+        // because this may be an update operation
+        bbContact = findByUniqueId(contact.id);
+    }
+
+    // contact not found on device, create a new one
+    if (!bbContact) {
+        bbContact = new blackberry.pim.Contact();
+    }
+    // update the existing contact
+    else {
+        update = true;
+    }
+
+    // NOTE: The user may be working with a partial Contact object, because only
+    // user-specified Contact fields are returned from a find operation (blame
+    // the W3C spec). If this is an update to an existing Contact, we don't
+    // want to clear an attribute from the contact database simply because the
+    // Contact object that the user passed in contains a null value for that
+    // attribute. So we only copy the non-null Contact attributes to the
+    // BlackBerry contact object before saving.
+    //
+    // This means that a user must explicitly set a Contact attribute to a
+    // non-null value in order to update it in the contact database.
+    //
+    // name
+    if (contact.name !== null) {
+        if (contact.name.givenName) {
+            bbContact.firstName = contact.name.givenName;
+        }
+        if (contact.name.familyName) {
+            bbContact.lastName = contact.name.familyName;
+        }
+        if (contact.name.honorificPrefix) {
+            bbContact.title = contact.name.honorificPrefix;
+        }
+    }
+
+    // display name
+    if (contact.displayName !== null) {
+        bbContact.user1 = contact.displayName;
+    }
+
+    // note
+    if (contact.note !== null) {
+        bbContact.note = contact.note;
+    }
+
+    // birthday
+    //
+    // user may pass in Date object or a string representation of a date
+    // if it is a string, we don't know the date format, so try to create a
+    // new Date with what we're given
+    //
+    // NOTE: BlackBerry's Date.parse() does not work well, so use new Date()
+    //
+    if (contact.birthday !== null) {
+        if (contact.birthday instanceof Date) {
+            bbContact.birthday = contact.birthday;
+        } else {
+            var bday = contact.birthday.toString();
+            bbContact.birthday = (bday.length > 0) ? new Date(bday) : "";
+        }
+    }
+
+    // BlackBerry supports three email addresses
+    if (contact.emails && contact.emails instanceof Array) {
+
+        // if this is an update, re-initialize email addresses
+        if (update) {
+            bbContact.email1 = "";
+            bbContact.email2 = "";
+            bbContact.email3 = "";
+        }
+
+        // copy the first three email addresses found
+        var email = null;
+        for ( var i = 0; i < contact.emails.length; i += 1) {
+            email = contact.emails[i];
+            if (!email || !email.value) {
+                continue;
+            }
+            if (bbContact.email1 === "") {
+                bbContact.email1 = email.value;
+            } else if (bbContact.email2 === "") {
+                bbContact.email2 = email.value;
+            } else if (bbContact.email3 === "") {
+                bbContact.email3 = email.value;
+            }
+        }
+    }
+
+    // BlackBerry supports a finite number of phone numbers
+    // copy into appropriate fields based on type
+    if (contact.phoneNumbers && contact.phoneNumbers instanceof Array) {
+
+        // if this is an update, re-initialize phone numbers
+        if (update) {
+            bbContact.homePhone = "";
+            bbContact.homePhone2 = "";
+            bbContact.workPhone = "";
+            bbContact.workPhone2 = "";
+            bbContact.mobilePhone = "";
+            bbContact.faxPhone = "";
+            bbContact.pagerPhone = "";
+            bbContact.otherPhone = "";
+        }
+
+        var type = null;
+        var number = null;
+        for ( var i = 0; i < contact.phoneNumbers.length; i += 1) {
+            if (!contact.phoneNumbers[i] || !contact.phoneNumbers[i].value) {
+                continue;
+            }
+            type = contact.phoneNumbers[i].type;
+            number = contact.phoneNumbers[i].value;
+            if (type === 'home') {
+                if (bbContact.homePhone === "") {
+                    bbContact.homePhone = number;
+                } else if (bbContact.homePhone2 === "") {
+                    bbContact.homePhone2 = number;
+                }
+            } else if (type === 'work') {
+                if (bbContact.workPhone === "") {
+                    bbContact.workPhone = number;
+                } else if (bbContact.workPhone2 === "") {
+                    bbContact.workPhone2 = number;
+                }
+            } else if (type === 'mobile' && bbContact.mobilePhone === "") {
+                bbContact.mobilePhone = number;
+            } else if (type === 'fax' && bbContact.faxPhone === "") {
+                bbContact.faxPhone = number;
+            } else if (type === 'pager' && bbContact.pagerPhone === "") {
+                bbContact.pagerPhone = number;
+            } else if (bbContact.otherPhone === "") {
+                bbContact.otherPhone = number;
+            }
+        }
+    }
+
+    // BlackBerry supports two addresses: home and work
+    // copy the first two addresses found from Contact
+    if (contact.addresses && contact.addresses instanceof Array) {
+
+        // if this is an update, re-initialize addresses
+        if (update) {
+            bbContact.homeAddress = null;
+            bbContact.workAddress = null;
+        }
+
+        var address = null;
+        var bbHomeAddress = null;
+        var bbWorkAddress = null;
+        for ( var i = 0; i < contact.addresses.length; i += 1) {
+            address = contact.addresses[i];
+            if (!address || address instanceof ContactAddress === false) {
+                continue;
+            }
+
+            if (bbHomeAddress === null
+                    && (!address.type || address.type === "home")) {
+                bbHomeAddress = createBlackBerryAddress(address);
+                bbContact.homeAddress = bbHomeAddress;
+            } else if (bbWorkAddress === null
+                    && (!address.type || address.type === "work")) {
+                bbWorkAddress = createBlackBerryAddress(address);
+                bbContact.workAddress = bbWorkAddress;
+            }
+        }
+    }
+
+    // copy first url found to BlackBerry 'webpage' field
+    if (contact.urls && contact.urls instanceof Array) {
+
+        // if this is an update, re-initialize web page
+        if (update) {
+            bbContact.webpage = "";
+        }
+
+        var url = null;
+        for ( var i = 0; i < contact.urls.length; i += 1) {
+            url = contact.urls[i];
+            if (!url || !url.value) {
+                continue;
+            }
+            if (bbContact.webpage === "") {
+                bbContact.webpage = url.value;
+                break;
+            }
+        }
+    }
+
+    // copy fields from first organization to the
+    // BlackBerry 'company' and 'jobTitle' fields
+    if (contact.organizations && contact.organizations instanceof Array) {
+
+        // if this is an update, re-initialize org attributes
+        if (update) {
+            bbContact.company = "";
+        }
+
+        var org = null;
+        for ( var i = 0; i < contact.organizations.length; i += 1) {
+            org = contact.organizations[i];
+            if (!org) {
+                continue;
+            }
+            if (bbContact.company === "") {
+                bbContact.company = org.name || "";
+                bbContact.jobTitle = org.title || "";
+                break;
+            }
+        }
+    }
+
+    // categories
+    if (contact.categories && contact.categories instanceof Array) {
+        bbContact.categories = [];
+        var category = null;
+        for ( var i = 0; i < contact.categories.length; i += 1) {
+            category = contact.categories[i];
+            if (typeof category == "string") {
+                bbContact.categories.push(category);
+            }
+        }
+    }
+
+    // save to device
+    bbContact.save();
+
+    // invoke native side to save photo
+    // fail gracefully if photo URL is no good, but log the error
+    if (contact.photos && contact.photos instanceof Array) {
+        var photo = null;
+        for ( var i = 0; i < contact.photos.length; i += 1) {
+            photo = contact.photos[i];
+            if (!photo || !photo.value) {
+                continue;
+            }
+            exec(
+            // success
+            function() {
+            },
+            // fail
+            function(e) {
+                console.log('Contact.setPicture failed:' + e);
+            }, "Contact", "setPicture", [ bbContact.uid, photo.type,
+                    photo.value ]);
+            break;
+        }
+    }
+
+    // Use the fully populated BlackBerry contact object to create a
+    // corresponding W3C contact object.
+    return ContactUtils.createContact(bbContact, [ "*" ]);
+};
+
+/**
+ * Creates a BlackBerry Address object from a W3C ContactAddress.
+ *
+ * @return {blackberry.pim.Address} a BlackBerry address object
+ */
+var createBlackBerryAddress = function(address) {
+    var bbAddress = new blackberry.pim.Address();
+
+    if (!address) {
+        return bbAddress;
+    }
+
+    bbAddress.address1 = address.streetAddress || "";
+    bbAddress.city = address.locality || "";
+    bbAddress.stateProvince = address.region || "";
+    bbAddress.zipPostal = address.postalCode || "";
+    bbAddress.country = address.country || "";
+
+    return bbAddress;
+};
+
+module.exports = {
+    /**
+     * Persists contact to device storage.
+     */
+    save : function(success, fail) {
+        try {
+            // save the contact and store it's unique id
+            var fullContact = saveToDevice(this);
+            this.id = fullContact.id;
+
+            // This contact object may only have a subset of properties
+            // if the save was an update of an existing contact. This is
+            // because the existing contact was likely retrieved using a
+            // subset of properties, so only those properties were set in the
+            // object. For this reason, invoke success with the contact object
+            // returned by saveToDevice since it is fully populated.
+            if (typeof success === 'function') {
+                success(fullContact);
+            }
+        } catch (e) {
+            console.log('Error saving contact: ' + e);
+            if (typeof fail === 'function') {
+                fail(new ContactError(ContactError.UNKNOWN_ERROR));
+            }
+        }
+    },
+
+    /**
+     * Removes contact from device storage.
+     *
+     * @param success
+     *            success callback
+     * @param fail
+     *            error callback
+     */
+    remove : function(success, fail) {
+        try {
+            // retrieve contact from device by id
+            var bbContact = null;
+            if (this.id) {
+                bbContact = findByUniqueId(this.id);
+            }
+
+            // if contact was found, remove it
+            if (bbContact) {
+                console.log('removing contact: ' + bbContact.uid);
+                bbContact.remove();
+                if (typeof success === 'function') {
+                    success(this);
+                }
+            }
+            // attempting to remove a contact that hasn't been saved
+            else if (typeof fail === 'function') {
+                fail(new ContactError(ContactError.UNKNOWN_ERROR));
+            }
+        } catch (e) {
+            console.log('Error removing contact ' + this.id + ": " + e);
+            if (typeof fail === 'function') {
+                fail(new ContactError(ContactError.UNKNOWN_ERROR));
+            }
+        }
+    }
+};
\ No newline at end of file