Merge fx-team to m-c
authorWes Kocher <wkocher@mozilla.com>
Fri, 23 May 2014 17:04:56 -0700
changeset 205010 0aead79c8716dad04e68b72d03b20f279aba5325
parent 204984 c695c2bd13ae188f7df4cd992eec6ee82140d19c (current diff)
parent 205009 d6d7351768dd98f6c06e6398437299106b11e018 (diff)
child 205030 51b7091471a969bc80a1e0cc4c08953f81181a73
push id494
push userraliiev@mozilla.com
push dateMon, 25 Aug 2014 18:42:16 +0000
treeherdermozilla-release@a3cc3e46b571 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c
browser/themes/shared/devtools/images/background-noise-toolbar.png
configure.in
mobile/android/base/locales/en-US/suggestedsites/fxmarketplace.json
mobile/android/base/locales/en-US/suggestedsites/list.txt
mobile/android/base/locales/en-US/suggestedsites/mozilla.json
mobile/android/modules/WebappManager.jsm
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -71,19 +71,16 @@ XPCOMUtils.defineLazyGetter(this, "libcu
 #endif
 
 #ifdef MOZ_CAPTIVEDETECT
 XPCOMUtils.defineLazyServiceGetter(Services, 'captivePortalDetector',
                                   '@mozilla.org/toolkit/captive-detector;1',
                                   'nsICaptivePortalDetector');
 #endif
 
-let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
-let { RootActor } = devtools.require("devtools/server/actors/root");
-
 function getContentWindow() {
   return shell.contentBrowser.contentWindow;
 }
 
 function debug(str) {
   dump(' -*- Shell.js: ' + str + '\n');
 }
 
@@ -966,16 +963,18 @@ let RemoteDebugger = {
           },
           // Use an explicit global actor list to prevent exposing
           // unexpected actors
           globalActorFactories: restrictPrivileges ? {
             webappsActor: DebuggerServer.globalActorFactories.webappsActor,
             deviceActor: DebuggerServer.globalActorFactories.deviceActor,
           } : DebuggerServer.globalActorFactories
         };
