Bug 584767 - webapps frontend [r=mfinkle, r=wjohnston, r=fabrice]
authorFabrice Desré <fabrice@mozilla.com>
Fri, 10 Jun 2011 17:02:00 -0400
changeset 73317 3e5c94427f4a215f7e2c065bced41ccdcbe83980
parent 73316 1433eaa829ecd035ff6671fc0c6ae1206751b9a3
child 73318 b1707b7835e217ed355ec0fb987f166e45ffc8a5
push id235
push userbzbarsky@mozilla.com
push dateTue, 27 Sep 2011 17:13:04 +0000
treeherdermozilla-beta@2d1e082d176a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, wjohnston, fabrice
bugs584767
milestone8.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 584767 - webapps frontend [r=mfinkle, r=wjohnston, r=fabrice]
mobile/chrome/content/browser-scripts.js
mobile/chrome/content/browser-ui.js
mobile/chrome/content/browser.js
mobile/chrome/content/browser.xul
mobile/chrome/content/common-ui.js
mobile/chrome/content/webapps.xul
mobile/chrome/jar.mn
mobile/components/BrowserCLH.js
mobile/installer/package-manifest.in
mobile/locales/en-US/chrome/browser.dtd
mobile/locales/en-US/chrome/webapps.dtd
mobile/locales/jar.mn
mobile/themes/core/browser.css
mobile/themes/core/gingerbread/browser.css
mobile/themes/core/gingerbread/platform.css
--- a/mobile/chrome/content/browser-scripts.js
+++ b/mobile/chrome/content/browser-scripts.js
@@ -64,16 +64,17 @@ Cu.import("resource://gre/modules/Geomet
 XPCOMUtils.defineLazyGetter(this, "CommonUI", function() {
   let CommonUI = {};
   Services.scriptloader.loadSubScript("chrome://browser/content/common-ui.js", CommonUI);
   return CommonUI;
 });
 
 [
   ["FullScreenVideo"],
+  ["WebappsUI"],
   ["BadgeHandlers"],
   ["ContextHelper"],
   ["SelectionHelper"],
   ["FormHelperUI"],
   ["FindHelperUI"],
   ["NewTabPopup"],
   ["PageActions"],
   ["BrowserSearch"],
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -1033,16 +1033,31 @@ var BrowserUI = {
         break;
       case "DOMWillOpenModalDialog":
         return this._domWillOpenModalDialog(browser);
         break;
       case "DOMWindowClose":
         return this._domWindowClose(browser);
         break;
       case "DOMLinkAdded":
+        // checks for an icon to use for a web app
+        // priority is : icon < apple-touch-icon
+        let rel = json.rel.toLowerCase().split(" ");
+        if ((rel.indexOf("icon") != -1) && !browser.appIcon) {
+          // We should also use the sizes attribute if available
+          // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+          browser.appIcon = json.href;
+        }
+        else if (rel.indexOf("apple-touch-icon") != -1) {
+          // XXX should we support apple-touch-icon-precomposed ?
+          // see http://developer.apple.com/safari/library/documentation/appleapplications/reference/safariwebcontent/configuringwebapplications/configuringwebapplications.html
+          browser.appIcon = json.href;
+        }
+
+        // Handle favicon changes
         if (Browser.selectedBrowser == browser)
           this._updateIcon(Browser.selectedBrowser.mIconURL);
         break;
       case "Browser:SaveAs:Return":
         if (json.type != Ci.nsIPrintSettings.kOutputFormatPDF)
           return;
 
         let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
@@ -1233,17 +1248,18 @@ var BrowserUI = {
           }
 
           return;
         }
 
         this.activePanel = RemoteTabsList;
         break;
       case "cmd_quit":
-        GlobalOverlay.goQuitApplication();
+        // Only close one window
+        this._closeOrQuit();
         break;
       case "cmd_close":
         this._closeOrQuit();
         break;
       case "cmd_menu":
         AppMenu.toggle();
         break;
       case "cmd_newTab":
--- a/mobile/chrome/content/browser.js
+++ b/mobile/chrome/content/browser.js
@@ -373,23 +373,24 @@ var Browser = {
     messageManager.addMessageListener("Browser:FormSubmit", this);
     messageManager.addMessageListener("Browser:KeyPress", this);
     messageManager.addMessageListener("Browser:ZoomToPoint:Return", this);
     messageManager.addMessageListener("Browser:CanUnload:Return", this);
     messageManager.addMessageListener("scroll", this);
     messageManager.addMessageListener("Browser:CertException", this);
     messageManager.addMessageListener("Browser:BlockedSite", this);
 
-    // broadcast a UIReady message so add-ons know we are finished with startup
+    // Broadcast a UIReady message so add-ons know we are finished with startup
     let event = document.createEvent("Events");
     event.initEvent("UIReady", true, false);
     window.dispatchEvent(event);
 
-    // if we have an opener this was not the first window opened and will not
+    // If we have an opener this was not the first window opened and will not
     // receive an initial resize event. instead we fire the resize handler manually
+    // Bug 610834
     if (window.opener)
       resizeHandler({ target: window });
   },
 
   _alertShown: function _alertShown() {
     // ensure that the full notification still visible, even if the urlbar is floating
     if (BrowserUI.isToolbarLocked())
       Browser.pageScrollboxScroller.scrollTo(0, 0);
@@ -936,17 +937,17 @@ var Browser = {
    * @param [optional] dx
    * @param [optional] dy an offset distance at which to perform the visibility
    * computation
    */
   computeSidebarVisibility: function computeSidebarVisibility(dx, dy) {
     function visibility(aSidebarRect, aVisibleRect) {
       let width = aSidebarRect.width;
       aSidebarRect.restrictTo(aVisibleRect);
-      return (aSidebarRect.width ? aSidebarRect.width / width : 0);
+      return (width ? aSidebarRect.width / width : 0);
     }
 
     if (!dx) dx = 0;
     if (!dy) dy = 0;
 
     let [leftSidebar, rightSidebar] = [Elements.tabs.getBoundingClientRect(), Elements.controls.getBoundingClientRect()];
 
     let visibleRect = new Rect(0, 0, window.innerWidth, 1);
@@ -1487,16 +1488,17 @@ Browser.WebProgress.prototype = {
 
         let locationHasChanged = (location != tab.browser.lastLocation);
         if (locationHasChanged) {
           Browser.getNotificationBox(tab.browser).removeTransientNotifications();
           tab.resetZoomLevel();
           tab.hostChanged = true;
           tab.browser.lastLocation = location;
           tab.browser.userTypedValue = "";
+          tab.browser.appIcon = null;
 
 #ifdef MOZ_CRASH_REPORTER
           if (CrashReporter.enabled)
             CrashReporter.annotateCrashReport("URL", spec);
 #endif
           this._waitForLoad(tab);
           tab.useFallbackWidth = false;
         }
@@ -1571,16 +1573,18 @@ Browser.WebProgress.prototype = {
 
       browser.messageManager.addMessageListener("MozScrolledAreaChanged", aTab.scrolledAreaChanged);
       aTab.updateContentCapture();
     });
   }
 };
 
 
+const OPEN_APPTAB = 100; // Hack until we get a real API
+
 function nsBrowserAccess() { }
 
 nsBrowserAccess.prototype = {
   QueryInterface: function(aIID) {
     if (aIID.equals(Ci.nsIBrowserDOMWindow) || aIID.equals(Ci.nsISupports))
       return this;
     throw Cr.NS_NOINTERFACE;
   },
@@ -1613,23 +1617,41 @@ nsBrowserAccess.prototype = {
       return null;
     } else if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
       let owner = isExternal ? null : Browser.selectedTab;
       let tab = Browser.addTab("about:blank", true, owner, { getAttention: true });
       if (isExternal)
         tab.closeOnExit = true;
       browser = tab.browser;
       BrowserUI.hidePanel();
+    } else if (aWhere == OPEN_APPTAB) {
+      Browser.tabs.forEach(function(aTab) {
+        if ("appURI" in aTab.browser && aTab.browser.appURI.spec == aURI.spec) {
+          Browser.selectedTab = aTab;
+          browser = aTab.browser;
+        }
+      });
+
+      if (!browser) {
+        // Make a new tab to hold the app
+        let tab = Browser.addTab("about:blank", true, null, { getAttention: true });
+        browser = tab.browser;
+        browser.appURI = aURI;
+      } else {
+        // Just use the existing browser, but return null to keep the system from trying to load the URI again
+        browser = null;
+      }
+      BrowserUI.hidePanel();
     } else { // OPEN_CURRENTWINDOW and illegal values
       browser = Browser.selectedBrowser;
     }
 
     try {
       let referrer;
-      if (aURI) {
+      if (aURI && browser) {
         if (aOpener) {
           location = aOpener.location;
           referrer = Services.io.newURI(location, null, null);
         }
         browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
       }
       browser.focus();
     } catch(e) { }
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -63,20 +63,19 @@
         onload="Browser.startup();"
         onunload="Browser.shutdown();"
         onclose="return Browser.closing();"
         windowtype="navigator:browser"
         chromedir="&locale.dir;"
         title="&brandShortName;"
 #ifdef MOZ_PLATFORM_MAEMO
         sizemode="fullscreen"
-#else
+#endif
         width="480"
         height="800"
-#endif
         onkeypress="onDebugKeyPress(event);"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript" src="chrome://browser/content/browser.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-ui.js"/>
   <script type="application/javascript" src="chrome://browser/content/browser-scripts.js"/>
   <script type="application/javascript" src="chrome://browser/content/Util.js"/>
@@ -344,16 +343,17 @@
           onclick="SharingUI.show(getBrowser().currentURI.spec, getBrowser().contentTitle);"/>
 #endif
         <pageaction id="pageaction-password" title="&pageactions.password.forget;"
           onclick="PageActions.forgetPassword(event);"/>
         <pageaction id="pageaction-reset" title="&pageactions.reset;"
           onclick="PageActions.clearPagePermissions(event);"/>
         <pageaction id="pageaction-search" title="&pageactions.search.addNew;"/>
         <pageaction id="pageaction-charset" title="&pageactions.charEncoding;" onclick="CharsetMenu.show();"/>
+        <pageaction id="pageaction-webapps-install" title="&pageactions.webapps.install;" onclick="WebappsUI.show();"/>
       </hbox>
     </arrowbox>
 
     <arrowbox id="bookmark-popup" class="arrowbox-dark" hidden="true" align="center" offset="12">
       <label value="&bookmarkPopup.label;"/>
       <separator class="thin"/>
       <vbox>
         <button id="bookmark-popup-edit" label="&bookmarkEdit.label;" oncommand="BookmarkHelper.edit();"/>
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -169,16 +169,17 @@ var PageActions = {
 
     this.register("pageaction-reset", this.updatePagePermissions, this);
     this.register("pageaction-password", this.updateForgetPassword, this);
 #ifdef NS_PRINTING
     this.register("pageaction-saveas", this.updatePageSaveAs, this);
 #endif
     this.register("pageaction-share", this.updateShare, this);
     this.register("pageaction-search", BrowserSearch.updatePageSearchEngines, BrowserSearch);
+    this.register("pageaction-webapps-install", WebappsUI.updateWebappsInstall, WebappsUI);
   },
 
   handleEvent: function handleEvent(aEvent) {
     switch (aEvent.type) {
       case "click":
         getIdentityHandler().hide();
         break;
     }
@@ -1653,8 +1654,133 @@ var CharsetMenu = {
     browser.messageManager.sendAsyncMessage("Browser:SetCharset", {
       charset: aCharset
     });
     let history = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
     history.setCharsetForURI(browser.documentURI, aCharset);
   }
 
 };
+
+var WebappsUI = {
+  _dialog: null,
+  _manifest: null,
+
+  checkBox: function(aEvent) {
+    let elem = aEvent.originalTarget;
+    let perm = elem.getAttribute("perm");
+    if (this._manifest.capabilities && this._manifest.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) {
+      // Try every way to get an icon
+      let browser = Browser.selectedBrowser;
+      let icon = browser.appIcon;
+      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
+      };
+    }
+
+    this._manifest = aManifest;
+    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;  
+
+    let uri = Services.io.newURI(aManifest.uri, null, null);
+
+    let perms = [["offline", "offline-app"], ["geoloc", "geo"], ["notifications", "desktop-notifications"]];
+    perms.forEach(function(tuple) {
+      let elem = document.getElementById("webapps-" + tuple[0] + "-checkbox");
+      let currentPerm = Services.perms.testExactPermission(uri, tuple[1]);
+      if ((aManifest.capabilities && (aManifest.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);
+
+    // Force a modal dialog
+    this._dialog.waitForClose();
+  },
+
+  hide: function hide() {
+    this._dialog.close();
+    this._dialog = null;
+    BrowserUI.popPopup(this);
+  },
+
+  _updatePermission: function updatePermission(aId, aPerm) {
+    try {
+      let uri = Services.io.newURI(this._manifest.uri, null, null);
+      Services.perms.add(uri, aPerm, document.getElementById(aId).checked ? 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._updatePermission("webapps-offline-checkbox", "offline-app");
+    this._updatePermission("webapps-geoloc-checkbox", "geo");
+    this._updatePermission("webapps-notifications-checkbox", "desktop-notification");
+
+    this.hide();
+    this.install(this._manifest.uri, title, this._manifest.icon);
+  },
+  
+  updateWebappsInstall: function updateWebappsInstall(aNode) {
+    return !document.getElementById("main-window").hasAttribute("webapp");
+  },
+  
+  install: function(aURI, aTitle, aIcon) {
+    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", "");
+      canvas = null;
+      try {
+        let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
+        webapp.installApplication(aTitle, aURI, aIcon, data);
+      } 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");
+    }
+    image.src = aIcon;
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/webapps.xul
@@ -0,0 +1,94 @@
+<?xml version="1.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 Corporation.
+   - Portions created by the Initial Developer are Copyright (C) 2008
+   - 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 LGPL or the GPL. 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 ***** -->
+<!DOCTYPE dialog [
+<!ENTITY % promptDTD SYSTEM "chrome://browser/locale/prompt.dtd">
+%promptDTD;
+<!ENTITY % webappsDTD SYSTEM "chrome://browser/locale/webapps.dtd">
+%webappsDTD;
+]>
+<dialog id="webapp-dialog" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <keyset>
+    <key keycode="VK_ESCAPE" command="cmd_cancel"/>
+    <key keycode="VK_RETURN" command="cmd_ok"/>
+  </keyset>
+
+  <commandset>
+    <command id="cmd_cancel" oncommand="WebappsUI.hide();"/>
+    <command id="cmd_ok" oncommand="WebappsUI.launch();"/>
+  </commandset>
+
+  <vbox class="prompt-title" id="webapps-title-box">
+    <hbox align="center">
+      <image id="webapps-icon"/>
+      <vbox flex="1">
+        <textbox id="webapps-title" placeholder="&webapps.title.placeholder;" flex="1"/>
+      </vbox>
+    </hbox>
+  </vbox>
+  <separator class="prompt-line"/>
+  <scrollbox class="prompt-message prompt-header" id="webapps-perm-box" orient="vertical" oncommand="WebappsUI.checkBox(event)" flex="1">
+    <label crop="center" flex="1" value="&webapps.permissions;"/>
+    <button id="webapps-geoloc-checkbox" perm="geo" type="checkbox" class="button-checkbox webapps-perm" flex="1">
+      <image class="button-image-icon"/>
+      <vbox flex="1">
+        <description class="prompt-checkbox-label" flex="1">&webapps.perm.geolocation;</description>
+        <description class="prompt-checkbox-label webapps-perm-requested-hint" id="webapps-geoloc-app">&webapps.perm.requested;</description>
+      </vbox>
+    </button>
+    <button id="webapps-offline-checkbox" perm="offline-app" type="checkbox" class="button-checkbox webapps-perm" flex="1">
+      <image class="button-image-icon"/>
+      <vbox flex="1">
+        <description class="prompt-checkbox-label" flex="1">&webapps.perm.offline;</description>
+        <description class="prompt-checkbox-label webapps-perm-requested-hint" id="webapps-offline-app">&webapps.perm.requested;</description>
+      </vbox>
+    </button>
+    <button id="webapps-notifications-checkbox" perm="desktop-notifications" type="checkbox" class="button-checkbox webapps-perm" flex="1">
+      <image class="button-image-icon"/>
+      <vbox flex="1">
+        <description class="prompt-checkbox-label" flex="1">&webapps.perm.notifications;</description>
+        <description class="prompt-checkbox-label webapps-perm-requested-hint" id="webapps-notifications-app">&webapps.perm.requested;</description>
+      </vbox>
+    </button>
+  </scrollbox>
+  <hbox pack="center" class="prompt-buttons">
+    <button class="prompt-button" command="cmd_ok" label="&ok.label;"/>
+    <separator/>
+    <button class="prompt-button" command="cmd_cancel" label="&cancel.label;"/>
+  </hbox>
+</dialog>
+
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -59,16 +59,17 @@ chrome.jar:
   content/console.js                   (content/console.js)
   content/prompt/alert.xul             (content/prompt/alert.xul)
   content/prompt/confirm.xul           (content/prompt/confirm.xul)
   content/prompt/prompt.xul            (content/prompt/prompt.xul)
   content/prompt/promptPassword.xul    (content/prompt/promptPassword.xul)
   content/prompt/select.xul            (content/prompt/select.xul)
   content/prompt/prompt.js             (content/prompt/prompt.js)
   content/share.xul                    (content/share.xul)
+  content/webapps.xul                  (content/webapps.xul)
   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)
 
--- a/mobile/components/BrowserCLH.js
+++ b/mobile/components/BrowserCLH.js
@@ -39,43 +39,41 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) {
   let argString = null;
-  if (aArgs) {
+  if (aArgs && !(aArgs instanceof Ci.nsISupportsArray)) {
     argString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
     argString.data = aArgs;
   }
 
-  return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString);
+  return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString || aArgs);
 }
 
 function resolveURIInternal(aCmdLine, aArgument) {
   let uri = aCmdLine.resolveURI(aArgument);
 
   if (!(uri instanceof Ci.nsIFileURL))
     return uri;
 
   try {
     if (uri.file.exists())
       return uri;
-  }
-  catch (e) {
+  } catch (e) {
     Cu.reportError(e);
   }
 
   try {
     let urifixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
     uri = urifixup.createFixupURI(aArgument, 0);
-  }
-  catch (e) {
+  } catch (e) {
     Cu.reportError(e);
   }
 
   return uri;
 }
 
 /**
  * Determines whether a home page override is needed.
@@ -150,26 +148,31 @@ BrowserCLH.prototype = {
         let uri = resolveURIInternal(aCmdLine, chromeParam);
         let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
         if (!netutil.URIChainHasFlags(uri, Ci.nsIHttpProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
           openWindow(null, uri.spec, "_blank", features, null);
 
           // Stop the normal commandline processing from continuing
           aCmdLine.preventDefault = true;
         }
-      }
-      catch (e) {
+      } catch (e) {
         Cu.reportError(e);
       }
       return;
     }
 
     // Check and remove the alert flag here, but we'll handle it a bit later - see below
     let alertFlag = aCmdLine.handleFlagWithParam("alert", false);
 
+    // Check and remove the webapp param
+    let appFlag = aCmdLine.handleFlagWithParam("webapp", false);
+    let appURI;
+    if (appFlag)
+      appURI = resolveURIInternal(aCmdLine, appFlag);
+
     // Keep an array of possible URL arguments
     let uris = [];
 
     // Check for the "url" flag
     let uriFlag = aCmdLine.handleFlagWithParam("url", false);
     if (uriFlag) {
       let uri = resolveURIInternal(aCmdLine, uriFlag);
       if (uri)
@@ -182,75 +185,81 @@ BrowserCLH.prototype = {
         continue;
 
       let uri = resolveURIInternal(aCmdLine, arg);
       if (uri)
         uris.push(uri);
     }
 
     // Open the main browser window, if we don't already have one
-    let win;
-    let localePickerWin;
+    let browserWin;
     try {
-      win = Services.wm.getMostRecentWindow("navigator:browser");
-      localePickerWin = Services.wm.getMostRecentWindow("navigator:localepicker");
-      if (localePickerWin) {
-        localePickerWin.focus();
+      let localeWin = Services.wm.getMostRecentWindow("navigator:localepicker");
+      if (localeWin) {
+        localeWin.focus();
         aCmdLine.preventDefault = true;
         return;
-      } else  if (!win) {
+      }
+
+      browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+      if (!browserWin) {
         // Default to the saved homepage
         let defaultURL = getHomePage();
 
         // Override the default if we have a URL passed on command line
         if (uris.length > 0) {
           defaultURL = uris[0].spec;
           uris = uris.slice(1);
         }
 
         // Show the locale selector if we have a new profile
         if (needHomepageOverride() == "new profile" && Services.prefs.getBoolPref("browser.firstrun.show.localepicker")) {
-          win = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL);
+          browserWin = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL);
           aCmdLine.preventDefault = true;
           return;
         }
 
-        win = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL);
+        browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL);
       }
 
-      win.focus();
+      browserWin.focus();
 
       // Stop the normal commandline processing from continuing. We just opened the main browser window
       aCmdLine.preventDefault = true;
-    } catch (e) { }
+    } catch (e) {
+      Cu.reportError(e);
+    }
 
     // Assumption: All remaining command line arguments have been sent remotely (browser is already running)
     // Action: Open any URLs we find into an existing browser window
 
     // First, get a browserDOMWindow object
-    while (!win.browserDOMWindow)
+    while (!browserWin.browserDOMWindow)
       Services.tm.currentThread.processNextEvent(true);
 
     // Open any URIs into new tabs
     for (let i = 0; i < uris.length; i++)
-      win.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+      browserWin.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+    if (appURI)
+      browserWin.browserDOMWindow.openURI(appURI, null, browserWin.OPEN_APPTAB, Ci.nsIBrowserDOMWindow.OPEN_NEW);
 
     // Handle the notification, if called from it
     if (alertFlag) {
       if (alertFlag == "update-app") {
         // Notification was already displayed and clicked, skip it next time
         Services.prefs.setBoolPref("app.update.skipNotification", true);
 
         var updateService = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService);
         var updateTimerCallback = updateService.QueryInterface(Ci.nsITimerCallback);
         updateTimerCallback.notify(null);
       } else if (alertFlag.length >= 9 && alertFlag.substr(0, 9) == "download:") {
-        showPanelWhenReady(win, "downloads-container");
+        showPanelWhenReady(browserWin, "downloads-container");
       } else if (alertFlag == "addons") {
-        showPanelWhenReady(win, "addons-container");
+        showPanelWhenReady(browserWin, "addons-container");
       }
     }
   },
 
   // QI
   QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
 
   // XPCOMUtils factory
--- a/mobile/installer/package-manifest.in
+++ b/mobile/installer/package-manifest.in
@@ -270,16 +270,17 @@
 @BINPATH@/components/xpcom_io.xpt
 @BINPATH@/components/xpcom_threads.xpt
 @BINPATH@/components/xpcom_xpti.xpt
 @BINPATH@/components/xpconnect.xpt
 @BINPATH@/components/xulapp.xpt
 @BINPATH@/components/xuldoc.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
+@BINPATH@/components/webapps.xpt
 
 ; JavaScript components
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPI.js
 @BINPATH@/components/FeedProcessor.manifest
 @BINPATH@/components/FeedProcessor.js
 @BINPATH@/components/BrowserFeeds.manifest
 @BINPATH@/components/FeedConverter.js
@@ -325,16 +326,17 @@
 @BINPATH@/components/GPSDGeolocationProvider.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/extensions.manifest
 @BINPATH@/components/addonManager.js
 @BINPATH@/components/amContentHandler.js
 @BINPATH@/components/amWebInstallListener.js
 @BINPATH@/components/nsBlocklistService.js
+
 #ifdef MOZ_UPDATER
 @BINPATH@/components/nsUpdateService.manifest
 @BINPATH@/components/nsUpdateService.js
 @BINPATH@/components/nsUpdateServiceStub.js
 #endif
 @BINPATH@/components/nsUpdateTimerManager.manifest
 @BINPATH@/components/nsUpdateTimerManager.js
 @BINPATH@/components/pluginGlue.manifest
@@ -605,11 +607,12 @@ 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/locales/en-US/chrome/browser.dtd
+++ b/mobile/locales/en-US/chrome/browser.dtd
@@ -106,10 +106,11 @@
 <!ENTITY pageactions.saveas.pdf      "Save As PDF">
 <!ENTITY pageactions.share.page      "Share Page">
 <!ENTITY pageactions.password.forget "Forget Password">
 <!ENTITY pageactions.quit            "Quit">
 <!ENTITY pageactions.reset           "Clear Site Preferences">
 <!ENTITY pageactions.findInPage      "Find In Page">
 <!ENTITY pageactions.search.addNew   "Add Search Engine">
 <!ENTITY pageactions.charEncoding    "Character Encoding">
+<!ENTITY pageactions.webapps.install "Install as App">
 
 <!ENTITY appMenu.siteOptions         "Site Options">
new file mode 100644
--- /dev/null
+++ b/mobile/locales/en-US/chrome/webapps.dtd
@@ -0,0 +1,6 @@
+<!ENTITY webapps.title.placeholder "Enter a title">
+<!ENTITY webapps.permissions "Allow access:">
+<!ENTITY webapps.perm.geolocation "Location-aware browsing">
+<!ENTITY webapps.perm.offline "Offline data storage">
+<!ENTITY webapps.perm.notifications "Desktop notifications">
+<!ENTITY webapps.perm.requested "requested">
--- a/mobile/locales/jar.mn
+++ b/mobile/locales/jar.mn
@@ -11,16 +11,17 @@
   locale/@AB_CD@/browser/localepicker.properties  (%chrome/localepicker.properties)
   locale/@AB_CD@/browser/region.properties        (%chrome/region.properties)
   locale/@AB_CD@/browser/preferences.dtd          (%chrome/preferences.dtd)
   locale/@AB_CD@/browser/checkbox.dtd             (%chrome/checkbox.dtd)
   locale/@AB_CD@/browser/notification.dtd         (%chrome/notification.dtd)
   locale/@AB_CD@/browser/sync.dtd                 (%chrome/sync.dtd)
   locale/@AB_CD@/browser/sync.properties          (%chrome/sync.properties)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
+  locale/@AB_CD@/browser/webapps.dtd              (%chrome/webapps.dtd)
   locale/@AB_CD@/browser/feedback.dtd             (%chrome/feedback.dtd)
   locale/@AB_CD@/browser/phishing.dtd             (%chrome/phishing.dtd)
   locale/@AB_CD@/browser/bookmarks.json           (bookmarks.json)
   locale/@AB_CD@/browser/searchplugins/list.txt   (%searchplugins/list.txt)
 
 # Fennec-specific overrides of generic strings
 * locale/@AB_CD@/browser/netError.dtd             (%chrome/overrides/netError.dtd)
 % override chrome://global/locale/netError.dtd    chrome://browser/locale/netError.dtd
--- a/mobile/themes/core/browser.css
+++ b/mobile/themes/core/browser.css
@@ -1324,16 +1324,35 @@ setting {
 }
 
 /* full-screen video ------------------------------------------------------- */
 .full-screen {
   position: absolute;
   z-index: 500;
 }
 
+/* openwebapps capabilities ------------------------------------------------------------ */
+.webapps-noperm description.webapps-perm-requested-hint {
+  display: block;
+}
+
+.webapps-perm description.webapps-perm-requested-hint {
+  display: none;
+}
+
+#webapps-icon {
+  width: 48px;
+  height: 48px;
+  margin: @margin_normal@;
+}
+
+#webapps-title {
+  -moz-margin-end: @margin_normal@;
+}
+
 /* Android menu ------------------------------------------------------------ */
 #appmenu {
   background: rgba(255,255,255,0.95);
   box-shadow: 0 @shadow_width_large@ @shadow_width_xlarge@ @shadow_width_large@ black;
   border-style: solid;
   border-color: #6d6d6d;
   border-width: @border_width_large@ @border_width_large@ 0 @border_width_large@;
 }
--- a/mobile/themes/core/gingerbread/browser.css
+++ b/mobile/themes/core/gingerbread/browser.css
@@ -1306,16 +1306,35 @@ setting {
 }
 
 /* full-screen video ------------------------------------------------------- */
 .full-screen {
   position: absolute;
   z-index: 500;
 }
 
+/* openwebapps capabilities ------------------------------------------------------------ */
+.webapps-noperm description.webapps-perm-requested-hint {
+  display: block;
+}
+
+.webapps-perm description.webapps-perm-requested-hint {
+  display: none;
+}
+
+#webapps-icon {
+  width: 32px;
+  height: 32px;
+  margin: @margin_normal@;
+}
+
+#webapps-title {
+  -moz-margin-end: @margin_normal@;
+}
+
 /* Android menu ------------------------------------------------------------ */
 #appmenu {
   background: @color_background_default@;
   border-style: solid;
   border-color: #6d6d6d;
   border-width: @border_width_small@ @border_width_small@ 0 @border_width_small@;
 }
 
--- a/mobile/themes/core/gingerbread/platform.css
+++ b/mobile/themes/core/gingerbread/platform.css
@@ -282,22 +282,23 @@ toolbarbutton[open="true"] {
   -moz-margin-end: @margin_normal@;
   list-style-image: url("chrome://browser/skin/images/check-unselected-hdpi.png");
 }
 
 .button-checkbox[checked="true"] > .button-image-icon {
   list-style-image: url("chrome://browser/skin/images/check-selected-hdpi.png");
 }
 
+.button-checkbox > .button-box,
 .button-checkbox:hover:active > .button-box,
 .button-checkbox[checked="true"] > .button-box {
   padding-top: @padding_tiny@;
   padding-bottom: @padding_xsmall@;
-  -moz-padding-start: @margin_small@;
-  -moz-padding-end: @margin_small@;
+  -moz-padding-start: @padding_small@;
+  -moz-padding-end: @padding_small@;
 }
 
 /* radio buttons ----------------------------------------------------------- */
 radiogroup {
   -moz-box-orient: horizontal;
 }
 
 .radio-label {