Bug 898647 - Backend for app updates. r=myk,fabrice
authorMarco Castelluccio <mar.castelluccio@studenti.unina.it>
Sat, 15 Mar 2014 14:37:37 -0700
changeset 173794 e9bff15a8d19bed540510b8230f71a1f10cb4a55
parent 173793 5a675f0c49fb20db22e4592d79ff635051ffe1cb
child 173795 c8c75db14c4ef7ab40df23e3c59bb028b0faf60c
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmyk, fabrice
bugs898647
milestone30.0a1
Bug 898647 - Backend for app updates. r=myk,fabrice
browser/components/nsBrowserGlue.js
browser/installer/package-manifest.in
browser/modules/WebappManager.jsm
browser/modules/moz.build
browser/modules/webappsUI.jsm
dom/apps/src/AppsUtils.jsm
dom/apps/src/Webapps.jsm
dom/tests/browser/browser_webapps_permissions.js
toolkit/webapps/LinuxNativeApp.js
toolkit/webapps/MacNativeApp.js
toolkit/webapps/NativeApp.jsm
toolkit/webapps/WebappOSUtils.jsm
toolkit/webapps/WebappsInstaller.jsm
toolkit/webapps/WinNativeApp.js
toolkit/webapps/moz.build
toolkit/webapps/tests/chrome.ini
toolkit/webapps/tests/head.js
toolkit/webapps/tests/test_hosted.xul
toolkit/webapps/tests/test_packaged.xul
webapprt/Startup.jsm
webapprt/WebappManager.jsm
webapprt/WebappRT.jsm
webapprt/WebappsHandler.jsm
webapprt/moz.build
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -33,18 +33,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
                                   "resource://gre/modules/BookmarkHTMLUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
                                   "resource://gre/modules/BookmarkJSONUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "webappsUI",
-                                  "resource:///modules/webappsUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
+                                  "resource:///modules/WebappManager.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
                                   "resource://gre/modules/PageThumbs.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserNewTabPreloader",