+        let devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+        let { RootActor } = devtools.require("devtools/server/actors/root");
         let root = new RootActor(connection, parameters);
         root.applicationType = "operating-system";
         return root;
       };
 
 #ifdef MOZ_WIDGET_GONK
       DebuggerServer.on("connectionchange", function() {
         AdbController.updateState();
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -273,17 +273,17 @@ pref("browser.shell.checkDefaultBrowser"
 pref("browser.shell.shortcutFavicons",true);
 
 // 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
 // The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
 pref("browser.startup.page",                1);
 pref("browser.startup.homepage",            "chrome://branding/locale/browserconfig.properties");
 
 pref("browser.slowStartup.notificationDisabled", false);
-pref("browser.slowStartup.timeThreshold", 60000);
+pref("browser.slowStartup.timeThreshold", 45000);
 pref("browser.slowStartup.maxSamples", 5);
 
 // 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.aboutHomeSnippets.updateUrl", "https://snippets.mozilla.com/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 pref("browser.enable_automatic_image_resizing", true);
@@ -1563,16 +1563,18 @@ pref("ui.key.menuAccessKeyFocuses", true
 pref("browser.cache.auto_delete_cache_version", 1);
 // Play with different values of the decay time and get telemetry,
 // 0 means to randomize (and persist) the experiment value in users' profiles,
 // -1 means no experiment is run and we use the preferred value for frecency (6h)
 pref("browser.cache.frecency_experiment", 0);
 
 pref("browser.translation.detectLanguage", false);
 pref("browser.translation.neverForLanguages", "");
+// Show the translation UI bits, like the info bar, notification icon and preferences.
+pref("browser.translation.ui.show", false);
 
 // Telemetry experiments settings.
 pref("experiments.enabled", true);
 pref("experiments.manifest.fetchIntervalSeconds", 86400);
 pref("experiments.manifest.uri", "https://telemetry-experiment.cdn.mozilla.net/manifest/v1/firefox/%VERSION%/%CHANNEL%");
 pref("experiments.manifest.certs.1.commonName", "*.cdn.mozilla.net");
 pref("experiments.manifest.certs.1.issuerName", "CN=Cybertrust Public SureServer SV CA,O=Cybertrust Inc");
 // Whether experiments are supported by the current application profile.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4423,34 +4423,31 @@ var TabsInTitlebar = {
       document.documentElement.setAttribute("tabsintitlebar", "true");
       updateTitlebarDisplay();
 
       // Try to avoid reflows in this code by calculating dimensions first and
       // then later set the properties affecting layout together in a batch.
 
       // Get the full height of the tabs toolbar:
       let tabsToolbar = $("TabsToolbar");
-      let fullTabsHeight = rect(tabsToolbar).height;
+      let tabsStyles = window.getComputedStyle(tabsToolbar);
+      let fullTabsHeight = rect(tabsToolbar).height + verticalMargins(tabsStyles);
       // Buttons first:
       let captionButtonsBoxWidth = rect($("titlebar-buttonbox-container")).width;
 
 #ifdef XP_MACOSX
       let secondaryButtonsWidth = rect($("titlebar-secondary-buttonbox")).width;
       // No need to look up the menubar stuff on OS X:
       let menuHeight = 0;
       let fullMenuHeight = 0;
-      // Instead, look up the titlebar padding:
-      let titlebarPadding = parseInt(window.getComputedStyle(titlebar).paddingTop, 10);
 #else
       // Otherwise, get the height and margins separately for the menubar
       let menuHeight = rect(menubar).height;
       let menuStyles = window.getComputedStyle(menubar);
       let fullMenuHeight = verticalMargins(menuStyles) + menuHeight;
-      let tabsStyles = window.getComputedStyle(tabsToolbar);
-      fullTabsHeight += verticalMargins(tabsStyles);
 #endif
 
       // And get the height of what's in the titlebar:
       let titlebarContentHeight = rect(titlebarContent).height;
 
       // Begin setting CSS properties which will cause a reflow
 
       // If the menubar is around (menuHeight is non-zero), try to adjust
@@ -4483,17 +4480,16 @@ var TabsInTitlebar = {
       // Next, we calculate how much we need to stretch the titlebar down to
       // go all the way to the bottom of the tab strip, if necessary.
       let tabAndMenuHeight = fullTabsHeight + fullMenuHeight;
 
       if (tabAndMenuHeight > titlebarContentHeight) {
         // We need to increase the titlebar content's outer height (ie including margins)
         // to match the tab and menu height:
         let extraMargin = tabAndMenuHeight - titlebarContentHeight;
-        // On non-OSX, we can just use bottom margin:
 #ifndef XP_MACOSX
         titlebarContent.style.marginBottom = extraMargin + "px";
 #endif
         titlebarContentHeight += extraMargin;
       }
 
       // Then we bring up the titlebar by the same amount, but we add any negative margin:
       titlebar.style.marginBottom = "-" + titlebarContentHeight + "px";
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -3699,16 +3699,17 @@ function XULWidgetSingleWrapper(aWidgetI
     }
     return node.getAttribute("overflowedItem") == "true";
   });
 
   Object.freeze(this);
 }
 
 const LAZY_RESIZE_INTERVAL_MS = 200;
+const OVERFLOW_PANEL_HIDE_DELAY_MS = 500
 
 function OverflowableToolbar(aToolbarNode) {
   this._toolbar = aToolbarNode;
   this._collapsed = new Map();
   this._enabled = true;
 
   this._toolbar.setAttribute("overflowable", "true");
   let doc = this._toolbar.ownerDocument;
@@ -3742,16 +3743,18 @@ OverflowableToolbar.prototype = {
     let window = doc.defaultView;
     window.addEventListener("resize", this);
     window.gNavToolbox.addEventListener("customizationstarting", this);
     window.gNavToolbox.addEventListener("aftercustomization", this);
 
     let chevronId = this._toolbar.getAttribute("overflowbutton");
     this._chevron = doc.getElementById(chevronId);
     this._chevron.addEventListener("command", this);
+    this._chevron.addEventListener("dragover", this);
+    this._chevron.addEventListener("dragend", this);
 
     let panelId = this._toolbar.getAttribute("overflowpanel");
     this._panel = doc.getElementById(panelId);
     this._panel.addEventListener("popuphiding", this);
     CustomizableUIInternal.addPanelCloseListeners(this._panel);
 
     CustomizableUI.addListener(this);
 
@@ -3776,42 +3779,49 @@ OverflowableToolbar.prototype = {
 
     this._disable();
 
     let window = this._toolbar.ownerDocument.defaultView;
     window.removeEventListener("resize", this);
     window.gNavToolbox.removeEventListener("customizationstarting", this);
     window.gNavToolbox.removeEventListener("aftercustomization", this);
     this._chevron.removeEventListener("command", this);
+    this._chevron.removeEventListener("dragover", this);
+    this._chevron.removeEventListener("dragend", this);
     this._panel.removeEventListener("popuphiding", this);
     CustomizableUI.removeListener(this);
     CustomizableUIInternal.removePanelCloseListeners(this._panel);
   },
 
   handleEvent: function(aEvent) {
     switch(aEvent.type) {
-      case "resize":
-        this._onResize(aEvent);
+      case "aftercustomization":
+        this._enable();
         break;
       case "command":
         if (aEvent.target == this._chevron) {
           this._onClickChevron(aEvent);
         } else {
           this._panel.hidePopup();
         }
         break;
+      case "customizationstarting":
+        this._disable();
+        break;
+      case "dragover":
+        this._showWithTimeout();
+        break;
+      case "dragend":
+        this._panel.hidePopup();
+        break;
       case "popuphiding":
         this._onPanelHiding(aEvent);
         break;
-      case "customizationstarting":
-        this._disable();
-        break;
-      case "aftercustomization":
-        this._enable();
-        break;
+      case "resize":
+        this._onResize(aEvent);
     }
   },
 
   show: function() {
     let deferred = Promise.defer();
     if (this._panel.state == "open") {
       deferred.resolve();
       return deferred.promise;
@@ -3819,18 +3829,21 @@ OverflowableToolbar.prototype = {
     let doc = this._panel.ownerDocument;
     this._panel.hidden = false;
     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
     gELS.addSystemEventListener(contextMenu, 'command', this, true);
     let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
     this._panel.openPopup(anchor || this._chevron);
     this._chevron.open = true;
 
-    this._panel.addEventListener("popupshown", function onPopupShown() {
+    let overflowableToolbarInstance = this;
+    this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
       this.removeEventListener("popupshown", onPopupShown);
+      this.addEventListener("dragover", overflowableToolbarInstance);
+      this.addEventListener("dragend", overflowableToolbarInstance);
       deferred.resolve();
     });
 
     return deferred.promise;
   },
 
   _onClickChevron: function(aEvent) {
     if (this._chevron.open) {
@@ -3838,16 +3851,18 @@ OverflowableToolbar.prototype = {
       this._chevron.open = false;
     } else {
       this.show();
     }
   },
 
   _onPanelHiding: function(aEvent) {
     this._chevron.open = false;
+    this._panel.removeEventListener("dragover", this);
+    this._panel.removeEventListener("dragend", this);
     let doc = aEvent.target.ownerDocument;
     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
     gELS.removeSystemEventListener(contextMenu, 'command', this, true);
   },
 
   onOverflow: function(aEvent) {
     if (!this._enabled ||
         (aEvent && aEvent.target != this._toolbar.customizationTarget))
@@ -4072,11 +4087,26 @@ OverflowableToolbar.prototype = {
   },
 
   getContainerFor: function(aNode) {
     if (aNode.getAttribute("overflowedItem") == "true") {
       return this._list;
     }
     return this._target;
   },
+
+  _hideTimeoutId: null,
+  _showWithTimeout: function() {
+    this.show();
+    let window = this._toolbar.ownerDocument.defaultView;
+    if (this._hideTimeoutId) {
+      window.clearTimeout(this._hideTimeoutId);
+      this._hideTimeoutId = null;
+    }
+    this._hideTimeoutId = window.setTimeout(() => {
+      if (!this._panel.firstChild.mozMatchesSelector(":hover")) {
+        this._panel.hidePopup();
+      }
+    }, OVERFLOW_PANEL_HIDE_DELAY_MS);
+  },
 };
 
 CustomizableUIInternal.initialize();
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -64,16 +64,17 @@ skip-if = e10s # Bug ?????? - test uses 
 [browser_942581_unregisterArea_keeps_placements.js]
 [browser_943683_migration_test.js]
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
 [browser_945739_showInPrivateBrowsing_customize_mode.js]
 [browser_947987_removable_default.js]
 [browser_948985_non_removable_defaultArea.js]
 [browser_952963_areaType_getter_no_area.js]
 [browser_956602_remove_special_widget.js]
+[browser_962069_drag_to_overflow_chevron.js]
 [browser_962884_opt_in_disable_hyphens.js]
 [browser_963639_customizing_attribute_non_customizable_toolbar.js]
 [browser_967000_button_charEncoding.js]
 skip-if = e10s # Bug ?????? - test uses promiseTabLoadEvent() which isn't e10s friendly.
 [browser_967000_button_feeds.js]
 skip-if = e10s # Bug ?????? - test uses promiseTabLoadEvent() which isn't e10s friendly.
 [browser_967000_button_sync.js]
 skip-if = e10s # Bug ?????? - test uses promiseTabLoadEvent() which isn't e10s friendly.
--- a/browser/components/customizableui/test/browser_938980_navbar_collapsed.js
+++ b/browser/components/customizableui/test/browser_938980_navbar_collapsed.js
@@ -26,17 +26,17 @@ add_task(function() {
   is(bookmarksToolbar.collapsed, true, "Test should start with bookmarks toolbar collapsed");
   is(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should have height=0");
   isnot(tabsToolbar.getBoundingClientRect().height, 0, "TabsToolbar should have non-zero height");
   is(navbar.collapsed, false, "The nav-bar should be shown by default");
 
   setToolbarVisibility(bookmarksToolbar, true);
   setToolbarVisibility(navbar, false);
   isnot(bookmarksToolbar.getBoundingClientRect().height, 0, "bookmarksToolbar should be visible now");
-  is(navbar.getBoundingClientRect().height, 1, "navbar should have a height=1 (due to border)");
+  ok(navbar.getBoundingClientRect().height <= 1, "navbar should have height=0 or 1 (due to border)");
   is(CustomizableUI.inDefaultState, false, "Should no longer be in default state");
 
   yield startCustomizing();
   gCustomizeMode.reset();
   yield waitForCondition(function() !gCustomizeMode.resetting);
   yield endCustomizing();
 
   is(bookmarksToolbar.collapsed, true, "Customization reset should restore collapsed-state to the bookmarks toolbar");
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let originalWindowWidth;
+
+// Drag to overflow chevron should open the overflow panel.
+add_task(function*() {
+  originalWindowWidth = window.outerWidth;
+  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+  ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar.");
+  ok(CustomizableUI.inDefaultState, "Should start in default state.");
+  let oldChildCount = navbar.customizationTarget.childElementCount;
+  window.resizeTo(400, window.outerHeight);
+  yield waitForCondition(() => navbar.hasAttribute("overflowing"));
+  ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar.");
+
+  let widgetOverflowPanel = document.getElementById("widget-overflow");
+  let panelShownPromise = promisePanelElementShown(window, widgetOverflowPanel);
+  let identityBox = document.getElementById("identity-box");
+  let overflowChevron = document.getElementById("nav-bar-overflow-button");
+  ChromeUtils.synthesizeDrop(identityBox, overflowChevron, [], null);
+  yield panelShownPromise;
+
+  ok(true, "Overflow panel is shown.");
+
+  let panelHiddenPromise = promisePanelElementHidden(window, widgetOverflowPanel);
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+  yield panelHiddenPromise;
+});
+
+add_task(function*() {
+  window.resizeTo(originalWindowWidth, window.outerHeight);
+  let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+  yield waitForCondition(() => !navbar.hasAttribute("overflowing"));
+  ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar.");
+});
--- a/browser/components/preferences/content.js
+++ b/browser/components/preferences/content.js
@@ -11,16 +11,23 @@ var gContentPane = {
   init: function ()
   {
     this._rebuildFonts();
     var menulist = document.getElementById("defaultFont");
     if (menulist.selectedIndex == -1) {
       menulist.insertItemAt(0, "", "", "");
       menulist.selectedIndex = 0;
     }
+
+    // Show translation preferences if we may:
+    const prefName = "browser.translation.ui.show";
+    if (Services.prefs.getBoolPref(prefName)) {
+      let row = document.getElementById("translationBox");
+      row.removeAttribute("hidden");
+    }
   },
 
   // UTILITY FUNCTIONS
 
   /**
    * Utility function to enable/disable the button specified by aButtonID based
    * on the value of the Boolean preference specified by aPreferenceID.
    */
--- a/browser/components/preferences/content.xul
+++ b/browser/components/preferences/content.xul
@@ -138,17 +138,17 @@
         <rows id="languagesRows">
           <row id="preferredLanguageRow">
             <label flex="1" control="chooseLanguage">&chooseLanguage.label;</label>
             <button id="chooseLanguage"
                     label="&chooseButton.label;"
                     accesskey="&chooseButton.accesskey;"
                     oncommand="gContentPane.showLanguages();"/>
           </row>
-          <row hidden="true">
+          <row id="translationBox" hidden="true">
             <checkbox id="translate" preference="browser.translation.detectLanguage" flex="1"
                       label="&translateWebPages.label;" accesskey="&translateWebPages.accesskey;"
                       onsyncfrompreference="return gContentPane.updateButtons('translateButton',
                                             'browser.translation.detectLanguage');"/>
             <button id="translateButton" label="&translateExceptions.label;"
                     oncommand="gContentPane.showTranslationExceptions();"
                     accesskey="&translateExceptions.accesskey;"/>
           </row>
--- a/browser/components/preferences/in-content/advanced.xul
+++ b/browser/components/preferences/in-content/advanced.xul
@@ -432,34 +432,24 @@
           <radio label="&certs.auto;" accesskey="&certs.auto.accesskey;"
                 value="Select Automatically"/>
           <radio label="&certs.ask;" accesskey="&certs.ask.accesskey;"
                 value="Ask Every Time"/>
         </radiogroup>
 
         <separator/>
 
-#ifdef XP_MACOSX
-        <vbox>
-#endif
         <hbox>
           <button id="viewCertificatesButton"
                   label="&viewCerts.label;" accesskey="&viewCerts.accesskey;"
                   oncommand="gAdvancedPane.showCertificates();"
                   preference="security.disable_button.openCertManager"/>
           <button id="verificationButton"
                   label="&verify2.label;" accesskey="&verify2.accesskey;"
                   oncommand="gAdvancedPane.showOCSP();"/>
-#ifdef XP_MACOSX
-        </hbox>
-        <hbox>
-#endif
           <button id="viewSecurityDevicesButton"
                   label="&viewSecurityDevices.label;" accesskey="&viewSecurityDevices.accesskey;"
                   oncommand="gAdvancedPane.showSecurityDevices();"
                   preference="security.disable_button.openDeviceManager"/>
         </hbox>
-#ifdef XP_MACOSX
-        </vbox>
-#endif
     </tabpanel>
   </tabpanels>
 </tabbox>
--- a/browser/components/preferences/in-content/content.js
+++ b/browser/components/preferences/in-content/content.js
@@ -10,16 +10,23 @@ var gContentPane = {
   init: function ()
   {
     this._rebuildFonts();
     var menulist = document.getElementById("defaultFont");
     if (menulist.selectedIndex == -1) {
       menulist.insertItemAt(0, "", "", "");
       menulist.selectedIndex = 0;
     }
+
+    // Show translation preferences if we may:
+    const prefName = "browser.translation.ui.show";
+    if (Services.prefs.getBoolPref(prefName)) {
+      let row = document.getElementById("translationBox");
+      row.removeAttribute("hidden");
+    }
   },
 
   // UTILITY FUNCTIONS
 
   /**
    * Utility function to enable/disable the button specified by aButtonID based
    * on the value of the Boolean preference specified by aPreferenceID.
    */
--- a/browser/components/translation/Translation.jsm
+++ b/browser/components/translation/Translation.jsm
@@ -3,16 +3,18 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["Translation"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+const TRANSLATION_PREF_SHOWUI = "browser.translation.ui.show";
+
 Cu.import("resource://gre/modules/Services.jsm");
 
 this.Translation = {
   supportedSourceLanguages: ["en", "zh", "ja", "es", "de", "fr", "ru", "ar", "ko", "pt"],
   supportedTargetLanguages: ["en", "pl", "tr", "vi"],
 
   _defaultTargetLanguage: "",
   get defaultTargetLanguage() {
@@ -21,16 +23,19 @@ this.Translation = {
                                       .getService(Ci.nsIXULChromeRegistry)
                                       .getSelectedLocale("global")
                                       .split("-")[0];
     }
     return this._defaultTargetLanguage;
   },
 
   languageDetected: function(aBrowser, aDetectedLanguage) {
+    if (!Services.prefs.getBoolPref(TRANSLATION_PREF_SHOWUI))
+      return;
+
     if (this.supportedSourceLanguages.indexOf(aDetectedLanguage) != -1 &&
         aDetectedLanguage != this.defaultTargetLanguage) {
       if (!aBrowser.translationUI)
         aBrowser.translationUI = new TranslationUI(aBrowser);
 
       aBrowser.translationUI.showTranslationUI(aDetectedLanguage);
     }
   }
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -62,17 +62,22 @@ TranslationContentHandler.prototype = {
 
   receiveMessage: function(msg) {
     switch (msg.name) {
       case "Translation:TranslateDocument":
       {
         Cu.import("resource:///modules/translation/TranslationDocument.jsm");
         Cu.import("resource:///modules/translation/BingTranslator.jsm");
 
-        let translationDocument = new TranslationDocument(this.global.content.document);
+        // If a TranslationDocument already exists for this document, it should
+        // be used instead of creating a new one so that we can use the original
+        // content of the page for the new translation instead of the newly
+        // translated text.
+        let translationDocument = this.global.content.translationDocument ||
+                                  new TranslationDocument(this.global.content.document);
         let bingTranslation = new BingTranslation(translationDocument,
                                                   msg.data.from,
                                                   msg.data.to);
 
         this.global.content.translationDocument = translationDocument;
         bingTranslation.translate().then(
           success => {
             this.global.sendAsyncMessage("Translation:Finished", {success: true});
--- a/browser/components/translation/TranslationDocument.jsm
+++ b/browser/components/translation/TranslationDocument.jsm
@@ -110,60 +110,68 @@ this.TranslationDocument.prototype = {
     return item;
   },
 
   /**
    * Generate the text string that represents a TranslationItem object.
    * Besides generating the string, it's also stored in the "original"
    * field of the TranslationItem object, which needs to be stored for
    * later to be used in the "Show Original" functionality.
+   * If this function had already been called for the given item (determined
+   * by the presence of the "original" array in the item), the text will
+   * be regenerated from the "original" data instead of from the related
+   * DOM nodes (because the nodes might contain translated data).
    *
    * @param item     A TranslationItem object
    *
    * @returns        A string representation of the TranslationItem.
    */
   generateTextForItem: function(item) {
+    if (item.original) {
+      return regenerateTextFromOriginalHelper(item);
+    }
+
     if (item.isSimpleRoot) {
       let text = item.nodeRef.firstChild.nodeValue.trim();
       item.original = [text];
       return text;
     }
 
-    let localName = item.isRoot ? "div" : "b";
-    let str = '<' + localName + ' id="n' + item.id + '">';
-
+    let str = "";
     item.original = [];
 
     for (let child of item.nodeRef.childNodes) {
       if (child.nodeType == TEXT_NODE) {
         let x = child.nodeValue.trim();
         str += x;
         item.original.push(x);
         continue;
       }
 
       let objInMap = this.itemsMap.get(child);
-      if (objInMap) {
+      if (objInMap && !objInMap.isRoot) {
         // If this childNode is present in the itemsMap, it means
         // it's a translation node: it has useful content for translation.
         // In this case, we need to stringify this node.
+        // However, if this item is a root, we should skip it here in this
+        // object's child list (and just add a placeholder for it), because
+        // it will be stringfied separately for being a root.
         item.original.push(objInMap);
         str += this.generateTextForItem(objInMap);
       } else {
         // Otherwise, if this node doesn't contain any useful content,
-        // we can simply replace it by a placeholder node.
+        // or if it is a root itself, we can replace it with a placeholder node.
         // We can't simply eliminate this node from our string representation
         // because that could change the HTML structure (e.g., it would
         // probably merge two separate text nodes).
         str += '<br/>';
       }
     }
 
-    str += '</' + localName + '>';
-    return str;
+    return generateTranslationHtmlForItem(item, str);
   },
 
   /**
    * Changes the document to display its translated
    * content.
    */
   showTranslation: function() {
     this._swapDocumentContent("translation");
@@ -312,16 +320,68 @@ TranslationItem.prototype = {
    *                 or "original".
    */
   swapText: function(target) {
     swapTextForItem(this, target);
   }
 };
 
 /**
+ * Generate the outer HTML representation for a given item.
+ *
+ * @param   item       A TranslationItem object.
+ * param    content    The inner content for this item.
+ * @returns string     The outer HTML needed for translation
+ *                     of this item.
+ */
+function generateTranslationHtmlForItem(item, content) {
+  let localName = item.isRoot ? "div" : "b";
+  return '<' + localName + ' id="n' + item.id + '">' +
+         content +
+         "</" + localName + ">";
+}
+
+ /**
+ * Regenerate the text string that represents a TranslationItem object,
+ * with data from its "original" array. The array must have already
+ * been created by TranslationDocument.generateTextForItem().
+ *
+ * @param item     A TranslationItem object
+ *
+ * @returns        A string representation of the TranslationItem.
+ */
+function regenerateTextFromOriginalHelper(item) {
+  if (item.isSimpleRoot) {
+    return item.original[0];
+  }
+
+  let str = "";
+
+  let wasLastItemText = false;
+  for (let child of item.original) {
+    if (child instanceof TranslationItem) {
+      str += regenerateTextFromOriginalHelper(child);
+      wasLastItemText = false;
+    } else {
+      // The non-significant elements (which were replaced with <br/>
+      // during the first call to generateTextForItem) are not stored
+      // in the original array. If they are not properly re-generated,
+      // two adjacent text nodes would be merged into one.
+      if (wasLastItemText) {
+        str += "<br/>";
+      }
+      str += child;
+      wasLastItemText = true;
+    }
+  }
+
+  return generateTranslationHtmlForItem(item, str);
+}
+
+/**
  * Helper function to parse a HTML doc result.
  * How it works:
  *
  * An example result string is:
  *
  * <div id="n1">Hello <b id="n2">World</b> of Mozilla.</div>
  *
  * For an element node, we look at its id and find the corresponding
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -47,17 +47,16 @@
 }
 
 #navigator-toolbox > toolbar:not(#toolbar-menubar):not(#TabsToolbar):not(#nav-bar) {
   padding-top: 1px;
   padding-bottom: 1px;
 }
 
 #TabsToolbar:not([collapsed="true"]) + #nav-bar {
-  margin-top: -@tabToolbarNavbarOverlap@; /* Move up into the TabsToolbar */
   /* Position the toolbar above the bottom of background tabs */
   position: relative;
   z-index: 1;
 }
 
 #nav-bar {
   background-image: linear-gradient(@toolbarHighlight@, rgba(255,255,255,0));
   box-shadow: 0 1px 0 @toolbarHighlight@ inset;
@@ -1763,16 +1762,17 @@ richlistitem[type~="action"][actiontype=
   /* override the global style to allow the selected tab to be above the nav-bar */
   z-index: auto;
 }
 
 #TabsToolbar {
   min-height: 0;
   padding: 0;
   position: relative;
+  margin-bottom: -@tabToolbarNavbarOverlap@;
 }
 
 /*
  * Draw the bottom border of the tabstrip:
  */
 #TabsToolbar::after {
   content: "";
   position: absolute;
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -239,17 +239,16 @@ browser.jar:
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
   skin/classic/browser/devtools/magnifying-glass-light.png  (../shared/devtools/images/magnifying-glass-light.png)
   skin/classic/browser/devtools/magnifying-glass-light@2x.png (../shared/devtools/images/magnifying-glass-light@2x.png)
   skin/classic/browser/devtools/itemToggle.png        (../shared/devtools/images/itemToggle.png)
   skin/classic/browser/devtools/itemArrow-dark-rtl.svg (../shared/devtools/images/itemArrow-dark-rtl.svg)
   skin/classic/browser/devtools/itemArrow-dark-ltr.svg (../shared/devtools/images/itemArrow-dark-ltr.svg)
   skin/classic/browser/devtools/itemArrow-rtl.svg      (../shared/devtools/images/itemArrow-rtl.svg)
   skin/classic/browser/devtools/itemArrow-ltr.svg      (../shared/devtools/images/itemArrow-ltr.svg)
-  skin/classic/browser/devtools/background-noise-toolbar.png (../shared/devtools/images/background-noise-toolbar.png)
   skin/classic/browser/devtools/noise.png             (../shared/devtools/images/noise.png)
   skin/classic/browser/devtools/dropmarker.png        (../shared/devtools/images/dropmarker.png)
   skin/classic/browser/devtools/layoutview.css         (../shared/devtools/layoutview.css)
   skin/classic/browser/devtools/debugger-collapse.png  (../shared/devtools/images/debugger-collapse.png)
   skin/classic/browser/devtools/debugger-collapse@2x.png  (../shared/devtools/images/debugger-collapse@2x.png)
   skin/classic/browser/devtools/debugger-expand.png    (../shared/devtools/images/debugger-expand.png)
   skin/classic/browser/devtools/debugger-expand@2x.png    (../shared/devtools/images/debugger-expand@2x.png)
   skin/classic/browser/devtools/debugger-pause.png     (../shared/devtools/images/debugger-pause.png)
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -105,17 +105,16 @@ toolbarseparator {
 }
 
 #navigator-toolbox > toolbar:not(#TabsToolbar):not(#nav-bar):not(:-moz-lwtheme) {
   -moz-appearance: none;
   background: url(chrome://browser/skin/Toolbar-background-noise.png) hsl(0,0%,83%);
 }
 
 #TabsToolbar:not([collapsed="true"]) + #nav-bar {
-  margin-top: -@tabToolbarNavbarOverlap@; /* Move up into the TabsToolbar */
   /* Position the toolbar above the bottom of background tabs */
   position: relative;
   z-index: 1;
 }
 
 #nav-bar {
   -moz-appearance: none;
   background: url(chrome://browser/skin/Toolbar-background-noise.png),
@@ -2792,16 +2791,17 @@ toolbarbutton.chevron > .toolbarbutton-m
 #main-window:not(:-moz-any([customizing],[tabsintitlebar])) #navigator-toolbox:not(:-moz-lwtheme)::before {
   visibility: visible;
 }
 
 #TabsToolbar {
   position: relative;
   -moz-appearance: none;
   background-repeat: repeat-x;
+  margin-bottom: -@tabToolbarNavbarOverlap@;
 }
 
 #TabsToolbar:not(:-moz-lwtheme) {
   color: #333;
   text-shadow: @loweredShadow@;
 }
 
 /*
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -359,17 +359,16 @@ browser.jar:
   skin/classic/browser/devtools/magnifying-glass@2x.png     (../shared/devtools/images/magnifying-glass@2x.png)
   skin/classic/browser/devtools/magnifying-glass-light.png  (../shared/devtools/images/magnifying-glass-light.png)
   skin/classic/browser/devtools/magnifying-glass-light@2x.png (../shared/devtools/images/magnifying-glass-light@2x.png)
   skin/classic/browser/devtools/itemToggle.png              (../shared/devtools/images/itemToggle.png)
   skin/classic/browser/devtools/itemArrow-dark-rtl.svg      (../shared/devtools/images/itemArrow-dark-rtl.svg)
   skin/classic/browser/devtools/itemArrow-dark-ltr.svg      (../shared/devtools/images/itemArrow-dark-ltr.svg)
   skin/classic/browser/devtools/itemArrow-rtl.svg           (../shared/devtools/images/itemArrow-rtl.svg)
   skin/classic/browser/devtools/itemArrow-ltr.svg           (../shared/devtools/images/itemArrow-ltr.svg)
-  skin/classic/browser/devtools/background-noise-toolbar.png (../shared/devtools/images/background-noise-toolbar.png)
   skin/classic/browser/devtools/noise.png                   (../shared/devtools/images/noise.png)
   skin/classic/browser/devtools/dropmarker.png              (../shared/devtools/images/dropmarker.png)
   skin/classic/browser/devtools/layoutview.css              (../shared/devtools/layoutview.css)
   skin/classic/browser/devtools/debugger-collapse.png       (../shared/devtools/images/debugger-collapse.png)
   skin/classic/browser/devtools/debugger-collapse@2x.png    (../shared/devtools/images/debugger-collapse@2x.png)
   skin/classic/browser/devtools/debugger-expand.png         (../shared/devtools/images/debugger-expand.png)
   skin/classic/browser/devtools/debugger-expand@2x.png      (../shared/devtools/images/debugger-expand@2x.png)
   skin/classic/browser/devtools/debugger-pause.png          (../shared/devtools/images/debugger-pause.png)
--- a/browser/themes/shared/devtools/canvasdebugger.inc.css
+++ b/browser/themes/shared/devtools/canvasdebugger.inc.css
@@ -13,22 +13,22 @@
 /* Reload and waiting notices */
 
 .notice-container {
   margin-top: -50vh;
   font-size: 120%;
 }
 
 .theme-dark .notice-container {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .notice-container {
-  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+  background-color: #f0f1f2; /* Toolbars */
   color: #585959; /* Grey foreground text */
 }
 
 #reload-notice > button {
   min-height: 2em;
 }
 
 #empty-notice > button {
@@ -434,23 +434,21 @@
 /* Snapshot filmstrip */
 
 #snapshot-filmstrip {
   overflow: hidden;
 }
 
 .theme-dark #snapshot-filmstrip {
   border-top: 1px solid #000;
-  background-image: url(background-noise-toolbar.png);
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light #snapshot-filmstrip {
   border-top: 1px solid #aaa;
-  background-image: url(background-noise-toolbar.png);
   color: #585959; /* Grey foreground text */
 }
 
 .filmstrip-thumbnail {
   image-rendering: -moz-crisp-edges;
   background-image: @checkerboardPattern@;
   background-size: 12px 12px, 12px 12px;
   background-position: 0px -1px, 6px 5px;
--- a/browser/themes/shared/devtools/commandline.inc.css
+++ b/browser/themes/shared/devtools/commandline.inc.css
@@ -5,17 +5,17 @@
 %endif
 
 /* Developer toolbar */
 
 #developer-toolbar {
   -moz-appearance: none;
   padding: 0;
   min-height: 32px;
-  background-image: url(devtools/background-noise-toolbar.png), linear-gradient(#303840, #2d3640);
+  background-color: #343C45; /* Toolbars */
   border-top: 1px solid #060a0d;
   box-shadow: 0 1px 0 hsla(204,45%,98%,.05) inset, 0 -1px 0 hsla(206,37%,4%,.1) inset;
 }
 
 #developer-toolbar > toolbarbutton {
   -moz-appearance: none;
   border: none;
   background: transparent;
deleted file mode 100644
index d09ba9dafb5ede66e92ce577ecaa6f9575dba104..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/shared/devtools/netmonitor.inc.css
+++ b/browser/themes/shared/devtools/netmonitor.inc.css
@@ -412,17 +412,17 @@ label.requests-menu-status-code {
 
 #details-pane-toggle:active {
   -moz-image-region: rect(0px,32px,16px,16px);
 }
 
 /* Network request details tabpanels */
 
 .theme-dark .tabpanel-content {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
 }
 
 /* Summary tabpanel */
 
 .tabpanel-summary-container {
   padding: 1px;
 }
@@ -456,22 +456,22 @@ label.requests-menu-status-code {
 /* Response tabpanel */
 
 #response-content-info-header {
   margin: 0;
   padding: 3px 8px;
 }
 
 .theme-dark  #response-content-info-header {
-  background: url(background-noise-toolbar.png), #eb5368; /* Red highlight */
+  background-color: #eb5368; /* Red highlight */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light  #response-content-info-header {
-  background: url(background-noise-toolbar.png), #ed2655; /* Red highlight */
+  background-color: #ed2655; /* Red highlight */
   color: #f5f7fa; /* Light foreground text */
 }
 
 #response-content-image-box {
   padding-top: 10px;
   padding-bottom: 10px;
 }
 
@@ -526,23 +526,23 @@ label.requests-menu-status-code {
   width: 4.5em;
 }
 
 /* Footer */
 
 .theme-dark #requests-menu-footer {
   border-top: 1px solid @table_itemDarkStartBorder@;
   box-shadow: 0 1px 0 @table_itemDarkEndBorder@ inset;
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
 }
 
 .theme-light #requests-menu-footer {
   border-top: 1px solid @table_itemLightStartBorder@;
   box-shadow: 0 1px 0 @table_itemLightEndBorder@ inset;
-  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+  background-color: #f0f1f2; /* Toolbars */
 }
 
 .requests-menu-footer-button,
 .requests-menu-footer-label {
   min-width: 1em;
   margin: 0;
   border: none;
   padding: 2px 1vw;
@@ -651,21 +651,21 @@ label.requests-menu-status-code {
   pointer-events: none;
 }
 
 #network-statistics-charts {
   min-height: 1px;
 }
 
 .theme-dark #network-statistics-charts {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
 }
 
 .theme-light #network-statistics-charts {
-  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+  background-color: #f0f1f2; /* Toolbars */
 }
 
 #network-statistics-charts .pie-chart-container {
   -moz-margin-start: 3vw;
   -moz-margin-end: 1vw;
 }
 
 #network-statistics-charts .table-chart-container {
--- a/browser/themes/shared/devtools/shadereditor.inc.css
+++ b/browser/themes/shared/devtools/shadereditor.inc.css
@@ -4,22 +4,22 @@
 
 /* Reload and waiting notices */
 
 .notice-container {
   margin-top: -50vh;
 }
 
 .theme-dark .notice-container {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .notice-container {
-  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+  background-color: #f0f1f2; /* Toolbars */
   color: #585959; /* Grey foreground text */
 }
 
 #reload-notice {
   font-size: 120%;
 }
 
 #waiting-notice {
--- a/browser/themes/shared/devtools/splitview.css
+++ b/browser/themes/shared/devtools/splitview.css
@@ -13,17 +13,17 @@
 
 .loading .splitview-nav-container {
   background-image: url(chrome://global/skin/icons/loading_16.png);
   background-repeat: no-repeat;
   background-position: center center;
 }
 
 .theme-dark .splitview-nav-container {
-  background: url(background-noise-toolbar.png), #343c45;
+  background-color: #343c45; /* Toolbars */
 }
 
 .splitview-nav {
   -moz-appearance: none;
   list-style-image: none;
   list-style: none;
   padding: 0;
   margin: 0;
--- a/browser/themes/shared/devtools/webaudioeditor.inc.css
+++ b/browser/themes/shared/devtools/webaudioeditor.inc.css
@@ -3,22 +3,22 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Reload and waiting notices */
 .notice-container {
   margin-top: -50vh;
 }
 
 .theme-dark .notice-container {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .notice-container {
-  background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
+  background-color: #f0f1f2; /* Toolbars */
   color: #585959; /* Grey foreground text */
 }
 
 #reload-notice {
   font-size: 120%;
 }
 
 #waiting-notice {
--- a/browser/themes/shared/devtools/widgets.inc.css
+++ b/browser/themes/shared/devtools/widgets.inc.css
@@ -367,17 +367,17 @@
   /* Hack: force hardware acceleration */
   transform: translateZ(1px);
 }
 
 /* SideMenuWidget container */
 
 .theme-dark .side-menu-widget-container,
 .theme-dark .side-menu-widget-empty-text {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
 }
 
 .theme-dark .side-menu-widget-container:-moz-locale-dir(ltr),
 .theme-dark .side-menu-widget-empty-text:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 @smw_marginDark@;
 }
 
 .theme-dark .side-menu-widget-container:-moz-locale-dir(rtl),
@@ -517,21 +517,21 @@
   margin-top: 4px;
 }
 
 .side-menu-widget-item-other:last-of-type {
   margin-bottom: -4px;
 }
 
 .theme-dark .side-menu-widget-item-other {
-  background: url(background-noise-toolbar.png), rgba(0,0,0,.1);
+  background-color: rgba(0,0,0,.1);
 }
 
 .theme-light .side-menu-widget-item-other {
-  background: url(background-noise-toolbar.png), rgba(128,128,128,.1);
+  background-color: rgba(128,128,128,.1);
 }
 
 .theme-dark .side-menu-widget-item.selected .side-menu-widget-item-other {
   background-color: rgba(0,0,0,.2); /* Darken the selection by 20% */
   color: #f5f7fa; /* Light foreground text */
 }
 
 .theme-light .side-menu-widget-item.selected .side-menu-widget-item-other {
@@ -563,17 +563,17 @@
 
 /* SideMenuWidget misc */
 
 .side-menu-widget-empty-text {
   padding: 4px 8px;
 }
 
 .theme-dark .side-menu-widget-empty-text {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
   color: #b6babf; /* Foreground (Text) - Grey */
 }
 
 .theme-light .side-menu-widget-empty-text {
   background: #f7f7f7; /* Toolbars */
   color: #585959; /* Grey foreground text */
 }
 
@@ -1062,17 +1062,17 @@
 }
 
 .theme-light .table-widget-body {
   background: #F7F7F7; /* Background-Sidebar */
 }
 
 .theme-dark .table-widget-body,
 .theme-dark .table-widget-empty-text {
-  background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
+  background-color: #343c45; /* Toolbars */
 }
 
 .theme-dark .table-widget-body:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 @smw_marginDark@;
 }
 
 .theme-dark .table-widget-body:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 @smw_marginDark@;
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -277,17 +277,16 @@ browser.jar:
         skin/classic/browser/devtools/magnifying-glass@2x.png       (../shared/devtools/images/magnifying-glass@2x.png)
         skin/classic/browser/devtools/magnifying-glass-light.png    (../shared/devtools/images/magnifying-glass-light.png)
         skin/classic/browser/devtools/magnifying-glass-light@2x.png  (../shared/devtools/images/magnifying-glass-light@2x.png)
         skin/classic/browser/devtools/itemToggle.png                (../shared/devtools/images/itemToggle.png)
         skin/classic/browser/devtools/itemArrow-dark-rtl.svg        (../shared/devtools/images/itemArrow-dark-rtl.svg)
         skin/classic/browser/devtools/itemArrow-dark-ltr.svg        (../shared/devtools/images/itemArrow-dark-ltr.svg)
         skin/classic/browser/devtools/itemArrow-rtl.svg             (../shared/devtools/images/itemArrow-rtl.svg)
         skin/classic/browser/devtools/itemArrow-ltr.svg             (../shared/devtools/images/itemArrow-ltr.svg)
-        skin/classic/browser/devtools/background-noise-toolbar.png  (../shared/devtools/images/background-noise-toolbar.png)
         skin/classic/browser/devtools/noise.png                     (../shared/devtools/images/noise.png)
         skin/classic/browser/devtools/dropmarker.png                (../shared/devtools/images/dropmarker.png)
         skin/classic/browser/devtools/layoutview.css                (../shared/devtools/layoutview.css)
         skin/classic/browser/devtools/debugger-collapse.png         (../shared/devtools/images/debugger-collapse.png)
         skin/classic/browser/devtools/debugger-collapse@2x.png      (../shared/devtools/images/debugger-collapse@2x.png)
         skin/classic/browser/devtools/debugger-expand.png           (../shared/devtools/images/debugger-expand.png)
         skin/classic/browser/devtools/debugger-expand@2x.png        (../shared/devtools/images/debugger-expand@2x.png)
         skin/classic/browser/devtools/debugger-pause.png            (../shared/devtools/images/debugger-pause.png)
@@ -669,17 +668,16 @@ browser.jar:
         skin/classic/aero/browser/devtools/magnifying-glass@2x.png   (../shared/devtools/images/magnifying-glass@2x.png)
         skin/classic/aero/browser/devtools/magnifying-glass-light.png (../shared/devtools/images/magnifying-glass-light.png)
         skin/classic/aero/browser/devtools/magnifying-glass-light@2x.png  (../shared/devtools/images/magnifying-glass-light@2x.png)
         skin/classic/aero/browser/devtools/itemToggle.png            (../shared/devtools/images/itemToggle.png)
         skin/classic/aero/browser/devtools/itemArrow-dark-rtl.svg    (../shared/devtools/images/itemArrow-dark-rtl.svg)
         skin/classic/aero/browser/devtools/itemArrow-dark-ltr.svg    (../shared/devtools/images/itemArrow-dark-ltr.svg)
         skin/classic/aero/browser/devtools/itemArrow-rtl.svg         (../shared/devtools/images/itemArrow-rtl.svg)
         skin/classic/aero/browser/devtools/itemArrow-ltr.svg         (../shared/devtools/images/itemArrow-ltr.svg)
-        skin/classic/aero/browser/devtools/background-noise-toolbar.png (../shared/devtools/images/background-noise-toolbar.png)
         skin/classic/aero/browser/devtools/noise.png                 (../shared/devtools/images/noise.png)
         skin/classic/aero/browser/devtools/dropmarker.png            (../shared/devtools/images/dropmarker.png)
         skin/classic/aero/browser/devtools/layoutview.css            (../shared/devtools/layoutview.css)
         skin/classic/aero/browser/devtools/debugger-collapse.png     (../shared/devtools/images/debugger-collapse.png)
         skin/classic/aero/browser/devtools/debugger-collapse@2x.png  (../shared/devtools/images/debugger-collapse@2x.png)
         skin/classic/aero/browser/devtools/debugger-expand.png       (../shared/devtools/images/debugger-expand.png)
         skin/classic/aero/browser/devtools/debugger-expand@2x.png    (../shared/devtools/images/debugger-expand@2x.png)
         skin/classic/aero/browser/devtools/debugger-pause.png        (../shared/devtools/images/debugger-pause.png)
--- a/configure.in
+++ b/configure.in
@@ -4046,16 +4046,29 @@ AC_SUBST(MOZ_MOZILLA_API_KEY)
 MOZ_ARG_WITH_STRING(google-api-keyfile,
 [  --with-google-api-keyfile=file   Use the secret key contained in the given keyfile for Google API requests],
   MOZ_GOOGLE_API_KEY=`cat $withval`)
 if test -z "$MOZ_GOOGLE_API_KEY"; then
     MOZ_GOOGLE_API_KEY=no-google-api-key
 fi
 AC_SUBST(MOZ_GOOGLE_API_KEY)
 
+# Allow specifying a Bing API key file that contains the client ID and the
+# secret key to be used for the Bing Translation API requests.
+MOZ_ARG_WITH_STRING(bing-api-keyfile,
+[  --with-bing-api-keyfile=file   Use the client id and secret key contained in the given keyfile for Bing API requests],
+ [MOZ_BING_API_CLIENTID=`cat $withval | cut -f 1 -d " "`
+  MOZ_BING_API_KEY=`cat $withval | cut -f 2 -d " "`])
+if test -z "$MOZ_BING_API_CLIENTID"; then
+    MOZ_BING_API_CLIENTID=no-bing-api-clientid
+    MOZ_BING_API_KEY=no-bing-api-key
+fi
+AC_SUBST(MOZ_BING_API_CLIENTID)
+AC_SUBST(MOZ_BING_API_KEY)
+
 # Allow the application to influence configure with a confvars.sh script.
 AC_MSG_CHECKING([if app-specific confvars.sh exists])
 if test -f "${srcdir}/${MOZ_BUILD_APP}/confvars.sh" ; then
   AC_MSG_RESULT([${srcdir}/${MOZ_BUILD_APP}/confvars.sh])
   . "${srcdir}/${MOZ_BUILD_APP}/confvars.sh"
 else
   AC_MSG_RESULT([no])
 fi
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -793,17 +793,17 @@ public abstract class GeckoApp
                 } else {
                     toast = Toast.makeText(GeckoApp.this, message, Toast.LENGTH_SHORT);
                 }
                 toast.show();
             }
         });
     }
 
-    protected ButtonToast getButtonToast() {
+    public ButtonToast getButtonToast() {
         if (mToast != null) {
             return mToast;
         }
 
         ViewStub toastStub = (ViewStub) findViewById(R.id.toast_stub);
         mToast = new ButtonToast(toastStub.inflate());
 
         return mToast;
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -433,12 +433,14 @@ public class BrowserContract {
 
         public static final String TYPE = "type";
     }
 
     @RobocopTarget
     public static final class SuggestedSites implements CommonColumns, URLColumns {
         private SuggestedSites() {}
 
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "suggestedsites");
+
         public static final String IMAGE_URL = "image_url";
         public static final String BG_COLOR = "bg_color";
     }
 }
--- a/mobile/android/base/db/SuggestedSites.java
+++ b/mobile/android/base/db/SuggestedSites.java
@@ -1,35 +1,38 @@
 /* -*- 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.db;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
 import android.text.TextUtils;
 import android.util.Log;
 
 import java.io.IOException;
 import java.lang.ref.SoftReference;
 import java.util.Collections;
 import java.util.List;
 import java.util.ArrayList;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.mozglue.RobocopTarget;
+import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.RawResource;
 
 /**
  * {@code SuggestedSites} provides API to get a list of locale-specific
  * suggested sites to be used in Fennec's top sites panel. It provides
  * only a single method to fetch the list as a {@code Cursor}. This cursor
  * will then be wrapped by {@code TopSitesCursorWrapper} to blend top,
  * pinned, and suggested sites in the UI. The returned {@code Cursor}
@@ -145,40 +148,51 @@ public class SuggestedSites {
 
         // Update cached list of sites
         cachedSites = new SoftReference<List<Site>>(Collections.unmodifiableList(sites));
 
         // Return the refreshed list
         return sites;
     }
 
+    private boolean isEnabled() {
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+        return prefs.getBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, true);
+    }
+
     /**
      * Returns a {@code Cursor} with the list of suggested websites.
      *
      * @param limit maximum number of suggested sites.
      */
     public Cursor get(int limit) {
         return get(limit, null);
     }
 
     /**
      * Returns a {@code Cursor} with the list of suggested websites.
      *
      * @param limit maximum number of suggested sites.
      * @param excludeUrls list of URLs to be excluded from the list.
      */
     public Cursor get(int limit, List<String> excludeUrls) {
+        final MatrixCursor cursor = new MatrixCursor(COLUMNS);
+
+        // Return an empty cursor if suggested sites have been
+        // disabled by the user.
+        if (!isEnabled()) {
+            return cursor;
+        }
+
         List<Site> sites = cachedSites.get();
         if (sites == null) {
             Log.d(LOGTAG, "No cached sites, refreshing.");
             sites = refresh();
         }
 
-        final MatrixCursor cursor = new MatrixCursor(COLUMNS);
-
         // Return empty cursor if there was an error when
         // loading the suggested sites or the list is empty.
         if (sites == null || sites.isEmpty()) {
             return cursor;
         }
 
         final int sitesCount = sites.size();
         Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
@@ -194,11 +208,14 @@ public class SuggestedSites {
             final RowBuilder row = cursor.newRow();
             row.add(-1);
             row.add(site.url);
             row.add(site.title);
             row.add(site.imageUrl);
             row.add(site.bgColor);
         }
 
+        cursor.setNotificationUri(context.getContentResolver(),
+                                  BrowserContract.SuggestedSites.CONTENT_URI);
+
         return cursor;
     }
 }
