merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 29 Aug 2014 15:13:15 +0200
changeset 202349 d728b2f31c561a464b0f151250f8b68f1bd8730e
parent 202330 5f66dd3d63f2d6ab511231084c7e61fefed5c1ea (current diff)
parent 202348 32032887080222cd4cb42a84f75fba1f01d957cd (diff)
child 202350 04344190ceec0cdb17c091e1374f245d8a91b8bf
child 202398 4a3334d999a69b7f04d5925a1593c1dfd30bb5f2
child 202495 fa7fa3d2187a34252a0280ca8025e74260ac975c
push id48401
push usercbook@mozilla.com
push dateFri, 29 Aug 2014 13:31:34 +0000
treeherdermozilla-inbound@04344190ceec [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.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 mozilla-central a=merge
browser/base/content/browser.js
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1272,16 +1272,22 @@ var gBrowserInit = {
         let slot = secmodDB.findSlotByName("");
         let mpEnabled = slot &&
                         slot.status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED &&
                         slot.status != Ci.nsIPKCS11Slot.SLOT_READY;
         if (mpEnabled) {
           Services.telemetry.getHistogramById("MASTER_PASSWORD_ENABLED").add(mpEnabled);
         }
       }, 5000);
+
+      // Telemetry for tracking protection.
+      let tpEnabled = gPrefService
+                      .getBoolPref("privacy.trackingprotection.enabled");
+      Services.telemetry.getHistogramById("TRACKING_PROTECTION_ENABLED")
+        .add(tpEnabled);
     });
     this.delayedStartupFinished = true;
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
   // Returns the URI(s) to load at startup.
@@ -6489,16 +6495,20 @@ var gIdentityHandler = {
     // - tracking content is blocked
     // - tracking content is not blocked
     if (state &
         (nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT |
          nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT  |
          nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT     |
          nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT)) {
       this.showBadContentDoorhanger(state);
+    } else {
+      // We didn't show the shield
+      Services.telemetry.getHistogramById("TRACKING_PROTECTION_SHIELD")
+        .add(0);
     }
   },
 
   showBadContentDoorhanger : function(state) {
     var currentNotification =
       PopupNotifications.getNotification("bad-content",
         gBrowser.selectedBrowser);
 
@@ -6510,16 +6520,29 @@ var gIdentityHandler = {
       /* keep doorhanger collapsed */
       dismissed: true,
       state: state
     };
 
     // default
     let iconState = "bad-content-blocked-notification-icon";
 
+    // Telemetry for whether the shield was due to tracking protection or not
+    let histogram = Services.telemetry.getHistogramById
+                      ("TRACKING_PROTECTION_SHIELD");
+    if (state & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) {
+      histogram.add(1);
+    } else if (state &
+               Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
+      histogram.add(2);
+    } else {
+      // The shield is due to mixed content, just keep a count so we can
+      // normalize later.
+      histogram.add(3);
+    }
     if (state &
         (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
          Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT)) {
       iconState = "bad-content-unblocked-notification-icon";
     }
 
     PopupNotifications.show(gBrowser.selectedBrowser, "bad-content",
                             "", iconState, null, null, options);
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -1814,26 +1814,34 @@
           let normalizedUrl = Services.io.newURI(
             "https://" + gBrowser.selectedBrowser.currentURI.hostPort,
             null, null);
           // Add the current host in the 'trackingprotection' consumer of
           // the permission manager using a normalized URI. This effectively
           // places this host on the tracking protection white list.
           Services.perms.add(normalizedUrl,
             "trackingprotection", Services.perms.ALLOW_ACTION);
+          // Telemetry for disable protection
+          let histogram = Services.telemetry.getHistogramById(
+              "TRACKING_PROTECTION_EVENTS");
+          histogram.add(1);
           BrowserReload();
         ]]></body>
       </method>
       <method name="enableTrackingContentProtection">
         <body><![CDATA[
           // Remove the current host from the 'trackingprotection' consumer
           // of the permission manager. This effectively removes this host
           // from the tracking protection white list (any list actually).
           Services.perms.remove(gBrowser.selectedBrowser.currentURI.host,
             "trackingprotection");
+          // Telemetry for enable protection
+          let histogram = Services.telemetry.getHistogramById(
+              "TRACKING_PROTECTION_EVENTS");
+          histogram.add(2);
           BrowserReload();
         ]]></body>
       </method>
     </implementation>
   </binding>
 
   <binding id="click-to-play-plugins-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
     <content align="start" style="width: &pluginNotification.width;;">
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -8,16 +8,18 @@ const { classes: Cc, interfaces: Ci, uti
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
 Cu.import("resource:///modules/loop/LoopContacts.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "hookWindowCloseForPanelClose",
                                         "resource://gre/modules/MozSocialAPI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+                                        "resource://gre/modules/PluralForm.jsm");
 XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
   return Cc["@mozilla.org/xre/app-info;1"]
            .getService(Ci.nsIXULAppInfo)
            .QueryInterface(Ci.nsIXULRuntime);
 });
 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
                                          "@mozilla.org/widget/clipboardhelper;1",
                                          "nsIClipboardHelper");
