Merge fx-team to mozilla-central
authorMatt Brubeck <mbrubeck@mozilla.com>
Sat, 23 Nov 2013 14:24:52 -0800
changeset 157220 74ab61b8d0f054ab353a1101913441d75af66b16
parent 157189 ad6589ed742c1c29ba3db5ec8a02623d95cd2635 (current diff)
parent 157219 b26502022122a0248fce90301b6842f74fbc0eb6 (diff)
child 157221 641d5e86ed847bf4c3c2ff65468a79846d0f3dad
child 157278 b709129278df508be3031fde907944940aab4259
child 157323 84fcf9879bad7925eddc0d5ba02b12c06cfe4d35
push id25704
push usermbrubeck@mozilla.com
push dateSat, 23 Nov 2013 22:25:36 +0000
treeherdermozilla-central@74ab61b8d0f0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone28.0a1
first release with
nightly linux32
74ab61b8d0f0 / 28.0a1 / 20131124104839 / files
nightly linux64
74ab61b8d0f0 / 28.0a1 / 20131124104839 / files
nightly mac
74ab61b8d0f0 / 28.0a1 / 20131124104839 / files
nightly win32
74ab61b8d0f0 / 28.0a1 / 20131124104839 / files
nightly win64
74ab61b8d0f0 / 28.0a1 / 20131124030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to mozilla-central
browser/components/sessionstore/test/browser_408470.js
browser/components/sessionstore/test/browser_408470_sample.html
mobile/android/base/resources/drawable-hdpi-v11/ic_menu_share.png
mobile/android/base/resources/drawable-mdpi-v11/ic_menu_share.png
mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_share.png
--- a/CLOBBER
+++ b/CLOBBER
@@ -13,10 +13,9 @@
 #          |               |
 #          O <-- Clobber   O  <-- Clobber
 #
 # Note: The description below will be part of the error message shown to users.
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
-Bug 942184 - Landing and subsequent backout of bug 941450 increased the
-             possibility of random build bustage.
+Bug 941844 - Because Android resource builds are flaky.
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -141,20 +141,20 @@ pref("app.update.cert.maxErrors", 5);
 // This condition protects us against accidentally using it for release builds.
 #ifndef RELEASE_BUILD
 pref("app.update.certs.1.issuerName", "CN=DigiCert Secure Server CA,O=DigiCert Inc,C=US");
 pref("app.update.certs.1.commonName", "aus4.mozilla.org");
 
 pref("app.update.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
 pref("app.update.certs.2.commonName", "aus4.mozilla.org");
 #else
-pref("app.update.certs.1.issuerName", "OU=Equifax Secure Certificate Authority,O=Equifax,C=US");
+pref("app.update.certs.1.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
 pref("app.update.certs.1.commonName", "aus3.mozilla.org");
 
-pref("app.update.certs.2.issuerName", "CN=Thawte SSL CA,O=\"Thawte, Inc.\",C=US");
+pref("app.update.certs.2.issuerName", "CN=DigiCert Secure Server CA,O=DigiCert Inc,C=US");
 pref("app.update.certs.2.commonName", "aus3.mozilla.org");
 #endif
 #endif
 
 // Whether or not app updates are enabled
 pref("app.update.enabled", true);
 
 // This preference turns on app.update.mode and allows automatic download and
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -44,29 +44,23 @@ SocialUI = {
   // Called on delayed startup to initialize the UI
   init: function SocialUI_init() {
     Services.obs.addObserver(this, "social:ambient-notification-changed", false);
     Services.obs.addObserver(this, "social:profile-changed", false);
     Services.obs.addObserver(this, "social:frameworker-error", false);
     Services.obs.addObserver(this, "social:provider-set", false);
     Services.obs.addObserver(this, "social:providers-changed", false);
     Services.obs.addObserver(this, "social:provider-reload", false);
-    Services.obs.addObserver(this, "social:provider-installed", false);
-    Services.obs.addObserver(this, "social:provider-uninstalled", false);
     Services.obs.addObserver(this, "social:provider-enabled", false);
     Services.obs.addObserver(this, "social:provider-disabled", false);
 
     Services.prefs.addObserver("social.sidebar.open", this, false);
     Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
 
     gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
-    window.addEventListener("aftercustomization", function() {
-      if (SocialUI.enabled)
-        SocialMarks.populateContextMenu(SocialMarks);
-    }, false);
 
     if (!Social.initialized) {
       Social.init();
     } else if (Social.providers.length > 0) {
       // Social was initialized during startup in a previous window. If we have
       // providers enabled initialize the UI for this window.
       this.observe(null, "social:providers-changed", null);
       this.observe(null, "social:provider-set", Social.provider ? Social.provider.origin : null);
@@ -76,42 +70,32 @@ SocialUI = {
   // Called on window unload
   uninit: function SocialUI_uninit() {
     Services.obs.removeObserver(this, "social:ambient-notification-changed");
     Services.obs.removeObserver(this, "social:profile-changed");
     Services.obs.removeObserver(this, "social:frameworker-error");
     Services.obs.removeObserver(this, "social:provider-set");
     Services.obs.removeObserver(this, "social:providers-changed");
     Services.obs.removeObserver(this, "social:provider-reload");
-    Services.obs.removeObserver(this, "social:provider-installed");
-    Services.obs.removeObserver(this, "social:provider-uninstalled");
     Services.obs.removeObserver(this, "social:provider-enabled");
     Services.obs.removeObserver(this, "social:provider-disabled");
 
     Services.prefs.removeObserver("social.sidebar.open", this);
     Services.prefs.removeObserver("social.toast-notifications.enabled", this);
   },
 
   _matchesCurrentProvider: function (origin) {
     return Social.provider && Social.provider.origin == origin;
   },
 
   observe: function SocialUI_observe(subject, topic, data) {
     // Exceptions here sometimes don't get reported properly, report them
     // manually :(
     try {
       switch (topic) {
-        case "social:provider-installed":
-          SocialMarks.setPosition(data);
-          SocialStatus.setPosition(data);
-          break;
-        case "social:provider-uninstalled":
-          SocialMarks.removePosition(data);
-          SocialStatus.removePosition(data);
-          break;
         case "social:provider-enabled":
           SocialMarks.populateToolbarPalette();
           SocialStatus.populateToolbarPalette();
           break;
         case "social:provider-disabled":
           SocialMarks.removeProvider(data);
           SocialStatus.removeProvider(data);
           break;
@@ -1288,169 +1272,73 @@ SocialSidebar = {
     } else {
       let url = encodeURIComponent(Social.provider.sidebarURL);
       sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url, null, null);
     }
   }
 }
 
 // this helper class is used by removable/customizable buttons to handle
-// location persistence and insertion into palette and/or toolbars
+// widget creation/destruction
 
 // When a provider is installed we show all their UI so the user will see the
 // functionality of what they installed. The user can later customize the UI,
 // moving buttons around or off the toolbar.
 //
-// To make this happen, on install we add a button id to the navbar currentset.
-// On enabling the provider (happens just after install) we insert the button
-// into the toolbar as well. The button is then persisted on restart (assuming
-// it was not removed).
-//
-// When a provider is disabled, we do not remove the buttons from currentset.
-// That way, if the provider is re-enabled during the same session, the buttons
-// will reappear where they were before. When a provider is uninstalled, we make
-// sure that the id is removed from currentset.
-//
-// On startup, we insert the buttons of any enabled provider into either the
-// apropriate toolbar or the palette.
+// On startup, we create the button widgets of any enabled provider.
+// CustomizableUI handles placement and persistence of placement.
 function ToolbarHelper(type, createButtonFn) {
   this._createButton = createButtonFn;
   this._type = type;
 }
 
 ToolbarHelper.prototype = {
-  idFromOrgin: function(origin) {
-    return this._type + "-" + origin;
-  },
-
-  // find a button either in the document or the palette
-  _getExistingButton: function(id) {
-    let button = document.getElementById(id);
-    if (button)
-      return button;
-    let palette = document.getElementById("navigator-toolbox").palette;
-    let paletteItem = palette.firstChild;
-    while (paletteItem) {
-      if (paletteItem.id == id)
-        return paletteItem;
-      paletteItem = paletteItem.nextSibling;
-    }
-    return null;
+  idFromOrigin: function(origin) {
+    // this id needs to pass the checks in CustomizableUI, so remove characters
+    // that wont pass.
+    return this._type + "-" + Services.io.newURI(origin, null, null).hostPort.replace(/[\.:]/g,'-');
   },
 
-  setPersistentPosition: function(id) {
-    // called when a provider is installed.  add provider buttons to nav-bar
-    let toolbar = document.getElementById("nav-bar");
-    // first startups will not have a currentset attribute, always rely on
-    // currentSet since it will be derived from the defaultset in that case.
-    let currentset = toolbar.currentSet;
-    if (currentset == "__empty")
-      currentset = []
-    else
-      currentset = currentset.split(",");
-    if (currentset.indexOf(id) >= 0)
-      return;
-    // we do not set toolbar.currentSet since that will try to add the button,
-    // and we have not added it yet (happens on provider being enabled)
-    currentset.push(id);
-    toolbar.setAttribute("currentset", currentset.join(","));
-    document.persist(toolbar.id, "currentset");
+  // should be called on disable of a provider
+  removeProviderButton: function(origin) {
+    CustomizableUI.destroyWidget(this.idFromOrigin(origin));
   },
 
-  removeProviderButton: function(origin) {
-    // this will remove the button from the palette or the toolbar
-    let button = this._getExistingButton(this.idFromOrgin(origin));
-    if (button)
-      button.parentNode.removeChild(button);
-  },
-
-  removePersistence: function(id) {
-    let persisted = document.querySelectorAll("*[currentset]");
-    for (let pent of persisted) {
-      // the button will have been removed, but left in the currentset attribute
-      // in case the user re-enables (e.g. undo in addon manager). So we only
-      // check the attribute here.
-      let currentset = pent.getAttribute("currentset").split(",");
-
-      let pos = currentset.indexOf(id);
-      if (pos >= 0) {
-        currentset.splice(pos, 1);
-        pent.setAttribute("currentset", currentset.join(","));
-        document.persist(pent.id, "currentset");
-        return;
-      }
-    }
-  },
-
-  // if social is entirely disabled, we need to clear the palette, but leave
-  // the persisted id's in place
   clearPalette: function() {
     [this.removeProviderButton(p.origin) for (p of Social.providers)];
   },
 
-  // should be called on startup of each window, otherwise the addon manager
-  // listener will handle new activations, or enable/disabling of a provider
-  // XXX we currently call more regularly, will fix during refactoring
+  // should be called on enable of a provider
   populatePalette: function() {
     if (!Social.enabled) {
       this.clearPalette();
       return;
     }
-    let persisted = document.querySelectorAll("*[currentset]");
-    let persistedById = {};
-    for (let pent of persisted) {
-      let pset = pent.getAttribute("currentset").split(',');
-      for (let id of pset)
-        persistedById[id] = pent;
-    }
 
     // create any buttons that do not exist yet if they have been persisted
     // as a part of the UI (otherwise they belong in the palette).
     for (let provider of Social.providers) {
-      let id = this.idFromOrgin(provider.origin);
-      if (this._getExistingButton(id))
-        continue;
-      let button = this._createButton(provider);
-      if (button && persistedById.hasOwnProperty(id)) {
-        let parent = persistedById[id];
-        let pset = persistedById[id].getAttribute("currentset").split(',');
-        let pi = pset.indexOf(id) + 1;
-        let next = document.getElementById(pset[pi]);
-        parent.insertItem(id, next, null, false);
-      }
+      let id = this.idFromOrigin(provider.origin);
+      let widget = CustomizableUI.getWidget(id);
+      // The widget is only null if we've created then destroyed the widget.
+      // Once we've actually called createWidget the provider will be set to
+      // PROVIDER_API.
+      if (!widget || widget.provider != CustomizableUI.PROVIDER_API)
+        this._createButton(provider);
     }
   }
 }
 
 SocialStatus = {
   populateToolbarPalette: function() {
     if (!Social.allowMultipleWorkers)
       return;
     this._toolbarHelper.populatePalette();
   },
 
-  setPosition: function(origin) {
-    if (!Social.allowMultipleWorkers)
-      return;
-    // this is called during install, before the provider is enabled so we have
-    // to use the manifest rather than the provider instance as we do elsewhere.
-    let manifest = Social.getManifestByOrigin(origin);
-    if (!manifest.statusURL)
-      return;
-    let tbh = this._toolbarHelper;
-    tbh.setPersistentPosition(tbh.idFromOrgin(origin));
-  },
-
-  removePosition: function(origin) {
-    if (!Social.allowMultipleWorkers)
-      return;
-    let tbh = this._toolbarHelper;
-    tbh.removePersistence(tbh.idFromOrgin(origin));
-  },
-
   removeProvider: function(origin) {
     if (!Social.allowMultipleWorkers)
       return;
     this._removeFrame(origin);
     this._toolbarHelper.removeProviderButton(origin);
   },
 
   _removeFrame: function(origin) {
@@ -1471,30 +1359,40 @@ SocialStatus = {
   get _dynamicResizer() {
     delete this._dynamicResizer;
     this._dynamicResizer = new DynamicResizeWatcher();
     return this._dynamicResizer;
   },
 
   _createButton: function(provider) {
     if (!provider.statusURL)
-      return null;
-    let palette = document.getElementById("navigator-toolbox").palette;
-    let button = document.createElement("toolbarbutton");
-    button.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-status-button");
-    button.setAttribute("type", "badged");
-    button.setAttribute("removable", "true");
-    button.setAttribute("image", provider.iconURL);
-    button.setAttribute("label", provider.name);
-    button.setAttribute("tooltiptext", provider.name);
-    button.setAttribute("origin", provider.origin);
-    button.setAttribute("oncommand", "SocialStatus.showPopup(this);");
-    button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin));
-    palette.appendChild(button);
-    return button;
+      return;
+    let aId = this._toolbarHelper.idFromOrigin(provider.origin);
+    CustomizableUI.createWidget({
+      id: aId,
+      type: 'custom',
+      removable: true,
+      defaultArea: CustomizableUI.AREA_NAVBAR,
+      onBuild: function(document) {
+        let window = document.defaultView;
+
+        let node = document.createElement('toolbarbutton');
+
+        node.id = this.id;
+        node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-status-button');
+        node.setAttribute('type', "badged");
+        node.style.listStyleImage = "url(" + provider.iconURL + ")";
+        node.setAttribute("origin", provider.origin);
+
+        node.setAttribute("label", provider.name);
+        node.setAttribute("tooltiptext", provider.name);
+        node.setAttribute("oncommand", "SocialStatus.showPopup(this);");
+        return node;
+      }
+    });
   },
 
   // status panels are one-per button per-process, we swap the docshells between
   // windows when necessary
   _attachNotificatonPanel: function(aButton, provider) {
     let panel = document.getElementById("social-notification-panel");
     panel.hidden = !SocialUI.enabled;
     let notificationFrameId = "social-status-" + provider.origin;
@@ -1533,17 +1431,17 @@ SocialStatus = {
     }
     aButton.setAttribute("notificationFrameId", notificationFrameId);
   },
 
   updateNotification: function(origin) {
     if (!Social.allowMultipleWorkers)
       return;
     let provider = Social._getProviderFromOrigin(origin);
-    let button = document.getElementById(this._toolbarHelper.idFromOrgin(provider.origin));
+    let button = document.getElementById(this._toolbarHelper.idFromOrigin(provider.origin));
     if (button) {
       // we only grab the first notification, ignore all others
       let icons = provider.ambientNotificationIcons;
       let iconNames = Object.keys(icons);
       let notif = icons[iconNames[0]];
       if (!notif) {
         button.setAttribute("badge", "");
         button.setAttribute("aria-label", "");
@@ -1668,17 +1566,17 @@ SocialMarks = {
 
   getProviders: function() {
     // only rely on providers that the user has placed in the UI somewhere. This
     // also means that populateToolbarPalette must be called prior to using this
     // method, otherwise you get a big fat zero. For our use case with context
     // menu's, this is ok.
     let tbh = this._toolbarHelper;
     return [p for (p of Social.providers) if (p.markURL &&
-                                              document.getElementById(tbh.idFromOrgin(p.origin)))];
+                                              document.getElementById(tbh.idFromOrigin(p.origin)))];
   },
 
   populateContextMenu: function() {
     // only show a selection if enabled and there is more than one
     let providers = this.getProviders();
 
     // remove all previous entries by class
     let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
@@ -1727,55 +1625,51 @@ SocialMarks = {
     }
   },
 
   populateToolbarPalette: function() {
     this._toolbarHelper.populatePalette();
     this.populateContextMenu();
   },
 
-  setPosition: function(origin) {
-    // this is called during install, before the provider is enabled so we have
-    // to use the manifest rather than the provider instance as we do elsewhere.
-    let manifest = Social.getManifestByOrigin(origin);
-    if (!manifest.markURL)
-      return;
-    let tbh = this._toolbarHelper;
-    tbh.setPersistentPosition(tbh.idFromOrgin(origin));
-  },
-
-  removePosition: function(origin) {
-    let tbh = this._toolbarHelper;
-    tbh.removePersistence(tbh.idFromOrgin(origin));
-  },
-
   removeProvider: function(origin) {
     this._toolbarHelper.removeProviderButton(origin);
   },
 
   get _toolbarHelper() {
     delete this._toolbarHelper;
     this._toolbarHelper = new ToolbarHelper("social-mark-button", this._createButton.bind(this));
     return this._toolbarHelper;
   },
 
   _createButton: function(provider) {
     if (!provider.markURL)
-      return null;
-    let palette = document.getElementById("navigator-toolbox").palette;
-    let button = document.createElement("toolbarbutton");
-    button.setAttribute("type", "socialmark");
-    button.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional social-mark-button");
-    button.style.listStyleImage = "url(" + provider.iconURL + ")";
-    button.setAttribute("origin", provider.origin);
-    button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin));
-    palette.appendChild(button);
-    return button
+      return;
+    let aId = this._toolbarHelper.idFromOrigin(provider.origin);
+    CustomizableUI.createWidget({
+      id: aId,
+      type: 'custom',
+      removable: true,
+      defaultArea: CustomizableUI.AREA_NAVBAR,
+      onBuild: function(document) {
+        let window = document.defaultView;
+
+        let node = document.createElement('toolbarbutton');
+
+        node.id = this.id;
+        node.setAttribute('class', 'toolbarbutton-1 chromeclass-toolbar-additional social-mark-button');
+        node.setAttribute('type', "socialmark");
+        node.style.listStyleImage = "url(" + provider.iconURL + ")";
+        node.setAttribute("origin", provider.origin);
+
+        return node;
+      }
+    });
   },
 
   markLink: function(aOrigin, aUrl) {
     // find the button for this provider, and open it
-    let id = this._toolbarHelper.idFromOrgin(aOrigin);
+    let id = this._toolbarHelper.idFromOrigin(aOrigin);
     document.getElementById(id).markLink(aUrl);
   }
 };
 
 })();
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -959,17 +959,17 @@
                          context="placesContext"/>
             </toolbarbutton>
           </hbox>
         </hbox>
       </toolbaritem>
     </toolbar>
 
     <!-- This is a shim which will go away ASAP. See bug 749804 for details -->
-    <toolbar id="addon-bar" toolbar-delegate="nav-bar">
+    <toolbar id="addon-bar" toolbar-delegate="nav-bar" mode="icons" iconsize="small">
       <hbox id="addonbar-closebutton"/>
       <statusbar id="status-bar"/>
     </toolbar>
 
     <toolbarpalette id="BrowserToolbarPalette">
 
 # Update primaryToolbarButtons in browser/themes/shared/browser.inc when adding
 # or removing default items with the toolbarbutton-1 class.
--- a/browser/base/content/chatWindow.xul
+++ b/browser/base/content/chatWindow.xul
@@ -52,49 +52,92 @@ var gChatWindow = {
                          'Browser:OpenLocation', 'Tools:Search',
 #endif
                          'Tools:Sanitize', 'Tools:DevToolbox',
                          'key_selectTab1', 'key_selectTab2', 'key_selectTab3',
                          'key_selectTab4', 'key_selectTab5', 'key_selectTab6',
                          'key_selectTab7', 'key_selectTab8', 'key_selectLastTab',
                          'viewHistorySidebar', 'viewBookmarksSidebar',
                          'Browser:AddBookmarkAs', 'Browser:BookmarkAllTabs',
-                         'Browser:ToggleTabView', 'Browser:ToggleAddonBar'];
+                         'Browser:ToggleTabView'];
 
     for (let disabledItem of disabledItems) {
       document.getElementById(disabledItem).setAttribute("disabled", "true");
     }
 
+    window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow =
+      new chatBrowserAccess();
+
     // initialise the offline listener
     BrowserOffline.init();
   },
 
   onUnload: function() {
     BrowserOffline.uninit();
   }
 }
 
-__defineGetter__("content", function()
-  document.getElementById("chatter").contentWindow);
-
 // define a popupnotifications handler for this window.  we don't use
 // an iconbox here, and only support the browser frame for chat.
 XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () {
   let tmp = {};
   Cu.import("resource://gre/modules/PopupNotifications.jsm", tmp);
   try {
     return new tmp.PopupNotifications(document.getElementById("chatter").content,
                                       document.getElementById("notification-popup"),
                                       null);
   } catch (ex) {
     console.error(ex);
     return null;
   }
 });
 
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
+
+function chatBrowserAccess() { }
+
+chatBrowserAccess.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow, Ci.nsISupports]),
+
+  _openURIInNewTab: function(aURI, aWhere) {
+    if (aWhere != Ci.nsIBrowserDOMWindow.OPEN_NEWTAB)
+      return null;
+
+    let win = RecentWindow.getMostRecentBrowserWindow();
+    if (!win) {
+      // We couldn't find a suitable window, a new one needs to be opened.
+      return null;
+    }
+
+    let loadInBackground =
+      Services.prefs.getBoolPref("browser.tabs.loadDivertedInBackground");
+    let tab = win.gBrowser.loadOneTab(aURI ? aURI.spec : "about:blank",
+                                      {inBackground: loadInBackground});
+    let browser = win.gBrowser.getBrowserForTab(tab);
+    win.focus();
+
+    return browser;
+  },
+
+  openURI: function (aURI, aOpener, aWhere, aContext) {
+    let browser = this._openURIInNewTab(aURI, aWhere);
+    return browser ? browser.contentWindow : null;
+  },
+
+  openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
+    let browser = this._openURIInNewTab(aURI, aWhere);
+    return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+  },
+
+  isTabContentWindow: function (aWindow) this.contentWindow == aWindow,
+
+  get contentWindow() document.getElementById("chatter").contentWindow
+};
+
 </script>
 
 #include browser-sets.inc
 
 #ifdef XP_MACOSX
 #include browser-menubar.inc
 #endif
 
--- a/browser/base/content/test/social/browser_social_marks.js
+++ b/browser/base/content/test/social/browser_social_marks.js
@@ -59,131 +59,86 @@ function test() {
   let toolbar = document.getElementById("nav-bar");
   let currentsetAtStart = toolbar.currentSet;
   runSocialTestWithProvider(manifest, function () {
     runSocialTests(tests, undefined, undefined, function () {
       Services.prefs.clearUserPref("social.remote-install.enabled");
       // just in case the tests failed, clear these here as well
       Services.prefs.clearUserPref("social.allowMultipleWorkers");
       Services.prefs.clearUserPref("social.whitelist");
-
-      // This post-test test ensures that a new window maintains the same
-      // toolbar button set as when we started. That means our insert/removal of
-      // persistent id's is working correctly
-      is(currentsetAtStart, toolbar.currentSet, "toolbar currentset unchanged");
-      openWindowAndWaitForInit(function(w1) {
-        checkSocialUI(w1);
-        // Sometimes the new window adds other buttons to currentSet that are
-        // outside the scope of what we're checking. So we verify that all
-        // buttons from startup are in currentSet for a new window, and that the
-        // provider buttons are properly removed. (e.g on try, window-controls
-        // was not present in currentsetAtStart, but present on the second
-        // window)
-        let tb1 = w1.document.getElementById("nav-bar");
-        let startupSet = Set(toolbar.currentSet.split(','));
-        let newSet = Set(tb1.currentSet.split(','));
-        let intersect = Set([x for (x of startupSet) if (newSet.has(x))]);
-        let difference = Set([x for (x of newSet) if (!startupSet.has(x))]);
-        is(startupSet.size, intersect.size, "new window toolbar same as old");
-        // verify that our provider buttons are not in difference
-        let id = SocialMarks._toolbarHelper.idFromOrgin(manifest2.origin);
-        ok(!difference.has(id), "mark button not persisted at end");
-        w1.close();
-        finish();
-      });
+      ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
+      CustomizableUI.reset();
+      finish();
     });
   });
 }
 
 var tests = {
-  testNoButtonOnInstall: function(next) {
+  testNoButtonOnEnable: function(next) {
     // we expect the addon install dialog to appear, we need to accept the
     // install from the dialog.
-    info("Waiting for install dialog");
     let panel = document.getElementById("servicesInstall-notification");
     PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
       PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
       info("servicesInstall-notification panel opened");
       panel.button.click();
-    })
-
-    let id = "social-mark-button-" + manifest3.origin;
-    let toolbar = document.getElementById("nav-bar");
-    let currentset = toolbar.getAttribute("currentset").split(',');
-    ok(currentset.indexOf(id) < 0, "button is not part of currentset at start");
+    });
 
     let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
       let doc = tab.linkedBrowser.contentDocument;
       Social.installProvider(doc, manifest3, function(addonManifest) {
         // enable the provider so we know the button would have appeared
         SocialService.addBuiltinProvider(manifest3.origin, function(provider) {
-          ok(provider, "provider is installed");
-          currentset = toolbar.getAttribute("currentset").split(',');
-          ok(currentset.indexOf(id) < 0, "button was not added to currentset");
+          is(provider.origin, manifest3.origin, "provider is installed");
+          let id = SocialMarks._toolbarHelper.idFromOrigin(provider.origin);
+          let widget = CustomizableUI.getWidget(id);
+          ok(!widget || !widget.forWindow(window).node, "no button added to widget set");
           Social.uninstallProvider(manifest3.origin, function() {
             gBrowser.removeTab(tab);
             next();
           });
         });
       });
     });
   },
 
-  testButtonOnInstall: function(next) {
-    // we expect the addon install dialog to appear, we need to accept the
-    // install from the dialog.
-    info("Waiting for install dialog");
+  testButtonOnEnable: function(next) {
     let panel = document.getElementById("servicesInstall-notification");
     PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
       PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
       info("servicesInstall-notification panel opened");
       panel.button.click();
-    })
+    });
 
+    // enable the provider now
     let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
       let doc = tab.linkedBrowser.contentDocument;
       Social.installProvider(doc, manifest2, function(addonManifest) {
-        // at this point, we should have a button id in the currentset for our provider
-        let id = "social-mark-button-" + manifest2.origin;
-        let toolbar = document.getElementById("nav-bar");
-
-        waitForCondition(function() {
-                            let currentset = toolbar.getAttribute("currentset").split(',');
-                            return currentset.indexOf(id) >= 0;
-                         },
-                         function() {
-                           // no longer need the tab
-                           gBrowser.removeTab(tab);
-                           next();
-                         }, "mark button added to currentset");
+        SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
+          is(provider.origin, manifest2.origin, "provider is installed");
+          let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+          let widget = CustomizableUI.getWidget(id).forWindow(window)
+          ok(widget.node, "button added to widget set");
+          checkSocialUI(window);
+          gBrowser.removeTab(tab);
+          next();
+        });
       });
     });
   },
 
-  testButtonOnEnable: function(next) {
-    // enable the provider now
-    SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
-      ok(provider, "provider is installed");
-      let id = "social-mark-button-" + manifest2.origin;
-      waitForCondition(function() { return document.getElementById(id) },
-                       function() {
-                         checkSocialUI(window);
-                         next();
-                       }, "button exists after enabling social");
-    });
-  },
-
   testMarkPanel: function(next) {
     // click on panel to open and wait for visibility
     let provider = Social._getProviderFromOrigin(manifest2.origin);
     ok(provider.enabled, "provider is enabled");
-    let id = "social-mark-button-" + provider.origin;
-    let btn = document.getElementById(id)
+    let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
     ok(btn, "got a mark button");
     let port = provider.getWorkerPort();
     ok(port, "got a port");
 
     // verify markbutton is disabled when there is no browser url
     ok(btn.disabled, "button is disabled");
     let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
@@ -232,18 +187,19 @@ var tests = {
       port.postMessage({topic: "test-init"});
     });
   },
 
   testMarkPanelLoggedOut: function(next) {
     // click on panel to open and wait for visibility
     let provider = Social._getProviderFromOrigin(manifest2.origin);
     ok(provider.enabled, "provider is enabled");
-    let id = "social-mark-button-" + provider.origin;
-    let btn = document.getElementById(id)
+    let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
     ok(btn, "got a mark button");
     let port = provider.getWorkerPort();
     ok(port, "got a port");
 
     // verify markbutton is disabled when there is no browser url
     ok(btn.disabled, "button is disabled");
     let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
@@ -295,36 +251,28 @@ var tests = {
     });
   },
 
   testButtonOnDisable: function(next) {
     // enable the provider now
     let provider = Social._getProviderFromOrigin(manifest2.origin);
     ok(provider, "provider is installed");
     SocialService.removeProvider(manifest2.origin, function() {
-      let id = "social-mark-button-" + manifest2.origin;
-      waitForCondition(function() { return !document.getElementById(id) },
+      let id = SocialMarks._toolbarHelper.idFromOrigin(manifest2.origin);
+      waitForCondition(function() {
+                        // getWidget now returns null since we've destroyed the widget
+                        return !CustomizableUI.getWidget(id)
+                       },
                        function() {
                          checkSocialUI(window);
-                         next();
+                         Social.uninstallProvider(manifest2.origin, next);
                        }, "button does not exist after disabling the provider");
     });
   },
 
-  testButtonOnUninstall: function(next) {
-    Social.uninstallProvider(manifest2.origin, function() {
-      // test that the button is no longer persisted
-      let id = "social-mark-button-" + manifest2.origin;
-      let toolbar = document.getElementById("nav-bar");
-      let currentset = toolbar.getAttribute("currentset").split(',');
-      is(currentset.indexOf(id), -1, "button no longer in currentset");
-      next();
-    });
-  },
-
   testContextSubmenu: function(next) {
     // install 4 providers to test that the menu's are added as submenus
     let manifests = [
       makeMarkProvider("sub1.test1"),
       makeMarkProvider("sub2.test1"),
       makeMarkProvider("sub1.test2"),
       makeMarkProvider("sub2.test2")
     ];
@@ -343,41 +291,34 @@ var tests = {
       let panel = document.getElementById("servicesInstall-notification");
       PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
         PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
         info("servicesInstall-notification panel opened");
         panel.button.click();
       })
 
       let activationURL = manifest.origin + "/browser/browser/base/content/test/social/social_activate.html"
-      let id = "social-mark-button-" + manifest.origin;
+      let id = SocialMarks._toolbarHelper.idFromOrigin(manifest.origin);
       let toolbar = document.getElementById("nav-bar");
       addTab(activationURL, function(tab) {
         let doc = tab.linkedBrowser.contentDocument;
         Social.installProvider(doc, manifest, function(addonManifest) {
-
-          waitForCondition(function() {
-            let currentset = toolbar.getAttribute("currentset").split(',');
-            return currentset.indexOf(id) >= 0;
-          },
-          function() {
-            // enable the provider so we know the button would have appeared
-            SocialService.addBuiltinProvider(manifest.origin, function(provider) {
-              waitForCondition(function() { return document.getElementById(id) },
-                               function() {
-                gBrowser.removeTab(tab);
-                installed.push(manifest.origin);
-                // checkSocialUI will properly check where the menus are located
-                checkSocialUI(window);
-                executeSoon(function() {
-                  addProviders(callback);
-                });
-              }, "button exists after enabling social");
-            });
-          }, "mark button added to currentset");
+          // enable the provider so we know the button would have appeared
+          SocialService.addBuiltinProvider(manifest.origin, function(provider) {
+            waitForCondition(function() { return CustomizableUI.getWidget(id) },
+                             function() {
+              gBrowser.removeTab(tab);
+              installed.push(manifest.origin);
+              // checkSocialUI will properly check where the menus are located
+              checkSocialUI(window);
+              executeSoon(function() {
+                addProviders(callback);
+              });
+            }, "button exists after enabling social");
+          });
         });
       });
     }
 
     function removeProviders(callback) {
       let origin = installed.pop();
       if (!origin) {
         executeSoon(callback);
--- a/browser/base/content/test/social/browser_social_status.js
+++ b/browser/base/content/test/social/browser_social_status.js
@@ -36,142 +36,94 @@ function openWindowAndWaitForInit(callba
     executeSoon(() => callback(w));
   }, topic, false);
 }
 
 function test() {
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
-  let toolbar = document.getElementById("nav-bar");
-  let currentsetAtStart = toolbar.currentSet;
-  info("tb0 "+currentsetAtStart);
   runSocialTestWithProvider(manifest, function () {
     runSocialTests(tests, undefined, undefined, function () {
       Services.prefs.clearUserPref("social.remote-install.enabled");
       // just in case the tests failed, clear these here as well
       Services.prefs.clearUserPref("social.allowMultipleWorkers");
       Services.prefs.clearUserPref("social.whitelist");
-      
-      // This post-test test ensures that a new window maintains the same
-      // toolbar button set as when we started. That means our insert/removal of
-      // persistent id's is working correctly
-      is(currentsetAtStart, toolbar.currentSet, "toolbar currentset unchanged");
-      openWindowAndWaitForInit(function(w1) {
-        checkSocialUI(w1);
-        // Sometimes the new window adds other buttons to currentSet that are
-        // outside the scope of what we're checking. So we verify that all
-        // buttons from startup are in currentSet for a new window, and that the
-        // provider buttons are properly removed. (e.g on try, window-controls
-        // was not present in currentsetAtStart, but present on the second
-        // window)
-        let tb1 = w1.document.getElementById("nav-bar");
-        info("tb0 "+toolbar.currentSet);
-        info("tb1 "+tb1.currentSet);
-        let startupSet = Set(toolbar.currentSet.split(','));
-        let newSet = Set(tb1.currentSet.split(','));
-        let intersect = Set([x for (x of startupSet) if (newSet.has(x))]);
-        info("intersect "+intersect);
-        let difference = Set([x for (x of newSet) if (!startupSet.has(x))]);
-        info("difference "+difference);
-        is(startupSet.size, intersect.size, "new window toolbar same as old");
-        // verify that our provider buttons are not in difference
-        let id = SocialStatus._toolbarHelper.idFromOrgin(manifest2.origin);
-        ok(!difference.has(id), "status button not persisted at end");
-        w1.close();
-        finish();
-      });
+      ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish");
+      CustomizableUI.reset();
+      finish();
     });
   });
 }
 
 var tests = {
-  testNoButtonOnInstall: function(next) {
+  testNoButtonOnEnable: function(next) {
     // we expect the addon install dialog to appear, we need to accept the
     // install from the dialog.
-    info("Waiting for install dialog");
     let panel = document.getElementById("servicesInstall-notification");
     PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
       PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
       info("servicesInstall-notification panel opened");
       panel.button.click();
     })
 
-    let id = "social-status-button-" + manifest3.origin;
-    let toolbar = document.getElementById("nav-bar");
-    let currentset = toolbar.getAttribute("currentset").split(',');
-    ok(currentset.indexOf(id) < 0, "button is not part of currentset at start");
-
     let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
       let doc = tab.linkedBrowser.contentDocument;
       Social.installProvider(doc, manifest3, function(addonManifest) {
         // enable the provider so we know the button would have appeared
         SocialService.addBuiltinProvider(manifest3.origin, function(provider) {
-          ok(provider, "provider is installed");
-          currentset = toolbar.getAttribute("currentset").split(',');
-          ok(currentset.indexOf(id) < 0, "button was not added to currentset");
+          is(provider.origin, manifest3.origin, "provider is installed");
+          let id = SocialStatus._toolbarHelper.idFromOrigin(provider.origin);
+          let widget = CustomizableUI.getWidget(id);
+          ok(!widget || !widget.forWindow(window).node, "no button added to widget set");
           Social.uninstallProvider(manifest3.origin, function() {
             gBrowser.removeTab(tab);
             next();
           });
         });
       });
     });
   },
-  testButtonOnInstall: function(next) {
-    // we expect the addon install dialog to appear, we need to accept the
-    // install from the dialog.
-    info("Waiting for install dialog");
+  testButtonOnEnable: function(next) {
     let panel = document.getElementById("servicesInstall-notification");
     PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
       PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
       info("servicesInstall-notification panel opened");
       panel.button.click();
-    })
+    });
 
