You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ag...@apache.org on 2013/05/23 22:03:36 UTC

[13/30] git commit: New filenames.Url aliases.(New AppBundle plugin needed).Desc cont below Android has a bug where the web view will not throw intercept events if you are navigating to a location that matches a filename in your bundle. Thus in order to

New filenames.Url aliases.(New AppBundle plugin needed).Desc cont below
Android has a bug where the web view will not throw intercept events if you are navigating to a location that matches a filename in your bundle. Thus in order to support url aliasing, we need to ensure there is no collision of the running app's(the app running in the harness) filenames and the app harness file names.
Added pre launch hooks that so we can attach things to do before launching an app.
Url Aliasing - support for using bundle directories Eg:"file:///android_asset/www/index.html" in your app.


Project: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/commit/771f02b2
Tree: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/tree/771f02b2
Diff: http://git-wip-us.apache.org/repos/asf/cordova-app-harness/diff/771f02b2

Branch: refs/heads/master
Commit: 771f02b25dea717fbfb2009cc5c2ec9012a52478
Parents: eba56cc
Author: Shravan Narayan <sh...@google.com>
Authored: Tue May 7 19:02:01 2013 -0400
Committer: Shravan Narayan <sh...@google.com>
Committed: Tue May 7 19:23:33 2013 -0400

----------------------------------------------------------------------
 .jshintignore                            |    2 +-
 checkjs                                  |    2 +-
 packapp                                  |    2 +-
 www/cdvah_contextMenu.html               |   53 ++++
 www/cdvah_index.html                     |   20 ++
 www/cdvah_js/AddCtrl.js                  |   24 ++
 www/cdvah_js/AppBundleAlias.js           |   36 +++
 www/cdvah_js/AppConstants.js             |   12 +
 www/cdvah_js/AppsService.js              |  276 +++++++++++++++++
 www/cdvah_js/CdvhPackageHandler.js       |   60 ++++
 www/cdvah_js/ContextMenu.js              |   83 +++++
 www/cdvah_js/KnownExtensionDownloader.js |   43 +++
 www/cdvah_js/ListCtrl.js                 |  117 +++++++
 www/cdvah_js/ResourcesLoader.js          |  404 +++++++++++++++++++++++++
 www/cdvah_js/app.js                      |   11 +
 www/cdvah_js/libs/angular.min.js         |  173 +++++++++++
 www/cdvah_js/libs/q.min.js               |   19 ++
 www/cdvah_views/add.html                 |   17 +
 www/cdvah_views/list.html                |   12 +
 www/contextMenu.html                     |   53 ----
 www/index.html                           |   19 --
 www/js/AddCtrl.js                        |   24 --
 www/js/AppConstants.js                   |   12 -
 www/js/AppsService.js                    |  253 ----------------
 www/js/CdvhPackageHandler.js             |   60 ----
 www/js/ContextMenu.js                    |   84 -----
 www/js/KnownExtensionDownloader.js       |   43 ---
 www/js/ListCtrl.js                       |   94 ------
 www/js/ResourcesLoader.js                |  404 -------------------------
 www/js/app.js                            |   11 -
 www/js/libs/angular.min.js               |  173 -----------
 www/js/libs/q.min.js                     |   19 --
 www/views/add.html                       |   17 -
 www/views/list.html                      |   12 -
 34 files changed, 1363 insertions(+), 1281 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/.jshintignore