\ No newline at end of file
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -1,31 +1,34 @@
 /* -*- 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.home;
 
 import org.mozilla.gecko.EditBookmarkDialog;
+import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
+import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
+import org.mozilla.gecko.widget.ButtonToast;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
@@ -171,27 +174,47 @@ abstract class HomeFragment extends Frag
 
         if (itemId == R.id.home_open_private_tab || itemId == R.id.home_open_new_tab) {
             if (info.url == null) {
                 Log.e(LOGTAG, "Can't open in new tab because URL is null");
                 return false;
             }
 
             int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
-            if (item.getItemId() == R.id.home_open_private_tab)
+            final boolean isPrivate = (item.getItemId() == R.id.home_open_private_tab);
+            if (isPrivate) {
                 flags |= Tabs.LOADURL_PRIVATE;
+            }
 
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU);
 
             final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
 
             // Some pinned site items have "user-entered" urls. URLs entered in the PinSiteDialog are wrapped in
             // a special URI until we can get a valid URL. If the url is a user-entered url, decode the URL before loading it.
-            Tabs.getInstance().loadUrl(decodeUserEnteredUrl(url), flags);
-            Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
+            final Tab newTab = Tabs.getInstance().loadUrl(decodeUserEnteredUrl(url), flags);
+            final int newTabId = newTab.getId(); // We don't want to hold a reference to the Tab.
+
+            final String message = isPrivate ?
+                    getResources().getString(R.string.new_private_tab_opened) :
+                    getResources().getString(R.string.new_tab_opened);
+            final GeckoApp geckoApp = (GeckoApp) context;
+            geckoApp.getButtonToast().show(false,
+                    message,
+                    null,
+                    R.drawable.select_opened_tab,
+                    new ButtonToast.ToastListener() {
+                        @Override
+                        public void onButtonClicked() {
+                            Tabs.getInstance().selectTab(newTabId);
+                        }
+
+                        @Override
+                        public void onToastHidden(ButtonToast.ReasonHidden reason) { }
+                    });
             return true;
         }
 
         if (itemId == R.id.home_edit_bookmark) {
             // UI Dialog associates to the activity context, not the applications'.
             new EditBookmarkDialog(context).show(info.url);
             return true;
         }
--- a/mobile/android/base/locales/Makefile.in
+++ b/mobile/android/base/locales/Makefile.in
@@ -84,26 +84,26 @@ strings-xml-preqs =\
 	  -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE=$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE) \
 	  -DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE=$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE) \
 	  -DMOZ_APP_DISPLAYNAME='@MOZ_APP_DISPLAYNAME@' \
 	  -DSTRINGSPATH='$(STRINGSPATH)' \
 	  -DSYNCSTRINGSPATH='$(SYNCSTRINGSPATH)' \
       $< \
 	  -o $@)
 
-suggestedsites-srcdir := $(dir $(abspath $(call MERGE_FILE,suggestedsites/list.txt)))
+suggestedsites-srcdir := $(if $(filter en-US,$(AB_CD)),,$(or $(realpath $(L10NBASEDIR)),$(abspath $(L10NBASEDIR)))/$(AB_CD)/mobile/chrome)
 
 # Determine the ../res/raw[-*] path.  This can be ../res/raw when no
 # locale is explicitly specified.
 suggestedsites-json-bypath = $(filter %/suggestedsites.json,$(MAKECMDGOALS))
 ifeq (,$(strip $(suggestedsites-json-bypath)))
   suggestedsites-json-bypath = $(suggestedsites-json)
 endif
 suggestedsites-dstdir-raw = $(patsubst %/,%,$(dir $(suggestedsites-json-bypath)))
 
 $(suggestedsites-dstdir-raw)/suggestedsites.json: FORCE
 	$(call py_action,generate_suggestedsites, \
 		--verbose \
 		--android-package-name=$(ANDROID_PACKAGE_NAME) \
 		--resources=$(srcdir)/../resources \
 		$(if $(filter en-US,$(AB_CD)),,--srcdir=$(suggestedsites-srcdir)) \
-		--srcdir=$(addprefix $(srcdir)/,en-US/suggestedsites) \
+		--srcdir=$(topsrcdir)/mobile/locales/en-US/chrome \
 		$@)
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -57,16 +57,17 @@
 <!ENTITY edit_mode_cancel "Cancel">
 
 <!ENTITY close_tab "Close Tab">
 <!ENTITY one_tab "1 tab">
 <!-- Localization note (num_tabs2) : Number of tabs is always more than one.
      We can't use android plural forms, sadly. See bug #753859. -->
 <!ENTITY num_tabs2 "&formatD; tabs">
 <!ENTITY new_tab_opened "New tab opened">
+<!ENTITY new_private_tab_opened "New private tab opened">
 
 <!ENTITY settings "Settings">
 <!ENTITY settings_title "Settings">
 <!ENTITY pref_category_advanced "Advanced">
 <!ENTITY pref_category_customize "Customize">
 
 <!-- Localization note (pref_category_language) : This is the preferences
      section in which the user picks the locale in which to display Firefox
@@ -110,16 +111,18 @@
 <!-- Localization note (home_add_panel_installed):
      The &formatS; will be replaced with the name of the new panel the user just
      selected to be added to the home page. -->
 <!ENTITY home_add_panel_installed "\'&formatS;\' added to homepage">
 <!ENTITY pref_category_home_content_settings "Content settings">
 <!ENTITY pref_home_updates "Automatic updates">
 <!ENTITY pref_home_updates_enabled "Enabled">
 <!ENTITY pref_home_updates_wifi "Only over Wi-Fi">
+<!ENTITY pref_home_suggested_sites "Show site suggestions">
+<!ENTITY pref_home_suggested_sites_summary "Display shortcuts to sites on your homepage that we think you might find interesting">
 
 <!-- Localization note: These are shown in the left sidebar on tablets -->
 <!ENTITY pref_header_customize "Customize">
 <!ENTITY pref_header_display "Display">
 <!ENTITY pref_header_privacy_short "Privacy">
 <!ENTITY pref_header_help "Help">
 <!ENTITY pref_header_language "Language">
 <!ENTITY pref_header_vendor "&vendorShortName;">
deleted file mode 100644
--- a/mobile/android/base/locales/en-US/suggestedsites/fxmarketplace.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{ "title": "Firefox Marketplace",
-  "url": "https://marketplace.firefox.com/",
-  "bgcolor": "#0095dd" }
deleted file mode 100644
--- a/mobile/android/base/locales/en-US/suggestedsites/list.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-mozilla
-fxmarketplace
deleted file mode 100644
--- a/mobile/android/base/locales/en-US/suggestedsites/mozilla.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{ "title": "Mozilla",
-  "url": "http://www.mozilla.org/en-US/",
-  "bgcolor": "#c13832" }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -410,16 +410,17 @@ gbjar.sources += [
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
     'widget/DateTimePicker.java',
     'widget/Divider.java',
     'widget/DoorHanger.java',
     'widget/EllipsisTextView.java',
     'widget/FaviconView.java',
+    'widget/FloatingHintEditText.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/GeckoSwipeRefreshLayout.java',
     'widget/GeckoViewFlipper.java',
     'widget/IconTabWidget.java',
     'widget/SquaredImageView.java',
     'widget/TabRow.java',
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -24,27 +24,30 @@ import org.mozilla.gecko.GeckoSharedPref
 import org.mozilla.gecko.LocaleManager;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.background.announcements.AnnouncementsConstants;
 import org.mozilla.gecko.background.common.GlobalConstants;
 import org.mozilla.gecko.background.healthreport.HealthReportConstants;
+import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
 import org.mozilla.gecko.home.HomePanelPicker;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.FloatingHintEditText;
 
 import android.app.ActionBar;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.NotificationManager;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.os.Bundle;
@@ -107,16 +110,17 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
     private static final String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom";
     private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
+    public static final String PREFS_SUGGESTED_SITES = NON_PREF_PREFIX + "home_suggested_sites";
 
     // These values are chosen to be distinct from other Activity constants.
     private static final int REQUEST_CODE_PREF_SCREEN = 5;
     private static final int RESULT_CODE_EXIT_SETTINGS = 6;
 
     // Result code used when a locale preference changes.
     // Callers can recognize this code to refresh themselves to
     // accommodate a locale change.
@@ -896,16 +900,22 @@ OnSharedPreferenceChangeListener
      * tablets, Honeycomb and up, where we'll have a multi-pane view and prefs
      * changing multiple times.
      */
     @Override
     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
         if (PREFS_BROWSER_LOCALE.equals(key)) {
             onLocaleSelected(BrowserLocaleManager.getLanguageTag(lastLocale),
                              sharedPreferences.getString(key, null));
+        } else if (PREFS_SUGGESTED_SITES.equals(key)) {
+            final ContentResolver cr = getApplicationContext().getContentResolver();
+
+            // This will force all active suggested sites cursors
+            // to request a refresh (e.g. cursor loaders).
+            cr.notifyChange(SuggestedSites.CONTENT_URI, null);
         }
     }
 
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         final String prefName = preference.getKey();
         if (PREFS_MP_ENABLED.equals(prefName)) {
             showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD);
