Bug 1201331 (part 3) - add a Synced Tabs panel. r=Gijs
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 03 Dec 2015 10:05:03 +1100
changeset 309367 b48a56ce709d51203c8ff7043946fe44bea4021d
parent 309366 549690eaf36d8d81355fd176e570e6d708f6f1a3
child 309368 5a286e996755193abee301b9bb542cd154c1d495
push id5513
push userraliiev@mozilla.com
push dateMon, 25 Jan 2016 13:55:34 +0000
treeherdermozilla-beta@5ee97dd05b5c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs
bugs1201331
milestone45.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1201331 (part 3) - add a Synced Tabs panel. r=Gijs
browser/app/profile/firefox.js
browser/base/content/browser.css
browser/base/content/browser.xul
browser/base/content/test/general/browser_syncui.js
browser/components/customizableui/CustomizableUI.jsm
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
browser/components/customizableui/test/browser_967000_button_sync.js
browser/components/customizableui/test/browser_987185_syncButton.js
browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js
browser/components/preferences/in-content/sync.js
browser/components/preferences/in-content/sync.xul
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
browser/themes/shared/customizableui/panelUIOverlay.inc.css
browser/themes/shared/fxa/sync-illustration.svg
browser/themes/shared/jar.inc.mn
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1466,16 +1466,21 @@ pref("identity.fxaccounts.remote.profile
 pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
 
 // Whether we display profile images in the UI or not.
 pref("identity.fxaccounts.profile_image.enabled", true);
 
 // Token server used by the FxA Sync identity.
 pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
 
+// URLs for promo links to mobile browsers. Note that consumers are expected to
+// append a value for utm_campaign.
+pref("identity.mobilepromo.android", "https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
+pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
+
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
 pref("identity.fxaccounts.migrateToDevEdition", true);
 #else
 pref("identity.fxaccounts.migrateToDevEdition", false);
 #endif
 
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1131,16 +1131,17 @@ chatbox:-moz-full-screen-ancestor > .cha
 }
 
 #PanelUI-contents > .panel-customization-placeholder > .panel-customization-placeholder-child {
   list-style-image: none;
 }
 
 /* Apply crisp rendering for favicons at exactly 2dppx resolution */
 @media (resolution: 2dppx) {
+  #PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon,
   #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
   #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
   #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
     image-rendering: -moz-crisp-edges;
   }
 }
 
 #customization-panelHolder {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -1033,22 +1033,16 @@
                      ondragenter="newWindowButtonObserver.onDragOver(event)"
                      ondragexit="newWindowButtonObserver.onDragExit(event)"/>
 
       <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                      observes="View:FullScreen"
                      type="checkbox"
                      label="&fullScreenCmd.label;"
                      tooltip="dynamic-shortcut-tooltip"/>
-
-      <toolbarbutton id="sync-button"
-                     class="toolbarbutton-1 chromeclass-toolbar-additional"
-                     observes="sync-status"
-                     label="&syncToolbarButton.label;"
-                     oncommand="gSyncUI.handleToolbarButton()"/>
     </toolbarpalette>
   </toolbox>
 
   <hbox id="fullscr-toggler" hidden="true"/>
 
   <deck id="content-deck" flex="1">
     <hbox flex="1" id="browser">
       <vbox id="browser-border-start" hidden="true" layer="true"/>