+    // enable the provider now
     let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
     addTab(activationURL, function(tab) {
       let doc = tab.linkedBrowser.contentDocument;
       Social.installProvider(doc, manifest2, function(addonManifest) {
-          // at this point, we should have a button id in the currentset for our provider
-          let id = "social-status-button-" + manifest2.origin;
-          let toolbar = document.getElementById("nav-bar");
-
-          waitForCondition(function() {
-                             let currentset = toolbar.getAttribute("currentset").split(',');
-                             return currentset.indexOf(id) >= 0;
-                           },
-                           function() {
-                             // no longer need the tab
-                             gBrowser.removeTab(tab);
-                             next();
-                           }, "status button added to currentset");
+        SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
+          is(provider.origin, manifest2.origin, "provider is installed");
+          let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+          let widget = CustomizableUI.getWidget(id).forWindow(window);
+          ok(widget.node, "button added to widget set");
+          checkSocialUI(window);
+          gBrowser.removeTab(tab);
+          next();
+        });
       });
     });
   },
-  testButtonOnEnable: function(next) {
-    // enable the provider now
-    SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
-      ok(provider, "provider is installed");
-      let id = "social-status-button-" + manifest2.origin;
-      waitForCondition(function() { return document.getElementById(id) },
-                       next, "button exists after enabling social");
-    });
-  },
   testStatusPanel: function(next) {
     let icon = {
       name: "testIcon",
       iconURL: "chrome://browser/skin/Info.png",
       counter: 1
     };
     // click on panel to open and wait for visibility
     let provider = Social._getProviderFromOrigin(manifest2.origin);
-    let id = "social-status-button-" + provider.origin;
-    let btn = document.getElementById(id)
+    let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
+    let widget = CustomizableUI.getWidget(id);
+    let btn = widget.forWindow(window).node;
     ok(btn, "got a status button");
     let port = provider.getWorkerPort();
 
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "test-init-done":
           ok(true, "test-init-done received");
@@ -197,24 +149,16 @@ var tests = {
     };
     port.postMessage({topic: "test-init"});
   },
   testButtonOnDisable: function(next) {
     // enable the provider now
     let provider = Social._getProviderFromOrigin(manifest2.origin);
     ok(provider, "provider is installed");
     SocialService.removeProvider(manifest2.origin, function() {
-      let id = "social-status-button-" + manifest2.origin;
+      let id = SocialStatus._toolbarHelper.idFromOrigin(manifest2.origin);
       waitForCondition(function() { return !document.getElementById(id) },
-                       next, "button does not exist after disabling the provider");
-    });
-  },
-  testButtonOnUninstall: function(next) {
-    Social.uninstallProvider(manifest2.origin, function() {
-      // test that the button is no longer persisted
-      let id = "social-status-button-" + manifest2.origin;
-      let toolbar = document.getElementById("nav-bar");
-      let currentset = toolbar.getAttribute("currentset").split(',');
-      is(currentset.indexOf(id), -1, "button no longer in currentset");
-      next();
+                       function() {
+                        Social.uninstallProvider(manifest2.origin, next);
+                       }, "button does not exist after disabling the provider");
     });
   }
 }
--- a/browser/base/content/test/social/browser_social_toolbar.js
+++ b/browser/base/content/test/social/browser_social_toolbar.js
@@ -14,27 +14,19 @@ let manifests = [{
   iconURL: "https://test1.example.com/browser/browser/base/content/test/general/moz.png",
 }];
 
 function test() {
   waitForExplicitFinish();
 
   // required to test status button in combination with the toolbaritem
   Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
-
-  // Preset the currentSet so the statusbutton is in the toolbar on addition. We
-  // bypass the SocialStatus class here since it requires the manifest already
-  // be installed.
-  let tbh = SocialStatus._toolbarHelper;
-  tbh.setPersistentPosition(tbh.idFromOrgin(manifests[1].origin));
-
   runSocialTestWithProvider(manifests, function (finishcb) {
     runSocialTests(tests, undefined, undefined, function() {
       Services.prefs.clearUserPref("social.allowMultipleWorkers");
-      SocialStatus.removePosition(manifests[1].origin);
       finishcb();
     });
   });
 }
 
 var tests = {
   testProfileNone: function(next, useNull) {
     let profile = useNull ? null : {};
--- a/browser/branding/aurora/branding.nsi
+++ b/browser/branding/aurora/branding.nsi
@@ -8,17 +8,17 @@
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Aurora"
 !define CompanyName           "mozilla.org"
 !define URLInfoAbout          "http://www.mozilla.org"
 !define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
 
-!define URLStubDownload "http://download.mozilla.org/?product=firefox-aurora-latest&os=win&lang=${AB_CD}"
+!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-aurora-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=aurora&installer_lang=${AB_CD}"
 !define Channel "aurora"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "DigiCert Assured ID Code Signing CA-1"
 
 # Dialog units are used so the UI displays correctly with the system's DPI
--- a/browser/branding/nightly/branding.nsi
+++ b/browser/branding/nightly/branding.nsi
@@ -8,17 +8,17 @@
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Nightly"
 !define CompanyName           "mozilla.org"
 !define URLInfoAbout          "http://www.mozilla.org"
 !define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
 
-!define URLStubDownload "http://download.mozilla.org/?product=firefox-nightly-latest&os=win&lang=${AB_CD}"
+!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-nightly-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=nightly&installer_lang=${AB_CD}"
 !define Channel "nightly"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "DigiCert Assured ID Code Signing CA-1"
 
 # Dialog units are used so the UI displays correctly with the system's DPI
--- a/browser/branding/official/branding.nsi
+++ b/browser/branding/official/branding.nsi
@@ -12,17 +12,17 @@
 !define CompanyName           "Mozilla Corporation"
 !define URLInfoAbout          "https://www.mozilla.org/${AB_CD}/"
 !define URLUpdateInfo         "https://www.mozilla.org/${AB_CD}/firefox/"
 
 ; The OFFICIAL define is a workaround to support different urls for Release and
 ; Beta since they share the same branding when building with other branches that
 ; set the update channel to beta.
 !define OFFICIAL
-!define URLStubDownload "http://download.mozilla.org/?product=firefox-latest&os=win&lang=${AB_CD}"
+!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
 !define Channel "release"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "DigiCert Assured ID Code Signing CA-1"
 
 # Dialog units are used so the UI displays correctly with the system's DPI
--- a/browser/branding/unofficial/branding.nsi
+++ b/browser/branding/unofficial/branding.nsi
@@ -8,17 +8,17 @@
 
 # BrandFullNameInternal is used for some registry and file system values
 # instead of BrandFullName and typically should not be modified.
 !define BrandFullNameInternal "Mozilla Developer Preview"
 !define CompanyName           "mozilla.org"
 !define URLInfoAbout          "http://www.mozilla.org"
 !define URLUpdateInfo         "http://www.mozilla.org/projects/firefox"
 
-!define URLStubDownload "http://download.mozilla.org/?product=firefox-latest&os=win&lang=${AB_CD}"
+!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-latest"
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=release&installer_lang=${AB_CD}"
 !define Channel "unofficial"
 
 # The installer's certificate name and issuer expected by the stub installer
 !define CertNameDownload   "Mozilla Corporation"
 !define CertIssuerDownload "Thawte Code Signing CA - G2"
 
 # Dialog units are used so the UI displays correctly with the system's DPI
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -117,17 +117,16 @@ const PanelUI = {
    */
   show: function(aEvent) {
     if (this.panel.state == "open" || this.panel.state == "showing" ||
         document.documentElement.hasAttribute("customizing")) {
       return;
     }
 
     this.ensureReady().then(() => {
-      this.panel.hidden = false;
       let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
       if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
         updateEditUIVisibility();
       }
 
       let anchor;
       if (!aEvent ||
           aEvent.type == "command") {
@@ -219,16 +218,17 @@ const PanelUI = {
 
       if (aCustomizing) {
         CustomizableUI.registerMenuPanel(this.contents);
       } else {
         this.beginBatchUpdate();
         CustomizableUI.registerMenuPanel(this.contents);
         this.endBatchUpdate();
       }
+      this.panel.hidden = false;
     }.bind(this)).then(null, Cu.reportError);
 
     return this._readyPromise;
   },
 
   /**
    * Switch the panel to the main view if it's not already
    * in that view.
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -554,31 +554,31 @@ let CustomizableUIInternal = {
   registerMenuPanel: function(aPanel) {
     if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
         gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanel)) {
       return;
     }
 
     let document = aPanel.ownerDocument;
 
+    aPanel.toolbox = document.getElementById("navigator-toolbox");
+    aPanel.customizationTarget = aPanel;
+
+    this.addPanelCloseListeners(aPanel);
+
+    let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
+    this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanel);
     for (let btn of aPanel.querySelectorAll("toolbarbutton")) {
       btn.setAttribute("tabindex", "0");
       this.ensureButtonContextMenu(btn, aPanel);
       if (!btn.hasAttribute("type")) {
         btn.setAttribute("type", "wrap");
       }
     }
 
-    aPanel.toolbox = document.getElementById("navigator-toolbox");
-    aPanel.customizationTarget = aPanel;
-
-    this.addPanelCloseListeners(aPanel);
-
-    let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
-    this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanel);
     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanel);
   },
 
   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
     this.insertNode(aWidgetId, aArea, aPosition, true);
   },
 
   onWidgetRemoved: function(aWidgetId, aArea) {
--- a/browser/components/customizableui/test/browser_880164_customization_context_menus.js
+++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js
@@ -178,60 +178,102 @@ let gTests = [
       checkContextMenu(contextMenu, expectedEntries);
 
       let hiddenContextPromise = contextMenuHidden(contextMenu);
       contextMenu.hidePopup();
       yield hiddenContextPromise;
     },
     teardown: endCustomizing
   },
+  {
+    desc: "Test the toolbarbutton panel context menu in customization mode without opening the panel before customization mode",
+    setup: null,
+    run: function() {
+      this.otherWin = yield openAndLoadWindow(null, true);
+
+      yield startCustomizing(this.otherWin);
+
+      let contextMenu = this.otherWin.document.getElementById("customizationPanelItemContextMenu");
+      let shownPromise = contextMenuShown(contextMenu);
+      let newWindowButton = this.otherWin.document.getElementById("wrapper-new-window-button");
+      EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}, this.otherWin);
+      yield shownPromise;
+
+      let expectedEntries = [
+        [".customize-context-addToToolbar", true],
+        [".customize-context-removeFromPanel", true],
+        ["---"],
+        [".viewCustomizeToolbar", false]
+      ];
+      checkContextMenu(contextMenu, expectedEntries, this.otherWin);
+
+      let hiddenContextPromise = contextMenuHidden(contextMenu);
+      contextMenu.hidePopup();
+      yield hiddenContextPromise;
+    },
+    teardown: function() {
+      yield endCustomizing(this.otherWin);
+      this.otherWin.close();
+      this.otherWin = null;
+    }
+  },
 ];
 
 function test() {
   waitForExplicitFinish();
   runTests(gTests);
 }
 
 function contextMenuShown(aContextMenu) {
   let deferred = Promise.defer();
+  let win = aContextMenu.ownerDocument.defaultView;
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Context menu (" + aContextMenu.id + ") did not show within 20 seconds.");
+  }, 20000);
   function onPopupShown(e) {
     aContextMenu.removeEventListener("popupshown", onPopupShown);
+    win.clearTimeout(timeoutId);
     deferred.resolve();
   };
   aContextMenu.addEventListener("popupshown", onPopupShown);
   return deferred.promise;
 }
 
 function contextMenuHidden(aContextMenu) {
   let deferred = Promise.defer();
+  let win = aContextMenu.ownerDocument.defaultView;
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Context menu (" + aContextMenu.id + ") did not hide within 20 seconds.");
+  }, 20000);
   function onPopupHidden(e) {
+    win.clearTimeout(timeoutId);
     aContextMenu.removeEventListener("popuphidden", onPopupHidden);
     deferred.resolve();
   };
   aContextMenu.addEventListener("popuphidden", onPopupHidden);
   return deferred.promise;
 }
 
 // This is a simpler version of the context menu check that
 // exists in contextmenu_common.js.
-function checkContextMenu(aContextMenu, aExpectedEntries) {
+function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) {
   let childNodes = aContextMenu.childNodes;
   for (let i = 0; i < childNodes.length; i++) {
     let menuitem = childNodes[i];
     try {
       if (aExpectedEntries[i][0] == "---") {
         is(menuitem.localName, "menuseparator", "menuseparator expected");
         continue;
       }
 
       let selector = aExpectedEntries[i][0];
       ok(menuitem.mozMatchesSelector(selector), "menuitem should match " + selector + " selector");
       let commandValue = menuitem.getAttribute("command");
-      let relatedCommand = commandValue ? document.getElementById(commandValue) : null;
+      let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null;
       let menuItemDisabled = relatedCommand ?
                                relatedCommand.getAttribute("disabled") == "true" :
                                menuitem.getAttribute("disabled") == "true";
-      is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state wrong for " + selector);
+      is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector);
     } catch (e) {
       ok(false, "Exception when checking context menu: " + e);
     }
   }
 }
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -99,33 +99,33 @@ function getAreaWidgetIds(areaId) {
 function simulateItemDrag(toDrag, target) {
   let docId = toDrag.ownerDocument.documentElement.id;
   let dragData = [[{type: 'text/toolbarwrapper-id/' + docId,
                     data: toDrag.id}]];
   synthesizeDragStart(toDrag.parentNode, dragData);
   synthesizeDrop(target, target, dragData);
 }
 
-function endCustomizing() {
-  if (document.documentElement.getAttribute("customizing") != "true") {
+function endCustomizing(aWindow=window) {
+  if (aWindow.document.documentElement.getAttribute("customizing") != "true") {
     return true;
   }
   let deferredEndCustomizing = Promise.defer();
   function onCustomizationEnds() {
-    window.gNavToolbox.removeEventListener("aftercustomization", onCustomizationEnds);
+    aWindow.gNavToolbox.removeEventListener("aftercustomization", onCustomizationEnds);
     deferredEndCustomizing.resolve();
   }
-  window.gNavToolbox.addEventListener("aftercustomization", onCustomizationEnds);
-  window.gCustomizeMode.exit();
+  aWindow.gNavToolbox.addEventListener("aftercustomization", onCustomizationEnds);
+  aWindow.gCustomizeMode.exit();
 
   return deferredEndCustomizing.promise.then(function() {
     let deferredLoadNewTab = Promise.defer();
 
     //XXXgijs so some tests depend on this tab being about:blank. Make it so.
-    let newTabBrowser = window.gBrowser.selectedBrowser;
+    let newTabBrowser = aWindow.gBrowser.selectedBrowser;
     newTabBrowser.stop();
 
     // If we stop early enough, this might actually be about:blank.
     if (newTabBrowser.contentDocument.location.href == "about:blank") {
       return;
     }
 
     // Otherwise, make it be about:blank, and wait for that to be done.
@@ -134,27 +134,27 @@ function endCustomizing() {
       deferredLoadNewTab.resolve();
     }
     newTabBrowser.addEventListener("load", onNewTabLoaded, true);
     newTabBrowser.contentDocument.location.replace("about:blank");
     return deferredLoadNewTab.promise;
   });
 }
 
-function startCustomizing() {
-  if (document.documentElement.getAttribute("customizing") == "true") {
+function startCustomizing(aWindow=window) {
+  if (aWindow.document.documentElement.getAttribute("customizing") == "true") {
     return;
   }
   let deferred = Promise.defer();
   function onCustomizing() {
-    window.gNavToolbox.removeEventListener("customizationready", onCustomizing);
+    aWindow.gNavToolbox.removeEventListener("customizationready", onCustomizing);
     deferred.resolve();
   }
-  window.gNavToolbox.addEventListener("customizationready", onCustomizing);
-  window.gCustomizeMode.enter();
+  aWindow.gNavToolbox.addEventListener("customizationready", onCustomizing);
+  aWindow.gCustomizeMode.enter();
   return deferred.promise;
 }
 
 function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) {
   let deferred = Promise.defer();
   let win = OpenBrowserWindow(aOptions);
   if (aWaitForDelayedStartup) {
     Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
@@ -172,29 +172,37 @@ function openAndLoadWindow(aOptions, aWa
     });
   }
   return deferred.promise;
 }
 
 function promisePanelShown(win) {
   let panelEl = win.PanelUI.panel;
   let deferred = Promise.defer();
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Panel did not show within 20 seconds.");
+  }, 20000);
   function onPanelOpen(e) {
     panelEl.removeEventListener("popupshown", onPanelOpen);
+    win.clearTimeout(timeoutId);
     deferred.resolve();
   };
   panelEl.addEventListener("popupshown", onPanelOpen);
   return deferred.promise;
 }
 
 function promisePanelHidden(win) {
   let panelEl = win.PanelUI.panel;
   let deferred = Promise.defer();
+  let timeoutId = win.setTimeout(() => {
+    deferred.reject("Panel did not hide within 20 seconds.");
+  }, 20000);
   function onPanelClose(e) {
     panelEl.removeEventListener("popuphidden", onPanelClose);
+    win.clearTimeout(timeoutId);
     deferred.resolve();
   }
   panelEl.addEventListener("popuphidden", onPanelClose);
   return deferred.promise;
 }
 
 function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) {
   function tryNow() {
@@ -226,17 +234,21 @@ function testRunner(testAry, asyncCleanu
   Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", true);
   for (let test of testAry) {
     info(test.desc);
 
     if (test.setup)
       yield test.setup();
 
     info("Running test");
-    yield test.run();
+    try {
+      yield test.run();
+    } catch (ex) {
+      ok(false, "Unexpected exception occurred while running the test:\n" + ex);
+    }
     info("Cleanup");
     if (test.teardown)
       yield test.teardown();
     ok(!document.getElementById(CustomizableUI.AREA_NAVBAR).hasAttribute("overflowing"), "Shouldn't overflow");
   }
   if (asyncCleanup) {
     yield asyncCleanup();
   }
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1280,17 +1280,17 @@ BrowserGlue.prototype = {
     var notifyBox = win.gBrowser.getNotificationBox();
     var notification = notifyBox.appendNotification(text, title, null,
                                                     notifyBox.PRIORITY_CRITICAL_MEDIUM,
                                                     buttons);
     notification.persistence = -1; // Until user closes it
   },
 
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 17;
+    const UI_VERSION = 18;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul#";
     let currentUIVersion = 0;
     try {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } catch(ex) {}
     if (currentUIVersion >= UI_VERSION)
       return;
 
@@ -1474,33 +1474,17 @@ BrowserGlue.prototype = {
 
     if (currentUIVersion < 14) {
       // DOM Storage doesn't specially handle about: pages anymore.
       let path = OS.Path.join(OS.Constants.Path.profileDir,
                               "chromeappsstore.sqlite");
       OS.File.remove(path);
     }
 
-    if (currentUIVersion < 15) {
-      // Migrate users from text or text&icons mode to icons mode.
-      let updateToolbars = function (aToolbarIds, aResourceName, aResourceValue) {
-        let resource = this._rdf.GetResource(aResourceName);
-        for (toolbarId of aToolbarIds) {
-          let toolbar = this._rdf.GetResource(BROWSER_DOCURL + toolbarId);
-          let oldValue = this._getPersist(toolbar, resource);
-          if (oldValue && oldValue != aResourceValue) {
-            this._setPersist(toolbar, resource, aResourceValue);
-          }
-        }
-      }.bind(this);
-
-      updateToolbars(["navigator-toolbox", "nav-bar", "PersonalToolbar", "addon-bar"], "mode", "icons");
-      // Exclude PersonalToolbar and addon-bar since they have lockiconsize="true".
-      updateToolbars(["navigator-toolbox", "nav-bar"], "iconsize", "large");
-    }
+    // Version 15 was obsoleted in favour of 18.
 
     if (currentUIVersion < 16) {
       let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar");
       let collapsedResource = this._rdf.GetResource("collapsed");
       let isCollapsed = this._getPersist(toolbarResource, collapsedResource);
       if (isCollapsed == "true") {
         this._setPersist(toolbarResource, collapsedResource, "false");
       }
@@ -1528,16 +1512,31 @@ BrowserGlue.prototype = {
             currentset = currentset.replace(/(^|,)window-controls($|,)/,
                                             "$1bookmarks-menu-button,window-controls$2")
           }
           this._setPersist(toolbarResource, currentsetResource, currentset);
         }
       }
     }
 
+    if (currentUIVersion < 18) {
+      // Remove iconsize and mode from all the toolbars
+      let toolbars = ["navigator-toolbox", "nav-bar", "PersonalToolbar",
+                      "addon-bar", "TabsToolbar", "toolbar-menubar"];
+      for (let resourceName of ["mode", "iconsize"]) {
+        let resource = this._rdf.GetResource(resourceName);
+        for (let toolbarId of toolbars) {
+          let toolbar = this._rdf.GetResource(BROWSER_DOCURL + toolbarId);
+          if (this._getPersist(toolbar, resource)) {
+            this._setPersist(toolbar, resource);
+          }
+        }
+      }
+    }
+
     if (this._dirty)
       this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush();
 
     delete this._rdf;
     delete this._dataSource;
 
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
--- a/browser/components/sessionstore/content/content-sessionStore.js
+++ b/browser/components/sessionstore/content/content-sessionStore.js
@@ -9,86 +9,101 @@ function debug(msg) {
 }
 
 let Cu = Components.utils;
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
 
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+  "resource:///modules/sessionstore/Utils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DocShellCapabilities",
   "resource:///modules/sessionstore/DocShellCapabilities.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageStyle",
   "resource:///modules/sessionstore/PageStyle.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
   "resource:///modules/sessionstore/SessionHistory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
   "resource:///modules/sessionstore/SessionStorage.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TextAndScrollData",
   "resource:///modules/sessionstore/TextAndScrollData.jsm");
 
 /**
+ * Returns a lazy function that will evaluate the given
+ * function |fn| only once and cache its return value.
+ */
+function createLazy(fn) {
+  let cached = false;
+  let cachedValue = null;
+
+  return function lazy() {
+    if (!cached) {
+      cachedValue = fn();
+      cached = true;
+    }
+
+    return cachedValue;
+  };
+}
+
+/**
+ * Determines whether the given storage event was triggered by changes
+ * to the sessionStorage object and not the local or globalStorage.
+ */
+function isSessionStorageEvent(event) {
+  try {
+    return event.storageArea == content.sessionStorage;
+  } catch (ex if ex instanceof Ci.nsIException && ex.result == Cr.NS_ERROR_NOT_AVAILABLE) {
+    // This page does not have a DOMSessionStorage
+    // (this is typically the case for about: pages)
+    return false;
+  }
+}
+
+/**
  * Listens for and handles content events that we need for the
  * session store service to be notified of state changes in content.
  */
 let EventListener = {
 
   DOM_EVENTS: [
-    "pageshow", "change", "input", "MozStorageChanged"
+    "pageshow", "change", "input"
   ],
 
   init: function () {
     this.DOM_EVENTS.forEach(e => addEventListener(e, this, true));
   },
 
   handleEvent: function (event) {
     switch (event.type) {
       case "pageshow":
         if (event.persisted)
           sendAsyncMessage("SessionStore:pageshow");
         break;
       case "input":
       case "change":
         sendAsyncMessage("SessionStore:input");
         break;
-      case "MozStorageChanged": {
-        let isSessionStorage = true;
-        // We are only interested in sessionStorage events
-        try {
-          if (event.storageArea != content.sessionStorage) {
-            isSessionStorage = false;
-          }
-        } catch (ex) {
-          // This page does not even have sessionStorage
-          // (this is typically the case of about: pages)
-          isSessionStorage = false;
-        }
-        if (isSessionStorage) {
-          sendAsyncMessage("SessionStore:MozStorageChanged");
-        }
-        break;
-      }
       default:
         debug("received unknown event '" + event.type + "'");
         break;
     }
   }
 };
 
 /**
  * Listens for and handles messages sent by the session store service.
  */
 let MessageListener = {
 
   MESSAGES: [
-    "SessionStore:collectSessionHistory",
-    "SessionStore:collectSessionStorage",
-    "SessionStore:collectDocShellCapabilities",
-    "SessionStore:collectPageStyle"
+    "SessionStore:collectSessionHistory"
   ],
 
   init: function () {
     this.MESSAGES.forEach(m => addMessageListener(m, this));
   },
 
   receiveMessage: function ({name, data: {id}}) {
     switch (name) {
@@ -99,28 +114,16 @@ let MessageListener = {
           // Don't include private data. It's only needed when duplicating
           // tabs, which collects data synchronously.
           TextAndScrollData.updateFrame(history.entries[tabIndex],
                                         content,
                                         docShell.isAppTab);
         }
         sendAsyncMessage(name, {id: id, data: history});
         break;
-      case "SessionStore:collectSessionStorage":
-        let storage = SessionStorage.serialize(docShell);
-        sendAsyncMessage(name, {id: id, data: storage});
-        break;
-      case "SessionStore:collectDocShellCapabilities":
-        let disallow = DocShellCapabilities.collect(docShell);
-        sendAsyncMessage(name, {id: id, data: disallow});
-        break;
-      case "SessionStore:collectPageStyle":
-        let pageStyle = PageStyle.collect(docShell);
-        sendAsyncMessage(name, {id: id, data: pageStyle});
-        break;
       default:
         debug("received unknown message '" + name + "'");
         break;
     }
   }
 };
 
 /**
@@ -147,27 +150,39 @@ let SyncHandler = {
       TextAndScrollData.updateFrame(history.entries[tabIndex],
                                     content,
                                     docShell.isAppTab,
                                     {includePrivateData: includePrivateData});
     }
     return history;
   },
 
-  collectSessionStorage: function () {
-    return SessionStorage.serialize(docShell);
+  /**
+   * This function is used to make the tab process flush all data that
+   * hasn't been sent to the parent process, yet.
+   *
+   * @param id (int)
+   *        A unique id that represents the last message received by the chrome
+   *        process before flushing. We will use this to determine data that
+   *        would be lost when data has been sent asynchronously shortly
+   *        before flushing synchronously.
+   */
+  flush: function (id) {
+    MessageQueue.flush(id);
   },
 
-  collectDocShellCapabilities: function () {
-    return DocShellCapabilities.collect(docShell);
-  },
-
-  collectPageStyle: function () {
-    return PageStyle.collect(docShell);
-  },
+  /**
+   * DO NOT USE - DEBUGGING / TESTING ONLY
+   *
+   * This function is used to simulate certain situations where race conditions
+   * can occur by sending data shortly before flushing synchronously.
+   */
+  flushAsync: function () {
+    MessageQueue.flushAsync();
+  }
 };
 
 let ProgressListener = {
   init: function() {
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIWebProgress);
     webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
   },
@@ -178,12 +193,271 @@ let ProgressListener = {
   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {},
   onProgressChange: function() {},
   onStatusChange: function() {},
   onSecurityChange: function() {},
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                          Ci.nsISupportsWeakReference])
 };
 
+/**
+ * Listens for changes to the page style. Whenever a different page style is
+ * selected or author styles are enabled/disabled we send a message with the
+ * currently applied style to the chrome process.
+ *
+ * Causes a SessionStore:update message to be sent that contains the currently
+ * selected pageStyle, if any. The pageStyle is represented by a string.
+ */
+let PageStyleListener = {
+  init: function () {
+    Services.obs.addObserver(this, "author-style-disabled-changed", true);
+    Services.obs.addObserver(this, "style-sheet-applicable-state-changed", true);
+  },
+
+  observe: function (subject, topic) {
+    if (subject.defaultView && subject.defaultView.top == content) {
+      MessageQueue.push("pageStyle", () => PageStyle.collect(docShell) || null);
+    }
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
+/**
+ * Listens for changes to docShell capabilities. Whenever a new load is started
+ * we need to re-check the list of capabilities and send message when it has
+ * changed.
+ *
+ * Causes a SessionStore:update message to be sent that contains the currently
+ * disabled docShell capabilities (all nsIDocShell.allow* properties set to
+ * false) as a string - i.e. capability names separate by commas.
+ */
+let DocShellCapabilitiesListener = {
+  /**
+   * This field is used to compare the last docShell capabilities to the ones
+   * that have just been collected. If nothing changed we won't send a message.
+   */
+  _latestCapabilities: "",
+
+  init: function () {
+    let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                              .getInterface(Ci.nsIWebProgress);
+
+    webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+  },
+
+  /**
+   * onLocationChange() is called as soon as we start loading a page after
+   * we are certain that there's nothing blocking the load (e.g. a content
+   * policy added by AdBlock or the like).
+   */
+  onLocationChange: function() {
+    // The order of docShell capabilities cannot change while we're running
+    // so calling join() without sorting before is totally sufficient.
+    let caps = DocShellCapabilities.collect(docShell).join(",");
+
+    // Send new data only when the capability list changes.
+    if (caps != this._latestCapabilities) {
+      this._latestCapabilities = caps;
+      MessageQueue.push("disallow", () => caps || null);
+    }
+  },
+
+  onStateChange: function () {},
+  onProgressChange: function () {},
+  onStatusChange: function () {},
+  onSecurityChange: function () {},
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+                                         Ci.nsISupportsWeakReference])
+};
+
+/**
+ * Listens for changes to the DOMSessionStorage. Whenever new keys are added,
+ * existing ones removed or changed, or the storage is cleared we will send a
+ * message to the parent process containing up-to-date sessionStorage data.
+ *
+ * Causes a SessionStore:update message to be sent that contains the current
+ * DOMSessionStorage contents. The data is a nested object using host names
+ * as keys and per-host DOMSessionStorage data as values.
+ */
+let SessionStorageListener = {
+  init: function () {
+    addEventListener("MozStorageChanged", this);
+    Services.obs.addObserver(this, "browser:purge-domain-data", true);
+    Services.obs.addObserver(this, "browser:purge-session-history", true);
+  },
+
+  handleEvent: function (event) {
+    // Ignore events triggered by localStorage or globalStorage changes.
+    if (isSessionStorageEvent(event)) {
+      this.collect();
+    }
+  },
+
+  observe: function () {
+    // Collect data on the next tick so that any other observer
+    // that needs to purge data can do its work first.
+    setTimeout(() => this.collect(), 0);
+  },
+
+  collect: function () {
+    MessageQueue.push("storage", () => SessionStorage.collect(docShell));
+  },
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+                                         Ci.nsISupportsWeakReference])
+};
+
+/**
+ * A message queue that takes collected data and will take care of sending it
+ * to the chrome process. It allows flushing using synchronous messages and
+ * takes care of any race conditions that might occur because of that. Changes
+ * will be batched if they're pushed in quick succession to avoid a message
+ * flood.
+ */
+let MessageQueue = {
+  /**
+   * A unique, monotonically increasing ID used for outgoing messages. This is
+   * important to make it possible to reuse tabs and allow sync flushes before
+   * data could be destroyed.
+   */
+  _id: 1,
+
+  /**
+   * A map (string -> lazy fn) holding lazy closures of all queued data
+   * collection routines. These functions will return data collected from the
+   * docShell.
+   */
+  _data: new Map(),
+
+  /**
+   * A map holding the |this._id| value for every type of data back when it
+   * was pushed onto the queue. We will use those IDs to find the data to send
+   * and flush.
+   */
+  _lastUpdated: new Map(),
+
+  /**
+   * The delay (in ms) used to delay sending changes after data has been
+   * invalidated.
+   */
+  BATCH_DELAY_MS: 1000,
+
+  /**
+   * The current timeout ID, null if there is no queue data. We use timeouts
+   * to damp a flood of data changes and send lots of changes as one batch.
+   */
+  _timeout: null,
+
+  /**
+   * Pushes a given |value| onto the queue. The given |key| represents the type
+   * of data that is stored and can override data that has been queued before
+   * but has not been sent to the parent process, yet.
+   *
+   * @param key (string)
+   *        A unique identifier specific to the type of data this is passed.
+   * @param fn (function)
+   *        A function that returns the value that will be sent to the parent
+   *        process.
+   */
+  push: function (key, fn) {
+    this._data.set(key, createLazy(fn));
+    this._lastUpdated.set(key, this._id);
+
+    if (!this._timeout) {
+      // Wait a little before sending the message to batch multiple changes.
+      this._timeout = setTimeout(() => this.send(), this.BATCH_DELAY_MS);
+    }
+  },
+
+  /**
+   * Sends queued data to the chrome process.
+   *
+   * @param options (object)
+   *        {id: 123} to override the update ID used to accumulate data to send.
+   *        {sync: true} to send data to the parent process synchronously.
+   */
+  send: function (options = {}) {
+    // Looks like we have been called off a timeout after the tab has been
+    // closed. The docShell is gone now and we can just return here as there
+    // is nothing to do.
+    if (!docShell) {
+      return;
+    }
+
+    if (this._timeout) {
+      clearTimeout(this._timeout);
+      this._timeout = null;
+    }
+
+    let sync = options && options.sync;
+    let startID = (options && options.id) || this._id;
+    let sendMessage = sync ? sendSyncMessage : sendAsyncMessage;
+
+    let data = {};
+    for (let [key, id] of this._lastUpdated) {
+      // There is no data for the given key anymore because
+      // the parent process already marked it as received.
+      if (!this._data.has(key)) {
+        continue;
+      }
+
+      if (startID > id) {
+        // If the |id| passed by the parent process is higher than the one
+        // stored in |_lastUpdated| for the given key we know that the parent
+        // received all necessary data and we can remove it from the map.
+        this._data.delete(key);
+        continue;
+      }
+
+      data[key] = this._data.get(key)();
+    }
+
+    // Send all data to the parent process.
+    sendMessage("SessionStore:update", {id: this._id, data: data});
+
+    // Increase our unique message ID.
+    this._id++;
+  },
+
+  /**
+   * This function is used to make the message queue flush all queue data that
+   * hasn't been sent to the parent process, yet.
+   *
+   * @param id (int)
+   *        A unique id that represents the latest message received by the
+   *        chrome process. We can use this to determine which messages have not
+   *        yet been received because they are still stuck in the event queue.
+   */
+  flush: function (id) {
+    // It's important to always send data, even if there is nothing to flush.
+    // The update message will be received by the parent process that can then
+    // update its last received update ID to ignore stale messages.
+    this.send({id: id + 1, sync: true});
+
+    this._data.clear();
+    this._lastUpdated.clear();
+  },
+
+  /**
+   * DO NOT USE - DEBUGGING / TESTING ONLY
+   *
+   * This function is used to simulate certain situations where race conditions
+   * can occur by sending data shortly before flushing synchronously.
+   */
+  flushAsync: function () {
+    if (!Services.prefs.getBoolPref("browser.sessionstore.debug")) {
+      throw new Error("flushAsync() must be used for testing, only.");
+    }
+
+    this.send();
+  }
+};
+
 EventListener.init();
 MessageListener.init();
 SyncHandler.init();
 ProgressListener.init();
+PageStyleListener.init();
+SessionStorageListener.init();
+DocShellCapabilitiesListener.init();
--- a/browser/components/sessionstore/src/SessionStorage.jsm
+++ b/browser/components/sessionstore/src/SessionStorage.jsm
@@ -10,87 +10,90 @@ const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
   "resource:///modules/sessionstore/PrivacyLevel.jsm");
 