@@ -146,16 +148,33 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(key) {
         return MozLoopService.getStrings(key);
       }
     },
 
     /**
+     * Returns the correct form of a semi-colon separated string
+     * based on the value of the `num` argument and the current locale.
+     *
+     * @param {Integer} num The number used to find the plural form.
+     * @param {String} str The semi-colon separated string of word forms.
+     * @returns {String} The correct word form based on the value of the number
+     *                   and the current locale.
+     */
+    getPluralForm: {
+      enumerable: true,
+      writable: true,
+      value: function(num, str) {
+        return PluralForm.get(num, str);
+      }
+    },
+
+    /**
      * Call to ensure that any necessary registrations for the Loop Service
      * have taken place.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      *
      * @param {Function} callback Will be called once registration is complete,
      *                            or straight away if registration has already
--- a/browser/components/loop/content/libs/l10n.js
+++ b/browser/components/loop/content/libs/l10n.js
@@ -8,37 +8,46 @@
 // and does not automatically translate on DOMContentLoaded, but requires
 // initialize to be called. This improves testability and helps to avoid race
 // conditions.
 (function(window) {
   var gL10nDetails;
   var gLanguage = '';
 
   // fetch an l10n objects
-  function getL10nData(key) {
+  function getL10nData(key, num) {
     var response = gL10nDetails.getStrings(key);
     var data = JSON.parse(response);
     if (!data)
       console.warn('[l10n] #' + key + ' missing for [' + gLanguage + ']');
+    if (num !== undefined) {
+      for (var prop in data) {
+        data[prop] = gL10nDetails.getPluralForm(num, data[prop]);
+      }
+    }
     return data;
   }
 
   // replace {{arguments}} with their values
   function substArguments(text, args) {
     if (!args)
       return text;
 
     return text.replace(/\{\{\s*(\w+)\s*\}\}/g, function(all, name) {
       return name in args ? args[name] : '{{' + name + '}}';
     });
   }
 
   // translate a string
   function translateString(key, args, fallback) {
-    var data = getL10nData(key);
+    if (args && args.num) {
+      var num = args && args.num;
+      delete args.num;
+    }
+    var data = getL10nData(key, num);
     if (!data && fallback)
       data = {textContent: fallback};
     if (!data)
       return '{{' + key + '}}';
     return substArguments(data.textContent, args);
   }
 
   // translate an HTML element
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -545,17 +545,18 @@ loop.shared.views = (function(_, OT, l10
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
         window.close();
       }
       return (
         FeedbackLayout({title: l10n.get("feedback_thank_you_heading")}, 
           React.DOM.p({className: "info thank-you"}, 
             l10n.get("feedback_window_will_close_in", {
-              countdown: this.state.countdown
+              countdown: this.state.countdown,
+              num: this.state.countdown
             }))
         )
       );
     }
   });
 
   /**
    * Feedback view.
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -545,17 +545,18 @@ loop.shared.views = (function(_, OT, l10
       if (this.state.countdown < 1) {
         clearInterval(this._timer);
         window.close();
       }
       return (
         <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
           <p className="info thank-you">{
             l10n.get("feedback_window_will_close_in", {
-              countdown: this.state.countdown
+              countdown: this.state.countdown,
+              num: this.state.countdown
             })}</p>
         </FeedbackLayout>
       );
     }
   });
 
   /**
    * Feedback view.
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -7,8 +7,9 @@ support-files =
 [browser_fxa_login.js]
 skip-if = !debug
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
+[browser_mozLoop_pluralStrings.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This is an integration test from navigator.mozLoop through to the end
+ * effects - rather than just testing MozLoopAPI alone.
+ */
+
+add_task(loadLoopPanel);
+
+add_task(function* test_mozLoop_pluralStrings() {
+  Assert.ok(gMozLoopAPI, "mozLoop should exist");
+
+  var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in"));
+  Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
+               "This window will close in {{countdown}} seconds");
+  Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
+               "This window will close in {{countdown}} second");
+});
--- a/browser/devtools/shared/autocomplete-popup.js
+++ b/browser/devtools/shared/autocomplete-popup.js
@@ -60,16 +60,18 @@ function AutocompletePopup(aDocument, aO
     this._panel.className = "devtools-autocomplete-popup devtools-monospace "
                             + theme + "-theme";
 
     this._panel.setAttribute("noautofocus", "true");
     this._panel.setAttribute("level", "top");
     if (!aOptions.onKeypress) {
       this._panel.setAttribute("ignorekeys", "true");
     }
+    // Stop this appearing as an alert to accessibility.
+    this._panel.setAttribute("role", "presentation");
 
     let mainPopupSet = this._document.getElementById("mainPopupSet");
     if (mainPopupSet) {
       mainPopupSet.appendChild(this._panel);
     }
     else {
       this._document.documentElement.appendChild(this._panel);
     }
@@ -101,16 +103,17 @@ function AutocompletePopup(aDocument, aO
 
   if (this.onClick) {
     this._list.addEventListener("click", this.onClick, false);
   }
 
   if (this.onKeypress) {
     this._list.addEventListener("keypress", this.onKeypress, false);
   }
+  this._itemIdCounter = 0;
 }
 exports.AutocompletePopup = AutocompletePopup;
 
 AutocompletePopup.prototype = {
   _document: null,
   _panel: null,
   _list: null,
   __scrollbarWidth: null,
@@ -143,16 +146,18 @@ AutocompletePopup.prototype = {
     }
   },
 
   /**
    * Hide the autocomplete popup panel.
    */
   hidePopup: function AP_hidePopup()
   {
+    // Return accessibility focus to the input.
+    this._document.activeElement.removeAttribute("aria-activedescendant");
     this._panel.hidePopup();
   },
 
   /**
    * Check if the autocomplete popup is open.
    */
   get isOpen() {
     return this._panel &&
@@ -291,16 +296,33 @@ AutocompletePopup.prototype = {
       return;
     }
 
     this._list.style.width = (this._maxLabelLength + 3) +"ch";
     this._list.ensureIndexIsVisible(this._list.selectedIndex);
   },
 
   /**
+   * Update accessibility appropriately when the selected item is changed.
+   *
+   * @private
+   */
+  _updateAriaActiveDescendant: function AP__updateAriaActiveDescendant()
+  {
+    if (!this._list.selectedItem) {
+      // Return accessibility focus to the input.
+      this._document.activeElement.removeAttribute("aria-activedescendant");
+      return;
+    }
+    // Focus this for accessibility so users know about the selected item.
+    this._document.activeElement.setAttribute("aria-activedescendant",
+                                              this._list.selectedItem.id);
+  },
+
+  /**
    * Clear all the items from the autocomplete list.
    */
   clearItems: function AP_clearItems()
   {
     // Reset the selectedIndex to -1 before clearing the list
     this.selectedIndex = -1;
 
     while (this._list.hasChildNodes()) {
@@ -335,16 +357,17 @@ AutocompletePopup.prototype = {
    * @param number aIndex
    *        The number (index) of the item you want to select in the list.
    */
   set selectedIndex(aIndex) {
     this._list.selectedIndex = aIndex;
     if (this.isOpen && this._list.ensureIndexIsVisible) {
       this._list.ensureIndexIsVisible(this._list.selectedIndex);
     }
+    this._updateAriaActiveDescendant();
   },
 
   /**
    * Getter for the selected item.
    * @type object
    */
   get selectedItem() {
     return this._list.selectedItem ?
@@ -357,16 +380,17 @@ AutocompletePopup.prototype = {
    * @param object aItem
    *        The object you want selected in the list.
    */
   set selectedItem(aItem) {
     this._list.selectedItem = this._findListItem(aItem);
     if (this.isOpen) {
       this._list.ensureIndexIsVisible(this._list.selectedIndex);
     }
+    this._updateAriaActiveDescendant();
   },
 
   /**
    * Append an item into the autocomplete list.
    *
    * @param object aItem
    *        The item you want appended to the list.
    *        The item object can have the following properties:
@@ -378,16 +402,18 @@ AutocompletePopup.prototype = {
    *                   present, |preLabel.length| starting characters will be
    *                   removed from label.
    *        - count {Number} [Optional] The number to represent the count of
    *                autocompleted label.
    */
   appendItem: function AP_appendItem(aItem)
   {
     let listItem = this._document.createElementNS(XUL_NS, "richlistitem");
+    // Items must have an id for accessibility.
+    listItem.id = this._panel.id + "_item_" + this._itemIdCounter++;
     if (this.direction) {
       listItem.setAttribute("dir", this.direction);
     }
     let label = this._document.createElementNS(XUL_NS, "label");
     label.setAttribute("value", aItem.label);
     label.setAttribute("class", "autocomplete-value");
     if (aItem.preLabel) {
       let preDesc = this._document.createElementNS(XUL_NS, "label");
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -41,16 +41,17 @@ support-files =
   doc_uncached.html
 
 [browser_styleeditor_autocomplete.js]
 [browser_styleeditor_bug_740541_iframes.js]
 skip-if = os == "linux" || "mac" # bug 949355
 [browser_styleeditor_bug_851132_middle_click.js]
 [browser_styleeditor_bug_870339.js]
 [browser_styleeditor_cmd_edit.js]
+skip-if = e10s # Bug 1055333 - style editor tests disabled with e10s
 [browser_styleeditor_enabled.js]
 [browser_styleeditor_fetch-from-cache.js]
 [browser_styleeditor_filesave.js]
 [browser_styleeditor_highlight-selector.js]
 [browser_styleeditor_import.js]
 [browser_styleeditor_import_rule.js]
 [browser_styleeditor_init.js]
 [browser_styleeditor_inline_friendly_names.js]
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_585991_autocomplete_popup.js
@@ -17,79 +17,98 @@ function consoleOpened(HUD) {
   let items = [
     {label: "item0", value: "value0"},
     {label: "item1", value: "value1"},
     {label: "item2", value: "value2"},
   ];
 
   let popup = HUD.jsterm.autocompletePopup;
 
+  let input = popup._document.activeElement;
+  function getActiveDescendant() {
+    return input.ownerDocument.getElementById(
+      input.getAttribute("aria-activedescendant"));
+  }
+
   ok(!popup.isOpen, "popup is not open");
+  ok(!input.hasAttribute("aria-activedescendant"), "no aria-activedescendant");
 
   popup._panel.addEventListener("popupshown", function() {
     popup._panel.removeEventListener("popupshown", arguments.callee, false);
 
     ok(popup.isOpen, "popup is open");
 
     is(popup.itemCount, 0, "no items");
+    ok(!input.hasAttribute("aria-activedescendant"),
+       "no aria-activedescendant");
 
     popup.setItems(items);
 
     is(popup.itemCount, items.length, "items added");
 
     let sameItems = popup.getItems();
     is(sameItems.every(function(aItem, aIndex) {
       return aItem === items[aIndex];
     }), true, "getItems returns back the same items");
 
     is(popup.selectedIndex, 2,
        "Index of the first item from bottom is selected.");
     is(popup.selectedItem, items[2], "First item from bottom is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     popup.selectedIndex = 1;
 
     is(popup.selectedIndex, 1, "index 1 is selected");
     is(popup.selectedItem, items[1], "item1 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     popup.selectedItem = items[2];
 
     is(popup.selectedIndex, 2, "index 2 is selected");
     is(popup.selectedItem, items[2], "item2 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     is(popup.selectPreviousItem(), items[1], "selectPreviousItem() works");
 
     is(popup.selectedIndex, 1, "index 1 is selected");
     is(popup.selectedItem, items[1], "item1 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     is(popup.selectNextItem(), items[2], "selectPreviousItem() works");
 
     is(popup.selectedIndex, 2, "index 2 is selected");
     is(popup.selectedItem, items[2], "item2 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     ok(popup.selectNextItem(), "selectPreviousItem() works");
 
     is(popup.selectedIndex, 0, "index 0 is selected");
     is(popup.selectedItem, items[0], "item0 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     items.push({label: "label3", value: "value3"});
     popup.appendItem(items[3]);
 
     is(popup.itemCount, items.length, "item3 appended");
 
     popup.selectedIndex = 3;
     is(popup.selectedItem, items[3], "item3 is selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
 
     popup.removeItem(items[2]);
 
     is(popup.selectedIndex, 2, "index2 is selected");
     is(popup.selectedItem, items[3], "item3 is still selected");
+    ok(getActiveDescendant().selected, "aria-activedescendant is correct");
     is(popup.itemCount, items.length - 1, "item2 removed");
 
     popup.clearItems();
     is(popup.itemCount, 0, "items cleared");
+    ok(!input.hasAttribute("aria-activedescendant"),
+       "no aria-activedescendant");
 
     popup.hidePopup();
     finishTest();
   }, false);
 
   popup.openPopup();
 }
 
--- a/browser/devtools/webconsole/webconsole.xul
+++ b/browser/devtools/webconsole/webconsole.xul
@@ -178,17 +178,18 @@ function goUpdateConsoleCommands() {
              tabindex="0" role="document" aria-live="polite" />
       </hbox>
       <notificationbox id="webconsole-notificationbox">
         <hbox class="jsterm-input-container" style="direction:ltr">
           <stack class="jsterm-stack-node" flex="1">
             <textbox class="jsterm-complete-node devtools-monospace"
                      multiline="true" rows="1" tabindex="-1"/>
             <textbox class="jsterm-input-node devtools-monospace"
-                     multiline="true" rows="1" tabindex="0"/>
+                     multiline="true" rows="1" tabindex="0"
+                     aria-autocomplete="list"/>
           </stack>
         </hbox>
       </notificationbox>
     </vbox>
 
     <splitter class="devtools-side-splitter"/>
 
     <tabbox id="webconsole-sidebar" class="devtools-sidebar-tabs" hidden="true" width="300">
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -50,19 +50,21 @@ feedback_thank_you_heading=Thank you for
 feedback_category_audio_quality=Audio quality
 feedback_category_video_quality=Video quality
 feedback_category_was_disconnected=Was disconnected
 feedback_category_confusing=Confusing
 feedback_category_other=Other:
 feedback_custom_category_text_placeholder=What went wrong?
 feedback_submit_button=Submit
 feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in): In this item, don't
-## translate the part between {{..}}
-feedback_window_will_close_in=This window will close in {{countdown}} seconds
+## LOCALIZATION NOTE (feedback_window_will_close_in):
+## Semicolon-separated list of plural forms. See:
+## http://developer.mozilla.org/en/docs/Localization_and_Plurals
+## In this item, don't translate the part between {{..}}
+feedback_window_will_close_in=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
 
 share_email_subject2=Invitation to chat
 ## LOCALIZATION NOTE (share_email_body2): In this item, don't translate the
 ## part between {{..}} and leave the \r\n\r\n part alone
 share_email_body2=Please click this link to call me:\r\n\r\n{{callUrl}}
 share_button=Email
 copy_url_button=Copy
 copied_url_button=Copied!
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1625,17 +1625,19 @@ var BrowserApp = {
           type: "Search:Keyword",
           identifier: engine.identifier,
           name: engine.name,
           query: query
         });
         break;
 
       case "Browser:Quit":
-        this.quit(aData ? JSON.parse(aData) : null);
+        // Add-ons like QuitNow and CleanQuit provide aData as an empty-string ("").
+        // Pass undefined to invoke the methods default parms.
+        this.quit(aData ? JSON.parse(aData) : undefined);
         break;
 
       case "SaveAs:PDF":
         this.saveAsPDF(browser);
         break;
 
       case "Preferences:Set":
         this.setPreferences(aData);
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -112,16 +112,19 @@ SessionStore.prototype = {
         this._lastClosedTabIndex = -1;
 
         if (this._loadState == STATE_RUNNING) {
           // Save the purged state immediately
           this.saveState();
         }
 
         Services.obs.notifyObservers(null, "sessionstore-state-purge-complete", "");
+        if (this._notifyClosedTabs) {
+          this._sendClosedTabsToJava(Services.wm.getMostRecentWindow("navigator:browser"));
+        }
         break;
       case "timer-callback":
         // Timer call back for delayed saving
         this._saveTimer = null;
         if (this._pendingWrite) {
           this.saveState();
         }
         break;
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -158,17 +158,28 @@ AccountState.prototype = {
           validUntil: willBeValidUntil
         };
         return cert;
       }
     ).then(result => this.resolve(result));
   },
 
   getKeyPair: function(mustBeValidUntil) {
-    if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
+    // If the debugging pref to ignore cached authentication credentials is set for Sync,
+    // then don't use any cached key pair, i.e., generate a new one and get it signed.
+    // The purpose of this pref is to expedite any auth errors as the result of a
+    // expired or revoked FxA session token, e.g., from resetting or changing the FxA
+    // password.
+    let ignoreCachedAuthCredentials = false;
+    try {
+      ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
+    } catch(e) {
+      // Pref doesn't exist
+    }
+    if (!ignoreCachedAuthCredentials && this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
       log.debug("getKeyPair: already have a keyPair");
       return this.resolve(this.keyPair.keyPair);
     }
     // Otherwse, create a keypair and set validity limit.
     let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
     let d = Promise.defer();
     jwcrypto.generateKeyPair("DS160", (err, kp) => {
       if (err) {
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -470,16 +470,27 @@ this.BrowserIDManager.prototype = {
     );
   },
 
   /**
    * Do we have a non-null, not yet expired token for the user currently
    * signed in?
    */
   hasValidToken: function() {
+    // If pref is set to ignore cached authentication credentials for debugging,
+    // then return false to force the fetching of a new token.
+    let ignoreCachedAuthCredentials = false;
+    try {
+      ignoreCachedAuthCredentials = Svc.Prefs.get("debug.ignoreCachedAuthCredentials");
+    } catch(e) {
+      // Pref doesn't exist
+    }
+    if (ignoreCachedAuthCredentials) {
+      return false;
+    }
     if (!this._token) {
       return false;
     }
     if (this._token.expiration < this._now()) {
       return false;
     }
     return true;
   },
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6501,10 +6501,27 @@
     "description": "Social content has been interacted with (0:share, 1:status, 2:bookmark, 3: sidebar)"
   },
   "SOCIAL_SIDEBAR_OPEN_DURATION": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "10000000",
     "n_buckets": 10,
     "description": "Sidebar showing: seconds that the sidebar has been opened"
+  },
+  "TRACKING_PROTECTION_ENABLED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Whether or not a session has tracking protection enabled"
+  },
+  "TRACKING_PROTECTION_SHIELD": {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 4,
+    "description": "Tracking protection shield (0 = not shown, 1 = blocked, 2 = loaded, 3 = due to mixed content"
+  },
+  "TRACKING_PROTECTION_EVENTS": {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 3,
+    "description": "Doorhanger shown = 0, Disable = 1, Enable = 2"
   }
 }