--- a/browser/base/content/test/general/browser_syncui.js
+++ b/browser/base/content/test/general/browser_syncui.js
@@ -48,17 +48,17 @@ function promiseObserver(topic) {
       Services.obs.removeObserver(obs, topic);
       resolve(subject);
     }
     Services.obs.addObserver(obs, topic, false);
   });
 }
 
 function checkButtonTooltips(stringPrefix) {
-  for (let butId of ["sync-button", "PanelUI-fxa-icon"]) {
+  for (let butId of ["PanelUI-remotetabs-syncnow", "PanelUI-fxa-icon"]) {
     let text = document.getElementById(butId).getAttribute("tooltiptext");
     let desc = `Text is "${text}", expecting it to start with "${stringPrefix}"`
     Assert.ok(text.startsWith(stringPrefix), desc);
   }
 }
 
 add_task(function* prepare() {
   // add the Sync button to the toolbar so we can get it!
@@ -84,16 +84,18 @@ add_task(function* prepare() {
     window.gSyncUI._needsSetup = oldNeedsSetup;
     // and an observer to set the state back to what it should be now we've
     // restored the stub.
     Services.obs.notifyObservers(null, "weave:service:login:finish", null);
   });
   // and a notification to have the state change away from "needs setup"
   yield notifyAndPromiseUIUpdated("weave:service:login:finish");
   checkBroadcasterVisible("sync-syncnow-state");
+  // open the sync-button panel so we can check elements in that.
+  document.getElementById("sync-button").click();
 });
 
 add_task(function* testSyncNeedsVerification() {
   // mock out the "_needsVerification()" function
   let oldNeedsVerification = window.gSyncUI._needsVerification;
   window.gSyncUI._needsVerification = () => true;
   try {
     // a notification for the state change
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -175,16 +175,17 @@ var CustomizableUIInternal = {
       "history-panelmenu",
       "fullscreen-button",
       "find-button",
       "preferences-button",
       "add-ons-button",
 #ifndef MOZ_DEV_EDITION
       "developer-button",
 #endif
+      "sync-button",
     ];
 
 #ifdef E10S_TESTING_ONLY
     if (gPalette.has("e10s-button")) {
       let newWindowIndex = panelPlacements.indexOf("new-window-button");
       if (newWindowIndex > -1) {
         panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button");
       }
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -25,16 +25,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
   "resource://gre/modules/CharsetMenu.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
   "resource://gre/modules/SocialService.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SyncedTabs",
+  "resource://services-sync/SyncedTabs.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() {
   const kCharsetBundle = "chrome://global/locale/charsetMenu.properties";
   return Services.strings.createBundle(kCharsetBundle);
 });
 XPCOMUtils.defineLazyGetter(this, "BrandBundle", function() {
   const kBrandBundle = "chrome://branding/locale/brand.properties";
   return Services.strings.createBundle(kBrandBundle);
@@ -288,16 +290,217 @@ const CustomizableWidgets = [
       let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows");
       recentlyClosedTabs.addEventListener("click", onRecentlyClosedClick);
       recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick);
     },
     onViewHiding: function(aEvent) {
       LOG("History view is being hidden!");
     }
   }, {
+    id: "sync-button",
+    label: "remotetabs-panelmenu.label",
+    tooltiptext: "remotetabs-panelmenu.tooltiptext",
+    type: "view",
+    viewId: "PanelUI-remotetabs",
+    defaultArea: CustomizableUI.AREA_PANEL,
+    deckIndices: {
+      DECKINDEX_TABS: 0,
+      DECKINDEX_TABSDISABLED: 1,
+      DECKINDEX_FETCHING: 2,
+      DECKINDEX_NOCLIENTS: 3,
+    },
+    onCreated(aNode) {
+      // Add an observer to the button so we get the animation during sync.
+      // (Note the observer sets many attributes, including label and
+      // tooltiptext, but we only want the 'syncstatus' attribute for the
+      // animation)
+      let doc = aNode.ownerDocument;
+      let obnode = doc.createElementNS(kNSXUL, "observes");
+      obnode.setAttribute("element", "sync-status");
+      obnode.setAttribute("attribute", "syncstatus");
+      aNode.appendChild(obnode);
+
+      // A somewhat complicated dance to format the mobilepromo label.
+      let bundle = doc.getElementById("bundle_browser");
+      let formatArgs = ["android", "ios"].map(os => {
+        let link = doc.createElement("label");
+        link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`)
+        link.href = Services.prefs.getCharPref(`identity.mobilepromo.${os}`) + "synced-tabs";
+        link.className = "text-link remotetabs-promo-link";
+        return link.outerHTML;
+      });
+      // Put it all together...
+      let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo", formatArgs);
+      doc.getElementById("PanelUI-remotetabs-mobile-promo").innerHTML = contents;
+    },
+    onViewShowing(aEvent) {
+      let doc = aEvent.target.ownerDocument;
+      this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
+      Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, false);
+
+      let deck = doc.getElementById("PanelUI-remotetabs-deck");
+      if (SyncedTabs.isConfiguredToSyncTabs) {
+        if (SyncedTabs.hasSyncedThisSession) {
+          deck.selectedIndex = this.deckIndices.DECKINDEX_TABS;
+        } else {
+          // Sync hasn't synced tabs yet, so show the "fetching" panel.
+          deck.selectedIndex = this.deckIndices.DECKINDEX_FETCHING;
+        }
+        // force a background sync.
+        SyncedTabs.syncTabs().catch(ex => {
+          Cu.reportError(ex);
+        });
+        // show the current list - it will be updated by our observer.
+        this._showTabs();
+      } else {
+        // not configured to sync tabs, so no point updating the list.
+        deck.selectedIndex = this.deckIndices.DECKINDEX_TABSDISABLED;
+      }
+    },
+    onViewHiding() {
+      Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
+      this._tabsList = null;
+    },
+    _tabsList: null,
+    observe(subject, topic, data) {
+      switch (topic) {
+        case SyncedTabs.TOPIC_TABS_CHANGED:
+          this._showTabs();
+          break;
+        default:
+          break;
+      }
+    },
+    _showTabsPromise: Promise.resolve(),
+    // Update the tab list after any existing in-flight updates are complete.
+    _showTabs() {
+      this._showTabsPromise = this._showTabsPromise.then(() => {
+        return this.__showTabs();
+      });
+    },
+    // Return a new promise to update the tab list.
+    __showTabs() {
+      let doc = this._tabsList.ownerDocument;
+      let deck = doc.getElementById("PanelUI-remotetabs-deck");
+      return SyncedTabs.getTabClients().then(clients => {
+        // The view may have been hidden while the promise was resolving.
+        if (!this._tabsList) {
+          return;
+        }
+        if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
+          // the "fetching tabs" deck is being shown - let's leave it there.
+          // When that first sync completes we'll be notified and update.
+          return;
+        }
+
+        if (clients.length === 0) {
+          deck.selectedIndex = this.deckIndices.DECKINDEX_NOCLIENTS;
+          return;
+        }
+
+        deck.selectedIndex = this.deckIndices.DECKINDEX_TABS;
+        this._clearTabList();
+        this._sortFilterClientsAndTabs(clients);
+        let fragment = doc.createDocumentFragment();
+
+        for (let client of clients) {
+          // add a menu separator for all clients other than the first.
+          if (fragment.lastChild) {
+            let separator = doc.createElementNS(kNSXUL, "menuseparator");
+            fragment.appendChild(separator);
+          }
+          this._appendClient(client, fragment);
+        }
+        this._tabsList.appendChild(fragment);
+      }).catch(err => {
+        Cu.reportError(err);
+      }).then(() => {
+        // an observer for tests.
+        Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated", null);
+      });
+    },
+    _clearTabList () {
+      let list = this._tabsList;
+      while (list.lastChild) {
+        list.lastChild.remove();
+      }
+    },
+    _showNoClientMessage() {
+      this._appendMessageLabel("notabslabel");
+    },
+    _appendMessageLabel(messageAttr, appendTo = null) {
+      if (!appendTo) {
+        appendTo = this._tabsList;
+      }
+      let message = this._tabsList.getAttribute(messageAttr);
+      let doc = this._tabsList.ownerDocument;
+      let messageLabel = doc.createElementNS(kNSXUL, "label");
+      messageLabel.textContent = message;
+      appendTo.appendChild(messageLabel);
+      return messageLabel;
+    },
+    _appendClient: function (client, attachFragment) {
+      let doc = attachFragment.ownerDocument;
+      // Create the element for the remote client.
+      let clientItem = doc.createElementNS(kNSXUL, "label");
+      clientItem.setAttribute("itemtype", "client");
+      clientItem.textContent = client.name;
+
+      attachFragment.appendChild(clientItem);
+
+      if (client.tabs.length == 0) {
+        let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
+        label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
+      } else {
+        for (let tab of client.tabs) {
+          let tabEnt = this._createTabElement(doc, tab);
+          attachFragment.appendChild(tabEnt);
+        }
+      }
+    },
+    _createTabElement(doc, tabInfo) {
+      let win = doc.defaultView;
+      let item = doc.createElementNS(kNSXUL, "toolbarbutton");
+      item.setAttribute("itemtype", "tab");
+      item.setAttribute("class", "subviewbutton");
+      item.setAttribute("targetURI", tabInfo.url);
+      item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
+      item.setAttribute("image", tabInfo.icon);
+      // We need to use "click" instead of "command" here so openUILink
+      // respects different buttons (eg, to open in a new tab).
+      item.addEventListener("click", e => {
+        doc.defaultView.openUILink(tabInfo.url, e);
+        CustomizableUI.hidePanelForNode(item);
+      });
+      return item;
+    },
+    _sortFilterClientsAndTabs(clients) {
+      // First sort and filter the list of tabs for each client. Note that the
+      // SyncedTabs module promises that the objects it returns are never
+      // shared, so we are free to mutate those objects directly.
+      const maxTabs = 15;
+      for (let client of clients) {
+        let tabs = client.tabs;
+        tabs.sort((a, b) => b.lastUsed - a.lastUsed);
+        client.tabs = tabs.slice(0, maxTabs);
+      }
+      // Now sort the clients - the clients are sorted in the order of the
+      // most recent tab for that client (ie, it is important the tabs for
+      // each client are already sorted.)
+      clients.sort((a, b) => {
+        if (a.tabs.length == 0) {
+          return 1; // b comes first.
+        }
+        if (b.tabs.length == 0) {
+          return -1; // a comes first.
+        }
+        return b.tabs[0].lastUsed - a.tabs[0].lastUsed;
+      });
+    },
+  }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       if (e.target && e.target.ownerDocument && e.target.ownerDocument.defaultView) {
         let win = e.target.ownerDocument.defaultView;
         if (typeof win.OpenBrowserWindow == "function") {
           win.OpenBrowserWindow({private: true});
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -101,16 +101,93 @@
         <vbox id="PanelUI-historyItems" tooltip="bhTooltip"/>
       </vbox>
       <toolbarbutton id="PanelUI-historyMore"
                      class="panel-subview-footer subviewbutton"
                      label="&appMenuHistory.showAll.label;"
                      oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/>
     </panelview>
 
+    <panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView">
+      <label value="&appMenuRemoteTabs.label;" class="panel-subview-header"/>
+      <vbox class="panel-subview-body">
+        <!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
+        <!-- When Sync is ready to sync -->
+        <vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
+          <toolbarbutton id="PanelUI-remotetabs-syncnow"
+                         observes="sync-status"
+                         class="subviewbutton"
+                         oncommand="gSyncUI.doSync();"
+                         closemenu="none"/>
+          <menuseparator id="PanelUI-remotetabs-separator"/>
+          <deck id="PanelUI-remotetabs-deck">
+            <!-- Sync is ready to Sync and the "tabs" engine is enabled -->
+            <vbox id="PanelUI-remotetabs-tabspane">
+              <vbox id="PanelUI-remotetabs-tabslist"
+                    notabsforclientlabel="&appMenuRemoteTabs.notabs.label;"
+                    />
+            </vbox>
+            <!-- Sync is ready to Sync but the "tabs" engine isn't enabled-->
+            <vbox id="PanelUI-remotetabs-tabsdisabledpane"
+                  class="PanelUI-remotetabs-instruction-box">
+              <hbox pack="center">
+                <image class="fxaSyncIllustration" alt=""/>
+              </hbox>
+              <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
+              <hbox pack="center">
+                <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+                               label="&appMenuRemoteTabs.openprefs.label;"
+                               oncommand="gSyncUI.openSetup();"/>
+              </hbox>
+            </vbox>
+            <!-- Sync is ready to Sync but we are still fetching the tabs to show -->
+            <vbox id="PanelUI-remotetabs-fetching">
+              <label>&appMenuRemoteTabs.fetching.label;</label>
+            </vbox>
+            <!-- Sync has only 1 (ie, this) device connected -->
+            <vbox id="PanelUI-remotetabs-nodevicespane"
+                   class="PanelUI-remotetabs-instruction-box">
+              <hbox pack="center">
+                <image class="fxaSyncIllustration" alt=""/>
+              </hbox>
+              <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.label;</label>
+              <!-- The inner HTML for PanelUI-remotetabs-mobile-promo is built at runtime -->
+              <label id="PanelUI-remotetabs-mobile-promo"/>
+            </vbox>
+          </deck>
+        </vbox>
+        <!-- When Sync is not configured -->
+        <vbox id="PanelUI-remotetabs-setupsync"
+              flex="1"
+              align="center"
+              class="PanelUI-remotetabs-instruction-box"
+              observes="sync-setup-state">
+          <image class="fxaSyncIllustration" alt=""/>
+          <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
+          <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+                         label="&appMenuRemoteTabs.signin.label;"
+                         oncommand="gSyncUI.openSetup();"/>
+        </vbox>
+        <!-- When Sync needs re-authentication. This uses the exact same messaging
+             as "Sync is not configured" but remains a separate box so we get
+             the goodness of observing broadcasters to manage the hidden states -->
+        <vbox id="PanelUI-remotetabs-reauthsync"
+              flex="1"
+              align="center"
+              class="PanelUI-remotetabs-instruction-box"
+              observes="sync-reauth-state">
+          <image class="fxaSyncIllustration" alt=""/>
+          <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
+          <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+                         label="&appMenuRemoteTabs.signin.label;"
+                         oncommand="gSyncUI.openSetup();"/>
+        </vbox>
+      </vbox>
+    </panelview>
+
     <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
       <label value="&bookmarksMenu.label;" class="panel-subview-header"/>
       <vbox class="panel-subview-body">
         <toolbarbutton id="panelMenuBookmarkThisPage"
                        class="subviewbutton"
                        observes="bookmarkThisPageBroadcaster"
                        command="Browser:AddBookmarkAs"
                        onclick="PanelUI.hide();"/>
--- a/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
+++ b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
@@ -22,26 +22,29 @@ add_task(function testWrapUnwrap() {
   ok(!wrapper, "There should be a wrapper");
   let item = document.getElementById(kTestWidget1);
   ok(!item, "There should no longer be an item");
 });
 
 // Creating and destroying a widget should correctly deal with panel placeholders
 add_task(function testPanelPlaceholders() {
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
-  let expectedPlaceholders = 2 + (isInDevEdition() ? 1 : 0);
+  // The value of expectedPlaceholders depends on the default palette layout.
+  // Bug 1229236 is for these tests to be smarter so the test doesn't need to
+  // change when the default placements change.
+  let expectedPlaceholders = 1 + (isInDevEdition() ? 1 : 0);
   is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct.");
   CustomizableUI.createWidget({id: kTestWidget2, label: 'Pretty label', tooltiptext: 'Pretty tooltip', defaultArea: CustomizableUI.AREA_PANEL});
   let elem = document.getElementById(kTestWidget2);
   let wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(elem, "There should be an item");
   ok(wrapper, "There should be a wrapper");
   is(wrapper.firstChild.id, kTestWidget2, "Wrapper should have test widget");
   is(wrapper.parentNode, panel, "Wrapper should be in panel");
-  expectedPlaceholders = 1 + (isInDevEdition() ? 1 : 0);
+  expectedPlaceholders = isInDevEdition() ? 1 : 3;
   is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct.");
   CustomizableUI.destroyWidget(kTestWidget2);
   wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(!wrapper, "There should be a wrapper");
   let item = document.getElementById(kTestWidget2);
   ok(!item, "There should no longer be an item");
   yield endCustomizing();
 });
--- a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
+++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
@@ -17,17 +17,19 @@ add_task(function() {
                              "save-page-button",
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(zoomControls, printButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -43,17 +45,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(zoomControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
 });
 
 
 // Dragging the zoom controls to be before the new-window button should not move any widgets.
@@ -67,17 +71,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(zoomControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the zoom controls to be before the history-panelmenu should move the zoom-controls in to the row higher than the history-panelmenu.
 add_task(function() {
@@ -90,17 +96,19 @@ add_task(function() {
                              "save-page-button",
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(zoomControls, historyPanelMenu);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -117,17 +125,19 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "zoom-controls",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(zoomControls, preferencesButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -144,17 +154,19 @@ add_task(function() {
                                "zoom-controls",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
-                               "developer-button"];
+                               "developer-button",
+                               "sync-button",
+                              ];
   removeDeveloperButtonIfDevEdition(placementsAfterInsert);
   simulateItemDrag(openFileButton, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -183,17 +195,19 @@ add_task(function() {
                                "zoom-controls",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
-                               "developer-button"];
+                               "developer-button",
+                               "sync-button",
+                              ];
   removeDeveloperButtonIfDevEdition(placementsAfterInsert);
   simulateItemDrag(openFileButton, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -219,17 +233,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the new-window-button should
 // move the zoom-controls before the edit-controls.
@@ -243,17 +259,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -270,17 +288,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, privateBrowsingButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -297,17 +317,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -323,17 +345,19 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, panel);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -348,17 +372,19 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "sync-button",
+                            ];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   let paletteChildElementCount = palette.childElementCount;
   simulateItemDrag(editControls, palette);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   is(paletteChildElementCount + 1, palette.childElementCount,
      "The palette should have a new child, congratulations!");
   is(editControls.parentNode.id, "wrapper-edit-controls",
      "The edit-controls should be properly wrapped.");
@@ -374,16 +400,22 @@ add_task(function() {
 // Dragging the edit-controls to each of the panel placeholders
 // should append the edit-controls to the bottom of the panel.
 add_task(function() {
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let numPlaceholders = 2;
   for (let i = 0; i < numPlaceholders; i++) {
+    // This test relies on there being a specific number of widgets in the
+    // panel. The addition of sync-button screwed this up, so we remove it
+    // here. We should either fix the tests to not rely on the specific layout,
+    // or fix bug 1007910 which would change the placeholder logic in different
+    // ways. Bug 1229236 is for these tests to be smarter.
+    CustomizableUI.removeWidgetFromArea("sync-button");
     // NB: We can't just iterate over all of the placeholders
     // because each drag-drop action recreates them.
     let placeholder = panel.getElementsByClassName("panel-customization-placeholder")[i];
     let placementsAfterMove = ["zoom-controls",
                                "new-window-button",
                                "privatebrowsing-button",
                                "save-page-button",
                                "print-button",
@@ -394,16 +426,17 @@ add_task(function() {
                                "add-ons-button",
                                "edit-controls",
                                "developer-button"];
     removeDeveloperButtonIfDevEdition(placementsAfterMove);
     simulateItemDrag(editControls, placeholder);
     assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
     let zoomControls = document.getElementById("zoom-controls");
     simulateItemDrag(editControls, zoomControls);
+    CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
     ok(CustomizableUI.inDefaultState, "Should still be in default state.");
   }
 });
 
 // Dragging the open-file-button back on to itself should work.
 add_task(function() {
   yield startCustomizing();
   let openFileButton = document.getElementById("open-file-button");
@@ -415,16 +448,20 @@ add_task(function() {
   let editControls = document.getElementById("edit-controls");
   is(editControls.parentNode.tagName, "toolbarpaletteitem",
      "edit-controls should be wrapped by a toolbarpaletteitem");
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging a small button onto the last big button should work.
 add_task(function() {
+  // Bug 1007910 requires there be a placeholder on the final row for this
+  // test to work as written. The addition of sync-button meant that's not true
+  // so we remove it from here. Bug 1229236 is for these tests to be smarter.
+  CustomizableUI.removeWidgetFromArea("sync-button");
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let target = panel.getElementsByClassName("panel-customization-placeholder")[0];
   let placementsAfterMove = ["zoom-controls",
                              "new-window-button",
                              "privatebrowsing-button",
                              "save-page-button",
@@ -434,26 +471,27 @@ add_task(function() {
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
                              "developer-button"];
   removeDeveloperButtonIfDevEdition(placementsAfterMove);
   simulateItemDrag(editControls, target);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
-  let itemToDrag = "sync-button";
+  let itemToDrag = "email-link-button"; // any button in the palette by default.
   let button = document.getElementById(itemToDrag);
   placementsAfterMove.splice(11, 0, itemToDrag);
   simulateItemDrag(button, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
 
   // Put stuff back:
   let palette = document.getElementById("customization-palette");
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(button, palette);
   simulateItemDrag(editControls, zoomControls);
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
   yield resetCustomization();
 });
--- a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
+++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
@@ -15,42 +15,49 @@ add_task(function() {
     ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   }
   if (!isInDevEdition()) {
     ok(CustomizableUI.inDefaultState, "Should be in default state.");
   } else {
     ok(!CustomizableUI.inDefaultState, "Should not be in default state if on DevEdition.");
   }
 
-  let btn = document.getElementById("open-file-button");
+  // This test relies on an exact number of widgets being in the panel.
+  // Remove the sync-button to satisfy that. (bug 1229236)
+  CustomizableUI.removeWidgetFromArea("sync-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering");
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 1).
 add_task(function() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
 
+  // This test relies on an exact number of widgets being in the panel.
+  // Remove the sync-button to satisfy that. (bug 1229236)
+  CustomizableUI.removeWidgetFromArea("sync-button");
+
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
   let placementsAfterAppend = placements;
 
   placementsAfterAppend = placements.concat(["open-file-button"]);
   simulateItemDrag(btn, panel);
 
@@ -69,26 +76,30 @@ add_task(function() {
 
   btn = document.getElementById("open-file-button");
   simulateItemDrag(btn, palette);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
-  ok(CustomizableUI.inDefaultState, "Should be in default state again."); 
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 2).
 add_task(function() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
+  // This test relies on an exact number of widgets being in the panel.
+  // Remove the sync-button to satisfy that. (bug 1229236)
+  CustomizableUI.removeWidgetFromArea("sync-button");
 
   let btn = document.getElementById("add-ons-button");
   let btn2 = document.getElementById("developer-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id);
@@ -107,27 +118,32 @@ add_task(function() {
   simulateItemDrag(btn2, panel);
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // A wide widget at the bottom of the panel should have three placeholders after it.
 add_task(function() {
   yield startCustomizing();
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
   }
 
+  // This test relies on an exact number of widgets being in the panel.
+  // Remove the sync-button to satisfy that. (bug 1229236)
+  CustomizableUI.removeWidgetFromArea("sync-button");
+
   let btn = document.getElementById("edit-controls");
   let btn2 = document.getElementById("developer-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   placements.pop();
   simulateItemDrag(btn2, palette);
@@ -146,38 +162,45 @@ add_task(function() {
 
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(btn, zoomControls);
 
   if (isInDevEdition()) {
     CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
   }
 
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // The default placements should have two placeholders at the bottom (or 1 in win8).
 add_task(function() {
   yield startCustomizing();
   let numPlaceholders = -1;
 
   if (isInDevEdition()) {
     numPlaceholders = 3;
   } else {
     numPlaceholders = 2;
   }
 
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
+
+  // This test relies on an exact number of widgets being in the panel.
+  // Remove the sync-button to satisfy that. (bug 1229236)
+  CustomizableUI.removeWidgetFromArea("sync-button");
+
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering");
 
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 add_task(function asyncCleanup() {
   yield endCustomizing();
   yield resetCustomization();
 });
 
--- a/browser/components/customizableui/test/browser_967000_button_sync.js
+++ b/browser/components/customizableui/test/browser_967000_button_sync.js
@@ -1,22 +1,57 @@
 /* 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/. */
 
-// The test expects the about:accounts page to open in the current tab
-
 "use strict";
 
+let {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
+
 XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
 
+// These are available on the widget implementation, but it seems impossible
+// to grab that impl at runtime.
+const DECKINDEX_TABS = 0;
+const DECKINDEX_TABSDISABLED = 1;
+const DECKINDEX_FETCHING = 2;
+const DECKINDEX_NOCLIENTS = 3;
+
 var initialLocation = gBrowser.currentURI.spec;
 var newTab = null;
 
-function openAboutAccountsFromMenuPanel(entryPoint) {
+// A helper to notify there are new tabs. Returns a promise that is resolved
+// once the UI has been updated.
+function updateTabsPanel() {
+  let promiseTabsUpdated = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
+  Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED, null);
+  return promiseTabsUpdated;
+}
+
+// This is the mock we use for SyncedTabs.jsm - tests may override various
+// functions.
+let mockedInternal = {
+  get isConfiguredToSyncTabs() { return true; },
+  getTabClients() { return []; },
+  syncTabs() {},
+  hasSyncedThisSession: false,
+};
+
+
+add_task(function* setup() {
+  let oldInternal = SyncedTabs._internal;
+  SyncedTabs._internal = mockedInternal;
+
+  registerCleanupFunction(() => {
+    SyncedTabs._internal = oldInternal;
+  });
+});
+
+// The test expects the about:preferences#sync page to open in the current tab
+function openPrefsFromMenuPanel(expectedPanelId, entryPoint) {
   info("Check Sync button functionality");
   Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "http://example.com/");
 
   // add the Sync button to the panel
   CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
 
   // check the button's functionality
   yield PanelUI.show();
@@ -24,29 +59,40 @@ function openAboutAccountsFromMenuPanel(
   if (entryPoint == "uitour") {
     UITour.tourBrowsersByWindow.set(window, new Set());
     UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser);
   }
 
   let syncButton = document.getElementById("sync-button");
   ok(syncButton, "The Sync button was added to the Panel Menu");
 
+  syncButton.click();
+  let syncPanel = document.getElementById("PanelUI-remotetabs");
+  ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+
+  // Sync is not configured - verify that state is reflected.
+  let subpanel = document.getElementById(expectedPanelId)
+  ok(!subpanel.hidden, "sync setup element is visible");
+
+  // Find and click the "setup" button.
+  let setupButton = subpanel.querySelector(".PanelUI-remotetabs-prefs-button");
+  setupButton.click();
+
   let deferred = Promise.defer();
   let handler = (e) => {
     if (e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
         e.target.location.href == "about:blank") {
       info("Skipping spurious 'load' event for " + e.target.location.href);
       return;
     }
     gBrowser.selectedBrowser.removeEventListener("load", handler, true);
     deferred.resolve();
   }
   gBrowser.selectedBrowser.addEventListener("load", handler, true);
 
-  syncButton.click();
   yield deferred.promise;
   newTab = gBrowser.selectedTab;
 
   is(gBrowser.currentURI.spec, "about:preferences?entrypoint=" + entryPoint + "#sync",
     "Firefox Sync preference page opened with `menupanel` entrypoint");
   ok(!isPanelUIOpen(), "The panel closed");
 
   if(isPanelUIOpen()) {
@@ -63,13 +109,167 @@ function asyncCleanup() {
   ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
 
   // restore the tabs
   gBrowser.addTab(initialLocation);
   gBrowser.removeTab(newTab);
   UITour.tourBrowsersByWindow.delete(window);
 }
 
-add_task(() => openAboutAccountsFromMenuPanel("syncbutton"));
+// When Sync is not setup.
+add_task(() => openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "syncbutton"));
 add_task(asyncCleanup);
 // Test that uitour is in progress, the entrypoint is `uitour` and not `menupanel`
-add_task(() => openAboutAccountsFromMenuPanel("uitour"));
+add_task(() => openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "uitour"));
 add_task(asyncCleanup);
+
+// When Sync is configured in a "needs reauthentication" state.
+add_task(function* () {
+  // configure our broadcasters so we are in the right state.
+  document.getElementById("sync-reauth-state").hidden = false;
+  document.getElementById("sync-setup-state").hidden = true;
+  document.getElementById("sync-syncnow-state").hidden = true;
+  yield openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "syncbutton")
+});
+
+// Test the "Sync Now" button
+add_task(function* () {
+  let nSyncs = 0;
+  mockedInternal.getTabClients = () => [];
+  mockedInternal.syncTabs = () => {
+    nSyncs++;
+    return Promise.resolve();
+  }
+
+  // configure our broadcasters so we are in the right state.
+  document.getElementById("sync-reauth-state").hidden = true;
+  document.getElementById("sync-setup-state").hidden = true;
+  document.getElementById("sync-syncnow-state").hidden = false;
+
+  // add the Sync button to the panel
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+  yield PanelUI.show();
+  document.getElementById("sync-button").click();
+  let syncPanel = document.getElementById("PanelUI-remotetabs");
+  ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+
+  let subpanel = document.getElementById("PanelUI-remotetabs-main")
+  ok(!subpanel.hidden, "main pane is visible");
+  let deck = document.getElementById("PanelUI-remotetabs-deck");
+
+  // The widget is still fetching tabs, as we've neutered everything that
+  // provides them
+  is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible");
+
+  let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+
+  let didSync = false;
+  let oldDoSync = gSyncUI.doSync;
+  gSyncUI.doSync = function() {
+    didSync = true;
+    mockedInternal.hasSyncedThisSession = true;
+    gSyncUI.doSync = oldDoSync;
+  }
+  syncNowButton.click();
+  ok(didSync, "clicking the button called the correct function");
+
+  // Tell the widget there are tabs available, but with zero clients.
+  mockedInternal.getTabClients = () => {
+    return Promise.resolve([]);
+  }
+  yield updateTabsPanel();
+  // The UI should be showing the "no clients" pane.
+  is(deck.selectedIndex, DECKINDEX_NOCLIENTS, "no-clients deck entry is visible");
+
+  // Tell the widget there are tabs available - we have 3 clients, one with no
+  // tabs.
+  mockedInternal.getTabClients = () => {
+    return Promise.resolve([
+      {
+        id: "guid_mobile",
+        type: "client",
+        name: "My Phone",
+        tabs: [],
+      },
+      {
+        id: "guid_desktop",
+        type: "client",
+        name: "My Desktop",
+        tabs: [
+          {
+            title: "http://example.com/10",
+            lastUsed: 10, // the most recent
+          },
+          {
+            title: "http://example.com/1",
+            lastUsed: 1, // the least recent.
+          },
+          {
+            title: "http://example.com/5",
+            lastUsed: 5,
+          },
+        ],
+      },
+      {
+        id: "guid_second_desktop",
+        name: "My Other Desktop",
+        tabs: [
+          {
+            title: "http://example.com/6",
+            lastUsed: 6,
+          }
+        ],
+      },
+    ]);
+  };
+  yield updateTabsPanel();
+
+  // The UI should be showing tabs!
+  is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible");
+  let tabList = document.getElementById("PanelUI-remotetabs-tabslist");
+  let node = tabList.firstChild;
+  // First entry should be the client with the most-recent tab.
+  is(node.getAttribute("itemtype"), "client", "node is a client entry");
+  is(node.textContent, "My Desktop", "correct client");
+  // Next entry is the most-recent tab
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "tab", "node is a tab");
+  is(node.getAttribute("label"), "http://example.com/10");
+
+  // Next entry is the next-most-recent tab
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "tab", "node is a tab");
+  is(node.getAttribute("label"), "http://example.com/5");
+
+  // Next entry is the least-recent tab from the first client.
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "tab", "node is a tab");
+  is(node.getAttribute("label"), "http://example.com/1");
+
+  // Next is a menuseparator between the clients.
+  node = node.nextSibling;
+  is(node.nodeName, "menuseparator");
+
+  // Next is the client with 1 tab.
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "client", "node is a client entry");
+  is(node.textContent, "My Other Desktop", "correct client");
+  // Its single tab
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "tab", "node is a tab");
+  is(node.getAttribute("label"), "http://example.com/6");
+
+  // Next is a menuseparator between the clients.
+  node = node.nextSibling;
+  is(node.nodeName, "menuseparator");
+
+  // Next is the client with no tab.
+  node = node.nextSibling;
+  is(node.getAttribute("itemtype"), "client", "node is a client entry");
+  is(node.textContent, "My Phone", "correct client");
+  // There is a single node saying there's no tabs for the client.
+  node = node.nextSibling;
+  is(node.nodeName, "label", "node is a label");
+  is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client");
+
+  node = node.nextSibling;
+  is(node, null, "no more entries");
+});
--- a/browser/components/customizableui/test/browser_987185_syncButton.js
+++ b/browser/components/customizableui/test/browser_987185_syncButton.js
@@ -21,17 +21,25 @@ add_task(function* testSyncButtonFunctio
   CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
 
   // check the button's functionality
   yield PanelUI.show();
   info("The panel menu was opened");
 
   let syncButton = document.getElementById("sync-button");
   ok(syncButton, "The Sync button was added to the Panel Menu");
+  // click the button - the panel should open.
   syncButton.click();
+  let syncPanel = document.getElementById("PanelUI-remotetabs");
+  ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
+
+  // Find and click the "setup" button.
+  let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow");
+  syncNowButton.click();
+
   info("The sync button was clicked");
 
   yield waitForCondition(() => syncWasCalled);
 });
 
 add_task(function* asyncCleanup() {
   // reset the panel UI to the default state
   yield resetCustomization();
--- a/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js
+++ b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js
@@ -1,43 +1,46 @@
 /* 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";
 
 const TOOLBARID = "test-toolbar-added-during-customize-mode";
 
+// The ID of a button that is not placed (ie, is in the palette) by default
+const kNonPlacedWidgetId = "open-file-button";
+
 add_task(function*() {
   yield startCustomizing();
   let toolbar = createToolbarWithPlacements(TOOLBARID, []);
-  CustomizableUI.addWidgetToArea("sync-button", TOOLBARID);
-  let syncButton = document.getElementById("sync-button");
-  ok(syncButton, "Sync button should exist.");
-  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should be a wrapper.");
+  CustomizableUI.addWidgetToArea(kNonPlacedWidgetId, TOOLBARID);
+  let button = document.getElementById(kNonPlacedWidgetId);
+  ok(button, "Button should exist.");
+  is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should be a wrapper.");
 
-  simulateItemDrag(syncButton, gNavToolbox.palette);
-  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
-  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
+  simulateItemDrag(button, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette.");
 
-  syncButton.scrollIntoView();
-  simulateItemDrag(syncButton, toolbar);
-  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
-  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
-  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
+  button.scrollIntoView();
+  simulateItemDrag(button, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar.");
 
   yield endCustomizing();
-  isnot(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should not be a wrapper outside customize mode.");
+  isnot(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should not be a wrapper outside customize mode.");
   yield startCustomizing();
 
-  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should be a wrapper back in customize mode.");
+  is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should be a wrapper back in customize mode.");
 
-  simulateItemDrag(syncButton, gNavToolbox.palette);
-  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
-  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
+  simulateItemDrag(button, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette.");
 
   ok(!CustomizableUI.inDefaultState, "Not in default state while toolbar is not collapsed yet.");
   setToolbarVisibility(toolbar, false);
   ok(CustomizableUI.inDefaultState, "In default state while toolbar is collapsed.");
 
   setToolbarVisibility(toolbar, true);
 
   info("Check that removing the area registration from within customize mode works");
@@ -45,21 +48,21 @@ add_task(function*() {
   ok(CustomizableUI.inDefaultState, "Now that the toolbar is no longer registered, should be in default state.");
   ok(!gCustomizeMode.areas.has(toolbar), "Toolbar shouldn't be known to customize mode.");
 
   CustomizableUI.registerArea(TOOLBARID, {legacy: true, defaultPlacements: []});
   CustomizableUI.registerToolbarNode(toolbar, []);
   ok(!CustomizableUI.inDefaultState, "Now that the toolbar is registered again, should no longer be in default state.");
   ok(gCustomizeMode.areas.has(toolbar), "Toolbar should be known to customize mode again.");
 
-  syncButton.scrollIntoView();
-  simulateItemDrag(syncButton, toolbar);
-  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
-  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
-  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
+  button.scrollIntoView();
+  simulateItemDrag(button, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar.");
 
   let otherWin = yield openAndLoadWindow({}, true);
   let otherTB = otherWin.document.createElementNS(kNSXUL, "toolbar");
   otherTB.id = TOOLBARID;
   otherTB.setAttribute("customizable", "true");
   let wasInformedCorrectlyOfAreaAppearing = false;
   let listener = {
     onAreaNodeRegistered: function(aArea, aNode) {
@@ -68,29 +71,29 @@ add_task(function*() {
       }
     }
   };
   CustomizableUI.addListener(listener);
   otherWin.gNavToolbox.appendChild(otherTB);
   ok(wasInformedCorrectlyOfAreaAppearing, "Should have been told area was registered.");
   CustomizableUI.removeListener(listener);
 
-  ok(otherTB.querySelector("#sync-button"), "Sync button is on other toolbar, too.");
+  ok(otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is on other toolbar, too.");
 
-  simulateItemDrag(syncButton, gNavToolbox.palette);
-  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
-  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
-  ok(!otherTB.querySelector("#sync-button"), "Sync button is in palette in other window, too.");
+  simulateItemDrag(button, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette.");
+  ok(!otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is in palette in other window, too.");
 
-  syncButton.scrollIntoView();
-  simulateItemDrag(syncButton, toolbar);
-  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
-  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
-  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
-  ok(otherTB.querySelector("#sync-button"), "Sync button is on other toolbar, too.");
+  button.scrollIntoView();
+  simulateItemDrag(button, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar.");
+  ok(otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is on other toolbar, too.");
 
   let wasInformedCorrectlyOfAreaDisappearing = false;
   //XXXgijs So we could be using promiseWindowClosed here. However, after
   // repeated random oranges, I'm instead relying on onWindowClosed below to
   // fire appropriately - it is linked to an unload event as well, and so
   // reusing it prevents a potential race between unload handlers where the
   // one from promiseWindowClosed could fire before the onWindowClosed
   // (and therefore onAreaNodeRegistered) one, causing the test to fail.
@@ -115,17 +118,17 @@ add_task(function*() {
   };
   CustomizableUI.addListener(listener);
   otherWin.close();
   let windowClosed = yield windowCloseDeferred.promise;
 
   is(windowClosed, otherWin, "Window should have sent onWindowClosed notification.");
   ok(wasInformedCorrectlyOfAreaDisappearing, "Should be told about window closing.");
   // Closing the other window should not be counted against this window's customize mode:
-  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should still be a wrapper.");
+  is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should still be a wrapper.");
   ok(gCustomizeMode.areas.has(toolbar), "Toolbar should still be a customizable area for this customize mode instance.");
 
   yield gCustomizeMode.reset();
 
   yield endCustomizing();
 
   CustomizableUI.removeListener(listener);
   wasInformedCorrectlyOfAreaDisappearing = false;
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -129,21 +129,26 @@ var gSyncPane = {
     window.addEventListener("unload", function() {
       topics.forEach(function (topic) {
         Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
       }, gSyncPane);
     }, false);
 
     XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
       return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
-    }),
+    });
 
     XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
       return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
-    }),
+    });
+
+    let url = Services.prefs.getCharPref("identity.mobilepromo.android") + "sync-preferences";
+    document.getElementById("fxaMobilePromo-android").setAttribute("href", url);
+    url = Services.prefs.getCharPref("identity.mobilepromo.ios") + "sync-preferences";
+    document.getElementById("fxaMobilePromo-ios").setAttribute("href", url);
 
     this.updateWeavePrefs();
 
     this._initProfileImageUI();
   },
 
   _toggleComputerNameControls: function(editMode) {
     let textbox = document.getElementById("fxaSyncComputerName");
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -209,22 +209,22 @@
         </groupbox>
       </vbox>
       <vbox>
         <image class="fxaSyncIllustration"/>
       </vbox>
     </hbox>
     <label class="fxaMobilePromo">
         &mobilePromo2.start;<!-- We put these comments to avoid inserting white spaces
-        --><label class="androidLink text-link"
-                  href="https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&amp;utm_medium=firefox-browser&amp;utm_campaign=sync-preferences"><!--
+        --><label id="fxaMobilePromo-android"
+                  class="androidLink text-link"><!--
         -->&mobilePromo2.androidLink;</label><!--
         -->&mobilePromo2.iOSBefore;<!--
-        --><label class="iOSLink text-link"
-                  href="https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&amp;utm_medium=firefox-browser&amp;utm_campaign=sync-preferences"><!--
+        --><label id="fxaMobilePromo-ios"
+                  class="iOSLink text-link"><!--
         -->&mobilePromo2.iOSLink;</label><!--
         -->&mobilePromo2.end;
     </label>
   </vbox>
 
   <vbox id="hasFxaAccount">
     <hbox>
       <vbox id="fxaContentWrapper">
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -405,16 +405,32 @@ These should match what Safari and other
 <!ENTITY appMenuCustomizeExit.tooltip "Finish Customizing">
 <!ENTITY appMenuHistory.label "History">
 <!ENTITY appMenuHistory.showAll.label "Show All History">
 <!ENTITY appMenuHistory.clearRecent.label "Clear Recent History…">
 <!ENTITY appMenuHistory.restoreSession.label "Restore Previous Session">
 <!ENTITY appMenuHistory.viewSidebar.label "View History Sidebar">
 <!ENTITY appMenuHelp.tooltip "Open Help Menu">
 
+<!ENTITY appMenuRemoteTabs.label "Synced Tabs">
+<!ENTITY appMenuRemoteTabs.fetching.label "Fetching Synced Tabs…">
+<!-- LOCALIZATION NOTE (appMenuRemoteTabs.notabs.label): This is shown beneath
+     the name of a device when that device has no open tabs -->
+<!ENTITY appMenuRemoteTabs.notabs.label "No open tabs">
+<!-- LOCALIZATION NOTE (appMenuRemoteTabs.tabsnotsyncing.label): This is shown
+     when Sync is configured but syncing tabs is disabled. -->
+<!ENTITY appMenuRemoteTabs.tabsnotsyncing.label "Turn on tab syncing to view a list of tabs from your other devices.">
+<!-- LOCALIZATION NOTE (appMenuRemoteTabs.noclients.label): This is shown
+     when Sync is configured but this appears to be the only device attached to
+     the account. We also show links to download Firefox for android/ios. -->
+<!ENTITY appMenuRemoteTabs.noclients.label "Sign in to Firefox from your other devices to view their tabs here.">
+<!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences">
+<!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices.">
+<!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync">
+
 <!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar">
 <!ENTITY customizeMenu.addToToolbar.accesskey "A">
 <!ENTITY customizeMenu.addToPanel.label "Add to Menu">
 <!ENTITY customizeMenu.addToPanel.accesskey "M">
 <!ENTITY customizeMenu.moveToToolbar.label "Move to Toolbar">
 <!ENTITY customizeMenu.moveToToolbar.accesskey "o">
 <!-- LOCALIZATION NOTE (customizeMenu.moveToPanel.accesskey) can appear on the
      same context menu as menubarCmd and personalbarCmd, so they should have
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -723,16 +723,27 @@ appmenu.updateFailed.description = Backg
 appmenu.restartBrowserButton.label = Restart %S
 appmenu.downloadUpdateButton.label = Download Update
 
 # LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
 
 readingList.promo.firstUse.readerView.title = Reader View
 readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
 
+# LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo):
+# %1$S will be replaced with a link, the text of which is
+# appMenuRemoteTabs.mobilePromo.android and the link will be to
+# https://www.mozilla.org/firefox/android/.
+# %2$S will be replaced with a link, the text of which is
+# appMenuRemoteTabs.mobilePromo.ios
+# and the link will be to https://www.mozilla.org/firefox/ios/.
+appMenuRemoteTabs.mobilePromo = Get %1$S or %2$S.
+appMenuRemoteTabs.mobilePromo.android = Firefox for Android
+appMenuRemoteTabs.mobilePromo.ios = Firefox for iOS
+
 # LOCALIZATION NOTE (e10s.offerPopup.mainMessage
 #                    e10s.offerPopup.highlight1
 #                    e10s.offerPopup.highlight2
 #                    e10s.offerPopup.enableAndRestart.label
 #                    e10s.offerPopup.enableAndRestart.accesskey
 #                    e10s.offerPopup.noThanks.label
 #                    e10s.offerPopup.noThanks.accesskey
 #                    e10s.postActivationInfobar.message
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -1,16 +1,19 @@
 # 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/.
 
 history-panelmenu.label = History
 # LOCALIZATION NOTE(history-panelmenu.tooltiptext2): %S is the keyboard shortcut
 history-panelmenu.tooltiptext2 = Show your history (%S)
 
+remotetabs-panelmenu.label = Synced Tabs
+remotetabs-panelmenu.tooltiptext = Show your synced tabs from other devices
+
 privatebrowsing-button.label = New Private Window
 # LOCALIZATION NOTE(privatebrowsing-button.tooltiptext): %S is the keyboard shortcut
 privatebrowsing-button.tooltiptext = Open a new Private Browsing window (%S)
 
 save-page-button.label = Save Page
 # LOCALIZATION NOTE(save-page-button.tooltiptext3): %S is the keyboard shortcut
 save-page-button.tooltiptext3 = Save this page (%S)
 
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -629,16 +629,17 @@ toolbarpaletteitem[place="palette"] > to
 #PanelUI-quit > .toolbarbutton-text,
 #PanelUI-fxa-avatar > .toolbarbutton-text {
   display: none;
 }
 
 #PanelUI-update-status > .toolbarbutton-icon,
 #PanelUI-fxa-label > .toolbarbutton-icon,
 #PanelUI-fxa-icon > .toolbarbutton-icon,
+#PanelUI-remotetabs-syncnow > .toolbarbutton-icon,
 #PanelUI-customize > .toolbarbutton-icon,
 #PanelUI-help > .toolbarbutton-icon,
 #PanelUI-quit > .toolbarbutton-icon {
   -moz-margin-end: 0;
 }
 
 #PanelUI-fxa-icon {
   -moz-padding-start: 15px;
@@ -662,21 +663,101 @@ toolbarpaletteitem[place="palette"] > to
   -moz-padding-start: 15px;
   -moz-border-start-style: none;
 }
 
 #PanelUI-update-status {
   list-style-image: url(chrome://branding/content/icon16.png);
 }
 
+#PanelUI-remotetabs-syncnow,
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon {
   list-style-image: url(chrome://browser/skin/sync-horizontalbar.png);
 }
 
+.PanelUI-remotetabs-instruction-label,
+#PanelUI-remotetabs-mobile-promo {
+  margin: 15px;
+  text-align: center;
+  text-shadow: none;
+  max-width: 15em;
+  color: GrayText;
+}
+
+/* The boxes with "instructions" get extra padding for space around the
+   illustration and buttons */
+.PanelUI-remotetabs-instruction-box {
+  padding: 30px 15px 15px 15px;
+}
+
+.PanelUI-remotetabs-prefs-button {
+  -moz-appearance: none;
+  background-color: #0096dd;
+  color: white;
+  border-radius: 2px;
+  margin: 10px;
+  padding: 8px;
+  text-shadow: none;
+  min-width: 200px;
+}
+
+.PanelUI-remotetabs-prefs-button:hover,
+.PanelUI-remotetabs-prefs-button:hover:active {
+  background-color: #018acb;
+}
+
+.remotetabs-promo-link {
+  margin: 0;
+}
+
+.PanelUI-remotetabs-notabsforclient-label {
+  color: GrayText;
+  /* This margin is to line this label up with the labels in toolbarbuttons. */
+  margin-left: 28px;
+}
+
+.fxaSyncIllustration {
+  width: 180px;
+  list-style-image: url(chrome://browser/skin/fxa/sync-illustration.svg);
+}
+
+.PanelUI-remotetabs-prefs-button > .toolbarbutton-text {
+  /* !important to override ".cui-widget-panel toolbarbutton > .toolbarbutton-text" above. */
+  text-align: center !important;
+  text-shadow: none;
+}
+
+#PanelUI-remotetabs[mainview] { /* panel anchored to toolbar button might be too skinny */
+  min-width: 19em;
+}
+
+/* Work around bug 1224412 - these boxes will cause scrollbars to appear when
+   the panel is anchored to a toolbar button.
+*/
+#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync,
+#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-reauthsync,
+#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-nodevicespane,
+#PanelUI-remotetabs[mainview] #PanelUI-remotetabs-tabsdisabledpane {
+  min-height: 30em;
+}
+
+#PanelUI-remotetabs-tabslist > label[itemtype="client"] {
+  color: GrayText;
+}
+
+/* Collapse the non-active vboxes in the remotetabs deck to use only the
+   height the active box needs */
+#PanelUI-remotetabs-deck:not([selectedIndex="1"]) > #PanelUI-remotetabs-tabsdisabledpane,
+#PanelUI-remotetabs-deck:not([selectedIndex="2"]) > #PanelUI-remotetabs-fetching,
+#PanelUI-remotetabs-deck:not([selectedIndex="3"]) > #PanelUI-remotetabs-nodevicespane {
+  visibility: collapse;
+}
+
+#PanelUI-remotetabs-syncnow[syncstatus="active"],
 #PanelUI-fxa-icon[syncstatus="active"] {
   list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar.png);
 }
 
 #PanelUI-footer-fxa[fxastatus="migrate-signup"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
 #PanelUI-footer-fxa[fxastatus="migrate-verify"] > #PanelUI-fxa-status > #PanelUI-fxa-label {
   list-style-image: url(chrome://browser/skin/warning.svg);
   -moz-image-region: auto;
@@ -696,16 +777,17 @@ toolbarpaletteitem[place="palette"] > to
 
 #PanelUI-quit {
   -moz-border-end-style: none;
   list-style-image: url(chrome://browser/skin/menuPanel-exit.png);
 }
 
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon,
+#PanelUI-remotetabs-syncnow,
 #PanelUI-customize,
 #PanelUI-help,
 #PanelUI-quit {
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
 #PanelUI-footer-fxa[fxastatus="signedin"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon,
 #PanelUI-footer-fxa[fxastatus="error"][fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label > .toolbarbutton-icon {
@@ -1065,20 +1147,29 @@ menuitem.panel-subview-footer@menuStateA
 
 .subviewbutton > .menu-accel-container {
   -moz-box-pack: start;
   -moz-margin-start: 10px;
   -moz-margin-end: auto;
   color: GrayText;
 }
 
+#PanelUI-remotetabs-tabslist > toolbarbutton,
 #PanelUI-historyItems > toolbarbutton {
   list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
 }
 
+@media (min-resolution: 1.1dppx) {
+  #PanelUI-remotetabs-tabslist > toolbarbutton,
+  #PanelUI-historyItems > toolbarbutton {
+    list-style-image: url("chrome://mozapps/skin/places/defaultFavicon@2x.png");
+  }
+}
+
+#PanelUI-remotetabs-tabslist > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-recentlyClosedWindows > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-recentlyClosedTabs > toolbarbutton > .toolbarbutton-icon,
 #PanelUI-historyItems > toolbarbutton > .toolbarbutton-icon {
   width: 16px;
   height: 16px;
 }
 
 toolbarbutton[panel-multiview-anchor="true"],
@@ -1526,20 +1617,22 @@ menuitem[checked="true"].subviewbutton >
     list-style-image: url(chrome://branding/content/icon32.png);
   }
 
   #PanelUI-footer-fxa[fxaprofileimage="enabled"] > #PanelUI-fxa-status > #PanelUI-fxa-avatar {
     list-style-image: url(chrome://browser/skin/fxa/default-avatar@2x.png)
   }
 
   #PanelUI-fxa-label,
+  #PanelUI-remotetabs-syncnow,
   #PanelUI-fxa-icon {
     list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
   }
 
+  #PanelUI-remotetabs-syncnow[syncstatus="active"],
   #PanelUI-fxa-icon[syncstatus="active"] {
     list-style-image: url(chrome://browser/skin/syncProgress-horizontalbar@2x.png);
   }
 
   #PanelUI-customize {
     list-style-image: url(chrome://browser/skin/menuPanel-customize@2x.png);
   }
 
@@ -1552,25 +1645,27 @@ menuitem[checked="true"].subviewbutton >
   }
 
   #PanelUI-quit {
     list-style-image: url(chrome://browser/skin/menuPanel-exit@2x.png);
   }
 
   #PanelUI-fxa-label,
   #PanelUI-fxa-icon,
+  #PanelUI-remotetabs-syncnow,
   #PanelUI-customize,
   #PanelUI-help,
   #PanelUI-quit {
     -moz-image-region: rect(0, 32px, 32px, 0);
   }
 
   #PanelUI-update-status > .toolbarbutton-icon,
   #PanelUI-fxa-label > .toolbarbutton-icon,
   #PanelUI-fxa-icon > .toolbarbutton-icon,
+  #PanelUI-remotetabs-syncnow > .toolbarbutton-icon,
   #PanelUI-customize > .toolbarbutton-icon,
   #PanelUI-help > .toolbarbutton-icon,
   #PanelUI-quit > .toolbarbutton-icon {
     width: 16px;
   }
 
   #PanelUI-customize:hover,
   #PanelUI-help:not([disabled]):hover,
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/fxa/sync-illustration.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" width="320" height="280" viewBox="0 0 320 280">
+  <g fill="#cdcdcd">
+    <path d="M46.352,148.919 L46.352,148.919 L44.938,150.333 L43.523,148.919 L43.523,148.919 L37.866,143.262 L39.281,141.848 L44.938,147.505 L50.594,141.848 L52.009,143.262 L46.352,148.919 ZM43.937,134.000 L45.938,134.000 L45.938,142.000 L43.937,142.000 L43.937,134.000 ZM43.937,122.000 L45.938,122.000 L45.938,130.000 L43.937,130.000 L43.937,122.000 Z"/>
+    <path d="M306.641,132.110 L300.984,126.453 L295.328,132.110 L293.913,130.696 L300.984,123.625 L308.055,130.696 L306.641,132.110 ZM302.000,223.969 L300.000,223.969 L300.000,215.969 L302.000,215.969 L302.000,223.969 ZM302.000,211.969 L300.000,211.969 L300.000,203.969 L302.000,203.969 L302.000,211.969 ZM302.000,199.969 L300.000,199.969 L300.000,191.969 L302.000,191.969 L302.000,199.969 ZM302.000,187.969 L300.000,187.969 L300.000,179.969 L302.000,179.969 L302.000,187.969 ZM302.000,175.969 L300.000,175.969 L300.000,167.969 L302.000,167.969 L302.000,175.969 ZM302.000,163.969 L300.000,163.969 L300.000,155.969 L302.000,155.969 L302.000,163.969 ZM300.000,131.969 L302.000,131.969 L302.000,139.969 L300.000,139.969 L300.000,131.969 ZM302.000,151.969 L300.000,151.969 L300.000,143.969 L302.000,143.969 L302.000,151.969 ZM300.000,227.969 L302.000,227.969 L302.000,232.000 L302.000,234.000 L300.000,234.000 L292.000,234.000 L292.000,232.000 L300.000,232.000 L300.000,227.969 Z"/>
+    <path d="M101.335,236.009 L99.921,234.594 L105.578,228.938 L99.921,223.281 L101.335,221.866 L108.406,228.938 L101.335,236.009 ZM100.000,229.938 L92.000,229.938 L92.000,227.937 L100.000,227.937 L100.000,229.938 ZM80.000,227.937 L88.000,227.937 L88.000,229.938 L80.000,229.938 L80.000,227.937 Z"/>
+    <path d="M182.000,54.000 L182.000,52.000 L190.000,52.000 L190.000,54.000 L182.000,54.000 ZM170.000,52.000 L178.000,52.000 L178.000,54.000 L170.000,54.000 L170.000,52.000 ZM168.488,60.071 L161.417,53.000 L168.488,45.929 L169.902,47.343 L164.245,53.000 L169.902,58.657 L168.488,60.071 Z"/>
+    <path d="M297.688,276.000 L102.312,276.000 C97.721,276.000 94.000,272.279 94.000,267.688 L94.000,260.000 L306.000,260.000 L306.000,267.688 C306.000,272.279 302.279,276.000 297.688,276.000 ZM117.906,150.312 C117.906,145.721 121.628,142.000 126.218,142.000 L273.688,142.000 C278.279,142.000 282.000,145.721 282.000,150.312 L282.000,256.000 L117.906,256.000 L117.906,150.312 ZM132.000,242.000 L270.000,242.000 L270.000,156.000 L132.000,156.000 L132.000,242.000 Z"/>
+    <path d="M307.074,115.969 L206.926,115.969 C203.101,115.969 200.000,112.868 200.000,109.042 L200.000,38.926 C200.000,35.101 203.101,32.000 206.926,32.000 L307.074,32.000 C310.899,32.000 314.000,35.101 314.000,38.926 L314.000,109.042 C314.000,112.868 310.899,115.969 307.074,115.969 ZM210.000,65.875 C210.000,64.770 209.105,63.875 208.000,63.875 C206.895,63.875 206.000,64.770 206.000,65.875 L206.000,82.000 C206.000,83.105 206.895,84.000 208.000,84.000 C209.105,84.000 210.000,83.105 210.000,82.000 L210.000,65.875 ZM302.000,42.000 L216.000,42.000 L216.000,106.000 L302.000,106.000 L302.000,42.000 Z"/>
+    <path d="M65.844,240.000 L26.156,240.000 C23.861,240.000 22.000,238.139 22.000,235.844 L22.000,162.156 C22.000,159.861 23.861,158.000 26.156,158.000 L65.844,158.000 C68.139,158.000 70.000,159.861 70.000,162.156 L70.000,235.844 C70.000,238.139 68.139,240.000 65.844,240.000 ZM46.000,236.000 C48.287,236.000 50.141,234.195 50.141,231.969 C50.141,229.742 48.287,227.938 46.000,227.938 C43.713,227.938 41.859,229.742 41.859,231.969 C41.859,234.195 43.713,236.000 46.000,236.000 ZM66.000,168.000 L26.000,168.000 L26.000,224.000 L66.000,224.000 L66.000,168.000 Z"/>
+    <path d="M171.906,86.156 C171.906,102.329 159.026,115.469 143.017,115.797 L143.039,115.955 L28.850,115.955 L28.869,115.797 C12.872,115.475 -0.000,102.333 -0.000,86.156 C-0.000,71.661 10.336,59.603 23.994,57.019 C23.620,55.457 23.401,53.834 23.401,52.156 C23.401,40.714 32.606,31.438 43.962,31.438 C47.561,31.438 50.941,32.375 53.884,34.012 C53.883,33.930 53.878,33.848 53.878,33.766 C53.878,17.137 67.301,3.656 83.858,3.656 C97.763,3.656 109.453,13.164 112.843,26.059 C116.677,23.334 121.343,21.719 126.393,21.719 C139.394,21.719 149.933,32.331 149.933,45.422 C149.933,49.572 148.868,53.468 147.007,56.861 C161.114,59.082 171.906,71.351 171.906,86.156 Z"/>
+  </g>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -74,16 +74,17 @@
   skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
   skin/classic/browser/fxa/default-avatar.png                  (../shared/fxa/default-avatar.png)
   skin/classic/browser/fxa/default-avatar@2x.png               (../shared/fxa/default-avatar@2x.png)
   skin/classic/browser/fxa/logo.png                            (../shared/fxa/logo.png)
   skin/classic/browser/fxa/logo@2x.png                         (../shared/fxa/logo@2x.png)
   skin/classic/browser/fxa/sync-illustration.png               (../shared/fxa/sync-illustration.png)
   skin/classic/browser/fxa/sync-illustration@2x.png            (../shared/fxa/sync-illustration@2x.png)
+  skin/classic/browser/fxa/sync-illustration.svg               (../shared/fxa/sync-illustration.svg)
   skin/classic/browser/fxa/android.png                         (../shared/fxa/android.png)
   skin/classic/browser/fxa/android@2x.png                      (../shared/fxa/android@2x.png)
   skin/classic/browser/search-pref.png                         (../shared/search/search-pref.png)
   skin/classic/browser/search-indicator.png                    (../shared/search/search-indicator.png)
   skin/classic/browser/search-indicator@2x.png                 (../shared/search/search-indicator@2x.png)
   skin/classic/browser/search-engine-placeholder.png           (../shared/search/search-engine-placeholder.png)
   skin/classic/browser/search-engine-placeholder@2x.png        (../shared/search/search-engine-placeholder@2x.png)
   skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)