-this.SessionStorage = {
+this.SessionStorage = Object.freeze({
   /**
    * Updates all sessionStorage "super cookies"
    * @param aDocShell
    *        That tab's docshell (containing the sessionStorage)
-   * @param aFullData
-   *        always return privacy sensitive data (use with care)
+   * @return Returns a nested object that will have hosts as keys and per-host
+   *         session storage data as values. For example:
+   *         {"example.com": {"key": "value", "my_number": 123}}
    */
-  serialize: function ssto_serialize(aDocShell, aFullData) {
-    return DomStorage.read(aDocShell, aFullData);
+  collect: function (aDocShell) {
+    return SessionStorageInternal.collect(aDocShell);
   },
 
   /**
    * Restores all sessionStorage "super cookies".
    * @param aDocShell
    *        A tab's docshell (containing the sessionStorage)
    * @param aStorageData
-   *        Storage data to be restored
+   *        A nested object with storage data to be restored that has hosts as
+   *        keys and per-host session storage data as values. For example:
+   *        {"example.com": {"key": "value", "my_number": 123}}
    */
-  deserialize: function ssto_deserialize(aDocShell, aStorageData) {
-    DomStorage.write(aDocShell, aStorageData);
+  restore: function (aDocShell, aStorageData) {
+    SessionStorageInternal.restore(aDocShell, aStorageData);
   }
-};
+});
 
-Object.freeze(SessionStorage);
-
-let DomStorage = {
+let SessionStorageInternal = {
   /**
    * Reads all session storage data from the given docShell.
    * @param aDocShell
    *        A tab's docshell (containing the sessionStorage)
-   * @param aFullData
-   *        Always return privacy sensitive data (use with care)
+   * @return Returns a nested object that will have hosts as keys and per-host
+   *         session storage data as values. For example:
+   *         {"example.com": {"key": "value", "my_number": 123}}
    */
-  read: function DomStorage_read(aDocShell, aFullData) {
+  collect: function (aDocShell) {
     let data = {};
-    let isPinned = aDocShell.isAppTab;
     let webNavigation = aDocShell.QueryInterface(Ci.nsIWebNavigation);
     let shistory = webNavigation.sessionHistory;
 
     for (let i = 0; shistory && i < shistory.count; i++) {
       let principal = History.getPrincipalForEntry(shistory, i, aDocShell);
-      if (!principal)
+      if (!principal) {
         continue;
-
-      // Check if we're allowed to store sessionStorage data.
-      let isHttps = principal.URI && principal.URI.schemeIs("https");
-      if (aFullData || PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
-        let origin = principal.jarPrefix + principal.origin;
+      }
 
+      // Get the root domain of the current history entry
+      // and use that as a key for the per-host storage data.
+      let origin = principal.jarPrefix + principal.origin;
+      if (data.hasOwnProperty(origin)) {
         // Don't read a host twice.
-        if (!(origin in data)) {
-          let originData = this._readEntry(principal, aDocShell);
-          if (Object.keys(originData).length) {
-            data[origin] = originData;
-          }
-        }
+        continue;
+      }
+
+      let originData = this._readEntry(principal, aDocShell);
+      if (Object.keys(originData).length) {
+        data[origin] = originData;
       }
     }
 
-    return data;
+    return Object.keys(data).length ? data : null;
   },
 
   /**
    * Writes session storage data to the given tab.
    * @param aDocShell
    *        A tab's docshell (containing the sessionStorage)
    * @param aStorageData
-   *        Storage data to be restored
+   *        A nested object with storage data to be restored that has hosts as
+   *        keys and per-host session storage data as values. For example:
+   *        {"example.com": {"key": "value", "my_number": 123}}
    */
-  write: function DomStorage_write(aDocShell, aStorageData) {
+  restore: function (aDocShell, aStorageData) {
     for (let [host, data] in Iterator(aStorageData)) {
       let uri = Services.io.newURI(host, null, null);
       let principal = Services.scriptSecurityManager.getDocShellCodebasePrincipal(uri, aDocShell);
       let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager);
 
       // There is no need to pass documentURI, it's only used to fill documentURI property of
       // domstorage event, which in this case has no consumer. Prevention of events in case
       // of missing documentURI will be solved in a followup bug to bug 600307.
@@ -109,17 +112,17 @@ let DomStorage = {
 
   /**
    * Reads an entry in the session storage data contained in a tab's history.
    * @param aURI
    *        That history entry uri
    * @param aDocShell
    *        A tab's docshell (containing the sessionStorage)
    */
-  _readEntry: function DomStorage_readEntry(aPrincipal, aDocShell) {
+  _readEntry: function (aPrincipal, aDocShell) {
     let hostData = {};
     let storage;
 
     try {
       let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager);
       storage = storageManager.getStorage(aPrincipal);
     } catch (e) {
       // sessionStorage might throw if it's turned off, see bug 458954
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -54,27 +54,27 @@ const MESSAGES = [
   // standard form fields or of ContentEditables.
   "SessionStore:input",
 
   // The content script has received a pageshow event. This happens when a
   // page is loaded from bfcache without any network activity, i.e. when
   // clicking the back or forward button.
   "SessionStore:pageshow",
 
-  // The content script has received a MozStorageChanged event dealing
-  // with a change in the contents of the sessionStorage.
-  "SessionStore:MozStorageChanged",
-
   // The content script tells us that a new page just started loading in a
   // browser.
   "SessionStore:loadStart",
 
   // The content script gives us a reference to an object that performs
   // synchronous collection of session data.
-  "SessionStore:setupSyncHandler"
+  "SessionStore:setupSyncHandler",
+
+  // The content script sends us data that has been invalidated and needs to
+  // be saved to disk.
+  "SessionStore:update",
 ];
 
 // These are tab events that we listen to.
 const TAB_EVENTS = [
   "TabOpen", "TabClose", "TabSelect", "TabShow", "TabHide", "TabPinned",
   "TabUnpinned"
 ];
 
@@ -599,26 +599,26 @@ let SessionStoreInternal = {
 
     switch (aMessage.name) {
       case "SessionStore:pageshow":
         this.onTabLoad(win, browser);
         break;
       case "SessionStore:input":
         this.onTabInput(win, browser);
         break;
-      case "SessionStore:MozStorageChanged":
-        TabStateCache.delete(browser);
-        this.saveStateDelayed(win);
-        break;
       case "SessionStore:loadStart":
         TabStateCache.delete(browser);
         break;
       case "SessionStore:setupSyncHandler":
         TabState.setSyncHandler(browser, aMessage.objects.handler);
         break;
+      case "SessionStore:update":
+        TabState.update(browser, aMessage.data);
+        this.saveStateDelayed(win);
+        break;
       default:
         debug("received unknown message '" + aMessage.name + "'");
         break;
     }
 
     this._clearRestoringWindows();
   },
 
@@ -642,18 +642,18 @@ let SessionStoreInternal = {
         TabStateCache.delete(browser);
         if (browser.__SS_restore_data)
           this.restoreDocument(win, browser, aEvent);
         this.onTabLoad(win, browser);
         break;
       case "SwapDocShells":
         browser = aEvent.currentTarget;
         let otherBrowser = aEvent.detail;
-        TabState.onSwapDocShells(browser, otherBrowser);
-        TabStateCache.onSwapDocShells(browser, otherBrowser);
+        TabState.onBrowserContentsSwapped(browser, otherBrowser);
+        TabStateCache.onBrowserContentsSwapped(browser, otherBrowser);
         break;
       case "TabOpen":
         this.onTabAdd(win, aEvent.originalTarget);
         break;
       case "TabClose":
         // aEvent.detail determines if the tab was closed by moving to a different window
         if (!aEvent.detail)
           this.onTabClose(win, aEvent.originalTarget);
@@ -981,17 +981,22 @@ let SessionStoreInternal = {
     TAB_EVENTS.forEach(function(aEvent) {
       tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
     }, this);
 
     // remove the progress listener for this window
     tabbrowser.removeTabsProgressListener(gRestoreTabsProgressListener);
 
     let winData = this._windows[aWindow.__SSi];
-    if (this._loadState == STATE_RUNNING) { // window not closed during a regular shut-down
+
+    // Collect window data only when *not* closed during shutdown.
+    if (this._loadState == STATE_RUNNING) {
+      // Flush all data queued in the content script before the window is gone.
+      TabState.flushWindow(aWindow);
+
       // update all window data for a last time
       this._collectWindowData(aWindow);
 
       if (isFullyLoaded) {
         winData.title = aWindow.content.document.title || tabbrowser.selectedTab.label;
         winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
                                                   tabbrowser.selectedTab);
         SessionCookies.update([winData]);
@@ -1032,16 +1037,19 @@ let SessionStoreInternal = {
   },
 
   /**
    * On quit application requested
    */
   onQuitApplicationRequested: function ssi_onQuitApplicationRequested() {
     // get a current snapshot of all windows
     this._forEachBrowserWindow(function(aWindow) {
+      // Flush all data queued in the content script to not lose it when
+      // shutting down.
+      TabState.flushWindow(aWindow);
       this._collectWindowData(aWindow);
     });
     // we must cache this because _getMostRecentBrowserWindow will always
     // return null by the time quit-application occurs
     var activeWindow = this._getMostRecentBrowserWindow();
     if (activeWindow)
       this.activeWindowSSiCache = activeWindow.__SSi || "";
     DirtyWindows.clear();
@@ -1103,17 +1111,16 @@ let SessionStoreInternal = {
     if (this._loadState == STATE_QUITTING)
       return;
     LastSession.clear();
     let openWindows = {};
     this._forEachBrowserWindow(function(aWindow) {
       Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
         TabStateCache.delete(aTab);
         delete aTab.linkedBrowser.__SS_data;
-        delete aTab.linkedBrowser.__SS_tabStillLoading;
         if (aTab.linkedBrowser.__SS_restoreState)
           this._resetTabRestoringState(aTab);
       }, this);
       openWindows[aWindow.__SSi] = true;
     });
     // also clear all data about closed tabs and windows
     for (let ix in this._windows) {
       if (ix in openWindows) {
@@ -1255,17 +1262,16 @@ let SessionStoreInternal = {
     let browser = aTab.linkedBrowser;
     browser.removeEventListener("load", this, true);
     browser.removeEventListener("SwapDocShells", this, true);
 
     let mm = browser.messageManager;
     MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
 
     delete browser.__SS_data;
-    delete browser.__SS_tabStillLoading;
 
     // If this tab was in the middle of restoring or still needs to be restored,
     // we need to reset that state. If the tab was restoring, we will attempt to
     // restore the next tab.
     let previousState = browser.__SS_restoreState;
     if (previousState) {
       this._resetTabRestoringState(aTab);
       if (previousState == TAB_STATE_RESTORING)
@@ -1291,16 +1297,19 @@ let SessionStoreInternal = {
     event.initEvent("SSTabClosing", true, false);
     aTab.dispatchEvent(event);
 
     // don't update our internal state if we don't have to
     if (this._max_tabs_undo == 0) {
       return;
     }
 
+    // Flush all data queued in the content script before the tab is gone.
+    TabState.flush(aTab.linkedBrowser);
+
     // Get the latest data for this tab (generally, from the cache)
     let tabState = TabState.collectSync(aTab);
 
     // store closed-tab data for undo
     if (this._shouldSaveTabState(tabState)) {
       let tabTitle = aTab.label;
       let tabbrowser = aWindow.gBrowser;
       tabTitle = this._replaceLoadingTitle(tabTitle, tabbrowser, aTab);
@@ -1333,17 +1342,16 @@ let SessionStoreInternal = {
     if (aBrowser.__SS_restoreState &&
         aBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       return;
     }
 
     TabStateCache.delete(aBrowser);
 
     delete aBrowser.__SS_data;
-    delete aBrowser.__SS_tabStillLoading;
     this.saveStateDelayed(aWindow);
 
     // attempt to update the current URL we send in a crash report
     this._updateCrashReportURL(aWindow);
   },
 
   /**
    * Called when a browser sends the "input" notification
@@ -1538,16 +1546,20 @@ let SessionStoreInternal = {
     this.restoreTabs(window, [aTab], [tabState], 0);
   },
 
   duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) {
     if (!aTab.ownerDocument || !aTab.ownerDocument.defaultView.__SSi ||
         !aWindow.getBrowser)
       throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
 
+    // Flush all data queued in the content script because we will need that
+    // state to properly duplicate the given tab.
+    TabState.flush(aTab.linkedBrowser);
+
     // Duplicate the tab state
     let tabState = TabState.clone(aTab);
 
     tabState.index += aDelta;
     tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
     tabState.pinned = false;
 
     this._setWindowStateBusy(aWindow);
@@ -2367,21 +2379,19 @@ let SessionStoreInternal = {
       winData.tabs[0].hidden = false;
       tabbrowser.showTab(tabs[0]);
     }
 
     // If overwriting tabs, we want to reset each tab's "restoring" state. Since
     // we're overwriting those tabs, they should no longer be restoring. The
     // tabs will be rebuilt and marked if they need to be restored after loading
     // state (in restoreTabs).
-    // We also want to invalidate any cached information on the tab state.
     if (overwriteTabs) {
       for (let i = 0; i < tabbrowser.tabs.length; i++) {
         let tab = tabbrowser.tabs[i];
-        TabStateCache.delete(tab);
         if (tabbrowser.browsers[i].__SS_restoreState)
           this._resetTabRestoringState(tab);
       }
     }
 
     // We want to set up a counter on the window that indicates how many tabs
     // in this window are unrestored. This will be used in restoreNextTab to
     // determine if gRestoreTabsProgressListener should be removed from the window.
@@ -2604,25 +2614,35 @@ let SessionStoreInternal = {
       if (tabData.extData) {
         tab.__SS_extdata = {};
         for (let key in tabData.extData)
          tab.__SS_extdata[key] = tabData.extData[key];
       } else {
         delete tab.__SS_extdata;
       }
 
-      browser.__SS_tabStillLoading = true;
+      // Flush all data from the content script synchronously. This is done so
+      // that all async messages that are still on their way to chrome will
+      // be ignored and don't override any tab data set by restoreHistory().
+      TabState.flush(tab.linkedBrowser);
 
       // keep the data around to prevent dataloss in case
       // a tab gets closed before it's been properly restored
       browser.__SS_data = tabData;
       browser.__SS_restoreState = TAB_STATE_NEEDS_RESTORE;
       browser.setAttribute("pending", "true");
       tab.setAttribute("pending", "true");
 
+      // Update the persistent tab state cache with |tabData| information.
+      TabStateCache.updatePersistent(browser, {
+        storage: tabData.storage || null,
+        disallow: tabData.disallow || null,
+        pageStyle: tabData.pageStyle || null
+      });
+
       if (tabData.entries.length == 0) {
         // make sure to blank out this tab's content
         // (just purging the tab's history won't be enough)
         browser.loadURIWithFlags("about:blank",
                                  Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY,
                                  null, null, null);
         continue;
       }
@@ -2714,17 +2734,17 @@ let SessionStoreInternal = {
 
     SessionHistory.restore(browser.docShell, tabData);
 
     // make sure to reset the capabilities and attributes, in case this tab gets reused
     let disallow = new Set(tabData.disallow && tabData.disallow.split(","));
     DocShellCapabilities.restore(browser.docShell, disallow);
 
     if (tabData.storage && browser.docShell instanceof Ci.nsIDocShell)
-      SessionStorage.deserialize(browser.docShell, tabData.storage);
+      SessionStorage.restore(browser.docShell, tabData.storage);
 
     // notify the tabbrowser that the tab chrome has been restored
     var event = window.document.createEvent("Events");
     event.initEvent("SSTabRestoring", true, false);
     tab.dispatchEvent(event);
 
     // This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
     // it ensures each window will have its selected tab loaded.
@@ -3403,17 +3423,17 @@ let SessionStoreInternal = {
    * restoreTabs && restoreHistory) or if the tab is still marked
    * as loading.
    *
    * @param aTab
    * @returns boolean
    */
   _canRestoreTabHistory: function ssi_canRestoreTabHistory(aTab) {
     return aTab.parentNode && aTab.linkedBrowser &&
-           aTab.linkedBrowser.__SS_tabStillLoading;
+           aTab.linkedBrowser.__SS_data;
   },
 
   /**
    * This is going to take a state as provided at startup (via
    * nsISessionStartup.state) and split it into 2 parts. The first part
    * (defaultState) will be a state that should still be restored at startup,
    * while the second part (state) is a state that should be saved for later.
    * defaultState will be comprised of windows with only pinned tabs, extracted
--- a/browser/components/sessionstore/src/TabState.jsm
+++ b/browser/components/sessionstore/src/TabState.jsm
@@ -9,31 +9,47 @@ this.EXPORTED_SYMBOLS = ["TabState"];
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "Messenger",
   "resource:///modules/sessionstore/Messenger.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
+  "resource:///modules/sessionstore/PrivacyLevel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
   "resource:///modules/sessionstore/TabStateCache.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TabAttributes",
   "resource:///modules/sessionstore/TabAttributes.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+  "resource:///modules/sessionstore/Utils.jsm");
 
 /**
  * Module that contains tab state collection methods.
  */
 this.TabState = Object.freeze({
   setSyncHandler: function (browser, handler) {
     TabStateInternal.setSyncHandler(browser, handler);
   },
 
-  onSwapDocShells: function (browser, otherBrowser) {
-    TabStateInternal.onSwapDocShells(browser, otherBrowser);
+  onBrowserContentsSwapped: function (browser, otherBrowser) {
+    TabStateInternal.onBrowserContentsSwapped(browser, otherBrowser);
+  },
+
+  update: function (browser, data) {
+    TabStateInternal.update(browser, data);
+  },
+
+  flush: function (browser) {
+    TabStateInternal.flush(browser);
+  },
+
+  flushWindow: function (window) {
+    TabStateInternal.flushWindow(window);
   },
 
   collect: function (tab) {
     return TabStateInternal.collect(tab);
   },
 
   collectSync: function (tab) {
     return TabStateInternal.collectSync(tab);
@@ -53,56 +69,76 @@ let TabStateInternal = {
   // their promises when collecting tab data asynchronously.
   _pendingCollections: new WeakMap(),
 
   // A map (xul:browser -> handler) that maps a tab to the
   // synchronous collection handler object for that tab.
   // See SyncHandler in content-sessionStore.js.
   _syncHandlers: new WeakMap(),
 
+  // A map (xul:browser -> int) that maps a browser to the
+  // last "SessionStore:update" message ID we received for it.
+  _latestMessageID: new WeakMap(),
+
   /**
    * Install the sync handler object from a given tab.
    */
   setSyncHandler: function (browser, handler) {
     this._syncHandlers.set(browser, handler);
+    this._latestMessageID.set(browser, 0);
+  },
+
+  /**
+   * Processes a data update sent by the content script.
+   */
+  update: function (browser, {id, data}) {
+    // Only ever process messages that have an ID higher than the last one we
+    // saw. This ensures we don't use stale data that has already been received
+    // synchronously.
+    if (id > this._latestMessageID.get(browser)) {
+      this._latestMessageID.set(browser, id);
+      TabStateCache.updatePersistent(browser, data);
+    }
+  },
+
+  /**
+   * Flushes all data currently queued in the given browser's content script.
+   */
+  flush: function (browser) {
+    if (this._syncHandlers.has(browser)) {
+      let lastID = this._latestMessageID.get(browser);
+      this._syncHandlers.get(browser).flush(lastID);
+    }
+  },
+
+  /**
+   * Flushes queued content script data for all browsers of a given window.
+   */
+  flushWindow: function (window) {
+    for (let browser of window.gBrowser.browsers) {
+      this.flush(browser);
+    }
   },
 
   /**
    * When a docshell swap happens, a xul:browser element will be
    * associated with a different content-sessionStore.js script
    * global. In this case, the sync handler for the element needs to
    * be swapped just like the docshell.
    */
-  onSwapDocShells: function (browser, otherBrowser) {
+  onBrowserContentsSwapped: function (browser, otherBrowser) {
     // Data collected while docShells have been swapped should not go into
     // the TabStateCache. Collections will most probably time out but we want
     // to make sure.
     this.dropPendingCollections(browser);
     this.dropPendingCollections(otherBrowser);
 
-    // Make sure that one or the other of these has a sync handler,
-    // and let it be |browser|.
-    if (!this._syncHandlers.has(browser)) {
-      [browser, otherBrowser] = [otherBrowser, browser];
-      if (!this._syncHandlers.has(browser)) {
-        return;
-      }
-    }
-
-    // At this point, browser is guaranteed to have a sync handler,
-    // although otherBrowser may not. Perform the swap.
-    let handler = this._syncHandlers.get(browser);
-    if (this._syncHandlers.has(otherBrowser)) {
-      let otherHandler = this._syncHandlers.get(otherBrowser);
-      this._syncHandlers.set(browser, otherHandler);
-      this._syncHandlers.set(otherBrowser, handler);
-    } else {
-      this._syncHandlers.set(otherBrowser, handler);
-      this._syncHandlers.delete(browser);
-    }
+    // Swap data stored per-browser.
+    [this._syncHandlers, this._latestMessageID]
+      .forEach(map => Utils.swapMapEntries(map, browser, otherBrowser));
   },
 
   /**
    * Collect data related to a single tab, asynchronously.
    *
    * @param tab
    *        tabbrowser tab
    *
@@ -127,44 +163,27 @@ let TabStateInternal = {
 
     let browser = tab.linkedBrowser;
 
     let promise = Task.spawn(function task() {
       // Collect session history data asynchronously. Also collects
       // text and scroll data.
       let history = yield Messenger.send(tab, "SessionStore:collectSessionHistory");
 
-      // Collected session storage data asynchronously.
-      let storage = yield Messenger.send(tab, "SessionStore:collectSessionStorage");
-
-      // Collect docShell capabilities asynchronously.
-      let disallow = yield Messenger.send(tab, "SessionStore:collectDocShellCapabilities");
-
-      let pageStyle = yield Messenger.send(tab, "SessionStore:collectPageStyle");
-
       // Collect basic tab data, without session history and storage.
       let tabData = this._collectBaseTabData(tab);
 
       // Apply collected data.
       tabData.entries = history.entries;
       if ("index" in history) {
         tabData.index = history.index;
       }
 
-      if (Object.keys(storage).length) {
-        tabData.storage = storage;
-      }
-
-      if (disallow.length > 0) {
-        tabData.disallow = disallow.join(",");
-      }
-
-      if (pageStyle) {
-        tabData.pageStyle = pageStyle;
-      }
+      // Copy data from the persistent cache.
+      this._copyFromPersistentCache(tab, tabData);
 
       // If we're still the latest async collection for the given tab and
       // the cache hasn't been filled by collect() in the meantime, let's
       // fill the cache with the data we received.
       if (this._pendingCollections.get(browser) == promise) {
         TabStateCache.set(tab, tabData);
         this._pendingCollections.delete(browser);
       }
@@ -262,64 +281,90 @@ let TabStateInternal = {
     if (!this._syncHandlers.has(tab.linkedBrowser)) {
       return tabData;
     }
 
     let syncHandler = this._syncHandlers.get(tab.linkedBrowser);
 
     let includePrivateData = options && options.includePrivateData;
 
-    let history, storage, disallow, pageStyle;
+    let history;
     try {
       history = syncHandler.collectSessionHistory(includePrivateData);
-      storage = syncHandler.collectSessionStorage();
-      disallow = syncHandler.collectDocShellCapabilities();
-      pageStyle = syncHandler.collectPageStyle();
     } catch (e) {
       // This may happen if the tab has crashed.
       Cu.reportError(e);
       return tabData;
     }
 
     tabData.entries = history.entries;
     if ("index" in history) {
       tabData.index = history.index;
     }
 
-    if (Object.keys(storage).length) {
-      tabData.storage = storage;
+    // Copy data from the persistent cache.
+    this._copyFromPersistentCache(tab, tabData, options);
+
+    return tabData;
+  },
+
+  /**
+   * Copy tab data for the given |tab| from the persistent cache to |tabData|.
+   *
+   * @param tab (xul:tab)
+   *        The tab belonging to the given |tabData| object.
+   * @param tabData (object)
+   *        The tab data belonging to the given |tab|.
+   * @param options (object)
+   *        {includePrivateData: true} to always include private data
+   */
+  _copyFromPersistentCache: function (tab, tabData, options = {}) {
+    let data = TabStateCache.getPersistent(tab.linkedBrowser);
+
+    // Nothing to do without any cached data.
+    if (!data) {
+      return;
     }
 
-    if (disallow.length > 0) {
-      tabData.disallow = disallow.join(",");
-    }
+    let includePrivateData = options && options.includePrivateData;
+
+    for (let key of Object.keys(data)) {
+      if (key != "storage" || includePrivateData) {
+        tabData[key] = data[key];
+      } else {
+        tabData.storage = {};
+        let isPinned = tab.pinned;
 
-    if (pageStyle) {
-      tabData.pageStyle = pageStyle;
+        // If we're not allowed to include private data, let's filter out hosts
+        // based on the given tab's pinned state and the privacy level.
+        for (let host of Object.keys(data.storage)) {
+          let isHttps = host.startsWith("https:");
+          if (PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
+            tabData.storage[host] = data.storage[host];
+          }
+        }
+      }
     }
-
-    return tabData;
   },
 
   /*
    * Returns true if the xul:tab element is newly added (i.e., if it's
    * showing about:blank with no history).
    */
   _tabIsNew: function (tab) {
     let browser = tab.linkedBrowser;
     return (!browser || !browser.currentURI);
   },
 
   /*
    * Returns true if the xul:tab element is in the process of being
    * restored.
    */
   _tabIsRestoring: function (tab) {
-    let browser = tab.linkedBrowser;
-    return (browser.__SS_data && browser.__SS_tabStillLoading);
+    return !!tab.linkedBrowser.__SS_data;
   },
 
   /**
    * This function returns true if we need to collect history, page
    * style, and text and scroll data from the tab. Normally we do. The
    * cases when we don't are:
    * 1. the tab is about:blank with no history, or
    * 2. the tab is waiting to be restored.
@@ -373,17 +418,17 @@ let TabStateInternal = {
   _collectBaseTabData: function (tab) {
     let tabData = {entries: [], lastAccessed: tab.lastAccessed };
     let browser = tab.linkedBrowser;
 
     if (!browser || !browser.currentURI) {
       // can happen when calling this function right after .addTab()
       return tabData;
     }
-    if (browser.__SS_data && browser.__SS_tabStillLoading) {
+    if (browser.__SS_data) {
       // Use the data to be restored when the tab hasn't been
       // completely loaded. We clone the data, since we're updating it
       // here and the caller may update it further.
       tabData = JSON.parse(JSON.stringify(browser.__SS_data));
       if (tab.pinned)
         tabData.pinned = true;
       else
         delete tabData.pinned;
--- a/browser/components/sessionstore/src/TabStateCache.jsm
+++ b/browser/components/sessionstore/src/TabStateCache.jsm
@@ -3,17 +3,20 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["TabStateCache"];
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+  "resource:///modules/sessionstore/Utils.jsm");
 
 /**
  * A cache for tabs data.
  *
  * This cache implements a weak map from tabs (as XUL elements)
  * to tab data (as objects).
  *
  * Note that we should never cache private data, as:
@@ -99,18 +102,44 @@ this.TabStateCache = Object.freeze({
   /**
    * Swap cached data for two given browsers.
    *
    * @param {xul:browser} browser
    *        The first of the two browsers that swapped docShells.
    * @param {xul:browser} otherBrowser
    *        The second of the two browsers that swapped docShells.
    */
-  onSwapDocShells: function(browser, otherBrowser) {
-    TabStateCacheInternal.onSwapDocShells(browser, otherBrowser);
+  onBrowserContentsSwapped: function(browser, otherBrowser) {
+    TabStateCacheInternal.onBrowserContentsSwapped(browser, otherBrowser);
+  },
+
+  /**
+   * Retrieves persistently cached data for a given |browser|.
+   *
+   * @param browser (xul:browser)
+   *        The browser to retrieve cached data for.
+   * @return (object)
+   *         The persistently cached data stored for the given |browser|.
+   */
+  getPersistent: function (browser) {
+    return TabStateCacheInternal.getPersistent(browser);
+  },
+
+  /**
+   * Updates persistently cached data for a given |browser|. This data is
+   * persistently in the sense that we never clear it, it will always be
+   * overwritten.
+   *
+   * @param browser (xul:browser)
+   *        The browser belonging to the given tab data.
+   * @param newData (object)
+   *        The new data to be stored for the given |browser|.
+   */
+  updatePersistent: function (browser, newData) {
+    TabStateCacheInternal.updatePersistent(browser, newData);
   },
 
   /**
    * Total number of cache hits during the session.
    */
   get hits() {
     return TabStateCacheTelemetry.hits;
   },
@@ -127,16 +156,17 @@ this.TabStateCache = Object.freeze({
    */
   get clears() {
     return TabStateCacheTelemetry.clears;
   },
 });
 
 let TabStateCacheInternal = {
   _data: new WeakMap(),
+  _persistentData: new WeakMap(),
 
   /**
    * Tells whether an entry is in the cache.
    *
    * @param {XULElement} aKey The tab or the associated browser.
    * @return {bool} Whether there's a cached entry for the given tab.
    */
   has: function (aTab) {
@@ -228,37 +258,61 @@ let TabStateCacheInternal = {
   /**
    * Swap cached data for two given browsers.
    *
    * @param {xul:browser} browser
    *        The first of the two browsers that swapped docShells.
    * @param {xul:browser} otherBrowser
    *        The second of the two browsers that swapped docShells.
    */
-  onSwapDocShells: function(browser, otherBrowser) {
-    // Make sure that one or the other of these has cached data,
-    // and let it be |browser|.
-    if (!this._data.has(browser)) {
-      [browser, otherBrowser] = [otherBrowser, browser];
-      if (!this._data.has(browser)) {
-        return;
+  onBrowserContentsSwapped: function(browser, otherBrowser) {
+    // Swap data stored per-browser.
+    [this._data, this._persistentData]
+      .forEach(map => Utils.swapMapEntries(map, browser, otherBrowser));
+  },
+
+  /**
+   * Retrieves persistently cached data for a given |browser|.
+   *
+   * @param browser (xul:browser)
+   *        The browser to retrieve cached data for.
+   * @return (object)
+   *         The persistently cached data stored for the given |browser|.
+   */
+  getPersistent: function (browser) {
+    return this._persistentData.get(browser);
+  },
+
+  /**
+   * Updates persistently cached data for a given |browser|. This data is
+   * persistent in the sense that we never clear it, it will always be
+   * overwritten.
+   *
+   * @param browser (xul:browser)
+   *        The browser belonging to the given tab data.
+   * @param newData (object)
+   *        The new data to be stored for the given |browser|.
+   */
+  updatePersistent: function (browser, newData) {
+    let data = this._persistentData.get(browser) || {};
+
+    for (let key of Object.keys(newData)) {
+      let value = newData[key];
+      if (value === null) {
+        // Remove the field if the value is null.
+        this.removeField(browser, key);
+        delete data[key];
+      } else {
+        // Update the field otherwise.
+        this.updateField(browser, key, value);
+        data[key] = value;
       }
     }
 
-    // At this point, |browser| is guaranteed to have cached data,
-    // although |otherBrowser| may not. Perform the swap.
-    let data = this._data.get(browser);
-    if (this._data.has(otherBrowser)) {
-      let otherData = this._data.get(otherBrowser);
-      this._data.set(browser, otherData);
-      this._data.set(otherBrowser, data);
-    } else {
-      this._data.set(otherBrowser, data);
-      this._data.delete(browser);
-    }
+    this._persistentData.set(browser, data);
   },
 
   _normalizeToBrowser: function(aKey) {
     let nodeName = aKey.localName;
     if (nodeName == "tab") {
       return aKey.linkedBrowser;
     }
     if (nodeName == "browser") {
--- a/browser/components/sessionstore/src/Utils.jsm
+++ b/browser/components/sessionstore/src/Utils.jsm
@@ -10,16 +10,22 @@ const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 
 this.Utils = Object.freeze({
   makeURI: function (url) {
     return Services.io.newURI(url, null, null);
   },
 
+  /**
+   * Returns true if the |url| passed in is part of the given root |domain|.
+   * For example, if |url| is "www.mozilla.org", and we pass in |domain| as
+   * "mozilla.org", this will return true. It would return false the other way
+   * around.
+   */
   hasRootDomain: function (url, domain) {
     let host;
 
     try {
       host = this.makeURI(url).host;
     } catch (e) {
       // The given URL probably doesn't have a host.
       return false;
@@ -30,10 +36,33 @@ this.Utils = Object.freeze({
       return false;
 
     if (host == domain)
       return true;
 
     let prevChar = host[index - 1];
     return (index == (host.length - domain.length)) &&
            (prevChar == "." || prevChar == "/");
+  },
+
+  swapMapEntries: function (map, key, otherKey) {
+    // Make sure that one or the other of these has an entry in the map,
+    // and let it be |key|.
+    if (!map.has(key)) {
+      [key, otherKey] = [otherKey, key];
+      if (!map.has(key)) {
+        return;
+      }
+    }
+
+    // At this point, |key| is guaranteed to have an entry,
+    // although |otherKey| may not. Perform the swap.
+    let value = map.get(key);
+    if (map.has(otherKey)) {
+      let otherValue = map.get(otherKey);
+      map.set(key, otherValue);
+      map.set(otherKey, value);
+    } else {
+      map.set(otherKey, value);
+      map.delete(key);
+    }
   }
 });
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -5,23 +5,25 @@
 # browser_506482.js is disabled because of frequent failures (bug 538672)
 # browser_526613.js is disabled because of frequent failures (bug 534489)
 # browser_589246.js is disabled for leaking browser windows (bug 752467)
 # browser_580512.js is disabled for leaking browser windows (bug 752467)
 
 [DEFAULT]
 support-files =
   head.js
+  content.js
   browser_form_restore_events_sample.html
   browser_formdata_format_sample.html
   browser_input_sample.html
+  browser_pageStyle_sample.html
+  browser_pageStyle_sample_nested.html
   browser_248970_b_sample.html
   browser_339445_sample.html
   browser_346337_sample.html
-  browser_408470_sample.html
   browser_423132_sample.html
   browser_447951_sample.html
   browser_454908_sample.html
   browser_456342_sample.xhtml
   browser_463205_helper.html
   browser_463205_sample.html
   browser_463206_sample.html
   browser_466937_sample.html
@@ -42,23 +44,25 @@ support-files =
 #  browser_464620_xd.html
 
 
 #disabled-for-intermittent-failures--bug-766044, browser_459906_empty.html
 #disabled-for-intermittent-failures--bug-766044, browser_459906_sample.html
 #disabled-for-intermittent-failures--bug-765389, browser_461743_sample.html
 
 [browser_attributes.js]
+[browser_broadcast.js]
 [browser_capabilities.js]
 [browser_dying_cache.js]
 [browser_form_restore_events.js]
 [browser_formdata_format.js]
 [browser_global_store.js]
 [browser_input.js]
 [browser_pageshow.js]
+[browser_pageStyle.js]
 [browser_sessionStorage.js]
 [browser_swapDocShells.js]
 [browser_tabStateCache.js]
 [browser_upgrade_backup.js]
 [browser_windowRestore_perwindowpb.js]
 [browser_248970_b_perwindowpb.js]
 # Disabled because of leaks.
 # Re-enabling and rewriting this test is tracked in bug 936919.
@@ -69,17 +73,16 @@ skip-if = true
 [browser_350525.js]
 [browser_354894_perwindowpb.js]
 [browser_367052.js]
 [browser_393716.js]
 [browser_394759_basic.js]
 [browser_394759_behavior.js]
 [browser_394759_perwindowpb.js]
 [browser_394759_purge.js]
-[browser_408470.js]
 [browser_423132.js]
 [browser_447951.js]
 [browser_448741.js]
 [browser_454908.js]
 [browser_456342.js]
 [browser_461634.js]
 [browser_463205.js]
 [browser_463206.js]
--- a/browser/components/sessionstore/test/browser_394759_basic.js
+++ b/browser/components/sessionstore/test/browser_394759_basic.js
@@ -29,17 +29,17 @@ function test() {
     ss.setWindowValue(newWin, uniqueKey, uniqueValue);
     let [txt, chk] = newWin.content.document.querySelectorAll("#txt, #chk");
     txt.value = uniqueText;
 
     // Toggle the checkbox to cause a SessionStore:input message to be sent.
     EventUtils.sendMouseEvent({type: "click"}, chk);
 
     let browser = newWin.gBrowser.selectedBrowser;
-    waitForContentMessage(browser, "SessionStore:input", 1000, result => {
+    promiseContentMessage(browser, "SessionStore:input").then(result => {
       ok(result, "received message for input changes");
 
       newWin.close();
 
       // Now give it time to close
       executeSoon(function() {
         is(ss.getClosedWindowCount(), 1,
            "The closed window was added to Recently Closed Windows");
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_408470.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/* 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/. */
-
-function test() {
-  /** Test for Bug 408470 **/
-
-  waitForExplicitFinish();
-
-  let pendingCount = 1;
-  let rootDir = getRootDirectory(gTestPath);
-  let testUrl = rootDir + "browser_408470_sample.html";
-  let tab = gBrowser.addTab(testUrl);
-
-  tab.linkedBrowser.addEventListener("load", function(aEvent) {
-    tab.linkedBrowser.removeEventListener("load", arguments.callee, true);
-    // enable all stylesheets and verify that they're correctly persisted
-    Array.forEach(tab.linkedBrowser.contentDocument.styleSheets, function(aSS, aIx) {
-      pendingCount++;
-      let ssTitle = aSS.title;
-      gPageStyleMenu.switchStyleSheet(ssTitle, tab.linkedBrowser.contentWindow);
-
-      let newTab = gBrowser.duplicateTab(tab);
-      newTab.linkedBrowser.addEventListener("load", function(aEvent) {
-        newTab.linkedBrowser.removeEventListener("load", arguments.callee, true);
-        let states = Array.map(newTab.linkedBrowser.contentDocument.styleSheets,
-                               function(aSS) !aSS.disabled);
-        let correct = states.indexOf(true) == aIx && states.indexOf(true, aIx + 1) == -1;
-
-        if (/^fail_/.test(ssTitle))
-          ok(!correct, "didn't restore stylesheet " + ssTitle);
-        else
-          ok(correct, "restored stylesheet " + ssTitle);
-
-        gBrowser.removeTab(newTab);
-        if (--pendingCount == 0)
-          finish();
-      }, true);
-    });
-
-    // disable all styles and verify that this is correctly persisted
-    tab.linkedBrowser.markupDocumentViewer.authorStyleDisabled = true;
-    let newTab = gBrowser.duplicateTab(tab);
-    newTab.linkedBrowser.addEventListener("load", function(aEvent) {
-      newTab.linkedBrowser.removeEventListener("load", arguments.callee, true);
-      is(newTab.linkedBrowser.markupDocumentViewer.authorStyleDisabled, true,
-         "disabled all stylesheets");
-
-      gBrowser.removeTab(newTab);
-      if (--pendingCount == 0)
-        finish();
-    }, true);
-
-    gBrowser.removeTab(tab);
-  }, true);
-}
deleted file mode 100644
--- a/browser/components/sessionstore/test/browser_408470_sample.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<html>
-<head>
-<title>Test for bug 408470</title>
-
-<link href="404.css" title="default" rel="stylesheet">
-<link href="404.css" title="alternate" rel="alternate stylesheet">
-<link href="404.css" title="altERnate" rel=" styLEsheet altERnate ">
-<link href="404.css" title="media_empty" rel="alternate stylesheet" media="">
-<link href="404.css" title="media_all" rel="alternate stylesheet" media="all">
-<link href="404.css" title="media_ALL" rel="alternate stylesheet" media=" ALL ">
-<link href="404.css" title="media_screen" rel="alternate stylesheet" media="screen">
-<link href="404.css" title="media_print_screen" rel="alternate stylesheet" media="print,screen">
-<link href="404.css" title="fail_media_print" rel="alternate stylesheet" media="print">
-<link href="404.css" title="fail_media_projection" rel="stylesheet" media="projection">
-<link href="404.css" title="fail_media_invalid" rel="alternate stylesheet" media="hallo">
-
-</head>
-<body></body>
-</html>
--- a/browser/components/sessionstore/test/browser_579868.js
+++ b/browser/components/sessionstore/test/browser_579868.js
@@ -13,19 +13,16 @@ function test() {
 
     // Tell the session storer that the tab is pinned
     let newTabState = '{"entries":[{"url":"about:rights"}],"pinned":true,"userTypedValue":"Hello World!"}';
     ss.setTabState(tab1, newTabState);
 
     // Undo pinning
     gBrowser.unpinTab(tab1);
 
-    is(tab1.linkedBrowser.__SS_tabStillLoading, true,
-       "_tabStillLoading should be true.");
-
     // Close and restore tab
     gBrowser.removeTab(tab1);
     let savedState = JSON.parse(ss.getClosedTabData(window))[0].state;
     isnot(savedState.pinned, true, "Pinned should not be true");
     tab1 = ss.undoCloseTab(window, 0);
 
     isnot(tab1.pinned, true, "Should not be pinned");
     gBrowser.removeTab(tab1);
--- a/browser/components/sessionstore/test/browser_625257.js
+++ b/browser/components/sessionstore/test/browser_625257.js
@@ -16,22 +16,17 @@ let {Task, Promise} = Scope;
 //  1. Tab has been created is loading URI_TO_LOAD.
 //  2. Before URI_TO_LOAD finishes loading, browser.currentURI has changed and
 //     tab is scheduled to be removed.
 //  3. After the tab has been closed, undoCloseTab() has been called and the tab
 //     should fully load.
 const URI_TO_LOAD = "about:mozilla";
 
 function waitForLoadStarted(aTab) {
-  let deferred = Promise.defer();
-  waitForContentMessage(aTab.linkedBrowser,
-    "SessionStore:loadStart",
-    1000,
-    deferred.resolve);
-  return deferred.promise;
+  return promiseContentMessage(aTab.linkedBrowser, "SessionStore:loadStart");
 }
 
 function waitForTabLoaded(aTab) {
   let deferred = Promise.defer();
   whenBrowserLoaded(aTab.linkedBrowser, deferred.resolve);
   return deferred.promise;
 }
 
--- a/browser/components/sessionstore/test/browser_916390_form_data_loss.js
+++ b/browser/components/sessionstore/test/browser_916390_form_data_loss.js
@@ -1,11 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
+let tmp;
+Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", tmp);
+let {TabStateCache} = tmp;
+
 const URL = "http://mochi.test:8888/browser/" +
             "browser/components/sessionstore/test/browser_916390_sample.html";
 
 function test() {
   TestRunner.run();
 }
 
 function runTests() {
@@ -19,21 +23,19 @@ function runTests() {
   EventUtils.synthesizeKey("m", {});
   yield waitForInput();
 
   // Check that we'll save the form data state correctly.
   let state = JSON.parse(ss.getBrowserState());
   let {formdata} = state.windows[0].tabs[1].entries[0];
   is(formdata.id.txt, "m", "txt's value is correct");
 
-  // Change the number of session history entries and modify
-  // DOMSessionStorage data to invalidate the TabStateCache.
+  // Change the number of session history entries to invalidate the cache.
   browser.loadURI(URL + "#");
-  browser.contentWindow.sessionStorage.foo = "bar";
-  yield waitForStorageChange();
+  TabStateCache.delete(browser);
 
   // Check that we'll save the form data state correctly.
   let state = JSON.parse(ss.getBrowserState());
   let {formdata} = state.windows[0].tabs[1].entries[1];
   is(formdata.id.txt, "m", "txt's value is correct");
 
   // Clean up.
   gBrowser.removeTab(tab);
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_broadcast.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const INITIAL_VALUE = "initial-value-" + Date.now();
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * closing a tab.
+ */
+add_task(function flush_on_tabclose() {
+  let tab = yield createTabWithStorageData(["http://example.com"]);
+  let browser = tab.linkedBrowser;
+
+  yield modifySessionStorage(browser, {test: "on-tab-close"});
+  gBrowser.removeTab(tab);
+
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  is(storage["http://example.com"].test, "on-tab-close",
+    "sessionStorage data has been flushed on TabClose");
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * the application tries to quit.
+ */
+add_task(function flush_on_quit_requested() {
+  let tab = yield createTabWithStorageData(["http://example.com"]);
+  let browser = tab.linkedBrowser;
+
+  yield modifySessionStorage(browser, {test: "on-quit-requested"});
+
+  // Note that sending quit-application-requested should not interfere with
+  // other tests and code. We're just notifying about a shutdown request but
+  // we will not send quit-application-granted. Observers will thus assume
+  // that some other observer has canceled the request.
+  sendQuitApplicationRequested();
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  is(storage["http://example.com"].test, "on-quit-requested",
+    "sessionStorage data has been flushed when a quit is requested");
+
+  gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * duplicating a tab.
+ */
+add_task(function flush_on_duplicate() {
+  let tab = yield createTabWithStorageData(["http://example.com"]);
+  let browser = tab.linkedBrowser;
+
+  yield modifySessionStorage(browser, {test: "on-duplicate"});
+  let tab2 = ss.duplicateTab(window, tab);
+  let {storage} = JSON.parse(ss.getTabState(tab2));
+  is(storage["http://example.com"].test, "on-duplicate",
+    "sessionStorage data has been flushed when duplicating tabs");
+
+  yield promiseTabRestored(tab2);
+  let {storage} = JSON.parse(ss.getTabState(tab2));
+  is(storage["http://example.com"].test, "on-duplicate",
+    "sessionStorage data has been flushed when duplicating tabs");
+
+  gBrowser.removeTab(tab);
+  gBrowser.removeTab(tab2);
+});
+
+/**
+ * This test ensures we won't lose tab data queued in the content script when
+ * a window is closed.
+ */
+add_task(function flush_on_windowclose() {
+  let win = yield promiseNewWindow();
+  let tab = yield createTabWithStorageData(["http://example.com"], win);
+  let browser = tab.linkedBrowser;
+
+  yield modifySessionStorage(browser, {test: "on-window-close"});
+  yield closeWindow(win);
+
+  let [{tabs: [_, {storage}]}] = JSON.parse(ss.getClosedWindowData());
+  is(storage["http://example.com"].test, "on-window-close",
+    "sessionStorage data has been flushed when closing a window");
+});
+
+/**
+ * This test ensures that stale tab data is ignored when reusing a tab
+ * (via e.g. setTabState) and does not overwrite the new data.
+ */
+add_task(function flush_on_settabstate() {
+  let tab = yield createTabWithStorageData(["http://example.com"]);
+  let browser = tab.linkedBrowser;
+
+  // Flush to make sure our tab state is up-to-date.
+  SyncHandlers.get(browser).flush();
+
+  let state = ss.getTabState(tab);
+  yield modifySessionStorage(browser, {test: "on-set-tab-state"});
+
+  // Flush all data contained in the content script but send it using
+  // asynchronous messages.
+  SyncHandlers.get(browser).flushAsync();
+
+  ss.setTabState(tab, state);
+  yield promiseTabRestored(tab);
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data has not been overwritten");
+
+  gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that we won't lose tab data that has been sent
+ * asynchronously just before closing a tab. Flushing must re-send all data
+ * that hasn't been received by chrome, yet.
+ */
+add_task(function flush_on_tabclose_racy() {
+  let tab = yield createTabWithStorageData(["http://example.com"]);
+  let browser = tab.linkedBrowser;
+
+  // Flush to make sure we start with an empty queue.
+  SyncHandlers.get(browser).flush();
+
+  yield modifySessionStorage(browser, {test: "on-tab-close-racy"});
+
+  // Flush all data contained in the content script but send it using
+  // asynchronous messages.
+  SyncHandlers.get(browser).flushAsync();
+  gBrowser.removeTab(tab);
+
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  is(storage["http://example.com"].test, "on-tab-close-racy",
+    "sessionStorage data has been merged correctly to prevent data loss");
+});
+
+function promiseNewWindow() {
+  let deferred = Promise.defer();
+
+  whenNewWindowLoaded({private: false}, function (win) {
+    win.messageManager.loadFrameScript(FRAME_SCRIPT, true);
+    deferred.resolve(win);
+  });
+
+  return deferred.promise;
+}
+
+function closeWindow(win) {
+  let deferred = Promise.defer();
+  let outerID = win.QueryInterface(Ci.nsIInterfaceRequestor)
+                   .getInterface(Ci.nsIDOMWindowUtils)
+                   .outerWindowID;
+
+  Services.obs.addObserver(function obs(subject, topic) {
+    let id = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+    if (id == outerID) {
+      Services.obs.removeObserver(obs, topic);
+      deferred.resolve();
+    }
+  }, "outer-window-destroyed", false);
+
+  win.close();
+  return deferred.promise;
+}
+
+function createTabWithStorageData(urls, win = window) {
+  return Task.spawn(function task() {
+    let tab = win.gBrowser.addTab();
+    let browser = tab.linkedBrowser;
+
+    for (let url of urls) {
+      browser.loadURI(url);
+      yield promiseBrowserLoaded(browser);
+      yield modifySessionStorage(browser, {test: INITIAL_VALUE});
+    }
+
+    throw new Task.Result(tab);
+  });
+}
+
+function waitForStorageEvent(browser) {
+  return promiseContentMessage(browser, "ss-test:MozStorageChanged");
+}
+
+function sendQuitApplicationRequested() {
+  let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+                     .createInstance(Ci.nsISupportsPRBool);
+  Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+}
+
+function modifySessionStorage(browser, data) {
+  browser.messageManager.sendAsyncMessage("ss-test:modifySessionStorage", data);
+  return waitForStorageEvent(browser);
+}
--- a/browser/components/sessionstore/test/browser_capabilities.js
+++ b/browser/components/sessionstore/test/browser_capabilities.js
@@ -1,78 +1,78 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-function test() {
-  TestRunner.run();
-}
+"use strict";
 
 /**
- * This test ensures that disabling features by flipping nsIDocShell.allow*
+ * These tests ensures that disabling features by flipping nsIDocShell.allow*
  * properties are (re)stored as disabled. Disallowed features must be
  * re-enabled when the tab is re-used by another tab restoration.
  */
-
-function runTests() {
-  // Create a tab that we're going to use for our tests.
-  let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
+add_task(function docshell_capabilities() {
+  let tab = yield createTab();
   let browser = tab.linkedBrowser;
   let docShell = browser.docShell;
-  yield waitForLoad(browser);
 
   // Get the list of capabilities for docShells.
   let flags = Object.keys(docShell).filter(k => k.startsWith("allow"));
 
   // Check that everything is allowed by default for new tabs.
   let state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed by default");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Flip a couple of allow* flags.
   docShell.allowImages = false;
   docShell.allowMetaRedirects = false;
 
   // Now reload the document to ensure that these capabilities
-  // are taken into account
+  // are taken into account.
   browser.reload();
-  yield whenBrowserLoaded(browser);
+  yield promiseBrowserLoaded(browser);
+
+  // Flush to make sure chrome received all data.
+  SyncHandlers.get(browser).flush();
 
   // Check that we correctly save disallowed features.
   let disallowedState = JSON.parse(ss.getTabState(tab));
   let disallow = new Set(disallowedState.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Reuse the tab to restore a new, clean state into it.
   ss.setTabState(tab, JSON.stringify({ entries: [{url: "about:robots"}] }));
-  yield waitForLoad(browser);
+  yield promiseTabRestored(tab);
+
+  // Flush to make sure chrome received all data.
+  SyncHandlers.get(browser).flush();
 
   // After restoring disallowed features must be available again.
   state = JSON.parse(ss.getTabState(tab));
   ok(!("disallow" in state), "everything allowed again");
   ok(flags.every(f => docShell[f]), "all flags set to true");
 
   // Restore the state with disallowed features.
   ss.setTabState(tab, JSON.stringify(disallowedState));
-  yield waitForLoad(browser);
+  yield promiseTabRestored(tab);
 
   // Check that docShell flags are set.
   ok(!docShell.allowImages, "images not allowed");
   ok(!docShell.allowMetaRedirects, "meta redirects not allowed");
 
   // Check that we correctly restored features as disabled.
   state = JSON.parse(ss.getTabState(tab));
   disallow = new Set(state.disallow.split(","));
   ok(disallow.has("Images"), "images not allowed anymore");
   ok(disallow.has("MetaRedirects"), "meta redirects not allowed anymore");
   is(disallow.size, 2, "two capabilities disallowed");
 
   // Clean up after ourselves.
   gBrowser.removeTab(tab);
-}
+});
 
-function waitForLoad(aElement) {
-  aElement.addEventListener("load", function onLoad() {
-    aElement.removeEventListener("load", onLoad, true);
-    executeSoon(next);
-  }, true);
+function createTab() {
+  let tab = gBrowser.addTab("about:mozilla");
+  let browser = tab.linkedBrowser;
+  return promiseBrowserLoaded(browser).then(() => tab);
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const URL = getRootDirectory(gTestPath) + "browser_pageStyle_sample.html";
+const URL_NESTED = getRootDirectory(gTestPath) + "browser_pageStyle_sample_nested.html";
+
+/**
+ * This test ensures that page style information is correctly persisted.
+ */
+add_task(function page_style() {
+  let tab = gBrowser.addTab(URL);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
+  let sheets = yield getStyleSheets(browser);
+
+  // Enable all style sheets one by one.
+  for (let [title, disabled] of sheets) {
+    yield enableStyleSheetsForSet(browser, title);
+
+    let tab2 = gBrowser.duplicateTab(tab);
+    yield promiseTabRestored(tab2);
+
+    let sheets = yield getStyleSheets(tab2.linkedBrowser);
+    let enabled = sheets.filter(([title, disabled]) => !disabled);
+
+    if (title.startsWith("fail_")) {
+      ok(!enabled.length, "didn't restore " + title);
+    } else {
+      ok(enabled.length == 1 && enabled[0][0] == title, "restored " + title);
+    }
+
+    gBrowser.removeTab(tab2);
+  }
+
+  // Disable all styles and verify that this is correctly persisted.
+  yield setAuthorStyleDisabled(browser, true);
+
+  let tab2 = gBrowser.duplicateTab(tab);
+  yield promiseTabRestored(tab2);
+
+  let authorStyleDisabled = yield getAuthorStyleDisabled(tab2.linkedBrowser);
+  ok(authorStyleDisabled, "disabled all stylesheets");
+
+  // Clean up.
+  gBrowser.removeTab(tab);
+  gBrowser.removeTab(tab2);
+});
+
+/**
+ * This test ensures that page style notification from nested documents are
+ * received and the page style is persisted correctly.
+ */
+add_task(function nested_page_style() {
+  let tab = gBrowser.addTab(URL_NESTED);
+  let browser = tab.linkedBrowser;
+  yield promiseBrowserLoaded(browser);
+
+  yield enableSubDocumentStyleSheetsForSet(browser, "alternate");
+  gBrowser.removeTab(tab);
+
+  let [{state: {pageStyle}}] = JSON.parse(ss.getClosedTabData(window));
+  is(pageStyle, "alternate", "correct pageStyle persisted");
+});
+
+function getStyleSheets(browser) {
+  return sendMessage(browser, "ss-test:getStyleSheets").then(({data}) => data);
+}
+
+function enableStyleSheetsForSet(browser, name) {
+  return sendMessage(browser, "ss-test:enableStyleSheetsForSet", name);
+}
+
+function enableSubDocumentStyleSheetsForSet(browser, name) {
+  return sendMessage(browser, "ss-test:enableSubDocumentStyleSheetsForSet", {
+    id: "iframe", set: name
+  });
+}
+
+function getAuthorStyleDisabled(browser) {
+  return sendMessage(browser, "ss-test:getAuthorStyleDisabled")
+           .then(({data}) => data);
+}
+
+function setAuthorStyleDisabled(browser, val) {
+  return sendMessage(browser, "ss-test:setAuthorStyleDisabled", val)
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle_sample.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>pageStyle sample</title>
+
+  <link href="404.css" title="default" rel="stylesheet">
+  <link href="404.css" title="alternate" rel="alternate stylesheet">
+  <link href="404.css" title="altERnate" rel=" styLEsheet altERnate ">
+  <link href="404.css" title="media_empty" rel="alternate stylesheet" media="">
+  <link href="404.css" title="media_all" rel="alternate stylesheet" media="all">
+  <link href="404.css" title="media_ALL" rel="alternate stylesheet" media=" ALL ">
+  <link href="404.css" title="media_screen" rel="alternate stylesheet" media="screen">
+  <link href="404.css" title="media_print_screen" rel="alternate stylesheet" media="print,screen">
+  <link href="404.css" title="fail_media_print" rel="alternate stylesheet" media="print">
+  <link href="404.css" title="fail_media_projection" rel="stylesheet" media="projection">
+  <link href="404.css" title="fail_media_invalid" rel="alternate stylesheet" media="hallo">
+</head>
+<body></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_pageStyle_sample_nested.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>pageStyle sample (nested)</title>
+</head>
+<body>
+  <iframe id="iframe" src="browser_pageStyle_sample.html"/>
+</body>
+</html>
--- a/browser/components/sessionstore/test/browser_sessionStorage.js
+++ b/browser/components/sessionstore/test/browser_sessionStorage.js
@@ -1,78 +1,194 @@
-/* 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/. */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let tmp = {};
+Cu.import("resource://gre/modules/Promise.jsm", tmp);
+Cu.import("resource://gre/modules/ForgetAboutSite.jsm", tmp);
+let {Promise, ForgetAboutSite} = tmp;
+
+const INITIAL_VALUE = "initial-value-" + Date.now();
+
+/**
+ * This test ensures that setting, modifying and restoring sessionStorage data
+ * works as expected.
+ */
+add_task(function session_storage() {
+  let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]);
+  let browser = tab.linkedBrowser;
+
+  // Flush to make sure chrome received all data.
+  SyncHandlers.get(browser).flush();
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data for example.com has been serialized correctly");
+  is(storage["http://mochi.test:8888"].test, INITIAL_VALUE,
+    "sessionStorage data for mochi.test has been serialized correctly");
+
+  // Ensure that modifying sessionStore values works.
+  yield modifySessionStorage(browser, {test: "modified"});
+  SyncHandlers.get(browser).flush();
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data for example.com has been serialized correctly");
+  is(storage["http://mochi.test:8888"].test, "modified",
+    "sessionStorage data for mochi.test has been serialized correctly");
 
-let Scope = {};
-Cu.import("resource://gre/modules/Task.jsm", Scope);
-Cu.import("resource://gre/modules/Promise.jsm", Scope);
-let {Task, Promise} = Scope;
+  // Test that duplicating a tab works.
+  let tab2 = gBrowser.duplicateTab(tab);
+  let browser2 = tab2.linkedBrowser;
+  yield promiseTabRestored(tab2);
+
+  // Flush to make sure chrome received all data.
+  SyncHandlers.get(browser2).flush();
+
+  let {storage} = JSON.parse(ss.getTabState(tab2));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data for example.com has been duplicated correctly");
+  is(storage["http://mochi.test:8888"].test, "modified",
+    "sessionStorage data for mochi.test has been duplicated correctly");
+
+  // Ensure that the content script retains restored data
+  // (by e.g. duplicateTab) and send it along with new data.
+  yield modifySessionStorage(browser2, {test: "modified2"});
+  SyncHandlers.get(browser2).flush();
+
+  let {storage} = JSON.parse(ss.getTabState(tab2));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data for example.com has been duplicated correctly");
+  is(storage["http://mochi.test:8888"].test, "modified2",
+    "sessionStorage data for mochi.test has been duplicated correctly");
+
+  // Clean up.
+  gBrowser.removeTab(tab);
+  gBrowser.removeTab(tab2);
+});
+
+/**
+ * This test ensures that purging domain data also purges data from the
+ * sessionStorage data collected for tabs.
+ */
+add_task(function purge_domain() {
+  let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]);
+  let browser = tab.linkedBrowser;
+
+  ForgetAboutSite.removeDataFromDomain("mochi.test");
+  yield waitForUpdateMessage(browser);
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  ok(!storage["http://mochi.test:8888"],
+    "sessionStorage data for mochi.test has been purged");
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "sessionStorage data for example.com has been preserved");
 
+  gBrowser.removeTab(tab);
+});
 
-function waitForStorageChange(aTab) {
-  let deferred = Promise.defer();
-  waitForContentMessage(aTab.linkedBrowser,
-    "SessionStore:MozStorageChanged",
-    1000,
-    deferred.resolve);
-  return deferred.promise;
+/**
+ * This test ensures that purging session history data also purges data from
+ * sessionStorage data collected for tabs
+ */
+add_task(function purge_shistory() {
+  let tab = yield createTabWithStorageData(["http://example.com", "http://mochi.test:8888"]);
+  let browser = tab.linkedBrowser;
+
+  yield notifyObservers(browser, "browser:purge-session-history");
+
+  let {storage} = JSON.parse(ss.getTabState(tab));
+  ok(!storage["http://example.com"],
+    "sessionStorage data for example.com has been purged");
+  is(storage["http://mochi.test:8888"].test, INITIAL_VALUE,
+    "sessionStorage data for mochi.test has been preserved");
+
+  gBrowser.removeTab(tab);
+});
+
+/**
+ * This test ensures that collecting sessionStorage data respects the privacy
+ * levels as set by the user.
+ */
+add_task(function respect_privacy_level() {
+  let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]);
+  gBrowser.removeTab(tab);
+
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "http sessionStorage data has been saved");
+  is(storage["https://example.com"].test, INITIAL_VALUE,
+    "https sessionStorage data has been saved");
+
+  // Disable saving data for encrypted sites.
+  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 1);
+
+  let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]);
+  gBrowser.removeTab(tab);
+
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "http sessionStorage data has been saved");
+  ok(!storage["https://example.com"],
+    "https sessionStorage data has *not* been saved");
+
+  // Disable saving data for any site.
+  Services.prefs.setIntPref("browser.sessionstore.privacy_level", 2);
+
+  // Check that duplicating a tab copies all private data.
+  let tab = yield createTabWithStorageData(["http://example.com", "https://example.com"]);
+  let tab2 = gBrowser.duplicateTab(tab);
+  yield promiseBrowserLoaded(tab2.linkedBrowser);
+  gBrowser.removeTab(tab);
+
+  // With privacy_level=2 the |tab| shouldn't have any sessionStorage data.
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  ok(!storage["http://example.com"],
+    "http sessionStorage data has *not* been saved");
+  ok(!storage["https://example.com"],
+    "https sessionStorage data has *not* been saved");
+
+  // Restore the default privacy level and close the duplicated tab.
+  Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
+  gBrowser.removeTab(tab2);
+
+  // With privacy_level=0 the duplicated |tab2| should persist all data.
+  let [{state: {storage}}] = JSON.parse(ss.getClosedTabData(window));
+  is(storage["http://example.com"].test, INITIAL_VALUE,
+    "http sessionStorage data has been saved");
+  is(storage["https://example.com"].test, INITIAL_VALUE,
+    "https sessionStorage data has been saved");
+});
+
+function createTabWithStorageData(urls) {
+  return Task.spawn(function task() {
+    let tab = gBrowser.addTab();
+    let browser = tab.linkedBrowser;
+
+    for (let url of urls) {
+      browser.loadURI(url);
+      yield promiseBrowserLoaded(browser);
+      yield modifySessionStorage(browser, {test: INITIAL_VALUE});
+    }
+
+    throw new Task.Result(tab);
+  });
 }
 
-function test() {
-
-  waitForExplicitFinish();
-
-  let tab;
-  Task.spawn(function() {
-    try {
-      let SESSION_STORAGE_KEY = "SESSION_STORAGE_KEY " + Math.random();
-      let SESSION_STORAGE_VALUE = "SESSION_STORAGE_VALUE " + Math.random();
-      let LOCAL_STORAGE_KEY = "LOCAL_STORAGE_KEY " + Math.random();
-      let LOCAL_STORAGE_VALUE = "LOCAL_STORAGE_VALUE " + Math.random();
+function waitForStorageEvent(browser) {
+  return promiseContentMessage(browser, "ss-test:MozStorageChanged");
+}
 
-      tab = gBrowser.addTab("http://example.com");
-      // about:home supports sessionStorage and localStorage
-
-      let win = tab.linkedBrowser.contentWindow;
-
-      // Flush loading and next save, call getBrowserState()
-      // a few times to ensure that everything is cached.
-      yield promiseBrowserLoaded(tab.linkedBrowser);
-      yield forceSaveState();
-      info("Calling getBrowserState() to populate cache");
-      ss.getBrowserState();
+function waitForUpdateMessage(browser) {
+  return promiseContentMessage(browser, "SessionStore:update");
+}
 
-      info("Change sessionStorage, ensure that state is saved");
-      let storageChangedPromise = waitForStorageChange(tab);
-      win.sessionStorage[SESSION_STORAGE_KEY] = SESSION_STORAGE_VALUE;
-      let storageChanged = yield storageChangedPromise;
-      ok(storageChanged, "Changing sessionStorage triggered the right message");
-      yield forceSaveState();
-
-      let state = ss.getBrowserState();
-      ok(state.indexOf(SESSION_STORAGE_KEY) != -1, "Key appears in state");
-      ok(state.indexOf(SESSION_STORAGE_VALUE) != -1, "Value appears in state");
-
+function modifySessionStorage(browser, data) {
+  browser.messageManager.sendAsyncMessage("ss-test:modifySessionStorage", data);
+  return waitForStorageEvent(browser);
+}
 
-      info("Change localStorage, ensure that state is not saved");
-      storageChangedPromise = waitForStorageChange(tab);
-      win.localStorage[LOCAL_STORAGE_KEY] = LOCAL_STORAGE_VALUE;
-      storageChanged = yield storageChangedPromise;
-      ok(!storageChanged, "Changing localStorage did not trigger a message");
-      yield forceSaveState();
-
-      state = ss.getBrowserState();
-      ok(state.indexOf(LOCAL_STORAGE_KEY) == -1, "Key does not appear in state");
-      ok(state.indexOf(LOCAL_STORAGE_VALUE) == -1, "Value does not appear in state");
-    } catch (ex) {
-      ok(false, ex);
-      info(ex.stack);
-    } finally {
-      // clean up
-      if (tab) {
-        gBrowser.removeTab(tab);
-      }
-
-      executeSoon(finish);
-    }
-  });
+function notifyObservers(browser, topic) {
+  browser.messageManager.sendAsyncMessage("ss-test:notifyObservers", topic);
+  return waitForUpdateMessage(browser);
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/content.js
@@ -0,0 +1,52 @@
+/* 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 frame script is only loaded for sessionstore mochitests. It enables us
+ * to modify and query docShell data when running with multiple processes.
+ */
+
+addEventListener("MozStorageChanged", function () {
+  sendSyncMessage("ss-test:MozStorageChanged");
+});
+
+addMessageListener("ss-test:modifySessionStorage", function (msg) {
+  for (let key of Object.keys(msg.data)) {
+    content.sessionStorage[key] = msg.data[key];
+  }
+});
+
+addMessageListener("ss-test:notifyObservers", function (msg) {
+  Services.obs.notifyObservers(null, msg.data, "");
+});
+
+addMessageListener("ss-test:getStyleSheets", function (msg) {
+  let sheets = content.document.styleSheets;
+  let titles = Array.map(sheets, ss => [ss.title, ss.disabled]);
+  sendSyncMessage("ss-test:getStyleSheets", titles);
+});
+
+addMessageListener("ss-test:enableStyleSheetsForSet", function (msg) {
+  content.document.enableStyleSheetsForSet(msg.data);
+  sendSyncMessage("ss-test:enableStyleSheetsForSet");
+});
+
+addMessageListener("ss-test:enableSubDocumentStyleSheetsForSet", function (msg) {
+  let iframe = content.document.getElementById(msg.data.id);
+  iframe.contentDocument.enableStyleSheetsForSet(msg.data.set);
+  sendSyncMessage("ss-test:enableSubDocumentStyleSheetsForSet");
+});
+
+addMessageListener("ss-test:getAuthorStyleDisabled", function (msg) {
+  let {authorStyleDisabled} =
+    docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+  sendSyncMessage("ss-test:getAuthorStyleDisabled", authorStyleDisabled);
+});
+
+addMessageListener("ss-test:setAuthorStyleDisabled", function (msg) {
+  let markupDocumentViewer =
+    docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+  markupDocumentViewer.authorStyleDisabled = msg.data;
+  sendSyncMessage("ss-test:setAuthorStyleDisabled");
+});
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -1,18 +1,42 @@
 /* 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 TAB_STATE_NEEDS_RESTORE = 1;
 const TAB_STATE_RESTORING = 2;
 
+const FRAME_SCRIPT = "chrome://mochitests/content/browser/browser/components/" +
+                     "sessionstore/test/content.js";
+
+let mm = Cc["@mozilla.org/globalmessagemanager;1"]
+           .getService(Ci.nsIMessageListenerManager);
+mm.loadFrameScript(FRAME_SCRIPT, true);
+mm.addMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler);
+
+/**
+ * This keeps track of all SyncHandlers passed to chrome from frame scripts.
+ * We need this to let tests communicate with frame scripts and cause (a)sync
+ * flushes.
+ */
+let SyncHandlers = new WeakMap();
+function onSetupSyncHandler(msg) {
+  SyncHandlers.set(msg.target, msg.objects.handler);
+}
+
+registerCleanupFunction(() => {
+  mm.removeDelayedFrameScript(FRAME_SCRIPT);
+  mm.removeMessageListener("SessionStore:setupSyncHandler", onSetupSyncHandler);
+});
+
 let tmp = {};
+Cu.import("resource://gre/modules/Promise.jsm", tmp);
 Cu.import("resource:///modules/sessionstore/SessionStore.jsm", tmp);
-let SessionStore = tmp.SessionStore;
+let {Promise, SessionStore} = tmp;
 
 let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
 
 // Some tests here assume that all restored tabs are loaded without waiting for
 // the user to bring them to the foreground. We ensure this by resetting the
 // related preference (see the "firefox.js" defaults file for details).
 Services.prefs.setBoolPref("browser.sessionstore.restore_on_demand", false);
 registerCleanupFunction(function () {
@@ -154,46 +178,32 @@ function waitForTabState(aTab, aState, a
     }
   });
   ss.setTabState(aTab, JSON.stringify(aState));
 }
 
 /**
  * Wait for a content -> chrome message.
  */
-function waitForContentMessage(aBrowser, aTopic, aTimeout, aCallback) {
-  let mm = aBrowser.messageManager;
-  let observing = false;
-  function removeObserver() {
-    if (!observing)
-      return;
-    mm.removeMessageListener(aTopic, observer);
-    observing = false;
+function promiseContentMessage(browser, name) {
+  let deferred = Promise.defer();
+  let mm = browser.messageManager;
+
+  function removeListener() {
+    mm.removeMessageListener(name, listener);
   }
 
-  let timeout = setTimeout(function () {
-    removeObserver();
-    aCallback(false);
-  }, aTimeout);
-
-  function observer(aSubject, aTopic, aData) {
-    removeObserver();
-    timeout = clearTimeout(timeout);
-    executeSoon(() => aCallback(true));
+  function listener(msg) {
+    removeListener();
+    deferred.resolve(msg);
   }
 
-  registerCleanupFunction(function() {
-    removeObserver();
-    if (timeout) {
-      clearTimeout(timeout);
-    }
-  });
-
-  observing = true;
-  mm.addMessageListener(aTopic, observer);
+  mm.addMessageListener(name, listener);
+  registerCleanupFunction(removeListener);
+  return deferred.promise;
 }
 
 function waitForTopic(aTopic, aTimeout, aCallback) {
   let observing = false;
   function removeObserver() {
     if (!observing)
       return;
     Services.obs.removeObserver(observer, aTopic);
@@ -451,8 +461,24 @@ let TestRunner = {
     closeAllButPrimaryWindow();
     waitForBrowserState(this.backupState, finish);
   }
 };
 
 function next() {
   TestRunner.next();
 }
+
+function promiseTabRestored(tab) {
+  let deferred = Promise.defer();
+
+  tab.addEventListener("SSTabRestored", function onRestored() {
+    tab.removeEventListener("SSTabRestored", onRestored);
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
+
+function sendMessage(browser, name, data = {}) {
+  browser.messageManager.sendAsyncMessage(name, data);
+  return promiseContentMessage(browser, name);
+}
--- a/browser/installer/windows/nsis/defines.nsi.in
+++ b/browser/installer/windows/nsis/defines.nsi.in
@@ -1,26 +1,28 @@
 #filter substitution
 # 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/.
 
-# Defining FunnelcakeVersion will append the value of FunnelcakeVersion to the
-# stub data URL version using StubURLVersionAppend and an additional parameter
-# to both URLStubDownload and URLManualDownload using URLParamAppend. The value
-# should not be defined when it is not used and when it is defined its value
-# should never be empty.
-# !define FunnelcakeVersion     "999"
+# Defining FunnelcakeVersion will append the value of StubURLVersionAppend to
+# StubURLVersion, append the value of URLManualDownloadAppend to
+# URLManualDownload, and append the value of URLStubDownloadAppend to
+# URLStubDownload. The value of FunnelcakeVersion should not be defined when it
+# is not used and when it is defined its value should never be empty.
+# !define FunnelcakeVersion        "999"
 
 !ifdef FunnelcakeVersion
-!define URLParamAppend        "&f=${FunnelcakeVersion}"
-!define StubURLVersionAppend  "-${FunnelcakeVersion}"
+!define URLManualDownloadAppend  "&f=${FunnelcakeVersion}"
+!define URLStubDownloadAppend    "-f${FunnelcakeVersion}"
+!define StubURLVersionAppend     "-${FunnelcakeVersion}"
 !else
-!define URLParamAppend        ""
-!define StubURLVersionAppend  ""
+!define URLManualDownloadAppend  ""
+!define URLStubDownloadAppend    ""
+!define StubURLVersionAppend     ""
 !endif
 
 # These defines should match application.ini settings
 !define AppName               "Firefox"
 !define AppVersion            "@APP_VERSION@"
 !define GREVersion            @MOZILLA_VERSION@
 !define AB_CD                 "@AB_CD@"
 
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -212,17 +212,17 @@ Var ControlRightPX
 !include "defines.nsi"
 
 ; The OFFICIAL define is a workaround to support different urls for Release and
 ; Beta since they share the same branding when building with other branches that
 ; set the update channel to beta.
 !ifdef OFFICIAL
 !ifdef BETA_UPDATE_CHANNEL
 !undef URLStubDownload
-!define URLStubDownload "http://download.mozilla.org/?product=firefox-beta-latest&os=win&lang=${AB_CD}"
+!define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-beta-latest"
 !undef URLManualDownload
 !define URLManualDownload "https://www.mozilla.org/${AB_CD}/firefox/installer-help/?channel=beta&installer_lang=${AB_CD}"
 !undef Channel
 !define Channel "beta"
 !endif
 !endif
 
 !include "common.nsh"
@@ -1214,17 +1214,17 @@ Function createInstall
   ${NSD_FreeImage} $1
   ${NSD_FreeImage} $HwndBitmapBlurb1
   ${NSD_FreeImage} $HwndBitmapBlurb2
   ${NSD_FreeImage} $HWndBitmapBlurb3
 FunctionEnd
 
 Function StartDownload
   ${NSD_KillTimer} StartDownload
-  InetBgDL::Get "${URLStubDownload}${URLParamAppend}" "$PLUGINSDIR\download.exe" \
+  InetBgDL::Get "${URLStubDownload}${URLStubDownloadAppend}" "$PLUGINSDIR\download.exe" \
                 /CONNECTTIMEOUT 120 /RECEIVETIMEOUT 120 /END
   StrCpy $4 ""
   ${NSD_CreateTimer} OnDownload ${DownloadIntervalMS}
   ${If} ${FileExists} "$INSTDIR\${TO_BE_DELETED}"
     RmDir /r "$INSTDIR\${TO_BE_DELETED}"
   ${EndIf}
 FunctionEnd
 
@@ -1865,13 +1865,13 @@ Function DisplayDownloadError
       UAC::ExecCodeSegment $0
     ${EndIf}
   ${EndIf}
 
   Call SendPing
 FunctionEnd
 
 Function OpenManualDownloadURL
-  ExecShell "open" "${URLManualDownload}${URLParamAppend}"
+  ExecShell "open" "${URLManualDownload}${URLManualDownloadAppend}"
 FunctionEnd
 
 Section
 SectionEnd
--- a/browser/metro/base/content/contenthandlers/Content.js
+++ b/browser/metro/base/content/contenthandlers/Content.js
@@ -119,29 +119,28 @@ let Content = {
   _isZoomedIn: false,
 
   get formAssistant() {
     delete this.formAssistant;
     return this.formAssistant = new FormAssistant();
   },
 
   init: function init() {
-    this._isZoomedToElement = false;
-
     // Asyncronous messages sent from the browser
     addMessageListener("Browser:Blur", this);
     addMessageListener("Browser:SaveAs", this);
     addMessageListener("Browser:MozApplicationCache:Fetch", this);
     addMessageListener("Browser:SetCharset", this);
     addMessageListener("Browser:CanUnload", this);
     addMessageListener("Browser:PanBegin", this);
+    addMessageListener("Gesture:SingleTap", this);
+    addMessageListener("Gesture:DoubleTap", this);
 
     addEventListener("touchstart", this, false);
     addEventListener("click", this, true);
-    addEventListener("dblclick", this, true);
     addEventListener("keydown", this);
     addEventListener("keyup", this);
 
     // Synchronous events caught during the bubbling phase
     addEventListener("MozApplicationManifest", this, false);
     addEventListener("DOMContentLoaded", this, false);
     addEventListener("DOMAutoComplete", this, false);
     addEventListener("DOMFormHasPassword", this, false);
@@ -179,25 +178,16 @@ let Content = {
         // Allow down arrow to trigger autofill popup on empty input.
         if ((!aEvent.target.value && aEvent.keyCode != aEvent.DOM_VK_DOWN)
           || aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
           this.formAssistant.close();
         else
           this.formAssistant.open(aEvent.target, aEvent);
         break;
 
-      case "dblclick":
-        // XXX Once gesture listners are used(Bug 933236), apzc will notify us
-        if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
-          let selection = content.getSelection();
-          selection.removeAllRanges();
-          this._onZoomToTappedContent(aEvent.target);
-        }
-        break;
-
       case "click":
         // Workaround for bug 925457: we sometimes don't recognize the
         // correct tap target or are unable to identify if it's editable.
         // Instead always save tap co-ordinates for the keyboard to look for
         // when it is up.
         SelectionHandler.onClickCoords(aEvent.clientX, aEvent.clientY);
 
         if (aEvent.eventPhase == aEvent.BUBBLING_PHASE)
@@ -216,18 +206,17 @@ let Content = {
         break;
 
       case "DOMAutoComplete":
       case "blur":
         LoginManagerContent.onUsernameInput(aEvent);
         break;
 
       case "pagehide":
-        if (aEvent.target == content.document)
-          this._resetFontSize();          
+        this._isZoomedIn = false;
         break;
 
       case "touchstart":
         this._onTouchStart(aEvent);
         break;
     }
   },
 
@@ -267,16 +256,24 @@ let Content = {
         let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
         webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
         break;
       }
 
       case "Browser:PanBegin":
         this._cancelTapHighlight();
         break;
+
+      case "Gesture:SingleTap":
+        this._onSingleTap(json.x, json.y);
+        break;
+
+      case "Gesture:DoubleTap":
+        this._onDoubleTap(json.x, json.y);
+        break;
     }
   },
 
   /******************************************************
    * Event handlers
    */
 
   _onTouchStart: function _onTouchStart(aEvent) {
@@ -371,30 +368,38 @@ let Content = {
         let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
         webNav.loadURI(content.location,
                        Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
                        null, null, null);
       }
     }
   },
 
-  _onZoomToTappedContent: function (aElement) {
-    if (!aElement || this._isZoomedIn) {
+  _onSingleTap: function (aX, aY) {
+    let utils = Util.getWindowUtils(content);
+    for (let type of ["mousemove", "mousedown", "mouseup"]) {
+      utils.sendMouseEventToWindow(type, aX, aY, 0, 1, 0, true, 1.0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
+    }
+  },
+
+  _onDoubleTap: function (aX, aY) {
+    if (this._isZoomedIn) {
       this._zoomOut();
       return;
     }
 
-    while (aElement && !this._shouldZoomToElement(aElement)) {
-      aElement = aElement.parentNode;
+    let { element } = Content.getCurrentWindowAndOffset(aX, aY);
+    while (element && !this._shouldZoomToElement(element)) {
+      element = element.parentNode;
     }
 
-    if (!aElement) {
+    if (!element) {
       this._zoomOut();
     } else {
-      this._zoomToElement(aElement);
+      this._zoomToElement(element);
     }
   },
 
   /******************************************************
    * Zoom utilities
    */
   _zoomOut: function() {
     let rect = new Rect(0,0,0,0);
@@ -446,123 +451,91 @@ let Content = {
     }
   },
 
 
   /******************************************************
    * General utilities
    */
 
-  _getContentClientRects: function getContentClientRects(aElement) {
-    let offset = ContentScroll.getScrollOffset(content);
-    offset = new Point(offset.x, offset.y);
+  /*
+   * Retrieve the total offset from the window's origin to the sub frame
+   * element including frame and scroll offsets. The resulting offset is
+   * such that:
+   * sub frame coords + offset = root frame position
+   */
+  getCurrentWindowAndOffset: function(x, y) {
+    // If the element at the given point belongs to another document (such
+    // as an iframe's subdocument), the element in the calling document's
+    // DOM (e.g. the iframe) is returned.
+    let utils = Util.getWindowUtils(content);
+    let element = utils.elementFromPoint(x, y, true, false);
+    let offset = { x:0, y:0 };
+
+    while (element && (element instanceof HTMLIFrameElement ||
+                       element instanceof HTMLFrameElement)) {
+      // get the child frame position in client coordinates
+      let rect = element.getBoundingClientRect();
 
-    let nativeRects = aElement.getClientRects();
-    // step out of iframes and frames, offsetting scroll values
-    for (let frame = aElement.ownerDocument.defaultView; frame != content;
-         frame = frame.parent) {
-      // adjust client coordinates' origin to be top left of iframe viewport
-      let rect = frame.frameElement.getBoundingClientRect();
-      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
-      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
-      offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
+      // calculate offsets for digging down into sub frames
+      // using elementFromPoint:
+
+      // Get the content scroll offset in the child frame
+      scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView);
+      // subtract frame and scroll offset from our elementFromPoint coordinates
+      x -= rect.left + scrollOffset.x;
+      y -= rect.top + scrollOffset.y;
+
+      // calculate offsets we'll use to translate to client coords:
+
+      // add frame client offset to our total offset result
+      offset.x += rect.left;
+      offset.y += rect.top;
+
+      // get the frame's nsIDOMWindowUtils
+      utils = element.contentDocument
+                     .defaultView
+                     .QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindowUtils);
+
+      // retrieve the target element in the sub frame at x, y
+      element = utils.elementFromPoint(x, y, true, false);
     }
 
-    let result = [];
-    for (let i = nativeRects.length - 1; i >= 0; i--) {
-      let r = nativeRects[i];
-      result.push({ left: r.left + offset.x,
-                    top: r.top + offset.y,
-                    width: r.width,
-                    height: r.height
-                  });
-    }
-    return result;
+    if (!element)
+      return {};
+
+    return {
+      element: element,
+      contentWindow: element.ownerDocument.defaultView,
+      offset: offset,
+      utils: utils
+    };
   },
 
+
   _maybeNotifyErrorPage: function _maybeNotifyErrorPage() {
     // Notify browser that an error page is being shown instead
     // of the target location. Necessary to get proper thumbnail
     // updates on chrome for error pages.
     if (content.location.href !== content.document.documentURI)
       sendAsyncMessage("Browser:ErrorPage", null);
   },
 
-  _resetFontSize: function _resetFontSize() {
-    this._isZoomedToElement = false;
-    this._setMinFontSize(0);
-  },
-
   _highlightElement: null,
 
   _doTapHighlight: function _doTapHighlight(aElement) {
     gDOMUtils.setContentState(aElement, kStateActive);
     this._highlightElement = aElement;
   },
 
   _cancelTapHighlight: function _cancelTapHighlight(aElement) {
     gDOMUtils.setContentState(content.document.documentElement, kStateActive);
     this._highlightElement = null;
   },
-
-  /*
-   * _sendMouseEvent
-   *
-   * Delivers mouse events directly to the content window, bypassing
-   * the input overlay.
-   */
-  _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY, aButton) {
-    // Elements can be off from the aX/aY point because due to touch radius.
-    // If outside, we move the touch point to the center of the element.
-    if (!(aElement instanceof HTMLHtmlElement)) {
-      let isTouchClick = true;
-      let rects = this._getContentClientRects(aElement);
-      for (let i = 0; i < rects.length; i++) {
-        let rect = rects[i];
-        // We might be able to deal with fractional pixels, but mouse
-        // events won't. Deflate the bounds in by 1 pixel to deal with
-        // any fractional scroll offset issues.
-        let inBounds = 
-          (aX > rect.left + 1 && aX < (rect.left + rect.width - 1)) &&
-          (aY > rect.top + 1 && aY < (rect.top + rect.height - 1));
-        if (inBounds) {
-          isTouchClick = false;
-          break;
-        }
-      }
-
-      if (isTouchClick) {
-        let rect = new Rect(rects[0].left, rects[0].top,
-                            rects[0].width, rects[0].height);
-        if (rect.isEmpty())
-          return;
-
-        let point = rect.center();
-        aX = point.x;
-        aY = point.y;
-      }
-    }
-
-    let button = aButton || 0;
-    let scrollOffset = ContentScroll.getScrollOffset(content);
-    let x = aX - scrollOffset.x;
-    let y = aY - scrollOffset.y;
-
-    // setting touch source here is important so that when this gets
-    // captured by our precise input detection we can ignore it.
-    let windowUtils = Util.getWindowUtils(content);
-    windowUtils.sendMouseEventToWindow(aName, x, y, button, 1, 0, true,
-                                       1.0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE);
-  },
-
-  _setMinFontSize: function _setMinFontSize(aSize) {
-    let viewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
-    if (viewer)
-      viewer.minFontSize = aSize;
-  }
 };
 
 Content.init();
 
 var FormSubmitObserver = {
   init: function init(){
     addMessageListener("Browser:TabOpen", this);
     addMessageListener("Browser:TabClose", this);
--- a/browser/metro/base/content/contenthandlers/SelectionHandler.js
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -392,17 +392,17 @@ var SelectionHandler = {
    * client coordinate offset, target element, and dom utils interface.
    */
   _initTargetInfo: function _initTargetInfo(aX, aY) {
     // getCurrentWindowAndOffset takes client coordinates
     let { element: element,
           contentWindow: contentWindow,
           offset: offset,
           utils: utils } =
-      this.getCurrentWindowAndOffset(aX, aY);
+      Content.getCurrentWindowAndOffset(aX, aY);
     if (!contentWindow) {
       return false;
     }
     this._targetElement = element;
     this._contentWindow = contentWindow;
     this._contentOffset = offset;
     this._domWinUtils = utils;
     this._targetIsEditable = Util.isEditable(this._targetElement);
@@ -551,71 +551,16 @@ var SelectionHandler = {
         break;
     }
   },
 
   /*************************************************
    * Utilities
    */
 
-  /*
-   * Retrieve the total offset from the window's origin to the sub frame
-   * element including frame and scroll offsets. The resulting offset is
-   * such that:
-   * sub frame coords + offset = root frame position
-   */
-  getCurrentWindowAndOffset: function(x, y) {
-    // If the element at the given point belongs to another document (such
-    // as an iframe's subdocument), the element in the calling document's
-    // DOM (e.g. the iframe) is returned.
-    let utils = Util.getWindowUtils(content);
-    let element = utils.elementFromPoint(x, y, true, false);
-    let offset = { x:0, y:0 };
-
-    while (element && (element instanceof HTMLIFrameElement ||
-                       element instanceof HTMLFrameElement)) {
-      // get the child frame position in client coordinates
-      let rect = element.getBoundingClientRect();
-
-      // calculate offsets for digging down into sub frames
-      // using elementFromPoint:
-
-      // Get the content scroll offset in the child frame
-      scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView);
-      // subtract frame and scroll offset from our elementFromPoint coordinates
-      x -= rect.left + scrollOffset.x;
-      y -= rect.top + scrollOffset.y;
-
-      // calculate offsets we'll use to translate to client coords:
-
-      // add frame client offset to our total offset result
-      offset.x += rect.left;
-      offset.y += rect.top;
-
-      // get the frame's nsIDOMWindowUtils
-      utils = element.contentDocument
-                     .defaultView
-                     .QueryInterface(Ci.nsIInterfaceRequestor)
-                     .getInterface(Ci.nsIDOMWindowUtils);
-
-      // retrieve the target element in the sub frame at x, y
-      element = utils.elementFromPoint(x, y, true, false);
-    }
-
-    if (!element)
-      return {};
-
-    return {
-      element: element,
-      contentWindow: element.ownerDocument.defaultView,
-      offset: offset,
-      utils: utils
-    };
-  },
-
   _getDocShell: function _getDocShell(aWindow) {
     if (aWindow == null)
       return null;
     return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                   .getInterface(Ci.nsIWebNavigation)
                   .QueryInterface(Ci.nsIDocShell);
   },
 
--- a/browser/metro/base/content/input.js
+++ b/browser/metro/base/content/input.js
@@ -97,16 +97,19 @@ var TouchModule = {
     window.addEventListener("MozMouseHittest", this, true);
 
     // bubble phase
     window.addEventListener("contextmenu", this, false);
     window.addEventListener("touchstart", this, false);
     window.addEventListener("touchmove", this, false);
     window.addEventListener("touchend", this, false);
 
+    Services.obs.addObserver(this, "Gesture:SingleTap", false);
+    Services.obs.addObserver(this, "Gesture:DoubleTap", false);
+
     try {
       this._treatMouseAsTouch = Services.prefs.getBoolPref(kDebugMouseInputPref);
     } catch (e) {}
   },
 
   /*
    * Mouse input source tracking
    */
@@ -189,16 +192,26 @@ var TouchModule = {
               SelectionHelperUI.attachEditSession(Browser.selectedBrowser,
                                                   rect.left + rect.width / 2,
                                                   rect.top + rect.height / 2);
         }
       }, 50);
     }
   },
 
+  observe: function BrowserUI_observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "Gesture:SingleTap":
+      case "Gesture:DoubleTap":
+        Browser.selectedBrowser.messageManager.sendAsyncMessage(aTopic, JSON.parse(aData));
+        break;
+    }
+  },
+
+
   sample: function sample(aTimeStamp) {
     this._waitingForPaint = false;
   },
 
   /**
    * This gets invoked by the input handler if another module grabs.  We should
    * reset our state or something here.  This is probably doing the wrong thing
    * in its current form.
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -783,8 +783,22 @@ pref("gfx.canvas.azure.accelerated", tru
 
 pref("general.useragent.override.youtube.com", "Android; Tablet;#Android; Mobile;");
 
 // When true, phone number linkification is enabled.
 pref("browser.ui.linkify.phone", false);
 
 // Enables/disables Spatial Navigation
 pref("snav.enabled", true);
+
+// This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
+// this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
+// repackager of this code using an alternate snippet url, please keep your users safe
+pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
+
+// How frequently we check for new snippets, in seconds (1 day)
+pref("browser.snippets.updateInterval", 86400);
+
+// URL used to check for user's country code
+pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
+
+// This pref requires a restart to take effect.
+pref("browser.snippets.enabled", false);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/ActionModeCompat.java
@@ -0,0 +1,101 @@
+/* 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/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.widget.GeckoPopupMenu;
+
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+class ActionModeCompat implements GeckoPopupMenu.OnMenuItemClickListener,
+                                  View.OnClickListener {
+    private final String LOGTAG = "GeckoActionModeCompat";
+
+    private Callback mCallback;
+    private ActionModeCompatView mView;
+    private Presenter mPresenter;
+
+    /* A set of callbacks to be called during this ActionMode's lifecycle. These will control the
+     * creation, interaction with, and destruction of menuitems for the view */
+    public static interface Callback {
+        /* Called when action mode is first created. Implementors should use this to inflate menu resources. */
+        public boolean onCreateActionMode(ActionModeCompat mode, Menu menu);
+
+        /* Called to refresh an action mode's action menu. Called whenever the mode is invalidated. Implementors
+         * should use this to enable/disable/show/hide menu items. */
+        public boolean onPrepareActionMode(ActionModeCompat mode, Menu menu);
+
+        /* Called to report a user click on an action button. */
+        public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item);
+
+        /* Called when an action mode is about to be exited and destroyed. */
+        public void onDestroyActionMode(ActionModeCompat mode);
+    }
+
+    /* Presenters handle the actual showing/hiding of the action mode UI in the app. Its their responsibility
+     * to create an action mode, and assign it Callbacks and ActionModeCompatView's. */
+    public static interface Presenter {
+        /* Called when an action mode should be shown */
+        public void startActionModeCompat(final Callback callback);
+
+        /* Called when whatever action mode is showing should be hidden */
+        public void endActionModeCompat();
+    }
+
+    public ActionModeCompat(Presenter presenter, Callback callback, ActionModeCompatView view) {
+        mPresenter = presenter;
+        mCallback = callback;
+
+        mView = view;
+        mView.initForMode(this);
+    }
+
+    public void finish() {
+        // Clearing the menu will also clear the ActionItemBar
+        mView.getMenu().clear();
+        if (mCallback != null) {
+            mCallback.onDestroyActionMode(this);
+        }
+    }
+
+    public CharSequence getTitle() {
+        return mView.getTitle();
+    }
+
+    public void setTitle(CharSequence title) {
+        mView.setTitle(title);
+    }
+
+    public void setTitle(int resId) {
+        mView.setTitle(resId);
+    }
+
+    public Menu getMenu() {
+        return mView.getMenu();
+    }
+
+    public void invalidate() {
+        if (mCallback != null) {
+            mCallback.onPrepareActionMode(this, mView.getMenu());
+        }
+        mView.invalidate();
+    }
+
+    /* GeckoPopupMenu.OnMenuItemClickListener */
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (mCallback != null) {
+            return mCallback.onActionItemClicked(this, item);
+        }
+        return false;
+    }
+
+    /* View.OnClickListener*/
+    @Override
+    public void onClick(View v) {
+        mPresenter.endActionModeCompat();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/ActionModeCompatView.java
@@ -0,0 +1,159 @@
+/* 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/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.menu.GeckoMenu;
+import org.mozilla.gecko.menu.MenuPopup;
+import org.mozilla.gecko.menu.MenuPanel;
+import org.mozilla.gecko.menu.MenuItemActionBar;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
+
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuInflater;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+import android.view.View;
+import android.view.ViewGroup;
+import android.content.Context;
+import android.widget.TextView;
+import android.widget.ImageButton;
+import android.widget.Button;
+import android.widget.PopupWindow;
+import android.util.AttributeSet;
+
+class ActionModeCompatView extends LinearLayout implements GeckoMenu.ActionItemBarPresenter {
+    private final String LOGTAG = "GeckoActionModeCompatPresenter";
+
+    private static final int SPEC = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+
+    private Button mTitleView;
+    private ImageButton mMenuButton;
+    private ViewGroup mActionButtonBar;
+    private GeckoPopupMenu mPopupMenu;
+
+    // Maximum number of items to show as actions
+    private static final int MAX_ACTION_ITEMS = 4;
+
+    private int mActionButtonsWidth = 0;
+
+    public ActionModeCompatView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public ActionModeCompatView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public ActionModeCompatView(Context context, AttributeSet attrs, int style) {
+        super(context, attrs, style);
+        init(context);
+    }
+
+    public void init(Context context) {
+        LayoutInflater.from(context).inflate(R.layout.actionbar, this);
+
+        mTitleView = (Button) findViewById(R.id.actionmode_title);
+        mMenuButton = (ImageButton) findViewById(R.id.actionbar_menu);
+        mActionButtonBar = (ViewGroup) findViewById(R.id.actionbar_buttons);
+
+        mPopupMenu = new GeckoPopupMenu(getContext(), mMenuButton);
+        ((GeckoMenu) mPopupMenu.getMenu()).setActionItemBarPresenter(this);
+
+        mMenuButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                openMenu();
+            }
+        });
+    }
+
+    public void initForMode(final ActionModeCompat mode) {
+        mTitleView.setOnClickListener(mode);
+        mPopupMenu.setOnMenuItemClickListener(mode);
+    }
+
+    public CharSequence getTitle() {
+        return mTitleView.getText();
+    }
+
+    public void setTitle(CharSequence title) {
+        mTitleView.setText(title);
+    }
+
+    public void setTitle(int resId) {
+        mTitleView.setText(resId);
+    }
+
+    public Menu getMenu() {
+        return mPopupMenu.getMenu();
+    }
+
+    public void invalidate() {
+        // onFinishInflate may not have been called yet on some versions of Android
+        if (mPopupMenu != null && mMenuButton != null) {
+            mMenuButton.setVisibility(mPopupMenu.getMenu().hasVisibleItems() ? View.VISIBLE : View.GONE);
+        }
+        super.invalidate();
+    }
+
+    /* GeckoMenu.ActionItemBarPresenter */
+    @Override
+    public boolean addActionItem(View actionItem) {
+        final int count = mActionButtonBar.getChildCount();
+        if (count >= MAX_ACTION_ITEMS) {
+            return false;
+        }
+
+        int maxWidth = mActionButtonBar.getMeasuredWidth();
+        if (maxWidth == 0) {
+            mActionButtonBar.measure(SPEC, SPEC);
+            maxWidth = mActionButtonBar.getMeasuredWidth();
+        }
+
+        // Since we don't know how many items will be added, we always reserve space for the overflow menu
+        mMenuButton.measure(SPEC, SPEC);
+        maxWidth -= mMenuButton.getMeasuredWidth();
+
+        if (mActionButtonsWidth <= 0) {
+            mActionButtonsWidth = 0;
+
+            // Loop over child views, measure them, and add their width to the taken width
+            for (int i = 0; i < count; i++) {
+                View v = mActionButtonBar.getChildAt(i);
+                v.measure(SPEC, SPEC);
+                mActionButtonsWidth += v.getMeasuredWidth();
+            }
+        }
+
+        actionItem.measure(SPEC, SPEC);
+        int w = actionItem.getMeasuredWidth();
+        if (mActionButtonsWidth + w < maxWidth) {
+            // We cache the new width of our children.
+            mActionButtonsWidth += w;
+            mActionButtonBar.addView(actionItem);
+            return true;
+        }
+        return false;
+    }
+
+    /* GeckoMenu.ActionItemBarPresenter */
+    @Override
+    public void removeActionItem(View actionItem) {
+        mActionButtonsWidth -= actionItem.getMeasuredWidth();
+        mActionButtonBar.removeView(actionItem);
+    }
+
+    public void openMenu() {
+        mPopupMenu.openMenu();
+    }
+
+    public void closeMenu() {
+        mPopupMenu.dismiss();
+    }
+
+}
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -10,16 +10,17 @@ import org.mozilla.gecko.animation.ViewH
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.GeckoLayerClient;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
+import org.mozilla.gecko.gfx.LayerMarginsAnimator;
 import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.health.BrowserHealthReporter;
 import org.mozilla.gecko.home.BrowserSearch;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.preferences.GeckoPreferences;
@@ -67,34 +68,37 @@ import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 import android.widget.Toast;
+import android.widget.ViewFlipper;
 
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.Vector;
 
 abstract public class BrowserApp extends GeckoApp
                                  implements TabsPanel.TabsLayoutChangeListener,
                                             PropertyAnimator.PropertyAnimationListener,
                                             View.OnKeyListener,
                                             GeckoLayerClient.OnMetricsChangedListener,
                                             BrowserSearch.OnSearchListener,
                                             BrowserSearch.OnEditSuggestionListener,
                                             HomePager.OnNewTabsListener,
-                                            OnUrlOpenListener {
+                                            OnUrlOpenListener,
+                                            ActionModeCompat.Presenter {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final String PREF_CHROME_DYNAMICTOOLBAR = "browser.chrome.dynamictoolbar";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     private static final int READER_ADD_SUCCESS = 0;
     private static final int READER_ADD_FAILED = 1;
@@ -105,20 +109,23 @@ abstract public class BrowserApp extends
 
     private static final String STATE_ABOUT_HOME_TOP_PADDING = "abouthome_top_padding";
     private static final String STATE_DYNAMIC_TOOLBAR_ENABLED = "dynamic_toolbar";
 
     private static final String BROWSER_SEARCH_TAG = "browser_search";
     private BrowserSearch mBrowserSearch;
     private View mBrowserSearchContainer;
 
+    public ViewFlipper mViewFlipper;
+    public ActionModeCompatView mActionBar;
     private BrowserToolbar mBrowserToolbar;
     private HomePager mHomePager;
     private View mHomePagerContainer;
     protected Telemetry.Timer mAboutHomeStartupTimer = null;
+    private ActionModeCompat mActionMode;
 
     private static final int GECKO_TOOLS_MENU = -1;
     private static final int ADDON_MENU_OFFSET = 1000;
     private class MenuItemInfo {
         public int id;
         public String label;
         public String icon;
         public boolean checkable = false;
@@ -259,17 +266,17 @@ abstract public class BrowserApp extends
         }
 
         // Gamepad support only exists in API-level >= 9
         if (Build.VERSION.SDK_INT >= 9 &&
             (event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_BUTTON_Y:
                     // Toggle/focus the address bar on gamepad-y button.
-                    if (mBrowserToolbar.isVisible()) {
+                    if (mViewFlipper.getVisibility() == View.VISIBLE) {
                         if (isDynamicToolbarEnabled() && !isHomePagerVisible()) {
                             if (mLayerView != null) {
                                 mLayerView.getLayerMarginsAnimator().hideMargins(false);
                                 mLayerView.requestFocus();
                             }
                         } else {
                             // Just focus the address bar when about:home is visible
                             // or when the dynamic toolbar isn't enabled.
@@ -419,16 +426,19 @@ abstract public class BrowserApp extends
         if (args != null && args.contains(GUEST_BROWSING_ARG)) {
             mProfile = GeckoProfile.createGuestProfile(this);
         } else {
             GeckoProfile.maybeCleanupGuestProfile(this);
         }
 
         super.onCreate(savedInstanceState);
 
+        mViewFlipper = (ViewFlipper) findViewById(R.id.browser_actionbar);
+        mActionBar = (ActionModeCompatView) findViewById(R.id.actionbar);
+
         mBrowserToolbar = (BrowserToolbar) findViewById(R.id.browser_toolbar);
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
             // Show the target URL immediately in the toolbar
             mBrowserToolbar.setTitle(intent.getDataString());
         }
 
         ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideTabsTouchListener());
         ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
@@ -596,16 +606,21 @@ abstract public class BrowserApp extends
             return;
         }
 
         if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
             mSiteIdentityPopup.dismiss();
             return;
         }
 
+        if (mActionMode != null) {
+            endActionModeCompat();
+            return;
+        }
+
         super.onBackPressed();
     }
 
     @Override
     public void onResume() {
         super.onResume();
         unregisterEventListener("Prompt:ShowTop");
     }
@@ -654,26 +669,26 @@ abstract public class BrowserApp extends
     }
 
     private void setDynamicToolbarEnabled(boolean enabled) {
         if (enabled) {
             if (mLayerView != null) {
                 mLayerView.getLayerClient().setOnMetricsChangedListener(this);
             }
             setToolbarMargin(0);
-            mHomePagerContainer.setPadding(0, mBrowserToolbar.getHeight(), 0, 0);
+            mHomePagerContainer.setPadding(0, mViewFlipper.getHeight(), 0, 0);
         } else {
             // Immediately show the toolbar when disabling the dynamic
             // toolbar.
             if (mLayerView != null) {
                 mLayerView.getLayerClient().setOnMetricsChangedListener(null);
             }
             mHomePagerContainer.setPadding(0, 0, 0, 0);
-            if (mBrowserToolbar != null) {
-                ViewHelper.setTranslationY(mBrowserToolbar, 0);
+            if (mViewFlipper != null) {
+                ViewHelper.setTranslationY(mViewFlipper, 0);
             }
         }
 
         refreshToolbarHeight();
     }
 
     private boolean isDynamicToolbarEnabled() {
         return mDynamicToolbarEnabled && !mAccessibilityEnabled;
@@ -899,38 +914,38 @@ abstract public class BrowserApp extends
 
     private void setToolbarMargin(int margin) {
         ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
         mGeckoLayout.requestLayout();
     }
 
     @Override
     public void onMetricsChanged(ImmutableViewportMetrics aMetrics) {
-        if (isHomePagerVisible() || mBrowserToolbar == null) {
+        if (isHomePagerVisible() || mViewFlipper == null) {
             return;
         }
 
         // If the page has shrunk so that the toolbar no longer scrolls, make
         // sure the toolbar is visible.
         if (aMetrics.getPageHeight() <= aMetrics.getHeight()) {
             if (mDynamicToolbarCanScroll) {
                 mDynamicToolbarCanScroll = false;
-                if (!mBrowserToolbar.isVisible()) {
+                if (mViewFlipper.getVisibility() != View.VISIBLE) {
                     ThreadUtils.postToUiThread(new Runnable() {
                         public void run() {
                             mLayerView.getLayerMarginsAnimator().showMargins(false);
                         }
                     });
                 }
             }
         } else {
             mDynamicToolbarCanScroll = true;
         }
 
-        final View toolbarLayout = mBrowserToolbar;
+        final View toolbarLayout = mViewFlipper;
         final int marginTop = Math.round(aMetrics.marginTop);
         ThreadUtils.postToUiThread(new Runnable() {
             public void run() {
                 ViewHelper.setTranslationY(toolbarLayout, marginTop - toolbarLayout.getHeight());
 
                 if (mDoorHangerPopup.isShowing()) {
                     mDoorHangerPopup.updatePopup();
                 }
@@ -956,18 +971,18 @@ abstract public class BrowserApp extends
             mLayerView.getLayerMarginsAnimator().showMargins(false);
         } else {
             mLayerView.getLayerMarginsAnimator().hideMargins(false);
         }
     }
 
     public void refreshToolbarHeight() {
         int height = 0;
-        if (mBrowserToolbar != null) {
-            height = mBrowserToolbar.getHeight();
+        if (mViewFlipper != null) {
+            height = mViewFlipper.getHeight();
         }
 
         if (!isDynamicToolbarEnabled() || isHomePagerVisible()) {
             // Use aVisibleHeight here so that when the dynamic toolbar is
             // enabled, the padding will animate with the toolbar becoming
             // visible.
             if (isDynamicToolbarEnabled()) {
                 // When the dynamic toolbar is enabled, set the padding on the
@@ -990,36 +1005,36 @@ abstract public class BrowserApp extends
     }
 
     @Override
     void toggleChrome(final boolean aShow) {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 if (aShow) {
-                    mBrowserToolbar.show();
+                    mViewFlipper.setVisibility(View.VISIBLE);
                 } else {
-                    mBrowserToolbar.hide();
+                    mViewFlipper.setVisibility(View.GONE);
                     if (hasTabsSideBar()) {
                         hideTabs();
                     }
                 }
             }
         });
 
         super.toggleChrome(aShow);
     }
 
     @Override
     void focusChrome() {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
-                mBrowserToolbar.show();
-                mBrowserToolbar.requestFocusFromTouch();
+                mViewFlipper.setVisibility(View.VISIBLE);
+                mViewFlipper.requestFocusFromTouch();
             }
         });
     }
 
     @Override
     public void refreshChrome() {
         invalidateOptionsMenu();
         updateSideBarState();
@@ -1972,23 +1987,23 @@ abstract public class BrowserApp extends
 
     @Override
     public void setFullScreen(final boolean fullscreen) {
         super.setFullScreen(fullscreen);
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 if (fullscreen) {
-                    mBrowserToolbar.hide();
+                    mViewFlipper.setVisibility(View.GONE);
                     if (isDynamicToolbarEnabled()) {
                         mLayerView.getLayerMarginsAnimator().hideMargins(true);
                         mLayerView.getLayerMarginsAnimator().setMaxMargins(0, 0, 0, 0);
                     }
                 } else {
-                    mBrowserToolbar.show();
+                    mViewFlipper.setVisibility(View.VISIBLE);
                     if (isDynamicToolbarEnabled()) {
                         mLayerView.getLayerMarginsAnimator().showMargins(true);
                         mLayerView.getLayerMarginsAnimator().setMaxMargins(0, mToolbarHeight, 0, 0);
                     }
                 }
             }
         });
     }
@@ -2460,9 +2475,43 @@ abstract public class BrowserApp extends
         if (AppConstants.MOZ_UPDATER) {
             Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER);
             return true;
         }
 
         Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
         return false;
     }
+
+    /* Implementing ActionModeCompat.Presenter */
+    @Override
+    public void startActionModeCompat(final ActionModeCompat.Callback callback) {
+        // If actionMode is null, we're not currently showing one. Flip to the action mode view
+        if (mActionMode == null) {
+            mViewFlipper.showNext();
+            LayerMarginsAnimator margins = mLayerView.getLayerMarginsAnimator();
+            margins.setMaxMargins(0, mViewFlipper.getHeight(), 0, 0);
+            margins.setMarginsPinned(true);
+            margins.showMargins(false);
+        } else {
+            // Otherwise, we're already showing an action mode. Just finish it and show the new one
+            mActionMode.finish();
+        }
+
+        mActionMode = new ActionModeCompat(BrowserApp.this, callback, mActionBar);
+        if (callback.onCreateActionMode(mActionMode, mActionMode.getMenu())) {
+            mActionMode.invalidate();
+        }
+    }
+
+    /* Implementing ActionModeCompat.Presenter */
+    @Override
+    public void endActionModeCompat() {
+        if (mActionMode == null) {
+            return;
+        }
+
+        mActionMode.finish();
+        mActionMode = null;
+        mLayerView.getLayerMarginsAnimator().setMarginsPinned(false);
+        mViewFlipper.showPrevious();
+    }
 }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -309,19 +309,26 @@ public class Tab {
         mContentType = (contentType == null) ? "" : contentType;
     }
 
     public String getContentType() {
         return mContentType;
     }
 
     public synchronized void updateTitle(String title) {
-        // Keep the title unchanged while entering reader mode
-        if (mEnteringReaderMode)
+        // Keep the title unchanged while entering reader mode.
+        if (mEnteringReaderMode) {
             return;
+        }
+
+        // If there was a title, but it hasn't changed, do nothing.
+        if (mTitle != null &&
+            TextUtils.equals(mTitle, title)) {
+            return;
+        }
 
         mTitle = (title == null ? "" : title);
         Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.TITLE);
     }
 
     public void setState(int state) {
         mState = state;
 
@@ -613,25 +620,26 @@ public class Tab {
             // If we weren't at the last history entry, mHistoryIndex may have become too small
             if (mHistoryIndex < -1)
                 mHistoryIndex = -1;
         }
     }
 
     void handleLocationChange(JSONObject message) throws JSONException {
         final String uri = message.getString("uri");
-        mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(mUrl, uri);
+        final String oldUrl = getURL();
+        mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri);
         updateURL(uri);
         updateUserSearch(message.getString("userSearch"));
 
         mBaseDomain = message.optString("baseDomain");
         if (message.getBoolean("sameDocument")) {
             // We can get a location change event for the same document with an anchor tag
             // Notify listeners so that buttons like back or forward will update themselves
-            Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, uri);
+            Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
             return;
         }
 
         setContentType(message.getString("contentType"));
         clearFavicon();
         setHasFeeds(false);
         updateTitle(null);
         updateIdentityData(null);
@@ -641,17 +649,17 @@ public class Tab {
         setBackgroundColor(DEFAULT_BACKGROUND_COLOR);
         setErrorType(ErrorType.NONE);
 
         final String homePage = message.getString("aboutHomePage");
         if (!TextUtils.isEmpty(homePage)) {
             setAboutHomePage(HomePager.Page.valueOf(homePage));
         }
 
-        Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, uri);
+        Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl);
     }
 
     private static boolean shouldShowProgress(final String url) {
         return AboutPages.isAboutHome(url) ||
                AboutPages.isAboutReader(url);
     }
 
     void handleDocumentStart(boolean showProgress, String url) {
--- a/mobile/android/base/TextSelection.java
+++ b/mobile/android/base/TextSelection.java
@@ -1,40 +1,72 @@
 /* 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/. */
 
 package org.mozilla.gecko;
 
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.gfx.BitmapUtils.BitmapLoader;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
+import org.mozilla.gecko.menu.GeckoMenu;
+import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.util.EventDispatcher;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.ActionModeCompat.Callback;
+
+import android.content.Context;
+import android.app.Activity;
+import android.graphics.drawable.Drawable;
+import android.view.Menu;
+import android.view.MenuItem;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.Timer;
+import java.util.TimerTask;
+
 import android.util.Log;
 import android.view.View;
 
 class TextSelection extends Layer implements GeckoEventListener {
     private static final String LOGTAG = "GeckoTextSelection";
 
     private final TextSelectionHandle mStartHandle;
     private final TextSelectionHandle mMiddleHandle;
     private final TextSelectionHandle mEndHandle;
     private final EventDispatcher mEventDispatcher;
 
     private float mViewLeft;
     private float mViewTop;
     private float mViewZoom;
 
+    private TextSelectionActionModeCallback mCallback;
+
+    // These timers are used to avoid flicker caused by selection handles showing/hiding quickly. For isntance
+    // when moving between single handle caret mode and two handle selection mode.
+    private Timer mActionModeTimer = new Timer("actionMode");
+    private class ActionModeTimerTask extends TimerTask {
+        @Override
+        public void run() {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    endActionMode();
+                }
+            });
+        }
+    };
+    private ActionModeTimerTask mActionModeTimerTask;
+
     TextSelection(TextSelectionHandle startHandle,
                   TextSelectionHandle middleHandle,
                   TextSelectionHandle endHandle,
                   EventDispatcher eventDispatcher,
                   GeckoApp activity) {
         mStartHandle = startHandle;
         mMiddleHandle = middleHandle;
         mEndHandle = endHandle;
@@ -42,23 +74,25 @@ class TextSelection extends Layer implem
 
         // Only register listeners if we have valid start/middle/end handles
         if (mStartHandle == null || mMiddleHandle == null || mEndHandle == null) {
             Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
         } else {
             registerEventListener("TextSelection:ShowHandles");
             registerEventListener("TextSelection:HideHandles");
             registerEventListener("TextSelection:PositionHandles");
+            registerEventListener("TextSelection:Update");
         }
     }
 
     void destroy() {
         unregisterEventListener("TextSelection:ShowHandles");
         unregisterEventListener("TextSelection:HideHandles");
         unregisterEventListener("TextSelection:PositionHandles");
+        unregisterEventListener("TextSelection:Update");
     }
 
     private TextSelectionHandle getHandle(String name) {
         if (name.equals("START")) {
             return mStartHandle;
         } else if (name.equals("MIDDLE")) {
             return mMiddleHandle;
         } else {
@@ -81,22 +115,33 @@ class TextSelection extends Layer implem
 
                         mViewLeft = 0.0f;
                         mViewTop = 0.0f;
                         mViewZoom = 0.0f;
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
                             layerView.addLayer(TextSelection.this);
                         }
+
+                        if (mActionModeTimerTask != null)
+                            mActionModeTimerTask.cancel();
+                        showActionMode(message.getJSONArray("actions"));
+                    } else if (event.equals("TextSelection:Update")) {
+                        if (mActionModeTimerTask != null)
+                            mActionModeTimerTask.cancel();
+                        showActionMode(message.getJSONArray("actions"));
                     } else if (event.equals("TextSelection:HideHandles")) {
                         LayerView layerView = GeckoAppShell.getLayerView();
                         if (layerView != null) {
                             layerView.removeLayer(TextSelection.this);
                         }
 
+                        mActionModeTimerTask = new ActionModeTimerTask();
+                        mActionModeTimer.schedule(mActionModeTimerTask, 250);
+
                         mStartHandle.setVisibility(View.GONE);
                         mMiddleHandle.setVisibility(View.GONE);
                         mEndHandle.setVisibility(View.GONE);
                     } else if (event.equals("TextSelection:PositionHandles")) {
                         final boolean rtl = message.getBoolean("rtl");
                         final JSONArray positions = message.getJSONArray("positions");
                         for (int i=0; i < positions.length(); i++) {
                             JSONObject position = positions.getJSONObject(i);
@@ -110,16 +155,38 @@ class TextSelection extends Layer implem
                     }
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSON exception", e);
                 }
             }
         });
     }
 
+    private void showActionMode(final JSONArray items) {
+        if (mCallback != null) {
+            mCallback.updateItems(items);
+            return;
+        }
+
+        final Context context = mStartHandle.getContext();
+        if (context instanceof ActionModeCompat.Presenter) {
+            final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
+            mCallback = new TextSelectionActionModeCallback(items);
+            presenter.startActionModeCompat(mCallback);
+        }
+    }
+
+    private void endActionMode() {
+        Context context = mStartHandle.getContext();
+        if (context instanceof ActionModeCompat.Presenter) {
+            final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
+            presenter.endActionModeCompat();
+        }
+    }
+
     @Override
     public void draw(final RenderContext context) {
         // cache the relevant values from the context and bail out if they are the same. we do this
         // because this draw function gets called a lot (once per compositor frame) and we want to
         // avoid doing a lot of extra work in cases where it's not needed.
         final float viewLeft = context.viewport.left - context.offset.x;
         final float viewTop = context.viewport.top - context.offset.y;
         final float viewZoom = context.zoomFactor;
@@ -145,9 +212,77 @@ class TextSelection extends Layer implem
 
     private void registerEventListener(String event) {
         mEventDispatcher.registerEventListener(event, this);
     }
 
     private void unregisterEventListener(String event) {
         mEventDispatcher.unregisterEventListener(event, this);
     }
+
+    private class TextSelectionActionModeCallback implements Callback {
+        private JSONArray mItems;
+        private ActionModeCompat mActionMode;
+    
+        public TextSelectionActionModeCallback(JSONArray items) {
+            mItems = items;
+        }
+
+        public void updateItems(JSONArray items) {
+            mItems = items;
+            if (mActionMode != null) {
+                mActionMode.invalidate();
+            }
+        }
+
+        @Override
+        public boolean onPrepareActionMode(final ActionModeCompat mode, final Menu menu) {
+            // Android would normally expect us to only update the state of menu items here
+            // To make the js-java interaction a bit simpler, we just wipe out the menu here and recreate all
+            // the javascript menu items in onPrepare instead. This will be called any time invalidate() is called on the
+            // action mode.
+            menu.clear();
+
+            int length = mItems.length();
+            for (int i = 0; i < length; i++) {
+                try {
+                    final JSONObject obj = mItems.getJSONObject(i);
+                    final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
+                    menuitem.setShowAsAction(obj.optBoolean("showAsAction") ? 1 : 0);
+
+                    BitmapUtils.getDrawable(mStartHandle.getContext(), obj.optString("icon"), new BitmapLoader() {
+                        public void onBitmapFound(Drawable d) {
+                            if (d != null) {
+                                menuitem.setIcon(d);
+                            }
+                        }
+                    });
+                } catch(Exception ex) {
+                    Log.i(LOGTAG, "Exception building menu", ex);
+                }
+            }
+            return true;
+        }
+
+        public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) {
+            mActionMode = mode;
+            return true;
+        }
+
+        public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
+            try {
+                final JSONObject obj = mItems.getJSONObject(item.getItemId());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Action", obj.optString("id")));
+                return true;
+            } catch(Exception ex) {
+                Log.i(LOGTAG, "Exception calling action", ex);
+            }
+            return false;
+        }
+
+        // Called when the user exits the action mode
+        public void onDestroyActionMode(ActionModeCompat mode) {
+            mActionMode = null;
+            mCallback = null;
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:End", null));
+        }
+    }
 }
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -8,21 +8,21 @@ package org.mozilla.gecko.favicons;
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.cache.FaviconCache;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.NonEvictingLruCache;
 
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.support.v4.util.LruCache;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -31,17 +31,17 @@ import java.util.Set;
 
 public class Favicons {
     private static final String LOGTAG = "GeckoFavicons";
 
     // Size of the favicon bitmap cache, in bytes (Counting payload only).
     public static final int FAVICON_CACHE_SIZE_BYTES = 512 * 1024;
 
     // Number of URL mappings from page URL to Favicon URL to cache in memory.
-    public static final int PAGE_URL_MAPPINGS_TO_STORE = 128;
+    public static final int NUM_PAGE_URL_MAPPINGS_TO_STORE = 128;
 
     public static final int NOT_LOADING  = 0;
     public static final int LOADED       = 1;
     public static final int FLAG_PERSIST = 2;
     public static final int FLAG_SCALE   = 4;
 
     protected static Context sContext;
 
@@ -50,17 +50,17 @@ public class Favicons {
 
     // The density-adjusted default Favicon dimensions.
     public static int sDefaultFaviconSize;
 
     private static final Map<Integer, LoadFaviconTask> sLoadTasks = Collections.synchronizedMap(new HashMap<Integer, LoadFaviconTask>());
 
     // Cache to hold mappings between page URLs and Favicon URLs. Used to avoid going to the DB when
     // doing so is not necessary.
-    private static final LruCache<String, String> sPageURLMappings = new LruCache<String, String>(PAGE_URL_MAPPINGS_TO_STORE);
+    private static final NonEvictingLruCache<String, String> sPageURLMappings = new NonEvictingLruCache<String, String>(NUM_PAGE_URL_MAPPINGS_TO_STORE);
 
     public static String getFaviconURLForPageURLFromCache(String pageURL) {
         return sPageURLMappings.get(pageURL);
     }
 
     /**
      * Insert the given pageUrl->faviconUrl mapping into the memory cache of such mappings.
      * Useful for short-circuiting local database access.
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -367,8 +367,13 @@ just addresses the organization to follo
 <!ENTITY exit_guest_session "Exit Guest Session">
 <!ENTITY guest_session_dialog_continue "Continue">
 <!ENTITY guest_session_dialog_cancel "Cancel">
 <!ENTITY new_guest_session_title "&brandShortName; will now restart">
 <!ENTITY new_guest_session_text "The person using it will not be able to see any of your personal browsing data (like saved passwords, history or bookmarks).\n\nWhen your guest is done, their browsing data will be deleted and your session will be restored.">
 
 <!ENTITY exit_guest_session_title "&brandShortName; will now restart">
 <!ENTITY exit_guest_session_text "The browsing data from this session will be deleted.">
+
+<!-- These are only used for accessiblity for the done and overflow-menu buttons in the actionbar.
+     They are never shown to users -->
+<!ENTITY actionbar_menu "Menu">
+<!ENTITY actionbar_done "Done">
--- a/mobile/android/base/menu/GeckoMenu.java
+++ b/mobile/android/base/menu/GeckoMenu.java
@@ -61,17 +61,17 @@ public class GeckoMenu extends ListView
     /*
      * An interface for a presenter of action-items.
      * Either an Activity or a View can be a presenter, that can watch for events
      * and add/remove action-items. If not ActionItemBarPresenter, the menu uses a 
      * DefaultActionItemBar, that shows the action-items as a header over list-view.
      */
     public static interface ActionItemBarPresenter {
         // Add an action-item.
-        public void addActionItem(View actionItem);
+        public boolean addActionItem(View actionItem);
 
         // Remove an action-item.
         public void removeActionItem(View actionItem);
     }
 
     protected static final int NO_ID = 0;
 
     // List of all menu items.
@@ -146,17 +146,17 @@ public class GeckoMenu extends ListView
     }
 
     private void addItem(GeckoMenuItem menuItem) {
         menuItem.setOnShowAsActionChangedListener(this);
         mAdapter.addMenuItem(menuItem);
         mItems.add(menuItem);
     }
 
-    private void addActionItem(final GeckoMenuItem menuItem) {
+    private boolean addActionItem(final GeckoMenuItem menuItem) {
         menuItem.setOnShowAsActionChangedListener(this);
 
         if (mActionItems.size() == 0 && 
             mActionItemBarPresenter instanceof DefaultActionItemBar) {
             // Reset the adapter before adding the header view to a list.
             setAdapter(null);
             addHeaderView((DefaultActionItemBar) mActionItemBarPresenter);
             setAdapter(mAdapter);
@@ -168,16 +168,18 @@ public class GeckoMenu extends ListView
             public void onClick(View view) {
                 handleMenuItemClick(menuItem);
             }
         });
 
         mActionItems.put(menuItem, actionView);
         mActionItemBarPresenter.addActionItem(actionView);
         mItems.add(menuItem);
+
+        return true;
     }
 
     @Override
     public int addIntentOptions(int groupId, int itemId, int order, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) {
         return 0;
     }
 
     @Override
@@ -219,16 +221,22 @@ public class GeckoMenu extends ListView
             if (menuItem.hasSubMenu()) {
                 menuItem.getSubMenu().clear();
             }
         }
 
         mAdapter.clear();
 
         mItems.clear();
+
+        if (mActionItemBarPresenter != null) {
+            for (View item : mActionItems.values()) {
+                mActionItemBarPresenter.removeActionItem(item);
+            }
+        }
         mActionItems.clear();
     }
 
     @Override
     public void close() {
         if (mMenuPresenter != null)
             mMenuPresenter.closeMenu();
     }
@@ -261,17 +269,17 @@ public class GeckoMenu extends ListView
             return mItems.get(index);
 
         return null;
     }
 
     @Override
     public boolean hasVisibleItems() {
         for (GeckoMenuItem menuItem : mItems) {
-            if (menuItem.isVisible())
+            if (menuItem.isVisible() && !mActionItems.containsKey(menuItem))
                 return true;
         }
 
         return false;
     }
 
     @Override
     public boolean isShortcutKey(int keyCode, KeyEvent event) {
@@ -357,20 +365,21 @@ public class GeckoMenu extends ListView
     public boolean hasActionItemBar() {
          return (mActionItemBarPresenter != null);
     }
 
     @Override
     public void onShowAsActionChanged(GeckoMenuItem item, boolean isActionItem) {
         removeItem(item.getItemId());
 
-        if (isActionItem)
-            addActionItem(item);
-        else
-            addItem(item);
+        if (isActionItem && addActionItem(item)) {
+            return;
+        }
+
+        addItem(item);
     }
 
     public void onItemChanged(GeckoMenuItem item) {
         if (item.isActionItem()) {
            final MenuItemActionBar actionView = (MenuItemActionBar) mActionItems.get(item);
            if (actionView != null) {
                // The update could be coming from the background thread.
                // Post a runnable on the UI thread of the view for it to update.
@@ -471,21 +480,22 @@ public class GeckoMenu extends ListView
 
         public DefaultActionItemBar(Context context, AttributeSet attrs) {
             super(context, attrs);
 
             setWeightSum(3.0f);
         }
 
         @Override
-        public void addActionItem(View actionItem) {
+        public boolean addActionItem(View actionItem) {
             LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(actionItem.getLayoutParams());
             params.weight = 1.0f;
             actionItem.setLayoutParams(params);
             addView(actionItem);
+            return true;
         }
 
         @Override
         public void removeActionItem(View actionItem) {
             removeView(actionItem);
         }
     }
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -44,16 +44,17 @@ gujar.sources += [
     'util/GeckoBackgroundThread.java',
     'util/GeckoEventListener.java',
     'util/GeckoEventResponder.java',
     'util/GeckoJarReader.java',
     'util/HardwareUtils.java',
     'util/INIParser.java',
     'util/INISection.java',
     'util/JSONUtils.java',
+    'util/NonEvictingLruCache.java',
     'util/StringUtils.java',
     'util/ThreadUtils.java',
     'util/UiAsyncTask.java',
 ]
 gujar.extra_jars = [
     'gecko-mozglue.jar'
 ]
 gujar.javac_flags += ['-Xlint:all,-deprecation']
@@ -80,16 +81,18 @@ if CONFIG['MOZ_WEBRTC']:
         'gecko-util.jar',
         'gecko-mozglue.jar',
     ]
     wrjar.javac_flags += ['-Xlint:all,-deprecation,-cast']
 
 gbjar = add_java_jar('gecko-browser')
 gbjar.sources += [
     'AboutPages.java',
+    'ActionModeCompat.java',
+    'ActionModeCompatView.java',
     'ActivityHandlerHelper.java',
     'AlertNotification.java',
     'animation/AnimatorProxy.java',
     'animation/HeightChangeAnimation.java',
     'animation/PropertyAnimator.java',
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
     'ANRReporter.java',
@@ -401,31 +404,34 @@ ANDROID_RESFILES += [
     'resources/drawable-hdpi-v11/ic_menu_find_in_page.png',
     'resources/drawable-hdpi-v11/ic_menu_forward.png',
     'resources/drawable-hdpi-v11/ic_menu_new_private_tab.png',
     'resources/drawable-hdpi-v11/ic_menu_new_tab.png',
     'resources/drawable-hdpi-v11/ic_menu_quit.png',
     'resources/drawable-hdpi-v11/ic_menu_reload.png',
     'resources/drawable-hdpi-v11/ic_menu_save_as_pdf.png',
     'resources/drawable-hdpi-v11/ic_menu_settings.png',
-    'resources/drawable-hdpi-v11/ic_menu_share.png',
     'resources/drawable-hdpi-v11/ic_menu_tools.png',
     'resources/drawable-hdpi-v11/ic_status_logo.png',
+    'resources/drawable-hdpi/ab_done.png',
+    'resources/drawable-hdpi/ab_stacked_transparent_light_holo.9.png',
     'resources/drawable-hdpi/abouthome_thumbnail.png',
     'resources/drawable-hdpi/alert_addon.png',
     'resources/drawable-hdpi/alert_app.png',
     'resources/drawable-hdpi/alert_camera.png',
     'resources/drawable-hdpi/alert_download.png',
     'resources/drawable-hdpi/alert_mic.png',
     'resources/drawable-hdpi/alert_mic_camera.png',
     'resources/drawable-hdpi/arrow_popup_bg.9.png',
     'resources/drawable-hdpi/blank.png',
     'resources/drawable-hdpi/bookmark_folder_closed.png',
     'resources/drawable-hdpi/bookmark_folder_opened.png',
     'resources/drawable-hdpi/close.png',
+    'resources/drawable-hdpi/copy.png',
+    'resources/drawable-hdpi/cut.png',
     'resources/drawable-hdpi/favicon.png',
     'resources/drawable-hdpi/find_close.png',
     'resources/drawable-hdpi/find_next.png',
     'resources/drawable-hdpi/find_prev.png',
     'resources/drawable-hdpi/folder.png',
     'resources/drawable-hdpi/grid_icon_bg_activated.9.png',
     'resources/drawable-hdpi/grid_icon_bg_focused.9.png',
     'resources/drawable-hdpi/handle_end.png',
@@ -439,16 +445,17 @@ ANDROID_RESFILES += [
     'resources/drawable-hdpi/ic_menu_bookmark_add.png',
     'resources/drawable-hdpi/ic_menu_bookmark_remove.png',
     'resources/drawable-hdpi/ic_menu_character_encoding.png',
     'resources/drawable-hdpi/ic_menu_forward.png',
     'resources/drawable-hdpi/ic_menu_guest.png',
     'resources/drawable-hdpi/ic_menu_new_private_tab.png',
     'resources/drawable-hdpi/ic_menu_new_tab.png',
     'resources/drawable-hdpi/ic_menu_reload.png',
+    'resources/drawable-hdpi/ic_menu_share.png',
     'resources/drawable-hdpi/ic_status_logo.png',
     'resources/drawable-hdpi/ic_url_bar_go.png',
     'resources/drawable-hdpi/ic_url_bar_reader.png',
     'resources/drawable-hdpi/ic_url_bar_search.png',
     'resources/drawable-hdpi/ic_url_bar_star.png',
     'resources/drawable-hdpi/ic_url_bar_tab.png',
     'resources/drawable-hdpi/icon_bookmarks_empty.png',
     'resources/drawable-hdpi/icon_last_tabs.png',
@@ -461,28 +468,31 @@ ANDROID_RESFILES += [
     'resources/drawable-hdpi/icon_reading_list_empty.png',
     'resources/drawable-hdpi/larry.png',
     'resources/drawable-hdpi/lock_identified.png',
     'resources/drawable-hdpi/lock_verified.png',
     'resources/drawable-hdpi/menu.png',
     'resources/drawable-hdpi/menu_item_check.png',
     'resources/drawable-hdpi/menu_item_more.png',
     'resources/drawable-hdpi/menu_item_uncheck.png',
+    'resources/drawable-hdpi/menu_light.png',
     'resources/drawable-hdpi/menu_panel_bg.9.png',
     'resources/drawable-hdpi/menu_pb.png',
     'resources/drawable-hdpi/menu_popup_arrow_bottom.png',
     'resources/drawable-hdpi/menu_popup_arrow_top.png',
     'resources/drawable-hdpi/menu_popup_bg.9.png',
+    'resources/drawable-hdpi/paste.png',
     'resources/drawable-hdpi/pause.png',
     'resources/drawable-hdpi/pin.png',
     'resources/drawable-hdpi/play.png',
     'resources/drawable-hdpi/reader.png',
     'resources/drawable-hdpi/reader_active.png',
     'resources/drawable-hdpi/reader_cropped.png',
     'resources/drawable-hdpi/reading_list.png',
+    'resources/drawable-hdpi/select_all.png',
     'resources/drawable-hdpi/shield.png',
     'resources/drawable-hdpi/shield_doorhanger.png',
     'resources/drawable-hdpi/spinner_default.9.png',
     'resources/drawable-hdpi/spinner_focused.9.png',
     'resources/drawable-hdpi/spinner_pressed.9.png',
     'resources/drawable-hdpi/tab_close.png',
     'resources/drawable-hdpi/tab_indicator_divider.9.png',
     'resources/drawable-hdpi/tab_indicator_selected.9.png',
@@ -539,34 +549,37 @@ ANDROID_RESFILES += [
     'resources/drawable-mdpi-v11/ic_menu_find_in_page.png',
     'resources/drawable-mdpi-v11/ic_menu_forward.png',
     'resources/drawable-mdpi-v11/ic_menu_new_private_tab.png',
     'resources/drawable-mdpi-v11/ic_menu_new_tab.png',
     'resources/drawable-mdpi-v11/ic_menu_quit.png',
     'resources/drawable-mdpi-v11/ic_menu_reload.png',
     'resources/drawable-mdpi-v11/ic_menu_save_as_pdf.png',
     'resources/drawable-mdpi-v11/ic_menu_settings.png',
-    'resources/drawable-mdpi-v11/ic_menu_share.png',
     'resources/drawable-mdpi-v11/ic_menu_tools.png',
     'resources/drawable-mdpi-v11/ic_status_logo.png',
+    'resources/drawable-mdpi/ab_done.png',
+    'resources/drawable-mdpi/ab_stacked_transparent_light_holo.9.png',
     'resources/drawable-mdpi/abouthome_thumbnail.png',
     'resources/drawable-mdpi/alert_addon.png',
     'resources/drawable-mdpi/alert_app.png',
     'resources/drawable-mdpi/alert_camera.png',
     'resources/drawable-mdpi/alert_download.png',
     'resources/drawable-mdpi/alert_mic.png',
     'resources/drawable-mdpi/alert_mic_camera.png',
     'resources/drawable-mdpi/arrow_popup_bg.9.png',
     'resources/drawable-mdpi/autocomplete_list_bg.9.png',
     'resources/drawable-mdpi/blank.png',
     'resources/drawable-mdpi/bookmark_folder_closed.png',
     'resources/drawable-mdpi/bookmark_folder_opened.png',
     'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png',
     'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png',
     'resources/drawable-mdpi/close.png',
+    'resources/drawable-mdpi/copy.png',
+    'resources/drawable-mdpi/cut.png',
     'resources/drawable-mdpi/desktop_notification.png',
     'resources/drawable-mdpi/favicon.png',
     'resources/drawable-mdpi/find_close.png',
     'resources/drawable-mdpi/find_next.png',
     'resources/drawable-mdpi/find_prev.png',
     'resources/drawable-mdpi/folder.png',
     'resources/drawable-mdpi/grid_icon_bg_activated.9.png',
     'resources/drawable-mdpi/grid_icon_bg_focused.9.png',
@@ -579,16 +592,17 @@ ANDROID_RESFILES += [
     'resources/drawable-mdpi/ic_menu_bookmark_add.png',
     'resources/drawable-mdpi/ic_menu_bookmark_remove.png',
     'resources/drawable-mdpi/ic_menu_character_encoding.png',
     'resources/drawable-mdpi/ic_menu_forward.png',
     'resources/drawable-mdpi/ic_menu_guest.png',
     'resources/drawable-mdpi/ic_menu_new_private_tab.png',
     'resources/drawable-mdpi/ic_menu_new_tab.png',
     'resources/drawable-mdpi/ic_menu_reload.png',
+    'resources/drawable-mdpi/ic_menu_share.png',
     'resources/drawable-mdpi/ic_status_logo.png',
     'resources/drawable-mdpi/ic_url_bar_go.png',
     'resources/drawable-mdpi/ic_url_bar_reader.png',
     'resources/drawable-mdpi/ic_url_bar_search.png',
     'resources/drawable-mdpi/ic_url_bar_star.png',
     'resources/drawable-mdpi/ic_url_bar_tab.png',
     'resources/drawable-mdpi/icon_bookmarks_empty.png',
     'resources/drawable-mdpi/icon_last_tabs.png',
@@ -602,21 +616,23 @@ ANDROID_RESFILES += [
     'resources/drawable-mdpi/larry.png',
     'resources/drawable-mdpi/lock_identified.png',
     'resources/drawable-mdpi/lock_verified.png',
     'resources/drawable-mdpi/marketplace.png',
     'resources/drawable-mdpi/menu.png',
     'resources/drawable-mdpi/menu_item_check.png',
     'resources/drawable-mdpi/menu_item_more.png',
     'resources/drawable-mdpi/menu_item_uncheck.png',
+    'resources/drawable-mdpi/menu_light.png',
     'resources/drawable-mdpi/menu_panel_bg.9.png',
     'resources/drawable-mdpi/menu_pb.png',
     'resources/drawable-mdpi/menu_popup_arrow_bottom.png',
     'resources/drawable-mdpi/menu_popup_arrow_top.png',
     'resources/drawable-mdpi/menu_popup_bg.9.png',
+    'resources/drawable-mdpi/paste.png',
     'resources/drawable-mdpi/pause.png',
     'resources/drawable-mdpi/pin.png',
     'resources/drawable-mdpi/play.png',
     'resources/drawable-mdpi/progress_spinner.png',
     'resources/drawable-mdpi/reader.png',
     'resources/drawable-mdpi/reader_active.png',
     'resources/drawable-mdpi/reader_cropped.png',
     'resources/drawable-mdpi/reading_list.png',
@@ -675,31 +691,34 @@ ANDROID_RESFILES += [
     'resources/drawable-xhdpi-v11/ic_menu_find_in_page.png',
     'resources/drawable-xhdpi-v11/ic_menu_forward.png',
     'resources/drawable-xhdpi-v11/ic_menu_new_private_tab.png',
     'resources/drawable-xhdpi-v11/ic_menu_new_tab.png',
     'resources/drawable-xhdpi-v11/ic_menu_quit.png',
     'resources/drawable-xhdpi-v11/ic_menu_reload.png',
     'resources/drawable-xhdpi-v11/ic_menu_save_as_pdf.png',
     'resources/drawable-xhdpi-v11/ic_menu_settings.png',
-    'resources/drawable-xhdpi-v11/ic_menu_share.png',
     'resources/drawable-xhdpi-v11/ic_menu_tools.png',
     'resources/drawable-xhdpi-v11/ic_status_logo.png',
+    'resources/drawable-xhdpi/ab_done.png',
+    'resources/drawable-xhdpi/ab_stacked_transparent_light_holo.9.png',
     'resources/drawable-xhdpi/abouthome_thumbnail.png',
     'resources/drawable-xhdpi/alert_addon.png',
     'resources/drawable-xhdpi/alert_app.png',
     'resources/drawable-xhdpi/alert_camera.png',
     'resources/drawable-xhdpi/alert_download.png',
     'resources/drawable-xhdpi/alert_mic.png',
     'resources/drawable-xhdpi/alert_mic_camera.png',
     'resources/drawable-xhdpi/arrow_popup_bg.9.png',
     'resources/drawable-xhdpi/blank.png',
     'resources/drawable-xhdpi/bookmark_folder_closed.png',
     'resources/drawable-xhdpi/bookmark_folder_opened.png',
     'resources/drawable-xhdpi/close.png',
+    'resources/drawable-xhdpi/copy.png',
+    'resources/drawable-xhdpi/cut.png',
     'resources/drawable-xhdpi/favicon.png',
     'resources/drawable-xhdpi/find_close.png',
     'resources/drawable-xhdpi/find_next.png',
     'resources/drawable-xhdpi/find_prev.png',
     'resources/drawable-xhdpi/folder.png',
     'resources/drawable-xhdpi/grid_icon_bg_activated.9.png',
     'resources/drawable-xhdpi/grid_icon_bg_focused.9.png',
     'resources/drawable-xhdpi/handle_end.png',
@@ -711,16 +730,17 @@ ANDROID_RESFILES += [
     'resources/drawable-xhdpi/ic_menu_bookmark_add.png',
     'resources/drawable-xhdpi/ic_menu_bookmark_remove.png',
     'resources/drawable-xhdpi/ic_menu_character_encoding.png',
     'resources/drawable-xhdpi/ic_menu_forward.png',
     'resources/drawable-xhdpi/ic_menu_guest.png',
     'resources/drawable-xhdpi/ic_menu_new_private_tab.png',
     'resources/drawable-xhdpi/ic_menu_new_tab.png',
     'resources/drawable-xhdpi/ic_menu_reload.png',
+    'resources/drawable-xhdpi/ic_menu_share.png',
     'resources/drawable-xhdpi/ic_status_logo.png',
     'resources/drawable-xhdpi/ic_url_bar_go.png',
     'resources/drawable-xhdpi/ic_url_bar_reader.png',
     'resources/drawable-xhdpi/ic_url_bar_search.png',
     'resources/drawable-xhdpi/ic_url_bar_star.png',
     'resources/drawable-xhdpi/ic_url_bar_tab.png',
     'resources/drawable-xhdpi/icon_bookmarks_empty.png',
     'resources/drawable-xhdpi/icon_last_tabs.png',
@@ -733,21 +753,23 @@ ANDROID_RESFILES += [
     'resources/drawable-xhdpi/icon_reading_list_empty.png',
     'resources/drawable-xhdpi/larry.png',
     'resources/drawable-xhdpi/lock_identified.png',
     'resources/drawable-xhdpi/lock_verified.png',
     'resources/drawable-xhdpi/menu.png',
     'resources/drawable-xhdpi/menu_item_check.png',
     'resources/drawable-xhdpi/menu_item_more.png',
     'resources/drawable-xhdpi/menu_item_uncheck.png',
+    'resources/drawable-xhdpi/menu_light.png',
     'resources/drawable-xhdpi/menu_panel_bg.9.png',
     'resources/drawable-xhdpi/menu_pb.png',
     'resources/drawable-xhdpi/menu_popup_arrow_bottom.png',
     'resources/drawable-xhdpi/menu_popup_arrow_top.png',
     'resources/drawable-xhdpi/menu_popup_bg.9.png',
+    'resources/drawable-xhdpi/paste.png',
     'resources/drawable-xhdpi/pause.png',
     'resources/drawable-xhdpi/pin.png',
     'resources/drawable-xhdpi/play.png',
     'resources/drawable-xhdpi/reader.png',
     'resources/drawable-xhdpi/reader_active.png',
     'resources/drawable-xhdpi/reader_cropped.png',
     'resources/drawable-xhdpi/reading_list.png',
     'resources/drawable-xhdpi/shield.png',
@@ -830,16 +852,17 @@ ANDROID_RESFILES += [
     'resources/layout-large-v11/browser_toolbar.xml',
     'resources/layout-large-v11/home_pager.xml',
     'resources/layout-xlarge-v11/font_size_preference.xml',
     'resources/layout-xlarge-v11/home_history_list.xml',
     'resources/layout-xlarge-v11/home_history_page.xml',
     'resources/layout-xlarge-v11/home_history_tabs_indicator.xml',
     'resources/layout-xlarge-v11/remote_tabs_child.xml',
     'resources/layout-xlarge-v11/remote_tabs_group.xml',
+    'resources/layout/actionbar.xml',
     'resources/layout/arrow_popup.xml',
     'resources/layout/autocomplete_list.xml',
     'resources/layout/autocomplete_list_item.xml',
     'resources/layout/bookmark_edit.xml',
     'resources/layout/bookmark_folder_row.xml',
     'resources/layout/bookmark_item_row.xml',
     'resources/layout/browser_search.xml',
     'resources/layout/browser_toolbar.xml',
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ed03f620f8ef9e969d0471ab76329038e25c9f0d
GIT binary patch
literal 737
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}trX+877l!}s{b%+Ad7K3vk;OpT
z1B~5HX4`=T%L*LRVR{+Pz3T~QU|@Xd>Eaj?aro`Ec>l`*630Kjzs#3xF!4t}w}D!f
zGuv7>7UvyP%^UvgHQ2XXvE$=jjS~!k28lk(6^ThP&u=|G`f|hTTU$5Zjh%n`-ILF^
zD{swP9uN}fDta}t$uJ_0K~jeQx@Non?42LGwAXAn;GcC+{?p5u&#up0aMXAmt90n8
zPWhx>Bi~s$AGjT#Zf8Eb-q>@peaH`?C!5)~{Qs-s8x>-4kmFNiw2Fq;8l%=`@783V
zJs&2m`6D&$eEqv;o40rb%vh83@WDpGv+nFmZidd@Ui&ZDL|#;3#SxCP>puvvAJy)A
z^y7srkGcy7S97Y|&p*d2>}o<d5<WKN|KYIDi~W-<lQ8*kTupkzf*t3**99#T6JGdJ
zby@Ad1?KX64Qz{h7HzQX?GSkJ;QQns?=EjSDpxLX`0%o0uI?@S^u28kI^OU)T+YrR
zclC6~W`~w%^Nk;WzG_l)#pZ(GcFE&Mmz}db_B43w2_0UZxXsi1`sFOF9*I5P5pl)x
zxXZ$bCz`xI+kQ@%);~ML>XBM`;l`h*%2f`$U-ta5`?3cc-@8X#nVEd=dHJu0)pM=i
zif7HbKP8}IM$YGb_WQnH5Elu}E9hryIyw8_&n4@*WDO57$K3n0uHG(|n|IpGr}pXY
zpKkuoZ}p0qohZ-zTR~^libX{$_VX@ZKCSQF3MF9dtCqM%l%yn<q*^5xr2;7iBLhQ2
zT>}eU1M?6=Gb;llD`R6I+seQo<o~zLC>nC}Q!>*kQ8buZnVJI;M1!qaMleu=BuGPW
rep*R+Vo@rCV@iHfs)A>3VtQ&&YGO)d;mK4RpdtoOS3j3^P6<r_He5S5
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..55f8ae5a633f19ce0bb45c19619f5d1206a000b8
GIT binary patch
literal 222
zc%17D@N?(olHy`uVBq!ia0vp^Y9P$P1|(P5zFY^S*pj^6T^Rm@;DWu&Cj&(|3p^r=
z85p>QL70(Y)*K0-AbW|YuPggqZdP^~Vb?!-B|xEUPZ!6Kh{JDhY~*YR;AwfdyZwP&
z*|vQ3=Uca43Opy-A|m)tTA}8Ng6kZY(2a9d^kz<VdH(gvOD?1DzRtePJQ4;82O1ce
z*<cK{%o~2!duM9xKcFRFJ~dM1<&N-Ge{9cqUipyngVm>BeA3EWydY~CJYD@<);T3K
F0RSlSOHu#;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0dd8865f4a9aefc688496d3f3da1a3a131adf40c
GIT binary patch
literal 199
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtK2I0Nkc@k8Z|vqhpupoA7@Q_w
z_Q@)9udww*Ggp<C9}<U3J)8UGPIOEto;Ft-sNn&FL(fIF?+fF*HB}k*vt%q^mM^%I
z!GfvFlsTc9k?Vnq!}{HcKy@G3Bi`<AXJOdFYoW@>@YUVJlYya`=fKS7H+x@B<7D{2
uAY#tUut0Xhm*Zj#;RjB!GB7lpW#pH<6t>~W*2aFI0Sun5elF{r5}E)DS3++9
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c760936ee152c1e85e625404b32fcdb1e634f079
GIT binary patch
literal 564
zc$@(=0?Yl0P)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0005|Nkl<ZXx{Ca
z&r1S96o3~|L<A2ZT|xwrF5UVd0tpEs!i#hWIz$o?b?6X8#D;`)lOkkMV*gsZyoG7l
zUA3CksX08BnO)wSZ}$C~U2i&_dNzya033h=_@e+on3W6$18<f%0ILD!F|epOfPV!5
zcm^a*e-)?$#c0kJfE>`%?i~X;TL4aF8~}H64T0_|0_f6I?A2thA>*NGfOVk!gT?28
zXR$R=e7_=dV^aVHz>kao7qXL!0(3NcPe5DIeQWX!tN~Zr^Utz?(wuK^l2WOMKsAdP
z*i_sgRD$tLQeu+p_%Vwf1Gm6ov<X=SaSqrAMlp->ftI*kQp)s!Z2|kDJL@b|mYt7t
ziIJqsYi-BdX*!pcMr*)h48)rTRGX!1*>zwScokhenQO`TU>+UI1jvbdL~(JY0UWI|
zKojUE#DVJnja37v0GA0t{0^LG_b0PlDJy`y<nAD5;SUlEYKlD9p6LTSN~+BiU<-Ij
zNSPZc@o^sm`yt(EC+e|G0#t#Um|mu(9!O@i5<yBU@oKV_C@OhA5SOTbBW4aXdpE$I
zxSjE4E^6pXKTNfKC7?@6MGt|%)&os5s{_q%9VvTCZSdt90v%fb3NiJgYtvT=HKyun
z&9Dt%<+{<i08IWLupYp)IDo$g;QmrN00-azGLcVt>!g&I;4<<60000<MNUMnLSTYj
CdhCn<
rename from mobile/android/base/resources/drawable-hdpi-v11/ic_menu_share.png
rename to mobile/android/base/resources/drawable-hdpi/ic_menu_share.png
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bb6aef1d069a14a7fc1cea9780c919c61679e4fa
GIT binary patch
literal 148
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtXipc%kc@k8uWjUIP~c#3xZ62X
z=G4p^oaPsfxfS0Jish)R3f(&WG9v>B<f$ng%bPb-7{q>%?O@GdvXR#!2`sg~g?9%;
fkuML3{9uv5c>dV$g|n2o&Vl%zu6{1-oD!M<KsYGT
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..886c49317e0b1477bfdc4a4b7601d6caf8649198
GIT binary patch
literal 337
zc$@)I0j~auP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm0003PNkl<ZXx{CW
zAq&Dl7=|7AFDxce#5N560>LEy1jCY`U^ZARR>Kgx%_P`t3$i9+wfF}t7Gb`Fvk1f8
zZF+a|zVI-PvE!b7-`zP;6cucaLjfBET-TMIw+K3*CB^~Rf``l`VHj!?=z=yliE$n5
zz{VDVIXHur823R9jCBe43hjA#6KKj#00B>@K%Ap^HdH<~K_FAUC6F{}2~1M1Z3?al
z?|fYXuahTD_F!#_Kn3(bS+YZJ;#duI3RJ;BTwJPI#%t9jFcK13ase}40*j1)Kh7Nt
zP1wYFg#r|?Nx)a!sH^}5BwOfHJXU}$q}DF109z=hwqXU>LOESxtN;ZlKmiJvFYvQa
j;BN#>4h1Mc0m}uRhhk;0b4%-u00000NkvXXu0mjfBBFkh
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..902402e26efd69430ebd4ff0d25bf075f475ce73
GIT binary patch
literal 216
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUt`JOJ0AsP4H&OFF@KtX`z`n;UO
zRyxi)%B45jY-fhsDGD$J`p@~x`AcHYL%))>rfX%`ftnZ+#1B;0djxSPdIf2wp4xRy
z>O0E`%h?)}r%d|BvSa#%7k}OzC}OZkxZC);Ykraa=hXKG>}QzwOby6yc*i$`{Q#rP
z0)7K-L+*?0FPOhD${b+a&G$hqO)N?5P~%3v5B3I939TSo7|xvK%-7W4zva{KWgxDn
LtDnm{r-UW|osUjv
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..b28b3b54f4c81d482f797f31936cbd4013c093b5
GIT binary patch
literal 552
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UOiAAEE)4(M`_JqL@;D1TB8!2v
z2N=7Z%(epwmK8Xr!}Kz=|C%$Gfq~J$)5S5w<M`VvJN=X$Mcf{)mrC}y(Z+Hl=?H_+
zB@Yhg1H}^rb~?}F+c??q4T}w9gfNe!q+-fl7PectIVq1hI_<Tlmh`>byL8u|C)H{l
zyAC^>SF~_y-*jE*Y-5;pc#8Xf)|Gv`I#udAeJ#uSw`}IR_aXCm-zVFx-TfU?|CEN<
zpZLZiS@$TQLVN$l)>)>xUHq$O6uwgEf3D3@wNyUNa*~A5?9#OTg6CNnv(0$Di!W8x
zyjPyLKH*GLm*UCaxhjS$9&P{p%)++xd9-V)YUp{Tm)df(ixamWtto1H;5UC$X6X4U
z6}BVKVsB-(nsYT-yPi(_^EoBpXp{EswIBB^Shhk-Yxb9fyNh0*|LEqpEaKDg9n#U0
zZY|FYpP(sr``~Q#1qq?{@$qsCrFTqQ_wAm))hTY)OWq&1#NTKF2BK<-YeY#(Vo9o1
za#1RfVlXl=G}JY)&^0g*F*LI>FtRc>2C}UT3_|{Y+l-<iH$NpatrA6psg<cY5J5E9
znq>q7HAsRq1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$(@O1TaS?83{1OUi1
B(#-$>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..130d442b9cbc3aab9cf070078c3a99c311787004
GIT binary patch
literal 210
zc%17D@N?(olHy`uVBq!ia0vp^QXtI11|(N{`J4k%Y)RhkE)4%caKYZ?lYt_f1s;*b
z3=G`DAk4@xYmNj^kiEpy*OmP)H@lb>Q*`~AJfKj#r;B5V$MLt<47pkZcw8>-RNBFB
z9sT`DRqon9-aFqp)QI)>r~E3IF+piXChr2S3ClmPxY-}S``U!knZ`?kxwKX+XyVd(
z!N>IGYDfOscV~+~UGF`na`W6v>)wFN50C6*cpo5Db#1z?4A3eDPgg&ebxsLQ0CY%8
A^#A|>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..74cb920f487b60595b896c04ebc6bdf9dbd746ce
GIT binary patch
literal 161
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ3{Mxwkch)?uN#UrDDW^}xU+Kk
z+sxOm7VuB;YDqhB?1MaGoTE#FB*%y7hM7MT3eH_(u;Gy{n8|Y>KsA~%ZZ0R|J_eKb
z`?L~59+bZhIe3Pvg!>ZXxw?)yTbW}g`*|MN(%8a%;rt4o2Ml?B|4s?!&2|CW$>8bg
K=d#Wzp$P!zMLW3w
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8e6a93fe4fd079ccc034d09f86ffa37a2e9e2f5e
GIT binary patch
literal 357
zc$@)c0h<1aP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F80003jNkl<ZSi@uZ
z|NlS3Kw^eb2Mjs~7#kbUq!J$b4qzY?4;}|#!z0cC0zh00<(C7oJUtu$!z@5t1;ouj
ztPOQY10@bn0AgDp79!FC{!nwWppFRx;yy|oU<mR7RIV867)~6PYXI?dv}`#Qh)pPs
z0wy4~2I5Yrnkhh>4#c`pIR<D5^&rV70<jqo^HMVrvI4Oi5Kll-53)1@h})t3IGClh
z%mtt*2!{H!8yY(8(D<fHHUv2!5bA(VBnQOP+5w;_Z~>)iB=sP66jBr<(jp4b6Co&N
zX8^H2QX=GoMhAK#<fGI9Mo8JP6q*gWNyvsEz6B)?kcV0-LSjLb1H|mqD2T`@iff=n
zu?97YVsa`Y5omP5qD^|VJusx2WFxdyJlZoFaSi|g$re{=C@D2h00000NkvXXu0mjf
DLCbr?
rename from mobile/android/base/resources/drawable-mdpi-v11/ic_menu_share.png
rename to mobile/android/base/resources/drawable-mdpi/ic_menu_share.png
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..01d681697f799729aa60bcc03878b8718ef5f705
GIT binary patch
literal 131
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzFHaZ8kch)?uW#gKP~c&9OkaNH
zNWhsXa}P0HP*CC8e>x;QVw%w1wevX{81fiqT9$oZHH(4a215zM2{s3I28IOo7eJ#J
d>KPlRN5&o6d@ywr|9haR44$rjF6*2UngC}HDAND{
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..61fd91c8808a6cc1c8bcae5d7c5db8305c92bd44
GIT binary patch
literal 203
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJNuDl_ArXg@fBgS%f7l_Z&G9uu
za&od$1arsig%QmYs-i3w$aZX9xMQvu-waQdu;!$utjwnjiwsry9Qgg@ZNw@Q1N}|e
z4H!2X3w(c2cz|UEb7bbjh5(@^n~7q4FO&~53THfIDEj!JkYS&KyriPRip31^TYB6W
zV;3iwi1&!OEasD#$0zZ@?d1WBgf00NNiqx!(q7A+auxiV2y`8Tr>mdKI;Vst0O;>T
A>;M1&
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..bb19810bc2062509e4e4968099a359ad73818728
GIT binary patch
literal 915
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I0wfs{c7_5;rX+877l!}s{b%+Ad7K3vk;OpT
z1B~5HX4`=T%L*LRVR{)}eg2UE)Hd7G#W5t~-rE@`^Mw*++UCFCY}o3;e=O?2mC4&)
zg!X1%KAN|3t6TD;yZd>&qI&-+%f&<|C7Vn4FRM<RHoskR!?ebkjBG;2??uz*J+FN}
zdjdn@qa&Td>V9+lTuY-DbaQcC;Hl7_^J>Bo6aSxQ*Pr9o5HUE)p%%%PEn4#AZmZ`p
zjl@f*8HzqVp4ai4dvZ^4%Y-M)@3UDK#W*PX?D)YVU)dg6&9m5=neSg?_x~f(J|WM=
z0yvVdew$zUy0`xok48(Onuemvk3&jr=617KMGj>c)QB)iB~H;4|3B6B)Yp%1#RG2i
zgnYdJ=fm4NZj(bCm-Xs8!|Q&yaku<`8+3!m*p+iO%jrGej$e;I@ZIVBx5H-=5+*M>
z;qB%4?$O=`CkyE;*4DGOX$Bh(J$S8O&??R${ldXJqviLRT|eH7Sa9u1T-(63?o$uT
zbju{=16*7W`8LmZ`0*aoEJ?kM{3)8h*DVly_vymX^#KnI&TxJb5(s;t?kkx%A?UEE
ziP`r4hvJ7GzMjk2tdtovrBR3Nb*YHVpE=$C9UGfByzk%Ma42CWkLD>3=TZ@%;u(jF
zK34KE$wkF82F%czTqf@#HhHVC`T7UBI+NCH64+O(+}P#RWs;i1W*BL`t?Ae^wf;Nb
zRzJDPe}CV`1N*rS3510nJa~Th+v^$|uKQHmGrBMIu$SanvkIuv>NAI$=e3iYug6r^
zH=g1WU&VDu>kzM;@(h(5DUEm2ZnF!nSo0x#;StMuJKma~+<3dD`uR7<eads5Szfnr
zS$r_}|702KwOjbNH7rgDT++|OrVycWQ|Y*0_S<G|3;v`|JG~?Yl^aUu%T~WV%n=f1
z;ol~=?Zn2rRlf_E`VI+c`V_s*l(*x0@sua_|KX*pE3Ur%22MfG>rcxc`z&&gW5e8V
zV0u+8ag8WRNi0dVN-jzTQVd20hK9NZ7P<!JA%<pF21ZuK#z3}}fkDXsZ<|px<mRVj
zrd6V7Ftsu@2O@|DTeFN{paw~hhT#0PlJdl&R0hYC{G?O`&)mfH)S%SFl*+=BsWza@
N=IQF^vd$@?2>^=;f`tG8
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..28aeaa77b7fdfd3dde728b6f19956767fe230b51
GIT binary patch
literal 242
zc%17D@N?(olHy`uVBq!ia0vp^Mj*_=1|;R|J2nC-wj^(N7l!{JxM1({$v_d#0*}aI
z1_o|n5N2eUHAey{$X?><>&kwYn_bLGrL)Krq^{o6#W5t~-rHLnc^eFPTrOI-9FZ&8
z^5)2%Eg=S{E~c=^EPU~wb&-1X$~P*j&XuX`{G_XXvC{VItV7>&uP$+9Vq)PCP;h8q
zV8q3!a<~<GNI=+d^;dO|{>8K8&Q2=1!@ZjI$>TXL%ryt=7@f1lSKVXJ`V4dmgQu&X
J%Q~loCIE)8Oo#vg
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..364b1692f3eadd2b28e8dbf3d3e83aed90b7ba14
GIT binary patch
literal 252
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=hdo^!Ln>~)y}q0GumKON!>%)S
z_d0#MdL~`7&YUlzImNCuQTc=Zp6ja?@>MXIez~`}2&5SV4m|34Y$0n?d`@=TH6V9`
zzC&Kk!*rKB3?gh1q73Q`Gx*p%fZ}tw5?;F(F+udKW8RSBe?pt_j`E{e$p?ah>raU?
zY!G^IeZLb^03*XvtqBYv3=TWFR2W1V68Iw`dJi-1YiABv%fzsXag7uQ(153mWxPOY
g8|#nlP-nD$W_*0|_Js9L)AoY6p00i_>zopr00^E>i~s-t
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d4e4d81f367d31034dc364793a430f63c7bd19db
GIT binary patch
literal 996
zc$@*`0~`E_P)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF000B4Nkl<Zc-rln
zUx<ur7{GtFvu&DIlE{UQkkls9CRWaIfg}$Jkw|i(q`g8R#&O})Ij2r-=i;~$7eeIX
zMQ+3*HXb2u{<QhirfD~wPHEG$+pL>sPH$h|%+8#h-I;H^&($~Yzxm$Z`}}=pCJYY`
z^F7Oi03ZMe00MvjAOHve3QB8h>zP`5VG-F^&iHgX9RLIX0YC)-l=}js73bWU03ZMe
z00MvjpuqP#=Vk!wfEFMH^aGv1T@e|oYXHu<c|beR7&G-1@RzC%)g=JuTnhLFXtu6C
z;3KdY_`%vPtLp7U11JJ!2k^6fd?+I4Jd-#AGyxe^Jy~}EoO3gQqd<c-b%;o}*WYeF
zVNg{Mj5`45Tm!HOm;?L+bcx771pyoa=6X%nMZ{TOGjIZ!1oW%w@oE6r11$BL27xEQ
zeGz##%FQnU_QrHQ1~R}hU|JrGs=8`vzzSd!Ffr2I3*0xY$d<Wz|8z%^Uf_3C%~s6^
zn5W+8IV)eXK$mGnZ;9*e2NsSnY`_5D6p?IA<N@ZiwnyyNmv@%DJQb0_0&A=RHW!%R
zV{!j2&?6$bn#=^vem4V?i=18;&?zE)-n=$om*@5K(hXb{kx%1!Au`%9*aWNv)_4g^
zoV+$wZUS~jHlG15*ohYvRjoMyd6@=mDZ!@Wy#0BLEF>8~URr={!1PFaAMnBeP51Kq
z4j@<R;z<C2nZin7gQ@%waM*l83i!=TAZ^ti^#-8PO4`--oC6Me0RFIXr!BtM6@XTY
z3{xWQpFI<p>V>;MO+yk7U<R-Qm{;b!XTVuYq!I~WqL&>u6dBiTVXzU{6B*kFT(U8V
z0<hfFJyoFp3-HLY!p~m&k{Ag2T;WPFzfdaxW|_MGSfKx{ZTfi;`(6cXEmHWr1l|De
zee<X`fJs&nw#D>)1s<B3XG&aepIvzslPvH6xB(1Q3qU@uua7B*KLGbEqZ=$!lTyIR
z63gKX@N31(;SvGtj+}bdHr!cm9FLK>3z3|y-wXWYWsbL0^;)$|U;!{2_}5<UYXx(M
z1GfHu1tW0Xp2zk|Y>YC&|5He%y=t-}5)#MmQnW?@U=d-JJ$G4hgsJ~At0_LI>Zzmy
zDD?d)Q}xMS>fB?dk<T+Os_LD31%O?#wcBY&<t6ZU#eW*BF#vg$Ukl8)g6gA}P3DqP
z4vz*vUEBu*0ObG}X;bA$01yBq?2nQGKmZT`j4yzCmjECD2mk_r03ZPPj^!Ienu?#G
S=-;CN0000<MNUMnLSTXcoVgbO
rename from mobile/android/base/resources/drawable-xhdpi-v11/ic_menu_share.png
rename to mobile/android/base/resources/drawable-xhdpi/ic_menu_share.png
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..930ca8d95e8bee5a1240fba645d9dab919abd734
GIT binary patch
literal 184
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=^`0({Ar-gYUf;;epuoZ6aJO@&
z%&D0-IKvGL&8p&qV;O7KuliPO#ef9XsVf~j7nA0P5b0;!;>!>VWLO^%MTkBS4Qpdy
k*uWgah!ER_Y!HLrW`>5j&J(J5&IW+Ep00i_>zopr04&WXxBvhE
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6edd4b222ef06eeb46acd9c90ff81bcee1df867f
GIT binary patch
literal 442
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEU~KesaSW-r_4byb_hAQ_wvS>l
zE&_!Y7Ovi5(J_htq1K}2$+vl%c6e|%d9XXi#IRU%_~|`jFDT)+=v3g-Q|f5uP-+sm
ze}MPVlq1%Ap3G}*-?q^A<@!IDS#F)%v_|xE7BkSMrEwEfI=jEi2A|G4Ww$T<`Kzz%
zt^F>WR5W;A4OQO1ZU1WKuUASK7C*Z>ch`p7)2?jU6?WX0-6G_0miOM9xr@Ku{vRT}
zkeNqDwtkm=nw`Dco8327GWf(Y<Xu_0cmJ}qB<2sB7=ExfDCO^1Y^7>{dhsjqihBwr
z4SlRDHkEPD5oh5wSQmCB#<d~3G&4V&b;AEOYga|OFaEp6?~1c5!>8ao^JW#))cg*p
z7E{h>aNYXgr9%DEb586pZgb1Yzjn=S=POv!X2V#s^6Rd@Jip=?K){hPWM|YF{~|NS
z1Qv#lGL}<1%nZs54v*arFdX1u5V*=A!63oNV8Oq_LXE+Ii2)9D|7mansS37?TTs!4
aj_c+Do=+yc(wnmk#P@Xdb6Mw<&;$UzXRU7l
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/actionbar.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <Button android:id="@+id/actionmode_title"
+            android:layout_height="fill_parent"
+            android:layout_width="wrap_content"
+            style="@style/GeckoActionBar.Title"/>
+
+    <!-- Draw a separator to the left of the title -->
+    <View android:layout_height="fill_parent"
+          android:layout_width="1dp"
+          android:layout_marginTop="10dp"
+          android:layout_marginBottom="10dp"
+          android:background="@android:color/darker_gray"/>
+
+    <LinearLayout android:id="@+id/actionbar_buttons"
+                  android:layout_height="fill_parent"
+                  android:layout_width="0dip"
+                  android:layout_weight="1"
+                  style="@style/GeckoActionBar.Buttons"/>
+
+    <ImageButton android:id="@+id/actionbar_menu"
+                 android:layout_height="fill_parent"
+                 android:layout_width="@dimen/browser_toolbar_icon_width"
+                 style="@style/GeckoActionBar.Button.MenuButton"/>
+
+</merge>
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -62,25 +62,39 @@
                      android:background="@android:color/white"
                      android:visibility="invisible"/>
 
         <!-- When focus is cleared from from BrowserToolbar's EditText to
              lower the virtual keyboard, focus will be returned to the root
              view. To make sure the EditText is not the first focusable view in
              the root view, BrowserToolbar should be specified as low in the
              view hierarchy as possible. -->
-        <org.mozilla.gecko.toolbar.BrowserToolbar
-                android:id="@id/browser_toolbar"
-                style="@style/BrowserToolbar"
+
+        <ViewFlipper android:id="@+id/browser_actionbar"
                 android:layout_width="fill_parent"
                 android:layout_height="@dimen/browser_toolbar_height"
                 android:clickable="true"
-                android:focusable="true"
+                android:focusable="true">
+
+            <org.mozilla.gecko.toolbar.BrowserToolbar
+                android:id="@id/browser_toolbar"
+                    style="@style/BrowserToolbar"
+                    android:layout_width="fill_parent"
+                    android:layout_height="@dimen/browser_toolbar_height"
+                    android:clickable="true"
+                    android:focusable="true"
                 android:background="@drawable/url_bar_bg"/>
 
+            <org.mozilla.gecko.ActionModeCompatView android:id="@+id/actionbar"
+                                                    android:layout_height="fill_parent"
+                                                    android:layout_width="fill_parent"
+                                                    style="@style/GeckoActionBar"/>
+
+        </ViewFlipper>
+
     </view>
 
     <LinearLayout android:id="@+id/toast"
                   style="@style/Toast">
 
         <TextView android:id="@+id/toast_message"
                   style="@style/ToastMessage" />
 
--- a/mobile/android/base/resources/values-v11/styles.xml
+++ b/mobile/android/base/resources/values-v11/styles.xml
@@ -69,9 +69,25 @@
     <style name="Widget.Spinner" parent="android:style/Widget.Holo.Light.Spinner">
         <item name="android:minWidth">@dimen/doorhanger_input_width</item>
     </style>
 
     <style name="Widget.TextView.SpinnerItem" parent="android:style/Widget.Holo.Light.TextView.SpinnerItem">
         <item name="android:textColor">#FF000000</item>
     </style>
 
+    <style name="GeckoActionBar" parent="@android:style/Widget.Holo.Light.ActionMode">
+        <item name="android:background">@drawable/ab_stacked_transparent_light_holo</item>
+    </style>
+
+    <style name="GeckoActionBar.Title" parent="@android:style/TextAppearance.Holo.Widget.ActionBar.Title">
+        <item name="android:drawableLeft">?android:attr/actionModeCloseDrawable</item>
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:paddingLeft">15dp</item>
+        <item name="android:paddingRight">15dp</item>
+    </style>
+
+    <style name="GeckoActionBar.Button.MenuButton" parent="android:style/Widget.Holo.Light.ActionButton.Overflow">
+        <item name="android:scaleType">center</item>
+        <item name="android:background">@android:color/transparent</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -535,9 +535,42 @@
         <item name="android:textAppearance">?android:textAppearanceSmall</item>
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:maxWidth">160dp</item>
         <item name="android:textColor">#fff</item>
         <item name="android:layout_gravity">center_vertical</item>
     </style>
 
+    <style name="GeckoActionBar">
+        <item name="android:background">@drawable/ab_stacked_transparent_light_holo</item>
+    </style>
+
+    <style name="GeckoActionBar.Title">
+        <item name="android:gravity">center_vertical</item>
+        <item name="android:minWidth">0dp</item>
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:textAppearance">@style/TextAppearance.Medium</item>
+        <item name="android:drawableLeft">@drawable/ab_done</item>
+        <item name="android:paddingLeft">15dp</item>
+        <item name="android:paddingRight">15dp</item>
+        <item name="android:contentDescription">@string/actionbar_done</item>
+    </style>
+
+    <style name="GeckoActionBar.Button" parent="Widget.MenuItemActionBar">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:gravity">center</item>
+    </style>
+
+    <style name="GeckoActionBar.Button.MenuButton">
+        <item name="android:scaleType">center</item>
+        <item name="android:src">@drawable/menu_light</item>
+        <item name="android:contentDescription">@string/actionbar_menu</item>
+        <item name="android:background">@android:color/transparent</item>
+    </style>
+
+    <style name="GeckoActionBar.Buttons">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:textColor">@color/text_color_primary</item>
+        <item name="android:gravity">right</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -77,13 +77,15 @@
         <item name="android:gridViewStyle">@style/Widget.GridView</item>
         <item name="android:textViewStyle">@style/Widget.TextView</item>
         <item name="android:spinnerStyle">@style/Widget.Spinner</item>
         <item name="bookmarksListViewStyle">@style/Widget.BookmarksListView</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
+        <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
+        <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
     </style>
 
     <style name="Gecko.Preferences" parent="GeckoPreferencesBase"/>
 
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -345,9 +345,13 @@
   <string name="exit_guest_session">&exit_guest_session;</string>
   <string name="guest_session_dialog_continue">&guest_session_dialog_continue;</string>
   <string name="guest_session_dialog_cancel">&guest_session_dialog_cancel;</string>
   <string name="new_guest_session_title">&new_guest_session_title;</string>
   <string name="new_guest_session_text">&new_guest_session_text;</string>
 
   <string name="exit_guest_session_title">&exit_guest_session_title;</string>
   <string name="exit_guest_session_text">&exit_guest_session_text;</string>
+
+  <string name="actionbar_menu">&actionbar_menu;</string>
+  <string name="actionbar_done">&actionbar_done;</string>
+
 </resources>
--- a/mobile/android/base/tests/BaseTest.java
+++ b/mobile/android/base/tests/BaseTest.java
@@ -17,16 +17,17 @@ import android.content.res.AssetManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Build;
 import android.os.SystemClock;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentActivity;
 import android.support.v4.app.FragmentManager;
 import android.text.InputType;
+import android.text.TextUtils;
 import android.test.ActivityInstrumentationTestCase2;
 import android.util.DisplayMetrics;
 import android.view.inputmethod.InputMethodManager;
 import android.view.KeyEvent;
 import android.view.View;
 import android.widget.AdapterView;
 import android.widget.Button;
 import android.widget.EditText;
@@ -830,9 +831,56 @@ abstract class BaseTest extends Activity
      * @param t Throwable to get stack trace for
      * @return Stack trace as a string
      */
     public static String getStackTraceString(Throwable t) {
         StringWriter sw = new StringWriter();
         t.printStackTrace(new PrintWriter(sw));
         return sw.toString();
     }
+
+    /**
+     * Condition class that waits for a view, and allows callers access it when done.
+     */
+    private class DescriptionCondition<T extends View> implements Condition {
+        public T mView;
+        private String mDescr;
+        private Class<T> mCls;
+
+        public DescriptionCondition(Class<T> cls, String descr) {
+            mDescr = descr;
+            mCls = cls;
+        }
+
+        @Override
+        public boolean isSatisfied() {
+            mView = findViewWithContentDescription(mCls, mDescr);
+            return (mView != null);
+        }
+    }
+
+    /**
+     * Wait for a view with the specified description .
+     */
+    public <T extends View> T waitForViewWithDescription(Class<T> cls, String description) {
+        DescriptionCondition<T> c = new DescriptionCondition<T>(cls, description);
+        waitForCondition(c, MAX_WAIT_ENABLED_TEXT_MS);
+        return c.mView;
+    }
+
+    /**
+     * Get an active view with the specified description .
+     */
+    public <T extends View> T findViewWithContentDescription(Class<T> cls, String description) {
+        for (T view : mSolo.getCurrentViews(cls)) {
+            final String descr = (String) view.getContentDescription();
+            if (TextUtils.isEmpty(descr)) {
+                continue;
+            }
+
+            if (TextUtils.equals(description, descr)) {
+                return view;
+            }
+        }
+
+        return null;
+    }
 }
--- a/mobile/android/base/tests/testAddSearchEngine.java
+++ b/mobile/android/base/tests/testAddSearchEngine.java
@@ -1,14 +1,15 @@
 package org.mozilla.gecko.tests;
 
 import org.mozilla.gecko.*;
 import android.view.View;
 import android.widget.ListAdapter;
 import android.widget.ListView;
+import android.widget.ImageView;
 import java.util.ArrayList;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 /**
  * Test adding a search engine from an input field context menu.
  * 1. Get the number of existing search engines from the SearchEngine:Data event and as displayed in about:home.
@@ -63,20 +64,21 @@ public class testAddSearchEngine extends
         waitForText(StringHelper.ROBOCOP_SEARCH_TITLE);
         verifyPageTitle(StringHelper.ROBOCOP_SEARCH_TITLE);
 
         // Used to long-tap on the search input box for the search engine to add.
         getInstrumentation().waitForIdleSync();
         mAsserter.dumpLog("Long Clicking at width = " + String.valueOf(width) + " and height = " + String.valueOf(height));
         mSolo.clickLongOnScreen(width,height);
 
-        mAsserter.ok(waitForText(ADD_SEARCHENGINE_OPTION_TEXT), "Waiting for the context menu to be opened", "The context menu was opened");
+        ImageView view = waitForViewWithDescription(ImageView.class, ADD_SEARCHENGINE_OPTION_TEXT);
+        mAsserter.isnot(view, null, "The action mode was opened");
 
         // Add the search engine
-        mSolo.clickOnText(ADD_SEARCHENGINE_OPTION_TEXT);
+        mSolo.clickOnView(view);
         waitForText("Cancel");
         clickOnButton("OK");
         mAsserter.ok(!mSolo.searchText(ADD_SEARCHENGINE_OPTION_TEXT), "Adding the Search Engine", "The add Search Engine pop-up has been closed");
         waitForText(StringHelper.ROBOCOP_SEARCH_TITLE); // Make sure the pop-up is closed and we are back at the searchengine page
 
         // Load Robocop Blank 1 again to give the time for the searchengine to be added
         // TODO: This is a potential source of intermittent oranges - it's a race condition!
         loadUrl(blankPageURL);
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -527,83 +527,95 @@ public class BrowserToolbar extends Geck
                     mActivity.refreshToolbarHeight();
                 }
             });
         }
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
-        switch(msg) {
-            case TITLE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+        final Tabs tabs = Tabs.getInstance();
+
+        // These conditions are split into three phases:
+        // * Always do first
+        // * Handling specific to the selected tab
+        // * Always do afterwards.
+
+        switch (msg) {
+            case ADDED:
+                updateTabCount(tabs.getDisplayCount());
+                break;
+            case RESTORED:
+                // TabCount fixup after OOM
+            case SELECTED:
+                updateTabCount(tabs.getDisplayCount());
+                mSwitchingTabs = true;
+                // Fall through.
+        }
+
+        if (tabs.isSelectedTab(tab)) {
+            switch (msg) {
+                case TITLE:
                     updateTitle();
-                }
-                break;
-            case START:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case START:
                     updateBackButton(canDoBack(tab));
                     updateForwardButton(canDoForward(tab));
                     Boolean showProgress = (Boolean)data;
-                    if (showProgress && tab.getState() == Tab.STATE_LOADING)
+                    if (showProgress && tab.getState() == Tab.STATE_LOADING) {
                         setProgressVisibility(true);
+                    }
                     setSecurityMode(tab.getSecurityMode());
                     setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
-                }
-                break;
-            case STOP:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case STOP:
                     updateBackButton(canDoBack(tab));
                     updateForwardButton(canDoForward(tab));
                     setProgressVisibility(false);
                     // Reset the title in case we haven't navigated to a new page yet.
                     updateTitle();
-                }
-                break;
-            case LOADED:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case SELECTED:
+                case LOAD_ERROR:
                     updateTitle();
-                }
-                break;
-            case RESTORED:
-                // TabCount fixup after OOM
-            case SELECTED:
-                updateTabCount(Tabs.getInstance().getDisplayCount());
-                mSwitchingTabs = true;
-                // fall through
-            case LOCATION_CHANGE:
-            case LOAD_ERROR:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    // Fall through.
+                case LOCATION_CHANGE:
+                    // A successful location change will cause Tab to notify
+                    // us of a title change, so we don't update the title here.
                     refresh();
-                }
-                mSwitchingTabs = false;
-                break;
-            case CLOSED:
-            case ADDED:
-                updateTabCount(Tabs.getInstance().getDisplayCount());
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case CLOSED:
+                case ADDED:
                     updateBackButton(canDoBack(tab));
                     updateForwardButton(canDoForward(tab));
-                }
-                break;
-            case FAVICON:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case FAVICON:
                     setFavicon(tab.getFavicon());
-                }
-                break;
-            case SECURITY_CHANGE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case SECURITY_CHANGE:
                     setSecurityMode(tab.getSecurityMode());
-                }
-                break;
-            case READER_ENABLED:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
+                    break;
+
+                case READER_ENABLED:
                     setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
-                }
-                break;
+                    break;
+            }
+        }
+
+        switch (msg) {
+            case SELECTED:
+            case LOAD_ERROR:
+            case LOCATION_CHANGE:
+                mSwitchingTabs = false;
         }
     }
 
     public boolean isVisible() {
         return ViewHelper.getTranslationY(this) == 0;
     }
 
     public void setNextFocusDownId(int nextId) {
@@ -1450,37 +1462,37 @@ public class BrowserToolbar extends Geck
                       mUrlBarViewOffset);
             anim.attach(mSiteSecurity,
                       PropertyAnimator.Property.TRANSLATION_X,
                       mUrlBarViewOffset);
         }
     }
 
     @Override
-    public void addActionItem(View actionItem) {
+    public boolean addActionItem(View actionItem) {
         mActionItemBar.addView(actionItem);
+        return true;
     }
 
     @Override
     public void removeActionItem(View actionItem) {
         mActionItemBar.removeView(actionItem);
     }
 
     public void show() {
         setVisibility(View.VISIBLE);
     }
 
     public void hide() {
         setVisibility(View.GONE);
     }
 
-    public void refresh() {
+    private void refresh() {
         Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab != null) {
-            updateTitle();
             setFavicon(tab.getFavicon());
             setProgressVisibility(tab.getState() == Tab.STATE_LOADING);
             setSecurityMode(tab.getSecurityMode());
             setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
             updateBackButton(canDoBack(tab));
             updateForwardButton(canDoForward(tab));
 
             final boolean isPrivate = tab.isPrivate();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/util/NonEvictingLruCache.java
@@ -0,0 +1,44 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.util;
+
+import android.support.v4.util.LruCache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An LruCache that also supports a set of items that will never be evicted.
+ *
+ * Alas, LruCache is final, so we compose rather than inherit.
+ */
+public class NonEvictingLruCache<K, V> {
+    private final ConcurrentHashMap<K, V> permanent = new ConcurrentHashMap<K, V>();
+    private final LruCache<K, V> evictable;
+
+    public NonEvictingLruCache(final int evictableSize) {
+        evictable = new LruCache<K, V>(evictableSize);
+    }
+
+    public V get(K key) {
+        V val = permanent.get(key);
+        if (val == null) {
+            return evictable.get(key);
+        }
+        return val;
+    }
+
+    public void putWithoutEviction(K key, V value) {
+        permanent.put(key, value);
+    }
+
+    public void put(K key, V value) {
+        evictable.put(key, value);
+    }
+
+    public void evictAll() {
+        evictable.evictAll();
+    }
+}
--- a/mobile/android/base/widget/GeckoPopupMenu.java
+++ b/mobile/android/base/widget/GeckoPopupMenu.java
@@ -60,16 +60,19 @@ public class GeckoPopupMenu implements G
         mMenu = new GeckoMenu(context, null);
         mMenu.setCallback(this);
         mMenu.setMenuPresenter(this);
         mMenuInflater = new GeckoMenuInflater(context);
 
         mMenuPopup = new MenuPopup(context);
         mMenuPanel = new MenuPanel(context, null);
 
+        mMenuPanel.addView(mMenu);
+        mMenuPopup.setPanelView(mMenuPanel);
+
         setAnchor(anchor);
     }
 
     /**
      * Returns the menu that is current being shown.
      *
      * @return The menu being shown.
      */
@@ -90,18 +93,16 @@ public class GeckoPopupMenu implements G
      * Inflates a menu resource to the menu using the menu inflater.
      *
      * @param menuRes The menu resource to be inflated.
      */
     public void inflate(int menuRes) {
         if (menuRes > 0) {
             mMenuInflater.inflate(menuRes, mMenu);
         }
-        mMenuPanel.addView(mMenu);
-        mMenuPopup.setPanelView(mMenuPanel);
     }
 
     /**
      * Set a different anchor after the menu is inflated.
      *
      * @param anchor The new anchor for the popup.
      */
     public void setAnchor(View anchor) {
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ b/mobile/android/chrome/content/SelectionHandler.js
@@ -44,31 +44,33 @@ var SelectionHandler = {
     return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
                                                     getInterface(Ci.nsIDOMWindowUtils);
   },
 
   _isRTL: false,
 
   _addObservers: function sh_addObservers() {
     Services.obs.addObserver(this, "Gesture:SingleTap", false);
-    Services.obs.addObserver(this, "Window:Resize", false);
     Services.obs.addObserver(this, "Tab:Selected", false);
     Services.obs.addObserver(this, "after-viewport-change", false);
     Services.obs.addObserver(this, "TextSelection:Move", false);
     Services.obs.addObserver(this, "TextSelection:Position", false);
+    Services.obs.addObserver(this, "TextSelection:End", false);
+    Services.obs.addObserver(this, "TextSelection:Action", false);
     BrowserApp.deck.addEventListener("compositionend", this, false);
   },
 
   _removeObservers: function sh_removeObservers() {
     Services.obs.removeObserver(this, "Gesture:SingleTap");
-    Services.obs.removeObserver(this, "Window:Resize");
     Services.obs.removeObserver(this, "Tab:Selected");
     Services.obs.removeObserver(this, "after-viewport-change");
     Services.obs.removeObserver(this, "TextSelection:Move");
     Services.obs.removeObserver(this, "TextSelection:Position");
+    Services.obs.removeObserver(this, "TextSelection:End");
+    Services.obs.removeObserver(this, "TextSelection:Action");
     BrowserApp.deck.removeEventListener("compositionend", this);
   },
 
   observe: function sh_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "Gesture:SingleTap": {
         if (this._activeType == this.TYPE_SELECTION) {
           let data = JSON.parse(aData);
@@ -79,27 +81,27 @@ var SelectionHandler = {
         } else if (this._activeType == this.TYPE_CURSOR) {
           // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
           // We're guaranteed to call this first, because this observer was added last
           this._deactivate();
         }
         break;
       }
       case "Tab:Selected":
+      case "TextSelection:End":
         this._closeSelection();
         break;
-
-      case "Window:Resize": {
-        if (this._activeType == this.TYPE_SELECTION) {
-          // Knowing when the page is done drawing is hard, so let's just cancel
-          // the selection when the window changes. We should fix this later.
-          this._closeSelection();
+      case "TextSelection:Action":
+        for (let type in this.actions) {
+          if (this.actions[type].id == aData) {
+            this.actions[type].action(this._targetElement);
+            break;
+          }
         }
         break;
-      }
       case "after-viewport-change": {
         if (this._activeType == this.TYPE_SELECTION) {
           // Update the cache after the viewport changes (e.g. panning, zooming).
           this._updateCacheForSelection();
         }
         break;
       }
       case "TextSelection:Move": {
@@ -224,26 +226,34 @@ var SelectionHandler = {
     // Clear out any existing active selection
     this._closeSelection();
 
     this._initTargetInfo(aElement);
 
     // Clear any existing selection from the document
     this._contentWindow.getSelection().removeAllRanges();
 
+    // If we didn't have any coordinates to associate with this event (for instance, selectAll is chosen from
+    // the actionMode), set them to a point inside the top left corner of the target
+    if (aX == undefined || aY == undefined) {
+      let rect = this._targetElement.getBoundingClientRect();
+      aX = rect.left + 1;
+      aY = rect.top  + 1;
+    }
+
     if (!this._domWinUtils.selectAtPoint(aX, aY, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
       this._deactivate();
-      return;
+      return false;
     }
 
     let selection = this._getSelection();
     // If the range didn't have any text, let's bail
-    if (!selection || selection.rangeCount == 0) {
+    if (!selection || selection.rangeCount == 0 || selection.getRangeAt(0).collapsed) {
       this._deactivate();
-      return;
+      return false;
     }
 
     // Add a listener to end the selection if it's removed programatically
     selection.QueryInterface(Ci.nsISelectionPrivate).addSelectionListener(this);
     this._activeType = this.TYPE_SELECTION;
 
     // Initialize the cache
     this._cache = { start: {}, end: {}};
@@ -269,46 +279,167 @@ var SelectionHandler = {
       let dy = Math.abs(selectposY - clickY);
       distance = dx + dy;
     }
 
     let maxSelectionDistance = Services.prefs.getIntPref("browser.ui.selection.distance");
     // Do not select text far away from where the user clicked
     if (distance > maxSelectionDistance) {
       this._closeSelection();
-      return;
+      return false;
     }
 
     this._positionHandles(positions);
+    this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END], aX, aY);
+    return true;
+  },
+
+
+  /* Reads a value from an action. If the action defines the value as a function, will return the result of calling
+     the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
+  _getValue: function(obj, name, defaultValue) {
+    if (!(name in obj))
+      return defaultValue;
+
+    if (typeof obj[name] == "function")
+      return obj[name](this._targetElement);
+
+    return obj[name];
+  },
+
+  _sendMessage: function(type, handles, aX, aY) {
+    let actions = [];
+    for (let type in this.actions) {
+      let action = this.actions[type];
+      if (action.selector.matches(this._targetElement, aX, aY)) {
+        let a = {
+          id: action.id,
+          label: this._getValue(action, "label", ""),
+          icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
+          showAsAction: this._getValue(action, "showAsAction", true),
+        };
+        actions.push(a);
+      }
+    }
 
     sendMessageToJava({
-      type: "TextSelection:ShowHandles",
-      handles: [this.HANDLE_TYPE_START, this.HANDLE_TYPE_END]
+      type: type,
+      handles: handles,
+      actions: actions,
     });
   },
 
+  _updateMenu: function() {
+    this._sendMessage("TextSelection:Update");
+  },
+
+  actions: {
+    SELECT_ALL: {
+      label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
+      id: "selectall_action",
+      icon: "drawable://select_all",
+      action: function(aElement) {
+        SelectionHandler.selectAll(aElement);
+      },
+      selector: ClipboardHelper.selectAllContext,
+    },
+
+    CUT: {
+      label: Strings.browser.GetStringFromName("contextmenu.cut"),
+      id: "cut_action",
+      icon: "drawable://cut",
+      action: function(aElement) {
+        let start = aElement.selectionStart;
+        let end   = aElement.selectionEnd;
+
+        SelectionHandler.copySelection();
+        aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
+
+        SelectionHandler._updateMenu();
+      },
+      selector: ClipboardHelper.cutContext,
+    },
+
+    COPY: {
+      label: Strings.browser.GetStringFromName("contextmenu.copy"),
+      id: "copy_action",
+      icon: "drawable://copy",
+      action: function() {
+        SelectionHandler.copySelection();
+        SelectionHandler._updateMenu();
+      },
+      selector: ClipboardHelper.getCopyContext(false)
+    },
+
+    PASTE: {
+      label: Strings.browser.GetStringFromName("contextmenu.paste"),
+      id: "paste_action",
+      icon: "drawable://paste",
+      action: function(aElement) {
+        ClipboardHelper.paste(aElement);
+        SelectionHandler._positionHandles();
+        SelectionHandler._updateMenu();
+      },
+      selector: ClipboardHelper.pasteContext,
+    },
+
+    SHARE: {
+      label: Strings.browser.GetStringFromName("contextmenu.share"),
+      id: "share_action",
+      icon: "drawable://ic_menu_share",
+      action: function() {
+        SelectionHandler.shareSelection();
+        SelectionHandler._closeSelection();
+      },
+      showAsAction: function(aElement) {
+        return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
+      },
+      selector: ClipboardHelper.shareContext,
+    },
+
+    SEARCH: {
+      label: function() {
+        return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
+      },
+      id: "search_action",
+      icon: "drawable://ic_url_bar_search",
+      showAsAction: function(aElement) {
+        return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
+      },
+      action: function() {
+        SelectionHandler.searchSelection();
+        SelectionHandler._closeSelection();
+      },
+      selector: ClipboardHelper.searchWithContext,
+    },
+
+  },
+
   /*
    * Called by BrowserEventHandler when the user taps in a form input.
    * Initializes SelectionHandler and positions the caret handle.
    *
    * @param aX, aY tap location in client coordinates.
    */
   attachCaret: function sh_attachCaret(aElement) {
+    // See if its an input element, and it isn't disabled, nor handled by Android native dialog
+    if (aElement.disabled ||
+        InputWidgetHelper.hasInputWidget(aElement) ||
+        !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
+          (aElement instanceof HTMLTextAreaElement)))
+      return;
     this._initTargetInfo(aElement);
 
     this._contentWindow.addEventListener("keydown", this, false);
     this._contentWindow.addEventListener("blur", this, true);
 
     this._activeType = this.TYPE_CURSOR;
     this._positionHandles();
 
-    sendMessageToJava({
-      type: "TextSelection:ShowHandles",
-      handles: [this.HANDLE_TYPE_MIDDLE]
-    });
+    this._sendMessage("TextSelection:ShowHandles", [this.HANDLE_TYPE_MIDDLE]);
   },
 
   _initTargetInfo: function sh_initTargetInfo(aElement) {
     this._targetElement = aElement;
     if (aElement instanceof Ci.nsIDOMNSEditableElement) {
       aElement.focus();
     }
 
@@ -349,18 +480,18 @@ var SelectionHandler = {
       return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
                                  getInterface(Ci.nsIWebNavigation).
                                  QueryInterface(Ci.nsIInterfaceRequestor).
                                  getInterface(Ci.nsISelectionDisplay).
                                  QueryInterface(Ci.nsISelectionController);
   },
 
   // Used by the contextmenu "matches" functions in ClipboardHelper
-  shouldShowContextMenu: function sh_shouldShowContextMenu(aX, aY) {
-    return (this._activeType == this.TYPE_SELECTION) && this._pointInSelection(aX, aY);
+  isSelectionActive: function sh_isSelectionActive() {
+    return (this._activeType == this.TYPE_SELECTION);
   },
 
   selectAll: function sh_selectAll(aElement, aX, aY) {
     if (this._activeType != this.TYPE_SELECTION)
       this.startSelection(aElement, aX, aY);
 
     let selectionController = this._getSelectionController();
     selectionController.selectAll();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -322,17 +322,16 @@ var BrowserApp = {
 
     NativeWindow.init();
     LightWeightThemeWebInstaller.init();
     Downloads.init();
     FormAssistant.init();
     IndexedDB.init();
     HealthReportStatusListener.init();
     XPInstallObserver.init();
-    ClipboardHelper.init();
     CharacterEncoding.init();
     ActivityObserver.init();
     WebappsUI.init();
     RemoteDebugger.init();
     Reader.init();
     UserAgentOverrides.init();
     DesktopUserAgent.init();
     Distribution.init();
@@ -2097,18 +2096,21 @@ var NativeWindow = {
                              0, aX, aY, aX, aY, false, false, false, false,
                              0, null);
         target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
         target.dispatchEvent(event);
       } else {
         this._target = null;
         BrowserEventHandler._cancelTapHighlight();
 
-        if (SelectionHandler.canSelect(target))
-          SelectionHandler.startSelection(target, aX, aY);
+        if (SelectionHandler.canSelect(target)) {
+          if (!SelectionHandler.startSelection(target, aX, aY)) {
+            SelectionHandler.attachCaret(target);
+          }
+        }
       }
     },
 
     // Actually shows the native context menu by passing a list of context menu items to
     // show to the Java.
     _show: function(aEvent) {
       let popupNode = this._target;
       this._target = null;
@@ -4383,22 +4385,17 @@ var BrowserEventHandler = {
               [x, y] = this._moveClickPoint(element, x, y);
               element = ElementTouchHelper.anyElementFromPoint(x, y);
             }
 
             this._sendMouseEvent("mousemove", element, x, y);
             this._sendMouseEvent("mousedown", element, x, y);
             this._sendMouseEvent("mouseup",   element, x, y);
 
-            // See if its an input element, and it isn't disabled, nor handled by Android native dialog
-            if (!element.disabled &&
-                !InputWidgetHelper.hasInputWidget(element) &&
-                ((element instanceof HTMLInputElement && element.mozIsTextField(false)) ||
-                (element instanceof HTMLTextAreaElement)))
-              SelectionHandler.attachCaret(element);
+            SelectionHandler.attachCaret(element);
 
             // scrollToFocusedInput does its own checks to find out if an element should be zoomed into
             BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
           } catch(e) {
             Cu.reportError(e);
           }
         }
         this._cancelTapHighlight();
@@ -6176,58 +6173,28 @@ var IndexedDB = {
     timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
   }
 };
 
 var ClipboardHelper = {
   // Recorded so search with option can be removed/replaced when default engine changed.
   _searchMenuItem: -1,
 
-  init: function() {
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copy"), ClipboardHelper.getCopyContext(false), ClipboardHelper.copy.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyAll"), ClipboardHelper.getCopyContext(true), ClipboardHelper.copy.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.selectWord"), ClipboardHelper.selectWordContext, ClipboardHelper.selectWord.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.selectAll"), ClipboardHelper.selectAllContext, ClipboardHelper.selectAll.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.share"), ClipboardHelper.shareContext, ClipboardHelper.share.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.paste"), ClipboardHelper.pasteContext, ClipboardHelper.paste.bind(ClipboardHelper));
-    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.changeInputMethod"), NativeWindow.contextmenus.textContext, ClipboardHelper.inputMethod.bind(ClipboardHelper));
-
-    // We add this contextmenu item right before the menu is built to avoid having to initialise the search service early.
-    Services.obs.addObserver(this, "before-build-contextmenu", false);
-  },
-
-  uninit: function ch_uninit() {
-    Services.obs.removeObserver(this, "before-build-contextmenu");
-  },
-
-  observe: function observe(aSubject, aTopic) {
-    if (aTopic == "before-build-contextmenu") {
-      this._setSearchMenuItem();
-    }
-  },
-
-  _setSearchMenuItem: function setSearchMenuItem() {
-    if (this._searchMenuItem) {
-      NativeWindow.contextmenus.remove(this._searchMenuItem);
-    }
-    this._searchMenuItem = NativeWindow.contextmenus.add(Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1), ClipboardHelper.searchWithContext, ClipboardHelper.searchWith.bind(ClipboardHelper));
-  },
-
   get clipboardHelper() {
     delete this.clipboardHelper;
     return this.clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
   },
 
   get clipboard() {
     delete this.clipboard;
     return this.clipboard = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
   },
 
   copy: function(aElement, aX, aY) {
-    if (SelectionHandler.shouldShowContextMenu(aX, aY)) {
+    if (SelectionHandler.isSelectionActive()) {
       SelectionHandler.copySelection();
       return;
     }
 
     let selectionStart = aElement.selectionStart;
     let selectionEnd = aElement.selectionEnd;
     if (selectionStart != selectionEnd) {
       string = aElement.value.slice(selectionStart, selectionEnd);
@@ -6264,17 +6231,17 @@ var ClipboardHelper = {
   inputMethod: function(aElement) {
     Cc["@mozilla.org/imepicker;1"].getService(Ci.nsIIMEPicker).show();
   },
 
   getCopyContext: function(isCopyAll) {
     return {
       matches: function(aElement, aX, aY) {
         // Do not show "Copy All" for normal non-input text selection.
-        if (!isCopyAll && SelectionHandler.shouldShowContextMenu(aX, aY))
+        if (!isCopyAll && SelectionHandler.isSelectionActive())
           return true;
 
         if (NativeWindow.contextmenus.textContext.matches(aElement)) {
           // Don't include "copy" for password fields.
           // mozIsTextField(true) tests for only non-password fields.
           if (aElement instanceof Ci.nsIDOMHTMLInputElement && !aElement.mozIsTextField(true))
             return false;
 
@@ -6297,46 +6264,56 @@ var ClipboardHelper = {
         return aElement.textLength > 0;
 
       return false;
     }
   },
 
   selectAllContext: {
     matches: function selectAllContextMatches(aElement, aX, aY) {
-      if (SelectionHandler.shouldShowContextMenu(aX, aY))
+      if (SelectionHandler.isSelectionActive())
         return true;
 
       if (NativeWindow.contextmenus.textContext.matches(aElement))
         return (aElement.selectionStart > 0 || aElement.selectionEnd < aElement.textLength);
 
       return false;
     }
   },
 
   shareContext: {
     matches: function shareContextMatches(aElement, aX, aY) {
-      return SelectionHandler.shouldShowContextMenu(aX, aY);
+      return SelectionHandler.isSelectionActive();
     }
   },
 
   searchWithContext: {
     matches: function searchWithContextMatches(aElement, aX, aY) {
-      return SelectionHandler.shouldShowContextMenu(aX, aY);
+      return SelectionHandler.isSelectionActive();
     }
   },
 
   pasteContext: {
     matches: function(aElement) {
       if (NativeWindow.contextmenus.textContext.matches(aElement)) {
         let flavors = ["text/unicode"];
         return ClipboardHelper.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
       }
       return false;
     }
+  },
+
+  cutContext: {
+    matches: function(aElement) {
+      let copyctx = ClipboardHelper.getCopyContext(false);
+      if (NativeWindow.contextmenus.textContext.matches(aElement)) {
+        return copyctx.matches(aElement);
+      }
+      return false;
+    }
   }
 };
 
 var CharacterEncoding = {
   _charsets: [],
 
   init: function init() {
     Services.obs.addObserver(this, "CharEncoding:Get", false);
@@ -6633,23 +6610,31 @@ var SearchEngines = {
   PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
 
   init: function init() {
     Services.obs.addObserver(this, "SearchEngines:Add", false);
     Services.obs.addObserver(this, "SearchEngines:Get", false);
     Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
     Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
     Services.obs.addObserver(this, "SearchEngines:Remove", false);
-    let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
+
     let filter = {
       matches: function (aElement) {
         return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement));
       }
     };
-    this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine);
+    SelectionHandler.actions.SEARCH_ADD = {
+      id: "add_search_action",
+      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
+      icon: "drawable://ic_url_bar_search",
+      selector: filter,
+      action: function(aElement) {
+        SearchEngines.addEngine(aElement);
+      }
+    }
   },
 
   uninit: function uninit() {
     Services.obs.removeObserver(this, "SearchEngines:Add");
     Services.obs.removeObserver(this, "SearchEngines:Get");
     Services.obs.removeObserver(this, "SearchEngines:GetVisible");
     Services.obs.removeObserver(this, "SearchEngines:SetDefault");
     Services.obs.removeObserver(this, "SearchEngines:Remove");
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -94,8 +94,14 @@ contract @mozilla.org/dom/site-specific-
 
 # PaymentsUI.js
 component {3c6c9575-f57e-427b-a8aa-57bc3cbff48f} PaymentsUI.js
 contract @mozilla.org/payment/ui-glue;1 {3c6c9575-f57e-427b-a8aa-57bc3cbff48f}
 
 # FilePicker.js
 component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
 contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
+
+# Snippets.js
+component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
+contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
+category profile-after-change Snippets @mozilla.org/snippets;1
+category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/Snippets.js
@@ -0,0 +1,233 @@
+/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
+XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
+
+const SNIPPETS_ENABLED = Services.prefs.getBoolPref("browser.snippets.enabled");
+
+// URL to fetch snippets, in the urlFormatter service format.
+const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
+
+// URL to fetch country code, a value that's cached and refreshed once per month.
+const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
+
+// Timestamp when we last updated the user's country code.
+const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
+
+// Pref where we'll cache the user's country.
+const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
+
+// How frequently we update the user's country code from the server (30 days).
+const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 86400000*30;
+
+// Should be bumped up if the snippets content format changes.
+const SNIPPETS_VERSION = 1;
+
+XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
+  let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
+  return Services.urlFormatter.formatURL(updateURL);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
+  return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
+  try {
+    return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
+  } catch (e) {
+    // Return an empty string if the country code pref isn't set yet.
+    return "";
+  }
+});
+
+XPCOMUtils.defineLazyGetter(this, "gChromeWin", function() {
+  return Services.wm.getMostRecentWindow("navigator:browser");
+});
+
+/**
+ * Updates snippet data and country code (if necessary).
+ */
+function update() {
+  // Check to see if we should update the user's country code from the geo server.
+  let lastUpdate = 0;
+  try {
+    lastUpdate = parseFloat(Services.prefs.getCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF));
+  } catch (e) {}
+
+  if (Date.now() - lastUpdate > SNIPPETS_GEO_UPDATE_INTERVAL_MS) {
+    // We should update the snippets after updating the country code,
+    // so that we can filter snippets to add to the banner.
+    updateCountryCode(updateSnippets);
+  } else {
+    updateSnippets();
+  }
+}
+
+/**
+ * Fetches the user's country code from the geo server and stores the value in a pref.
+ *
+ * @param callback function called once country code is updated
+ */
+function updateCountryCode(callback) {
+  _httpGetRequest(gGeoURL, function(responseText) {
+    // Store the country code in a pref.
+    let data = JSON.parse(responseText);
+    Services.prefs.setCharPref(SNIPPETS_COUNTRY_CODE_PREF, data.country_code);
+
+    // Set last update time.
+    Services.prefs.setCharPref(SNIPPETS_GEO_LAST_UPDATE_PREF, Date.now());
+
+    callback();
+  });
+}
+
+/**
+ * Loads snippets from snippets server, caches the response, and
+ * updates the home banner with the new set of snippets.
+ */
+function updateSnippets() {
+  _httpGetRequest(gSnippetsURL, function(responseText) {
+    cacheSnippets(responseText);
+    updateBanner(responseText);
+  });
+}
+
+/**
+ * Caches snippets server response text to `snippets.json` in profile directory.
+ *
+ * @param response responseText returned from snippets server
+ */
+function cacheSnippets(response) {
+  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+  let data = gEncoder.encode(response);
+  let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
+  promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
+}
+
+/**
+ * Loads snippets from cached `snippets.json`.
+ */
+function loadSnippetsFromCache() {
+  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+  let promise = OS.File.read(path);
+  promise.then(array => updateBanner(gDecoder.decode(array)), e => {
+    // If snippets.json doesn't exist, update data from the server.
+    if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+      update();
+    } else {
+      Cu.reportError("Error loading snippets from cache: " + e);
+    }
+  });
+}
+
+// Array of the message ids added to the home banner, used to remove
+// older set of snippets when new ones are available.
+var gMessageIds = [];
+
+/**
+ * Updates set of snippets in the home banner message rotation.
+ *
+ * @param response responseText returned from snippets server.
+ *   This should be a JSON array of message data JSON objects.
+ *   Each message object should have the following properties:
+ *     - id (?): Unique identifier for this snippets message
+ *     - text (string): Text to show as banner message
+ *     - url (string): URL to open when banner is clicked
+ *     - icon (data URI): Icon to appear in banner
+ *     - target_geo (string): Country code for where this message should be shown (e.g. "US")
+ */
+function updateBanner(response) {
+  // Remove the current messages, if there are any.
+  gMessageIds.forEach(function(id) {
+    Home.banner.remove(id);
+  })
+  gMessageIds = [];
+
+  let messages = JSON.parse(response);
+  messages.forEach(function(message) {
+    // Don't add this message to the banner if it's not supposed to be shown in this country.
+    if ("target_geo" in message && message.target_geo != gCountryCode) {
+      return;
+    }
+    let id = Home.banner.add({
+      text: message.text,
+      icon: message.icon,
+      onclick: function() {
+        gChromeWin.BrowserApp.addTab(message.url);
+      },
+      onshown: function() {
+        // XXX: 10% of the time, let the metrics server know which message was shown (bug 937373)
+      }
+    });
+    // Keep track of the message we added so that we can remove it later.
+    gMessageIds.push(id);
+  });
+}
+
+/**
+ * Helper function to make HTTP GET requests.
+ *
+ * @param url where we send the request
+ * @param callback function that is called with the xhr responseText
+ */
+function _httpGetRequest(url, callback) {
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+  try {
+    xhr.open("GET", url, true);
+  } catch (e) {
+    Cu.reportError("Error opening request to " + url + ": " + e);
+    return;
+  }
+  xhr.onerror = function onerror(e) {
+    Cu.reportError("Error making request to " + url + ": " + e.error);
+  }
+  xhr.onload = function onload(event) {
+    if (xhr.status !== 200) {
+      Cu.reportError("Request to " + url + " returned status " + xhr.status);
+      return;
+    }
+    if (callback) {
+      callback(xhr.responseText);
+    }
+  }
+  xhr.send(null);
+}
+
+function Snippets() {}
+
+Snippets.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsITimerCallback]),
+  classID: Components.ID("{a78d7e59-b558-4321-a3d6-dffe2f1e76dd}"),
+
+  observe: function(subject, topic, data) {
+    if (!SNIPPETS_ENABLED) {
+      return;
+    }
+    switch(topic) {
+      case "profile-after-change":
+        loadSnippetsFromCache();
+        break;
+    }
+  },
+
+  // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
+  notify: function(timer) {
+    if (!SNIPPETS_ENABLED) {
+      return;
+    }
+    update();
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -17,16 +17,17 @@ EXTRA_COMPONENTS += [
     'ContentPermissionPrompt.js',
     'DownloadManagerUI.js',
     'FilePicker.js',
     'LoginManagerPrompter.js',
     'NSSDialogService.js',
     'PaymentsUI.js',
     'PromptService.js',
     'SiteSpecificUserAgent.js',
+    'Snippets.js',
     'XPIDialogService.js',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'AboutRedirector.js',
     'BrowserCLH.js',
     'DirectoryProvider.js',
     'HelperAppDialog.js',
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -566,16 +566,17 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/HelperAppDialog.js
 @BINPATH@/components/LoginManagerPrompter.js
 @BINPATH@/components/MobileComponents.manifest
 @BINPATH@/components/MobileComponents.xpt
 @BINPATH@/components/NSSDialogService.js
 @BINPATH@/components/PromptService.js
 @BINPATH@/components/SessionStore.js
 @BINPATH@/components/Sidebar.js
+@BINPATH@/components/Snippets.js
 
 @BINPATH@/components/Payment.js
 @BINPATH@/components/PaymentFlowInfo.js
 @BINPATH@/components/PaymentRequestInfo.js
 @BINPATH@/components/Payment.manifest
 @BINPATH@/components/PaymentsUI.js
 
 #ifdef MOZ_SAFE_BROWSING
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -188,18 +188,17 @@ contextmenu.shareMedia=Share Video
 contextmenu.showControls2=Show Controls
 contextmenu.mute=Mute
 contextmenu.unmute=Unmute
 contextmenu.saveVideo=Save Video
 contextmenu.saveAudio=Save Audio
 contextmenu.addToContacts=Add to Contacts
 
 contextmenu.copy=Copy
-contextmenu.copyAll=Copy All
-contextmenu.selectWord=Select Word
+contextmenu.cut=Cut
 contextmenu.selectAll=Select All
 contextmenu.paste=Paste
 
 # Select UI
 selectHelper.closeMultipleSelectDialog=Done
 
 #Input widgets UI
 inputWidgetHelper.date=Pick a date
--- a/toolkit/mozapps/installer/windows/nsis/common.nsh
+++ b/toolkit/mozapps/installer/windows/nsis/common.nsh
@@ -7567,17 +7567,17 @@
         ${EndWhile}
         IntOp $7 $7 * $R1
 
         StrCpy $R0 $9
         ${Do}
           IntOp $R0 $R0 + 20
           System::Call '*(i, i, i R0, i r7) i .r6'
           System::Call 'user32::DrawTextW(i r4, t $\"$2$\", i r5, i r6, \
-                                          i $R2|${DT_WORDBREAK}|${DT_NOFULLWIDTHCHARBREAK}) i .R1'
+                                          i $R2|${DT_WORDBREAK}) i .R1'
           System::Call '*$6(i, i, i .r8, i .r9)'
           System::Free $6
         ${LoopUntil} $7 >= $R1
       ${Else}
         ; When there are newlines in the text just return the size of the
         ; rectangle for the text.
         System::Call 'user32::DrawTextW(i r4, t $\"$2$\", i r5, i r6, i $R2)'
         System::Call '*$6(i, i, i .r8, i .r9)'
--- a/widget/windows/winrt/APZController.cpp
+++ b/widget/windows/winrt/APZController.cpp
@@ -275,26 +275,30 @@ APZController::UpdateScrollOffset(const 
 #ifdef DEBUG_CONTROLLER
     WinUtils::Log("Skipping UpdateScrollOffset");
 #endif
     return;
   }
   sAPZC->UpdateScrollOffset(aScrollLayerId, aScrollOffset);
 }
 
-// Gesture event handlers from the APZC. Currently not in use.
-
 void
 APZController::HandleDoubleTap(const CSSIntPoint& aPoint)
 {
+  NS_ConvertASCIItoUTF16 data(nsPrintfCString("{ \"x\": %d, \"y\": %d }",
+                              (int32_t)aPoint.x, (int32_t)aPoint.y));
+  MetroUtils::FireObserver("Gesture:DoubleTap", data.get());
 }
 
 void
 APZController::HandleSingleTap(const CSSIntPoint& aPoint)
 {
+  NS_ConvertASCIItoUTF16 data(nsPrintfCString("{ \"x\": %d, \"y\": %d }",
+                              (int32_t)aPoint.x, (int32_t)aPoint.y));
+  MetroUtils::FireObserver("Gesture:SingleTap", data.get());
 }
 
 void
 APZController::HandleLongTap(const CSSIntPoint& aPoint)
 {
 }
 
 // requests that we send a mozbrowserasyncscroll domevent. not in use.
--- a/widget/windows/winrt/MetroInput.cpp
+++ b/widget/windows/winrt/MetroInput.cpp
@@ -658,32 +658,39 @@ MetroInput::HitTestChrome(const LayoutDe
   WidgetMouseEvent hittest(true, NS_MOUSE_MOZHITTEST, mWidget.Get(),
                            WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
   hittest.refPoint = pt;
   nsEventStatus status;
   mWidget->DispatchEvent(&hittest, status);
   return (status == nsEventStatus_eConsumeNoDefault);
 }
 
-void
+/**
+ * Returns true if the position is in chrome, false otherwise.
+ */
+bool
 MetroInput::TransformRefPoint(const Foundation::Point& aPosition, LayoutDeviceIntPoint& aRefPointOut)
 {
   // If this event is destined for content we need to transform our ref point through
   // the apz so that zoom can be accounted for.
   aRefPointOut = LayoutDeviceIntPoint::FromUntyped(MetroUtils::LogToPhys(aPosition));
   ScreenIntPoint spt;
   spt.x = aRefPointOut.x;
   spt.y = aRefPointOut.y;
   // This is currently a general contained rect hit test, it may produce a false positive for
   // overlay chrome elements.
   bool apzIntersect = mWidget->ApzHitTest(spt);
-  if (apzIntersect && HitTestChrome(aRefPointOut)) {
-    return;
+  if (!apzIntersect) {
+    return true;
+  }
+  if (HitTestChrome(aRefPointOut)) {
+    return true;
   }
   mWidget->ApzTransformGeckoCoordinate(spt, &aRefPointOut);
+  return false;
 }
 
 void
 MetroInput::TransformTouchEvent(WidgetTouchEvent* aEvent)
 {
   nsTArray< nsRefPtr<dom::Touch> >& touches = aEvent->touches;
   for (uint32_t i = 0; i < touches.Length(); ++i) {
     dom::Touch* touch = touches[i];
@@ -935,19 +942,23 @@ MetroInput::OnRightTapped(UI::Input::IGe
 }
 
 void
 MetroInput::HandleTap(const Foundation::Point& aPoint, unsigned int aTapCount)
 {
 #ifdef DEBUG_INPUT
   LogFunction();
 #endif
-  
+
   LayoutDeviceIntPoint refPoint;
-  TransformRefPoint(aPoint, refPoint);
+  bool hitTestChrome = TransformRefPoint(aPoint, refPoint);
+  if (!hitTestChrome) {
+    // Let APZC handle tap/doubletap detection for content.
+    return;
+  }
 
   // send mousemove
   WidgetMouseEvent* mouseEvent =
     new WidgetMouseEvent(true, NS_MOUSE_MOVE, mWidget.Get(),
                          WidgetMouseEvent::eReal, WidgetMouseEvent::eNormal);
   mouseEvent->refPoint = refPoint;
   mouseEvent->clickCount = aTapCount;
   mouseEvent->inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
@@ -1125,40 +1136,40 @@ MetroInput::DeliverNextQueuedTouchEvent(
    * Notes:
    * - never rely on the contents of mTouches here, since this is a delayed
    *   callback. mTouches will likely have been modified.
    */
 
   // Test for chrome vs. content target. To do this we only use the first touch
   // point since that will be the input batch target. Cache this for touch events
   // since HitTestChrome has to send a dom event.
-  if (mCancelable && event->message == NS_TOUCH_START) {
+  if (mCancelable && event->message == NS_TOUCH_START && mTouches.Count() == 1) {
     nsRefPtr<Touch> touch = event->touches[0];
     LayoutDeviceIntPoint pt = LayoutDeviceIntPoint::FromUntyped(touch->mRefPoint);
     bool apzIntersect = mWidget->ApzHitTest(mozilla::ScreenIntPoint(pt.x, pt.y));
     mChromeHitTestCacheForTouch = (apzIntersect && HitTestChrome(pt));
   }
 
   // If this event is destined for chrome, deliver it directly there bypassing
   // the apz.
-  if (!mCancelable && mChromeHitTestCacheForTouch) {
+  if (mChromeHitTestCacheForTouch) {
     DUMP_TOUCH_IDS("DOM(1)", event);
     mWidget->DispatchEvent(event, status);
     return;
   }
 
   // If we have yet to deliver the first touch start and touch move, deliver the
   // event to both content and the apz. Ignore the apz's return result since we
   // give content the option of saying it wants to consume touch for both events.
   if (mCancelable) {
     WidgetTouchEvent transformedEvent(*event);
     DUMP_TOUCH_IDS("APZC(1)", event);
     mWidget->ApzReceiveInputEvent(event, &mTargetAPZCGuid, &transformedEvent);
     DUMP_TOUCH_IDS("DOM(2)", event);
-    mWidget->DispatchEvent(mChromeHitTestCacheForTouch ? event : &transformedEvent, status);
+    mWidget->DispatchEvent(&transformedEvent, status);
     if (event->message == NS_TOUCH_START) {
       mContentConsumingTouch = (nsEventStatus_eConsumeNoDefault == status);
       // If we know content wants touch here, we can bail early on mCancelable
       // processing. This insures the apz gets an update when the user touches
       // the screen, but doesn't move soon after.
       if (mContentConsumingTouch) {
         mCancelable = false;
         mWidget->ApzContentConsumingTouch(mTargetAPZCGuid);
@@ -1191,37 +1202,33 @@ MetroInput::DeliverNextQueuedTouchEvent(
   }
 
   // If content is consuming touch, we may need to transform event coords
   // through the apzc before sending to the dom. Otherwise send the event
   // to apzc.
   if (mContentConsumingTouch) {
     // Only translate if we're dealing with web content that's transformed
     // by the apzc.
-    if (!mChromeHitTestCacheForTouch) {
-      TransformTouchEvent(event);
-    }
+    TransformTouchEvent(event);
     DUMP_TOUCH_IDS("DOM(3)", event);
     mWidget->DispatchEvent(event, status);
     return;
   }
 
   DUMP_TOUCH_IDS("APZC(2)", event);
   status = mWidget->ApzReceiveInputEvent(event, nullptr);
 
   // Send the event to content unless APZC is consuming it.
   if (!mApzConsumingTouch) {
     if (status == nsEventStatus_eConsumeNoDefault) {
       mApzConsumingTouch = true;
       DispatchTouchCancel(event);
       return;
     }
-    if (!mChromeHitTestCacheForTouch) {
-      TransformTouchEvent(event);
-    }
+    TransformTouchEvent(event);
     DUMP_TOUCH_IDS("DOM(4)", event);
     mWidget->DispatchEvent(event, status);
   }
 }
 
 void
 MetroInput::DispatchTouchCancel(WidgetTouchEvent* aEvent)
 {
--- a/widget/windows/winrt/MetroInput.h
+++ b/widget/windows/winrt/MetroInput.h
@@ -164,17 +164,17 @@ private:
   void RegisterInputEvents();
   void UnregisterInputEvents();
 
   // Hit testing for chrome content
   bool mChromeHitTestCacheForTouch;
   bool HitTestChrome(const LayoutDeviceIntPoint& pt);
 
   // Event processing helpers.  See function definitions for more info.
-  void TransformRefPoint(const Point& aPosition,
+  bool TransformRefPoint(const Point& aPosition,
                          LayoutDeviceIntPoint& aRefPointOut);
   void TransformTouchEvent(WidgetTouchEvent* aEvent);
   void OnPointerNonTouch(IPointerPoint* aPoint);
   void AddPointerMoveDataToRecognizer(IPointerEventArgs* aArgs);
   void InitGeckoMouseEventFromPointerPoint(WidgetMouseEvent* aEvent,
                                            IPointerPoint* aPoint);
   void ProcessManipulationDelta(ManipulationDelta const& aDelta,
                                 Point const& aPosition,