@@ -463,17 +463,17 @@ BrowserGlue.prototype = {
     // prefs are applied in _onAppDefaults()
     this._distributionCustomizer.applyCustomizations();
 
     // handle any UI migration
     this._migrateUI();
 
     this._syncSearchEngines();
 
-    webappsUI.init();
+    WebappManager.init();
     PageThumbs.init();
     NewTabUtils.init();
     BrowserNewTabPreloader.init();
     SignInToWebsiteUX.init();
     PdfJs.init();
 #ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
 #endif
@@ -649,17 +649,17 @@ BrowserGlue.prototype = {
                          .getService(Ci.nsIAppStartup);
       appStartup.trackStartupCrashEnd();
     } catch (e) {
       Cu.reportError("Could not end startup crash tracking in quit-application-granted: " + e);
     }
 
     BrowserNewTabPreloader.uninit();
     CustomizationTabPreloader.uninit();
-    webappsUI.uninit();
+    WebappManager.uninit();
     SignInToWebsiteUX.uninit();
     webrtcUI.uninit();
   },
 
   // All initial windows have opened.
   _onWindowsRestored: function BG__onWindowsRestored() {
     // Show update notification, if needed.
     if (Services.prefs.prefHasUserValue("app.update.postupdate"))
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -793,17 +793,17 @@ bin/libfreebl_32int64_3.so
 @BINPATH@/webapprt/components/CommandLineHandler.js
 @BINPATH@/webapprt/components/ContentPermission.js
 @BINPATH@/webapprt/components/DirectoryProvider.js
 @BINPATH@/webapprt/components/PaymentUIGlue.js
 @BINPATH@/webapprt/components/components.manifest
 @BINPATH@/webapprt/defaults/preferences/prefs.js
 @BINPATH@/webapprt/modules/Startup.jsm
 @BINPATH@/webapprt/modules/WebappRT.jsm
-@BINPATH@/webapprt/modules/WebappsHandler.jsm
+@BINPATH@/webapprt/modules/WebappManager.jsm
 @BINPATH@/webapprt/modules/RemoteDebugger.jsm
 @BINPATH@/webapprt/modules/WebRTCHandler.jsm
 #endif
 
 #ifdef MOZ_METRO
 @BINPATH@/components/MetroUIUtils.js
 @BINPATH@/components/MetroUIUtils.manifest
 [metro]
rename from browser/modules/webappsUI.jsm
rename to browser/modules/WebappManager.jsm
--- a/browser/modules/webappsUI.jsm
+++ b/browser/modules/WebappManager.jsm
@@ -1,45 +1,45 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = ["webappsUI"];
+this.EXPORTED_SYMBOLS = ["WebappManager"];
 
 let Ci = Components.interfaces;
 let Cc = Components.classes;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
-Cu.import("resource://gre/modules/WebappsInstaller.jsm");
+Cu.import("resource://gre/modules/NativeApp.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                    "@mozilla.org/childprocessmessagemanager;1",
                                    "nsIMessageSender");
 
-this.webappsUI = {
+this.WebappManager = {
   // List of promises for in-progress installations
   installations: {},
 
-  init: function webappsUI_init() {
+  init: function() {
     Services.obs.addObserver(this, "webapps-ask-install", false);
     Services.obs.addObserver(this, "webapps-launch", false);
     Services.obs.addObserver(this, "webapps-uninstall", false);
     cpmm.addMessageListener("Webapps:Install:Return:OK", this);
     cpmm.addMessageListener("Webapps:Install:Return:KO", this);
     cpmm.addMessageListener("Webapps:UpdateState", this);
   },
 
-  uninit: function webappsUI_uninit() {
+  uninit: function() {
     Services.obs.removeObserver(this, "webapps-ask-install");
     Services.obs.removeObserver(this, "webapps-launch");
     Services.obs.removeObserver(this, "webapps-uninstall");
     cpmm.removeMessageListener("Webapps:Install:Return:OK", this);
     cpmm.removeMessageListener("Webapps:Install:Return:KO", this);
     cpmm.removeMessageListener("Webapps:UpdateState", this);
   },
 
@@ -66,17 +66,17 @@ this.webappsUI = {
       if (!manifest.appcache_path) {
         this.installations[manifestURL].resolve();
       }
     } else if (aMessage.name == "Webapps:Install:Return:KO") {
       this.installations[manifestURL].reject(data.error);
     }
   },
 
-  observe: function webappsUI_observe(aSubject, aTopic, aData) {
+  observe: function(aSubject, aTopic, aData) {
     let data = JSON.parse(aData);
     data.mm = aSubject;
 
     switch(aTopic) {
       case "webapps-ask-install":
         let win = this._getWindowForId(data.oid);
         if (win && win.location.href == data.from) {
           this.doInstall(data, win);
@@ -103,16 +103,18 @@ this.webappsUI = {
                          .chromeEventHandler;
     let chromeDoc = browser.ownerDocument;
     let chromeWin = chromeDoc.defaultView;
     let popupProgressContent =
       chromeDoc.getElementById("webapps-install-progress-content");
 
     let bundle = chromeWin.gNavigatorBundle;
 
+    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
+
     let notification;
 
     let mainAction = {
       label: bundle.getString("webapps.install"),
       accessKey: bundle.getString("webapps.install.accesskey"),
       callback: () => {
         notification.remove();
 
@@ -123,62 +125,60 @@ this.webappsUI = {
                              "webapps-notification-icon");
 
         let progressMeter = chromeDoc.createElement("progressmeter");
         progressMeter.setAttribute("mode", "undetermined");
         popupProgressContent.appendChild(progressMeter);
 
         let manifestURL = aData.app.manifestURL;
 
-        let cleanup = (ex) => {
+        let cleanup = () => {
           popupProgressContent.removeChild(progressMeter);
           delete this.installations[manifestURL];
           if (Object.getOwnPropertyNames(this.installations).length == 0) {
             notification.remove();
           }
         };
 
         this.installations[manifestURL] = Promise.defer();
         this.installations[manifestURL].promise.then(null, (error) => {
           Cu.reportError("Error installing webapp: " + error);
           cleanup();
         });
 
-        let app = WebappsInstaller.init(aData);
-
-        if (app) {
-          let localDir = null;
-          if (app.appProfile) {
-            localDir = app.appProfile.localDir;
-          }
-
-          DOMApplicationRegistry.confirmInstall(aData, localDir,
-            (aManifest, aZipPath) => {
-              Task.spawn(function() {
-                try {
-                  yield WebappsInstaller.install(aData, aManifest, aZipPath);
-                  yield this.installations[manifestURL].promise;
-                  installationSuccessNotification(aData, app, bundle);
-                } catch (ex) {
-                  Cu.reportError("Error installing webapp: " + ex);
-                  // TODO: Notify user that the installation has failed
-                } finally {
-                  cleanup();
-                }
-              }.bind(this));
-            });
-        } else {
+        let nativeApp = new NativeApp(aData.app, jsonManifest,
+                                      aData.app.categories);
+        let localDir;
+        try {
+          localDir = nativeApp.createProfile();
+        } catch (ex) {
+          Cu.reportError("Error installing webapp: " + ex);
           DOMApplicationRegistry.denyInstall(aData);
           cleanup();
+          return;
         }
+
+        DOMApplicationRegistry.confirmInstall(aData, localDir,
+          (aManifest, aZipPath) => Task.spawn((function*() {
+            try {
+              yield nativeApp.install(aManifest, aZipPath);
+              yield this.installations[manifestURL].promise;
+              notifyInstallSuccess(aData.app, nativeApp, bundle);
+            } catch (ex) {
+              Cu.reportError("Error installing webapp: " + ex);
+              // TODO: Notify user that the installation has failed
+            } finally {
+              cleanup();
+            }
+          }).bind(this))
+        );
       }
     };
 
     let requestingURI = chromeWin.makeURI(aData.from);
-    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
     let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
 
     let host;
     try {
       host = requestingURI.host;
     } catch(e) {
       host = requestingURI.spec;
     }
@@ -190,27 +190,27 @@ this.webappsUI = {
                                                      "webapps-install",
                                                      message,
                                                      "webapps-notification-icon",
                                                      mainAction);
 
   }
 }
 
-function installationSuccessNotification(aData, app, aBundle) {
+function notifyInstallSuccess(aApp, aNativeApp, aBundle) {
   let launcher = {
     observe: function(aSubject, aTopic) {
       if (aTopic == "alertclickcallback") {
-        WebappOSUtils.launch(aData.app);
+        WebappOSUtils.launch(aApp);
       }
     }
   };
 
   try {
     let notifier = Cc["@mozilla.org/alerts-service;1"].
                    getService(Ci.nsIAlertsService);
 
-    notifier.showAlertNotification(app.iconURI.spec,
+    notifier.showAlertNotification(aNativeApp.iconURI.spec,
                                    aBundle.getString("webapps.install.success"),
-                                   app.appNameAsFilename,
+                                   aNativeApp.appNameAsFilename,
                                    true, null, launcher);
   } catch (ex) {}
 }
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -15,17 +15,17 @@ EXTRA_JS_MODULES += [
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'SharedFrame.jsm',
     'SignInToWebsite.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
     'TabCrashReporter.jsm',
-    'webappsUI.jsm',
+    'WebappManager.jsm',
     'webrtcUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [
         'WindowsJumpLists.jsm',
         'WindowsPreviewPerTab.jsm',
     ]
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -203,17 +203,17 @@ this.AppsUtils = {
     // so we can't use the 'removable' property for isCoreApp
     // Instead, we check if the app is installed under /system/b2g
     let isCoreApp = false;
 
 #ifdef MOZ_WIDGET_GONK
     isCoreApp = app.basePath == this.getCoreAppsBasePath();
 #endif
     debug(app.basePath + " isCoreApp: " + isCoreApp);
-    return { "path":  WebappOSUtils.getInstallPath(app),
+    return { "path": WebappOSUtils.getPackagePath(app),
              "isCoreApp": isCoreApp };
   },
 
   /**
     * Remove potential HTML tags from displayable fields in the manifest.
     * We check name, description, developer name, and permission description
     */
   sanitizeManifest: function(aManifest) {
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -72,16 +72,18 @@ function supportSystemMessages() {
   return Services.prefs.getBoolPref("dom.sysmsg.enabled");
 }
 
 // Minimum delay between two progress events while downloading, in ms.
 const MIN_PROGRESS_EVENT_DELAY = 1500;
 
 const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
 
+const chromeWindowType = WEBAPP_RUNTIME ? "webapprt:webapp" : "navigator:browser";
+
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
                                    "@mozilla.org/parentprocessmessagemanager;1",
                                    "nsIMessageBroadcaster");
@@ -128,16 +130,17 @@ XPCOMUtils.defineLazyGetter(this, "updat
 const STORE_ID_PENDING_PREFIX = "#unknownID#";
 
 this.DOMApplicationRegistry = {
   // Path to the webapps.json file where we store the registry data.
   appsFile: null,
   webapps: { },
   children: [ ],
   allAppsLaunchable: false,
+  _updateHandlers: [ ],
 
   init: function() {
     this.messages = ["Webapps:Install", "Webapps:Uninstall",
                      "Webapps:GetSelf", "Webapps:CheckInstalled",
                      "Webapps:GetInstalled", "Webapps:GetNotInstalled",
                      "Webapps:Launch", "Webapps:GetAll",
                      "Webapps:InstallPackage",
                      "Webapps:GetList", "Webapps:RegisterForMessages",
@@ -1191,16 +1194,33 @@ this.DOMApplicationRegistry = {
     if (!(aMsgName in this.children)) {
       return;
     }
     this.children[aMsgName].forEach(function(mmRef) {
       mmRef.mm.sendAsyncMessage(aMsgName, aContent);
     });
   },
 
+  registerUpdateHandler: function(aHandler) {
+    this._updateHandlers.push(aHandler);
+  },
+
+  unregisterUpdateHandler: function(aHandler) {
+    let index = this._updateHandlers.indexOf(aHandler);
+    if (index != -1) {
+      this._updateHandlers.splice(index, 1);
+    }
+  },
+
+  notifyUpdateHandlers: function(aApp, aManifest, aZipPath) {
+    for (let updateHandler of this._updateHandlers) {
+      updateHandler(aApp, aManifest, aZipPath);
+    }
+  },
+
   _getAppDir: function(aId) {
     return FileUtils.getDir(DIRECTORY_NAME, ["webapps", aId], true, true);
   },
 
   _writeFile: function(aPath, aData) {
     debug("Saving " + aPath);
 
     return OS.File.writeAtomic(aPath,
@@ -1482,16 +1502,23 @@ this.DOMApplicationRegistry = {
           delete app.staged;
         }
 
         delete app.retryingDownload;
 
         this._saveApps().then(() => {
           // Update the handlers and permissions for this app.
           this.updateAppHandlers(aOldManifest, aData, app);
+
+          this._loadJSONAsync(staged.path).then((aUpdateManifest) => {
+            let appObject = AppsUtils.cloneAppObject(app);
+            appObject.updateManifest = aUpdateManifest;
+            this.notifyUpdateHandlers(appObject, aData, appFile.path);
+          });
+
           if (supportUseCurrentProfile()) {
             PermissionsInstaller.installPermissions(
               { manifest: aData,
                 origin: app.origin,
                 manifestURL: app.manifestURL },
               true);
           }
           this.updateDataStore(this.webapps[id].localId, app.origin,
@@ -1892,16 +1919,18 @@ this.DOMApplicationRegistry = {
     }
 
     aApp.manifest = aNewManifest || aOldManifest;
 
     let manifest;
     if (aNewManifest) {
       this.updateAppHandlers(aOldManifest, aNewManifest, aApp);
 
+      this.notifyUpdateHandlers(AppsUtils.cloneAppObject(aApp), aNewManifest);
+
       // Store the new manifest.
       let dir = this._getAppDir(aId).path;
       let manFile = OS.Path.join(dir, "manifest.webapp");
       this._writeFile(manFile, JSON.stringify(aNewManifest));
       manifest = new ManifestHelper(aNewManifest, aApp.origin);
 
       if (supportUseCurrentProfile()) {
         // Update the permissions for this app.
@@ -2664,17 +2693,17 @@ onInstallSuccessAck: function onInstallS
       aOnSuccess,
       this._revertDownloadPackage.bind(this, id, oldApp, aNewApp, aIsUpdate)
     );
   },
 
   _ensureSufficientStorage: function(aNewApp) {
     let deferred = Promise.defer();
 
-    let navigator = Services.wm.getMostRecentWindow("navigator:browser")
+    let navigator = Services.wm.getMostRecentWindow(chromeWindowType)
                             .navigator;
     let deviceStorage = null;
 
     if (navigator.getDeviceStorage) {
       deviceStorage = navigator.getDeviceStorage("apps");
     }
 
     if (deviceStorage) {
--- a/dom/tests/browser/browser_webapps_permissions.js
+++ b/dom/tests/browser/browser_webapps_permissions.js
@@ -58,17 +58,17 @@ function test() {
   var browser = gBrowser.selectedBrowser;
   PopupNotifications.panel.addEventListener("popupshown", handlePopup, false);
 
   registerCleanupFunction(function () {
     gWindow = null;
     gBrowser.removeTab(tab);
 
     // The installation may have created a XUL alert window
-    // (see webappsUI.installationSuccessNotification).
+    // (see notifyInstallSuccess in WebappManager.jsm).
     // It need to be closed before the test finishes.
     var browsers = windowMediator.getEnumerator('alert:alert');
       while (browsers.hasMoreElements()) {
       browsers.getNext().close();
     }
   });
 
   browser.addEventListener("DOMContentLoaded", function onLoad(event) {
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/LinuxNativeApp.js
@@ -0,0 +1,335 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Constructor for the Linux native app shell
+ *
+ * @param aApp {Object} the app object provided to the install function
+ * @param aManifest {Object} the manifest data provided by the web app
+ * @param aCategories {Array} array of app categories
+ * @param aRegistryDir {String} (optional) path to the registry
+ */
+function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
+  CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
+
+  this.iconFile = "icon.png";
+  this.webapprt = "webapprt-stub";
+  this.configJson = "webapp.json";
+  this.webappINI = "webapp.ini";
+  this.zipFile = "application.zip";
+
+  this.backupFiles = [ this.iconFile, this.configJson, this.webappINI ];
+  if (this.isPackaged) {
+    this.backupFiles.push(this.zipFile);
+  }
+
+  let xdg_data_home = Cc["@mozilla.org/process/environment;1"].
+                      getService(Ci.nsIEnvironment).
+                      get("XDG_DATA_HOME");
+  if (!xdg_data_home) {
+    xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share");
+  }
+
+  // The desktop file name is: "owa-" + sanitized app name +
+  // "-" + manifest url hash.
+  this.desktopINI = OS.Path.join(xdg_data_home, "applications",
+                                 "owa-" + this.uniqueName + ".desktop");
+}
+
+NativeApp.prototype = {
+  __proto__: CommonNativeApp.prototype,
+
+  /**
+   * Creates a native installation of the web app in the OS
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  install: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    // If the application is already installed, this is a reinstallation.
+    if (WebappOSUtils.getInstallPath(this.app)) {
+      return yield this.prepareUpdate(aManifest, aZipPath);
+    }
+
+    this._setData(aManifest);
+
+    // The installation directory name is: sanitized app name +
+    // "-" + manifest url hash.
+    let installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName);
+
+    let dir = getFile(TMP_DIR, this.uniqueName);
+    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+    let tmpDir = dir.path;
+
+    // Create the installation in a temporary directory.
+    try {
+      this._copyPrebuiltFiles(tmpDir);
+      yield this._createConfigFiles(tmpDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
+      }
+
+      yield this._getIcon(tmpDir);
+    } catch (ex) {
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+
+    // Apply the installation.
+    this._removeInstallation(true, installDir);
+
+    try {
+      yield this._applyTempInstallation(tmpDir, installDir);
+    } catch (ex) {
+      this._removeInstallation(false, installDir);
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Creates an update in a temporary directory to be applied later.
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  prepareUpdate: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    this._setData(aManifest);
+
+    let installDir = WebappOSUtils.getInstallPath(this.app);
+    if (!installDir) {
+      throw ERR_NOT_INSTALLED;
+    }
+
+    let baseName = OS.Path.basename(installDir)
+    let oldUniqueName = baseName.substring(1, baseName.length);
+    if (this.uniqueName != oldUniqueName) {
+      // Bug 919799: If the app is still in the registry, migrate its data to
+      // the new format.
+      throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
+    }
+
+    let updateDir = OS.Path.join(installDir, "update");
+    yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(updateDir);
+
+    try {
+      yield this._createConfigFiles(updateDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
+      }
+
+      yield this._getIcon(updateDir);
+    } catch (ex) {
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Applies an update.
+   */
+  applyUpdate: Task.async(function*() {
+    if (this._dryRun) {
+      return;
+    }
+
+    let installDir = WebappOSUtils.getInstallPath(this.app);
+    let updateDir = OS.Path.join(installDir, "update");
+
+    let backupDir = yield this._backupInstallation(installDir);
+
+    try {
+      yield this._applyTempInstallation(updateDir, installDir);
+    } catch (ex) {
+      yield this._restoreInstallation(backupDir, installDir);
+      throw ex;
+    } finally {
+      yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    }
+  }),
+
+  _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
+    yield moveDirectory(aTmpDir, aInstallDir);
+
+    this._createSystemFiles(aInstallDir);
+  }),
+
+  _removeInstallation: function(keepProfile, aInstallDir) {
+    let filesToRemove = [this.desktopINI];
+
+    if (keepProfile) {
+      for (let filePath of this.backupFiles) {
+        filesToRemove.push(OS.Path.join(aInstallDir, filePath));
+      }
+
+      filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
+    } else {
+      filesToRemove.push(aInstallDir);
+    }
+
+    removeFiles(filesToRemove);
+  },
+
+  _backupInstallation: Task.async(function*(aInstallDir) {
+    let backupDir = OS.Path.join(aInstallDir, "backup");
+    yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(backupDir);
+
+    for (let filePath of this.backupFiles) {
+      yield OS.File.move(OS.Path.join(aInstallDir, filePath),
+                         OS.Path.join(backupDir, filePath));
+    }
+
+    return backupDir;
+  }),
+
+  _restoreInstallation: function(aBackupDir, aInstallDir) {
+    return moveDirectory(aBackupDir, aInstallDir);
+  },
+
+  _copyPrebuiltFiles: function(aDir) {
+    let destDir = getFile(aDir);
+    let stub = getFile(this.runtimeFolder, this.webapprt);
+    stub.copyTo(destDir, null);
+  },
+
+  /**
+   * Translate marketplace categories to freedesktop.org categories.
+   *
+   * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry
+   *
+   * @return an array of categories
+   */
+  _translateCategories: function() {
+    let translations = {
+      "books": "Education;Literature",
+      "business": "Finance",
+      "education": "Education",
+      "entertainment": "Amusement",
+      "sports": "Sports",
+      "games": "Game",
+      "health-fitness": "MedicalSoftware",
+      "lifestyle": "Amusement",
+      "music": "Audio;Music",
+      "news-weather": "News",
+      "photo-video": "Video;AudioVideo;Photography",
+      "productivity": "Office",
+      "shopping": "Amusement",
+      "social": "Chat",
+      "travel": "Amusement",
+      "reference": "Science;Education;Documentation",
+      "maps-navigation": "Maps",
+      "utilities": "Utility"
+    };
+
+    // The trailing semicolon is needed as written in the freedesktop specification
+    let categories = "";
+    for (let category of this.categories) {
+      let catLower = category.toLowerCase();
+      if (catLower in translations) {
+        categories += translations[catLower] + ";";
+      }
+    }
+
+    return categories;
+  },
+
+  _createConfigFiles: function(aDir) {
+    // ${InstallDir}/webapp.json
+    yield writeToFile(OS.Path.join(aDir, this.configJson),
+                      JSON.stringify(this.webappJson));
+
+    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
+
+    // ${InstallDir}/webapp.ini
+    let webappINIfile = getFile(aDir, this.webappINI);
+
+    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
+                 getService(Ci.nsIINIParserFactory).
+                 createINIParser(webappINIfile).
+                 QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("Webapp", "Name", this.appName);
+    writer.setString("Webapp", "Profile", this.uniqueName);
+    writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1));
+    writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
+    writer.writeFile();
+  },
+
+  _createSystemFiles: function(aInstallDir) {
+    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
+
+    let webapprtPath = OS.Path.join(aInstallDir, this.webapprt);
+
+    // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop
+    let desktopINIfile = getFile(this.desktopINI);
+
+    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
+                 getService(Ci.nsIINIParserFactory).
+                 createINIParser(desktopINIfile).
+                 QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("Desktop Entry", "Name", this.appName);
+    writer.setString("Desktop Entry", "Comment", this.shortDescription);
+    writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"');
+    writer.setString("Desktop Entry", "Icon", OS.Path.join(aInstallDir,
+                                                           this.iconFile));
+    writer.setString("Desktop Entry", "Type", "Application");
+    writer.setString("Desktop Entry", "Terminal", "false");
+
+    let categories = this._translateCategories();
+    if (categories)
+      writer.setString("Desktop Entry", "Categories", categories);
+
+    writer.setString("Desktop Entry", "Actions", "Uninstall;");
+    writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label"));
+    writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove");
+
+    writer.writeFile();
+
+    desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR;
+  },
+
+  /**
+   * Process the icon from the imageStream as retrieved from
+   * the URL by getIconForApp().
+   *
+   * @param aMimeType     ahe icon mimetype
+   * @param aImageStream  the stream for the image data
+   * @param aDir          the directory where the icon should be stored
+   */
+  _processIcon: function(aMimeType, aImageStream, aDir) {
+    let deferred = Promise.defer();
+
+    let imgTools = Cc["@mozilla.org/image/tools;1"].
+                   createInstance(Ci.imgITools);
+
+    let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
+    let iconStream = imgTools.encodeImage(imgContainer, "image/png");
+
+    let iconFile = getFile(aDir, this.iconFile);
+    let outputStream = FileUtils.openSafeFileOutputStream(iconFile);
+    NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
+      if (Components.isSuccessCode(aResult)) {
+        deferred.resolve();
+      } else {
+        deferred.reject("Failure copying icon: " + aResult);
+      }
+    });
+
+    return deferred.promise;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/MacNativeApp.js
@@ -0,0 +1,308 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const USER_LIB_DIR = OS.Constants.Path.macUserLibDir;
+const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir;
+
+/**
+ * Constructor for the Mac native app shell
+ *
+ * @param aApp {Object} the app object provided to the install function
+ * @param aManifest {Object} the manifest data provided by the web app
+ * @param aCategories {Array} array of app categories
+ * @param aRegistryDir {String} (optional) path to the registry
+ */
+function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
+  CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
+
+  // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash
+  this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support",
+                                    this.uniqueName);
+  this.configJson = "webapp.json";
+
+  this.contentsDir = "Contents";
+  this.macOSDir = OS.Path.join(this.contentsDir, "MacOS");
+  this.resourcesDir = OS.Path.join(this.contentsDir, "Resources");
+  this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns");
+  this.zipFile = OS.Path.join(this.resourcesDir, "application.zip");
+}
+
+NativeApp.prototype = {
+  __proto__: CommonNativeApp.prototype,
+
+  /**
+   * Creates a native installation of the web app in the OS
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  install: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    // If the application is already installed, this is a reinstallation.
+    if (WebappOSUtils.getInstallPath(this.app)) {
+      return yield this.prepareUpdate(aManifest, aZipPath);
+    }
+
+    this._setData(aManifest);
+
+    let localAppDir = getFile(LOCAL_APP_DIR);
+    if (!localAppDir.isWritable()) {
+      throw("Not enough privileges to install apps");
+    }
+ 
+    let destinationName = yield getAvailableFileName([ LOCAL_APP_DIR ],
+                                                     this.appNameAsFilename,
+                                                     ".app");
+
+    let installDir = OS.Path.join(LOCAL_APP_DIR, destinationName);
+
+    let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app");
+    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+    let tmpDir = dir.path;
+
+    try {
+      yield this._createDirectoryStructure(tmpDir);
+      this._copyPrebuiltFiles(tmpDir);
+      yield this._createConfigFiles(tmpDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
+      }
+
+      yield this._getIcon(tmpDir);
+    } catch (ex) {
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+
+    this._removeInstallation(true, installDir);
+
+    try {
+      // Move the temp installation directory to the /Applications directory
+      yield this._applyTempInstallation(tmpDir, installDir);
+    } catch (ex) {
+      this._removeInstallation(false, installDir);
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Creates an update in a temporary directory to be applied later.
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  prepareUpdate: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    this._setData(aManifest);
+
+    let [ oldUniqueName, installDir ] = WebappOSUtils.getLaunchTarget(this.app);
+    if (!installDir) {
+      throw ERR_NOT_INSTALLED;
+    }
+
+    if (this.uniqueName != oldUniqueName) {
+      // Bug 919799: If the app is still in the registry, migrate its data to
+      // the new format.
+      throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
+    }
+
+    let updateDir = OS.Path.join(installDir, "update");
+    yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(updateDir);
+
+    try {
+      yield this._createDirectoryStructure(updateDir);
+      this._copyPrebuiltFiles(updateDir);
+      yield this._createConfigFiles(updateDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
+      }
+
+      yield this._getIcon(updateDir);
+    } catch (ex) {
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Applies an update.
+   */
+  applyUpdate: Task.async(function*() {
+    if (this._dryRun) {
+      return;
+    }
+
+    let installDir = WebappOSUtils.getInstallPath(this.app);
+    let updateDir = OS.Path.join(installDir, "update");
+
+    let backupDir = yield this._backupInstallation(installDir);
+
+    try {
+      // Move the update directory to the /Applications directory
+      yield this._applyTempInstallation(updateDir, installDir);
+    } catch (ex) {
+      yield this._restoreInstallation(backupDir, installDir);
+      throw ex;
+    } finally {
+      yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    }
+  }),
+
+  _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
+    yield OS.File.move(OS.Path.join(aTmpDir, this.configJson),
+                       OS.Path.join(this.appProfileDir, this.configJson));
+
+    yield moveDirectory(aTmpDir, aInstallDir);
+  }),
+
+  _removeInstallation: function(keepProfile, aInstallDir) {
+    let filesToRemove = [ aInstallDir ];
+
+    if (!keepProfile) {
+      filesToRemove.push(this.appProfileDir);
+    }
+
+    removeFiles(filesToRemove);
+  },
+
+  _backupInstallation: Task.async(function*(aInstallDir) {
+    let backupDir = OS.Path.join(aInstallDir, "backup");
+    yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(backupDir);
+
+    yield moveDirectory(OS.Path.join(aInstallDir, this.contentsDir),
+                        backupDir);
+    yield OS.File.move(OS.Path.join(this.appProfileDir, this.configJson),
+                       OS.Path.join(backupDir, this.configJson));
+
+    return backupDir;
+  }),
+
+  _restoreInstallation: Task.async(function*(aBackupDir, aInstallDir) {
+    yield OS.File.move(OS.Path.join(aBackupDir, this.configJson),
+                       OS.Path.join(this.appProfileDir, this.configJson));
+    yield moveDirectory(aBackupDir,
+                        OS.Path.join(aInstallDir, this.contentsDir));
+  }),
+
+  _createDirectoryStructure: Task.async(function*(aDir) {
+    yield OS.File.makeDir(this.appProfileDir,
+                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
+
+    yield OS.File.makeDir(OS.Path.join(aDir, this.contentsDir),
+                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
+
+    yield OS.File.makeDir(OS.Path.join(aDir, this.macOSDir),
+                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
+
+    yield OS.File.makeDir(OS.Path.join(aDir, this.resourcesDir),
+                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
+  }),
+
+  _copyPrebuiltFiles: function(aDir) {
+    let destDir = getFile(aDir, this.macOSDir);
+    let stub = getFile(this.runtimeFolder, "webapprt-stub");
+    stub.copyTo(destDir, "webapprt");
+  },
+
+  _createConfigFiles: function(aDir) {
+    // ${ProfileDir}/webapp.json
+    yield writeToFile(OS.Path.join(aDir, this.configJson),
+                      JSON.stringify(this.webappJson));
+
+    // ${InstallDir}/Contents/MacOS/webapp.ini
+    let applicationINI = getFile(aDir, this.macOSDir, "webapp.ini");
+
+    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
+                 getService(Ci.nsIINIParserFactory).
+                 createINIParser(applicationINI).
+                 QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("Webapp", "Name", this.appName);
+    writer.setString("Webapp", "Profile", this.uniqueName);
+    writer.writeFile();
+    applicationINI.permissions = PERMS_FILE;
+
+    // ${InstallDir}/Contents/Info.plist
+    let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\
+<plist version="1.0">\n\
+  <dict>\n\
+    <key>CFBundleDevelopmentRegion</key>\n\
+    <string>English</string>\n\
+    <key>CFBundleDisplayName</key>\n\
+    <string>' + escapeXML(this.appName) + '</string>\n\
+    <key>CFBundleExecutable</key>\n\
+    <string>webapprt</string>\n\
+    <key>CFBundleIconFile</key>\n\
+    <string>appicon</string>\n\
+    <key>CFBundleIdentifier</key>\n\
+    <string>' + escapeXML(this.uniqueName) + '</string>\n\
+    <key>CFBundleInfoDictionaryVersion</key>\n\
+    <string>6.0</string>\n\
+    <key>CFBundleName</key>\n\
+    <string>' + escapeXML(this.appName) + '</string>\n\
+    <key>CFBundlePackageType</key>\n\
+    <string>APPL</string>\n\
+    <key>CFBundleVersion</key>\n\
+    <string>0</string>\n\
+    <key>NSHighResolutionCapable</key>\n\
+    <true/>\n\
+    <key>NSPrincipalClass</key>\n\
+    <string>GeckoNSApplication</string>\n\
+    <key>FirefoxBinary</key>\n\
+#expand     <string>__MOZ_MACBUNDLE_ID__</string>\n\
+  </dict>\n\
+</plist>';
+
+    yield writeToFile(OS.Path.join(aDir, this.contentsDir, "Info.plist"),
+                      infoPListContent);
+  },
+
+  /**
+   * Process the icon from the imageStream as retrieved from
+   * the URL by getIconForApp(). This will bundle the icon to the
+   * app package at Contents/Resources/appicon.icns.
+   *
+   * @param aMimeType     the icon mimetype
+   * @param aImageStream  the stream for the image data
+   * @param aDir          the directory where the icon should be stored
+   */
+  _processIcon: function(aMimeType, aIcon, aDir) {
+    let deferred = Promise.defer();
+
+    function conversionDone(aSubject, aTopic) {
+      if (aTopic == "process-finished") {
+        deferred.resolve();
+      } else {
+        deferred.reject("Failure converting icon, exit code: " + aSubject.exitValue);
+      }
+    }
+
+    let process = Cc["@mozilla.org/process/util;1"].
+                  createInstance(Ci.nsIProcess);
+    let sipsFile = getFile("/usr/bin/sips");
+
+    process.init(sipsFile);
+    process.runAsync(["-s", "format", "icns",
+                      aIcon.path,
+                      "--out", OS.Path.join(aDir, this.iconFile),
+                      "-z", "128", "128"],
+                      9, conversionDone);
+
+    return deferred.promise;
+  }
+}
rename from toolkit/webapps/WebappsInstaller.jsm
rename to toolkit/webapps/NativeApp.jsm
--- a/toolkit/webapps/WebappsInstaller.jsm
+++ b/toolkit/webapps/NativeApp.jsm
@@ -1,158 +1,109 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-this.EXPORTED_SYMBOLS = ["WebappsInstaller"];
+this.EXPORTED_SYMBOLS = ["NativeApp"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
+const ERR_NOT_INSTALLED = "The application isn't installed";
+const ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME =
+  "Updates for apps installed with the old naming scheme unsupported";
+
 // 0755
 const PERMS_DIRECTORY = OS.Constants.libc.S_IRWXU |
                         OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IXGRP |
                         OS.Constants.libc.S_IROTH | OS.Constants.libc.S_IXOTH;
 
 // 0644
 const PERMS_FILE = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR |
                    OS.Constants.libc.S_IRGRP |
                    OS.Constants.libc.S_IROTH;
 
 const DESKTOP_DIR = OS.Constants.Path.desktopDir;
 const HOME_DIR = OS.Constants.Path.homeDir;
 const TMP_DIR = OS.Constants.Path.tmpDir;
 
-this.WebappsInstaller = {
-  shell: null,
-
-  /**
-   * Initializes the app object that takes care of the installation
-   * and creates the profile directory for an application
-   *
-   * @param aData the data provided to the install function
-   *
-   * @returns NativeApp on success, null on error
-   */
-  init: function(aData) {
-#ifdef XP_WIN
-    this.shell = new WinNativeApp(aData);
-#elifdef XP_MACOSX
-    this.shell = new MacNativeApp(aData);
-#elifdef XP_UNIX
-    this.shell = new LinuxNativeApp(aData);
-#else
-    return null;
-#endif
-
-    try {
-      if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
-        return this.shell;
-      }
-    } catch (ex) {}
-
-    try {
-      this.shell.createAppProfile();
-    } catch (ex) {
-      Cu.reportError("Error installing app: " + ex);
-      return null;
-    }
-
-    return this.shell;
-  },
-
-  /**
-   * Creates a native installation of the web app in the OS
-   *
-   * @param aData the data provided to the install function
-   * @param aManifest the manifest data provided by the web app
-   * @param aZipPath path to the zip file for packaged apps (undefined for
-   *                 hosted apps)
-   */
-  install: function(aData, aManifest, aZipPath) {
-    try {
-      if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
-        return Promise.resolve();
-      }
-    } catch (ex) {}
-
-    this.shell.init(aData, aManifest);
-
-    return this.shell.install(aZipPath).then(() => {
-      let data = {
-        "installDir": this.shell.installDir,
-        "app": {
-          "manifest": aManifest,
-          "origin": aData.app.origin
-        }
-      };
-
-      Services.obs.notifyObservers(null, "webapp-installed", JSON.stringify(data));
-    });
-  }
-}
-
 /**
  * This function implements the common constructor for
  * the Windows, Mac and Linux native app shells. It sets
  * the app unique name. It's meant to be called as
- * NativeApp.call(this, aData) from the platform-specific
+ * CommonNativeApp.call(this, ...) from the platform-specific
  * constructor.
  *
- * @param aData the data object provided to the install function
+ * @param aApp {Object} the app object provided to the install function
+ * @param aManifest {Object} the manifest data provided by the web app
+ * @param aCategories {Array} array of app categories
+ * @param aRegistryDir {String} (optional) path to the registry
  *
  */
-function NativeApp(aData) {
-  let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
-  let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
+function CommonNativeApp(aApp, aManifest, aCategories, aRegistryDir) {
+  let manifest = new ManifestHelper(aManifest, aApp.origin);
 
-  aData.app.name = manifest.name;
-  this.uniqueName = WebappOSUtils.getUniqueName(aData.app);
+  aApp.name = manifest.name;
+  this.uniqueName = WebappOSUtils.getUniqueName(aApp);
 
   this.appName = sanitize(manifest.name);
   this.appNameAsFilename = stripStringForFilename(this.appName);
+
+  if (aApp.updateManifest) {
+    this.isPackaged = true;
+  }
+
+  this.categories = aCategories.slice(0);
+
+  this.registryDir = aRegistryDir || OS.Constants.Path.profileDir;
+
+  this.app = aApp;
+
+  this._dryRun = false;
+  try {
+    if (Services.prefs.getBoolPref("browser.mozApps.installer.dry_run")) {
+      this._dryRun = true;
+    }
+  } catch (ex) {}
 }
 
-NativeApp.prototype = {
+CommonNativeApp.prototype = {
   uniqueName: null,
   appName: null,
   appNameAsFilename: null,
   iconURI: null,
   developerName: null,
   shortDescription: null,
   categories: null,
   webappJson: null,
   runtimeFolder: null,
   manifest: null,
+  registryDir: null,
 
   /**
    * This function reads and parses the data from the app
    * manifest and stores it in the NativeApp object.
    *
-   * @param aData the data object provided to the install function
-   * @param aManifest the manifest data provided by the web app
+   * @param aManifest {Object} the manifest data provided by the web app
    *
    */
-  init: function(aData, aManifest) {
-    let app = this.app = aData.app;
-    let manifest = this.manifest = new ManifestHelper(aManifest,
-                                                      app.origin);
-
-    let origin = Services.io.newURI(app.origin, null, null);
+  _setData: function(aManifest) {
+    let manifest = new ManifestHelper(aManifest, this.app.origin);
+    let origin = Services.io.newURI(this.app.origin, null, null);
 
     let biggestIcon = getBiggestIconURL(manifest.icons);
     try {
       let iconURI = Services.io.newURI(biggestIcon, null, null);
       if (iconURI.scheme == "data") {
         this.iconURI = iconURI;
       }
     } catch (ex) {}
@@ -182,905 +133,119 @@ NativeApp.prototype = {
       let shortDesc = firstLine.length <= 256
                       ? firstLine
                       : firstLine.substr(0, 253) + "…";
       this.shortDescription = sanitize(shortDesc);
     } else {
       this.shortDescription = this.appName;
     }
 
-    this.categories = app.categories.slice(0);
+    if (manifest.version) {
+      this.version = manifest.version;
+    }
 
     this.webappJson = {
       // The app registry is the Firefox profile from which the app
       // was installed.
-      "registryDir": OS.Constants.Path.profileDir,
+      "registryDir": this.registryDir,
       "app": {
         "manifest": aManifest,
-        "origin": app.origin,
-        "manifestURL": app.manifestURL,
-        "installOrigin": app.installOrigin,
-        "categories": app.categories,
-        "receipts": app.receipts,
-        "installTime": app.installTime,
+        "origin": this.app.origin,
+        "manifestURL": this.app.manifestURL,
+        "installOrigin": this.app.installOrigin,
+        "categories": this.categories,
+        "receipts": this.app.receipts,
+        "installTime": this.app.installTime,
       }
     };
 
-    if (app.etag) {
-      this.webappJson.app.etag = app.etag;
+    if (this.app.etag) {
+      this.webappJson.app.etag = this.app.etag;
     }
 
-    if (app.packageEtag) {
-      this.webappJson.app.packageEtag = app.packageEtag;
+    if (this.app.packageEtag) {
+      this.webappJson.app.packageEtag = this.app.packageEtag;
     }
 
-    if (app.updateManifest) {
-      this.webappJson.app.updateManifest = app.updateManifest;
+    if (this.app.updateManifest) {
+      this.webappJson.app.updateManifest = this.app.updateManifest;
     }
 
     this.runtimeFolder = OS.Constants.Path.libDir;
   },
 
   /**
    * This function retrieves the icon for an app.
    * If the retrieving fails, it uses the default chrome icon.
    */
-  getIcon: function() {
+  _getIcon: function(aTmpDir) {
     try {
       // If the icon is in the zip package, we should modify the url
       // to point to the zip file (we can't use the app protocol yet
       // because the app isn't installed yet).
       if (this.iconURI.scheme == "app") {
-        let zipUrl = OS.Path.toFileURI(OS.Path.join(this.tmpInstallDir,
+        let zipUrl = OS.Path.toFileURI(OS.Path.join(aTmpDir,
                                                     "application.zip"));
 
         let filePath = this.iconURI.QueryInterface(Ci.nsIURL).filePath;
 
         this.iconURI = Services.io.newURI("jar:" + zipUrl + "!" + filePath,
                                           null, null);
       }
 
 
       let [ mimeType, icon ] = yield downloadIcon(this.iconURI);
-      yield this.processIcon(mimeType, icon);
+      yield this._processIcon(mimeType, icon, aTmpDir);
     }
     catch(e) {
       Cu.reportError("Failure retrieving icon: " + e);
 
       let iconURI = Services.io.newURI(DEFAULT_ICON_URL, null, null);
 
       let [ mimeType, icon ] = yield downloadIcon(iconURI);
-      yield this.processIcon(mimeType, icon);
+      yield this._processIcon(mimeType, icon, aTmpDir);
 
       // Set the iconURI property so that the user notification will have the
       // correct icon.
       this.iconURI = iconURI;
     }
   },
 
   /**
    * Creates the profile to be used for this app.
    */
-  createAppProfile: function() {
-    let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"]
-                    .getService(Ci.nsIToolkitProfileService);
+  createProfile: function() {
+    if (this._dryRun) {
+      return null;
+    }
+
+    let profSvc = Cc["@mozilla.org/toolkit/profile-service;1"].
+                  getService(Ci.nsIToolkitProfileService);
 
     try {
-      this.appProfile = profSvc.createDefaultProfileForApp(this.uniqueName,
-                                                           null, null);
-    } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {}
+      let appProfile = profSvc.createDefaultProfileForApp(this.uniqueName,
+                                                          null, null);
+      return appProfile.localDir;
+    } catch (ex if ex.result == Cr.NS_ERROR_ALREADY_INITIALIZED) {
+      return null;
+    }
   },
 };
 
 #ifdef XP_WIN
 
-const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir;
-const APP_DATA_DIR = OS.Constants.Path.winAppDataDir;
-
-/*************************************
- * Windows app installer
- *
- * The Windows installation process will generate the following files:
- *
- * ${FolderName} = sanitized app name + "-" + manifest url hash
- *
- * %APPDATA%/${FolderName}
- *   - webapp.ini
- *   - webapp.json
- *   - ${AppName}.exe
- *   - ${AppName}.lnk
- *   / uninstall
- *     - webapp-uninstaller.exe
- *     - shortcuts_log.ini
- *     - uninstall.log
- *   / chrome/icons/default/
- *     - default.ico
- *
- * After the app runs for the first time, a profiles/ folder will also be
- * created which will host the user profile for this app.
- */
-
-/**
- * Constructor for the Windows native app shell
- *
- * @param aData the data object provided to the install function
- */
-function WinNativeApp(aData) {
-  NativeApp.call(this, aData);
-
-  if (aData.isPackage) {
-    this.size = aData.app.updateManifest.size / 1024;
-    this.isPackaged = true;
-  }
-
-  let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
-
-  this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, "");
-  if (this.appNameAsFilename == "") {
-    this.appNameAsFilename = "webapp";
-  }
-
-  this.webapprt = this.appNameAsFilename + ".exe";
-  this.configJson = "webapp.json";
-  this.webappINI = "webapp.ini";
-  this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico");
-  this.uninstallDir = "uninstall";
-  this.uninstallerFile = OS.Path.join(this.uninstallDir,
-                                      "webapp-uninstaller.exe");
-  this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini");
-
-  this.uninstallSubkeyStr = this.uniqueName;
-}
-
-WinNativeApp.prototype = {
-  __proto__: NativeApp.prototype,
-  size: null,
-
-  /**
-   * Install the app in the system
-   *
-   */
-  install: function(aZipPath) {
-    return Task.spawn(function() {
-      yield this._getInstallDir();
-
-      try {
-        yield this._createDirectoryStructure();
-        yield this._copyPrebuiltFiles();
-        yield this._createConfigFiles();
-
-        if (aZipPath) {
-          yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir,
-                                                    "application.zip"));
-        }
-
-        yield this.getIcon();
-
-        // Remove previously installed app
-        this._removeInstallation(true);
-      } catch (ex) {
-        removeFiles([this.tmpInstallDir]);
-        throw(ex);
-      }
-
-      try {
-        // On Windows, the webapprt executable can't be overwritten while it's
-        // running.
-        // As it takes care of updating itself, there's no need to update
-        // it here.
-        let filesToIgnore = [ this.webapprt ];
-        yield moveDirectory(this.tmpInstallDir, this.installDir, filesToIgnore);
-
-        this._createShortcutFiles();
-        this._writeSystemKeys();
-      } catch (ex) {
-        this._removeInstallation(false);
-        throw(ex);
-      }
-    }.bind(this));
-  },
-
-  _getInstallDir: function() {
-    // The ${InstallDir} is: sanitized app name + "-" + manifest url hash
-    this.installDir = WebappOSUtils.getInstallPath(this.app);
-    if (this.installDir) {
-      if (this.uniqueName != OS.Path.basename(this.installDir)) {
-        // Bug 919799: If the app is still in the registry, migrate its data to
-        // the new format.
-        throw("Updates for apps installed with the old naming scheme unsupported");
-      }
-
-      let shortcutLogsINIfile = getFile(this.installDir, this.shortcutLogsINI);
-      // If it's a reinstallation (or an update) get the shortcut names
-      // from the shortcut_log.ini file
-      let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
-                     .getService(Ci.nsIINIParserFactory)
-                     .createINIParser(shortcutLogsINIfile);
-      this.shortcutName = parser.getString("STARTMENU", "Shortcut0");
-    } else {
-      this.installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName);
-
-      // Check in both directories to see if a shortcut with the same name
-      // already exists.
-      this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
-                                                     this.appNameAsFilename,
-                                                     ".lnk");
-    }
-  },
-
-  /**
-   * Remove the current installation
-   */
-  _removeInstallation: function(keepProfile) {
-    let uninstallKey;
-    try {
-      uninstallKey = Cc["@mozilla.org/windows-registry-key;1"]
-                     .createInstance(Ci.nsIWindowsRegKey);
-      uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
-                        "SOFTWARE\\Microsoft\\Windows\\" +
-                        "CurrentVersion\\Uninstall",
-                        uninstallKey.ACCESS_WRITE);
-      if(uninstallKey.hasChild(this.uninstallSubkeyStr)) {
-        uninstallKey.removeChild(this.uninstallSubkeyStr);
-      }
-    } catch (e) {
-    } finally {
-      if(uninstallKey)
-        uninstallKey.close();
-    }
-
-    let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName),
-                          OS.Path.join(PROGS_DIR, this.shortcutName) ];
-
-    if (keepProfile) {
-      [ this.iconPath, this.webapprt, this.configJson,
-        this.webappINI, this.uninstallDir ].forEach((filePath) => {
-        filesToRemove.push(OS.Path.join(this.installDir, filePath));
-      });
-    } else {
-      filesToRemove.push(this.installDir);
-      filesToRemove.push(this.tmpInstallDir);
-    }
-
-    removeFiles(filesToRemove);
-  },
-
-  /**
-   * Creates the main directory structure.
-   */
-  _createDirectoryStructure: function() {
-    let dir = getFile(TMP_DIR, this.uniqueName);
-    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
-    this.tmpInstallDir = dir.path;
-
-    yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.uninstallDir),
-                          { ignoreExisting: true });
-
-    // Recursively create the icon path's directory structure.
-    let path = this.tmpInstallDir;
-    let components = OS.Path.split(OS.Path.dirname(this.iconPath)).components;
-    for (let component of components) {
-      path = OS.Path.join(path, component);
-      yield OS.File.makeDir(path, { ignoreExisting: true });
-    }
-  },
-
-  /**
-   * Copy the pre-built files into their destination folders.
-   */
-  _copyPrebuiltFiles: function() {
-    yield OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"),
-                       OS.Path.join(this.tmpInstallDir, this.webapprt));
-
-    yield OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"),
-                       OS.Path.join(this.tmpInstallDir, this.uninstallerFile));
-  },
-
-  /**
-   * Creates the configuration files into their destination folders.
-   */
-  _createConfigFiles: function() {
-    // ${InstallDir}/webapp.json
-    yield writeToFile(OS.Path.join(this.tmpInstallDir, this.configJson),
-                      JSON.stringify(this.webappJson));
-
-    let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
-                    .getService(Ci.nsIINIParserFactory);
-
-    // ${InstallDir}/webapp.ini
-    let webappINIfile = getFile(this.tmpInstallDir, this.webappINI);
-
-    let writer = factory.createINIParser(webappINIfile)
-                        .QueryInterface(Ci.nsIINIParserWriter);
-    writer.setString("Webapp", "Name", this.appName);
-    writer.setString("Webapp", "Profile", OS.Path.basename(this.installDir));
-    writer.setString("Webapp", "Executable", this.appNameAsFilename);
-    writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
-    writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
-
-    let shortcutLogsINIfile = getFile(this.tmpInstallDir, this.shortcutLogsINI);
-
-    writer = factory.createINIParser(shortcutLogsINIfile)
-                    .QueryInterface(Ci.nsIINIParserWriter);
-    writer.setString("STARTMENU", "Shortcut0", this.shortcutName);
-    writer.setString("DESKTOP", "Shortcut0", this.shortcutName);
-    writer.setString("TASKBAR", "Migrated", "true");
-    writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
-
-    // ${UninstallDir}/uninstall.log
-    let uninstallContent = 
-      "File: \\webapp.ini\r\n" +
-      "File: \\webapp.json\r\n" +
-      "File: \\webapprt.old\r\n" +
-      "File: \\chrome\\icons\\default\\default.ico";
-    if (this.isPackaged) {
-      uninstallContent += "\r\nFile: \\application.zip";
-    }
-
-    yield writeToFile(OS.Path.join(this.tmpInstallDir, this.uninstallDir,
-                                   "uninstall.log"),
-                      uninstallContent);
-  },
-
-  /**
-   * Writes the keys to the system registry that are necessary for the app operation
-   * and uninstall process.
-   */
-  _writeSystemKeys: function() {
-    let parentKey;
-    let uninstallKey;
-    let subKey;
-
-    try {
-      parentKey = Cc["@mozilla.org/windows-registry-key;1"]
-                  .createInstance(Ci.nsIWindowsRegKey);
-      parentKey.open(parentKey.ROOT_KEY_CURRENT_USER,
-                     "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
-                     parentKey.ACCESS_WRITE);
-      uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE)
-      subKey = uninstallKey.createChild(this.uninstallSubkeyStr, uninstallKey.ACCESS_WRITE);
-
-      subKey.writeStringValue("DisplayName", this.appName);
-
-      let uninstallerPath = OS.Path.join(this.installDir,
-                                         this.uninstallerFile);
-
-      subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"');
-      subKey.writeStringValue("InstallLocation", '"' + this.installDir + '"');
-      subKey.writeStringValue("AppFilename", this.appNameAsFilename);
-      subKey.writeStringValue("DisplayIcon", OS.Path.join(this.installDir,
-                                                          this.iconPath));
-
-      let date = new Date();
-      let year = date.getYear().toString();
-      let month = date.getMonth();
-      if (month < 10) {
-        month = "0" + month;
-      }
-      let day = date.getDate();
-      if (day < 10) {
-        day = "0" + day;
-      }
-      subKey.writeStringValue("InstallDate", year + month + day);
-      if (this.manifest.version) {
-        subKey.writeStringValue("DisplayVersion", this.manifest.version);
-      }
-      if (this.developerName) {
-        subKey.writeStringValue("Publisher", this.developerName);
-      }
-      subKey.writeStringValue("URLInfoAbout", this.developerUrl);
-      if (this.size) {
-        subKey.writeIntValue("EstimatedSize", this.size);
-      }
-
-      subKey.writeIntValue("NoModify", 1);
-      subKey.writeIntValue("NoRepair", 1);
-    } catch(ex) {
-      throw(ex);
-    } finally {
-      if(subKey) subKey.close();
-      if(uninstallKey) uninstallKey.close();
-      if(parentKey) parentKey.close();
-    }
-  },
-
-  /**
-   * Creates a shortcut file inside the app installation folder and makes
-   * two copies of it: one into the desktop and one into the start menu.
-   */
-  _createShortcutFiles: function() {
-    let shortcut = getFile(this.installDir, this.shortcutName).
-                      QueryInterface(Ci.nsILocalFileWin);
-
-    /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args,
-                                            description, iconFile, iconIndex) */
-
-    shortcut.setShortcut(getFile(this.installDir, this.webapprt),
-                         getFile(this.installDir),
-                         null,
-                         this.shortDescription,
-                         getFile(this.installDir, this.iconPath),
-                         0);
-
-    shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName);
-    shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName);
-
-    shortcut.followLinks = false;
-    shortcut.remove(false);
-  },
-
-  /**
-   * Process the icon from the imageStream as retrieved from
-   * the URL by getIconForApp(). This will save the icon to the
-   * topwindow.ico file.
-   *
-   * @param aMimeType     ahe icon mimetype
-   * @param aImageStream  the stream for the image data
-   */
-  processIcon: function(aMimeType, aImageStream) {
-    let deferred = Promise.defer();
-
-    let imgTools = Cc["@mozilla.org/image/tools;1"]
-                     .createInstance(Ci.imgITools);
-
-    let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
-    let iconStream = imgTools.encodeImage(imgContainer,
-                                          "image/vnd.microsoft.icon",
-                                          "format=bmp;bpp=32");
-
-    let tmpIconFile = getFile(this.tmpInstallDir, this.iconPath);
-
-    let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile);
-    NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
-      if (Components.isSuccessCode(aResult)) {
-        deferred.resolve();
-      } else {
-        deferred.reject("Failure copying icon: " + aResult);
-      }
-    });
-
-    return deferred.promise;
-  }
-}
+#include WinNativeApp.js
 
 #elifdef XP_MACOSX
 
-const USER_LIB_DIR = OS.Constants.Path.macUserLibDir;
-const LOCAL_APP_DIR = OS.Constants.Path.macLocalApplicationsDir;
-
-function MacNativeApp(aData) {
-  NativeApp.call(this, aData);
-
-  let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
-  this.appNameAsFilename = this.appNameAsFilename.replace(filenameRE, "");
-  if (this.appNameAsFilename == "") {
-    this.appNameAsFilename = "Webapp";
-  }
-
-  // The ${ProfileDir} is: sanitized app name + "-" + manifest url hash
-  this.appProfileDir = OS.Path.join(USER_LIB_DIR, "Application Support",
-                                    this.uniqueName);
-
-  this.contentsDir = "Contents";
-  this.macOSDir = OS.Path.join(this.contentsDir, "MacOS");
-  this.resourcesDir = OS.Path.join(this.contentsDir, "Resources");
-  this.iconFile = OS.Path.join(this.resourcesDir, "appicon.icns");
-}
-
-MacNativeApp.prototype = {
-  __proto__: NativeApp.prototype,
-
-  install: function(aZipPath) {
-    return Task.spawn(function() {
-      yield this._getInstallDir();
-
-      try {
-        yield this._createDirectoryStructure();
-        this._copyPrebuiltFiles();
-        yield this._createConfigFiles();
-
-        if (aZipPath) {
-          yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir,
-                                                    "application.zip"));
-        }
-
-        yield this.getIcon();
-
-        // Remove previously installed app
-        this._removeInstallation(true);
-      } catch (ex) {
-        removeFiles([this.tmpInstallDir]);
-        throw(ex);
-      }
-
-      try {
-        // Move the temp installation directory to the /Applications directory
-        yield moveDirectory(this.tmpInstallDir, this.installDir, []);
-      } catch (ex) {
-        this._removeInstallation(false);
-        throw(ex);
-      }
-    }.bind(this));
-  },
-
-  _getInstallDir: function() {
-    let [ oldUniqueName, installPath ] = WebappOSUtils.getLaunchTarget(this.app);
-    if (installPath) {
-      this.installDir = installPath;
-
-      if (this.uniqueName != oldUniqueName) {
-        // Bug 919799: If the app is still in the registry, migrate its data to
-        // the new format.
-        throw("Updates for apps installed with the old naming scheme unsupported");
-      }
-    } else {
-      let localAppDir = getFile(LOCAL_APP_DIR);
-      if (!localAppDir.isWritable()) {
-        throw("Not enough privileges to install apps");
-      }
-
-      let destinationName = yield getAvailableFileName([ LOCAL_APP_DIR ],
-                                                       this.appNameAsFilename,
-                                                       ".app");
-
-      this.installDir = OS.Path.join(LOCAL_APP_DIR, destinationName);
-    }
-  },
-
-  _removeInstallation: function(keepProfile) {
-    let filesToRemove = [this.installDir];
-
-    if (!keepProfile) {
-      filesToRemove.push(this.appProfileDir);
-    }
-
-    removeFiles(filesToRemove);
-  },
-
-  _createDirectoryStructure: function() {
-    let dir = getFile(TMP_DIR, this.appNameAsFilename + ".app");
-    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
-    this.tmpInstallDir = dir.path;
-
-    yield OS.File.makeDir(this.appProfileDir,
-                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
-
-    yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.contentsDir),
-                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
-
-    yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.macOSDir),
-                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
-
-    yield OS.File.makeDir(OS.Path.join(this.tmpInstallDir, this.resourcesDir),
-                          { unixMode: PERMS_DIRECTORY, ignoreExisting: true });
-  },
-
-  _copyPrebuiltFiles: function() {
-    let destDir = getFile(this.tmpInstallDir, this.macOSDir);
-    let stub = getFile(this.runtimeFolder, "webapprt-stub");
-    stub.copyTo(destDir, "webapprt");
-  },
-
-  _createConfigFiles: function() {
-    // ${ProfileDir}/webapp.json
-    yield writeToFile(OS.Path.join(this.appProfileDir, "webapp.json"),
-                      JSON.stringify(this.webappJson));
-
-    // ${InstallDir}/Contents/MacOS/webapp.ini
-    let applicationINI = getFile(this.tmpInstallDir, this.macOSDir, "webapp.ini");
-
-    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
-                   .getService(Ci.nsIINIParserFactory)
-                   .createINIParser(applicationINI)
-                   .QueryInterface(Ci.nsIINIParserWriter);
-    writer.setString("Webapp", "Name", this.appName);
-    writer.setString("Webapp", "Profile", OS.Path.basename(this.appProfileDir));
-    writer.writeFile();
-    applicationINI.permissions = PERMS_FILE;
-
-    // ${InstallDir}/Contents/Info.plist
-    let infoPListContent = '<?xml version="1.0" encoding="UTF-8"?>\n\
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n\
-<plist version="1.0">\n\
-  <dict>\n\
-    <key>CFBundleDevelopmentRegion</key>\n\
-    <string>English</string>\n\
-    <key>CFBundleDisplayName</key>\n\
-    <string>' + escapeXML(this.appName) + '</string>\n\
-    <key>CFBundleExecutable</key>\n\
-    <string>webapprt</string>\n\
-    <key>CFBundleIconFile</key>\n\
-    <string>appicon</string>\n\
-    <key>CFBundleIdentifier</key>\n\
-    <string>' + escapeXML(this.uniqueName) + '</string>\n\
-    <key>CFBundleInfoDictionaryVersion</key>\n\
-    <string>6.0</string>\n\
-    <key>CFBundleName</key>\n\
-    <string>' + escapeXML(this.appName) + '</string>\n\
-    <key>CFBundlePackageType</key>\n\
-    <string>APPL</string>\n\
-    <key>CFBundleVersion</key>\n\
-    <string>0</string>\n\
-    <key>NSHighResolutionCapable</key>\n\
-    <true/>\n\
-    <key>NSPrincipalClass</key>\n\
-    <string>GeckoNSApplication</string>\n\
-    <key>FirefoxBinary</key>\n\
-#expand     <string>__MOZ_MACBUNDLE_ID__</string>\n\
-  </dict>\n\
-</plist>';
-
-    yield writeToFile(OS.Path.join(this.tmpInstallDir, this.contentsDir, "Info.plist"),
-                      infoPListContent);
-  },
-
-  /**
-   * Process the icon from the imageStream as retrieved from
-   * the URL by getIconForApp(). This will bundle the icon to the
-   * app package at Contents/Resources/appicon.icns.
-   *
-   * @param aMimeType     the icon mimetype
-   * @param aImageStream  the stream for the image data
-   */
-  processIcon: function(aMimeType, aIcon) {
-    let deferred = Promise.defer();
-
-    function conversionDone(aSubject, aTopic) {
-      if (aTopic == "process-finished") {
-        deferred.resolve();
-      } else {
-        deferred.reject("Failure converting icon.");
-      }
-    }
-
-    let process = Cc["@mozilla.org/process/util;1"].
-                  createInstance(Ci.nsIProcess);
-    let sipsFile = getFile("/usr/bin/sips");
-
-    process.init(sipsFile);
-    process.runAsync(["-s",
-                "format", "icns",
-                aIcon.path,
-                "--out", OS.Path.join(this.tmpInstallDir, this.iconFile),
-                "-z", "128", "128"],
-                9, conversionDone);
-
-    return deferred.promise;
-  }
-
-}
+#include MacNativeApp.js
 
 #elifdef XP_UNIX
 
-function LinuxNativeApp(aData) {
-  NativeApp.call(this, aData);
-
-  this.iconFile = "icon.png";
-  this.webapprt = "webapprt-stub";
-  this.configJson = "webapp.json";
-  this.webappINI = "webapp.ini";
-
-  let xdg_data_home = Cc["@mozilla.org/process/environment;1"]
-                        .getService(Ci.nsIEnvironment)
-                        .get("XDG_DATA_HOME");
-  if (!xdg_data_home) {
-    xdg_data_home = OS.Path.join(HOME_DIR, ".local", "share");
-  }
-
-  this.desktopINI = OS.Path.join(xdg_data_home, "applications",
-                                 "owa-" + this.uniqueName + ".desktop");
-}
-
-LinuxNativeApp.prototype = {
-  __proto__: NativeApp.prototype,
-
-  install: function(aZipPath) {
-    return Task.spawn(function() {
-      this._getInstallDir();
-
-      try {
-        this._createDirectoryStructure();
-        this._copyPrebuiltFiles();
-        yield this._createConfigFiles();
-
-        if (aZipPath) {
-          yield OS.File.move(aZipPath, OS.Path.join(this.tmpInstallDir,
-                                                    "application.zip"));
-        }
-
-        yield this.getIcon();
-
-        // Remove previously installed app
-        this._removeInstallation(true);
-      } catch (ex) {
-        removeFiles([this.tmpInstallDir]);
-        throw(ex);
-      }
-
-      try {
-        yield moveDirectory(this.tmpInstallDir, this.installDir, []);
-
-        this._createSystemFiles();
-      } catch (ex) {
-        this._removeInstallation(false);
-        throw(ex);
-      }
-    }.bind(this));
-  },
-
-  _getInstallDir: function() {
-    // The ${InstallDir} and desktop entry filename are: sanitized app name +
-    // "-" + manifest url hash
-    this.installDir = WebappOSUtils.getInstallPath(this.app);
-    if (this.installDir) {
-      let baseName = OS.Path.basename(this.installDir)
-      let oldUniqueName = baseName.substring(1, baseName.length);
-      if (this.uniqueName != oldUniqueName) {
-        // Bug 919799: If the app is still in the registry, migrate its data to
-        // the new format.
-        throw("Updates for apps installed with the old naming scheme unsupported");
-      }
-    } else {
-      this.installDir = OS.Path.join(HOME_DIR, "." + this.uniqueName);
-    }
-  },
-
-  _removeInstallation: function(keepProfile) {
-    let filesToRemove = [this.desktopINI];
-
-    if (keepProfile) {
-      [ this.iconFile, this.webapprt,
-        this.configJson, this.webappINI ].forEach((filePath) => {
-        filesToRemove.push(OS.Path.join(this.installDir, filePath));
-      });
-    } else {
-      filesToRemove.push(this.installDir);
-      filesToRemove.push(this.tmpInstallDir);
-    }
-
-    removeFiles(filesToRemove);
-  },
-
-  _createDirectoryStructure: function() {
-    let dir = getFile(TMP_DIR, this.uniqueName);
-    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
-    this.tmpInstallDir = dir.path;
-  },
-
-  _copyPrebuiltFiles: function() {
-    let destDir = getFile(this.tmpInstallDir);
-    let stub = getFile(this.runtimeFolder, this.webapprt);
-
-    stub.copyTo(destDir, null);
-  },
-
-  /**
-   * Translate marketplace categories to freedesktop.org categories.
-   *
-   * @link http://standards.freedesktop.org/menu-spec/menu-spec-latest.html#category-registry
-   *
-   * @return an array of categories
-   */
-  _translateCategories: function() {
-    let translations = {
-      "books": "Education;Literature",
-      "business": "Finance",
-      "education": "Education",
-      "entertainment": "Amusement",
-      "sports": "Sports",
-      "games": "Game",
-      "health-fitness": "MedicalSoftware",
-      "lifestyle": "Amusement",
-      "music": "Audio;Music",
-      "news-weather": "News",
-      "photo-video": "Video;AudioVideo;Photography",
-      "productivity": "Office",
-      "shopping": "Amusement",
-      "social": "Chat",
-      "travel": "Amusement",
-      "reference": "Science;Education;Documentation",
-      "maps-navigation": "Maps",
-      "utilities": "Utility"
-    };
-
-    // The trailing semicolon is needed as written in the freedesktop specification
-    let categories = "";
-    for (let category of this.categories) {
-      let catLower = category.toLowerCase();
-      if (catLower in translations) {
-        categories += translations[catLower] + ";";
-      }
-    }
-
-    return categories;
-  },
-
-  _createConfigFiles: function() {
-    // ${InstallDir}/webapp.json
-    yield writeToFile(OS.Path.join(this.tmpInstallDir, this.configJson),
-                      JSON.stringify(this.webappJson));
-
-    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
-
-    // ${InstallDir}/webapp.ini
-    let webappINIfile = getFile(this.tmpInstallDir, this.webappINI);
-
-    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
-                   .getService(Ci.nsIINIParserFactory)
-                   .createINIParser(webappINIfile)
-                   .QueryInterface(Ci.nsIINIParserWriter);
-    writer.setString("Webapp", "Name", this.appName);
-    writer.setString("Webapp", "Profile", this.uniqueName);
-    writer.setString("Webapp", "UninstallMsg", webappsBundle.formatStringFromName("uninstall.notification", [this.appName], 1));
-    writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
-    writer.writeFile();
-  },
-
-  _createSystemFiles: function() {
-    let webappsBundle = Services.strings.createBundle("chrome://global/locale/webapps.properties");
-
-    let webapprtPath = OS.Path.join(this.installDir, this.webapprt);
-
-    // $XDG_DATA_HOME/applications/owa-<webappuniquename>.desktop
-    let desktopINIfile = getFile(this.desktopINI);
-
-    let writer = Cc["@mozilla.org/xpcom/ini-processor-factory;1"]
-                   .getService(Ci.nsIINIParserFactory)
-                   .createINIParser(desktopINIfile)
-                   .QueryInterface(Ci.nsIINIParserWriter);
-    writer.setString("Desktop Entry", "Name", this.appName);
-    writer.setString("Desktop Entry", "Comment", this.shortDescription);
-    writer.setString("Desktop Entry", "Exec", '"' + webapprtPath + '"');
-    writer.setString("Desktop Entry", "Icon", OS.Path.join(this.installDir,
-                                                           this.iconFile));
-    writer.setString("Desktop Entry", "Type", "Application");
-    writer.setString("Desktop Entry", "Terminal", "false");
-
-    let categories = this._translateCategories();
-    if (categories)
-      writer.setString("Desktop Entry", "Categories", categories);
-
-    writer.setString("Desktop Entry", "Actions", "Uninstall;");
-    writer.setString("Desktop Action Uninstall", "Name", webappsBundle.GetStringFromName("uninstall.label"));
-    writer.setString("Desktop Action Uninstall", "Exec", webapprtPath + " -remove");
-
-    writer.writeFile();
-
-    desktopINIfile.permissions = PERMS_FILE | OS.Constants.libc.S_IXUSR;
-  },
-
-  /**
-   * Process the icon from the imageStream as retrieved from
-   * the URL by getIconForApp().
-   *
-   * @param aMimeType     ahe icon mimetype
-   * @param aImageStream  the stream for the image data
-   */
-  processIcon: function(aMimeType, aImageStream) {
-    let deferred = Promise.defer();
-
-    let imgTools = Cc["@mozilla.org/image/tools;1"]
-                     .createInstance(Ci.imgITools);
-
-    let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
-    let iconStream = imgTools.encodeImage(imgContainer, "image/png");
-
-    let iconFile = getFile(this.tmpInstallDir, this.iconFile);
-    let outputStream = FileUtils.openSafeFileOutputStream(iconFile);
-    NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
-      if (Components.isSuccessCode(aResult)) {
-        deferred.resolve();
-      } else {
-        deferred.reject("Failure copying icon: " + aResult);
-      }
-    });
-
-    return deferred.promise;
-  }
-}
+#include LinuxNativeApp.js
 
 #endif
 
 /* Helper Functions */
 
 /**
  * Async write a data string into a file
  *
@@ -1106,26 +271,39 @@ function writeToFile(aPath, aData) {
  * Removes unprintable characters from a string.
  */
 function sanitize(aStr) {
   let unprintableRE = new RegExp("[\\x00-\\x1F\\x7F]" ,"gi");
   return aStr.replace(unprintableRE, "");
 }
 
 /**
- * Strips all non-word characters from the beginning and end of a string
+ * Strips all non-word characters from the beginning and end of a string.
+ * Strips invalid characters from the string.
+ *
  */
 function stripStringForFilename(aPossiblyBadFilenameString) {
-  //strip everything from the front up to the first [0-9a-zA-Z]
+  // Strip everything from the front up to the first [0-9a-zA-Z]
+  let stripFrontRE = new RegExp("^\\W*", "gi");
 
-  let stripFrontRE = new RegExp("^\\W*","gi");
-  let stripBackRE = new RegExp("\\s*$","gi");
+  // Strip white space characters starting from the last [0-9a-zA-Z]
+  let stripBackRE = new RegExp("\\s*$", "gi");
+
+  // Strip invalid characters from the filename
+  let filenameRE = new RegExp("[<>:\"/\\\\|\\?\\*]", "gi");
 
   let stripped = aPossiblyBadFilenameString.replace(stripFrontRE, "");
   stripped = stripped.replace(stripBackRE, "");
+  stripped = stripped.replace(filenameRE, "");
+
+  // If the filename ends up empty, let's call it "webapp".
+  if (stripped == "") {
+    stripped = "webapp";
+  }
+
   return stripped;
 }
 
 /**
  * Finds a unique name available in a folder (i.e., non-existent file)
  *
  * @param aPathSet a set of paths that represents the set of
  * directories where we want to write
@@ -1187,40 +365,29 @@ function removeFiles(aPaths) {
   }
 }
 
 /**
  * Move (overwriting) the contents of one directory into another.
  *
  * @param srcPath A path to the source directory
  * @param destPath A path to the destination directory
- * @param filesToIgnore An array of files. If one of those files can't be
- *                      overwritten the function will not fail.
  */
-function moveDirectory(srcPath, destPath, filesToIgnore) {
+function moveDirectory(srcPath, destPath) {
   let srcDir = getFile(srcPath);
   let destDir = getFile(destPath);
 
   let entries = srcDir.directoryEntries;
   let array = [];
   while (entries.hasMoreElements()) {
     let entry = entries.getNext().QueryInterface(Ci.nsIFile);
-
     if (entry.isDirectory()) {
       yield moveDirectory(entry.path, OS.Path.join(destPath, entry.leafName));
     } else {
-      try {
-        entry.moveTo(destDir, entry.leafName);
-      } catch (ex if ex.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) {
-        if (filesToIgnore.indexOf(entry.leafName) != -1) {
-          yield OS.File.remove(entry.path);
-        } else {
-          throw(ex);
-        }
-      }
+      entry.moveTo(destDir, entry.leafName);
     }
   }
 
   // The source directory is now empty, remove it.
   yield OS.File.removeEmptyDir(srcPath);
 }
 
 function escapeXML(aStr) {
--- a/toolkit/webapps/WebappOSUtils.jsm
+++ b/toolkit/webapps/WebappOSUtils.jsm
@@ -4,16 +4,17 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const CC = Components.Constructor;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
 
 this.EXPORTED_SYMBOLS = ["WebappOSUtils"];
 
 // Returns the MD5 hash of a string.
 function computeHash(aString) {
   let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                   createInstance(Ci.nsIScriptableUnicodeConverter);
   converter.charset = "UTF-8";
@@ -199,16 +200,26 @@ this.WebappOSUtils = {
     return execFile.parent.path;
 #endif
 
 #endif
     // Anything unsupported, like Metro
     throw new Error("Unsupported apps platform");
   },
 
+  getPackagePath: function(aApp) {
+    let packagePath = this.getInstallPath(aApp);
+
+#ifdef XP_MACOSX
+    packagePath = OS.Path.join(packagePath, "Contents", "Resources");
+#endif
+
+    return packagePath;
+  },
+
   launch: function(aApp) {
     let uniqueName = this.getUniqueName(aApp);
 
 #ifdef XP_WIN
     let initProcess = CC("@mozilla.org/process/util;1",
                          "nsIProcess", "init");
 
     let launchTarget = this.getLaunchTarget(aApp);
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/WinNativeApp.js
@@ -0,0 +1,474 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PROGS_DIR = OS.Constants.Path.winStartMenuProgsDir;
+const APP_DATA_DIR = OS.Constants.Path.winAppDataDir;
+
+/*************************************
+ * Windows app installer
+ *
+ * The Windows installation process will generate the following files:
+ *
+ * ${FolderName} = sanitized app name + "-" + manifest url hash
+ *
+ * %APPDATA%/${FolderName}
+ *   - webapp.ini
+ *   - webapp.json
+ *   - ${AppName}.exe
+ *   - ${AppName}.lnk
+ *   / uninstall
+ *     - webapp-uninstaller.exe
+ *     - shortcuts_log.ini
+ *     - uninstall.log
+ *   / chrome/icons/default/
+ *     - default.ico
+ *
+ * After the app runs for the first time, a profiles/ folder will also be
+ * created which will host the user profile for this app.
+ */
+
+/**
+ * Constructor for the Windows native app shell
+ *
+ * @param aApp {Object} the app object provided to the install function
+ * @param aManifest {Object} the manifest data provided by the web app
+ * @param aCategories {Array} array of app categories
+ * @param aRegistryDir {String} (optional) path to the registry
+ */
+function NativeApp(aApp, aManifest, aCategories, aRegistryDir) {
+  CommonNativeApp.call(this, aApp, aManifest, aCategories, aRegistryDir);
+
+  if (this.isPackaged) {
+    this.size = aApp.updateManifest.size / 1024;
+  }
+
+  this.webapprt = this.appNameAsFilename + ".exe";
+  this.configJson = "webapp.json";
+  this.webappINI = "webapp.ini";
+  this.iconPath = OS.Path.join("chrome", "icons", "default", "default.ico");
+  this.uninstallDir = "uninstall";
+  this.uninstallerFile = OS.Path.join(this.uninstallDir,
+                                      "webapp-uninstaller.exe");
+  this.shortcutLogsINI = OS.Path.join(this.uninstallDir, "shortcuts_log.ini");
+  this.zipFile = "application.zip";
+
+  this.backupFiles = [ "chrome", this.configJson, this.webappINI, "uninstall" ];
+  if (this.isPackaged) {
+    this.backupFiles.push(this.zipFile);
+  }
+
+  this.uninstallSubkeyStr = this.uniqueName;
+}
+
+NativeApp.prototype = {
+  __proto__: CommonNativeApp.prototype,
+  size: null,
+
+  /**
+   * Creates a native installation of the web app in the OS
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  install: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    // If the application is already installed, this is a reinstallation.
+    if (WebappOSUtils.getInstallPath(this.app)) {
+      return yield this.prepareUpdate(aManifest, aZipPath);
+    }
+
+    this._setData(aManifest);
+
+    let installDir = OS.Path.join(APP_DATA_DIR, this.uniqueName);
+
+    // Create a temporary installation directory.
+    let dir = getFile(TMP_DIR, this.uniqueName);
+    dir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+    let tmpDir = dir.path;
+
+    // Perform the installation in the temp directory.
+    try {
+      yield this._createDirectoryStructure(tmpDir);
+      yield this._getShortcutName(installDir);
+      yield this._copyWebapprt(tmpDir);
+      yield this._copyUninstaller(tmpDir);
+      yield this._createConfigFiles(tmpDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(tmpDir, this.zipFile));
+      }
+
+      yield this._getIcon(tmpDir);
+    } catch (ex) {
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+
+    // Apply the installation.
+    this._removeInstallation(true, installDir);
+
+    try {
+      yield this._applyTempInstallation(tmpDir, installDir);
+    } catch (ex) {
+      this._removeInstallation(false, installDir);
+      yield OS.File.removeDir(tmpDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Creates an update in a temporary directory to be applied later.
+   *
+   * @param aManifest {Object} the manifest data provided by the web app
+   * @param aZipPath {String} path to the zip file for packaged apps (undefined
+   *                          for hosted apps)
+   */
+  prepareUpdate: Task.async(function*(aManifest, aZipPath) {
+    if (this._dryRun) {
+      return;
+    }
+
+    this._setData(aManifest);
+
+    let installDir = WebappOSUtils.getInstallPath(this.app);
+    if (!installDir) {
+      throw ERR_NOT_INSTALLED;
+    }
+
+    if (this.uniqueName != OS.Path.basename(installDir)) {
+      // Bug 919799: If the app is still in the registry, migrate its data to
+      // the new format.
+      throw ERR_UPDATES_UNSUPPORTED_OLD_NAMING_SCHEME;
+    }
+
+    let updateDir = OS.Path.join(installDir, "update");
+    yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(updateDir);
+
+    // Perform the update in the "update" subdirectory.
+    try {
+      yield this._createDirectoryStructure(updateDir);
+      yield this._getShortcutName(installDir);
+      yield this._copyUninstaller(updateDir);
+      yield this._createConfigFiles(updateDir);
+
+      if (aZipPath) {
+        yield OS.File.move(aZipPath, OS.Path.join(updateDir, this.zipFile));
+      }
+
+      yield this._getIcon(updateDir);
+    } catch (ex) {
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+      throw ex;
+    }
+  }),
+
+  /**
+   * Applies an update.
+   */
+  applyUpdate: Task.async(function*() {
+    if (this._dryRun) {
+      return;
+    }
+
+    let installDir = WebappOSUtils.getInstallPath(this.app);
+    let updateDir = OS.Path.join(installDir, "update");
+
+    yield this._getShortcutName(installDir);
+
+    let backupDir = yield this._backupInstallation(installDir);
+
+    try {
+      yield this._applyTempInstallation(updateDir, installDir);
+    } catch (ex) {
+      yield this._restoreInstallation(backupDir, installDir);
+      throw ex;
+    } finally {
+      yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+      yield OS.File.removeDir(updateDir, { ignoreAbsent: true });
+    }
+  }),
+
+  _applyTempInstallation: Task.async(function*(aTmpDir, aInstallDir) {
+    yield moveDirectory(aTmpDir, aInstallDir);
+
+    this._createShortcutFiles(aInstallDir);
+    this._writeSystemKeys(aInstallDir);
+  }),
+
+  _getShortcutName: Task.async(function*(aInstallDir) {
+    let shortcutLogsINIfile = getFile(aInstallDir, this.shortcutLogsINI);
+
+    if (shortcutLogsINIfile.exists()) {
+      // If it's a reinstallation (or an update) get the shortcut names
+      // from the shortcut_log.ini file
+      let parser = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
+                   getService(Ci.nsIINIParserFactory).
+                   createINIParser(shortcutLogsINIfile);
+      this.shortcutName = parser.getString("STARTMENU", "Shortcut0");
+    } else {
+      // Check in both directories to see if a shortcut with the same name
+      // already exists.
+      this.shortcutName = yield getAvailableFileName([ PROGS_DIR, DESKTOP_DIR ],
+                                                     this.appNameAsFilename,
+                                                     ".lnk");
+    }
+  }),
+
+  /**
+   * Remove the current installation
+   */
+  _removeInstallation: function(keepProfile, aInstallDir) {
+    let uninstallKey;
+    try {
+      uninstallKey = Cc["@mozilla.org/windows-registry-key;1"].
+                     createInstance(Ci.nsIWindowsRegKey);
+      uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
+                        "SOFTWARE\\Microsoft\\Windows\\" +
+                        "CurrentVersion\\Uninstall",
+                        uninstallKey.ACCESS_WRITE);
+      if (uninstallKey.hasChild(this.uninstallSubkeyStr)) {
+        uninstallKey.removeChild(this.uninstallSubkeyStr);
+      }
+    } catch (e) {
+    } finally {
+      if (uninstallKey) {
+        uninstallKey.close();
+      }
+    }
+
+    let filesToRemove = [ OS.Path.join(DESKTOP_DIR, this.shortcutName),
+                          OS.Path.join(PROGS_DIR, this.shortcutName) ];
+
+    if (keepProfile) {
+      for (let filePath of this.backupFiles) {
+        filesToRemove.push(OS.Path.join(aInstallDir, filePath));
+      }
+
+      filesToRemove.push(OS.Path.join(aInstallDir, this.webapprt));
+    } else {
+      filesToRemove.push(aInstallDir);
+    }
+
+    removeFiles(filesToRemove);
+  },
+
+  _backupInstallation: Task.async(function*(aInstallDir) {
+    let backupDir = OS.Path.join(aInstallDir, "backup");
+    yield OS.File.removeDir(backupDir, { ignoreAbsent: true });
+    yield OS.File.makeDir(backupDir);
+
+    for (let filePath of this.backupFiles) {
+      yield OS.File.move(OS.Path.join(aInstallDir, filePath),
+                         OS.Path.join(backupDir, filePath));
+    }
+
+    return backupDir;
+  }),
+
+  _restoreInstallation: function(aBackupDir, aInstallDir) {
+    return moveDirectory(aBackupDir, aInstallDir);
+  },
+
+  /**
+   * Creates the main directory structure.
+   */
+  _createDirectoryStructure: Task.async(function*(aDir) {
+    yield OS.File.makeDir(OS.Path.join(aDir, this.uninstallDir));
+
+    // Recursively create the icon path's directory structure.
+    let path = aDir;
+    let components = OS.Path.split(OS.Path.dirname(this.iconPath)).components;
+    for (let component of components) {
+      path = OS.Path.join(path, component);
+      yield OS.File.makeDir(path);
+    }
+  }),
+
+  /**
+   * Copy the webrt executable into the installation directory.
+   */
+  _copyWebapprt: function(aDir) {
+    return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapprt-stub.exe"),
+                        OS.Path.join(aDir, this.webapprt));
+  },
+
+  /**
+   * Copy the uninstaller executable into the installation directory.
+   */
+  _copyUninstaller: function(aDir) {
+    return OS.File.copy(OS.Path.join(this.runtimeFolder, "webapp-uninstaller.exe"),
+                        OS.Path.join(aDir, this.uninstallerFile));
+  },
+
+  /**
+   * Creates the configuration files into their destination folders.
+   */
+  _createConfigFiles: function(aDir) {
+    // ${InstallDir}/webapp.json
+    yield writeToFile(OS.Path.join(aDir, this.configJson),
+                      JSON.stringify(this.webappJson));
+
+    let factory = Cc["@mozilla.org/xpcom/ini-processor-factory;1"].
+                  getService(Ci.nsIINIParserFactory);
+
+    // ${InstallDir}/webapp.ini
+    let webappINIfile = getFile(aDir, this.webappINI);
+
+    let writer = factory.createINIParser(webappINIfile)
+                        .QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("Webapp", "Name", this.appName);
+    writer.setString("Webapp", "Profile", this.uniqueName);
+    writer.setString("Webapp", "Executable", this.appNameAsFilename);
+    writer.setString("WebappRT", "InstallDir", this.runtimeFolder);
+    writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
+
+    let shortcutLogsINIfile = getFile(aDir, this.shortcutLogsINI);
+
+    writer = factory.createINIParser(shortcutLogsINIfile)
+                    .QueryInterface(Ci.nsIINIParserWriter);
+    writer.setString("STARTMENU", "Shortcut0", this.shortcutName);
+    writer.setString("DESKTOP", "Shortcut0", this.shortcutName);
+    writer.setString("TASKBAR", "Migrated", "true");
+    writer.writeFile(null, Ci.nsIINIParserWriter.WRITE_UTF16);
+
+    // ${UninstallDir}/uninstall.log
+    let uninstallContent =
+      "File: \\webapp.ini\r\n" +
+      "File: \\webapp.json\r\n" +
+      "File: \\webapprt.old\r\n" +
+      "File: \\chrome\\icons\\default\\default.ico";
+    if (this.isPackaged) {
+      uninstallContent += "\r\nFile: \\application.zip";
+    }
+
+    yield writeToFile(OS.Path.join(aDir, this.uninstallDir, "uninstall.log"),
+                      uninstallContent);
+  },
+
+  /**
+   * Writes the keys to the system registry that are necessary for the app
+   * operation and uninstall process.
+   */
+  _writeSystemKeys: function(aInstallDir) {
+    let parentKey;
+    let uninstallKey;
+    let subKey;
+
+    try {
+      parentKey = Cc["@mozilla.org/windows-registry-key;1"].
+                  createInstance(Ci.nsIWindowsRegKey);
+      parentKey.open(parentKey.ROOT_KEY_CURRENT_USER,
+                     "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
+                     parentKey.ACCESS_WRITE);
+      uninstallKey = parentKey.createChild("Uninstall", parentKey.ACCESS_WRITE)
+      subKey = uninstallKey.createChild(this.uninstallSubkeyStr,
+                                        uninstallKey.ACCESS_WRITE);
+
+      subKey.writeStringValue("DisplayName", this.appName);
+
+      let uninstallerPath = OS.Path.join(aInstallDir, this.uninstallerFile);
+
+      subKey.writeStringValue("UninstallString", '"' + uninstallerPath + '"');
+      subKey.writeStringValue("InstallLocation", '"' + aInstallDir + '"');
+      subKey.writeStringValue("AppFilename", this.appNameAsFilename);
+      subKey.writeStringValue("DisplayIcon", OS.Path.join(aInstallDir,
+                                                          this.iconPath));
+
+      let date = new Date();
+      let year = date.getYear().toString();
+      let month = date.getMonth();
+      if (month < 10) {
+        month = "0" + month;
+      }
+      let day = date.getDate();
+      if (day < 10) {
+        day = "0" + day;
+      }
+      subKey.writeStringValue("InstallDate", year + month + day);
+      if (this.version) {
+        subKey.writeStringValue("DisplayVersion", this.version);
+      }
+      if (this.developerName) {
+        subKey.writeStringValue("Publisher", this.developerName);
+      }
+      subKey.writeStringValue("URLInfoAbout", this.developerUrl);
+      if (this.size) {
+        subKey.writeIntValue("EstimatedSize", this.size);
+      }
+
+      subKey.writeIntValue("NoModify", 1);
+      subKey.writeIntValue("NoRepair", 1);
+    } catch(ex) {
+      throw ex;
+    } finally {
+      if(subKey) subKey.close();
+      if(uninstallKey) uninstallKey.close();
+      if(parentKey) parentKey.close();
+    }
+  },
+
+  /**
+   * Creates a shortcut file inside the app installation folder and makes
+   * two copies of it: one into the desktop and one into the start menu.
+   */
+  _createShortcutFiles: function(aInstallDir) {
+    let shortcut = getFile(aInstallDir, this.shortcutName).
+                      QueryInterface(Ci.nsILocalFileWin);
+
+    /* function nsILocalFileWin.setShortcut(targetFile, workingDir, args,
+                                            description, iconFile, iconIndex) */
+
+    shortcut.setShortcut(getFile(aInstallDir, this.webapprt),
+                         getFile(aInstallDir),
+                         null,
+                         this.shortDescription,
+                         getFile(aInstallDir, this.iconPath),
+                         0);
+
+    shortcut.copyTo(getFile(DESKTOP_DIR), this.shortcutName);
+    shortcut.copyTo(getFile(PROGS_DIR), this.shortcutName);
+
+    shortcut.followLinks = false;
+    shortcut.remove(false);
+  },
+
+  /**
+   * Process the icon from the imageStream as retrieved from
+   * the URL by getIconForApp(). This will save the icon to the
+   * topwindow.ico file.
+   *
+   * @param aMimeType     the icon mimetype
+   * @param aImageStream  the stream for the image data
+   * @param aDir          the directory where the icon should be stored
+   */
+  _processIcon: function(aMimeType, aImageStream, aDir) {
+    let deferred = Promise.defer();
+
+    let imgTools = Cc["@mozilla.org/image/tools;1"].
+                   createInstance(Ci.imgITools);
+
+    let imgContainer = imgTools.decodeImage(aImageStream, aMimeType);
+    let iconStream = imgTools.encodeImage(imgContainer,
+                                          "image/vnd.microsoft.icon",
+                                          "format=bmp;bpp=32");
+
+    let tmpIconFile = getFile(aDir, this.iconPath);
+
+    let outputStream = FileUtils.openSafeFileOutputStream(tmpIconFile);
+    NetUtil.asyncCopy(iconStream, outputStream, function(aResult) {
+      if (Components.isSuccessCode(aResult)) {
+        deferred.resolve();
+      } else {
+        deferred.reject("Failure copying icon: " + aResult);
+      }
+    });
+
+    return deferred.promise;
+  }
+}
--- a/toolkit/webapps/moz.build
+++ b/toolkit/webapps/moz.build
@@ -1,13 +1,15 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXTRA_PP_JS_MODULES += [
+    'NativeApp.jsm',
     'WebappOSUtils.jsm',
-    'WebappsInstaller.jsm',
 ]
 
+MOCHITEST_CHROME_MANIFESTS += ['tests/chrome.ini']
+
 if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
     DEFINES['MOZ_FENNEC'] = True
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/chrome.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+support-files =
+  head.js
+
+[test_hosted.xul]
+skip-if = os == "mac"
+[test_packaged.xul]
+skip-if = os == "mac"
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/head.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+
+function checkFiles(files) {
+  return Task.spawn(function*() {
+    for (let file of files) {
+      if (!(yield OS.File.exists(file))) {
+        info("File doesn't exist: " + file);
+        return false;
+      }
+    }
+
+    return true;
+  });
+}
+
+function checkDateHigherThan(files, date) {
+  return Task.spawn(function*() {
+    for (let file of files) {
+      if (!(yield OS.File.exists(file))) {
+        info("File doesn't exist: " + file);
+        return false;
+      }
+
+      let stat = yield OS.File.stat(file);
+      if (!(stat.lastModificationDate > date)) {
+        info("File not newer: " + file);
+        return false;
+      }
+    }
+
+    return true;
+  });
+}
+
+function wait(time) {
+  let deferred = Promise.defer();
+
+  setTimeout(function() {
+    deferred.resolve();
+  }, time);
+
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/test_hosted.xul
@@ -0,0 +1,311 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=898647
+-->
+<window title="Mozilla Bug 898647"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"/>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=898647"
+     target="_blank">Mozilla Bug 898647</a>
+  </body>
+
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 898647 **/
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NativeApp.jsm");
+Cu.import("resource://gre/modules/WebappOSUtils.jsm");
+
+let manifest = {
+  name: "Sample hosted app",
+};
+
+let app = {
+  name: "Sample hosted app",
+  manifestURL: "http://example.com/sample.manifest",
+  manifest: manifest,
+  origin: "http://example.com/",
+  categories: [],
+  installOrigin: "http://example.com/",
+  receipts: [],
+  installTime: Date.now(),
+};
+
+let profileDir;
+let profilesIni;
+let installPath;
+
+let installedFiles;
+let tempUpdatedFiles;
+let updatedFiles;
+
+let cleanup;
+
+if (navigator.platform.startsWith("Linux")) {
+  installPath = OS.Path.join(OS.Constants.Path.homeDir, "." + WebappOSUtils.getUniqueName(app));
+
+  let xdg_data_home = Cc["@mozilla.org/process/environment;1"].
+                      getService(Ci.nsIEnvironment).
+                      get("XDG_DATA_HOME");
+  if (!xdg_data_home) {
+    xdg_data_home = OS.Path.join(OS.Constants.Path.homeDir, ".local", "share");
+  }
+
+  let desktopINI = OS.Path.join(xdg_data_home, "applications",
+                                "owa-" + WebappOSUtils.getUniqueName(app) + ".desktop");
+
+  installedFiles = [
+    OS.Path.join(installPath, "icon.png"),
+    OS.Path.join(installPath, "webapprt-stub"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    desktopINI,
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "icon.png"),
+    OS.Path.join(installPath, "update", "webapp.json"),
+    OS.Path.join(installPath, "update", "webapp.ini"),
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "icon.png"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    desktopINI,
+  ];
+
+  profilesIni = OS.Path.join(installPath, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.remove(desktopINI, { ignoreAbsent: true });
+    });
+  };
+} else if (navigator.platform.startsWith("Win")) {
+  installPath = OS.Path.join(OS.Constants.Path.winAppDataDir, WebappOSUtils.getUniqueName(app));
+
+  let desktopShortcut = OS.Path.join(OS.Constants.Path.desktopDir, "Sample hosted app.lnk");
+  let startMenuShortcut = OS.Path.join(OS.Constants.Path.winStartMenuProgsDir, "Sample hosted app.lnk");
+
+  installedFiles = [
+    OS.Path.join(installPath, "Sample hosted app.exe"),
+    OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "uninstall", "uninstall.log"),
+    OS.Path.join(installPath, "uninstall", "webapp-uninstaller.exe"),
+    desktopShortcut,
+    startMenuShortcut,
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "update", "webapp.json"),
+    OS.Path.join(installPath, "update", "webapp.ini"),
+    OS.Path.join(installPath, "update", "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "update", "uninstall", "uninstall.log"),
+    OS.Path.join(installPath, "update", "uninstall", "webapp-uninstaller.exe"),
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "uninstall", "uninstall.log"),
+    desktopShortcut,
+    startMenuShortcut,
+  ];
+
+  profilesIni = OS.Path.join(installPath, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      let uninstallKey;
+      try {
+        uninstallKey = Cc["@mozilla.org/windows-registry-key;1"].
+                       createInstance(Ci.nsIWindowsRegKey);
+        uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
+                          "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
+                          uninstallKey.ACCESS_WRITE);
+        if (uninstallKey.hasChild(WebappOSUtils.getUniqueName(app))) {
+          uninstallKey.removeChild(WebappOSUtils.getUniqueName(app));
+        }
+      } catch (e) {
+      } finally {
+        if (uninstallKey) {
+          uninstallKey.close();
+        }
+      }
+
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.remove(desktopShortcut, { ignoreAbsent: true });
+      yield OS.File.remove(startMenuShortcut, { ignoreAbsent: true });
+    });
+  };
+} else if (navigator.platform.startsWith("Mac")) {
+  installPath = OS.Path.join(OS.Constants.Path.macLocalApplicationsDir, "Sample hosted app.app");
+  let appProfileDir = OS.Path.join(OS.Constants.Path.macUserLibDir, "Application Support",
+                                   WebappOSUtils.getUniqueName(app));
+
+  installedFiles = [
+    OS.Path.join(installPath, "Contents", "Info.plist"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapprt"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(appProfileDir, "webapp.json"),
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "Contents", "Info.plist"),
+    OS.Path.join(installPath, "update", "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "update", "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(installPath, "update", "webapp.json")
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "Contents", "Info.plist"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(appProfileDir, "webapp.json"),
+  ];
+
+  profilesIni = OS.Path.join(appProfileDir, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.removeDir(appProfileDir, { ignoreAbsent: true });
+    });
+  };
+}
+
+let old_dry_run;
+try {
+  old_dry_run = Services.prefs.getBoolPref("browser.mozApps.installer.dry_run");
+} catch (ex) {}
+
+Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", false);
+
+SimpleTest.registerCleanupFunction(function() {
+  if (old_dry_run === undefined) {
+    Services.prefs.clearUserPref("browser.mozApps.installer.dry_run");
+  } else {
+    Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", old_dry_run);
+  }
+
+  cleanup();
+});
+
+Task.spawn(function() {
+  // Get to a clean state before the test
+  yield cleanup();
+
+  let nativeApp = new NativeApp(app, manifest, app.categories);
+  ok(nativeApp, "NativeApp object created");
+
+  info("Test update for an uninstalled application");
+  try {
+    yield nativeApp.prepareUpdate(manifest);
+    ok(false, "Didn't thrown");
+  } catch (ex) {
+    is(ex, "The application isn't installed", "Exception thrown");
+  }
+
+  profileDir = nativeApp.createProfile();
+  ok(profileDir && profileDir.exists(), "Profile directory created");
+  ok((yield OS.File.exists(profilesIni)), "profiles.ini file created");
+
+  // Install application
+  info("Test installation");
+  yield nativeApp.install(manifest);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Files correctly written");
+
+  let stat = yield OS.File.stat(installPath);
+  let installTime = stat.lastModificationDate;
+
+  // Wait one second, otherwise the last modification date is the same.
+  yield wait(1000);
+
+  // Reinstall application
+  info("Test reinstallation");
+  yield nativeApp.install(manifest);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory");
+
+  yield nativeApp.applyUpdate();
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed");
+  ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher");
+
+  stat = yield OS.File.stat(installPath);
+  installTime = stat.lastModificationDate;
+
+  // Wait one second, otherwise the last modification date is the same.
+  yield wait(1000);
+
+  // Update application
+  info("Test update");
+  yield nativeApp.prepareUpdate(manifest);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory");
+
+  yield nativeApp.applyUpdate();
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed");
+  ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher");
+
+  SimpleTest.finish();
+}).then(null, function(e) {
+  ok(false, "Error during test: " + e);
+  SimpleTest.finish();
+});
+
+]]>
+</script>
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/webapps/tests/test_packaged.xul
@@ -0,0 +1,337 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=898647
+-->
+<window title="Mozilla Bug 898647"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript"
+          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="head.js"/>
+
+  <!-- test results are displayed in the html:body -->
+  <body xmlns="http://www.w3.org/1999/xhtml">
+  <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=898647"
+     target="_blank">Mozilla Bug 898647</a>
+  </body>
+
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 898647 **/
+
+"use strict";
+
+SimpleTest.waitForExplicitFinish();
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NativeApp.jsm");
+Cu.import("resource://gre/modules/WebappOSUtils.jsm");
+
+let zipPath = OS.Path.join(OS.Constants.Path.profileDir, "sample.zip");
+
+let manifest = {
+  name: "Sample packaged app",
+  version: "0.1a",
+  size: 777,
+  package_path: "/sample.zip",
+};
+
+let app = {
+  name: "Sample packaged app",
+  manifestURL: "http://example.com/sample.manifest",
+  manifest: manifest,
+  updateManifest: manifest,
+  origin: "http://example.com/",
+  categories: [],
+  installOrigin: "http://example.com/",
+  receipts: [],
+  installTime: Date.now(),
+};
+
+let profileDir;
+let profilesIni;
+let installPath;
+
+let installedFiles;
+let tempUpdatedFiles;
+let updatedFiles;
+
+let cleanup;
+
+if (navigator.platform.startsWith("Linux")) {
+  installPath = OS.Path.join(OS.Constants.Path.homeDir, "." + WebappOSUtils.getUniqueName(app));
+
+  let xdg_data_home = Cc["@mozilla.org/process/environment;1"].
+                      getService(Ci.nsIEnvironment).
+                      get("XDG_DATA_HOME");
+  if (!xdg_data_home) {
+    xdg_data_home = OS.Path.join(OS.Constants.Path.homeDir, ".local", "share");
+  }
+
+  let desktopINI = OS.Path.join(xdg_data_home, "applications",
+                                "owa-" + WebappOSUtils.getUniqueName(app) + ".desktop");
+
+  installedFiles = [
+    OS.Path.join(installPath, "icon.png"),
+    OS.Path.join(installPath, "webapprt-stub"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "application.zip"),
+    desktopINI,
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "icon.png"),
+    OS.Path.join(installPath, "update", "webapp.json"),
+    OS.Path.join(installPath, "update", "webapp.ini"),
+    OS.Path.join(installPath, "update", "application.zip"),
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "icon.png"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "application.zip"),
+    desktopINI,
+  ];
+
+  profilesIni = OS.Path.join(installPath, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.remove(desktopINI, { ignoreAbsent: true });
+    });
+  };
+} else if (navigator.platform.startsWith("Win")) {
+  installPath = OS.Path.join(OS.Constants.Path.winAppDataDir, WebappOSUtils.getUniqueName(app));
+
+  let desktopShortcut = OS.Path.join(OS.Constants.Path.desktopDir, "Sample packaged app.lnk");
+  let startMenuShortcut = OS.Path.join(OS.Constants.Path.winStartMenuProgsDir, "Sample packaged app.lnk");
+
+  installedFiles = [
+    OS.Path.join(installPath, "Sample packaged app.exe"),
+    OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "application.zip"),
+    OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "uninstall", "uninstall.log"),
+    OS.Path.join(installPath, "uninstall", "webapp-uninstaller.exe"),
+    desktopShortcut,
+    startMenuShortcut,
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "update", "webapp.json"),
+    OS.Path.join(installPath, "update", "webapp.ini"),
+    OS.Path.join(installPath, "update", "application.zip"),
+    OS.Path.join(installPath, "update", "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "update", "uninstall", "uninstall.log"),
+    OS.Path.join(installPath, "update", "uninstall", "webapp-uninstaller.exe"),
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "chrome", "icons", "default", "default.ico"),
+    OS.Path.join(installPath, "webapp.json"),
+    OS.Path.join(installPath, "webapp.ini"),
+    OS.Path.join(installPath, "application.zip"),
+    OS.Path.join(installPath, "uninstall", "shortcuts_log.ini"),
+    OS.Path.join(installPath, "uninstall", "uninstall.log"),
+    desktopShortcut,
+    startMenuShortcut,
+  ];
+
+  profilesIni = OS.Path.join(installPath, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      let uninstallKey;
+      try {
+        uninstallKey = Cc["@mozilla.org/windows-registry-key;1"].
+                       createInstance(Ci.nsIWindowsRegKey);
+        uninstallKey.open(uninstallKey.ROOT_KEY_CURRENT_USER,
+                          "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
+                          uninstallKey.ACCESS_WRITE);
+        if (uninstallKey.hasChild(WebappOSUtils.getUniqueName(app))) {
+          uninstallKey.removeChild(WebappOSUtils.getUniqueName(app));
+        }
+      } catch (e) {
+      } finally {
+        if (uninstallKey) {
+          uninstallKey.close();
+        }
+      }
+
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.remove(desktopShortcut, { ignoreAbsent: true });
+      yield OS.File.remove(startMenuShortcut, { ignoreAbsent: true });
+    });
+  };
+} else if (navigator.platform.startsWith("Mac")) {
+  installPath = OS.Path.join(OS.Constants.Path.macLocalApplicationsDir, "Sample packaged app.app");
+  let appProfileDir = OS.Path.join(OS.Constants.Path.macUserLibDir, "Application Support",
+                                   WebappOSUtils.getUniqueName(app));
+
+  installedFiles = [
+    OS.Path.join(installPath, "Contents", "Info.plist"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapprt"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(installPath, "Contents", "Resources", "application.zip"),
+    OS.Path.join(appProfileDir, "webapp.json"),
+  ];
+  tempUpdatedFiles = [
+    OS.Path.join(installPath, "update", "Contents", "Info.plist"),
+    OS.Path.join(installPath, "update", "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "update", "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(installPath, "update", "Contents", "Resources", "application.zip"),
+    OS.Path.join(installPath, "update", "webapp.json"),
+  ];
+  updatedFiles = [
+    OS.Path.join(installPath, "Contents", "Info.plist"),
+    OS.Path.join(installPath, "Contents", "MacOS", "webapp.ini"),
+    OS.Path.join(installPath, "Contents", "Resources", "appicon.icns"),
+    OS.Path.join(installPath, "Contents", "Resources", "application.zip"),
+    OS.Path.join(appProfileDir, "webapp.json"),
+  ];
+
+  profilesIni = OS.Path.join(appProfileDir, "profiles.ini");
+
+  cleanup = function() {
+    return Task.spawn(function*() {
+      if (profileDir) {
+        yield OS.File.removeDir(profileDir.parent.path, { ignoreAbsent: true });
+      }
+
+      yield OS.File.removeDir(installPath, { ignoreAbsent: true });
+
+      yield OS.File.removeDir(appProfileDir, { ignoreAbsent: true });
+    });
+  };
+}
+
+let old_dry_run;
+try {
+  old_dry_run = Services.prefs.getBoolPref("browser.mozApps.installer.dry_run");
+} catch (ex) {}
+
+Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", false);
+
+SimpleTest.registerCleanupFunction(function() {
+  if (old_dry_run === undefined) {
+    Services.prefs.clearUserPref("browser.mozApps.installer.dry_run");
+  } else {
+    Services.prefs.setBoolPref("browser.mozApps.installer.dry_run", old_dry_run);
+  }
+
+  cleanup();
+});
+
+Task.spawn(function() {
+  // Get to a clean state before the test
+  yield cleanup();
+
+  let zipFile = yield OS.File.open(zipPath, { create: true });
+  yield zipFile.close();
+
+  let nativeApp = new NativeApp(app, manifest, app.categories);
+  ok(nativeApp, "NativeApp object created");
+
+  info("Test update for an application that isn't installed");
+  try {
+    yield nativeApp.prepareUpdate(manifest, zipPath);
+    ok(false, "Didn't thrown");
+  } catch (ex) {
+    is(ex, "The application isn't installed", "Exception thrown");
+  }
+
+  profileDir = nativeApp.createProfile();
+  ok(profileDir && profileDir.exists(), "Profile directory created");
+  ok((yield OS.File.exists(profilesIni)), "profiles.ini file created");
+
+  // Install application
+  info("Test installation");
+  yield nativeApp.install(manifest, zipPath);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Files correctly written");
+
+  let stat = yield OS.File.stat(installPath);
+  let installTime = stat.lastModificationDate;
+
+  // Wait one second, otherwise the last modification date is the same.
+  yield wait(1000);
+
+  // Reinstall application
+  info("Test reinstallation");
+
+  zipFile = yield OS.File.open(zipPath, { create: true });
+  yield zipFile.close();
+
+  yield nativeApp.install(manifest, zipPath);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory");
+
+  yield nativeApp.applyUpdate();
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed");
+  ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher");
+
+  stat = yield OS.File.stat(installPath);
+  installTime = stat.lastModificationDate;
+
+  // Wait one second, otherwise the last modification date is the same.
+  yield wait(1000);
+
+  // Update application
+  info("Test update");
+
+  zipFile = yield OS.File.open(zipPath, { create: true });
+  yield zipFile.close();
+
+  yield nativeApp.prepareUpdate(manifest, zipPath);
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok((yield checkFiles(tempUpdatedFiles)), "Files correctly written in the update subdirectory");
+
+  yield nativeApp.applyUpdate();
+  while (!WebappOSUtils.isLaunchable(app)) {
+    yield wait(1000);
+  }
+  ok(true, "App launchable");
+  ok((yield checkFiles(installedFiles)), "Installation not corrupted");
+  ok(!(yield OS.File.exists(OS.Path.join(installPath, "update"))), "Update directory removed");
+  ok((yield checkDateHigherThan(updatedFiles, installTime)), "Modification date higher");
+
+  SimpleTest.finish();
+}).then(null, function(e) {
+  ok(false, "Error during test: " + e);
+  SimpleTest.finish();
+});
+
+]]>
+</script>
+</window>
--- a/webapprt/Startup.jsm
+++ b/webapprt/Startup.jsm
@@ -8,27 +8,23 @@
  * loaded.  */
 
 this.EXPORTED_SYMBOLS = ["startup"];
 
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
-// Initialize DOMApplicationRegistry by importing Webapps.jsm.
-Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 
-// Initialize window-independent handling of webapps- notifications.
-Cu.import("resource://webapprt/modules/WebappsHandler.jsm");
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
 Cu.import("resource://webapprt/modules/WebRTCHandler.jsm");
 
 const PROFILE_DIR = OS.Constants.Path.profileDir;
 
 function isFirstRunOrUpdate() {
   let savedBuildID = null;
   try {
@@ -88,25 +84,37 @@ this.startup = function(window) {
       deferredWindowLoad.resolve();
     } else {
       window.addEventListener("DOMContentLoaded", function onLoad() {
         window.removeEventListener("DOMContentLoaded", onLoad, false);
         deferredWindowLoad.resolve();
       });
     }
 
+    let appUpdated = false;
+    let updatePending = yield WebappRT.isUpdatePending();
+    if (updatePending) {
+      appUpdated = yield WebappRT.applyUpdate();
+    }
+
+    yield WebappRT.loadConfig();
+
+    // Initialize DOMApplicationRegistry by importing Webapps.jsm.
+    Cu.import("resource://gre/modules/Webapps.jsm");
+    // Initialize window-independent handling of webapps- notifications.
+    Cu.import("resource://webapprt/modules/WebappManager.jsm");
+
     // Wait for webapps registry loading.
     yield DOMApplicationRegistry.registryStarted;
 
     let manifestURL = WebappRT.config.app.manifestURL;
     if (manifestURL) {
       // On firstrun, set permissions to their default values.
       // When the webapp runtime is updated, update the permissions.
-      // TODO: Update the permissions when the application is updated.
-      if (isFirstRunOrUpdate(Services.prefs)) {
+      if (isFirstRunOrUpdate(Services.prefs) || appUpdated) {
         PermissionsInstaller.installPermissions(WebappRT.config.app, true);
         yield createBrandingFiles();
       }
     }
 
     // Branding substitution
     let aliasFile = Components.classes["@mozilla.org/file/local;1"]
                               .createInstance(Ci.nsIFile);
@@ -130,10 +138,12 @@ this.startup = function(window) {
 
     if (WebappRT.config.app.manifest.fullscreen) {
       appBrowser.addEventListener("load", function onLoad() {
         appBrowser.removeEventListener("load", onLoad, true);
         appBrowser.contentDocument.
           documentElement.mozRequestFullScreen();
       }, true);
     }
+
+    WebappRT.startUpdateService();
   }).then(null, Cu.reportError.bind(Cu));
 }
rename from webapprt/WebappsHandler.jsm
rename to webapprt/WebappManager.jsm
--- a/webapprt/WebappsHandler.jsm
+++ b/webapprt/WebappManager.jsm
@@ -1,28 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = ["WebappsHandler"];
+this.EXPORTED_SYMBOLS = ["WebappManager"];
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Webapps.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
-Cu.import("resource://gre/modules/WebappsInstaller.jsm");
+Cu.import("resource://gre/modules/NativeApp.jsm");
 Cu.import("resource://gre/modules/WebappOSUtils.jsm");
+Cu.import("resource://webapprt/modules/WebappRT.jsm");
 
-this.WebappsHandler = {
+this.WebappManager = {
   observe: function(subject, topic, data) {
     data = JSON.parse(data);
     data.mm = subject;
 
     switch (topic) {
       case "webapps-ask-install":
         let chromeWin = Services.wm.getOuterWindowWithId(data.oid);
         if (chromeWin)
@@ -32,16 +33,23 @@ this.WebappsHandler = {
         WebappOSUtils.launch(data);
         break;
       case "webapps-uninstall":
         WebappOSUtils.uninstall(data);
         break;
     }
   },
 
+  update: function(aApp, aManifest, aZipPath) {
+    let nativeApp = new NativeApp(aApp, aManifest,
+                                  WebappRT.config.app.categories,
+                                  WebappRT.config.registryDir);
+    nativeApp.prepareUpdate(aManifest, aZipPath);
+  },
+
   doInstall: function(data, window) {
     let jsonManifest = data.isPackage ? data.app.updateManifest : data.app.manifest;
     let manifest = new ManifestHelper(jsonManifest, data.app.origin);
     let name = manifest.name;
     let bundle = Services.strings.createBundle("chrome://webapprt/locale/webapp.properties");
 
     let choice = Services.prompt.confirmEx(
       window,
@@ -54,36 +62,39 @@ this.WebappsHandler = {
       bundle.GetStringFromName("webapps.install.install"),
       bundle.GetStringFromName("webapps.install.dontinstall"),
       null,
       null,
       {});
 
     // Perform the install if the user allows it
     if (choice == 0) {
-      let shell = WebappsInstaller.init(data);
-
-      if (shell) {
-        let localDir = null;
-        if (shell.appProfile) {
-          localDir = shell.appProfile.localDir;
-        }
+      let nativeApp = new NativeApp(data.app, jsonManifest,
+                                    WebappRT.config.app.categories,
+                                    WebappRT.config.registryDir);
+      let localDir;
+      try {
+        localDir = nativeApp.createProfile();
+      } catch (ex) {
+        DOMApplicationRegistry.denyInstall(aData);
+        return;
+      }
 
-        DOMApplicationRegistry.confirmInstall(data, localDir,
-          function (aManifest) {
-            WebappsInstaller.install(data, aManifest);
-          }
-        );
-      } else {
-        DOMApplicationRegistry.denyInstall(data);
-      }
+      DOMApplicationRegistry.confirmInstall(data, localDir,
+        function (aManifest, aZipPath) {
+          nativeApp.install(aManifest, aZipPath);
+        }
+      );
     } else {
       DOMApplicationRegistry.denyInstall(data);
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference])
 };
 
-Services.obs.addObserver(WebappsHandler, "webapps-ask-install", false);
-Services.obs.addObserver(WebappsHandler, "webapps-launch", false);
-Services.obs.addObserver(WebappsHandler, "webapps-uninstall", false);
+Services.obs.addObserver(WebappManager, "webapps-ask-install", false);
+Services.obs.addObserver(WebappManager, "webapps-launch", false);
+Services.obs.addObserver(WebappManager, "webapps-uninstall", false);
+Services.obs.addObserver(WebappManager, "webapps-update", false);
+
+DOMApplicationRegistry.registerUpdateHandler(WebappManager.update);
--- a/webapprt/WebappRT.jsm
+++ b/webapprt/WebappRT.jsm
@@ -10,48 +10,30 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
   "resource://gre/modules/FileUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+  "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, 'NativeApp',
+  'resource://gre/modules/NativeApp.jsm');
+
 XPCOMUtils.defineLazyServiceGetter(this, "appsService",
                                   "@mozilla.org/AppsService;1",
                                   "nsIAppsService");
 
 this.WebappRT = {
-  _config: null,
-
-  get config() {
-    if (this._config)
-      return this._config;
-
-    let webappFile = FileUtils.getFile("AppRegD", ["webapp.json"]);
-
-    let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
-                      createInstance(Ci.nsIFileInputStream);
-    inputStream.init(webappFile, -1, 0, Ci.nsIFileInputStream.CLOSE_ON_EOF);
-    let json = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
-    let config = json.decodeFromStream(inputStream, webappFile.fileSize);
-
-    return this._config = config;
-  },
-
-  // This exists to support test mode, which installs webapps after startup.
-  // Ideally we wouldn't have to have a setter, as tests can just delete
-  // the getter and then set the property.  But the object to which they set it
-  // will have a reference to its global object, so our reference to it
-  // will leak that object (per bug 780674).  The setter enables us to clone
-  // the new value so we don't actually retain a reference to it.
-  set config(newVal) {
-    this._config = JSON.parse(JSON.stringify(newVal));
-  },
-
   get launchURI() {
     let manifest = this.localeManifest;
     return manifest.fullLaunchPath();
   },
 
   get localeManifest() {
     return new ManifestHelper(this.config.app.manifest,
                               this.config.app.origin);
@@ -60,9 +42,98 @@ this.WebappRT = {
   get appID() {
     let manifestURL = WebappRT.config.app.manifestURL;
     if (!manifestURL) {
       return Ci.nsIScriptSecurityManager.NO_APP_ID;
     }
 
     return appsService.getAppLocalIdByManifestURL(manifestURL);
   },
+
+  loadConfig: function() {
+    if (this.config) {
+      return;
+    }
+
+    let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path,
+                                  "webapp.json");
+    this.config = yield AppsUtils.loadJSONAsync(webappJson);
+  },
+
+  isUpdatePending: Task.async(function*() {
+    let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path,
+                                  "update", "webapp.json");
+
+    if (!(yield OS.File.exists(webappJson))) {
+      return false;
+    }
+
+    return true;
+  }),
+
+  applyUpdate: Task.async(function*() {
+    let webappJson = OS.Path.join(Services.dirsvc.get("AppRegD", Ci.nsIFile).path,
+                                  "update", "webapp.json");
+    let config = yield AppsUtils.loadJSONAsync(webappJson);
+
+    let nativeApp = new NativeApp(config.app, config.app.manifest,
+                                  config.app.categories,
+                                  config.registryDir);
+    try {
+      yield nativeApp.applyUpdate();
+    } catch (ex) {
+      return false;
+    }
+
+    // The update has been applied successfully, the new config file
+    // is the config file that was in the update directory.
+    this.config = config;
+
+    return true;
+  }),
+
+  startUpdateService: function() {
+    let manifestURL = WebappRT.config.app.manifestURL;
+    // We used to install apps without storing their manifest URL.
+    // Now we can't update them.
+    if (!manifestURL) {
+      return;
+    }
+
+    // Check for updates once a day.
+    let timerManager = Cc["@mozilla.org/updates/timer-manager;1"].
+                       getService(Ci.nsIUpdateTimerManager);
+    timerManager.registerTimer("updateTimer", () => {
+      let window = Services.wm.getMostRecentWindow("webapprt:webapp");
+      window.navigator.mozApps.mgmt.getAll().onsuccess = function() {
+        let thisApp = null;
+        for (let app of this.result) {
+          if (app.manifestURL == manifestURL) {
+            thisApp = app;
+            break;
+          }
+        }
+
+        // This shouldn't happen if the app is installed.
+        if (!thisApp) {
+          Cu.reportError("Couldn't find the app in the webapps registry");
+          return;
+        }
+
+        thisApp.ondownloadavailable = () => {
+          // Download available, download it!
+          thisApp.download();
+        };
+
+        thisApp.ondownloadsuccess = () => {
+          // Update downloaded, apply it!
+          window.navigator.mozApps.mgmt.applyDownload(thisApp);
+        };
+
+        thisApp.ondownloadapplied = () => {
+          // Application updated, nothing to do.
+        };
+
+        thisApp.checkForUpdate();
+      }
+    }, 24 * 60 * 60);
+  },
 };
--- a/webapprt/moz.build
+++ b/webapprt/moz.build
@@ -19,18 +19,18 @@ EXTRA_COMPONENTS += [
     'ContentPermission.js',
     'DirectoryProvider.js',
     'PaymentUIGlue.js',
 ]
 
 EXTRA_JS_MODULES += [
     'RemoteDebugger.jsm',
     'Startup.jsm',
+    'WebappManager.jsm',
     'WebappRT.jsm',
-    'WebappsHandler.jsm',
     'WebRTCHandler.jsm',
 ]
 
 MOCHITEST_WEBAPPRT_CHROME_MANIFESTS += ['test/chrome/webapprt.ini']
 MOCHITEST_MANIFESTS += ['test/content/mochitest.ini']
 
 # Place webapprt resources in a separate app dir
 DIST_SUBDIR = 'webapprt'