----------------------------------------------------------------------
diff --git a/.jshintignore b/.jshintignore
index e491595..4e6ee77 100644
--- a/.jshintignore
+++ b/.jshintignore
@@ -1 +1 @@
-www/js/libs/*
\ No newline at end of file
+www/cdvah_js/libs/*
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/checkjs
----------------------------------------------------------------------
diff --git a/checkjs b/checkjs
index 200d824..b62fa32 100755
--- a/checkjs
+++ b/checkjs
@@ -22,4 +22,4 @@
 #   ./checkjs
 #
 echo "Running jsHint"
-jshint www/js/ --verbose --show-non-errors
+jshint www/cdvah_js/ --verbose --show-non-errors

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/packapp
----------------------------------------------------------------------
diff --git a/packapp b/packapp
index dd1202c..74dd08f 100755
--- a/packapp
+++ b/packapp
@@ -81,4 +81,4 @@ fi
 cd packtempDir
 zip -r $ORIG_DIR/$FILE_NAME android ios >/dev/null
 cd ..
-# rm -rf packtempDir
\ No newline at end of file
+rm -rf packtempDir
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_contextMenu.html
----------------------------------------------------------------------
diff --git a/www/cdvah_contextMenu.html b/www/cdvah_contextMenu.html
new file mode 100644
index 0000000..8f515c9
--- /dev/null
+++ b/www/cdvah_contextMenu.html
@@ -0,0 +1,53 @@
+<!-- Keep stylesheet as a part of the html as this file gets injected into the app -->
+<style>
+#__cordovaappharness_contextMenu_div
+{
+    position: fixed;
+    left : 0px;
+    top : 0px;
+    z-index: 2000;
+    background-color:rgba(0,0,0,0.75);
+    width: 100%;
+    height: 100%;
+    display: none;
+}
+#__cordovaappharness_contextMenu_div p
+{
+    color: white;
+    text-align: center;
+}
+#__cordovaappharness_contextMenu_div ul
+{
+    list-style-type: none;
+    margin-left: 20%;
+    margin-right: 20%;
+    padding: 0;
+    width: 60%;
+}
+#__cordovaappharness_contextMenu_div li
+{
+    margin-bottom: 0.5cm;
+}
+#__cordovaappharness_contextMenu_div li, #__cordovaappharness_contextMenu_div button
+{
+    width: 100%;
+    height: 1cm;
+}
+</style>
+<div id="__cordovaappharness_contextMenu_div">
+    <p>Tap Anywhere to Close</p>
+    <ul>
+        <li>
+            <a href="app-bundle:///cdvah_index.html#/?updateLastLaunched=true"><button>Update</button></a>
+        </li>
+        <li>
+            <a href="app-bundle:///cdvah_index.html#/?lastLaunched=true"><button>Restart</button></a>
+        </li>
+        <li>
+            <button id="__cordovaappharness_contextMenu_firebug_button">Firebug</button>
+        </li>
+        <li>
+            <a href="app-bundle:///cdvah_index.html"><button>Back to Main Menu</button></a>
+        </li>
+    </ul>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_index.html
----------------------------------------------------------------------
diff --git a/www/cdvah_index.html b/www/cdvah_index.html
new file mode 100644
index 0000000..b33fe29
--- /dev/null
+++ b/www/cdvah_index.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Cordova App Harness</title>
+        <script type="text/javascript" src="cordova.js"></script>
+        <script type="text/javascript" src="cdvah_js/libs/q.min.js"></script>
+        <script type="text/javascript" src="cdvah_js/libs/angular.min.js"></script>
+        <script type="text/javascript" src="cdvah_js/app.js"></script>
+        <script type="text/javascript" src="cdvah_js/AppConstants.js"></script>
+        <script type="text/javascript" src="cdvah_js/ResourcesLoader.js"></script>
+        <script type="text/javascript" src="cdvah_js/AppsService.js"></script>
+        <script type="text/javascript" src="cdvah_js/AppBundleAlias.js"></script>
+        <script type="text/javascript" src="cdvah_js/KnownExtensionDownloader.js"></script>
+        <script type="text/javascript" src="cdvah_js/CdvhPackageHandler.js"></script>
+        <script type="text/javascript" src="cdvah_js/ListCtrl.js"></script>
+        <script type="text/javascript" src="cdvah_js/AddCtrl.js"></script>
+    </head>
+    <body ng-app="CordovaAppHarness" ng-view>
+    </body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/AddCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/AddCtrl.js b/www/cdvah_js/AddCtrl.js
new file mode 100644
index 0000000..09a1c50
--- /dev/null
+++ b/www/cdvah_js/AddCtrl.js
@@ -0,0 +1,24 @@
+(function(){
+    "use strict";
+    /* global myApp */
+    myApp.controller("AddCtrl", ["$scope", "AppsService", function ($scope, AppsService) {
+
+        $scope.addApp = function(appName, appSource, appSourcePattern) {
+            if(appSource === "pattern") {
+                if(!appSourcePattern) {
+                    alert("Url not specified");
+                    return;
+                }
+
+                AppsService.addAppFromPattern(appName, appSourcePattern)
+                .then(function() {
+                    alert("Successfully installed");
+                }, function(error) {
+                    console.error(error);
+                    alert("Unable to add application because: \n" + JSON.stringify(error));
+                });
+            }
+        };
+    }]);
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/AppBundleAlias.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/AppBundleAlias.js b/www/cdvah_js/AppBundleAlias.js
new file mode 100644
index 0000000..1bdfbf8
--- /dev/null
+++ b/www/cdvah_js/AppBundleAlias.js
@@ -0,0 +1,36 @@
+(function(){
+    "use strict";
+    /* global myApp */
+    myApp.run(["AppsService", function(AppsService){
+
+        // URI aliasing : the ability to launch an app in the harness, query the document.location and get the same location as would have been got if you run the app separately
+        // Without URI aliasing, document.location in the harness would give something like file:///APP_HARNESS_INSTALLED_APPS_LOCATION/www/index.html
+
+        function aliasUri(sourceUriMatchRegex, sourceUriReplaceRegex, replaceString, redirectToReplacedUrl){
+            var deferred = Q.defer();
+            var appBundle = cordova.require("AppBundle.AppBundle");
+            try {
+                appBundle.addAlias(sourceUriMatchRegex, sourceUriReplaceRegex, replaceString, redirectToReplacedUrl, function(succeded){
+                    if(succeded){
+                        deferred.resolve();
+                    } else {
+                        deferred.reject(new Error("Unable to set up uri aliasing"));
+                    }
+                });
+            } catch(e) {
+                deferred.reject(new Error(e));
+            } finally {
+                return deferred.promise;
+            }
+        }
+
+        AppsService.addPreLaunchHook(function(appName, wwwLocation) {
+            console.log("Adding aliases for " + appName);
+            wwwLocation += (wwwLocation.charAt(wwwLocation.length - 1) === "/")? "" : "/";
+            wwwLocation = (wwwLocation.indexOf("file://") === 0) ? wwwLocation : "file://" + wwwLocation;
+            //Make any direct references to the bundle paths such as file:///android_asset point to the installed location without redirecting
+            //{BUNDLE_WWW} in the regex is automatically replaced by the appBundle component
+            return aliasUri("^{BUNDLE_WWW}.*", "^{BUNDLE_WWW}", wwwLocation, false /* redirect */);
+        });
+    }]);
+})();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/AppConstants.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/AppConstants.js b/www/cdvah_js/AppConstants.js
new file mode 100644
index 0000000..81f8b4d
--- /dev/null
+++ b/www/cdvah_js/AppConstants.js
@@ -0,0 +1,12 @@
+(function() {
+    var TEMP_DIRECTORY = "cordova_app_harness_tempDir/";
+    var INSTALL_DIRECTORY = "cordova_app_harness_installed_apps/";
+    var APPS_JSON = "cordova_app_harness_installed_apps/apps.json";
+    var METADATA_JSON = "cordova_app_harness_installed_apps/metadata.json";
+
+    /* global myApp */
+    myApp.value("TEMP_DIRECTORY", TEMP_DIRECTORY);
+    myApp.value("INSTALL_DIRECTORY", INSTALL_DIRECTORY);
+    myApp.value("APPS_JSON", APPS_JSON);
+    myApp.value("METADATA_JSON", METADATA_JSON);
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/AppsService.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/AppsService.js b/www/cdvah_js/AppsService.js
new file mode 100644
index 0000000..f961cc9
--- /dev/null
+++ b/www/cdvah_js/AppsService.js
@@ -0,0 +1,276 @@
+(function() {
+    "use strict";
+    /* global myApp */
+    myApp.factory("AppsService", [ "ResourcesLoader", "INSTALL_DIRECTORY", "TEMP_DIRECTORY", "APPS_JSON", "METADATA_JSON", function(ResourcesLoader, INSTALL_DIRECTORY, TEMP_DIRECTORY, APPS_JSON, METADATA_JSON) {
+
+        var platformId = cordova.require("cordova/platform").id;
+        // downloaders that know how to download from certain patterns.
+        // Eg: The KnownExtensionDownloader MAY know how to download from any uri's that end in known extensions
+        var downloadHandlers = [];
+        // handlers that have registered to unpack certain extensions during the installation of an app
+        var extensionHandlers = {};
+        // functions to run before launching an app
+        var preLaunchHooks = [];
+
+        function grabExtensionFromUri(uri) {
+            var lastSegment = uri.split("#")[0].split("?")[0].split("/").pop();
+            var dotLocation = lastSegment.lastIndexOf(".");
+            var extension = (dotLocation !== -1)? lastSegment.substring(dotLocation + 1) : "";
+            return extension;
+        }
+
+        function addNewAppFromPattern(appName, appSourcePattern) {
+            var _fullFilePath;
+
+            return ResourcesLoader.deleteDirectory(INSTALL_DIRECTORY + appName)
+            .then(function(){
+                for(var i = 0; i < downloadHandlers.length; i++){
+                    if(downloadHandlers[i].handler.canHandleSourcePattern(appSourcePattern)){
+                        return downloadHandlers[i].handler.downloadFromPattern(appName, appSourcePattern, TEMP_DIRECTORY);
+                    }
+                }
+                throw new Error("App Harness does not know how to install an app from the pattern: " + appSourcePattern);
+            })
+            .then(function(fullFilePath){
+                _fullFilePath = fullFilePath;
+                return ResourcesLoader.ensureDirectoryExists(INSTALL_DIRECTORY + appName);
+            })
+            .then(function(directoryPath){
+                var extension = grabExtensionFromUri(appSourcePattern);
+                if(!extensionHandlers[extension]) {
+                    throw new Error("No handler for extension " + extension + " found");
+                }
+                return extensionHandlers[extension].extractPackageToDirectory(_fullFilePath, directoryPath);
+            })
+            .then(function(){
+                return registerApp(appName, "pattern", appSourcePattern);
+            });
+        }
+
+        function registerApp(appName, appSource, appSourcePattern) {
+            return ResourcesLoader.readJSONFileContents(APPS_JSON)
+            .then(function(result){
+                result.installedApps = result.installedApps || [];
+                result.installedApps.push({
+                    "Name" :  appName,
+                    "Source" : appSource,
+                    "Data" : appSourcePattern,
+                    "Installed" : (new Date()).toLocaleString()
+                });
+                return ResourcesLoader.writeJSONFileContents(APPS_JSON, result);
+            });
+        }
+
+        function cleanPath(path){
+            if(path.indexOf("file://") === 0){
+                path = path.substring("file://".length);
+            }
+            // remove trailing slash
+            return (path.substring(path.length - 1) === "/") ? path.substring(0, path.length - 1) : path;
+        }
+
+        function isPathAbsolute(path){
+            return (path.match(/^[a-zA-Z0-9]+:/) != null);
+        }
+
+        function getAppStartPageFromConfig(configFile, appBaseDirectory) {
+            configFile = cleanPath(configFile);
+            appBaseDirectory = "file://" + cleanPath(appBaseDirectory);
+
+            return ResourcesLoader.readFileContents(configFile)
+            .then(function(contents){
+                if(!contents) {
+                    throw new Error("Config file is empty. Unable to find a start page for your app.");
+                } else {
+                    var startLocation = appBaseDirectory + "/index.html";
+                    var parser = new DOMParser();
+                    var xmlDoc = parser.parseFromString(contents, "text/xml");
+                    var els = xmlDoc.getElementsByTagName("content");
+
+                    if(els.length > 0) {
+                        // go through all "content" elements looking for the "src" attribute in reverse order
+                        for(var i = els.length - 1; i >= 0; i--) {
+                            var el = els[i];
+                            var srcValue = el.getAttribute("src");
+                            if(srcValue) {
+                                if(isPathAbsolute(srcValue)) {
+                                    startLocation = srcValue;
+                                } else {
+                                    srcValue = srcValue.charAt(0) === "/" ? srcValue.substring(1) : srcValue;
+                                    startLocation = appBaseDirectory + "/" + srcValue;
+                                }
+                                break;
+                            }
+                        }
+                    }
+
+                    return startLocation;
+                }
+            });
+        }
+
+        function removeApp(appName){
+            var entry;
+            return ResourcesLoader.ensureDirectoryExists(APPS_JSON)
+            .then(function() {
+                return ResourcesLoader.readJSONFileContents(APPS_JSON);
+            })
+            .then(function(result){
+                result.installedApps = result.installedApps || [];
+
+                for(var i = 0; i < result.installedApps.length; i++){
+                    if(result.installedApps[i].Name === appName) {
+                        entry = result.installedApps.splice(i, 1)[0];
+                        break;
+                    }
+                }
+
+                if(!entry) {
+                    throw new Error("The app " + appName + " was not found.");
+                }
+
+                return ResourcesLoader.writeJSONFileContents(APPS_JSON, result);
+            })
+            .then(function(){
+                return ResourcesLoader.deleteDirectory(INSTALL_DIRECTORY + appName);
+            })
+            .then(function(){
+                return entry;
+            });
+        }
+
+        return {
+            //return promise with the array of apps
+            getAppsList : function(getFullEntries) {
+                return ResourcesLoader.ensureDirectoryExists(APPS_JSON)
+                .then(function() {
+                    return ResourcesLoader.readJSONFileContents(APPS_JSON);
+                })
+                .then(function(result){
+                    result.installedApps = result.installedApps || [];
+                    var newAppsList = [];
+
+                    for(var i = 0; i < result.installedApps.length; i++){
+                        if(getFullEntries) {
+                            newAppsList.push(result.installedApps[i]);
+                        } else {
+                            newAppsList.push(result.installedApps[i].Name);
+                        }
+                    }
+
+                    return newAppsList;
+                });
+            },
+
+            launchApp : function(appName) {
+                var platformWWWLocation;
+                var startLocation;
+                return ResourcesLoader.readJSONFileContents(METADATA_JSON)
+                .then(function(settings){
+                    settings = settings || {};
+                    settings.lastLaunched = appName;
+                    return ResourcesLoader.writeJSONFileContents(METADATA_JSON, settings);
+                })
+                .then(function(){
+                    return ResourcesLoader.getFullFilePath(INSTALL_DIRECTORY + appName + "/" + platformId);
+                })
+                .then(function(platformLocation){
+                    platformWWWLocation = platformLocation + "/www/";
+                    return getAppStartPageFromConfig(platformLocation + "/config.xml", platformWWWLocation);
+                })
+                .then(function(_startLocation){
+                    startLocation = _startLocation;
+                    var promises = [];
+                    for (var i = preLaunchHooks.length - 1; i >= 0; i--) {
+                        promises.push(preLaunchHooks[i](appName, platformWWWLocation));
+                    }
+                    return Q.all(promises);
+                })
+                .then(function() {
+                    window.location = startLocation;
+                });
+            },
+
+            addAppFromPattern : function(appName, appSourcePattern) {
+                return this.getAppsList(false /* App names only */)
+                .then(function(appsList){
+                    if(appsList.indexOf(appName) !== -1) {
+                        throw new Error("An app with this name already exists");
+                    }
+                    return addNewAppFromPattern(appName, appSourcePattern);
+                });
+            },
+
+            uninstallApp : function(appName) {
+                return removeApp(appName);
+            },
+
+            getLastRunApp : function() {
+                return ResourcesLoader.readJSONFileContents(METADATA_JSON)
+                .then(function(settings){
+                    if(!settings || !settings.lastLaunched) {
+                        throw new Error("No App has been launched yet");
+                    }
+                    return settings.lastLaunched;
+                });
+            },
+
+            registerPatternDownloader : function(handler, priority){
+                if(!handler) {
+                    throw new Error("Expected handler");
+                }
+                if(typeof(handler.canHandleSourcePattern) !== "function") {
+                    throw new Error("Expected function for bool handler.canHandleSourcePattern(string pattern) to exist");
+                }
+                if(typeof(handler.downloadFromPattern) !== "function") {
+                    throw new Error("Expected function for (string fullFilePath or QPromise) handler.downloadFromPattern(string appName, string pattern, string tempDirectory) to exist");
+                }
+                if(!priority) {
+                    // Assign a default priority
+                    priority = 500;
+                }
+                var i = 0;
+                var objToInsert = { "priority" : priority, "handler" : handler };
+                for(i = 0; i < downloadHandlers.length; i++){
+                    if(downloadHandlers[i].priority > objToInsert.priority) {
+                        break;
+                    }
+                }
+                downloadHandlers.splice(i, 0, objToInsert);
+            },
+
+            registerPackageHandler : function(extension, handler) {
+                if(!extension) {
+                    throw new Error("Expcted extension");
+                }
+                if(!handler || typeof(handler.extractPackageToDirectory) !== "function") {
+                    throw new Error("Expected function for void handler.extractPackageToDirectory(string fullFilePath, string directoryPath) to exist");
+                }
+                if(handler[extension]) {
+                    throw new Error("Handler already exists for the extension: " + extension);
+                }
+                extensionHandlers[extension] = handler;
+            },
+
+            updateApp : function(appName){
+                return removeApp(appName)
+                .then(function(entry){
+                    if(entry.Source === "pattern") {
+                        return addNewAppFromPattern(entry.Name, entry.Data);
+                    }
+                });
+            },
+
+            getKnownExtensions : function() {
+                return Object.keys(extensionHandlers);
+            },
+
+            addPreLaunchHook : function(handler){
+                if(!handler || typeof(handler) !== "function") {
+                    throw new Error("Expected (QPromise or void) function(appName, wwwLocation) for handler");
+                }
+                preLaunchHooks.push(handler);
+            }
+        };
+    }]);
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/CdvhPackageHandler.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/CdvhPackageHandler.js b/www/cdvah_js/CdvhPackageHandler.js
new file mode 100644
index 0000000..1ea25ce
--- /dev/null
+++ b/www/cdvah_js/CdvhPackageHandler.js
@@ -0,0 +1,60 @@
+(function(){
+    "use strict";
+    /* global myApp */
+    myApp.run(["AppsService", "ResourcesLoader", function(AppsService, ResourcesLoader){
+
+        var platformId = cordova.require("cordova/platform").id;
+
+        function copyFile(startUrl, targetLocation){
+            /************ Begin Work around for File system bug ************/
+            if(targetLocation.indexOf("file://") === 0) {
+                targetLocation = targetLocation.substring("file://".length);
+            }
+            /************ End Work around for File system bug **************/
+            return ResourcesLoader.xhrGet(startUrl)
+            .then(function(xhr){
+                return ResourcesLoader.ensureDirectoryExists(targetLocation)
+                .then(function(){
+                    return ResourcesLoader.writeFileContents(targetLocation, xhr.responseText);
+                });
+            });
+        }
+
+        var appendScript = function() {
+            console.log("Injecting menu script");
+            var contextScript = document.createElement("script");
+            contextScript.setAttribute("type","text/javascript");
+            contextScript.setAttribute("src", "app-bundle:///cdvah_js/ContextMenu.js");
+            document.getElementsByTagName("head")[0].appendChild(contextScript);
+        };
+
+        AppsService.registerPackageHandler("cdvh", {
+            extractPackageToDirectory : function (fileName, outputDirectory){
+                var dataToAppend = "\n(" + appendScript.toString() + ")();";
+                var platformDirectory = outputDirectory + "/" + platformId + "/www/";
+                var cordovaFile = platformDirectory + "cordova.js";
+                var pluginsFile = platformDirectory + "cordova_plugins.json";
+
+                // We need to
+                // 1) Modify the cordova.js file
+                // 2) Copy the cordova_plugins.json we have, as the app that is being installed may have other plugins included which aren't in the harness.
+                // If we allow unavailable plugins to be included in this file, the plugin initialiser breaks
+                return ResourcesLoader.extractZipFile(fileName, outputDirectory)
+                .then(function(){
+                    return ResourcesLoader.doesFileExist(cordovaFile);
+                })
+                .then(function(fileExists){
+                    if(fileExists){
+                        return Q.all([
+                            ResourcesLoader.appendFileContents(cordovaFile, dataToAppend),
+                            copyFile("app-bundle:///cdvh_files/www/cordova_plugins.json", pluginsFile)
+                        ]);
+                    } else {
+                        throw new Error("The package does not seem to have the files required for the platform: " + platformId);
+                    }
+                });
+            }
+        });
+
+    }]);
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/ContextMenu.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/ContextMenu.js b/www/cdvah_js/ContextMenu.js
new file mode 100644
index 0000000..8854480
--- /dev/null
+++ b/www/cdvah_js/ContextMenu.js
@@ -0,0 +1,83 @@
+(function () {
+
+    function initialise() {
+        var contextHTMLUrl = "app-bundle:///cdvah_contextMenu.html";
+        var xhr = new window.XMLHttpRequest();
+        xhr.onreadystatechange=function()
+        {
+            if (xhr.readyState==4 && xhr.status==200)
+            {
+                var stringifiedHtml = xhr.responseText;
+                onInject(stringifiedHtml);
+            }
+        };
+        // retrieve the context menu
+        xhr.open("GET", contextHTMLUrl, true);
+        xhr.send();
+
+        loadFirebug(false);
+        attachErrorListener();
+    }
+
+    function onInject(stringifiedHtml) {
+
+        document.body.innerHTML += stringifiedHtml;
+
+        var contextDiv = "__cordovaappharness_contextMenu_div";
+        var showFirebugButton = "__cordovaappharness_contextMenu_firebug_button";
+
+        // Setup the listeners to toggle the context menu
+        document.addEventListener("touchmove", function (event) {
+            if(event.touches.length >= 3) {
+                document.getElementById(contextDiv).style.display = "inline";
+            }
+        }, false);
+
+        document.getElementById(contextDiv).onclick = function() {
+            document.getElementById(contextDiv).style.display = "none";
+        };
+        var firstTime = true;
+        document.getElementById(showFirebugButton).onclick = function(){
+            try {
+                if(firstTime){
+                    console.warn("Note that messages logged to the console at the app startup may not be visible here.");
+                    console.warn("Do not use the close button on Firebug. Your console logs will be cleared. Use minimize instead.");
+                    firstTime = false;
+                }
+                window.Firebug.chrome.open();
+            } catch(e) {
+                // hack - FirebugLite appears to have several bugs. One of which is - open firebug, user shuts down FirebugLite through the UI.
+                // FirebugLite is now in a bad state of neither being usable or removable. Any calls to open throw an error.
+                // The following lines removes the flags that FirebugLite looks for manually and makes it think it has not loaded it yet
+                // Then FirebugLite is loaded into the page again
+                // This hack should be revisited when FirebugLite moves from version 1.4
+                // Either the hack won't be needed anymore or the hack should be checked too see if it still works.
+                var el = document.getElementById("FirebugLite");
+                if(el) {
+                    el.setAttribute("id", "");
+                }
+                delete console.firebuglite;
+                loadFirebug(true);
+            }
+        };
+    }
+
+    function loadFirebug(startOpened){
+        var el = document.createElement("script");
+        el.setAttribute("id", "FirebugLite");
+        el.setAttribute("src", "https://getfirebug.com/firebug-lite.js");
+        el.setAttribute("FirebugLite", "4");
+        el.innerHTML = el.innerHTML = "{ debug : false, startOpened : "  + startOpened + ", showIconWhenHidden : false, saveCommandLineHistory : true, saveCookies : false }";
+        document.head.appendChild(el);
+    }
+
+    // FirebugLite doesn't catch errors from window.onerror like desktop browser's dev tools do. So we add it manually.
+    function attachErrorListener(){
+        window.onerror = function(msg, url, line) {
+            console.error("Error: " + msg + " on line: " +  line + " in file: " + url);
+        };
+    }
+
+    initialise();
+})();
+

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/KnownExtensionDownloader.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/KnownExtensionDownloader.js b/www/cdvah_js/KnownExtensionDownloader.js
new file mode 100644
index 0000000..cec413a
--- /dev/null
+++ b/www/cdvah_js/KnownExtensionDownloader.js
@@ -0,0 +1,43 @@
+(function(){
+    "use strict";
+    /* global myApp */
+    myApp.run(["AppsService", "ResourcesLoader", function(AppsService, ResourcesLoader){
+
+        function isUri(pattern){
+            var regexUri = /^(?:([a-z0-9+.-]+:\/\/)((?:(?:[a-z0-9-._~!$&'()*+,;=:]|%[0-9A-F]{2})*)@)?((?:[a-z0-9-._~!$&'()*+,;=]|%[0-9A-F]{2})*)(:(?:\d*))?(\/(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?|([a-z0-9+.-]+:)(\/?(?:[a-z0-9-._~!$&'()*+,;=:@]|%[0-9A-F]{2})+(?:[a-z0-9-._~!$&'()*+,;=:@\/]|%[0-9A-F]{2})*)?)(\?(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?(#(?:[a-z0-9-._~!$&'()*+,;=:\/?@]|%[0-9A-F]{2})*)?$/i;
+            var ret = (pattern.search(regexUri) !== -1);
+            return ret;
+        }
+
+        function grabExtensionFromUri(uri) {
+            var lastSegment = uri.split("#")[0].split("?")[0].split("/").pop();
+            var dotLocation = lastSegment.lastIndexOf(".");
+            var extension = (dotLocation !== -1)? lastSegment.substring(dotLocation + 1) : "";
+            return extension;
+        }
+
+        // Note the priority given has no meaning in and of itself. It is used solely to compare if any other component has higher priority.
+        AppsService.registerPatternDownloader({
+            canHandleSourcePattern : function (pattern) {
+                var canHandle = false;
+                if(isUri(pattern)) {
+                    var currentExtension = grabExtensionFromUri(pattern);
+                    if(currentExtension) {
+                        var knownExtensions = AppsService.getKnownExtensions();
+                        if(knownExtensions.indexOf(currentExtension) !== -1){
+                            canHandle = true;
+                        }
+                    }
+                }
+                return canHandle;
+            },
+
+            downloadFromPattern : function (appName, pattern, tempDirectory) {
+                var extension = grabExtensionFromUri(pattern);
+                var fileName = tempDirectory + appName + "." + extension;
+                return ResourcesLoader.downloadFromUrl(pattern, fileName);
+            }
+        }, 500 /* assign a priority */);
+
+    }]);
+})();

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/ListCtrl.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/ListCtrl.js b/www/cdvah_js/ListCtrl.js
new file mode 100644
index 0000000..5dd0794
--- /dev/null
+++ b/www/cdvah_js/ListCtrl.js
@@ -0,0 +1,117 @@
+(function(){
+    "use strict";
+    /* global myApp */
+    myApp.controller("ListCtrl", [ "$scope", "$routeParams", "AppsService", function ($scope, $routeParams, AppsService) {
+
+        $scope.appsList = [];
+
+        function clearAppBundleAliases(){
+            var deferred = Q.defer();
+            var appBundle = cordova.require("AppBundle.AppBundle");
+
+            try {
+                appBundle.clearAllAliases(function(succeded){
+                    if(succeded){
+                        deferred.resolve();
+                    } else {
+                        deferred.reject(new Error("Unable to clear old url aliases. Please restart App Harness."));
+                    }
+                });
+            } catch(e) {
+                deferred.reject(new Error(e));
+            } finally {
+                return deferred.promise;
+            }
+        }
+
+        function initialise() {
+            //if we are navigating here after running an app, reset any aliases set for the app by app harness or any aliases setup by the previous app
+            return clearAppBundleAliases()
+            .then(function(){
+                if($routeParams.lastLaunched) {
+                    return AppsService.getLastRunApp()
+                    .then(AppsService.launchApp, function(e){
+                        e = e || {};
+                        console.error("Error launching last run app: " + e.message);
+                        alert("Error launching last run app. Please try again.");
+                    });
+                }
+                else if($routeParams.updateLastLaunched) {
+                    var app;
+                    // updating may take a while so we show the apps list like we normally do
+                    return $scope.loadAppsList(true)
+                    .then(AppsService.getLastRunApp)
+                    .then(function(_app){
+                        app = _app;
+                        return AppsService.updateApp(app);
+                    })
+                    .then(function(){
+                        return AppsService.launchApp(app);
+                    }, function(e){
+                        e = e || {};
+                        console.error("Error updating last run app: " + e.message);
+                        alert("Error updating last run app. Please try again.");
+                    });
+                }
+                else {
+                    return $scope.loadAppsList(true);
+                }
+            });
+        }
+
+        $scope.loadAppsList = function(callApply) {
+            return AppsService.getAppsList(true /* get full information about the app */)
+            .then(function(newAppsList){
+                newAppsList.sort(function(a, b){
+                    if(a.Name < b.Name) {
+                        return -1;
+                    } else if(a.Name > b.Name) {
+                        return 1;
+                    }
+                    return 0;
+                });
+                //clear the old apps list
+                $scope.appsList.splice(0, $scope.appsList.length);
+                angular.extend($scope.appsList, newAppsList);
+                if(callApply) {
+                    $scope.$apply();
+                }
+            }, function(error){
+                var str = "There was an error retrieving the apps list";
+                console.error(str + JSON.stringify(error));
+                alert(str);
+            });
+        };
+
+        $scope.launchApp = function(app){
+            return AppsService.launchApp(app)
+            .then(null, function(error){
+                console.error("Error during loading of app " + app + ": " + error);
+                alert("Something went wrong during the loading of the app. Please try again.");
+            });
+        };
+
+        $scope.updateApp = function(app) {
+            return AppsService.updateApp(app)
+            .then(function(){
+                alert("Updated successfully");
+            }, function(error){
+                console.error("Error during updating of app " + app + ": " + error);
+                alert("Something went wrong during the updating of the app. Please try again.");
+            });
+        };
+
+        $scope.removeApp = function(app) {
+            var shouldUninstall = confirm("Are you sure you want to uninstall " + app + "?");
+            if(shouldUninstall) {
+                return AppsService.uninstallApp(app)
+                .then(function() { $scope.loadAppsList(true); }, function(error){
+                    console.error("Error during uninstall of app " + app + ": " + error);
+                    alert("Something went wrong during the uninstall of the app. Please try again.");
+                });
+            }
+        };
+
+        document.addEventListener("deviceready", initialise, false);
+    }]);
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/ResourcesLoader.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/ResourcesLoader.js b/www/cdvah_js/ResourcesLoader.js
new file mode 100644
index 0000000..d9ca013
--- /dev/null
+++ b/www/cdvah_js/ResourcesLoader.js
@@ -0,0 +1,404 @@
+(function() {
+    "use strict";
+
+    /* global myApp */
+    myApp.factory("ResourcesLoader", [ "$window", function ($window) {
+        var fs;
+        var initialised = false;
+
+        function initialiseFileSystem() {
+            var deferred = Q.defer();
+
+            if(!initialised) {
+
+                var failedFileSystemLookUp = function (error) {
+                    var errorString = "An error occurred while reading the file system.";
+                    if(error) {
+                        errorString += " " + JSON.stringify(error);
+                    }
+                    deferred.reject(new Error(errorString));
+                };
+
+                var success = function(_fs) {
+                    fs = _fs;
+                    initialised = true;
+                    deferred.resolve(fs);
+                };
+
+                try {
+                    $window.requestFileSystem($window.LocalFileSystem.PERSISTENT, 0, success, failedFileSystemLookUp);
+                } catch (e) {
+                    failedFileSystemLookUp(e);
+                }
+            } else {
+                deferred.resolve(fs);
+            }
+
+            return deferred.promise;
+        }
+
+        //promise returns full path to downloaded file
+        function downloadFromUrl(url, fullFilePath) {
+            var deferred = Q.defer();
+
+            try {
+                var downloadFail = function(error) {
+                    var str = "There was an error while downloading the file " + JSON.stringify(error);
+                    deferred.reject(new Error(str));
+                };
+
+                var downloadSuccess = function(fileEntry) {
+                    deferred.resolve(fileEntry.fullPath);
+                };
+
+                var fileTransfer = new $window.FileTransfer();
+                var uri = encodeURI(url);
+                fileTransfer.download(uri, fullFilePath, downloadSuccess, downloadFail);
+            } catch(e) {
+                deferred.reject(new Error(e));
+            } finally {
+                return deferred.promise;
+            }
+        }
+
+        function trim(str) {
+            return str && str.replace(/^\s+|\s+$/g, "");
+        }
+
+        function fixFilePath(path) {
+            if(path && path.indexOf("file://") === 0) {
+                path = path.substring("file://".length);
+            }
+            return path;
+        }
+
+        //promise returns the directory entry
+        function getDirectoryEntry(directoryName) {
+            var deferred = Q.defer();
+
+            try {
+                var errorWhileGettingDirectoryEntry = function(error) {
+                    var str = "There was an error while getting the directory entry for directory " + directoryName + " " + JSON.stringify(error);
+                    deferred.reject(new Error(str));
+                };
+                var success = function(directoryEntry) {
+                    deferred.resolve(directoryEntry);
+                };
+                fs.root.getDirectory(directoryName, {create: true, exclusive: false}, success, errorWhileGettingDirectoryEntry);
+            } catch(e) {
+                deferred.reject(new Error(e));
+            } finally {
+                return deferred.promise;
+            }
+        }
+
+        //promise returns the file entry
+        function getFileEntry(fileName, createFlag) {
+            var deferred = Q.defer();
+
+            try {
+                var errorWhileGettingFileEntry = function(error) {
+                    var str = "There was an error while getting the file entry for file " + fileName + " " + JSON.stringify(error);
+                    deferred.reject(new Error(str));
+                };
+                var success = function(fileEntry) {
+                    deferred.resolve(fileEntry);
+                };
+                // !! - ensures a boolean value
+                fs.root.getFile(fixFilePath(fileName), {create: !!createFlag, exclusive: false}, success, errorWhileGettingFileEntry);
+            } catch(e) {
+                deferred.reject(new Error(e));
+            } finally {
+                return deferred.promise;
+            }
+        }
+
+        //promise returns the file
+        function getFile(fileName) {
+            return getFileEntry(fileName, true  /* create */).
+            then(function(fileEntry){
+                var deferred = Q.defer();
+
+                try {
+                    var errorWhileGettingFile = function(error) {
+                        var str = "There was an error while getting the file for file " + fileName + " " + JSON.stringify(error);
+                        deferred.reject(new Error(str));
+                    };
+
+                    fileEntry.file(deferred.resolve, errorWhileGettingFile);
+                } catch(e) {
+                    deferred.reject(new Error(e));
+                } finally {
+                    return deferred.promise;
+                }
+            });
+        }
+
+        function truncateToDirectoryPath(path) {
+            //remove the filename if it exists
+            var lastLevelIndex = path.search(/[\w ]+(\.[\w ]+)+$/g);
+            if(lastLevelIndex !== -1) {
+                path = path.substring(0, lastLevelIndex);
+            }
+            return path;
+        }
+
+        function getPathSegments(path){
+            //truncate leading and trailing slashes
+            if(path.charAt(0) === "/"){
+                path = path.substring(1);
+            }
+            if(path.charAt(path.length - 1) === "/"){
+                path = path.substring(0, path.length - 1);
+            }
+            var segments = path.split("/");
+            return segments;
+        }
+
+        function ensureSingleDirectoryExists(directory){
+            var deferred = Q.defer();
+
+            var gotDirEntry = function(dirEntry) {
+                deferred.resolve(dirEntry.fullPath);
+            };
+
+            var failedToGetDirEntry = function(error) {
+                var str = "There was an error checking the directory: " + directory + " " + JSON.stringify(error);
+                deferred.reject(new Error(str));
+            };
+
+            fs.root.getDirectory(directory, {create: true, exclusive: false}, gotDirEntry, failedToGetDirEntry);
+            return deferred.promise;
+        }
+
+        function writeToFile(fileName, contents, append) {
+            return getFileEntry(fileName, true)
+            .then(function(fileEntry){
+                var deferred = Q.defer();
+
+                var errorGettingFileWriter = function(error) {
+                    var str = "There was an error writing the file." + JSON.stringify(error);
+                    deferred.reject(new Error(str));
+                };
+
+                var gotFileWriter = function(writer) {
+                    writer.onwrite = deferred.resolve;
+                    writer.onerror = function(evt) {
+                        deferred.reject(new Error(evt));
+                    };
+                    if(append){
+                        writer.seek(writer.length);
+                    }
+                    writer.write(contents);
+                };
+                fileEntry.createWriter(gotFileWriter, errorGettingFileWriter);
+                return deferred.promise;
+            });
+        }
+
+        return {
+            doesFileExist : function(fileName){
+                return initialiseFileSystem()
+                .then(function(){
+                    return getFileEntry(fileName, false /* create */);
+                })
+                .then(function(){
+                    return true;
+                }, function(){
+                    return false;
+                });
+            },
+
+            // returns a promise with a full path to the dir
+            ensureDirectoryExists : function(directory) {
+                return initialiseFileSystem()
+                .then(function(){
+                    directory = truncateToDirectoryPath(directory);
+                    directory = fixFilePath(directory);
+                    var segments = getPathSegments(directory);
+                    var currentDir = directory.charAt(0) === "/"? "/" : "";
+                    var promiseArr = [];
+                    while(segments.length !== 0) {
+                        currentDir +=  segments.shift() + "/";
+                        promiseArr.push(ensureSingleDirectoryExists(currentDir));
+                    }
+                    return Q.all(promiseArr);
+                })
+                .then(function(paths){
+                    return paths[paths.length - 1];
+                });
+            },
+
+            // promise returns full path to file
+            getFullFilePath : function(filePath) {
+                return initialiseFileSystem()
+                .then(function(){
+                    var deferred = Q.defer();
+
+                    // Use the file's parent folder to get the full path
+                    var directory = filePath;
+                    var fileName = "";
+
+                    //remove the filename if it exists
+                    var lastLevelIndex = directory.search(/\/[\w ]+\.[\w ]+$/g);
+                    if(lastLevelIndex !== -1) {
+                        directory = filePath.substring(0, lastLevelIndex);
+                        fileName = filePath.substring(lastLevelIndex + 1);
+                    }
+
+                    //we need the directory name w.r.t the root, so remove any slashes in the beginning
+                    if(directory.charAt(0) === "/") {
+                        directory = directory.substring(1);
+                    }
+
+                    var gotFullPath = function(dirEntry) {
+                        var fullFilePath = dirEntry.fullPath + "/" + fileName;
+                        deferred.resolve(fullFilePath);
+                    };
+
+                    var failedToGetFullPath = function(error) {
+                        var str = "There was an error getting the full path of file: " + filePath + " " + JSON.stringify(error);
+                        deferred.reject(new Error(str));
+                    };
+
+                    fs.root.getDirectory(directory, {create: true, exclusive: false}, gotFullPath, failedToGetFullPath);
+                    return deferred.promise;
+                });
+            },
+
+            // returns a promise with a full path to the downloaded file
+            downloadFromUrl : function(url, filePath) {
+                var self = this;
+                return initialiseFileSystem()
+                .then(function(){
+                    return self.ensureDirectoryExists(filePath);
+                })
+                .then(function(){
+                    return self.getFullFilePath(filePath);
+                })
+                .then(function(fullFilePath){
+                    return downloadFromUrl(url, fullFilePath);
+                });
+            },
+
+            //returns a promise with the contents of the file
+            readFileContents : function(fileName) {
+                return initialiseFileSystem()
+                .then(function(){
+                    return getFile(fileName);
+                })
+                .then(function(file){
+                    var deferred = Q.defer();
+
+                    var reader = new $window.FileReader();
+                    reader.onload = function(evt) {
+                        var text = evt.target.result;
+                        deferred.resolve(text);
+                    };
+                    reader.onerror = function(evt) {
+                        deferred.reject(new Error(evt));
+                    };
+                    reader.readAsText(file);
+
+                    return deferred.promise;
+                });
+            },
+
+            //returns a promise with the json contents of the file
+            readJSONFileContents : function(fileName) {
+                return this.readFileContents(fileName)
+                .then(function (text) {
+                    text = trim(text);
+                    var resultJson = {};
+                    if(text) {
+                        resultJson = JSON.parse(text);
+                    }
+                    return resultJson;
+                });
+            },
+
+            //returns a promise when file is written
+            writeFileContents : function(fileName, contents) {
+                return initialiseFileSystem()
+                .then(function(){
+                    return writeToFile(fileName, contents, false /* append */);
+                });
+            },
+
+            //returns a promise when file is appended
+            appendFileContents : function(fileName, contents) {
+                return initialiseFileSystem()
+                .then(function(){
+                    return writeToFile(fileName, contents, true /* append */);
+                });
+            },
+
+            //returns a promise when json file is written
+            writeJSONFileContents : function(fileName, contents) {
+                var stringContents;
+                if(typeof contents === "string") {
+                    stringContents = contents;
+                } else {
+                    stringContents = JSON.stringify(contents);
+                }
+                return this.writeFileContents(fileName, stringContents);
+            },
+
+            deleteDirectory : function(directoryName) {
+                return initialiseFileSystem()
+                .then(function(){
+                    return getDirectoryEntry(directoryName);
+                })
+                .then(function(dirEntry){
+                    var deferred = Q.defer();
+                    var failedToDeleteDirectory = function(error) {
+                        var str = "There was an error deleting the directory: " + directoryName + " " + JSON.stringify(error);
+                        deferred.reject(new Error(str));
+                    };
+                    dirEntry.removeRecursively(deferred.resolve, failedToDeleteDirectory);
+                    return deferred.promise;
+                });
+            },
+
+            extractZipFile : function(fileName, outputDirectory){
+                var deferred = Q.defer();
+
+                //will throw an exception if the zip plugin is not loaded
+                try {
+                    var onZipDone = function(returnCode) {
+                        if(returnCode !== 0) {
+                            deferred.reject(new Error("Something went wrong during the unzipping of: " + fileName));
+                        } else {
+                            deferred.resolve();
+                        }
+                    };
+
+                    /* global zip */
+                    zip.unzip(fileName, outputDirectory, onZipDone);
+                } catch(e) {
+                    deferred.reject(e);
+                } finally {
+                    return deferred.promise;
+                }
+            },
+
+            xhrGet : function(url) {
+                var deferred = Q.defer();
+                var xhr = new XMLHttpRequest();
+                xhr.onreadystatechange = function() {
+                    if (xhr.readyState === 4) {
+                        if(xhr.status === 200) {
+                            deferred.resolve(xhr);
+                        } else {
+                            deferred.reject("XHR return status: " + xhr.statusText);
+                        }
+                    }
+                };
+                xhr.open("GET", url, true);
+                xhr.send();
+                return deferred.promise;
+            }
+        };
+    }]);
+
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cordova-app-harness/blob/771f02b2/www/cdvah_js/app.js
----------------------------------------------------------------------
diff --git a/www/cdvah_js/app.js b/www/cdvah_js/app.js
new file mode 100644
index 0000000..a2d5488
--- /dev/null
+++ b/www/cdvah_js/app.js
@@ -0,0 +1,11 @@
+var myApp = angular.module("CordovaAppHarness", []);
+myApp.config(["$routeProvider", function($routeProvider){
+    $routeProvider.when("/", {
+        templateUrl: "cdvah_views/list.html",
+        controller: "ListCtrl"
+    });
+    $routeProvider.when("/add", {
+        templateUrl: "cdvah_views/add.html",
+        controller: "AddCtrl"
+    });
+}]);
\ No newline at end of file