@@ -957,17 +967,17 @@ OnSharedPreferenceChangeListener
             final FontSizePreference fontSizePref = (FontSizePreference) preference;
             fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
         }
 
         return true;
     }
 
     private EditText getTextBox(int aHintText) {
-        EditText input = new EditText(GeckoAppShell.getContext());
+        EditText input = new FloatingHintEditText(GeckoAppShell.getContext());
         int inputtype = InputType.TYPE_CLASS_TEXT;
         inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
         input.setInputType(inputtype);
 
         input.setHint(aHintText);
         return input;
     }
 
--- a/mobile/android/base/prompts/PromptInput.java
+++ b/mobile/android/base/prompts/PromptInput.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.prompts;
 
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
 import java.util.GregorianCalendar;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.widget.AllCapsTextView;
 import org.mozilla.gecko.widget.DateTimePicker;
+import org.mozilla.gecko.widget.FloatingHintEditText;
 
 import android.content.Context;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.text.Html;
 import android.text.InputType;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
@@ -57,17 +58,17 @@ public class PromptInput {
 
         public EditInput(JSONObject object) {
             super(object);
             mHint = object.optString("hint");
             mAutofocus = object.optBoolean("autofocus");
         }
 
         public View getView(final Context context) throws UnsupportedOperationException {
-            EditText input = new EditText(context);
+            EditText input = new FloatingHintEditText(context);
             input.setInputType(InputType.TYPE_CLASS_TEXT);
             input.setText(mValue);
 
             if (!TextUtils.isEmpty(mHint)) {
                 input.setHint(mHint);
             }
 
             if (mAutofocus) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/floating_hint_text.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Holo blue for focus -->
+    <item android:state_focused="true" android:color="@color/text_color_hint_floating_focused" />
+
+    <!-- Default gray for all other states -->
+    <item android:color="@color/text_color_hint"/>
+</selector>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3ebc746cda714b39d64997e18b126e72cc1cbf87
GIT binary patch
literal 875
zc%17D@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAifOS+@4BLl<6e(pbstUx|vage(c
z!@6@aFM(|R0G|+7px}lL8@6oOl9rZMRaLcp`}QM8j+{DmDm67VD=Vw8u&@*imM&eo
zZ{NP6q9UN0^78V-hYue-crZCR8K?j#vJMP@BAuO`1qB7w)zy<HPi}2(-LYfGp+koj
zEn2jD_wKT?GN3sb85z^2O>1ds0g`+6>;YN?v>m8%)22-`X3SWzVg*o5M@L6)ZtkpE
zvw-FR89))BEp>HuK#PE878e%-odI;-+_`h-&!0bM&YUGnmQ0*D5vUBP1?Y^$ix;n6
zy}GZjZ_1P@Q>RXyIdkTM1q*;~02)4f_Us80CM;jReATK|)2C0LH*enBwQJkk+t;sO
zpP!%K+}zyO)&{f)7#cwL0E42tySuTm5$K-Hn>SZhRsyYN&Yf)q3^KctAirRSy1Lc;
z^|5!$S@`*nGXD8@^yr^ItB;D$iHOZSdadnd`k|tlWw$0?-r_Ui_Mf{CSF>o9uRgxo
zsQgSSW2WeylKSWNF2G2b;OXKR5^?zL_3*qy4gw8{>Wa1w9545NzqdNtVB7T6*bRm=
z_;&u6+{E_j8He}dc9&1}4gRyPW-Uns1(wGAxO;~R#pZjx-)J!B>!KIujyV-b-AITo
zc<FKScHfzUOP!Ux`&6P6GA#qAtDH2u8OEj2d-iJX?P;9RKED4YHDjk|wv;{$n(DOd
z;BMoBlP11N8L2C_Ov)3UHKEa))8D8ih9l_gf>dtb9Zxt+dS-D4AF!`;)0lE}g-}S<
zj~O3cetf3)dg}8lr=k>ITFw0PW#wZf?f0!~8!l~L-BG6~_dWTj!{xoJq~6ay`c<({
zT)Vn?;Z-Jc4R7b!jtXT7ZJUiHti*f{n&<rG$qaF4wkiAbq~y-}&)jRi3NcykFDspS
zx<vSvLi(qBp~Z1GrOxbptHHY7=7Qu;tyLXX60cNZ1ut$e<|zw#RS>W|<XY$~)kaxY
zo`W;>O?Urp*xG)e&rddv&6?NsdMWQS1NM2&rx|3^_CJ5(v*2(}f=JilBhxv8xLDp)
rOmdkcAfxp*FW)uXJ0#?a{cGuGp;LD6JF&_H7|RTvu6{1-oD!M<o0zCU
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..93f18778546d09fa5725316ca47ce70d22a40235
GIT binary patch
literal 689
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sEa{HEjtmSN`?>!lvI6;x#X;^)
z4C~IxyacjY1AIbUfr5t)9V#y`@9yqSOG_&(EG#ZAK7IQ1nl)>-Z{NOY)20Il4jevw
zI4dg)sHV2IcFUG63l}Z~%5K=OVbP*R>FMeF_wQe}Y*|fBO-oA)P%%(tZ*MP9O?7p3
zQBhHAYwOOPJ7><EnV+A(d-v{&ii*<G(p|fDEnd7BsOH3p6F?j5>gqBxGl6P=BGac&
zPfbnj?ChLBe?Cyn+_`h-%$Wm}nm2FWtXZ>wj%;XX09psM8fZ&TPtT-DliJ(c%gV~8
zO`8UE)ykDCfeveGY6990bn3cw>-ziqw{G1!dGchSEkN&7R#pPtFk!-k9XoaaEiEZ2
z0lK;OkWdpaAoNRu{DK*N*07q^R=;Cjupl`2g9-Bj?w<?HKm61$uekHPeAdEw3ECh2
zzN^VP^z-n$4<9sw{dYRl0>hHu)5S5w<M`XDH-ioth&a6F+q7cEj_%*<I(=uU>BeU2
zibOBa`~P3_P;~7HpLeT^=AEDEV^p4Jl6x*biE~+giC(zlzsyCquQxn6BfKSDe-*p+
zTE{D_qFY<}69VUpM)GOhOP=+#Vf(7>>>D3{XLMa_`)bmn{Y*M-6COS73wqc0)b;a=
ztw)}RCs<uE+|#+`hCkEZmfMdx6qVKllznby`tMX=)H-{e?##Q%?-m%fNEl~SDOo1I
zFIao#aOmlTo{NE77g;rnFB5p-do$mzQbM@2{-@zbL;tIPkKH&Ov1{c!f1UZRum0t&
z5Lb#~|8ej_&(y!q-6VEc+zb1<b@m;mpSP~rrV7+Y-Kh)L-f*?b?tk1rTl<QOCfpMx
SzPJNJmBG{1&t;ucLK6VMIaO!?
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..25d2e60186ebae2cccf43c6506ba77c39916dca6
GIT binary patch
literal 1068
zc$@(&1k?M8P)<h;3K|Lk000e1NJLTq003YB003YJ0{{R3)kcR300004XF*Lt006O%
z3;baP00001b5ch_0Itp)=>Px$Vo*#}MF0Q*aBy(7wY7M7cyDiSzrVk*u&~O?%4KC`
ztgNhdc6M`fbHl^KxVX4xW@d|vi*|N)V`F2*#l@_wta^HSdwY9&dU~v^tY>FudwYAq
z!NHrGn}2_Qj*gCNYin(7ZKb89m6eso#>R4Ta=5s-i;Ih)p`m4EWx>J0b8~ZSY;3Z!
zvb?;!w6wHwadCr#gO!z)wzjr(baZ)nd3AMlii(Pol9F_Ebh5ItqN1Xzs;Zuzp1;4p
ze}8|RoSdMbpq`$doSdAWpP!wbos*N3yu7@8e0-y$qpq&5nVFf7kB_OTsg910mX?;1
zl9H^ftgEZ5kdTnCudkw_qNb*%p`oFal$4c~m7ANJjg5_(nwqGnsHdl=t*x!Fu&}DC
zs-&c(rKP2jk&%~|my3&wn3$M;etv*}fO&a&aBy(GzP_=sv4VnvySuxHh=_%Sg>7wZ
zv$L~oY;3r=xN>rG!otEp^;%y5002L9QchC<W2I*S5T|CD1O8;pqnCzemdiyL{toq+
z*31IOEqR#-Tu@NJ&3cWoK_B&dH{OQn!7&NcnW~s);LQ=unG}_8+Nb~k0v$<2K~!ko
z?bcUw+CUVBVcDdT-iYJ4r1##OIuZgSfrLN^Apt5B(F``mcK&}za$vZM%II9k+2;qm
z-|XJ(QWQZD1aZu~U)p$w*}wjwpNYp1`?s7LBlTj4ebp`+?NJP^&+*pK5|9>duQ`fY
zvMj4q&!gPZx~%@`qb;l}7GL^{H#CHW6K{8hCDetJS9gXyFw0u+augTNZx2mSC|9~D
z3swOXo9U=7@I_!Zq|7<wg%-xb>aJ-6fs=2bEEEJ#S8@?uI4stIp`t8&KeP={rbV0u
zp$wXAwZ9b6g(he)4#L8>uXbaGOF40W3yF<0|F*LE;}grxDM^0|iC0JcjEk|vaDf+M
zBQfnp(`jXas_*$&crEEE#tloWi|Cq<g-co5KQ_A;zwdQGr-Lo5c^#;+K`^O@p09`r
zqRBt<e1cQ}jmh#^j{|oVHR&5Sx!|==-FfhH+s*DoV1o9ifAxE(U48zdFHnYl`kp?+
zALVRqP7S}m)s;Kh8QKcHzZrM(nEjQbak||BY-u_4{$MQS+kgqbe{nEwX?c+1BiJ9w
z6%8a!jD9X%0m(x2`ujamn!U?<`28#Uy=qGWwc@Gh^}CMT0$Dr!{z2Et$)F=dtxr9k
z_Qf1%*F*0=8)Q)X)YGn06+s@MPbJ<kIm|vqUAxJM)!#G&r9WRl>%#(Kf4cD8;7r6m
mMZJ8o`U1005ClOG|Fd68({P#Bnl8lv0000<MNUMnLSTXsZy78A
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_030.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="30"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="30" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_060.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="60"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="60" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_090.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="90"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="90" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_120.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="120"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="120" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_150.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="150"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="150" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_180.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="180"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="180" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_210.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="210"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="210" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_240.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="240"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="240" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_270.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="270"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="270" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_300.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="300"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="300" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_330.xml
@@ -0,0 +1,6 @@
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/tabs_synced"
+    android:fromDegrees="330"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:toDegrees="330" />
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/tabs_synced_animation.xml
@@ -0,0 +1,44 @@
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+    android:oneshot="false" >
+
+    <!-- Forgive me, but this is actually a reasonable way to get a
+         frame-by-frame rotation animation without adding lots of small
+         resources. The inner drawables rotate the master drawable. -->
+    <item
+        android:drawable="@drawable/tabs_synced"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_030"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_060"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_090"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_120"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_150"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_180"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_210"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_240"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_270"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_300"
+        android:duration="100"/>
+    <item
+        android:drawable="@drawable/tabs_synced_330"
+        android:duration="100"/>
+
+</animation-list>
--- a/mobile/android/base/resources/layout/bookmark_edit.xml
+++ b/mobile/android/base/resources/layout/bookmark_edit.xml
@@ -4,28 +4,31 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="fill_parent"
               android:orientation="vertical"
               android:layout_height="fill_parent">
 
-    <EditText android:id="@+id/edit_bookmark_name"
+    <org.mozilla.gecko.widget.FloatingHintEditText
+              android:id="@+id/edit_bookmark_name"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:singleLine="true"
               android:hint="@string/bookmark_edit_name"/>
 
-    <EditText android:id="@+id/edit_bookmark_location"
+    <org.mozilla.gecko.widget.FloatingHintEditText
+              android:id="@+id/edit_bookmark_location"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:singleLine="true"
               android:hint="@string/bookmark_edit_location"
               android:inputType="textNoSuggestions"/>
 
-    <EditText android:id="@+id/edit_bookmark_keyword"
+    <org.mozilla.gecko.widget.FloatingHintEditText
+              android:id="@+id/edit_bookmark_keyword"
               android:layout_width="fill_parent"
               android:layout_height="wrap_content"
               android:singleLine="true"
               android:hint="@string/bookmark_edit_keyword"/>
 
 </LinearLayout>
--- a/mobile/android/base/resources/values-v11/themes.xml
+++ b/mobile/android/base/resources/values-v11/themes.xml
@@ -55,11 +55,12 @@
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeStyle">@style/GeckoActionBar</item>
         <item name="android:actionButtonStyle">@style/GeckoActionBar.Button</item>
         <item name="android:actionModeCutDrawable">@drawable/ab_cut</item>
         <item name="android:actionModeCopyDrawable">@drawable/ab_copy</item>
         <item name="android:actionModePasteDrawable">@drawable/ab_paste</item>
         <item name="android:actionModeSelectAllDrawable">@drawable/ab_select_all</item>
+        <item name="floatingHintEditTextStyle">@style/FloatingHintEditText</item>
     </style>
 
 </resources>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -239,10 +239,14 @@
     <declare-styleable name="TabMenuStrip">
         <attr name="strip" format="reference"/>
     </declare-styleable>
 
     <declare-styleable name="EllipsisTextView">
         <attr name="ellipsizeAtLine" format="integer"/>
     </declare-styleable>
 
+    <declare-styleable name="FloatingHintEditText">
+        <attr name="floatingHintEditTextStyle" format="reference" />
+    </declare-styleable>
+
 </resources>
 
--- a/mobile/android/base/resources/values/colors.xml
+++ b/mobile/android/base/resources/values/colors.xml
@@ -45,16 +45,17 @@
   <color name="text_color_tertiary_inverse">#A4A7A9</color>
 
   <!-- Disabled colors -->
   <color name="text_color_primary_disable_only">#999999</color>
 
   <!-- Hint colors -->
   <color name="text_color_hint">#666666</color>
   <color name="text_color_hint_inverse">#7F828A</color>
+  <color name="text_color_hint_floating_focused">#33b5e5</color>
 
   <!-- Highlight colors -->
   <color name="text_color_highlight">#FF9500</color>
   <color name="text_color_highlight_inverse">#D06BFF</color>
 
   <!-- Link colors -->
   <color name="text_color_link">#22629E</color>
 
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -679,9 +679,13 @@
     </style>
 
     <style name="TabInput.Tab">
         <item name="android:background">@drawable/tabs_panel_indicator</item>
         <item name="android:gravity">center</item>
         <item name="android:minHeight">@dimen/menu_item_row_height</item>
     </style>
 
+    <style name="FloatingHintEditText" parent="android:style/Widget.EditText">
+        <item name="android:paddingTop">0dp</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -87,13 +87,14 @@
         <item name="panelGridViewStyle">@style/Widget.PanelGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
         <item name="homeListViewStyle">@style/Widget.HomeListView</item>
         <item name="geckoMenuListViewStyle">@style/Widget.GeckoMenuListView</item>
         <item name="menuItemDefaultStyle">@style/Widget.MenuItemDefault</item>
         <item name="menuItemActionBarStyle">@style/Widget.MenuItemActionBar</item>
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="menuItemShareActionButtonStyle">@style/Widget.MenuItemSecondaryActionBar</item>
+        <item name="floatingHintEditTextStyle">@style/FloatingHintEditText</item>
     </style>
 
     <style name="Gecko.Preferences" parent="GeckoPreferencesBase"/>
 
 </resources>
--- a/mobile/android/base/resources/xml/preferences_home.xml
+++ b/mobile/android/base/resources/xml/preferences_home.xml
@@ -8,16 +8,21 @@
                   android:title="@string/pref_category_home"
                   android:enabled="false">
 
     <org.mozilla.gecko.preferences.PanelsPreferenceCategory
         android:title="@string/pref_category_home_panels"/>
 
     <PreferenceCategory android:title="@string/pref_category_home_content_settings">
 
+        <CheckBoxPreference android:key="android.not_a_preference.home_suggested_sites"
+                            android:title="@string/pref_home_suggested_sites"
+                            android:summary="@string/pref_home_suggested_sites_summary"
+                            android:defaultValue="true" />
+
         <ListPreference android:key="home.sync.updateMode"
                         android:title="@string/pref_home_updates"
                         android:entries="@array/pref_home_updates_entries"
                         android:entryValues="@array/pref_home_updates_values"
                         android:persistent="false" />
 
     </PreferenceCategory>
 
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -125,16 +125,18 @@
   <string name="pref_home_add_panel">&pref_home_add_panel;</string>
   <string name="home_add_panel_title">&home_add_panel_title;</string>
   <string name="home_add_panel_empty">&home_add_panel_empty;</string>
   <string name="home_add_panel_installed">&home_add_panel_installed;</string>
   <string name="pref_category_home_content_settings">&pref_category_home_content_settings;</string>
   <string name="pref_home_updates">&pref_home_updates;</string>
   <string name="pref_home_updates_enabled">&pref_home_updates_enabled;</string>
   <string name="pref_home_updates_wifi">&pref_home_updates_wifi;</string>
+  <string name="pref_home_suggested_sites">&pref_home_suggested_sites;</string>
+  <string name="pref_home_suggested_sites_summary">&pref_home_suggested_sites_summary;</string>
 
   <string name="pref_header_customize">&pref_header_customize;</string>
   <string name="pref_header_display">&pref_header_display;</string>
   <string name="pref_header_privacy_short">&pref_header_privacy_short;</string>
   <string name="pref_header_language">&pref_header_language;</string>
   <string name="pref_header_vendor">&pref_header_vendor;</string>
   <string name="pref_header_devtools">&pref_header_devtools;</string>
 
@@ -229,16 +231,17 @@
   <string name="reload">&reload;</string>
   <string name="forward">&forward;</string>
   <string name="menu">&menu;</string>
   <string name="back">&back;</string>
   <string name="stop">&stop;</string>
   <string name="site_security">&site_security;</string>
   <string name="close_tab">&close_tab;</string>
   <string name="new_tab_opened">&new_tab_opened;</string>
+  <string name="new_private_tab_opened">&new_private_tab_opened;</string>
   <string name="one_tab">&one_tab;</string>
   <string name="num_tabs">&num_tabs2;</string>
   <string name="addons">&addons;</string>
   <string name="downloads">&downloads;</string>
   <string name="apps">&apps;</string>
   <string name="char_encoding">&char_encoding;</string>
   <string name="new_tab">&new_tab;</string>
   <string name="new_private_tab">&new_private_tab;</string>
--- a/mobile/android/base/tabspanel/RemoteTabsContainerPanel.java
+++ b/mobile/android/base/tabspanel/RemoteTabsContainerPanel.java
@@ -1,37 +1,50 @@
 /* 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.tabspanel;
 
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.TabsAccessor;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.tabspanel.TabsPanel.Panel;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout;
 
 import android.accounts.Account;
 import android.content.Context;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 
 /**
- * Provides a container to wrap the list of synced tabs and provide swipe-to-refresh support. The
- * only child view should be an instance of {@link RemoteTabsList}.
+ * Provides a container to wrap the list of synced tabs and provide
+ * swipe-to-refresh support. The only child view should be an instance of
+ * {@link RemoteTabsList}.
  */
 public class RemoteTabsContainerPanel extends GeckoSwipeRefreshLayout
                                  implements TabsPanel.PanelView {
     private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "tabs" };
 
+    /**
+     * Refresh indicators (the swipe-to-refresh "laser show" and the spinning
+     * icon) will never be shown for less than the following duration, in
+     * milliseconds.
+     */
+    private static final long MINIMUM_REFRESH_INDICATOR_DURATION_IN_MS = 12 * 100; // 12 frames, 100 ms each.
+
     private final Context context;
     private final RemoteTabsSyncObserver syncListener;
+    private TabsPanel panel;
     private RemoteTabsList list;
 
     // Whether or not a sync status listener is attached.
     private boolean isListening = false;
 
     public RemoteTabsContainerPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
         this.context = context;
@@ -49,33 +62,39 @@ public class RemoteTabsContainerPanel ex
         // Must be called after the child view has been added.
         setColorScheme(R.color.swipe_refresh_orange_dark, R.color.background_tabs,
                        R.color.swipe_refresh_orange_dark, R.color.background_tabs);
     }
 
 
     @Override
     public boolean canChildScrollUp() {
-        // We are not supporting swipe-to-refresh for old sync. This disables the swipe gesture if
-        // no FxA are detected.
+        // We are not supporting swipe-to-refresh for old sync. This disables
+        // the swipe gesture if no FxA are detected.
         if (FirefoxAccounts.firefoxAccountsExist(getContext())) {
             return super.canChildScrollUp();
         } else {
             return true;
         }
     }
 
     @Override
     public void setTabsPanel(TabsPanel panel) {
+        this.panel = panel;
         list.setTabsPanel(panel);
     }
 
     @Override
     public void show() {
+        // Start fetching remote tabs.
         TabsAccessor.getTabs(context, list);
+        // The user can trigger a tabs sync, so we want to be very certain the
+        // locally-persisted tabs are fresh (tab writes are batched and delayed).
+        Tabs.getInstance().persistAllTabs();
+
         if (!isListening) {
             isListening = true;
             FirefoxAccounts.addSyncStatusListener(syncListener);
         }
         setVisibility(View.VISIBLE);
     }
 
     @Override
@@ -98,31 +117,69 @@ public class RemoteTabsContainerPanel ex
             if (FirefoxAccounts.firefoxAccountsExist(getContext())) {
                 final Account account = FirefoxAccounts.getFirefoxAccount(getContext());
                 FirefoxAccounts.requestSync(account, FirefoxAccounts.FORCE, STAGES_TO_SYNC_ON_REFRESH, null);
             }
         }
     }
 
     private class RemoteTabsSyncObserver implements FirefoxAccounts.SyncStatusListener {
+        // Written on the main thread, and read off the main thread, but no need
+        // to synchronize.
+        protected volatile long lastSyncStarted = 0;
+
         @Override
         public Context getContext() {
             return RemoteTabsContainerPanel.this.getContext();
         }
 
         @Override
         public Account getAccount() {
             return FirefoxAccounts.getFirefoxAccount(getContext());
         }
 
-        public void onSyncFinished() {
+        public void onSyncStarted() {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    TabsAccessor.getTabs(context, list);
-                    setRefreshing(false);
+                    lastSyncStarted = System.currentTimeMillis();
+
+                    // Get the sync icon and start the drawable's animation.
+                    final Drawable iconDrawable = panel.getIconDrawable(Panel.REMOTE_TABS);
+                    if (iconDrawable instanceof AnimationDrawable) {
+                        ((AnimationDrawable) iconDrawable).start();
+                    }
                 }
             });
         }
 
-        public void onSyncStarted() {}
+        public void onSyncFinished() {
+            final Handler uiHandler = ThreadUtils.getUiHandler();
+
+            // We want to update the list immediately ...
+            uiHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    TabsAccessor.getTabs(context, list);
+                }
+            });
+
+            // ... but we want the refresh indicators to persist for long enough
+            // to be visible.
+            final long last = lastSyncStarted;
+            final long now = System.currentTimeMillis();
+            final long delay = Math.max(0, MINIMUM_REFRESH_INDICATOR_DURATION_IN_MS - (now - last));
+
+            uiHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    setRefreshing(false);
+
+                    // Get the sync icon and stop the drawable's animation.
+                    final Drawable iconDrawable = panel.getIconDrawable(Panel.REMOTE_TABS);
+                    if (iconDrawable instanceof AnimationDrawable) {
+                        ((AnimationDrawable) iconDrawable).stop();
+                    }
+                }
+            }, delay);
+        }
     }
 }
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -15,16 +15,17 @@ import org.mozilla.gecko.LightweightThem
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
@@ -135,17 +136,18 @@ public class TabsPanel extends LinearLay
         });
 
         mTabWidget = (IconTabWidget) findViewById(R.id.tab_widget);
 
         mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal);
         mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private);
 
         if (!GeckoProfile.get(mContext).inGuestMode()) {
-            mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
+            // n.b.: the animation does not start automatically.
+            mTabWidget.addTab(R.drawable.tabs_synced_animation, R.string.tabs_synced);
         }
 
         mTabWidget.setTabSelectionListener(this);
     }
 
     public void addTab() {
         if (mCurrentPanel == Panel.NORMAL_TABS) {
            mActivity.addTab();
@@ -456,9 +458,18 @@ public class TabsPanel extends LinearLay
     public void setTabsLayoutChangeListener(TabsLayoutChangeListener listener) {
         mLayoutChangeListener = listener;
     }
 
     private void dispatchLayoutChange(int width, int height) {
         if (mLayoutChangeListener != null)
             mLayoutChangeListener.onTabsLayoutChange(width, height);
     }
+
+    /**
+     * Fetch the Drawable icon corresponding to the given panel.
+     * @param panel to fetch icon for.
+     * @return Drawable instance, or null if no icon is being displayed, or the icon does not exist.
+     */
+    public Drawable getIconDrawable(Panel panel) {
+        return mTabWidget.getIconDrawable(panel.ordinal());
+    }
 }
--- a/mobile/android/base/widget/ButtonToast.java
+++ b/mobile/android/base/widget/ButtonToast.java
@@ -24,25 +24,26 @@ import org.mozilla.gecko.animation.Prope
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.View;
 import android.widget.Button;
 import android.widget.TextView;
 
 public class ButtonToast {
+    @SuppressWarnings("unused")
     private final static String LOGTAG = "GeckoButtonToast";
+
     private final static int TOAST_DURATION = 5000;
 
     private final View mView;
     private final TextView mMessageView;
     private final Button mButton;
     private final Handler mHideHandler = new Handler();
 
-    private final ToastListener mListener;
     private final LinkedList<Toast> mQueue = new LinkedList<Toast>();
     private Toast mCurrentToast;
 
     public enum ReasonHidden {
         CLICKED,
         TIMEOUT,
         STARTUP
     }
@@ -65,17 +66,16 @@ public class ButtonToast {
 
     public interface ToastListener {
         void onButtonClicked();
         void onToastHidden(ReasonHidden reason);
     }
 
     public ButtonToast(View view) {
         mView = view;
-        mListener = null;
         mMessageView = (TextView) mView.findViewById(R.id.toast_message);
         mButton = (Button) mView.findViewById(R.id.toast_button);
         mButton.setOnClickListener(new View.OnClickListener() {
                     @Override
                     public void onClick(View view) {
                         Toast t = mCurrentToast;
                         if (t == null)
                             return;
@@ -86,35 +86,48 @@ public class ButtonToast {
                         }
                     }
                 });
 
         hide(true, ReasonHidden.STARTUP);
     }
 
     public void show(boolean immediate, CharSequence message,
+                     CharSequence buttonMessage, int buttonDrawableId,
+                     ToastListener listener) {
+        final Drawable d = mView.getContext().getResources().getDrawable(buttonDrawableId);
+        show(false, message, buttonMessage, d, listener);
+    }
+
+    public void show(boolean immediate, CharSequence message,
                      CharSequence buttonMessage, Drawable buttonDrawable,
                      ToastListener listener) {
         show(new Toast(message, buttonMessage, buttonDrawable, listener), immediate);
     }
 
     private void show(Toast t, boolean immediate) {
         // If we're already showing a toast, add this one to the queue to show later
         if (mView.getVisibility() == View.VISIBLE) {
             mQueue.offer(t);
             return;
         }
 
         mCurrentToast = t;
         mButton.setEnabled(true);
 
-        mMessageView.setText(t.message);
-        mButton.setText(t.buttonMessage);
-        mButton.setCompoundDrawablePadding(mView.getContext().getResources().getDimensionPixelSize(R.dimen.toast_button_padding));
-        mButton.setCompoundDrawablesWithIntrinsicBounds(t.buttonDrawable, null, null, null);
+        // Our toast is re-used, so we update all fields to clear any old values.
+        mMessageView.setText(null != t.message ? t.message : "");
+        mButton.setText(null != t.buttonMessage ? t.buttonMessage : "");
+        if (null != t.buttonDrawable) {
+            mButton.setCompoundDrawablePadding(mView.getContext().getResources().getDimensionPixelSize(R.dimen.toast_button_padding));
+            mButton.setCompoundDrawablesWithIntrinsicBounds(t.buttonDrawable, null, null, null);
+        } else {
+            mButton.setCompoundDrawablePadding(0);
+            mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+        }
 
         mHideHandler.removeCallbacks(mHideRunnable);
         mHideHandler.postDelayed(mHideRunnable, TOAST_DURATION);
 
         mView.setVisibility(View.VISIBLE);
         int duration = immediate ? 0 : mView.getResources().getInteger(android.R.integer.config_longAnimTime);
 
         PropertyAnimator animator = new PropertyAnimator(duration);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/FloatingHintEditText.java
@@ -0,0 +1,168 @@
+/* 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.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.EditText;
+
+import org.mozilla.gecko.R;
+
+public class FloatingHintEditText extends EditText {
+    private static enum Animation { NONE, SHRINK, GROW }
+    private static final float HINT_SCALE = 0.6f;
+    private static final int ANIMATION_STEPS = 6;
+
+    private final Paint floatingHintPaint = new Paint();
+    private final ColorStateList floatingHintColors;
+    private final ColorStateList normalHintColors;
+    private final int defaultFloatingHintColor;
+    private final int defaultNormalHintColor;
+
+    private boolean wasEmpty;
+
+    private int animationFrame;
+    private Animation animation = Animation.NONE;
+
+    public FloatingHintEditText(Context context) {
+        this(context, null);
+    }
+
+    public FloatingHintEditText(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.floatingHintEditTextStyle);
+    }
+
+    public FloatingHintEditText(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        floatingHintColors = getResources().getColorStateList(R.color.floating_hint_text);
+        normalHintColors = getHintTextColors();
+        defaultFloatingHintColor = floatingHintColors.getDefaultColor();
+        defaultNormalHintColor = normalHintColors.getDefaultColor();
+        wasEmpty = TextUtils.isEmpty(getText());
+    }
+
+    @Override
+    public int getCompoundPaddingTop() {
+        final FontMetricsInt metrics = getPaint().getFontMetricsInt();
+        final int floatingHintHeight = (int) ((metrics.bottom - metrics.top) * HINT_SCALE);
+        return super.getCompoundPaddingTop() + floatingHintHeight;
+    }
+
+    @Override
+    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+        super.onTextChanged(text, start, lengthBefore, lengthAfter);
+
+        final boolean isEmpty = TextUtils.isEmpty(getText());
+
+        // The empty state hasn't changed, so the hint stays the same.
+        if (wasEmpty == isEmpty) {
+            return;
+        }
+
+        wasEmpty = isEmpty;
+
+        // Don't animate if we aren't visible.
+        if (!isShown()) {
+            return;
+        }
+
+        if (isEmpty) {
+            animation = Animation.GROW;
+
+            // The TextView will show a hint since the field is empty, but since we're animating
+            // from the floating hint, we don't want the normal hint to appear yet. We set it to
+            // transparent here, then restore the hint color after the animation has finished.
+            setHintTextColor(Color.TRANSPARENT);
+        } else {
+            animation = Animation.SHRINK;
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (TextUtils.isEmpty(getHint())) {
+            return;
+        }
+
+        final boolean isAnimating = (animation != Animation.NONE);
+
+        // The large hint is drawn by Android, so do nothing.
+        if (!isAnimating && TextUtils.isEmpty(getText())) {
+            return;
+        }
+
+        final Paint paint = getPaint();
+        final float hintPosX = getCompoundPaddingLeft() + getScrollX();
+        final float normalHintPosY = getBaseline();
+        final float floatingHintPosY = normalHintPosY + paint.getFontMetricsInt().top + getScrollY();
+        final float normalHintSize = getTextSize();
+        final float floatingHintSize = normalHintSize * HINT_SCALE;
+        final int[] stateSet = getDrawableState();
+        final int floatingHintColor = floatingHintColors.getColorForState(stateSet, defaultFloatingHintColor);
+
+        floatingHintPaint.set(paint);
+
+        // If we're not animating, we're showing the floating hint, so draw it and bail.
+        if (!isAnimating) {
+            drawHint(canvas, floatingHintSize, floatingHintColor, hintPosX, floatingHintPosY);
+            return;
+        }
+
+        // We are animating, so draw the linearly interpolated frame.
+        final int normalHintColor = normalHintColors.getColorForState(stateSet, defaultNormalHintColor);
+        if (animation == Animation.SHRINK) {
+            drawAnimationFrame(canvas, normalHintSize, floatingHintSize,
+                    hintPosX, normalHintPosY, floatingHintPosY, normalHintColor, floatingHintColor);
+        } else {
+            drawAnimationFrame(canvas, floatingHintSize, normalHintSize,
+                    hintPosX, floatingHintPosY, normalHintPosY, floatingHintColor, normalHintColor);
+        }
+
+        animationFrame++;
+
+        if (animationFrame == ANIMATION_STEPS) {
+            // After the grow animation has finished, restore the normal TextView hint color that we
+            // removed in our onTextChanged listener.
+            if (animation == Animation.GROW) {
+                setHintTextColor(normalHintColors);
+            }
+
+            animation = Animation.NONE;
+            animationFrame = 0;
+        }
+
+        invalidate();
+    }
+
+    private void drawAnimationFrame(Canvas canvas, float fromSize, float toSize,
+                                    float hintPosX, float fromY, float toY, int fromColor, int toColor) {
+        final float textSize = lerp(fromSize, toSize);
+        final float hintPosY = lerp(fromY, toY);
+        final int color = Color.rgb((int) lerp(Color.red(fromColor), Color.red(toColor)),
+                                    (int) lerp(Color.green(fromColor), Color.green(toColor)),
+                                    (int) lerp(Color.blue(fromColor), Color.blue(toColor)));
+        drawHint(canvas, textSize, color, hintPosX, hintPosY);
+    }
+
+    private void drawHint(Canvas canvas, float textSize, int color, float x, float y) {
+        floatingHintPaint.setTextSize(textSize);
+        floatingHintPaint.setColor(color);
+        canvas.drawText(getHint().toString(), x, y, floatingHintPaint);
+    }
+
+    private float lerp(float from, float to) {
+        final float alpha = (float) animationFrame / (ANIMATION_STEPS - 1);
+        return from * (1 - alpha) + to * alpha;
+    }
+}
--- a/mobile/android/base/widget/IconTabWidget.java
+++ b/mobile/android/base/widget/IconTabWidget.java
@@ -3,22 +3,23 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.widget;
 
 import org.mozilla.gecko.R;
 
 import android.content.Context;
 import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ImageButton;
+import android.widget.TabWidget;
 import android.widget.TextView;
-import android.widget.TabWidget;
 
 public class IconTabWidget extends TabWidget {
     private OnTabChangedListener mListener;
     private final int mButtonLayoutId;
     private final boolean mIsIcon;
 
     public static interface OnTabChangedListener {
         public void onTabChanged(int tabIndex);
@@ -67,9 +68,29 @@ public class IconTabWidget extends TabWi
         }
 
         @Override
         public void onClick(View view) {
             if (mListener != null)
                 mListener.onTabChanged(mIndex);
         }
     }
+
+    /**
+     * Fetch the Drawable icon corresponding to the given panel.
+     * @param panel to fetch icon for.
+     * @return Drawable instance, or null if no icon is being displayed, or the icon does not exist.
+     */
+    public Drawable getIconDrawable(int index) {
+        if (!mIsIcon) {
+            return null;
+        }
+        // We can have multiple views in the tray for each child. This finds the
+        // first view corresponding to the given tab. This varies by Android
+        // version. The first view should always be our ImageButton, but let's
+        // be safe.
+        final View view = getChildTabViewAt(index);
+        if (view instanceof ImageButton) {
+            return ((ImageButton) view).getDrawable();
+        }
+        return null;
+    }
 }
--- a/mobile/android/chrome/content/aboutDevices.js
+++ b/mobile/android/chrome/content/aboutDevices.js
@@ -31,16 +31,21 @@ var Devices = {
     Services.obs.addObserver(this, EVENT_SERVICE_FOUND, false);
     Services.obs.addObserver(this, EVENT_SERVICE_LOST, false);
 
     let button = document.getElementById("refresh");
     button.addEventListener("click", () => {
       this.updateDeviceList();
     }, false);
 
+    let manual = document.getElementById("connect");
+    manual.addEventListener("click", (evt) => {
+      this.connectManually(evt);
+    }, false);
+
     this._savedSearchInterval = SimpleServiceDiscovery.search(SEARCH_INTERVAL_IN_MILLISECONDS);
 
     this.updateDeviceList();
   },
 
   uninit: function() {
     dump("Uninitializing.");
     Services.obs.removeObserver(this, EVENT_SERVICE_FOUND);
@@ -82,12 +87,52 @@ var Devices = {
     }
   },
 
   observe: function(subject, topic, data) {
     if (topic == EVENT_SERVICE_FOUND || topic == EVENT_SERVICE_LOST) {
       this.updateDeviceList();
     }
   },
+
+  _fixedTargetForType: function(type, ip) {
+    let fixedTarget = {};
+    if (type == "roku") {
+      fixedTarget.target = "roku:ecp";
+      fixedTarget.location = "http://" + ip + ":8060";
+    } else if (type == "chromecast") {
+      fixedTarget.target = "urn:dial-multiscreen-org:service:dial:1";
+      fixedTarget.location = "http://" + ip + ":8008";
+    }
+    return fixedTarget;
+  },
+
+  connectManually: function(evt) {
+    // Since there is no form submit event, this is not validated. However,
+    // after we process this event, the element's validation state is updated.
+    let ip = document.getElementById("ip");
+    if (!ip.checkValidity()) {
+      dump("Manually entered IP address is not valid!");
+      return;
+    }
+
+    let fixedTargets = [];
+    try {
+      fixedTargets = JSON.parse(Services.prefs.getCharPref("browser.casting.fixedTargets"));
+    } catch (e) {}
+
+    let type = document.getElementById("type").value;
+    let fixedTarget = this._fixedTargetForType(type, ip.value);
+
+    // Early abort if we're already looking for this target.
+    if (fixedTargets.indexOf(fixedTarget) > -1)
+      return;
+
+    fixedTargets.push(fixedTarget);
+    Services.prefs.setCharPref("browser.casting.fixedTargets", JSON.stringify(fixedTargets));
+
+    // The backend does not yet listen for pref changes, so we trigger a scan.
+    this.updateDeviceList();
+  },
 };
 
 window.addEventListener("load", Devices.init.bind(Devices), false);
 window.addEventListener("unload", Devices.uninit.bind(Devices), false);
--- a/mobile/android/chrome/content/aboutDevices.xhtml
+++ b/mobile/android/chrome/content/aboutDevices.xhtml
@@ -25,11 +25,25 @@
 
 <body>
   <h1>&aboutDevices.header;</h1>
 
   <ul id="devices-list"></ul>
 
   <button id="refresh">&aboutDevices.refresh;</button>
 
+  <h1>&aboutDevices.addDeviceHeader;</h1>
+
+  <div id="manual">
+    <select id="type">
+      <option value="roku">&aboutDevices.roku;</option>
+      <option value="chromecast">&aboutDevices.chromecast;</option>
+    </select>
+
+    <input id="ip" type="text" required="required"
+           pattern="((^|\.)((25[0-5])|(2[0-4]\d)|(1\d\d)|([1-9]?\d))){4}$"
+           placeholder="&aboutDevices.placeholder;" />
+    <button id="connect">&aboutDevices.connectManually;</button>
+  </div>
+
   <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutDevices.js"></script>
 </body>
 </html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -480,33 +480,43 @@ var BrowserApp = {
 
   initContextMenu: function ba_initContextMenu() {
     // TODO: These should eventually move into more appropriate classes
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
       NativeWindow.contextmenus.linkOpenableNonPrivateContext,
       function(aTarget) {
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
-        BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
+        let tab = BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
 
         let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
         let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
-        NativeWindow.toast.show(label, "short");
+        NativeWindow.toast.show(label, "long", {
+          button: {
+            icon: "drawable://select_opened_tab",
+            callback: () => { BrowserApp.selectTab(tab); },
+          }
+        });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"),
       NativeWindow.contextmenus.linkOpenableContext,
       function(aTarget) {
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
-        BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
+        let tab = BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
 
         let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
         let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
-        NativeWindow.toast.show(label, "short");
+        NativeWindow.toast.show(label, "long", {
+          button: {
+            icon: "drawable://select_opened_tab",
+            callback: () => { BrowserApp.selectTab(tab); },
+          }
+        });
       });
 
     NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"),
       NativeWindow.contextmenus.linkCopyableContext,
       function(aTarget) {
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
       });
@@ -1759,22 +1769,30 @@ var NativeWindow = {
       let msg = {
         type: "Toast:Show",
         message: aMessage,
         duration: aDuration
       };
 
       if (aOptions && aOptions.button) {
         msg.button = {
-          label: aOptions.button.label,
           id: uuidgen.generateUUID().toString(),
+        };
+
+        // null is badly handled by the receiver, so try to avoid including nulls.
+        if (aOptions.button.label) {
+          msg.button.label = aOptions.button.label;
+        }
+
+        if (aOptions.button.icon) {
           // If the caller specified a button, make sure we convert any chrome urls
           // to jar:jar urls so that the frontend can show them
-          icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null,
+          msg.button.icon = resolveGeckoURI(aOptions.button.icon);
         };
+
         this._callbacks[msg.button.id] = aOptions.button.callback;
       }
 
       sendMessageToJava(msg);
     }
   },
 
   pageactions: {
--- a/mobile/android/locales/en-US/chrome/aboutDevices.dtd
+++ b/mobile/android/locales/en-US/chrome/aboutDevices.dtd
@@ -1,7 +1,14 @@
 <!-- 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/. -->
 
 <!ENTITY aboutDevices.title              "Devices">
 <!ENTITY aboutDevices.header             "Your devices">
 <!ENTITY aboutDevices.refresh            "Refresh">
+<!ENTITY aboutDevices.addDeviceHeader    "Add a device">
+<!ENTITY aboutDevices.roku               "Roku">
+<!ENTITY aboutDevices.chromecast         "Chromecast">
+<!-- Localization note (aboutDevices.placeholder): this is the hint shown to the
+     user prompting them to input the IP address of a casting device. -->
+<!ENTITY aboutDevices.placeholder        "IP address">
+<!ENTITY aboutDevices.connectManually    "Connect manually">
--- a/mobile/locales/en-US/chrome/region.properties
+++ b/mobile/locales/en-US/chrome/region.properties
@@ -26,8 +26,26 @@ gecko.handlerService.schemes.mailto.0.na
 gecko.handlerService.schemes.mailto.0.uriTemplate=https://compose.mail.yahoo.com/?To=%s
 gecko.handlerService.schemes.mailto.1.name=Gmail
 gecko.handlerService.schemes.mailto.1.uriTemplate=https://mail.google.com/mail/?extsrc=mailto&url=%s
 
 # This is the default set of web based feed handlers shown in the reader
 # selection UI
 browser.contentHandlers.types.0.title=My Yahoo!
 browser.contentHandlers.types.0.uri=https://add.my.yahoo.com/rss?url=%s
+
+# Order of suggested websites displayed in the Top Sites panel.
+# Values for these keys must correspond to the name used in the keys that
+# define each suggested website's details. For example:
+# browser.suggestedsites.list.0=NAME
+# browser.suggestedsites.NAME.title=Displayed name
+# browser.suggestedsites.NAME.url=Website URL
+# browser.suggestedsites.NAME.bgcolor= Color (hex format)
+browser.suggestedsites.list.0=mozilla
+browser.suggestedsites.list.1=fxmarketplace
+
+browser.suggestedsites.mozilla.title=Mozilla
+browser.suggestedsites.mozilla.url=https://mozilla.org/en-US/
+browser.suggestedsites.mozilla.bgcolor=#c13832
+
+browser.suggestedsites.fxmarketplace.title=Firefox Marketplace
+browser.suggestedsites.fxmarketplace.url=https://marketplace.firefox.com/
+browser.suggestedsites.fxmarketplace.bgcolor=#0095dd
\ No newline at end of file
--- a/python/mozbuild/mozbuild/action/generate_suggestedsites.py
+++ b/python/mozbuild/mozbuild/action/generate_suggestedsites.py
@@ -2,47 +2,104 @@
 # 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/.
 
 ''' Script to generate the suggestedsites.json file for Fennec.
 
 This script follows these steps:
 
-1. Looks for a 'list.txt' file in one of the given source directories
-(see srcdir). The list.txt contains the list of site names, one per line.
+1. Read the region.properties file in all the given source directories
+(see srcdir option). Merge all properties into a single dict accounting for
+the priority of source directories.
 
-2. For each site name found in 'list.txt', it tries to find a matching
-.json file in one of the source directories.
+2. Read the list of sites from the 'browser.suggestedsites.list.INDEX'
+properties with value of these keys being an identifier for each suggested site
+e.g. browser.suggestedsites.list.0=mozilla, browser.suggestedsites.list.1=fxmarketplace.
 
-3. For each json file, load it and define the respective imageurl
-based on a image URL template composed by the target Android package name
-and the site name.
+3. For each site identifier defined by the list keys, look for matching branches
+containing the respective properties i.e. url, title, etc. For example,
+for a 'mozilla' identifier, we'll look for keys like:
+browser.suggestedsites.mozilla.url, browser.suggestedsites.mozilla.title, etc.
 
-4. Join the JSON representation of each site into a JSON array and write
-the result to suggestedsites.json on the locale-specific raw resource
+4. Generate a JSON representation of each site, join them in a JSON array, and
+write the result to suggestedsites.json on the locale-specific raw resource
 directory e.g. raw/suggestedsites.json, raw-pt-rBR/suggestedsites.json.
 '''
 
 from __future__ import print_function
 
 import argparse
 import json
+import re
 import sys
 import os
 
 from mozbuild.util import (
     FileAvoidWrite,
 )
 from mozpack.files import (
     FileFinder,
 )
 import mozpack.path as mozpath
 
 
+def read_properties_file(filename):
+    """Reads a properties file into a dict.
+
+    Ignores empty, comment lines, and keys not starting with the prefix for
+    suggested sites ('browser.suggestedsites'). Removes the prefix from all
+    matching keys i.e. turns 'browser.suggestedsites.foo' into simply 'foo'
+    """
+    prefix = 'browser.suggestedsites.'
+    properties = {}
+    for l in open(filename, 'rt').readlines():
+        line = l.strip()
+        if not line.startswith(prefix):
+            continue
+        (k, v) = re.split('\s*=\s*', line, 1)
+        properties[k[len(prefix):]] = v
+    return properties
+
+
+def merge_properties(filename, srcdirs):
+    """Merges properties from the given file in the given source directories."""
+    properties = {}
+    for srcdir in srcdirs:
+        path = mozpath.join(srcdir, filename)
+        try:
+            properties.update(read_properties_file(path))
+        except IOError, e:
+            # Ignore non-existing files
+            continue
+    return properties
+
+
+def get_site_list_from_properties(properties):
+    """Turns {'list.0':'foo', 'list.1':'bar'} into ['foo', 'bar']."""
+    prefix = 'list.'
+    indexes = []
+    for k, v in properties.iteritems():
+        if not k.startswith(prefix):
+            continue
+        indexes.append(int(k[len(prefix):]))
+    return [properties[prefix + str(index)] for index in sorted(indexes)]
+
+
+def get_site_from_properties(name, properties):
+    """Turns {'foo.title':'title', ...} into {'title':'title', ...}."""
+    prefix = '{name}.'.format(name=name)
+    try:
+        site = dict((k, properties[prefix + k]) for k in ('title', 'url', 'bgcolor'))
+    except IndexError, e:
+        raise Exception("Could not find required property for '{name}: {error}'"
+                        .format(name=name, error=str(e)))
+    return site
+
+
 def main(args):
     parser = argparse.ArgumentParser()
     parser.add_argument('--verbose', '-v', default=False, action='store_true',
                         help='be verbose')
     parser.add_argument('--silent', '-s', default=False, action='store_true',
                         help='be silent')
     parser.add_argument('--android-package-name', metavar='NAME',
                         required=True,
@@ -52,41 +109,31 @@ def main(args):
                         help='optional Android resource directory to find drawables in')
     parser.add_argument('--srcdir', metavar='SRCDIR',
                         action='append', required=True,
                         help='directories to read inputs from, in order of priority')
     parser.add_argument('output', metavar='OUTPUT',
                         help='output')
     opts = parser.parse_args(args)
 
-    def resolve_filename(filename):
-        for srcdir in opts.srcdir:
-            path = mozpath.join(srcdir, filename)
-            if os.path.exists(path):
-                return path
-        return None
-
-    # The list.txt file has one site name per line.
-    names = [s.strip() for s in open(resolve_filename('list.txt'), 'rt').readlines()]
+    # Use reversed order so that the first srcdir has higher priority to override keys.
+    all_properties = merge_properties('region.properties', reversed(opts.srcdir))
+    names = get_site_list_from_properties(all_properties)
     if opts.verbose:
         print('Reading {len} suggested sites: {names}'.format(len=len(names), names=names))
 
     # Keep these two in sync.
     image_url_template = 'android.resource://%s/drawable/suggestedsites_{name}' % opts.android_package_name
     drawables_template = 'drawable*/suggestedsites_{name}.*'
 
-    # Load json files corresponding to each site name and define their
+    # Load properties corresponding to each site name and define their
     # respective image URL.
     sites = []
     for name in names:
-        filename = resolve_filename(name + '.json')
-        if opts.verbose:
-            print("Reading '{name}' from {filename}"
-                .format(name=name, filename=filename))
-        site = json.load(open(filename, 'rt'))
+        site = get_site_from_properties(name, all_properties)
         site['imageurl'] = image_url_template.format(name=name)
         sites.append(site)
 
         # Now check for existence of an appropriately named drawable.  If none
         # exists, throw.  This stops a locale discovering, at runtime, that the
         # corresponding drawable was not added to en-US.
         if not opts.resources:
             continue
--- a/toolkit/components/urlformatter/Makefile.in
+++ b/toolkit/components/urlformatter/Makefile.in
@@ -1,18 +1,22 @@
 #
 # 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/.
 
-export:: mozilla_api_key google_api_key
+export:: mozilla_api_key google_api_key bing_api_key
 
-EXTRA_PP_COMPONENTS_FLAGS = -I mozilla_api_key -I google_api_key
+EXTRA_PP_COMPONENTS_FLAGS = -I mozilla_api_key -I google_api_key -I bing_api_key
 
 include $(topsrcdir)/config/rules.mk
 
 mozilla_api_key:
 	@echo '#define MOZ_MOZILLA_API_KEY $(MOZ_MOZILLA_API_KEY)' > $@
 
 google_api_key:
 	@echo '#define MOZ_GOOGLE_API_KEY $(MOZ_GOOGLE_API_KEY)' > $@
 
-GARBAGE += google_api_key moz_google_api_key
+bing_api_key:
+	@echo '#define MOZ_BING_API_KEY $(MOZ_BING_API_KEY)' > $@
+	@echo '#define MOZ_BING_API_CLIENTID $(MOZ_BING_API_CLIENTID)' >> $@
+
+GARBAGE += mozilla_api_key google_api_key bing_api_key
--- a/toolkit/components/urlformatter/nsURLFormatter.js
+++ b/toolkit/components/urlformatter/nsURLFormatter.js
@@ -100,16 +100,18 @@ nsURLFormatterService.prototype = {
     APP:              function() this.appInfo.name.toLowerCase().replace(/ /, ""),
     OS:               function() this.appInfo.OS,
     XPCOMABI:         function() this.ABI,
     BUILD_TARGET:     function() this.appInfo.OS + "_" + this.ABI,
     OS_VERSION:       function() this.OSVersion,
     CHANNEL:          function() UpdateChannel.get(),
     MOZILLA_API_KEY:   function() "@MOZ_MOZILLA_API_KEY@",
     GOOGLE_API_KEY:   function() "@MOZ_GOOGLE_API_KEY@",
+    BING_API_CLIENTID:function() "@MOZ_BING_API_CLIENTID@",
+    BING_API_KEY:     function() "@MOZ_BING_API_KEY@",
     DISTRIBUTION:     function() this.distribution.id,
     DISTRIBUTION_VERSION: function() this.distribution.version
   },
 
   formatURL: function uf_formatURL(aFormat) {
     var _this = this;
     var replacementCallback = function(aMatch, aKey) {
       if (aKey in _this._defaults) {