Bug 609043 - Add support for Open Web Apps [r=mfinkle]
authorFabrice Desré <fabrice@mozilla.com>
Mon, 15 Aug 2011 12:16:50 -0700
changeset 75301 f085bbca2ee95bf54b689fee1d6ee1b7027eab27
parent 75300 180442fd64486339031ed83a956dcb35c2a16b61
child 75302 3f2f6b0897bc854e343d5727f7c9ffd96f9a6527
child 75335 fbc41a899acbb5f7214607abb548fe00c90df14e
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersmfinkle
bugs609043
milestone8.0a1
Bug 609043 - Add support for Open Web Apps [r=mfinkle]
mobile/chrome/content/browser-ui.js
mobile/chrome/content/common-ui.js
mobile/chrome/content/content.js
mobile/chrome/content/injected.js
mobile/chrome/jar.mn
mobile/components/Makefile.in
mobile/components/MobileComponents.manifest
mobile/components/WebappsSupport.idl
mobile/components/WebappsSupport.js
mobile/installer/package-manifest.in
mobile/modules/Makefile.in
mobile/modules/openWebapps.jsm
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -545,16 +545,17 @@ var BrowserUI = {
       // Init helpers
       BadgeHandlers.register(BrowserUI._edit.popup);
       FormHelperUI.init();
       FindHelperUI.init();
       PageActions.init();
       FullScreenVideo.init();
       NewTabPopup.init();
       CharsetMenu.init();
+      WebappsUI.init();
 
       // If some add-ons were disabled during during an application update, alert user
       let addonIDs = AddonManager.getStartupChanges("disabled");
       if (addonIDs.length > 0) {
         let disabledStrings = Strings.browser.GetStringFromName("alertAddonsDisabled");
         let label = PluralForm.get(addonIDs.length, disabledStrings).replace("#1", addonIDs.length);
         let image = "chrome://browser/skin/images/alert-addons-30.png";
 
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -1672,69 +1672,168 @@ var CharsetMenu = {
     let history = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
     history.setCharsetForURI(browser.documentURI, aCharset);
   }
 
 };
 
 var WebappsUI = {
   _dialog: null,
-  _manifest: null,
   _perms: [],
-  
+  _openwebapps: null,
+  _application: null,
+
+  init: function() {
+    Cu.import("resource:///modules/openWebapps.jsm");
+    messageManager.addMessageListener("OpenWebapps:Install", this);
+    messageManager.addMessageListener("OpenWebapps:GetInstalledBy", this);
+    messageManager.addMessageListener("OpenWebapps:AmInstalled", this);
+    messageManager.addMessageListener("OpenWebapps:MgmtLaunch", this);
+    messageManager.addMessageListener("OpenWebapps:MgmtList", this);
+    messageManager.addMessageListener("OpenWebapps:MgmtUninstall", this);
+  },
+
+  // converts a manifest to an application as expected by openwebapps.install()
+  convertManifest: function(aData) {
+    let app = {
+      manifest : JSON.parse(aData.manifest),
+      installData : aData.installData,
+      storeURI : aData.storeURI,
+      manifestURI : aData.manifestURIs,
+      capabilities : [],
+      callbackID : aData.callbackID
+    }
+
+    let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry).QueryInterface(Ci.nsIToolkitChromeRegistry);
+    let locale = chrome.getSelectedLocale("browser");
+
+    let localeRoot;
+    if (app.manifest.locales)
+      localeRoot = app.manifest.locales[locale];
+
+    if (!localeRoot)
+      localeRoot = app.manifest;
+
+    let baseURI = Services.io.newURI(aData.manifestURI, null, null);
+    app.title = localeRoot.name || app.manifest.name;
+
+    // choose the larger icon
+    let max = 0;
+    let icon;
+    for (let size in app.manifest.icons) {
+      let iSize = parseInt(size);
+      if (iSize > max) {
+        icon = baseURI.resolve(app.manifest.icons[size]);
+        max = iSize;
+      }
+    }
+    if (icon)
+      app.iconURI = icon;
+
+    let root = baseURI.resolve("/").toString();
+    app.appURI = app.manifest.launch_path ? baseURI.resolve(app.manifest.launch_path) : root.substring(0, root.length - 1);
+
+    return app;
+  },
+
+  receiveMessage: function(aMessage) {
+    let browser = aMessage.target;
+    switch(aMessage.name) {
+      case "OpenWebapps:Install":
+        WebappsUI._openwebapps = browser;
+        WebappsUI.show(WebappsUI.convertManifest(aMessage.json));
+        break;
+      case "OpenWebapps:GetInstalledBy":
+        let apps = OpenWebapps.getInstalledBy(aMessage.json.storeURI);
+        browser.messageManager.sendAsyncMessage("OpenWebapps:GetInstalledBy:Return",
+            { apps: apps, callbackID: aMessage.json.callbackID });
+        break;
+      case "OpenWebapps:AmInstalled":
+        let app = OpenWebapps.amInstalled(aMessage.json.appURI);
+        browser.messageManager.sendAsyncMessage("OpenWebapps:AmInstalled:Return",
+            { installed: app != null, app: app, callbackID: aMessage.json.callbackID });
+        break;
+      case "OpenWebapps:MgmtList":
+        let list = OpenWebapps.mgmtList();
+        browser.messageManager.sendAsyncMessage("OpenWebapps:MgmtList:Return",
+            { ok: true, apps: list, callbackID: aMessage.json.callbackID });
+        break;
+      case "OpenWebapps:MgmtLaunch":
+        let res = OpenWebapps.mgmtLaunch(aMessage.json.origin);
+        browser.messageManager.sendAsyncMessage("OpenWebapps:MgmtLaunch:Return",
+            { ok: res, callbackID: aMessage.json.callbackID });
+        break;
+      case "OpenWebapps:MgmtUninstall":
+        let uninstalled = OpenWebapps.mgmtUninstall(aMessage.json.origin);
+        browser.messageManager.sendAsyncMessage("OpenWebapps:MgmtUninstall:Return",
+            { ok: uninstalled, callbackID: aMessage.json.callbackID });
+        break;
+    }
+  },
+
   checkBox: function(aEvent) {
     let elem = aEvent.originalTarget;
     let perm = elem.getAttribute("perm");
-    if (this._manifest.capabilities && this._manifest.capabilities.indexOf(perm) != -1) {
+    if (this._application.capabilities && this._application.capabilities.indexOf(perm) != -1) {
       if (elem.checked) {
         elem.classList.remove("webapps-noperm");
         elem.classList.add("webapps-perm");
       } else {
         elem.classList.remove("webapps-perm");
         elem.classList.add("webapps-noperm");
       }
     }
   },
 
-  show: function show(aManifest) {
-    if (!aManifest) {
+  show: function show(aApplication) {
+    if (!aApplication) {
+      this._openwebapps = null;
+
       // Try every way to get an icon
       let browser = Browser.selectedBrowser;
       let icon = browser.appIcon.href;
       if (!icon)
         icon = browser.mIconURL;
       if (!icon) 
         icon = gFaviconService.getFaviconImageForPage(browser.currentURI).spec;
 
-      // Create a simple manifest
-      aManifest = {
-        uri: browser.currentURI.spec,
-        name: browser.contentTitle,
-        icon: icon,
+      // Create a simple application object
+      aApplication = {
+        appURI: browser.currentURI.spec,
+        storeURI: browser.currentURI.spec,
+        title: browser.contentTitle,
+        iconURI: icon,
         capabilities: [],
-      };
+        manifest: {
+          name: browser.contentTitle,
+          launch_path: browser.currentURI.spec,
+          icons: {
+            "64": icon
+          }
+        }
+      }
     }
 
-    this._manifest = aManifest;
+    this._application = aApplication;
     this._dialog = importDialog(window, "chrome://browser/content/webapps.xul", null);
 
-    if (aManifest.name)
-      document.getElementById("webapps-title").value = aManifest.name;
-    if (aManifest.icon)
-      document.getElementById("webapps-icon").src = aManifest.icon;  
+    if (aApplication.title)
+      document.getElementById("webapps-title").value = aApplication.title;
+    if (aApplication.iconURI)
+      document.getElementById("webapps-icon").src = aApplication.iconURI;  
 
-    let uri = Services.io.newURI(aManifest.uri, null, null);
+    let uri = Services.io.newURI(aApplication.appURI, null, null);
 
     let perms = [["offline", "offline-app"], ["geoloc", "geo"], ["notifications", "desktop-notification"]];
     let self = this;
     perms.forEach(function(tuple) {
       let elem = document.getElementById("webapps-" + tuple[0] + "-checkbox");
       let currentPerm = Services.perms.testExactPermission(uri, tuple[1]);
       self._perms[tuple[1]] = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
-      if ((aManifest.capabilities && (aManifest.capabilities.indexOf(tuple[1]) != -1)) || (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION))
+      if ((aApplication.capabilities && (aApplication.capabilities.indexOf(tuple[1]) != -1)) || (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION))
         elem.checked = true;
       else
         elem.checked = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
       elem.classList.remove("webapps-noperm");
       elem.classList.add("webapps-perm");
     });
 
     BrowserUI.pushPopup(this, this._dialog);
@@ -1742,72 +1841,86 @@ var WebappsUI = {
     // Force a modal dialog
     this._dialog.waitForClose();
   },
 
   hide: function hide() {
     this._dialog.close();
     this._dialog = null;
     BrowserUI.popPopup(this);
+
+    if (this._openwebapps) {
+      let browser = this._openwebapps;
+      browser.messageManager.sendAsyncMessage("OpenWebapps:InstallAborted", { callbackID: this._application.callbackID });
+    }
+
+    this._openwebapps = null;
   },
 
   _updatePermission: function updatePermission(aId, aPerm) {
     try {
-      let uri = Services.io.newURI(this._manifest.uri, null, null);
+      let uri = Services.io.newURI(this._application.appURI, null, null);
       let currentState = document.getElementById(aId).checked;
       if (currentState != this._perms[aPerm])
         Services.perms.add(uri, aPerm, currentState ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION);
     } catch(e) {
       Cu.reportError(e);
     }
   },
   
   launch: function launch() {
     let title = document.getElementById("webapps-title").value;
     if (!title)
       return;
 
+    this._application.title = title;
+
     this._updatePermission("webapps-offline-checkbox", "offline-app");
     this._updatePermission("webapps-geoloc-checkbox", "geo");
     this._updatePermission("webapps-notifications-checkbox", "desktop-notification");
 
+    let browser = this._openwebapps;
+    this._openwebapps = null;
     this.hide();
-    this.install(this._manifest.uri, title, this._manifest.icon);
+    this.install();
+
+    if (browser != null)
+      browser.messageManager.sendAsyncMessage("OpenWebapps:InstallDone", { callbackID: this._application.callbackID });
   },
   
   updateWebappsInstall: function updateWebappsInstall(aNode) {
     if (document.getElementById("main-window").hasAttribute("webapp"))
       return false;
 
     let browser = Browser.selectedBrowser;
 
-    let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
-    return !(webapp && webapp.isApplicationInstalled(browser.currentURI.spec));
+    return !(OpenWebapps.amInstalled(browser.currentURI.spec));
   },
   
-  install: function(aURI, aTitle, aIcon) {
+  install: function(aIconURI) {
     const kIconSize = 64;
     
     let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     canvas.setAttribute("style", "display: none");
 
     let self = this;
     let image = new Image();
     image.onload = function() {
       canvas.width = canvas.height = kIconSize; // clears the canvas
       let ctx = canvas.getContext("2d");
       ctx.drawImage(image, 0, 0, kIconSize, kIconSize);
-      let data = canvas.toDataURL("image/png", "");
+      self._application.iconData = canvas.toDataURL("image/png", "");
       canvas = null;
       try {
-        let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
-        webapp.installApplication(aTitle, aURI, aIcon, data);
+        OpenWebapps.install(self._application);
       } catch(e) {
         Cu.reportError(e);
       }
     }
+
     image.onerror = function() {
       // can't load the icon (bad URI) : fallback to the default one from chrome
-      self.install(aURI, aTitle, "chrome://browser/skin/images/favicon-default-30.png");
+      self.install("chrome://browser/skin/images/favicon-default-30.png");
     }
-    image.src = aIcon;
+
+    image.src = aIconURI || this._application.iconURI;
   }
 };
--- a/mobile/chrome/content/content.js
+++ b/mobile/chrome/content/content.js
@@ -1518,8 +1518,179 @@ var SelectionHandler = {
     cache.rect.bottom += aOffset.y;
     cache.offset = aOffset;
 
     return cache;
   }
 };
 
 SelectionHandler.init();
+
+// Implementation of https://developer.mozilla.org/en/OpenWebApps/The_JavaScript_API
+let OpenWebapps = {
+  _callbacks: [],
+
+  /** from https://developer.mozilla.org/en/OpenWebApps/The_Manifest
+   * only the name property is mandatory
+   */
+  checkManifest: function(aManifest) {
+    return ("name" in aManifest);
+  },
+  
+  getCallbackId: function(aCallback) {
+    let id = "id" + Math.random();
+    this._callbacks[id] = aCallback;
+    return id;
+  },
+  
+  getCallback: function(aId) {
+    return this._callbacks[aId];
+  },
+
+  removeCallback: function(aId) {
+    delete this._callbacks[aId];
+  },
+
+  install: function(aStoreURI, aManifestURI, aInstallData, aSuccessCallback, aErrorCallback) {
+    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+    xhr.open("GET", aManifestURI, true);
+
+    xhr.onload = function() {
+      if (xhr.status == 200) {
+        try {
+          let manifest = JSON.parse(xhr.responseText);
+          if (!OpenWebapps.checkManifest(manifest)) {
+            if (aErrorCallback)
+              aErrorCallback({ code: "invalidManifest", message: "Invalid manifest" });
+          } else {
+            sendAsyncMessage("OpenWebapps:Install", { storeURI: aStoreURI.href, manifestURI: aManifestURI, manifest: xhr.responseText,
+                             installData: aInstallData, callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+          }
+        } catch(e) {
+          if (aErrorCallback)
+            aErrorCallback({ code: "manifestParseError", message: "Unable to parse the manifest" });
+        }
+      }
+      else if (aErrorCallback) {
+        aErrorCallback({ code: "networkError", message: "Unable to retrieve manifest" });
+      }      
+    }
+
+    xhr.onerror = function() {
+      if (aErrorCallback)
+        aErrorCallback({ code: "networkError", message: "Unable to retrieve manifest" });
+    }
+
+    xhr.send(null);
+  },
+  
+  amInstalled: function(aAppURI, aSuccessCallback, aErrorCallback) {
+    sendAsyncMessage("OpenWebapps:AmInstalled", { appURI: aAppURI, callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+  },
+  
+  getInstalledBy: function(aStoreURI, aSuccessCallback, aErrorCallback) {
+    sendAsyncMessage("OpenWebapps:GetInstalledBy", { storeURI: aStoreURI.href, callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+  },
+  
+  mgmtLaunch: function(aOrigin, aSuccessCallback, aErrorCallback) {
+    sendAsyncMessage("OpenWebapps:MgmtLaunch", { origin: aOrigin, callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+  },
+  
+  mgmtList: function(aSuccessCallback, aErrorCallback) {
+    sendAsyncMessage("OpenWebapps:MgmtList", { callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+  },
+  
+  mgmtUninstall: function(aOrigin, aSuccessCallback, aErrorCallback) {
+    sendAsyncMessage("OpenWebapps:MgmtUninstall", { origin: aOrigin, callbackID:  OpenWebapps.getCallbackId({ success: aSuccessCallback, error: aErrorCallback }) });
+  },
+  
+  receiveMessage: function(aMessage) {
+    let msg = aMessage.json;
+    let callbacks = OpenWebapps.getCallback(msg.callbackID);
+    switch(aMessage.name) {
+      case "OpenWebapps:InstallAborted" :
+        if (callbacks.error)
+          callbacks.error({ code: "denied", message: "User denied installation" });
+        break;
+      case "OpenWebapps:InstallDone" :
+        callbacks.success();
+        break;
+      case "OpenWebapps:GetInstalledBy:Return":
+        callbacks.success(msg.apps);
+        break;
+      case "OpenWebapps:AmInstalled:Return":
+        callbacks.success(msg.installed ? msg.app : null);
+        break;
+      case "OpenWebapps:MgmtLaunch:Return":
+        if (msg.ok && callbacks.success)
+          callbacks.success();
+        else if (!msg.ok && callbacks.error)
+          callbacks.error({ code: "noSuchApp", message: "Unable to launch application"});
+        break;
+      case "OpenWebapps:MgmtList:Return":
+        if (msg.ok && callbacks.success)
+          callbacks.success(msg.apps);
+        else if (!msg.ok && callbacks.error)
+          callbacks.error({ code: "noAppList", message: "Unable to get application list"});
+        break;
+      case "OpenWebapps:MgmtUninstall:Return":
+        if (msg.ok && callbacks.success)
+          callbacks.success();
+        else if (!msg.ok && callbacks.error)
+          callbacks.error({ code: "noSuchApp", message: "Unable to uninstall application"});
+        break;
+    }
+    OpenWebapps.removeCallback(msg.callbackID);
+  },
+  
+  init: function() {
+    addMessageListener("OpenWebapps:InstallDone", this);
+    addMessageListener("OpenWebapps:InstallAborted", this);
+    addMessageListener("OpenWebapps:GetInstalledBy:Return", this);
+    addMessageListener("OpenWebapps:AmInstalled:Return", this);
+    addMessageListener("OpenWebapps:MgmtLaunch:Return", this);
+    addMessageListener("OpenWebapps:MgmtList:Return", this);
+    addMessageListener("OpenWebapps:MgmtUninstall:Return", this);
+    Services.obs.addObserver(this, "content-document-global-created", false);
+  },
+  
+  getInjected: function() {
+    let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+    xhr.open("GET", "chrome://browser/content/injected.js", false);
+    xhr.overrideMimeType("text/plain");
+    xhr.send(null);
+    return xhr.responseText;
+  },
+
+  observe: function(subject, topic, data) {
+    let sandbox = new Components.utils.Sandbox(Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal));
+    sandbox.window = subject.wrappedJSObject;
+
+    sandbox.importFunction(function(aStoreURI, aManifestURI, aInstallData, aSuccessCallback, aErrorCallback) {
+        OpenWebapps.install(aStoreURI, aManifestURI, aInstallData, aSuccessCallback, aErrorCallback);
+        }, "OpenWebapps_install");
+    
+    sandbox.importFunction(function(aAppURI, aSuccessCallback, aErrorCallback) {
+        OpenWebapps.amInstalled(aAppURI, aSuccessCallback, aErrorCallback);
+        }, "OpenWebapps_amInstalled");
+    
+    sandbox.importFunction(function(aStoreURI, aSuccessCallback, aErrorCallback) {
+        OpenWebapps.getInstalledBy(aStoreURI, aSuccessCallback, aErrorCallback);
+        }, "OpenWebapps_getInstalledBy");
+    
+    sandbox.importFunction(function(aOrigin, aSuccessCallback, aErrorCallback) {
+        OpenWebapps.mgmtLaunch(aOrigin, aSuccessCallback, aErrorCallback);
+        }, "OpenWebappsMgmt_launch");
+    
+    sandbox.importFunction(function(aSuccessCallback, aErrorCallback) {
+        OpenWebapps.mgmtList(aSuccessCallback, aErrorCallback);
+        }, "OpenWebappsMgmt_list");
+    
+    sandbox.importFunction(function(aOrigin, aSuccessCallback, aErrorCallback) {
+        OpenWebapps.mgmtUninstall(aOrigin, aSuccessCallback, aErrorCallback);
+        }, "OpenWebappsMgmt_uninstall");
+
+    let toInject = OpenWebapps.getInjected();
+    Cu.evalInSandbox(toInject, sandbox, "1.8", "chrome://browser/content/injected.js", 1);
+  }
+};
+
+OpenWebapps.init();
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/injected.js
@@ -0,0 +1,66 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Fabrice Desré <fabrice@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* Expose API under window.navigator.apps */
+if (window && window.navigator) {
+  window.navigator.mozApps = {
+    install: function(aParam) {
+      return OpenWebapps_install(window.location, aParam.url, aParam.install_data, aParam.onsuccess, aParam.onerror);
+    },
+    
+    amInstalled: function(aSuccessCallback, aErrorCallback) {
+      return OpenWebapps_amInstalled(window.location, aSuccessCallback, aErrorCallback);
+    },
+    
+    getInstalledBy: function(aSuccessCallback, aErrorCallback) {
+      return OpenWebapps_getInstalledBy(window.location, aSuccessCallback, aErrorCallback);
+    }
+  }
+  
+  window.navigator.mozApps.mgmt = {
+    launch: function(aOrigin, aSuccessCallback, aErrorCallback) {
+      return OpenWebappsMgmt_launch(aOrigin, aSuccessCallback, aErrorCallback);
+    },
+    
+    list: function(aSuccessCallback, aErrorCallback) {
+      return OpenWebappsMgmt_list(aSuccessCallback, aErrorCallback);
+    },
+    
+    uninstall: function(aOrigin, aSuccessCallback, aErrorCallback) {
+      return OpenWebappsMgmt_uninstall(aOrigin, aSuccessCallback, aErrorCallback);
+    }
+  }
+}
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -68,11 +68,12 @@ chrome.jar:
   content/AnimatedZoom.js              (content/AnimatedZoom.js)
 #ifdef MOZ_SERVICES_SYNC
   content/sync.js                      (content/sync.js)
 #endif
   content/LoginManagerChild.js         (content/LoginManagerChild.js)
   content/fullscreen-video.js          (content/fullscreen-video.js)
   content/fullscreen-video.xhtml       (content/fullscreen-video.xhtml)
   content/netError.xhtml               (content/netError.xhtml)
+  content/injected.js                  (content/injected.js)
 
 % override chrome://global/content/config.xul chrome://browser/content/config.xul
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
--- a/mobile/components/Makefile.in
+++ b/mobile/components/Makefile.in
@@ -44,17 +44,16 @@ VPATH      = @srcdir@
 include $(DEPTH)/config/autoconf.mk
 
 MODULE = MobileComponents
 XPIDL_MODULE = MobileComponents
 
 XPIDLSRCS = \
         SessionStore.idl \
         LoginManagerPrompter.idl \
-        WebappsSupport.idl \
         $(NULL)
 
 EXTRA_PP_COMPONENTS = \
         MobileComponents.manifest \
         AboutRedirector.js \
         BrowserCLH.js \
         DirectoryProvider.js\
         HelperAppDialog.js \
@@ -71,17 +70,16 @@ EXTRA_COMPONENTS = \
         PromptService.js \
         ContentDispatchChooser.js \
         AutoCompleteCache.js \
         AddonUpdateService.js \
         FormAutoComplete.js \
         LoginManager.js \
         LoginManagerPrompter.js \
         BlocklistPrompt.js \
-        WebappsSupport.js \
         $(NULL)
 
 ifdef MOZ_SAFE_BROWSING
 EXTRA_COMPONENTS += SafeBrowsing.js
 endif
 
 ifdef MOZ_UPDATER
 EXTRA_COMPONENTS += UpdatePrompt.js
--- a/mobile/components/MobileComponents.manifest
+++ b/mobile/components/MobileComponents.manifest
@@ -115,12 +115,8 @@ contract @mozilla.org/safebrowsing/appli
 category app-startup SafeBrowsing service,@mozilla.org/safebrowsing/application;1
 #endif
 
 #ifdef MOZ_UPDATER
 # UpdatePrompt.js
 component {88b3eb21-d072-4e3b-886d-f89d8c49fe59} UpdatePrompt.js
 contract @mozilla.org/updates/update-prompt;1 {88b3eb21-d072-4e3b-886d-f89d8c49fe59}
 #endif
-
-# webapps
-component {cb1107c1-1e15-4f11-99c8-27b9ec221a2a} WebappsSupport.js
-contract @mozilla.org/webapps/support;1 {cb1107c1-1e15-4f11-99c8-27b9ec221a2a}
deleted file mode 100644
--- a/mobile/components/WebappsSupport.idl
+++ /dev/null
@@ -1,60 +0,0 @@
-/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla Webapp code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2010
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Fabrice Desré <fabrice@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-#include "nsISupports.idl"
-
-[scriptable, uuid(adb91273-0cf1-4bbe-a37b-22e660192e2a)]
-interface nsIWebappsSupport : nsISupports
-{
-  /**
-   * This method installs a web app.
-   *
-   * @param title     the user-friendly name of the application.
-   * @param uri       the uri of the web app.
-   * @param iconData  a base64 encoded representation of the application's icon.
-   */
-  void installApplication(in wstring title, in wstring uri, in wstring iconUri, in wstring iconData);
-  
-  /**
-   * Checks is a web app is already installed
-   *
-   * @param uri the uri of the web app
-   * @return true if the web app is installed, false if it's not installed
-   */
-  boolean isApplicationInstalled(in wstring uri);
-};
-
deleted file mode 100644
--- a/mobile/components/WebappsSupport.js
+++ /dev/null
@@ -1,125 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla Mobile Browser.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Fabrice Desré <fabrice@mozilla.com>
- *   Mark Finkle <mfinkle@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-const Cu = Components.utils; 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-const DB_VERSION = 1;
-
-function WebappsSupport() {
-  this.init();
-}
-
-WebappsSupport.prototype = {
-  db: null,
-  
-  init: function() {
-    let file =  Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
-    file.append("webapps.sqlite");
-    this.db = Services.storage.openDatabase(file);
-    let version = this.db.schemaVersion;
-    
-    if (version == 0) {
-      this.db.executeSimpleSQL("CREATE TABLE webapps (title TEXT, uri TEXT PRIMARY KEY, icon TEXT)");
-      this.db.schemaVersion = DB_VERSION;
-    }
-
-    XPCOMUtils.defineLazyGetter(this, "_installQuery", function() {
-      return this.db.createAsyncStatement("INSERT INTO webapps (title, uri, icon) VALUES(:title, :uri, :icon)");
-    });
-
-    XPCOMUtils.defineLazyGetter(this, "_findQuery", function() {
-      return this.db.createStatement("SELECT uri FROM webapps where uri = :uri");
-    });
-
-    Services.obs.addObserver(this, "quit-application-granted", false);
-  },
- 
-  // entry point
-  installApplication: function(aTitle, aURI, aIconURI, aIconData) {
-    let stmt = this._installQuery;
-    stmt.params.title = aTitle;
-    stmt.params.uri = aURI;
-    stmt.params.icon = aIconData;
-    stmt.executeAsync();
-  },
- 
-  isApplicationInstalled: function(aURI) {
-    let stmt = this._findQuery;
-    let found = false;
-    try {
-      stmt.params.uri = aURI;
-      found = stmt.executeStep();
-    } finally {
-      stmt.reset();
-    }
-    return found;
-  },
-
-  // nsIObserver
-  observe: function(aSubject, aTopic, aData) {
-    Services.obs.removeObserver(this, "quit-application-granted");
-
-    // Finalize the statements that we have used
-    let stmts = [
-      "_installQuery",
-      "_findQuery"
-    ];
-    for (let i = 0; i < stmts.length; i++) {
-      // We do not want to create any query we haven't already created, so
-      // see if it is a getter first.
-      if (Object.getOwnPropertyDescriptor(this, stmts[i]).value !== undefined) {
-        this[stmts[i]].finalize();
-      }
-    }
-
-    this.db.asyncClose();
-  },
-
-  // QI
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebappsSupport]),
-
-  // XPCOMUtils factory
-  classID: Components.ID("{cb1107c1-1e15-4f11-99c8-27b9ec221a2a}")
-};
-
-const NSGetFactory = XPCOMUtils.generateNSGetFactory([WebappsSupport]);
-
--- a/mobile/installer/package-manifest.in
+++ b/mobile/installer/package-manifest.in
@@ -607,12 +607,11 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/SessionStore.js
 @BINPATH@/components/Sidebar.js
 #ifdef MOZ_SAFE_BROWSING
 @BINPATH@/components/SafeBrowsing.js
 #endif
 #ifdef MOZ_UPDATER
 @BINPATH@/components/UpdatePrompt.js
 #endif
-@BINPATH@/components/WebappsSupport.js
 @BINPATH@/components/XPIDialogService.js
 @BINPATH@/components/browsercomps.xpt
 @BINPATH@/extensions/feedback@mobile.mozilla.org.xpi
--- a/mobile/modules/Makefile.in
+++ b/mobile/modules/Makefile.in
@@ -41,15 +41,16 @@ srcdir     = @srcdir@
 VPATH      = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
   LocaleRepository.jsm \
   linuxTypes.jsm \
   video.jsm \
+  openWebapps.jsm \
   $(NULL)
 
 EXTRA_PP_JS_MODULES = \
   contacts.jsm \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/mobile/modules/openWebapps.jsm
@@ -0,0 +1,209 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Fabrice Desré <fabrice@mozilla.com>
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils; 
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let EXPORTED_SYMBOLS = ["OpenWebapps"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
+  Cu.import("resource://gre/modules/NetUtil.jsm");
+  return NetUtil;
+});
+
+let OpenWebapps = {
+  appsDir: null,
+  appsFile: null,
+  webapps: { },
+
+  init: function() {
+    let file =  Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("ProfD", Ci.nsIFile);
+    file.append("webapps");
+    if (!file.exists() || !file.isDirectory()) {
+      file.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
+    }
+    this.appsDir = file;
+    this.appsFile = file.clone();
+    this.appsFile.append("webapps.json");
+    if (!this.appsFile.exists())
+      return;
+    
+    try {
+      let channel = NetUtil.newChannel(this.appsFile);
+      channel.contentType = "application/json";
+      let self = this;
+      NetUtil.asyncFetch(channel, function(aStream, aResult) {
+        if (!Components.isSuccessCode(aResult)) {
+          Cu.reportError("OpenWebappsSupport: Could not read from webapps.json file");
+          return;
+        }
+
+        // Read webapps json file into a string
+        let data = null;
+        try {
+          data = JSON.parse(NetUtil.readInputStreamToString(aStream, aStream.available()) || "");
+          self.webapps = data;
+          aStream.close();
+        } catch (ex) {
+          Cu.reportError("OpenWebsappsStore: Could not parse JSON: " + ex);
+        }
+      });
+    } catch (ex) {
+      Cu.reportError("OpenWebappsSupport: Could not read from webapps.json file: " + ex);
+    }
+  },
+ 
+  _writeFile: function ss_writeFile(aFile, aData) {
+    // Initialize the file output stream.
+    let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+    ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN);
+
+    // Obtain a converter to convert our data to a UTF-8 encoded input stream.
+    let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
+    converter.charset = "UTF-8";
+
+    // Asynchronously copy the data to the file.
+    let istream = converter.convertToInputStream(aData);
+    NetUtil.asyncCopy(istream, ostream, function(rc) {
+      // nothing to do
+    });
+  },
+  
+  install: function(aApplication) {
+    // Don't install twice an application
+    if (this.amInstalled(aApplication.appURI))
+      return;
+
+    let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+    let id = uuidGenerator.generateUUID().toString();
+    let dir = this.appsDir.clone();
+    dir.append(id);
+    dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
+    
+    let manFile = dir.clone();
+    manFile.append("manifest.json");
+    this._writeFile(manFile, JSON.stringify(aApplication.manifest));
+    
+    this.webapps[id] = {
+      title: aApplication.title,
+      storeURI: aApplication.storeURI,
+      appURI: aApplication.appURI,
+      iconData: aApplication.iconData,
+      installData: aApplication.installData,
+      installTime: (new Date()).getTime(),
+      manifest: aApplication.manifest
+    };
+    this._writeFile(this.appsFile, JSON.stringify(this.webapps));
+  },
+ 
+  amInstalled: function(aURI) {
+    for (let id in this.webapps) {
+      let app = this.webapps[id];
+      if (app.appURI == aURI) {
+        return { origin: app.appURI,
+                 install_origin: app.storeURI,
+                 install_data: app.installData,
+                 install_time: app.installTime,
+                 manifest: app.manifest };
+      }
+    }
+    return null;
+  },
+
+  getInstalledBy: function(aStoreURI) {
+    let res = [];
+    for (let id in this.webapps) {
+      let app = this.webapps[id];
+      if (app.storeURI == aStoreURI)
+        res.push({ origin: app.appURI,
+                   install_origin: app.storeURI,
+                   install_data: app.installData,
+                   install_time: app.installTime,
+                   manifest: app.manifest });
+    }
+    return res;
+  },
+  
+  mgmtList: function() {
+    let res = {};
+    for (let id in this.webapps) {
+      let app = this.webapps[id];
+      res[app.appURI] = { origin: app.appURI,
+                          install_origin: app.storeURI,
+                          install_data: app.installData,
+                          install_time: app.installTime,
+                          manifest: app.manifest };
+    }
+    return res;
+  },
+  
+  mgmtLaunch: function(aOrigin) {
+    for (let id in this.webapps) {
+      let app = this.webapps[id];
+      if (app.appURI == aOrigin) {
+        let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+        browserWin.browserDOMWindow.openURI(Services.io.newURI(aOrigin, null, null), null, browserWin.OPEN_APPTAB, Ci.nsIBrowserDOMWindow.OPEN_NEW);
+        return true;
+      }
+    }
+    return false;
+  },
+  
+  mgmtUninstall: function(aOrigin) {
+    for (let id in this.webapps) {
+      let app = this.webapps[id];
+      if (app.appURI == aOrigin) {
+        delete this.webapps[id];
+        this._writeFile(this.appsFile, JSON.stringify(this.webapps));
+        let dir = this.appsFile.clone();
+        dir.append(id);
+        try {
+          dir.remove(true);
+        } catch (e) {
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+};
+
+OpenWebapps.init();