Merge mozilla-central to inbound. a=merge CLOSED TREE
authorBogdan Tara <btara@mozilla.com>
Fri, 03 Aug 2018 13:28:32 +0300
changeset 826276 d138efcfa0061239d896c306229f06a8f7398c8a
parent 826270 21b78cbe6b726f1cc2722f2c58118eb315f3175f (current diff)
parent 826275 9ad7706def240500bb39914bc3706efda4c46fc9 (diff)
child 826277 d2a1a9bd64832a3d55895dd42e5786b408e8038b
child 826315 61310ac63cfd3f5fdd04420311c8273e20b2b27d
push id118275
push userbmo:dharvey@mozilla.com
push dateFri, 03 Aug 2018 11:44:33 +0000
reviewersmerge
milestone63.0a1
Merge mozilla-central to inbound. a=merge CLOSED TREE
browser/extensions/pocket/content/panels/img/pocket.svg
docshell/base/nsDocShell.cpp
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -251,19 +251,17 @@ nsContextMenu.prototype = {
       }
     }
 
     if (context.shouldInitInlineSpellCheckerUIWithChildren) {
       if (this.isRemote) {
         InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
       } else {
         var targetWin = this.ownerDoc.defaultView;
-        var editingSession = targetWin.docShell
-                                      .QueryInterface(Ci.nsIInterfaceRequestor)
-                                      .getInterface(Ci.nsIEditingSession);
+        var {editingSession} = targetWin.docShell;
 
         InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
         InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,
                                            document.popupRangeOffset);
       }
 
       let canSpell = InlineSpellCheckerUI.canSpellCheck && this.canSpellCheck;
       this.showItem("spell-check-enabled", canSpell);
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -2527,27 +2527,41 @@ window._gBrowser = {
     // don't set the pref unless they press OK and it's false
     if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
       Services.prefs.setBoolPref(pref, false);
 
     return reallyClose;
   },
 
   getTabsToTheEndFrom(aTab) {
+    let tab;
+    if (aTab.multiselected) {
+      // In a multi-select context, pick the rightmost
+      // selected tab as reference.
+      let selectedTabs = this.selectedTabs;
+      tab = selectedTabs[selectedTabs.length - 1];
+    } else {
+      tab = aTab;
+    }
+
     let tabsToEnd = [];
     let tabs = this.visibleTabs;
     for (let i = tabs.length - 1; i >= 0; --i) {
-      if (tabs[i] == aTab || tabs[i].pinned) {
+      if (tabs[i] == tab || tabs[i].pinned) {
         break;
       }
       tabsToEnd.push(tabs[i]);
     }
     return tabsToEnd;
   },
 
+  /**
+   * In a multi-select context, the tabs (except pinned tabs) that are located to the
+   * right of the rightmost selected tab will be removed.
+   */
   removeTabsToTheEndFrom(aTab) {
     let tabs = this.getTabsToTheEndFrom(aTab);
     if (!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)) {
       return;
     }
 
     this.removeTabs(tabs);
   },
@@ -2557,17 +2571,16 @@ window._gBrowser = {
    * Otherwise all unpinned tabs except aTab are removed.
    */
   removeAllTabsBut(aTab) {
     let tabsToRemove = [];
     if (aTab && aTab.multiselected) {
       tabsToRemove = this.visibleTabs.filter(tab => !tab.multiselected && !tab.pinned);
     } else {
       tabsToRemove = this.visibleTabs.filter(tab => tab != aTab && !tab.pinned);
-      this.selectedTab = aTab;
     }
 
     if (!this.warnAboutClosingTabs(tabsToRemove.length, this.closingTabsEnum.OTHER)) {
       return;
     }
 
     this.removeTabs(tabsToRemove);
   },
@@ -2577,37 +2590,46 @@ window._gBrowser = {
     if (!this.warnAboutClosingTabs(selectedTabs.length, this.closingTabsEnum.MULTI_SELECTED)) {
       return;
     }
 
     this.removeTabs(selectedTabs);
   },
 
   removeTabs(tabs) {
-    let tabsWithBeforeUnload = [];
-    let lastToClose;
-    let params = { animate: true };
-    for (let tab of tabs) {
-      if (tab.selected) {
-        lastToClose = tab;
-      } else if (this._hasBeforeUnload(tab)) {
-        tabsWithBeforeUnload.push(tab);
-      } else {
-        this.removeTab(tab, params);
+    this._clearMultiSelectionLocked = true;
+
+    // Guarantee that _clearMultiSelectionLocked lock gets released.
+    try {
+      let tabsWithBeforeUnload = [];
+      let lastToClose;
+      let aParams = { animate: true };
+      for (let tab of tabs) {
+        if (tab.selected)
+          lastToClose = tab;
+        else if (this._hasBeforeUnload(tab))
+          tabsWithBeforeUnload.push(tab);
+        else
+          this.removeTab(tab, aParams);
       }
-    }
-    for (let tab of tabsWithBeforeUnload) {
-      this.removeTab(tab, params);
-    }
-
-    // Avoid changing the selected browser several times by removing it,
-    // if appropriate, lastly.
-    if (lastToClose) {
-      this.removeTab(lastToClose, params);
-    }
+      for (let tab of tabsWithBeforeUnload) {
+        this.removeTab(tab, aParams);
+      }
+
+      // Avoid changing the selected browser several times by removing it,
+      // if appropriate, lastly.
+      if (lastToClose) {
+        this.removeTab(lastToClose, aParams);
+      }
+    } catch (e) {
+      Cu.reportError(e);
+    }
+
+    this._clearMultiSelectionLocked = false;
+    this.avoidSingleSelectedTab();
   },
 
   removeCurrentTab(aParams) {
     this.removeTab(this.selectedTab, aParams);
   },
 
   removeTab(aTab, {
     animate,
@@ -3688,60 +3710,85 @@ window._gBrowser = {
     if (this._clearMultiSelectionLocked) {
       if (this._clearMultiSelectionLockedOnce) {
         this._clearMultiSelectionLockedOnce = false;
         this._clearMultiSelectionLocked = false;
       }
       return;
     }
 
-    let selectedTabs = this.selectedTabs;
-    if (selectedTabs.length < 2) {
+    if (this.multiSelectedTabsCount < 1) {
       return;
     }
 
-    for (let tab of selectedTabs) {
+    for (let tab of this.selectedTabs) {
       tab.removeAttribute("multiselected");
     }
     this._multiSelectedTabsSet = new WeakSet();
     this._lastMultiSelectedTabRef = null;
     if (updatePositionalAttributes) {
       this.tabContainer._setPositionalAttributes();
     }
   },
 
   lockClearMultiSelectionOnce() {
     this._clearMultiSelectionLockedOnce = true;
     this._clearMultiSelectionLocked = true;
   },
 
   /**
-   * Remove the active tab from the multiselection if it's the only one left there.
+   * Remove a tab from the multiselection if it's the only one left there.
+   *
+   * In fact, some scenario may lead to only one single tab multi-selected,
+   * this is something to avoid (Chrome does the same)
+   * Consider 4 tabs A,B,C,D with A having the focus
+   * 1. select C with Ctrl
+   * 2. Right-click on B and "Close Tabs to The Right"
+   *
+   * Expected result
+   * C and D closing
+   * A being the only multi-selected tab, selection should be cleared
+   *
+   *
+   * Single selected tab could even happen with a none-focused tab.
+   * For exemple with the menu "Close other tabs", it could happen
+   * with a multi-selected pinned tab.
+   * For illustration, consider 4 tabs A,B,C,D with B active
+   * 1. pin A and Ctrl-select it
+   * 2. Ctrl-select C
+   * 3. right-click on D and click "Close Other Tabs"
+   *
+   * Expected result
+   * B and C closing
+   * A[pinned] being the only multi-selected tab, selection should be cleared.
    */
-  updateActiveTabMultiSelectState() {
-    if (this.selectedTabs.length == 1) {
+  avoidSingleSelectedTab() {
+    if (this.multiSelectedTabsCount == 1 ) {
       this.clearMultiSelectedTabs();
     }
   },
 
   switchToNextMultiSelectedTab() {
     this._clearMultiSelectionLocked = true;
+
+    // Guarantee that _clearMultiSelectionLocked lock gets released.
     try {
       let lastMultiSelectedTab = gBrowser.lastMultiSelectedTab;
       if (lastMultiSelectedTab != gBrowser.selectedTab) {
         gBrowser.selectedTab = lastMultiSelectedTab;
       } else {
         let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
           .filter(tab => tab.isConnected && !tab.closing);
         let length = selectedTabs.length;
         gBrowser.selectedTab = selectedTabs[length - 1];
       }
     } catch (e) {
       Cu.reportError(e);
     }
+
     this._clearMultiSelectionLocked = false;
   },
 
   set selectedTabs(tabs) {
     this.clearMultiSelectedTabs(false);
     this.selectedTab = tabs[0];
     if (tabs.length > 1) {
       for (let tab of tabs) {
@@ -5093,20 +5140,19 @@ var TabContextMenu = {
     contextUnpinSelectedTabs.hidden = !this.contextTab.pinned || !multiselectionContext;
 
     // Disable "Close Tabs to the Right" if there are no tabs
     // following it.
     document.getElementById("context_closeTabsToTheEnd").disabled =
       gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0;
 
     // Disable "Close other Tabs" if there are no unpinned tabs.
-    let unpinnedTabsToClose = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
-    if (!this.contextTab.pinned) {
-      unpinnedTabsToClose--;
-    }
+    let unpinnedTabsToClose = multiselectionContext ?
+      gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length :
+      gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned).length;
     document.getElementById("context_closeOtherTabs").disabled = unpinnedTabsToClose < 1;
 
     // Only one of close_tab/close_selected_tabs should be visible
     document.getElementById("context_closeTab").hidden = multiselectionContext;
     document.getElementById("context_closeSelectedTabs").hidden = !multiselectionContext;
 
     // Hide "Bookmark All Tabs" for a pinned tab or multiselection.
     // Update its state if visible.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2081,17 +2081,17 @@
           }
           if (accelKey) {
             // Ctrl (Cmd for mac) key is pressed
             if (this.multiselected) {
               gBrowser.removeFromMultiSelectedTabs(this);
               if (this == gBrowser.selectedTab) {
                 gBrowser.switchToNextMultiSelectedTab();
               }
-              gBrowser.updateActiveTabMultiSelectState();
+              gBrowser.avoidSingleSelectedTab();
             } else if (this != gBrowser.selectedTab) {
               for (let tab of [this, gBrowser.selectedTab]) {
                 gBrowser.addToMultiSelectedTabs(tab, true);
               }
               gBrowser.tabContainer._setPositionalAttributes();
               gBrowser.lastMultiSelectedTab = this;
             }
             return;
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -18,16 +18,17 @@ tags = audiochannel
 skip-if = (verify && debug && (os == 'linux'))
 support-files =
   test_bug1358314.html
 [browser_isLocalAboutURI.js]
 [browser_multiselect_tabs_active_tab_selected_by_default.js]
 [browser_multiselect_tabs_bookmark.js]
 [browser_multiselect_tabs_clear_selection_when_tab_switch.js]
 [browser_multiselect_tabs_close_other_tabs.js]
+[browser_multiselect_tabs_close_tabs_to_the_right.js]
 [browser_multiselect_tabs_close_using_shortcuts.js]
 [browser_multiselect_tabs_close.js]
 [browser_multiselect_tabs_move_to_new_window_contextmenu.js]
 [browser_multiselect_tabs_mute_unmute.js]
 [browser_multiselect_tabs_pin_unpin.js]
 [browser_multiselect_tabs_positional_attrs.js]
 [browser_multiselect_tabs_reload.js]
 [browser_multiselect_tabs_reorder.js]
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_other_tabs.js
@@ -60,33 +60,41 @@ add_task(async function withAMultiSelect
 });
 
 add_task(async function withNotAMultiSelectedTab() {
   let initialTab = gBrowser.selectedTab;
   let tab1 = await addTab();
   let tab2 = await addTab();
   let tab3 = await addTab();
   let tab4 = await addTab();
+  let tab5 = await addTab();
 
   is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
 
   await BrowserTestUtils.switchTab(gBrowser, tab1);
   await triggerClickOn(tab2, { ctrlKey: true });
+  await triggerClickOn(tab5, { ctrlKey: true });
 
   let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
   gBrowser.pinTab(tab4);
   await tab4Pinned;
 
+  let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned");
+  gBrowser.pinTab(tab5);
+  await tab5Pinned;
+
   ok(!initialTab.multiselected, "InitialTab is not multiselected");
   ok(tab1.multiselected, "Tab1 is multiselected");
   ok(tab2.multiselected, "Tab2 is multiselected");
   ok(!tab3.multiselected, "Tab3 is not multiselected");
   ok(!tab4.multiselected, "Tab4 is not multiselected");
   ok(tab4.pinned, "Tab4 is pinned");
-  is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+  ok(tab5.multiselected, "Tab5 is multiselected");
+  ok(tab5.pinned, "Tab5 is pinned");
+  is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
   is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
 
   let closingTabs = [tab1, tab2, tab3];
   let tabClosingPromises = [];
   for (let tab of closingTabs) {
     tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
   }
 
@@ -96,13 +104,15 @@ add_task(async function withNotAMultiSel
     await promise;
   }
 
   ok(!initialTab.closing, "InitialTab is not closing");
   ok(tab1.closing, "Tab1 is closing");
   ok(tab2.closing, "Tab2 is closing");
   ok(tab3.closing, "Tab3 is closing");
   ok(!tab4.closing, "Tab4 is not closing");
-  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+  ok(!tab5.closing, "Tab5 is not closing");
+  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs, selection is cleared");
   is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab now");
 
-  BrowserTestUtils.removeTab(tab4);
+  for (let tab of [tab4, tab5])
+    BrowserTestUtils.removeTab(tab);
 });
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_close_tabs_to_the_right.js
@@ -0,0 +1,114 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
+
+add_task(async function setPref() {
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [PREF_MULTISELECT_TABS, true],
+      [PREF_WARN_ON_CLOSE, false]
+    ]
+  });
+});
+
+add_task(async function withAMultiSelectedTab() {
+  let tab1 = await addTab();
+  let tab2 = await addTab();
+  let tab3 = await addTab();
+  let tab4 = await addTab();
+  let tab5 = await addTab();
+
+  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await triggerClickOn(tab3, { ctrlKey: true });
+
+  ok(tab1.multiselected, "Tab1 is multiselected");
+  ok(!tab2.multiselected, "Tab2 is not multiselected");
+  ok(tab3.multiselected, "Tab3 is multiselected");
+  ok(!tab4.multiselected, "Tab4 is not multiselected");
+  ok(!tab5.multiselected, "Tab5 is not multiselected");
+  is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+  let closingTabs = [tab4, tab5];
+  let tabClosingPromises = [];
+  for (let tab of closingTabs) {
+    tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+  }
+
+  gBrowser.removeTabsToTheEndFrom(tab1);
+
+  for (let promise of tabClosingPromises) {
+    await promise;
+  }
+
+  ok(!tab1.closing, "Tab1 is not closing");
+  ok(!tab2.closing, "Tab2 is not closing");
+  ok(!tab3.closing, "Tab3 is not closing");
+  ok(tab4.closing, "Tab4 is closing");
+  ok(tab5.closing, "Tab5 is closing");
+  is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
+
+  for (let tab of [tab1, tab2, tab3])
+    BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function withNotAMultiSelectedTab() {
+  let tab1 = await addTab();
+  let tab2 = await addTab();
+  let tab3 = await addTab();
+  let tab4 = await addTab();
+  let tab5 = await addTab();
+
+  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+  await BrowserTestUtils.switchTab(gBrowser, tab1);
+  await triggerClickOn(tab3, { ctrlKey: true });
+  await triggerClickOn(tab5, { ctrlKey: true });
+
+  ok(tab1.multiselected, "Tab1 is multiselected");
+  ok(!tab2.multiselected, "Tab2 is not multiselected");
+  ok(tab3.multiselected, "Tab3 is multiselected");
+  ok(!tab4.multiselected, "Tab4 is not multiselected");
+  ok(tab5.multiselected, "Tab5 is multiselected");
+  is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+  let closingTabs = [tab5];
+  let tabClosingPromises = [];
+  for (let tab of closingTabs) {
+    tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+  }
+
+  gBrowser.removeTabsToTheEndFrom(tab4);
+
+  for (let promise of tabClosingPromises) {
+    await promise;
+  }
+
+  ok(!tab1.closing, "Tab1 is not closing");
+  ok(!tab2.closing, "Tab2 is not closing");
+  ok(!tab3.closing, "Tab3 is not closing");
+  ok(!tab4.closing, "Tab4 is not closing");
+  ok(tab5.closing, "Tab5 is closing");
+  is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared");
+
+  closingTabs = [tab3, tab4];
+  tabClosingPromises = [];
+  for (let tab of closingTabs) {
+    tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
+  }
+
+  gBrowser.removeTabsToTheEndFrom(tab2);
+
+  for (let promise of tabClosingPromises) {
+    await promise;
+  }
+
+  ok(!tab1.closing, "Tab1 is not closing");
+  ok(!tab2.closing, "Tab2 is not closing");
+  ok(tab3.closing, "Tab3 is closing");
+  ok(tab4.closing, "Tab4 is closing");
+  is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared");
+
+  for (let tab of [tab1, tab2])
+    BrowserTestUtils.removeTab(tab);
+});
--- a/browser/components/newtab/data/content/assets/glyph-pocket-16.svg
+++ b/browser/components/newtab/data/content/assets/glyph-pocket-16.svg
@@ -1,1 +1,6 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/></svg>
\ No newline at end of file
+<!-- 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" width="16" height="16" viewBox="0 0 16 16">
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
+</svg>
--- a/browser/components/payments/res/containers/address-form.css
+++ b/browser/components/payments/res/containers/address-form.css
@@ -19,21 +19,16 @@ address-form[address-fields] #postal-cod
 address-form[address-fields] #country-container,
 address-form[address-fields]:not([address-fields~='email']) #email-container,
 address-form[address-fields]:not([address-fields~='tel']) #tel-container {
   /* !important is needed because autofillEditForms.js sets
      inline styles on the form fields with display: flex; */
   display: none !important;
 }
 
-label[required] > span:first-of-type::after {
-  /* The asterisk should be localized, bug 1472278 */
-  content: "*";
-}
-
 .error-text:not(:empty) {
   color: #fff;
   background-color: #d70022;
   border-radius: 2px;
   /* The padding-top and padding-bottom are referenced by address-form.js */
   padding: 5px 12px;
   position: absolute;
   z-index: 1;
--- a/browser/components/payments/res/containers/address-form.js
+++ b/browser/components/payments/res/containers/address-form.js
@@ -148,28 +148,17 @@ export default class AddressForm extends
       this.persistCheckbox.hidden = false;
       this.persistCheckbox.checked = state.isPrivate ? false :
                                                        defaults.saveAddressDefaultChecked;
     }
 
     this.formHandler.loadRecord(record);
 
     // Add validation to some address fields
-    for (let formElement of this.form.elements) {
-      let container = formElement.closest(`#${formElement.id}-container`);
-      if (formElement.localName == "button" || !container) {
-        continue;
-      }
-      let required = formElement.required && !formElement.disabled;
-      if (required) {
-        container.setAttribute("required", "true");
-      } else {
-        container.removeAttribute("required");
-      }
-    }
+    this.updateRequiredState();
 
     let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
     for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
       let container = this.form.querySelector(errorSelector + "-container");
       let field = this.form.querySelector(errorSelector);
       let errorText = (shippingAddressErrors && shippingAddressErrors[errorName]) || "";
       container.classList.toggle("error", !!errorText);
       field.setCustomValidity(errorText);
@@ -200,16 +189,17 @@ export default class AddressForm extends
       data.span.style.top = (data.top - 10) + "px";
       if (isRTL) {
         data.span.style.right = data.right + "px";
       } else {
         data.span.style.left = data.left + "px";
       }
     }
   }
+
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
       }
     }
   }
@@ -241,16 +231,33 @@ export default class AddressForm extends
         break;
       }
       default: {
         throw new Error("Unexpected click target");
       }
     }
   }
 
+  updateRequiredState() {
+    for (let formElement of this.form.elements) {
+      let container = formElement.closest(`#${formElement.id}-container`);
+      if (formElement.localName == "button" || !container) {
+        continue;
+      }
+      let span = container.querySelector("span");
+      span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
+      let required = formElement.required && !formElement.disabled;
+      if (required) {
+        container.setAttribute("required", "true");
+      } else {
+        container.removeAttribute("required");
+      }
+    }
+  }
+
   async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
       page,
       tempAddresses,
       savedBasicCards,
       "address-page": addressPage,
--- a/browser/components/payments/res/containers/basic-card-form.js
+++ b/browser/components/payments/res/containers/basic-card-form.js
@@ -176,16 +176,17 @@ export default class BasicCardForm exten
     } else if (!editing) {
       if (paymentRequest.getAddresses(state)[selectedShippingAddress]) {
         billingAddressSelect.value = selectedShippingAddress;
       } else {
         billingAddressSelect.value = Object.keys(addresses)[0];
       }
     }
 
+    this.updateRequiredState();
     this.updateSaveButtonState();
   }
 
   handleEvent(event) {
     switch (event.type) {
       case "click": {
         this.onClick(event);
         break;
@@ -294,16 +295,30 @@ export default class BasicCardForm exten
   onInvalid(event) {
     this.saveButton.disabled = true;
   }
 
   updateSaveButtonState() {
     this.saveButton.disabled = !this.form.checkValidity();
   }
 
+  updateRequiredState() {
+    for (let formElement of this.form.elements) {
+      let container = formElement.closest("label") || formElement.closest("div");
+      let span = container.querySelector("span");
+      span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
+      let required = formElement.required && !formElement.disabled;
+      if (required) {
+        container.setAttribute("required", "true");
+      } else {
+        container.removeAttribute("required");
+      }
+    }
+  }
+
   async saveRecord() {
     let record = this.formHandler.buildFormObject();
     let currentState = this.requestStore.getState();
     let {
       tempBasicCards,
       "basic-card-page": basicCardPage,
     } = currentState;
     let editing = !!basicCardPage.guid;
new file mode 100644
--- /dev/null
+++ b/browser/components/payments/res/containers/form.css
@@ -0,0 +1,7 @@
+/* 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/. */
+
+:-moz-any(label, div)[required] > span:first-of-type::after {
+  content: attr(fieldRequiredSymbol);
+}
--- a/browser/components/payments/res/containers/payment-method-picker.js
+++ b/browser/components/payments/res/containers/payment-method-picker.js
@@ -13,17 +13,17 @@ import paymentRequest from "../paymentRe
  */
 
 export default class PaymentMethodPicker extends RichPicker {
   constructor() {
     super();
     this.dropdown.setAttribute("option-type", "basic-card-option");
     this.securityCodeInput = document.createElement("input");
     this.securityCodeInput.autocomplete = "off";
-    this.securityCodeInput.placeholder = "CVV"; /* XXX Bug 1473772 */
+    this.securityCodeInput.placeholder = this.dataset.cvvPlaceholder;
     this.securityCodeInput.size = 3;
     this.securityCodeInput.classList.add("security-code");
     this.securityCodeInput.addEventListener("change", this);
   }
 
   connectedCallback() {
     super.connectedCallback();
     this.dropdown.after(this.securityCodeInput);
--- a/browser/components/payments/res/paymentRequest.xhtml
+++ b/browser/components/payments/res/paymentRequest.xhtml
@@ -6,16 +6,17 @@
   <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
   %globalDTD;
   <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
   %brandDTD;
 
   <!ENTITY viewAllItems               "View All Items">
   <!ENTITY paymentSummaryTitle        "Your Payment">
   <!ENTITY header.payTo               "Pay to">
+  <!ENTITY fieldRequiredSymbol        "*">
 
   <!ENTITY shippingAddressLabel       "Shipping Address">
   <!ENTITY deliveryAddressLabel       "Delivery Address">
   <!ENTITY pickupAddressLabel         "Pickup Address">
   <!ENTITY shippingOptionsLabel       "Shipping Options">
   <!ENTITY paymentMethodsLabel        "Payment Method">
   <!ENTITY address.addLink.label      "Add">
   <!ENTITY address.editLink.label     "Edit">
@@ -28,16 +29,17 @@
   <!ENTITY deliveryAddress.addPage.title  "Add Delivery Address">
   <!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
   <!ENTITY pickupAddress.addPage.title    "Add Pickup Address">
   <!ENTITY pickupAddress.editPage.title   "Edit Pickup Address">
   <!ENTITY billingAddress.addPage.title   "Add Billing Address">
   <!ENTITY billingAddress.editPage.title  "Edit Billing Address">
   <!ENTITY basicCard.addPage.title    "Add Credit Card">
   <!ENTITY basicCard.editPage.title   "Edit Credit Card">
+  <!ENTITY basicCard.cvv.placeholder  "CVV&fieldRequiredSymbol;">
   <!ENTITY payer.addPage.title        "Add Payer Contact">
   <!ENTITY payer.editPage.title       "Edit Payer Contact">
   <!ENTITY payerLabel                 "Contact Information">
   <!ENTITY cancelPaymentButton.label   "Cancel">
   <!ENTITY approvePaymentButton.label  "Pay">
   <!ENTITY processingPaymentButton.label "Processing">
   <!ENTITY successPaymentButton.label    "Done">
   <!ENTITY unknownPaymentButton.label    "Unknown">
@@ -77,16 +79,17 @@
 
   <link rel="stylesheet" href="chrome://global/skin/in-content/common.css"/>
   <link rel="stylesheet" href="paymentRequest.css"/>
   <link rel="stylesheet" href="components/rich-select.css"/>
   <link rel="stylesheet" href="components/address-option.css"/>
   <link rel="stylesheet" href="components/basic-card-option.css"/>
   <link rel="stylesheet" href="components/shipping-option.css"/>
   <link rel="stylesheet" href="components/payment-details-item.css"/>
+  <link rel="stylesheet" href="containers/form.css"/>
   <link rel="stylesheet" href="containers/address-form.css"/>
   <link rel="stylesheet" href="containers/basic-card-form.css"/>
   <link rel="stylesheet" href="containers/order-details.css"/>
   <link rel="stylesheet" href="containers/rich-picker.css"/>
   <link rel="stylesheet" href="containers/error-page.css"/>
 
   <script src="unprivileged-fallbacks.js"></script>
 
@@ -119,16 +122,17 @@
                           selected-state-key="selectedShippingAddress"></address-picker>
 
           <shipping-option-picker class="shipping-related"
                                   label="&shippingOptionsLabel;"></shipping-option-picker>
 
           <payment-method-picker selected-state-key="selectedPaymentCard"
                                  data-add-link-label="&basicCard.addLink.label;"
                                  data-edit-link-label="&basicCard.editLink.label;"
+                                 data-cvv-placeholder="&basicCard.cvv.placeholder;"
                                  label="&paymentMethodsLabel;">
           </payment-method-picker>
           <address-picker class="payer-related"
                           label="&payerLabel;"
                           data-add-link-label="&payer.addLink.label;"
                           data-edit-link-label="&payer.editLink.label;"
                           selected-state-key="selectedPayerAddress"></address-picker>
         </div>
@@ -157,25 +161,27 @@
                        data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
                        data-billing-address-title-add="&billingAddress.addPage.title;"
                        data-billing-address-title-edit="&billingAddress.editPage.title;"
                        data-back-button-label="&basicCardPage.backButton.label;"
                        data-add-button-label="&basicCardPage.addButton.label;"
                        data-update-button-label="&basicCardPage.updateButton.label;"
                        data-cancel-button-label="&cancelPaymentButton.label;"
                        data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
+                       data-field-required-symbol="&fieldRequiredSymbol;"
                        hidden="hidden"></basic-card-form>
 
       <address-form id="address-page"
                     data-error-generic-save="&addressPage.error.genericSave;"
                     data-cancel-button-label="&addressPage.cancelButton.label;"
                     data-back-button-label="&addressPage.backButton.label;"
                     data-add-button-label="&addressPage.addButton.label;"
                     data-update-button-label="&addressPage.updateButton.label;"
                     data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
+                    data-field-required-symbol="&fieldRequiredSymbol;"
                     hidden="hidden"></address-form>
 
       <completion-error-page id="completion-timeout-error" class="illustrated"
                   data-page-title="&timeoutErrorPage.title;"
                   data-suggestion-1="&timeoutErrorPage.suggestion1;"
                   data-suggestion-2="&timeoutErrorPage.suggestion2;"
                   data-suggestion-3="&timeoutErrorPage.suggestion3;"
                   data-done-button-label="&timeoutErrorPage.doneButton.label;"
--- a/browser/components/payments/test/mochitest/test_address_form.html
+++ b/browser/components/payments/test/mochitest/test_address_form.html
@@ -12,16 +12,17 @@ Test the address-form element
   <script src="sinon-2.3.2.js"></script>
   <script src="payments_common.js"></script>
   <script src="../../res/vendor/custom-elements.min.js"></script>
   <script src="../../res/unprivileged-fallbacks.js"></script>
   <script src="autofillEditForms.js"></script>
 
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
+  <link rel="stylesheet" type="text/css" href="../../res/containers/form.css"/>
   <link rel="stylesheet" type="text/css" href="../../res/containers/address-form.css"/>
 </head>
 <body>
   <p id="display">
   </p>
 <div id="content" style="display: none">
 
 </div>
@@ -270,16 +271,17 @@ add_task(async function test_restricted_
   ok(!isHidden(form.form.querySelector("#tel")),
      "tel should be visible");
 
   form.remove();
 });
 
 add_task(async function test_field_validation() {
   let form = new AddressForm();
+  form.dataset.fieldRequiredSymbol = "*";
   await form.promiseReady;
   display.appendChild(form);
   await asyncElementRendered();
 
   let postalCodeInput = form.form.querySelector("#postal-code");
   let addressLevel1Input = form.form.querySelector("#address-level1");
   ok(!postalCodeInput.value, "postal-code should be empty by default");
   ok(!addressLevel1Input.value, "address-level1 should be empty by default");
@@ -293,18 +295,21 @@ add_task(async function test_field_valid
     form.form.querySelector("#address-level2"),
     postalCodeInput,
     addressLevel1Input,
     countrySelect,
   ];
   for (let field of requiredFields) {
     let container = field.closest("label");
     ok(container.hasAttribute("required"), "Container should have required attribute");
-    let label = field.closest("label").querySelector("span");
-    is(getComputedStyle(label, "::after").content, "\"*\"", "Asterisk should be on " + field.id);
+    let span = container.querySelector("span");
+    is(span.getAttribute("fieldRequiredSymbol"), "*",
+       "span should have asterisk as fieldRequiredSymbol");
+    is(getComputedStyle(span, "::after").content, "attr(fieldRequiredSymbol)",
+       "Asterisk should be on " + field.id);
   }
 
   countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "US");
   countrySelect.dispatchEvent(new Event("change"));
 
   sendStringAndCheckValidity(addressLevel1Input, "MI", true);
   sendStringAndCheckValidity(addressLevel1Input, "", false);
   sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false);
--- a/browser/components/payments/test/mochitest/test_basic_card_form.html
+++ b/browser/components/payments/test/mochitest/test_basic_card_form.html
@@ -131,16 +131,48 @@ add_task(async function test_saveButton(
       "cc-number": "4111 1111-1111 1111",
       "billingAddressGUID": "",
       "isTemporary": true,
     },
   }, "Check event details for the message to chrome");
   form.remove();
 });
 
+add_task(async function test_requiredAttributePropagated() {
+  let form = new BasicCardForm();
+  await form.promiseReady;
+  display.appendChild(form);
+  await asyncElementRendered();
+
+  let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
+  ok(requiredElements.length, "There should be at least one required element");
+  for (let element of requiredElements) {
+    let container = element.closest("label") || element.closest("div");
+    ok(container.hasAttribute("required"), "Container should also be marked as required");
+  }
+  // Now test that toggling the `required` attribute will affect the container.
+  let sampleRequiredElement = requiredElements[0];
+  let sampleRequiredContainer = sampleRequiredElement.closest("label") ||
+                                sampleRequiredElement.closest("div");
+  sampleRequiredElement.removeAttribute("required");
+  await form.requestStore.setState({});
+  await asyncElementRendered();
+  ok(!sampleRequiredElement.hasAttribute("required"),
+     `"required" attribute should still be removed from element (${sampleRequiredElement.id})`);
+  ok(!sampleRequiredContainer.hasAttribute("required"),
+     `"required" attribute should be removed from container`);
+  sampleRequiredElement.setAttribute("required", "true");
+  await form.requestStore.setState({});
+  await asyncElementRendered();
+  ok(sampleRequiredContainer.hasAttribute("required"),
+     "`required` attribute is re-added to container");
+
+  form.remove();
+});
+
 add_task(async function test_genericError() {
   let form = new BasicCardForm();
   await form.requestStore.setState({
     page: {
       id: "test-page",
       error: "Generic Error",
     },
   });
@@ -257,16 +289,24 @@ add_task(async function test_edit() {
     savedBasicCards: {
       [card1.guid]: deepClone(card1),
     },
   });
   await asyncElementRendered();
   is(form.saveButton.textContent, "Update", "Check label");
   checkCCForm(form, card1);
 
+  let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
+  ok(requiredElements.length, "There should be at least one required element");
+  for (let element of requiredElements) {
+    let container = element.closest("label") || element.closest("div");
+    ok(element.hasAttribute("required"), "Element should be marked as required");
+    ok(container.hasAttribute("required"), "Container should also be marked as required");
+  }
+
   info("test future year");
   card1["cc-exp-year"] = 2100;
 
   await form.requestStore.setState({
     savedBasicCards: {
       [card1.guid]: deepClone(card1),
     },
   });
--- a/browser/components/shell/HeadlessShell.jsm
+++ b/browser/components/shell/HeadlessShell.jsm
@@ -43,19 +43,19 @@ function loadContentWindow(webNavigation
     progressListeners.set(progressListener, progressListener);
     webProgress.addProgressListener(progressListener,
                                     Ci.nsIWebProgress.NOTIFY_LOCATION);
   });
 }
 
 async function takeScreenshot(fullWidth, fullHeight, contentWidth, contentHeight, path, url) {
   try {
-    let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-    var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-    let contentWindow = await loadContentWindow(webNavigation, url);
+    var windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
+    // nsIWindowlessBrowser inherits from nsIWebNavigation.
+    let contentWindow = await loadContentWindow(windowlessBrowser, url);
     contentWindow.resizeTo(contentWidth, contentHeight);
 
     let canvas = contentWindow.document.createElementNS("http://www.w3.org/1999/xhtml", "html:canvas");
     let context = canvas.getContext("2d");
     let width = fullWidth ? contentWindow.innerWidth + contentWindow.scrollMaxX - contentWindow.scrollMinX
                           : contentWindow.innerWidth;
     let height = fullHeight ? contentWindow.innerHeight + contentWindow.scrollMaxY - contentWindow.scrollMinY
                             : contentWindow.innerHeight;
@@ -77,18 +77,18 @@ async function takeScreenshot(fullWidth,
 
     let blob = await getBlob();
     let reader = await readBlob(blob);
     await OS.File.writeAtomic(path, new Uint8Array(reader.result), {flush: true});
     dump("Screenshot saved to: " + path + "\n");
   } catch (e) {
     dump("Failure taking screenshot: " + e + "\n");
   } finally {
-    if (webNavigation) {
-      webNavigation.close();
+    if (windowlessBrowser) {
+      windowlessBrowser.close();
     }
   }
 }
 
 let HeadlessShell = {
   async handleCmdLineArgs(cmdLine, URLlist) {
     try {
       // Don't quit even though we don't create a window
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -339,32 +339,39 @@ var PocketReader = {
     mm.removeMessageListener("Reader:OnSetup", this);
     mm.removeMessageListener("Reader:Clicked-pocket-button", this);
     this.hidden = true;
   },
   update() {
     if (this.hidden) {
       Services.mm.broadcastAsyncMessage("Reader:RemoveButton", { id: "pocket-button" });
     } else {
-      Services.mm.broadcastAsyncMessage("Reader:AddButton",
-                               { id: "pocket-button",
-                                 title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
-                                 image: "chrome://pocket/content/panels/img/pocket.svg#pocket-mark" });
+      Services.mm.broadcastAsyncMessage("Reader:AddButton", {
+        id: "pocket-button",
+        title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
+        image: "chrome://pocket/content/panels/img/pocket-outline.svg",
+        width: 20,
+        height: 20,
+      });
     }
   },
   receiveMessage(message) {
     switch (message.name) {
       case "Reader:OnSetup": {
         // Tell the reader about our button.
         if (this.hidden)
           break;
         message.target.messageManager.
-          sendAsyncMessage("Reader:AddButton", { id: "pocket-button",
-                                                 title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
-                                                 image: "chrome://pocket/content/panels/img/pocket.svg#pocket-mark"});
+          sendAsyncMessage("Reader:AddButton", {
+            id: "pocket-button",
+            title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
+            image: "chrome://pocket/content/panels/img/pocket-outline.svg",
+            width: 20,
+            height: 20,
+          });
         break;
       }
       case "Reader:Clicked-pocket-button": {
         if (PocketPageAction.pageAction) {
           PocketPageAction.pageAction.doCommand(message.target.ownerGlobal);
         }
         break;
       }
new file mode 100644
--- /dev/null
+++ b/browser/extensions/pocket/content/panels/img/pocket-outline.svg
@@ -0,0 +1,12 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+  This file is the same as skin/shared/pocket-outline.svg and the two should be
+  kept in sync.  The only reason this file exists is that it lives in the
+  contentaccessible content directory so that Firefox's reader mode can use it.
+-->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
+</svg>
deleted file mode 100644
--- a/browser/extensions/pocket/content/panels/img/pocket.svg
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
-  <style>
-    use:not(:target) {
-      display: none;
-    }
-    use {
-      fill: #808080;
-    }
-    use[id$="-added"] {
-      fill: #ee4056;
-    }
-  </style>
-  <defs>
-    <path id="pocket-mark-shape" d="M21.901,4.204C21.642,3.484,20.956,3,20.196,3h-0.01h-1.721H3.814C3.067,3,2.385,3.474,2.119,4.179 C2.04,4.388,2,4.606,2,4.828v6.082l0.069,1.21c0.29,2.751,1.707,5.155,3.899,6.832c0.039,0.03,0.079,0.06,0.119,0.089l0.025,0.018 c1.175,0.866,2.491,1.452,3.91,1.741C10.677,20.932,11.347,21,12.013,21c0.615,0,1.232-0.057,1.839-0.171 c0.073-0.014,0.145-0.028,0.219-0.044c0.02-0.004,0.042-0.012,0.064-0.023c1.359-0.299,2.621-0.87,3.753-1.704l0.025-0.018 c0.04-0.029,0.08-0.059,0.119-0.089c2.192-1.677,3.609-4.08,3.898-6.832L22,10.91V4.828C22,4.618,21.975,4.409,21.901,4.204z M17.667,10.539l-4.704,4.547c-0.266,0.256-0.608,0.385-0.949,0.385c-0.342,0-0.684-0.129-0.949-0.385l-4.705-4.547 c-0.547-0.528-0.565-1.403-0.04-1.954c0.524-0.551,1.392-0.569,1.939-0.041l3.756,3.63l3.755-3.63 c0.547-0.528,1.415-0.51,1.939,0.04C18.231,9.136,18.213,10.011,17.667,10.539z"/>
-  </defs>
-  <use id="pocket-mark" xlink:href="#pocket-mark-shape"/>
-  <use id="pocket-mark-added" xlink:href="#pocket-mark-shape"/>
-</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/pocket/skin/shared/pocket-outline.svg
@@ -0,0 +1,13 @@
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!--
+  This file is the same as content/panels/img/pocket-outline.svg and the two
+  should be kept in sync.  The only reason that file exists is that it lives in
+  the contentaccessible content directory so that Firefox's reader mode can use
+  it.
+-->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
+</svg>
--- a/browser/extensions/pocket/skin/shared/pocket.css
+++ b/browser/extensions/pocket/skin/shared/pocket.css
@@ -2,19 +2,24 @@
   padding-top: 0;
   padding-bottom: 0;
 }
 
 #pageActionActivatedActionPanel[actionID="pocket"] > .panel-arrowcontainer > .panel-arrowbox > .panel-arrow {
   fill: #fbfbfb;
 }
 
+#pocket-button,
+#pageAction-panel-pocket {
+  list-style-image: url("chrome://pocket-shared/skin/pocket-outline.svg");
+}
+
 #appMenu-library-pocket-button,
-#pageAction-panel-pocket,
-#pocket-button {
+#pocket-button-box[open="true"] > #pocket-button,
+#pocket-button-box[pocketed="true"] > #pocket-button {
   list-style-image: url("chrome://pocket-shared/skin/pocket.svg");
 }
 
 #pocket-button-box[animate="true"] > #pocket-button,
 #pocket-button[open="true"][animationsenabled] > .toolbarbutton-icon {
   fill: transparent;
 }
 
--- a/browser/extensions/pocket/skin/shared/pocket.svg
+++ b/browser/extensions/pocket/skin/shared/pocket.svg
@@ -1,6 +1,6 @@
 <!-- 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" width="16" height="16" viewBox="0 0 16 16">
-  <path fill-opacity="context-fill-opacity" fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
+  <path fill-opacity="context-fill-opacity" fill="context-fill" fill-rule="evenodd" d="M14.496.933C15.323.933 16 1.61 16 2.436v4.497c0 4.418-3.581 8-8 8-4.418 0-8-3.582-8-8V2.436C0 1.61.677.933 1.504.933h12.992zM8.013 8.222L4.994 5.319c-.434-.418-1.125-.404-1.543.03-.418.434-.404 1.125.03 1.543l3.776 3.63c.423.406 1.09.406 1.513 0l3.775-3.63c.435-.418.448-1.109.03-1.543-.417-.434-1.108-.448-1.542-.03l-3.02 2.903z"/>
 </svg>
--- a/build.gradle
+++ b/build.gradle
@@ -47,16 +47,17 @@ buildscript {
         // For in tree plugins.
         maven {
             url "file://${gradle.mozconfig.topsrcdir}/mobile/android/gradle/m2repo"
         }
     }
 
     ext.kotlin_version = '1.2.41'
     ext.support_library_version = '26.1.0'
+    ext.jacoco_version = '0.8.1'
 
     if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
         ext.google_play_services_version = '15.0.1'
     }
 
     dependencies {
         classpath 'com.android.tools.build:gradle:3.1.0'
         classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
--- a/build/moz.configure/java.configure
+++ b/build/moz.configure/java.configure
@@ -61,8 +61,15 @@ def javac_version(javac):
                                          stderr=subprocess.STDOUT).rstrip()
         version = Version(output.split(' ')[-1])
         if version < '1.8':
             die('javac 1.8 or higher is required (found %s). '
                 'Check the JAVA_HOME environment variable.' % version)
         return version
     except subprocess.CalledProcessError as e:
         die('Failed to get javac version: %s', e.output)
+
+
+# Java Code Coverage
+# ========================================================
+option('--enable-java-coverage', env='MOZ_JAVA_CODE_COVERAGE', help='Enable Java code coverage')
+
+set_config('MOZ_JAVA_CODE_COVERAGE', depends('--enable-java-coverage')(lambda v: bool(v)))
--- a/build/unix/elfhack/elf.cpp
+++ b/build/unix/elfhack/elf.cpp
@@ -403,16 +403,33 @@ void Elf::normalize()
         std::list<ElfSection *>::iterator it = (*seg)->begin();
         for (ElfSection *last = *(it++); it != (*seg)->end(); last = *(it++)) {
             if (((*it)->getType() != SHT_NOBITS) &&
                 ((*it)->getAddr() - last->getAddr()) != ((*it)->getOffset() - last->getOffset())) {
                     throw std::runtime_error("Segments inconsistency");
             }
         }
     }
+
+    ElfSegment* prevLoad = nullptr;
+    for (auto& it : segments) {
+        if (it->getType() == PT_LOAD) {
+            if (prevLoad) {
+                size_t alignedPrevEnd =
+                    (prevLoad->getAddr() + prevLoad->getMemSize() + prevLoad->getAlign() - 1)
+                    & ~(prevLoad->getAlign() - 1);
+                size_t alignedStart = it->getAddr() & ~(it->getAlign() - 1);
+                if (alignedPrevEnd > alignedStart) {
+                    throw std::runtime_error("Segments overlap");
+                }
+            }
+            prevLoad = it;
+        }
+    }
+
     // fixup ehdr before writing
     if (ehdr->e_phnum != segments.size()) {
         ehdr->e_phnum = segments.size();
         phdr_section->getShdr().sh_size = segments.size() * Elf_Phdr::size(ehdr->e_ident[EI_CLASS]);
         phdr_section->getNext()->markDirty();
     }
     // fixup shdr before writing
     if (ehdr->e_shnum != shdr_section->getSize() / shdr_section->getEntSize())
@@ -548,24 +565,16 @@ unsigned int ElfSection::getOffset()
         if ((getAddr() & mask) < (offset & mask))
             offset = (offset | mask) + (getAddr() & mask) + 1;
         else
             offset = (offset & ~mask) + (getAddr() & mask);
     }
     if ((getType() != SHT_NOBITS) && (offset & (getAddrAlign() - 1)))
         offset = (offset | (getAddrAlign() - 1)) + 1;
 
-    // Two subsequent sections can't be mapped in the same page in memory
-    // if they aren't in the same 4K block on disk.
-    if ((getType() != SHT_NOBITS) && getAddr()) {
-        if (((offset >> 12) != (previous->getOffset() >> 12)) &&
-            ((getAddr() >> 12) == (previous->getAddr() >> 12)))
-            throw std::runtime_error("Moving section would require overlapping segments");
-    }
-
     return (shdr.sh_offset = offset);
 }
 
 int ElfSection::getIndex()
 {
     if (index != -1)
         return index;
     if (getType() == SHT_NULL)
--- a/build/unix/elfhack/elfhack.cpp
+++ b/build/unix/elfhack/elfhack.cpp
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #undef NDEBUG
 #include <assert.h>
 #include <cstring>
 #include <cstdlib>
 #include <cstdio>
 #include "elfxx.h"
+#include "mozilla/CheckedInt.h"
 
 #define ver "0"
 #define elfhack_data ".elfhack.data.v" ver
 #define elfhack_text ".elfhack.text.v" ver
 
 #ifndef R_ARM_V4BX
 #define R_ARM_V4BX 0x28
 #endif
@@ -191,22 +192,32 @@ public:
                         apply_relocations((ElfRel_Section<Elf_Rela> *)rel, *c);
                 }
             }
 
         ElfSection::serialize(file, ei_class, ei_data);
     }
 
     bool isRelocatable() {
-        return true;
+        return false;
     }
 
     unsigned int getEntryPoint() {
         return entry_point;
     }
+
+    void insertBefore(ElfSection *section, bool dirty = true) override {
+        // Adjust the address so that this section is adjacent to the one it's
+        // being inserted before. This avoids creating holes which subsequently
+        // might lead the PHDR-adjusting code to create unnecessary additional
+        // PT_LOADs.
+        shdr.sh_addr = (section->getAddr() - shdr.sh_size) & ~(shdr.sh_addralign - 1);
+        ElfSection::insertBefore(section, dirty);
+    }
+
 private:
     void add_code_section(ElfSection *section)
     {
         if (section) {
             /* Don't add section if it's already been added in the past */
             for (auto s = code.begin(); s != code.end(); ++s) {
                 if (section == *s)
                     return;
@@ -512,16 +523,248 @@ void maybe_split_segment(Elf *elf, ElfSe
             } else {
                 elf->normalize();
             }
             break;
         }
     }
 }
 
+// EH_FRAME constants
+static const char DW_EH_PE_absptr = 0x00;
+static const char DW_EH_PE_omit = 0xff;
+
+// Data size
+static const char DW_EH_PE_LEB128 = 0x01;
+static const char DW_EH_PE_data2 = 0x02;
+static const char DW_EH_PE_data4 = 0x03;
+static const char DW_EH_PE_data8 = 0x04;
+
+// Data signedness
+static const char DW_EH_PE_signed = 0x08;
+
+// Modifiers
+static const char DW_EH_PE_pcrel = 0x10;
+
+
+// Return the data size part of the encoding value
+static char encoding_data_size(char encoding)
+{
+    return encoding & 0x07;
+}
+
+// Advance `step` bytes in the buffer at `data` with size `size`, returning
+// the advanced buffer pointer and remaining size.
+// Returns true if step <= size.
+static bool advance_buffer(char** data, size_t* size, size_t step)
+{
+    if (step > *size)
+        return false;
+
+    *data += step;
+    *size -= step;
+    return true;
+}
+
+// Advance in the given buffer, skipping the full length of the variable-length
+// encoded LEB128 type in CIE/FDE data.
+static bool skip_LEB128(char** data, size_t* size)
+{
+    if (!*size)
+        return false;
+
+    while (*size && (*(*data)++ & (char)0x80)) {
+        (*size)--;
+    }
+    return true;
+}
+
+// Advance in the given buffer, skipping the full length of a pointer encoded
+// with the given encoding.
+static bool skip_eh_frame_pointer(char** data, size_t* size, char encoding)
+{
+    switch (encoding_data_size(encoding)) {
+    case DW_EH_PE_data2:
+        return advance_buffer(data, size, 2);
+    case DW_EH_PE_data4:
+        return advance_buffer(data, size, 4);
+    case DW_EH_PE_data8:
+        return advance_buffer(data, size, 8);
+    case DW_EH_PE_LEB128:
+        return skip_LEB128(data, size);
+    }
+    throw std::runtime_error("unreachable");
+}
+
+// Specialized implementations for adjust_eh_frame_pointer().
+template <typename T>
+static bool adjust_eh_frame_sized_pointer(char** data, size_t* size, ElfSection* eh_frame,
+                                          unsigned int origAddr, Elf* elf)
+{
+    if (*size < sizeof(T))
+        return false;
+
+    serializable<FixedSizeData<T>> pointer(*data, *size, elf->getClass(), elf->getData());
+    mozilla::CheckedInt<T> value = pointer.value;
+    if (origAddr < eh_frame->getAddr()) {
+        unsigned int diff = eh_frame->getAddr() - origAddr;
+        value -= diff;
+    } else {
+        unsigned int diff = origAddr - eh_frame->getAddr();
+        value += diff;
+    }
+    if (!value.isValid())
+        throw std::runtime_error("Overflow while adjusting eh_frame");
+    pointer.value = value.value();
+    pointer.serialize(*data, *size, elf->getClass(), elf->getData());
+    return advance_buffer(data, size, sizeof(T));
+}
+
+// In the given eh_frame section, adjust the pointer with the given encoding, pointed to
+// by the given buffer (`data`, `size`), considering the eh_frame section was originally
+// at `origAddr`.
+// Also advances in the buffer.
+static bool adjust_eh_frame_pointer(char** data, size_t* size, char encoding, ElfSection* eh_frame,
+                                    unsigned int origAddr, Elf* elf)
+{
+    if ((encoding & 0x70) != DW_EH_PE_pcrel)
+        return skip_eh_frame_pointer(data, size, encoding);
+
+    if (encoding & DW_EH_PE_signed) {
+        switch (encoding_data_size(encoding)) {
+        case DW_EH_PE_data2:
+             return adjust_eh_frame_sized_pointer<int16_t>(data, size, eh_frame, origAddr, elf);
+        case DW_EH_PE_data4:
+             return adjust_eh_frame_sized_pointer<int32_t>(data, size, eh_frame, origAddr, elf);
+        case DW_EH_PE_data8:
+             return adjust_eh_frame_sized_pointer<int64_t>(data, size, eh_frame, origAddr, elf);
+        }
+    } else {
+        switch (encoding_data_size(encoding)) {
+        case DW_EH_PE_data2:
+             return adjust_eh_frame_sized_pointer<uint16_t>(data, size, eh_frame, origAddr, elf);
+        case DW_EH_PE_data4:
+             return adjust_eh_frame_sized_pointer<uint32_t>(data, size, eh_frame, origAddr, elf);
+        case DW_EH_PE_data8:
+             return adjust_eh_frame_sized_pointer<uint64_t>(data, size, eh_frame, origAddr, elf);
+        }
+    }
+
+    throw std::runtime_error("Unsupported eh_frame pointer encoding");
+}
+
+// The eh_frame section may contain "PC"-relative pointers. If we move the section,
+// those need to be adjusted. Other type of pointers are relative to sections we
+// don't touch.
+static void adjust_eh_frame(ElfSection* eh_frame, unsigned int origAddr, Elf* elf)
+{
+    if (eh_frame->getAddr() == origAddr) // nothing to do;
+        return;
+
+    char* data = const_cast<char*>(eh_frame->getData());
+    size_t size = eh_frame->getSize();
+    char LSDAencoding = DW_EH_PE_omit;
+    char FDEencoding = DW_EH_PE_absptr;
+    bool hasZ = false;
+
+    // Decoding of eh_frame based on https://www.airs.com/blog/archives/460
+    while (size) {
+        if (size < 2 * sizeof(uint32_t)) goto malformed;
+
+        serializable<FixedSizeData<uint32_t>> entryLength(data, size, elf->getClass(), elf->getData());
+        if (!advance_buffer(&data, &size, sizeof(uint32_t))) goto malformed;
+
+        char* cursor = data;
+        size_t length = entryLength.value;
+
+        serializable<FixedSizeData<uint32_t>> id(data, size, elf->getClass(), elf->getData());
+        if (!advance_buffer(&cursor, &length, sizeof(uint32_t))) goto malformed;
+
+        if (id.value == 0) {
+            // This is a Common Information Entry
+            if (length < 2) goto malformed;
+            // Reset LSDA and FDE encodings, and hasZ for subsequent FDEs.
+            LSDAencoding = DW_EH_PE_omit;
+            FDEencoding = DW_EH_PE_absptr;
+            hasZ = false;
+            // CIE version. Should only be 1 or 3.
+            char version = *cursor++; length--;
+            if (version != 1 && version != 3) {
+                throw std::runtime_error("Unsupported eh_frame version");
+            }
+            // NUL terminated string.
+            const char* augmentationString = cursor;
+            size_t l = strnlen(augmentationString, length - 1);
+            if (l == length - 1) goto malformed;
+            if (!advance_buffer(&cursor, &length, l + 1)) goto malformed;
+            // Skip code alignment factor (LEB128)
+            if (!skip_LEB128(&cursor, &length)) goto malformed;
+            // Skip data alignment factor (LEB128)
+            if (!skip_LEB128(&cursor, &length)) goto malformed;
+            // Skip return address register (single byte in CIE version 1, LEB128
+            // in CIE version 3)
+            if (version == 1) {
+                if (!advance_buffer(&cursor, &length, 1)) goto malformed;
+            } else {
+                if (!skip_LEB128(&cursor, &length)) goto malformed;
+            }
+            // Past this, it's data driven by the contents of the augmentation string.
+            for (size_t i = 0; i < l; i++) {
+                if (!length) goto malformed;
+                switch (augmentationString[i]) {
+                case 'z':
+                    if (!skip_LEB128(&cursor, &length)) goto malformed;
+                    hasZ = true;
+                    break;
+                case 'L':
+                    LSDAencoding = *cursor++;
+                    length--;
+                    break;
+                case 'R':
+                    FDEencoding = *cursor++;
+                    length--;
+                    break;
+                case 'P':
+                    {
+                        char encoding = *cursor++;
+                        length--;
+                        if (!adjust_eh_frame_pointer(&cursor, &length, encoding, eh_frame, origAddr, elf))
+                            goto malformed;
+                    }
+                    break;
+                default:
+                    goto malformed;
+                }
+            }
+        } else {
+            // This is a Frame Description Entry
+            // Starting address
+            if (!adjust_eh_frame_pointer(&cursor, &length, FDEencoding, eh_frame, origAddr, elf))
+                goto malformed;
+
+            if (LSDAencoding != DW_EH_PE_omit) {
+                // Skip number of bytes, same size as the starting address.
+                if (!skip_eh_frame_pointer(&cursor, &length, FDEencoding)) goto malformed;
+                if (hasZ) {
+                    if (!skip_LEB128(&cursor, &length)) goto malformed;
+                }
+                // pointer to the LSDA.
+                if (!adjust_eh_frame_pointer(&cursor, &length, LSDAencoding, eh_frame, origAddr, elf))
+                    goto malformed;
+            }
+        }
+
+        data += entryLength.value; size -= entryLength.value;
+    }
+    return;
+
+malformed:
+    throw std::runtime_error("malformed .eh_frame");
+}
+
 template <typename Rel_Type>
 int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type2, bool force, bool fill)
 {
     ElfDynamic_Section *dyn = elf->getDynSection();
     if (dyn == nullptr) {
         fprintf(stderr, "Couldn't find SHT_DYNAMIC section\n");
         return -1;
     }
@@ -570,17 +813,18 @@ int do_relocation_section(Elf *elf, unsi
     ElfRelHack_Section *relhack = new ElfRelHack_Section(relhack_section);
 
     ElfSymtab_Section *symtab = (ElfSymtab_Section *) section->getLink();
     Elf_SymValue *sym = symtab->lookup("__cxa_pure_virtual");
 
     std::vector<Rel_Type> new_rels;
     Elf_RelHack relhack_entry;
     relhack_entry.r_offset = relhack_entry.r_info = 0;
-    size_t init_array_reloc = 0;
+    std::vector<Rel_Type> init_array_relocs;
+    size_t init_array_insert = 0;
     for (typename std::vector<Rel_Type>::iterator i = section->rels.begin();
          i != section->rels.end(); ++i) {
         // We don't need to keep R_*_NONE relocations
         if (!ELF32_R_TYPE(i->r_info))
             continue;
         ElfLocation loc(i->r_offset, elf);
         // __cxa_pure_virtual is a function used in vtables to point at pure
         // virtual methods. The __cxa_pure_virtual function usually abort()s.
@@ -607,24 +851,21 @@ int do_relocation_section(Elf *elf, unsi
                 // have absolute relocations (rel_type2) for it.
                 if ((ELF32_R_TYPE(i->r_info) == rel_type2) &&
                     (sym == &symtab->syms[ELF32_R_SYM(i->r_info)])) {
                     memset((char *)loc.getBuffer(), 0, entry_sz);
                     continue;
                 }
             }
         }
-        // Keep track of the relocation associated with the first init_array entry.
-        if (init_array && i->r_offset == init_array->getAddr()) {
-            if (init_array_reloc) {
-                fprintf(stderr, "Found multiple relocations for the first init_array entry. Skipping\n");
-                return -1;
-            }
-            new_rels.push_back(*i);
-            init_array_reloc = new_rels.size();
+        // Keep track of the relocations associated with the init_array section.
+        if (init_array && i->r_offset >= init_array->getAddr() &&
+            i->r_offset < init_array->getAddr() + init_array->getSize()) {
+            init_array_relocs.push_back(*i);
+            init_array_insert = new_rels.size();
         } else if (!(loc.getSection()->getFlags() & SHF_WRITE) || (ELF32_R_TYPE(i->r_info) != rel_type)) {
             // Don't pack relocations happening in non writable sections.
             // Our injected code is likely not to be allowed to write there.
             new_rels.push_back(*i);
         } else {
             // With Elf_Rel, the value pointed by the relocation offset is the addend.
             // With Elf_Rela, the addend is in the relocation entry, but the elfhacked
             // relocation info doesn't contain it. Elfhack relies on the value pointed
@@ -651,59 +892,85 @@ int do_relocation_section(Elf *elf, unsi
         }
     }
     if (relhack_entry.r_offset)
         relhack->push_back(relhack_entry);
     // Last entry must be nullptr
     relhack_entry.r_offset = relhack_entry.r_info = 0;
     relhack->push_back(relhack_entry);
 
-    if (init_array && !init_array_reloc) {
+    if (init_array) {
         // Some linkers create a DT_INIT_ARRAY section that, for all purposes,
         // is empty: it only contains 0x0 or 0xffffffff pointers with no relocations.
+        // In some other cases, there can be null pointers with no relocations in
+        // the middle of the section. Example: crtend_so.o in the Android NDK contains
+        // a sized .init_array with a null pointer and no relocation, which ends up
+        // in all Android libraries, and in some cases it ends up in the middle of
+        // the final .init_array section.
+        // If we have such a reusable slot at the beginning of .init_array, we just
+        // use it. It we have one in the middle of .init_array, we slide its content
+        // to move the "hole" at the beginning and use it there (we need our injected
+        // code to run before any other).
+        // Otherwise, replace the first entry and keep the original pointer.
+        std::sort(init_array_relocs.begin(), init_array_relocs.end(),
+                  [](Rel_Type& a, Rel_Type& b) { return a.r_offset < b.r_offset; });
+        size_t expected = init_array->getAddr();
         const size_t zero = 0;
         const size_t all = SIZE_MAX;
         const char *data = init_array->getData();
         size_t length = Elf_Addr::size(elf->getClass());
-        bool empty = true;
-        for (size_t off = 0; off < init_array->getSize(); off += length) {
-            if (memcmp(data + off, &zero, length) &&
-                memcmp(data + off, &all, length)) {
-                empty = false;
+	size_t off = 0;
+	for (; off < init_array_relocs.size(); off++) {
+            auto& r = init_array_relocs[off];
+            if (r.r_offset >= expected + length &&
+                (memcmp(data + off * length, &zero, length) == 0 ||
+                 memcmp(data + off * length, &all, length) == 0)) {
+                // We found a hole, move the preceding entries.
+                while (off) {
+                    auto& p = init_array_relocs[--off];
+                    if (ELF32_R_TYPE(p.r_info) == rel_type) {
+                        unsigned int addend = get_addend(&p, elf);
+                        p.r_offset += length;
+                        set_relative_reloc(&p, elf, addend);
+                    } else {
+                        fprintf(stderr, "Unsupported relocation type in DT_INIT_ARRAY. Skipping\n");
+                        return -1;
+                    }
+                }
                 break;
             }
+            expected = r.r_offset + length;
+        }
+
+        if (off == 0) {
+            // We either found a hole above, and can now use the first entry,
+            // or the init_array section is effectively empty (see further above)
+            // and we also can use the first entry.
+            // Either way, code further below will take care of actually setting
+            // the right r_info and r_added for the relocation.
+            Rel_Type rel;
+            rel.r_offset = init_array->getAddr();
+            init_array_relocs.insert(init_array_relocs.begin(), rel);
+        } else {
+            // Use relocated value of DT_INIT_ARRAY's first entry for the
+            // function to be called by the injected code.
+            auto& rel = init_array_relocs[0];
+            unsigned int addend = get_addend(&rel, elf);
+            if (ELF32_R_TYPE(rel.r_info) == rel_type) {
+                original_init = addend;
+            } else if (ELF32_R_TYPE(rel.r_info) == rel_type2) {
+                ElfSymtab_Section *symtab = (ElfSymtab_Section *)section->getLink();
+                original_init = symtab->syms[ELF32_R_SYM(rel.r_info)].value.getValue() + addend;
+            } else {
+                fprintf(stderr, "Unsupported relocation type for DT_INIT_ARRAY's first entry. Skipping\n");
+                return -1;
+            }
         }
-	// If we encounter such an empty DT_INIT_ARRAY section, we add a
-	// relocation for its first entry to point to our init. Code further
-	// below will take care of actually setting the right r_info and
-	// r_addend for the relocation, as if we had a normal DT_INIT_ARRAY
-	// section.
-        if (empty) {
-            new_rels.emplace_back();
-            init_array_reloc = new_rels.size();
-            Rel_Type *rel = &new_rels[init_array_reloc - 1];
-            rel->r_offset = init_array->getAddr();
-        } else {
-            fprintf(stderr, "Didn't find relocation for DT_INIT_ARRAY's first entry. Skipping\n");
-            return -1;
-        }
-    } else if (init_array) {
-        Rel_Type *rel = &new_rels[init_array_reloc - 1];
-        unsigned int addend = get_addend(rel, elf);
-        // Use relocated value of DT_INIT_ARRAY's first entry for the
-        // function to be called by the injected code.
-        if (ELF32_R_TYPE(rel->r_info) == rel_type) {
-            original_init = addend;
-        } else if (ELF32_R_TYPE(rel->r_info) == rel_type2) {
-            ElfSymtab_Section *symtab = (ElfSymtab_Section *)section->getLink();
-            original_init = symtab->syms[ELF32_R_SYM(rel->r_info)].value.getValue() + addend;
-        } else {
-            fprintf(stderr, "Unsupported relocation type for DT_INIT_ARRAY's first entry. Skipping\n");
-            return -1;
-        }
+
+        new_rels.insert(std::next(new_rels.begin(), init_array_insert), init_array_relocs.begin(), init_array_relocs.end());
     }
 
     unsigned int mprotect_cb = 0;
     unsigned int sysconf_cb = 0;
     // If there is a relro segment, our injected code will run after the linker sets the
     // corresponding pages read-only. We need to make our code change that to read-write
     // before applying relocations, which means it needs to call mprotect.
     // To do that, we need to find a reference to the mprotect symbol. In case the library
@@ -774,16 +1041,18 @@ int do_relocation_section(Elf *elf, unsi
         }
 
         if (mprotect_cb == 0 || sysconf_cb == 0) {
             fprintf(stderr, "Couldn't find .bss. Skipping\n");
             return -1;
         }
     }
 
+    size_t old_size = section->getSize();
+
     section->rels.assign(new_rels.begin(), new_rels.end());
     section->shrink(new_rels.size() * section->getEntSize());
 
     ElfRelHackCode_Section *relhackcode = new ElfRelHackCode_Section(
         relhackcode_section, *elf, *relhack, original_init, mprotect_cb, sysconf_cb);
     // Find the first executable section, and insert the relhack code before
     // that. The relhack data is inserted between .rel.dyn and .rel.plt.
     ElfSection *first_executable = nullptr;
@@ -795,46 +1064,74 @@ int do_relocation_section(Elf *elf, unsi
         }
     }
 
     if (!first_executable) {
         fprintf(stderr, "Couldn't find executable section. Skipping\n");
         return -1;
     }
 
-    unsigned int old_exec = first_executable->getOffset();
-
     relhack->insertBefore(section);
     relhackcode->insertBefore(first_executable);
 
-    // Trying to get first_executable->getOffset() now may throw if the new
-    // layout would require it to move, so we look at the end of the relhack
-    // code section instead, comparing it to where the first executable
-    // section used to start.
-    if (relhackcode->getOffset() + relhackcode->getSize() >= old_exec) {
+    // Don't try further if we can't gain from the relocation section size change.
+    size_t align = first_executable->getSegmentByType(PT_LOAD)->getAlign();
+    size_t new_size = relhack->getSize() + relhackcode->getSize();
+    if (!force && (new_size >= old_size || old_size - new_size < align)) {
         fprintf(stderr, "No gain. Skipping\n");
         return -1;
     }
 
+    // .eh_frame/.eh_frame_hdr may be between the relocation sections and the
+    // executable sections. When that happens, we may end up creating a separate
+    // PT_LOAD for just both of them because they are not considered relocatable.
+    // But they are, in fact, kind of relocatable, albeit with some manual work.
+    // Which we'll do here.
+    ElfSegment* eh_frame_segment = elf->getSegmentByType(PT_GNU_EH_FRAME);
+    ElfSection* eh_frame_hdr = eh_frame_segment ? eh_frame_segment->getFirstSection() : nullptr;
+    // The .eh_frame section usually follows the eh_frame_hdr section.
+    ElfSection* eh_frame = eh_frame_hdr ? eh_frame_hdr->getNext() : nullptr;
+    if (eh_frame_hdr && !eh_frame) {
+        throw std::runtime_error("Expected to find an .eh_frame section after .eh_frame_hdr");
+    }
+    if (eh_frame && strcmp(eh_frame->getName(), ".eh_frame") == 0) {
+        // The distance between both sections needs to be preserved because eh_frame_hdr
+        // contains relative offsets to eh_frame. Well, they could be relocated too, but
+        // it's not worth the effort for the few number of bytes this would save.
+        size_t distance = eh_frame->getAddr() - eh_frame_hdr->getAddr();
+        ElfSection* previous = eh_frame_hdr->getPrevious();
+        eh_frame_hdr->getShdr().sh_addr =
+            (previous->getAddr() + previous->getSize() + eh_frame_hdr->getAddrAlign() - 1)
+            & ~(eh_frame_hdr->getAddrAlign() - 1);
+        unsigned int origAddr = eh_frame->getAddr();
+        eh_frame->getShdr().sh_addr =
+            (eh_frame_hdr->getAddr() + eh_frame_hdr->getSize() + eh_frame->getAddrAlign() - 1)
+            & ~(eh_frame->getAddrAlign() - 1);
+        // Re-adjust the eh_frame_hdr address to keep the original distance.
+        eh_frame_hdr->getShdr().sh_addr = eh_frame->getAddr() - distance;
+        eh_frame_hdr->markDirty();
+        adjust_eh_frame(eh_frame, origAddr, elf);
+    }
+
     // Adjust PT_LOAD segments
     for (ElfSegment *segment = elf->getSegmentByType(PT_LOAD); segment;
          segment = elf->getSegmentByType(PT_LOAD, segment)) {
         maybe_split_segment(elf, segment, fill);
     }
 
     // Ensure Elf sections will be at their final location.
     elf->normalize();
     ElfLocation *init = new ElfLocation(relhackcode, relhackcode->getEntryPoint());
     if (init_array) {
         // Adjust the first DT_INIT_ARRAY entry to point at the injected code
         // by transforming its relocation into a relative one pointing to the
         // address of the injected code.
-        Rel_Type *rel = &section->rels[init_array_reloc - 1];
+        Rel_Type *rel = &section->rels[init_array_insert];
         rel->r_info = ELF32_R_INFO(0, rel_type); // Set as a relative relocation
-        set_relative_reloc(&section->rels[init_array_reloc - 1], elf, init->getValue());
+        set_relative_reloc(rel, elf, init->getValue());
     } else if (!dyn->setValueForType(DT_INIT, init)) {
         fprintf(stderr, "Can't grow .dynamic section to set DT_INIT. Skipping\n");
         return -1;
     }
     // TODO: adjust the value according to the remaining number of relative relocations
     if (dyn->getValueForType(Rel_Type::d_tag_count))
         dyn->setValueForType(Rel_Type::d_tag_count, new ElfPlainValue(0));
 
--- a/build/unix/elfhack/elfxx.h
+++ b/build/unix/elfhack/elfxx.h
@@ -44,16 +44,31 @@ class ElfSection;
 class ElfSegment;
 // TODO: Rename Elf_* types
 class Elf_Ehdr;
 class Elf_Phdr;
 class Elf;
 class ElfDynamic_Section;
 class ElfStrtab_Section;
 
+template <typename X>
+class FixedSizeData {
+public:
+    struct Wrapper {
+        X value;
+    };
+    typedef Wrapper Type32;
+    typedef Wrapper Type64;
+
+    template <class endian, typename R, typename T>
+    static void swap(T &t, R &r) {
+        r.value = endian::swap(t.value);
+    }
+};
+
 class Elf_Ehdr_Traits {
 public:
     typedef Elf32_Ehdr Type32;
     typedef Elf64_Ehdr Type64;
 
     template <class endian, typename R, typename T>
     static void swap(T &t, R &r);
 };
@@ -378,17 +393,17 @@ public:
             next = nullptr;
         if (next != nullptr)
             next->previous = this;
         if (dirty)
             markDirty();
         insertInSegments(section->segments);
     }
 
-    void insertBefore(ElfSection *section, bool dirty = true) {
+    virtual void insertBefore(ElfSection *section, bool dirty = true) {
         if (previous != nullptr)
             previous->next = next;
         if (next != nullptr)
             next->previous = previous;
         next = section;
         if (section != nullptr) {
             previous = section->previous;
             section->previous = this;
--- a/build/unix/elfhack/moz.build
+++ b/build/unix/elfhack/moz.build
@@ -11,16 +11,17 @@ if not CONFIG['CROSS_COMPILE']:
     SOURCES += [
         'dummy.c',
         'test-array.c',
         'test-ctors.c',
     ]
 
     for f in CONFIG['OS_CFLAGS']:
         if f.startswith('-flto'):
+            SOURCES['dummy.c'].flags += ['-fno-lto']
             SOURCES['test-array.c'].flags += ['-fno-lto']
             SOURCES['test-ctors.c'].flags += ['-fno-lto']
 
 HOST_SOURCES += [
     'elf.cpp',
     'elfhack.cpp',
 ]
 
--- a/build/unix/mozconfig.lto
+++ b/build/unix/mozconfig.lto
@@ -11,14 +11,11 @@ export CC="$topsrcdir/clang/bin/clang"
 export CXX="$topsrcdir/clang/bin/clang++"
 
 # Use a newer binutils, from the tooltool gcc package, if it's there
 if [ -e "$topsrcdir/gcc/bin/ld" ]; then
     export CC="$CC -B $topsrcdir/gcc/bin"
     export CXX="$CXX -B $topsrcdir/gcc/bin"
 fi
 
-# Until Bug 1423822 is resolved
-ac_add_options --disable-elf-hack
-
 ac_add_options --enable-lto
 
 . "$topsrcdir/build/unix/mozconfig.stdcxx"
--- a/devtools/client/webconsole/test/mochitest/browser.ini
+++ b/devtools/client/webconsole/test/mochitest/browser.ini
@@ -265,16 +265,17 @@ skip-if = (os == 'linux' && bits == 32 &
 [browser_webconsole_context_menu_copy_link_location.js]
 subsuite = clipboard
 skip-if = (os == 'linux' && bits == 32 && debug) || (os == 'linux') # bug 1328915, disable linux32 debug devtools for timeouts, bug 1473120
 [browser_webconsole_context_menu_copy_object.js]
 subsuite = clipboard
 [browser_webconsole_context_menu_object_in_sidebar.js]
 [browser_webconsole_context_menu_open_url.js]
 [browser_webconsole_context_menu_store_as_global.js]
+[browser_webconsole_cors_errors.js]
 [browser_webconsole_csp_ignore_reflected_xss_message.js]
 [browser_webconsole_csp_violation.js]
 [browser_webconsole_cspro.js]
 [browser_webconsole_document_focus.js]
 [browser_webconsole_duplicate_errors.js]
 [browser_webconsole_error_with_longstring_stack.js]
 [browser_webconsole_error_with_unicode.js]
 [browser_webconsole_errors_after_page_reload.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/mochitest/browser_webconsole_cors_errors.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Ensure that the different CORS error are logged to the console with the appropriate
+// "Learn more" link.
+
+"use strict";
+
+const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html";
+const BASE_CORS_ERROR_URL = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
+const BASE_CORS_ERROR_URL_PARAMS = new URLSearchParams({
+  utm_source: "devtools",
+  utm_medium: "firefox-cors-errors",
+  utm_campaign: "default",
+});
+
+add_task(async function() {
+  await pushPref("devtools.webconsole.filter.netxhr", true);
+
+  const hud = await openNewTabAndConsole(TEST_URI);
+
+  let onCorsMessage;
+  let message;
+
+  info(`Setting "content.cors.disable" to true to test CORSDisabled message`);
+  await pushPref("content.cors.disable", true);
+  onCorsMessage = waitForMessage(hud, "Reason: CORS disabled");
+  makeFaultyCorsCall("CORSDisabled");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSDisabled");
+  await pushPref("content.cors.disable", false);
+
+  info("Test CORSPreflightDidNotSucceed");
+  onCorsMessage = waitForMessage(hud, `CORS preflight channel did not succeed`);
+  makeFaultyCorsCall("CORSPreflightDidNotSucceed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSPreflightDidNotSucceed");
+
+  info("Test CORS did not succeed");
+  onCorsMessage = waitForMessage(hud, "Reason: CORS request did not succeed");
+  makeFaultyCorsCall("CORSDidNotSucceed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSDidNotSucceed");
+
+  info("Test CORSExternalRedirectNotAllowed");
+  onCorsMessage = waitForMessage(hud,
+    "Reason: CORS request external redirect not allowed");
+  makeFaultyCorsCall("CORSExternalRedirectNotAllowed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSExternalRedirectNotAllowed");
+
+  info("Test CORSMissingAllowOrigin");
+  onCorsMessage = waitForMessage(hud,
+    `Reason: CORS header ${quote("Access-Control-Allow-Origin")} missing`);
+  makeFaultyCorsCall("CORSMissingAllowOrigin");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowOrigin");
+
+  info("Test CORSMultipleAllowOriginNotAllowed");
+  onCorsMessage = waitForMessage(hud,
+    `Reason: Multiple CORS header ${quote("Access-Control-Allow-Origin")} not allowed`);
+  makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMultipleAllowOriginNotAllowed");
+
+  info("Test CORSAllowOriginNotMatchingOrigin");
+  onCorsMessage = waitForMessage(hud, `Reason: CORS header ` +
+    `${quote("Access-Control-Allow-Origin")} does not match ${quote("mochi.test")}`);
+  makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSAllowOriginNotMatchingOrigin");
+
+  info("Test CORSNotSupportingCredentials");
+  onCorsMessage = waitForMessage(hud, `Reason: Credential is not supported if the CORS ` +
+    `header ${quote("Access-Control-Allow-Origin")} is ${quote("*")}`);
+  makeFaultyCorsCall("CORSNotSupportingCredentials");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSNotSupportingCredentials");
+
+  info("Test CORSMethodNotFound");
+  onCorsMessage = waitForMessage(hud, `Reason: Did not find method in CORS header ` +
+    `${quote("Access-Control-Allow-Methods")}`);
+  makeFaultyCorsCall("CORSMethodNotFound");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMethodNotFound");
+
+  info("Test CORSMissingAllowCredentials");
+  onCorsMessage = waitForMessage(hud, `Reason: expected ${quote("true")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Credentials")}`);
+  makeFaultyCorsCall("CORSMissingAllowCredentials");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowCredentials");
+
+  info("Test CORSInvalidAllowMethod");
+  onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Methods")}`);
+  makeFaultyCorsCall("CORSInvalidAllowMethod");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSInvalidAllowMethod");
+
+  info("Test CORSInvalidAllowHeader");
+  onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Headers")}`);
+  makeFaultyCorsCall("CORSInvalidAllowHeader");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSInvalidAllowHeader");
+
+  info("Test CORSMissingAllowHeaderFromPreflight");
+  onCorsMessage = waitForMessage(hud, `Reason: missing token ${quote("xyz")} in CORS ` +
+    `header ${quote("Access-Control-Allow-Headers")} from CORS preflight channel`);
+  makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight");
+  message = await onCorsMessage;
+  await checkCorsMessage(message, "CORSMissingAllowHeaderFromPreflight");
+
+  // See Bug 1480671.
+  // XXX: how to make Origin to not be included in the request ?
+  // onCorsMessage = waitForMessage(hud,
+  //   `Reason: CORS header ${quote("Origin")} cannot be added`);
+  // makeFaultyCorsCall("CORSOriginHeaderNotAdded");
+  // message = await onCorsMessage;
+  // await checkCorsMessage(message, "CORSOriginHeaderNotAdded");
+
+  // See Bug 1480672.
+  // XXX: Failing with another error: Console message: Security Error: Content at
+  // http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html
+  // may not load or link to file:///Users/nchevobbe/Projects/mozilla-central/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs.
+  // info("Test CORSRequestNotHttp");
+  // onCorsMessage = waitForMessage(hud, "Reason: CORS request not http");
+  // const dir = getChromeDir(getResolvedURI(gTestPath));
+  // dir.append("sjs_cors-test-server.sjs");
+  // makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec);
+  // message = await onCorsMessage;
+  // await checkCorsMessage(message, "CORSRequestNotHttp");
+});
+
+async function checkCorsMessage(message, category) {
+  const node = message.node;
+  ok(node.classList.contains("warn"), "The cors message has the expected classname");
+  const learnMoreLink = node.querySelector(".learn-more-link");
+  ok(learnMoreLink, "There is a Learn more link displayed");
+  const linkSimulation = await simulateLinkClick(learnMoreLink);
+  is(linkSimulation.link, getCategoryUrl(category),
+    "Click on the link opens the expected page");
+}
+
+function makeFaultyCorsCall(errorCategory, corsUrl) {
+  ContentTask.spawn(gBrowser.selectedBrowser, [errorCategory, corsUrl],
+    ([category, url]) => {
+      if (!url) {
+        const baseUrl =
+          "http://mochi.test:8888/browser/devtools/client/webconsole/test/mochitest";
+        url = `${baseUrl}/sjs_cors-test-server.sjs?corsErrorCategory=${category}`;
+      }
+
+      // Preflight request are not made for GET requests, so let's do a PUT.
+      const method = "PUT";
+      const options = { method };
+      if (category === "CORSNotSupportingCredentials"
+        || category === "CORSMissingAllowCredentials"
+      ) {
+        options.credentials = "include";
+      }
+
+      if (category === "CORSMissingAllowHeaderFromPreflight") {
+        options.headers = new content.Headers({"xyz": true});
+      }
+
+      content.fetch(url, options);
+    });
+}
+
+function quote(str) {
+  const openingQuote = String.fromCharCode(8216);
+  const closingQuote = String.fromCharCode(8217);
+  return `${openingQuote}${str}${closingQuote}`;
+}
+
+function getCategoryUrl(category) {
+  return `${BASE_CORS_ERROR_URL}${category}?${BASE_CORS_ERROR_URL_PARAMS}`;
+}
--- a/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs
+++ b/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs
@@ -1,17 +1,150 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 function handleRequest(request, response) {
-  response.setStatusLine(request.httpVersion, 200, "Och Aye");
+  const params = new Map(
+    request.queryString
+      .replace("?", "")
+      .split("&")
+      .map(s => s.split("="))
+  );
+
+  if (!params.has("corsErrorCategory")) {
+    response.setStatusLine(request.httpVersion, 200, "Och Aye");
+    setCacheHeaders(response);
+    response.setHeader("Access-Control-Allow-Origin", "*", false);
+    response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+    response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+    response.write("Access-Control-Allow-Origin: *");
+    return;
+  }
+
+  const category = params.get("corsErrorCategory");
+  switch (category) {
+    case "CORSDidNotSucceed":
+      corsDidNotSucceed(request, response);
+      break;
+    case "CORSExternalRedirectNotAllowed":
+      corsExternalRedirectNotAllowed(request, response);
+      break;
+    case "CORSMissingAllowOrigin":
+      corsMissingAllowOrigin(request, response);
+      break;
+    case "CORSMultipleAllowOriginNotAllowed":
+      corsMultipleOriginNotAllowed(request, response);
+      break;
+    case "CORSAllowOriginNotMatchingOrigin":
+      corsAllowOriginNotMatchingOrigin(request, response);
+      break;
+    case "CORSNotSupportingCredentials":
+      corsNotSupportingCredentials(request, response);
+      break;
+    case "CORSMethodNotFound":
+      corsMethodNotFound(request, response);
+      break;
+    case "CORSMissingAllowCredentials":
+      corsMissingAllowCredentials(request, response);
+      break;
+    case "CORSPreflightDidNotSucceed":
+      corsPreflightDidNotSucceed(request, response);
+      break;
+    case "CORSInvalidAllowMethod":
+      corsInvalidAllowMethod(request, response);
+      break;
+    case "CORSInvalidAllowHeader":
+      corsInvalidAllowHeader(request, response);
+      break;
+    case "CORSMissingAllowHeaderFromPreflight":
+      corsMissingAllowHeaderFromPreflight(request, response);
+      break;
+  }
+}
+
+function corsDidNotSucceed(request, response) {
+  setCacheHeaders(response);
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Location", "http://example.com");
+}
 
+function corsExternalRedirectNotAllowed(request, response) {
+  response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+  response.setHeader("Access-Control-Allow-Origin", "*", false);
+  response.setHeader("Access-Control-Allow-Headers", "content-type", false);
+  response.setHeader("Location", "http://redirect.test/");
+}
+
+function corsMissingAllowOrigin(request, response) {
+  setCacheHeaders(response);
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowOrigin");
+}
+
+function corsMultipleOriginNotAllowed(request, response) {
+  // We can't set the same header twice with response.setHeader, so we need to seizePower
+  // and write the response manually.
+  response.seizePower();
+  response.write("HTTP/1.0 200 OK\r\n");
+  response.write("Content-Type: text/plain\r\n");
+  response.write("Access-Control-Allow-Origin: *\r\n");
+  response.write("Access-Control-Allow-Origin: mochi.test\r\n");
+  response.write("\r\n");
+  response.finish();
+  setCacheHeaders(response);
+}
+
+function corsAllowOriginNotMatchingOrigin(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsAllowOriginNotMatchingOrigin");
+  response.setHeader("Access-Control-Allow-Origin", "mochi.test");
+}
+
+function corsNotSupportingCredentials(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsNotSupportingCredentials");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+}
+
+function corsMethodNotFound(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMethodNotFound");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  // Will make the request fail since it is a "PUT".
+  response.setHeader("Access-Control-Allow-Methods", "POST");
+}
+
+function corsMissingAllowCredentials(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowCredentials");
+  // Need to set an explicit origin (i.e. not "*") to make the request fail.
+  response.setHeader("Access-Control-Allow-Origin", "http://example.com");
+}
+
+function corsPreflightDidNotSucceed(request, response) {
+  const isPreflight = request.method == "OPTIONS";
+  if (isPreflight) {
+    response.setStatusLine(request.httpVersion, 500, "Preflight fail");
+    response.setHeader("Access-Control-Allow-Origin", "*");
+  }
+}
+
+function corsInvalidAllowMethod(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowMethod");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "xyz;");
+}
+
+function corsInvalidAllowHeader(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowHeader");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "PUT");
+  response.setHeader("Access-Control-Allow-Headers", "xyz;");
+}
+
+function corsMissingAllowHeaderFromPreflight(request, response) {
+  response.setStatusLine(request.httpVersion, 200, "corsMissingAllowHeaderFromPreflight");
+  response.setHeader("Access-Control-Allow-Origin", "*");
+  response.setHeader("Access-Control-Allow-Methods", "PUT");
+}
+
+function setCacheHeaders(response) {
   response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
   response.setHeader("Pragma", "no-cache");
   response.setHeader("Expires", "0");
-
-  response.setHeader("Access-Control-Allow-Origin", "*", false);
-  response.setHeader("Access-Control-Allow-Headers", "content-type", false);
-
-  response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
-
-  response.write("Access-Control-Allow-Origin: *");
 }
--- a/devtools/server/actors/errordocs.js
+++ b/devtools/server/actors/errordocs.js
@@ -4,19 +4,20 @@
 
 /**
  * A mapping of error message names to external documentation. Any error message
  * included here will be displayed alongside its link in the web console.
  */
 
 "use strict";
 
-const baseURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
+const baseErrorURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
 const params =
   "?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default";
+
 const ErrorDocs = {
   JSMSG_READ_ONLY: "Read-only",
   JSMSG_BAD_ARRAY_LENGTH: "Invalid_array_length",
   JSMSG_NEGATIVE_REPETITION_COUNT: "Negative_repetition_count",
   JSMSG_RESULTING_STRING_TOO_LARGE: "Resulting_string_too_large",
   JSMSG_BAD_RADIX: "Bad_radix",
   JSMSG_PRECISION_RANGE: "Precision_range",
   JSMSG_STMT_AFTER_RETURN: "Stmt_after_return",
@@ -103,24 +104,50 @@ const ErrorCategories = {
   "Invalid HPKP Headers": PUBLIC_KEY_PINS_LEARN_MORE,
   "Invalid HSTS Headers": STRICT_TRANSPORT_SECURITY_LEARN_MORE,
   "SHA-1 Signature": WEAK_SIGNATURE_ALGORITHM_LEARN_MORE,
   "Tracking Protection": TRACKING_PROTECTION_LEARN_MORE,
   "MIMEMISMATCH": MIME_TYPE_MISMATCH_LEARN_MORE,
   "source map": SOURCE_MAP_LEARN_MORE,
 };
 
+const baseCorsErrorUrl = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
+const corsParams =
+  "?utm_source=devtools&utm_medium=firefox-cors-errors&utm_campaign=default";
+const CorsErrorDocs = {
+  CORSDisabled: "CORSDisabled",
+  CORSDidNotSucceed: "CORSDidNotSucceed",
+  CORSOriginHeaderNotAdded: "CORSOriginHeaderNotAdded",
+  CORSExternalRedirectNotAllowed: "CORSExternalRedirectNotAllowed",
+  CORSRequestNotHttp: "CORSRequestNotHttp",
+  CORSMissingAllowOrigin: "CORSMissingAllowOrigin",
+  CORSMultipleAllowOriginNotAllowed: "CORSMultipleAllowOriginNotAllowed",
+  CORSAllowOriginNotMatchingOrigin: "CORSAllowOriginNotMatchingOrigin",
+  CORSNotSupportingCredentials: "CORSNotSupportingCredentials",
+  CORSMethodNotFound: "CORSMethodNotFound",
+  CORSMissingAllowCredentials: "CORSMissingAllowCredentials",
+  CORSPreflightDidNotSucceed: "CORSPreflightDidNotSucceed",
+  CORSInvalidAllowMethod: "CORSInvalidAllowMethod",
+  CORSInvalidAllowHeader: "CORSInvalidAllowHeader",
+  CORSMissingAllowHeaderFromPreflight: "CORSMissingAllowHeaderFromPreflight",
+};
+
 exports.GetURL = (error) => {
   if (!error) {
     return undefined;
   }
 
   const doc = ErrorDocs[error.errorMessageName];
   if (doc) {
-    return baseURL + doc + params;
+    return baseErrorURL + doc + params;
+  }
+
+  const corsDoc = CorsErrorDocs[error.category];
+  if (corsDoc) {
+    return baseCorsErrorUrl + corsDoc + corsParams;
   }
 
   const categoryURL = ErrorCategories[error.category];
   if (categoryURL) {
     return categoryURL + params;
   }
   return undefined;
 };
--- a/devtools/shared/tests/unit/test_console_filtering.js
+++ b/devtools/shared/tests/unit/test_console_filtering.js
@@ -49,18 +49,17 @@ function createFakeAddonWindow({addonId}
     localizeCallback() {},
   });
   policy.active = true;
 
   const baseURI = Services.io.newURI(`moz-extension://${uuid}/`);
   const principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
   const chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-  const docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                             .getInterface(Ci.nsIDocShell);
+  const { docShell } = chromeWebNav;
   docShell.createAboutBlankContentViewer(principal);
   const addonWindow = docShell.contentViewer.DOMDocument.defaultView;
 
   return {addonWindow, chromeWebNav};
 }
 
 /**
  * Tests that the consoleID property of the ConsoleAPI options gets passed
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -614,21 +614,16 @@ nsDocShell::GetInterface(const nsIID& aI
     nsresult rv = EnsureFind();
     if (NS_FAILED(rv)) {
       return rv;
     }
 
     *aSink = mFind;
     NS_ADDREF((nsISupports*)*aSink);
     return NS_OK;
-  } else if (aIID.Equals(NS_GET_IID(nsIEditingSession))) {
-    nsCOMPtr<nsIEditingSession> es;
-    GetEditingSession(getter_AddRefs(es));
-    es.forget(aSink);
-    return *aSink ? NS_OK : NS_NOINTERFACE;
   } else if (aIID.Equals(NS_GET_IID(nsISelectionDisplay))) {
     nsIPresShell* shell = GetPresShell();
     if (shell) {
       return shell->QueryInterface(aIID, aSink);
     }
   } else if (aIID.Equals(NS_GET_IID(nsIDocShellTreeOwner))) {
     nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
     nsresult rv = GetTreeOwner(getter_AddRefs(treeOwner));
--- a/docshell/test/chrome/test_bug565388.xul
+++ b/docshell/test/chrome/test_bug565388.xul
@@ -46,24 +46,22 @@ function test() {
     QueryInterface: function(iid) {
       if (iid.equals(Ci.nsIWebProgressListener) ||
           iid.equals(Ci.nsISupportsWeakReference))
         return this;
       throw Cr.NS_ERROR_NO_INTERFACE;
     }
   }
 
-  var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].
-  createInstance(Ci.nsIPrincipal);
-  var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
-  getService(Ci.nsIAppShellService).
-  createWindowlessBrowser(true);
-  var docShell = webNav.
-  QueryInterface(Ci.nsIInterfaceRequestor).
-  getInterface(Ci.nsIDocShell);
+  var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
+                          .getService(Ci.nsIPrincipal);
+  var webNav = Cc["@mozilla.org/appshell/appShellService;1"]
+                 .getService(Ci.nsIAppShellService)
+                 .createWindowlessBrowser(true);
+  var docShell = webNav.docShell;
   docShell.createAboutBlankContentViewer(systemPrincipal);
   var win = docShell.contentViewer.DOMDocument.defaultView;
 
   progressListener.add(docShell, function(){
     is(win.document.documentURI, "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window/>");
     webNav.close();
     SimpleTest.finish();
   });
--- a/docshell/test/chrome/test_bug846906.xul
+++ b/docshell/test/chrome/test_bug846906.xul
@@ -28,17 +28,17 @@ https://bugzilla.mozilla.org/show_bug.cg
      "Windowless browser should implement nsIWindowlessBrowser");
 
   var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
   ok(webNavigation, "Windowless browser should implement nsIWebNavigation");
 
   var interfaceRequestor = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor);
   ok(interfaceRequestor, "Should be able to query interface requestor interface");
 
-  var docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  var docShell = windowlessBrowser.docShell;
   ok(docShell, "Should be able to get doc shell interface");
 
   var document = webNavigation.document;
   ok(document, "Should be able to get document");
 
   var iframe = document.createElement("iframe");
   ok(iframe, "Should be able to create iframe");
 
--- a/docshell/test/chrome/test_docRedirect.xul
+++ b/docshell/test/chrome/test_docRedirect.xul
@@ -66,20 +66,19 @@ https://bugzilla.mozilla.org/show_bug.cg
           iid.equals(Ci.nsISupportsWeakReference))
         return this;
       throw Cr.NS_ERROR_NO_INTERFACE;
     }
   }
 
   var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
       getService(Ci.nsIAppShellService).createWindowlessBrowser(true);
-  let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor).
-      getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
   docShell.createAboutBlankContentViewer(
-      Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal));
+      Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal));
 
   progressListener.add(docShell, function(success) {
     webNav.close();
     SimpleTest.is(success, true, "Received document redirect event");
     SimpleTest.finish();
   });
 
   var win = docShell.contentViewer.DOMDocument.defaultView;
--- a/docshell/test/unit/test_setUsePrivateBrowsing.js
+++ b/docshell/test/unit/test_setUsePrivateBrowsing.js
@@ -1,20 +1,19 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 add_task(async function() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
 
-  let loadContext = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                          .getInterface(Ci.nsILoadContext);
+  let docShell = webNav.docShell;
 
-  let docShell = webNav.getInterface(Ci.nsIDocShell);
+  let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
 
   equal(loadContext.usePrivateBrowsing, false, "Should start out in non-private mode");
 
   loadContext.usePrivateBrowsing = true;
   equal(loadContext.usePrivateBrowsing, true,
         "Should be able to change to private mode prior to a document load");
 
   loadContext.usePrivateBrowsing = false;
--- a/dom/html/AutoplayPermissionManager.cpp
+++ b/dom/html/AutoplayPermissionManager.cpp
@@ -7,20 +7,20 @@
 #include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/AutoplayPermissionRequest.h"
 
 #include "nsGlobalWindowInner.h"
 #include "nsISupportsImpl.h"
 #include "mozilla/Logging.h"
 #include "nsContentPermissionHelper.h"
 
-extern mozilla::LazyLogModule gMediaElementLog;
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
 
 #define PLAY_REQUEST_LOG(msg, ...)                                             \
-  MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+  MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
 
 namespace mozilla {
 
 RefPtr<GenericPromise>
 AutoplayPermissionManager::RequestWithPrompt()
 {
   // If we've already requested permission, we'll just return the promise,
   // as we don't want to show multiple permission requests at once.
--- a/dom/html/AutoplayPermissionRequest.cpp
+++ b/dom/html/AutoplayPermissionRequest.cpp
@@ -4,20 +4,20 @@
  * 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/. */
 
 #include "mozilla/AutoplayPermissionRequest.h"
 #include "mozilla/AutoplayPermissionManager.h"
 
 #include "mozilla/Logging.h"
 
-extern mozilla::LazyLogModule gMediaElementLog;
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
 
 #define PLAY_REQUEST_LOG(msg, ...)                                             \
-  MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+  MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
 
 namespace mozilla {
 
 NS_IMPL_ISUPPORTS(AutoplayPermissionRequest, nsIContentPermissionRequest)
 
 AutoplayPermissionRequest::AutoplayPermissionRequest(
   AutoplayPermissionManager* aManager,
   nsGlobalWindowInner* aWindow,
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -122,16 +122,20 @@
 #include "xpcpublic.h"
 #include <algorithm>
 #include <cmath>
 #include <limits>
 
 mozilla::LazyLogModule gMediaElementLog("nsMediaElement");
 static mozilla::LazyLogModule gMediaElementEventsLog("nsMediaElementEvents");
 
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
+#define AUTOPLAY_LOG(msg, ...)                                             \
+  MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
 #define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg)
 #define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg)
 
 using namespace mozilla::layers;
 using mozilla::net::nsMediaFragmentURIParser;
 using namespace mozilla::dom::HTMLMediaElement_Binding;
 
 namespace mozilla {
@@ -3063,16 +3067,17 @@ HTMLMediaElement::SetMutedInternal(uint3
 
 void
 HTMLMediaElement::PauseIfShouldNotBePlaying()
 {
   if (GetPaused()) {
     return;
   }
   if (AutoplayPolicy::IsAllowedToPlay(*this) != nsIAutoplay::ALLOWED) {
+    AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
     ErrorResult rv;
     Pause(rv);
     OwnerDoc()->SetDocTreeHadPlayRevoked();
   }
 }
 
 void
 HTMLMediaElement::SetVolumeInternal()
@@ -4098,17 +4103,17 @@ HTMLMediaElement::Play(ErrorResult& aRv)
   switch (AutoplayPolicy::IsAllowedToPlay(*this)) {
     case nsIAutoplay::ALLOWED: {
       mPendingPlayPromises.AppendElement(promise);
       PlayInternal(handlingUserInput);
       UpdateCustomPolicyAfterPlayed();
       break;
     }
     case nsIAutoplay::BLOCKED: {
-      LOG(LogLevel::Debug, ("%p play not blocked.", this));
+      AUTOPLAY_LOG("%p play blocked.", this);
       promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
       if (StaticPrefs::MediaBlockEventEnabled()) {
         DispatchAsyncEvent(NS_LITERAL_STRING("blocked"));
       }
       break;
     }
     case nsIAutoplay::PROMPT: {
       // Prompt the user for permission to play.
@@ -4123,47 +4128,44 @@ HTMLMediaElement::Play(ErrorResult& aRv)
 void
 HTMLMediaElement::EnsureAutoplayRequested(bool aHandlingUserInput)
 {
   if (mAutoplayPermissionRequest.Exists()) {
     // Autoplay has already been requested in a previous play() call.
     // Await for the previous request to be approved or denied. This
     // play request's promise will be fulfilled with all other pending
     // promises when the permission prompt is resolved.
-    LOG(LogLevel::Debug,
-        ("%p EnsureAutoplayRequested() existing request, bailing.", this));
+    AUTOPLAY_LOG("%p EnsureAutoplayRequested() existing request, bailing.", this);
     return;
   }
 
   RefPtr<AutoplayPermissionManager> request =
     AutoplayPolicy::RequestFor(*OwnerDoc());
   if (!request) {
     AsyncRejectPendingPlayPromises(NS_ERROR_DOM_INVALID_STATE_ERR);
     return;
   }
   RefPtr<HTMLMediaElement> self = this;
   request->RequestWithPrompt()
     ->Then(mAbstractMainThread,
            __func__,
            [ self, handlingUserInput = aHandlingUserInput, request ](
              bool aApproved) {
              self->mAutoplayPermissionRequest.Complete();
-             LOG(LogLevel::Debug,
-                 ("%p Autoplay request approved request=%p",
-                  self.get(),
-                  request.get()));
+             AUTOPLAY_LOG("%p Autoplay request approved request=%p",
+                          self.get(),
+                          request.get());
              self->PlayInternal(handlingUserInput);
              self->UpdateCustomPolicyAfterPlayed();
            },
            [self, request](nsresult aError) {
              self->mAutoplayPermissionRequest.Complete();
-             LOG(LogLevel::Debug,
-                 ("%p Autoplay request denied request=%p",
-                  self.get(),
-                  request.get()));
+             AUTOPLAY_LOG("%p Autoplay request denied request=%p",
+                          self.get(),
+                          request.get());
              LOG(LogLevel::Debug, ("%s rejecting play promimses", __func__));
              self->AsyncRejectPendingPlayPromises(
                NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
            })
     ->Track(mAutoplayPermissionRequest);
 }
 
 void
--- a/dom/media/AutoplayPolicy.cpp
+++ b/dom/media/AutoplayPolicy.cpp
@@ -2,29 +2,50 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "AutoplayPolicy.h"
 
 #include "mozilla/EventStateManager.h"
+#include "mozilla/Logging.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/AudioContext.h"
 #include "mozilla/AutoplayPermissionManager.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "mozilla/dom/HTMLMediaElementBinding.h"
 #include "nsIAutoplay.h"
 #include "nsContentUtils.h"
 #include "nsIDocument.h"
 #include "MediaManager.h"
 #include "nsIDocShell.h"
 #include "nsIDocShellTreeItem.h"
 #include "nsPIDOMWindow.h"
 
+mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay");
+
+#define AUTOPLAY_LOG(msg, ...)                                             \
+  MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+static const char*
+AllowAutoplayToStr(const uint32_t state)
+{
+  switch (state) {
+    case nsIAutoplay::ALLOWED:
+      return "allowed";
+    case nsIAutoplay::BLOCKED:
+      return "blocked";
+    case nsIAutoplay::PROMPT:
+      return "prompt";
+    default:
+      return "unknown";
+  }
+}
+
 namespace mozilla {
 namespace dom {
 
 static nsIDocument*
 ApproverDocOf(const nsIDocument& aDocument)
 {
   nsCOMPtr<nsIDocShell> ds = aDocument.GetDocShell();
   if (!ds) {
@@ -57,27 +78,27 @@ IsWindowAllowedToPlay(nsPIDOMWindowInner
 
   if (!aWindow->GetExtantDoc()) {
     return false;
   }
 
   nsIDocument* approver = ApproverDocOf(*aWindow->GetExtantDoc());
   if (nsContentUtils::IsExactSitePermAllow(approver->NodePrincipal(),
                                            "autoplay-media")) {
-    // Autoplay permission has been granted already.
+    AUTOPLAY_LOG("Allow autoplay as document has autoplay permission.");
     return true;
   }
 
   if (approver->HasBeenUserGestureActivated()) {
-    // Document has been activated by user gesture.
+    AUTOPLAY_LOG("Allow autoplay as document activated by user gesture.");
     return true;
   }
 
   if (approver->IsExtensionPage()) {
-    // Always allow extension page to autoplay.
+    AUTOPLAY_LOG("Allow autoplay as in extension document.");
     return true;
   }
 
   return false;
 }
 
 /* static */
 already_AddRefed<AutoplayPermissionManager>
@@ -103,20 +124,33 @@ DefaultAutoplayBehaviour()
     return nsIAutoplay::ALLOWED;
   }
   return prefValue;
 }
 
 static bool
 IsMediaElementAllowedToPlay(const HTMLMediaElement& aElement)
 {
-  return ((aElement.Volume() == 0.0 || aElement.Muted()) &&
-           Preferences::GetBool("media.autoplay.allow-muted", true)) ||
-          IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow()) ||
-          (aElement.OwnerDoc()->MediaDocumentKind() == nsIDocument::MediaDocumentKind::Video);
+  if ((aElement.Volume() == 0.0 || aElement.Muted()) &&
+       Preferences::GetBool("media.autoplay.allow-muted", true)) {
+    AUTOPLAY_LOG("Allow muted media %p to autoplay.", &aElement);
+    return true;
+  }
+
+  if (IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow())) {
+    AUTOPLAY_LOG("Autoplay allowed as activated/whitelisted window, media %p.", &aElement);
+    return true;
+  }
+
+  if (aElement.OwnerDoc()->MediaDocumentKind() == nsIDocument::MediaDocumentKind::Video) {
+    AUTOPLAY_LOG("Allow video document %p to autoplay\n", &aElement);
+    return true;
+  }
+
+  return false;
 }
 
 /* static */ bool
 AutoplayPolicy::WouldBeAllowedToPlayIfAutoplayDisabled(const HTMLMediaElement& aElement)
 {
   return IsMediaElementAllowedToPlay(aElement);
 }
 
@@ -129,21 +163,22 @@ AutoplayPolicy::IsAllowedToPlay(const HT
   if (!Preferences::GetBool("media.autoplay.enabled.user-gestures-needed", false)) {
     // If element is blessed, it would always be allowed to play().
     return (autoplayDefault == nsIAutoplay::ALLOWED ||
             aElement.IsBlessed() ||
             EventStateManager::IsHandlingUserInput())
               ? nsIAutoplay::ALLOWED : nsIAutoplay::BLOCKED;
   }
 
-  if (IsMediaElementAllowedToPlay(aElement)) {
-    return nsIAutoplay::ALLOWED;
-  }
+  const uint32_t result = IsMediaElementAllowedToPlay(aElement) ?
+    nsIAutoplay::ALLOWED : autoplayDefault;
 
-  return autoplayDefault;
+  AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s",
+                &aElement, AllowAutoplayToStr(result));
+  return result;
 }
 
 /* static */ bool
 AutoplayPolicy::IsAudioContextAllowedToPlay(NotNull<AudioContext*> aContext)
 {
   if (!Preferences::GetBool("media.autoplay.block-webaudio", false)) {
     return true;
   }
--- a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
+++ b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js
@@ -64,18 +64,17 @@ function test()
   });
   policy.active = true;
 
   let baseURI = Services.io.newURI(url);
   let principal = Services.scriptSecurityManager
         .createCodebasePrincipal(baseURI, {});
 
   let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-  let interfaceRequestor = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor);
-  let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  let docShell = chromeWebNav.docShell;
   docShell.createAboutBlankContentViewer(principal);
 
   info("fake webextension docShell created");
 
   registerCleanupFunction(function() {
     policy.active = false;
     if (chromeWebNav) {
       chromeWebNav.close();
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -1705,16 +1705,17 @@ WebRenderBridgeParent::ClearResources()
     }
   }
   mTextureHosts.clear();
   for (const auto& entry : mAsyncCompositables) {
     wr::PipelineId pipelineId = wr::AsPipelineId(entry.first);
     RefPtr<WebRenderImageHost> host = entry.second;
     host->ClearWrBridge();
     mAsyncImageManager->RemoveAsyncImagePipeline(pipelineId, txn);
+    txn.RemovePipeline(pipelineId);
   }
   mAsyncCompositables.clear();
   for (const auto& entry : mSharedSurfaceIds) {
     wr::ExternalImageId id = wr::ToExternalImageId(entry);
     mAsyncImageManager->HoldExternalImage(mPipelineId, mWrEpoch, id);
   }
   mSharedSurfaceIds.clear();
 
--- a/gfx/tests/browser/browser_windowless_troubleshoot_crash.js
+++ b/gfx/tests/browser/browser_windowless_troubleshoot_crash.js
@@ -1,16 +1,15 @@
 let { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm", {});
 
 add_task(async function test_windowlessBrowserTroubleshootCrash() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
 
   let onLoaded = new Promise((resolve, reject) => {
-    let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDocShell);
+    let docShell = webNav.docShell;
     let listener = {
       observe(contentWindow, topic, data) {
         let observedDocShell = contentWindow.docShell
                                             .sameTypeRootTreeItem
                                             .QueryInterface(Ci.nsIDocShell);
           if (docShell === observedDocShell) {
             Services.obs.removeObserver(listener, "content-document-global-created");
             resolve();
--- a/hal/windows/WindowsProcessPriority.cpp
+++ b/hal/windows/WindowsProcessPriority.cpp
@@ -1,16 +1,16 @@
 /* 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/. */
 
 #include "Hal.h"
 #include "HalLog.h"
 
-#include <Windows.h>
+#include <windows.h>
 
 using namespace mozilla::hal;
 
 namespace mozilla {
 namespace hal_impl {
 
 bool
 SetProcessPrioritySupported()
--- a/js/xpconnect/tests/unit/test_nuke_sandbox_event_listeners.js
+++ b/js/xpconnect/tests/unit/test_nuke_sandbox_event_listeners.js
@@ -13,18 +13,17 @@ function promiseEvent(target, event) {
 }
 
 add_task(async function() {
   let principal = Services.scriptSecurityManager
     .createCodebasePrincipalFromOrigin("http://example.com/");
 
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(principal);
 
   let window = webnav.document.defaultView;
   let sandbox = Cu.Sandbox(window, {sandboxPrototype: window});
 
   function sandboxContent() {
     window.onload = function SandboxOnLoad() {};
--- a/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
+++ b/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js
@@ -9,18 +9,17 @@ function getWindowlessBrowser(url) {
   let ssm = Services.scriptSecurityManager;
 
   let uri = NetUtil.newURI(url);
 
   let principal = ssm.createCodebasePrincipal(uri, {});
 
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(principal);
 
   return webnav;
 }
 
 function StubPolicy(id) {
   return new WebExtensionPolicy({
--- a/js/xpconnect/tests/unit/test_xray_named_element_access.js
+++ b/js/xpconnect/tests/unit/test_xray_named_element_access.js
@@ -2,18 +2,17 @@
 "use strict"
 
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
 add_task(async function() {
   let webnav = Services.appShell.createWindowlessBrowser(false);
 
-  let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webnav.docShell;
 
   docShell.createAboutBlankContentViewer(null);
 
   let window = webnav.document.defaultView;
   let unwrapped = Cu.waiveXrays(window);
 
   window.document.body.innerHTML = '<div id="foo"></div>';
 
--- a/mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/nightly
+++ b/mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/nightly
@@ -29,16 +29,19 @@ ac_add_options --disable-tests
 # advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
 # If you think you can't handle the whole set of changes, please reach out to the Release
 # Engineering team.
 ac_add_options --with-android-min-sdk=16
 ac_add_options --target=arm-linux-androideabi
 
 ac_add_options --with-branding=mobile/android/branding/nightly
 
+# Pull code coverage dependencies too.
+ac_add_options --enable-java-coverage
+
 export MOZILLA_OFFICIAL=1
 export MOZ_TELEMETRY_REPORTING=1
 export MOZ_ANDROID_MMA=1
 export MOZ_ANDROID_POCKET=1
 
 . "$topsrcdir/mobile/android/config/mozconfigs/common.override"
 
 # End ../android-api-16-frontend/nightly.
new file mode 100644
--- /dev/null
+++ b/mobile/android/config/mozconfigs/android-api-16/debug-ccov
@@ -0,0 +1,29 @@
+. "$topsrcdir/build/mozconfig.artifact.automation"
+
+NO_CACHE=1
+NO_NDK=1
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common"
+
+# Global options
+ac_add_options --enable-debug
+ac_add_options --enable-java-coverage
+
+# Android
+# Warning: Before increasing the with-android-min-sdk value, please note several places in and out
+# of tree have to be changed. Otherwise, places like Treeherder or archive.mozilla.org will
+# advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
+# If you think you can't handle the whole set of changes, please reach out to the Release
+# Engineering team.
+ac_add_options --with-android-min-sdk=16
+ac_add_options --target=arm-linux-androideabi
+
+. "$topsrcdir/mobile/android/config/mozconfigs/android-api-16/nightly"
+
+. "$topsrcdir/build/mozconfig.artifact"
+
+ac_add_options --enable-artifact-build-symbols
+
+ac_add_options --with-branding=mobile/android/branding/nightly
+
+. "$topsrcdir/mobile/android/config/mozconfigs/common.override"
--- a/mobile/android/config/proguard/proguard.cfg
+++ b/mobile/android/config/proguard/proguard.cfg
@@ -160,16 +160,21 @@
 #-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
 
 -include "play-services-keeps.cfg"
 
 # Don't print spurious warnings from the support library.
 # See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
 -dontnote android.support.**
 
+# Don't warn when classes referenced by JaCoCo are missing when running the build from android-dependencies.
+-dontwarn java.lang.instrument.**
+-dontwarn java.lang.management.**
+-dontwarn javax.management.**
+
 -include "adjust-keeps.cfg"
 
 -include "leakcanary-keeps.cfg"
 
 -include "appcompat-v7-keeps.cfg"
 
 -include "proguard-android.cfg"
 
--- a/mobile/android/docs/index.rst
+++ b/mobile/android/docs/index.rst
@@ -16,15 +16,16 @@ Contents:
    activitystreamtelemetry
    downloadcontenttelemetry
    adjust
    mma
    defaultdomains
    shutdown
    push
    gradle
+   testcoverage
 
 Indices and tables
 ==================
 
 * :ref:`genindex`
 * :ref:`modindex`
 * :ref:`search`
new file mode 100644
--- /dev/null
+++ b/mobile/android/docs/testcoverage.rst
@@ -0,0 +1,117 @@
+.. -*- Mode: rst; fill-column: 80; -*-
+
+========================================================
+ Collecting code coverage information for Android tests
+========================================================
+
+The Android-specific test suites are outlined on MDN_. A more elaborate description can be found on
+the Wiki_. This page describes how collecting code coverage information is implemented for these
+test suites.
+
+.. _MDN: https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites
+.. _WIKI: https://wiki.mozilla.org/Mobile/Fennec/Android/Testing
+
+Collecting and exporting code coverage information
+==================================================
+
+Relevant tools and libraries
+----------------------------
+
+JaCoCo_ is the tool used to gather code coverage data. JaCoCo uses class file
+instrumentation to record execution coverage data. It has two operating modes:
+
+- `online instrumentation`_: Class files are instrumented on-the-fly using a so
+  called Java agent. The JaCoCo agent collects execution information and dumps
+  it on request or when the JVM exits. This method is used for test suites
+  which run on the JVM (generally, `*UnitTest` Gradle tasks).
+- `offline instrumentation`_: At runtime the pre-instrumented classes needs be
+  on the classpath instead of the original classes. In addition,
+  `jacocoagent.jar` must be put on the classpath. This method is used for test
+  suites with run on the Android emulator (generally, `*AndroidTest` Gradle
+  tasks, including `robocop`).
+
+JaCoCo is integrated with Gradle in two ways: a Gradle plugin (activated in the
+``build.gradle`` file as a top-level ``apply plugin: 'jacoco'``), and an
+Android-specific Gradle plugin, activated with ``android { buildTypes { debug {
+testCoverageEnabled true } } }``. These two methods of activating JaCoCo have a
+different syntax of choosing the JaCoCo tool version, and both should be
+configured to use the same version. See the
+``mobile/android/geckoview/build.gradle`` file for example usage.
+
+grcov_ is a tool implemented in Rust that transforms code coverage reports
+between formats. Among others, it supports reading JaCoCo XML reports.
+
+.. _JaCoCo: https://www.eclemma.org/jacoco/
+.. _online instrumentation: https://www.jacoco.org/jacoco/trunk/doc/agent.html
+.. _offline instrumentation: https://www.jacoco.org/jacoco/trunk/doc/offline.html
+.. _grcov: https://github.com/mozilla/grcov/
+
+Generating the coverage report artifacts
+----------------------------------------
+
+All tasks that output code coverage information do so by exporting an artifact
+named ``code-coverage-grcov.zip`` which contains a single file named
+``grcov_lcov_output.info``. The ``grcov_lcov_output.info`` file should contain
+coverage information in the lcov format. The artifact, once uploaded, is picked
+up and indexed by ActiveData_.
+
+The code that generates the ``code-coverage-grcov.zip`` artifact after a
+generally resides in the ``CodeCoverageMixin`` class, in the module
+``testing/mozharness/mozharness/mozilla/testing/codecoverage.py``. This class is
+responsible for downloading ``grcov`` along with other artifacts from the build
+job. It is also responsible for running ``grcov`` after the tests are finished,
+to convert and merge the coverage reports.
+
+.. _ActiveData: https://wiki.mozilla.org/EngineeringProductivity/Projects/ActiveData
+
+Code coverage for android-test
+===============================
+
+The `android-test` suite is a JUnit test suite that runs locally on the
+host's JVM. It can be run with ``mach android test``. The test suite is
+implemented as a build task, defined at
+``taskcluster/ci/build/android-stuff.yml``.
+
+To collect code coverage from this suite, a duplicate build task is defined,
+called `android-test-ccov`. It can be run with ``mach android test-ccov``.
+
+The mach subcommand is responsible for downloading and running `grcov`,
+instead of the ``CodeCoverageMixin`` class. This is because the
+`android-test-ccov` task is a build task (not a test task), so it doesn't use
+mozharness to run the tests.
+
+
+Code coverage for geckoview-junit
+==================================
+
+The geckoview-junit_ tests are on-device Android JUnit tests written for
+GeckoView_. The tests are implemented with ``mochitest``. The automation
+entry point is ``testing/mozharness/scripts/android_emulator_unittest.py``,
+which then calls ``testing/mochitest/runjunit.py``. This is an out-of-tree
+task, so a source tree clone is unavailable.
+
+To generate the coverage report, we need three things:
+
+- The classfiles before instrumentation. These are archived as a public
+  artifact during build time, and downloaded on the test machine during testing
+  time;
+- The coverage.ec file with coverage counters. This is generated while running
+  the tests on the emulator, then downloaded with ``adb pull``;
+- ``jacoco-cli``, a command-line package JaCoCo component that takes the
+  classfiles and the coverage counters as input and generates XML reports as
+  output.
+
+The ``mach android archive-geckoview-coverage-artifacts`` command archives the
+class files and exports the ``jacoco-cli`` jar file after the build is done.
+These files are later saved as public artifacts of the build.
+
+To enable offline instrumentation for the test suites, the mozconfig flag
+``--enable-java-coverage`` should be set. When the flag is checked both during
+build and test time. During test time, the flag instructs ``CodeCoverageMixin``
+to download the coverage artifacts from the build task before the tests run,
+and generate and export the reports after testing is finished. The flag also
+instructs the ``runjunit.py`` script to insert the arguments ``-e coverage
+true`` to ``am instrument``.
+
+.. _GeckoView: https://wiki.mozilla.org/Mobile/GeckoView
+.. _geckoview-junit: https://developer.mozilla.org/en-US/docs/Mozilla/Geckoview-Junit_Tests
--- a/mobile/android/geckoview/build.gradle
+++ b/mobile/android/geckoview/build.gradle
@@ -367,8 +367,76 @@ task("generateSDKBindings", type: JavaEx
 
     dependsOn project(':annotations').jar
 }
 
 apply from: "${topsrcdir}/mobile/android/gradle/jacoco_dependencies.gradle"
 if (project.hasProperty('enable_code_coverage')) {
     apply from: "${topsrcdir}/mobile/android/gradle/jacoco_for_junit.gradle"
 }
+
+// Set up code coverage for tests on emulators.
+if (mozconfig.substs.MOZ_JAVA_CODE_COVERAGE) {
+    apply plugin: "jacoco"
+    jacoco {
+        toolVersion = "${project.jacoco_version}"
+    }
+
+    android {
+        jacoco {
+            version = "$jacoco_version"
+        }
+        buildTypes {
+            debug {
+                testCoverageEnabled true
+            }
+        }
+    }
+
+    configurations {
+        // This configuration is used for dependencies that are not needed at compilation or
+        // runtime, but need to be exported as artifacts of the build for usage on the testing
+        // machines.
+        coverageDependency
+    }
+
+    dependencies {
+        // This is required both in the instrumented application classes and the test classes,
+        // so `api` has to be used instead of `androidTestImplementation`.
+        api "org.jacoco:org.jacoco.agent:$jacoco_version:runtime"
+
+        coverageDependency ("org.jacoco:org.jacoco.cli:$jacoco_version:nodeps") {
+            exclude group: 'org.ow2.asm', module: '*'
+        }
+    }
+
+    // This task is used by `mach android archive-geckoview-coverage-artifacts`.
+    task copyCoverageDependencies(type: Copy) {
+        from(configurations.coverageDependency) {
+            include 'org.jacoco.cli-*-nodeps.jar'
+            rename { _ -> 'target.jacoco-cli.jar' }
+        }
+        into "$buildDir/coverage"
+    }
+
+    // Generate tasks to archive compiled classfiles for later use with JaCoCo report generation.
+    // One of these tasks is used by `mach android archive-geckoview-coverage-artifacts`.
+    android.libraryVariants.all { variant ->
+        def name = variant.name
+        def compileTask = tasks.getByName("compile${name.capitalize()}JavaWithJavac")
+        task "archiveClassfiles${name.capitalize()}"(type: Zip, dependsOn: compileTask) {
+            description = "Archive compiled classfiles for $name in order to export them as code coverage artifacts."
+            def fileFilter = ['**/androidTest/**',
+                              '**/test/**',
+                              '**/R.class',
+                              '**/R$*.class',
+                              '**/BuildConfig.*',
+                              '**/Manifest*.*',
+                              '**/*Test*.*',
+                              'android/**/*.*']
+            from fileTree(dir: compileTask.destinationDir, excludes: fileFilter)
+            destinationDir = file("${buildDir}/coverage")
+            // Note: This task assumes only one variant of archiveClassfiles* will be used.
+            // Running multiple variants of this task will overwrite the output archive.
+            archiveName = 'target.geckoview_classfiles.zip'
+        }
+    }
+}
--- a/mobile/android/gradle.configure
+++ b/mobile/android/gradle.configure
@@ -232,26 +232,39 @@ def gradle_android_archive_geckoview_tas
         'geckoview_example:assemble{geckoview_example.variant.name}'.format(geckoview_example=build_config.geckoview_example),
         'geckoview_example:assemble{geckoview_example.variant.name}AndroidTest'.format(geckoview_example=build_config.geckoview_example),
         'geckoview:uploadArchives',
     ]
 
 set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geckoview_tasks)
 
 
+@depends(gradle_android_build_config)
+def gradle_android_archive_geckoview_coverage_artifacts_tasks(build_config):
+    '''Gradle tasks run by |mach android archive-geckoview-coverage-artifacts|.'''
+    return [
+        'geckoview:archiveClassfiles{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
+        'geckoview:copyCoverageDependencies',
+    ]
+
+set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS',
+           gradle_android_archive_geckoview_coverage_artifacts_tasks)
+
+
 @depends(
     gradle_android_app_tasks,
     gradle_android_test_tasks,
     gradle_android_lint_tasks,
     gradle_android_checkstyle_tasks,
     gradle_android_findbugs_tasks,
     gradle_android_archive_geckoview_tasks,
     gradle_android_generate_sdk_bindings_tasks,
     gradle_android_generate_generated_jni_wrappers_tasks,
     gradle_android_generate_fennec_jni_wrappers_tasks,
+    gradle_android_archive_geckoview_coverage_artifacts_tasks,
 )
 @imports(_from='itertools', _import='imap')
 @imports(_from='itertools', _import='chain')
 @imports(_from='itertools', _import='ifilterfalse')
 def gradle_android_dependencies_tasks(*tasks):
     '''Gradle tasks run by |mach android dependencies|.'''
     # The union, plus a bit more, of all of the Gradle tasks
     # invoked by the android-* automation jobs.
--- a/mobile/android/gradle/jacoco_dependencies.gradle
+++ b/mobile/android/gradle/jacoco_dependencies.gradle
@@ -1,13 +1,11 @@
 /* -*- Mode: Groovy; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-project.ext.jacoco_version = "0.7.8"
-
 dependencies {
-    testImplementation "org.jacoco:org.jacoco.agent:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.ant:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.core:${project.jacoco_version}"
-    testImplementation "org.jacoco:org.jacoco.report:${project.jacoco_version}"
+    testImplementation "org.jacoco:org.jacoco.agent:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.ant:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.core:$jacoco_version"
+    testImplementation "org.jacoco:org.jacoco.report:$jacoco_version"
 }
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -202,17 +202,17 @@ class MachCommands(MachCommandBase):
         if not found_reports:
             print('TEST-UNEXPECTED-FAIL | android-test | No reports found under {}'.format(gradledir))  # NOQA: E501
             return 1
 
         return ret
 
     @SubCommand('android', 'test-ccov',
                 """Run Android local unit tests in order to get a code coverage report.
-                See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-test""")  # NOQA: E501
+        See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""")  # NOQA: E501
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def android_test_ccov(self, args):
         enable_ccov = '-Penable_code_coverage'
 
         # Don't care if the tests are failing, we only want the coverage information.
         self.android_test([enable_ccov])
 
         self.gradle(self.substs['GRADLE_ANDROID_TEST_CCOV_REPORT_TASKS'] +
@@ -408,16 +408,26 @@ class MachCommands(MachCommandBase):
         # We don't want to gate producing dependency archives on clean
         # lint or checkstyle, particularly because toolchain versions
         # can change the outputs for those processes.
         self.gradle(self.substs['GRADLE_ANDROID_DEPENDENCIES_TASKS'] +
                     ["--continue"] + args, verbose=True)
 
         return 0
 
+    @SubCommand('android', 'archive-geckoview-coverage-artifacts',
+                """Archive compiled geckoview classfiles to be used later in generating code
+        coverage reports. See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""")  # NOQA: E501
+    @CommandArgument('args', nargs=argparse.REMAINDER)
+    def android_archive_geckoview_classfiles(self, args):
+        self.gradle(self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS'] +
+                    ["--continue"] + args, verbose=True)
+
+        return 0
+
     @SubCommand('android', 'archive-geckoview',
                 """Create GeckoView archives.
         See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""")  # NOQA: E501
     @CommandArgument('args', nargs=argparse.REMAINDER)
     def android_archive_geckoview(self, args):
         ret = self.gradle(
             self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS'] + ["--continue"] + args,
             verbose=True)
--- a/mozglue/tests/gtest/Injector/Injector.cpp
+++ b/mozglue/tests/gtest/Injector/Injector.cpp
@@ -1,13 +1,13 @@
 /* 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/. */
 
-#include <Windows.h>
+#include <windows.h>
 #include <stdio.h>
 #include <stdlib.h>
 
 int
 main(int argc, char** argv)
 {
   if (argc < 4) {
     fprintf(stderr,
--- a/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp
+++ b/mozglue/tests/gtest/InjectorDLL/InjectorDLL.cpp
@@ -1,11 +1,11 @@
 /* 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/. */
 
-#include <Windows.h>
+#include <windows.h>
 
 BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID)
 {
   return TRUE;
 }
 
--- a/mozglue/tests/gtest/TestDLLEject.cpp
+++ b/mozglue/tests/gtest/TestDLLEject.cpp
@@ -1,13 +1,13 @@
 /* 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/. */
 
-#include <Windows.h>
+#include <windows.h>
 #include <winternl.h>
 #include "gtest/gtest.h"
 #include "nsReadableUtils.h"
 #include "nsString.h"
 #include "nsUnicharUtils.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/UniquePtr.h"
--- a/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_checks_configure.py
@@ -507,16 +507,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': java,
             'JAVAH': javah,
             'JAVAC': javac,
             'JAR': jar,
             'JARSIGNER': jarsigner,
             'KEYTOOL': keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -550,16 +551,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -579,16 +581,17 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
@@ -609,28 +612,49 @@ class TestChecksConfigure(unittest.TestC
         self.assertEqual(status, 0)
         self.assertEqual(config, {
             'JAVA': alt_java,
             'JAVAH': alt_javah,
             'JAVAC': alt_javac,
             'JAR': alt_jar,
             'JARSIGNER': alt_jarsigner,
             'KEYTOOL': alt_keytool,
+            'MOZ_JAVA_CODE_COVERAGE': False,
         })
         self.assertEqual(out, textwrap.dedent('''\
              checking for java... %s
              checking for javah... %s
              checking for jar... %s
              checking for jarsigner... %s
              checking for keytool... %s
              checking for javac... %s
              checking for javac version... 1.8
         ''' % (alt_java, alt_javah, alt_jar, alt_jarsigner,
                alt_keytool, alt_javac)))
 
+        # --enable-java-coverage should set MOZ_JAVA_CODE_COVERAGE.
+        config, out, status = self.get_result(
+            args=['--enable-java-coverage'],
+            includes=includes,
+            extra_paths=paths,
+            environ={
+                'PATH': mozpath.dirname(java),
+                'JAVA_HOME': mozpath.dirname(mozpath.dirname(java)),
+            })
+        self.assertEqual(status, 0)
+        self.assertEqual(config, {
+            'JAVA': java,
+            'JAVAH': javah,
+            'JAVAC': javac,
+            'JAR': jar,
+            'JARSIGNER': jarsigner,
+            'KEYTOOL': keytool,
+            'MOZ_JAVA_CODE_COVERAGE': True,
+        })
+
         def mock_old_javac(_, args):
             if len(args) == 1 and args[0] == '-version':
                 return 0, '1.6.9', ''
             self.fail("Unexpected arguments to mock_old_javac: %s" % args)
 
         # An old javac is fatal.
         paths[javac] = mock_old_javac
         config, out, status = self.get_result(includes=includes,
--- a/services/sync/modules/UIState.jsm
+++ b/services/sync/modules/UIState.jsm
@@ -27,16 +27,17 @@ const TOPICS = [
   "weave:service:sync:start",
   "weave:service:sync:finish",
   "weave:service:sync:error",
   "weave:service:start-over:finish",
   "fxaccounts:onverified",
   "fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
   "fxaccounts:onlogout",
   "fxaccounts:profilechange",
+  "fxaccounts:statechange",
 ];
 
 const ON_UPDATE = "sync-ui-state:update";
 
 const STATUS_NOT_CONFIGURED = "not_configured";
 const STATUS_LOGIN_FAILED = "login_failed";
 const STATUS_NOT_VERIFIED = "not_verified";
 const STATUS_SIGNED_IN = "signed_in";
--- a/taskcluster/ci/build/android.yml
+++ b/taskcluster/ci/build/android.yml
@@ -43,16 +43,72 @@ android-api-16/debug:
         - android-ndk-linux
         - android-sdk-linux
         - linux64-clang
         - linux64-rust-android
         - linux64-rust-size
         - linux64-sccache
         - linux64-node
 
+android-api-16-ccov/debug:
+    description: "Android 4.0 api-16+ Debug Coverage"
+    index:
+        product: mobile
+        job-name: android-api-16-ccov-debug
+    treeherder:
+        platform: android-4-0-armv7-api16-ccov/debug
+        symbol: B
+    worker-type: aws-provisioner-v1/gecko-{level}-b-android
+    worker:
+        docker-image: {in-tree: android-build}
+        max-run-time: 7200
+        env:
+            GRADLE_USER_HOME: "/builds/worker/workspace/build/src/mobile/android/gradle/dotgradle-offline"
+            TOOLTOOL_MANIFEST: "mobile/android/config/tooltool-manifests/android/releng.manifest"
+        artifacts:
+            - name: public/android/R
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
+              type: directory
+            - name: public/android/maven
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
+              type: directory
+            - name: public/build/geckoview-androidTest.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
+              type: file
+            - name: public/build/geckoview_example.apk
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
+              type: file
+            - name: public/build/target.geckoview_classfiles.zip
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/coverage/target.geckoview_classfiles.zip
+              type: file
+            - name: public/build/target.jacoco-cli.jar
+              path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/coverage/target.jacoco-cli.jar
+              type: file
+            - name: public/build
+              path: /builds/worker/artifacts/
+              type: directory
+    run:
+        using: mozharness
+        actions: [get-secrets build multi-l10n update]
+        config:
+            - builds/releng_base_android_64_builds.py
+        script: "mozharness/scripts/fx_desktop_build.py"
+        secrets: true
+        custom-build-variant-cfg: api-16-debug-ccov
+        tooltool-downloads: internal
+    toolchains:
+        - android-gradle-dependencies
+        - android-ndk-linux
+        - android-sdk-linux
+        - linux64-clang
+        - linux64-rust-android
+        - linux64-rust-size
+        - linux64-sccache
+        - linux64-node
+
 android-x86/opt:
     description: "Android 4.2 x86 Opt"
     index:
         product: mobile
         job-name: android-x86-opt
     treeherder:
         platform: android-4-2-x86/opt
         symbol: B
--- a/taskcluster/ci/packages/kind.yml
+++ b/taskcluster/ci/packages/kind.yml
@@ -95,36 +95,36 @@ jobs:
 
   deb7-mercurial:
     description: "Modern Mercurial for Debian wheezy"
     treeherder:
       symbol: Deb7(hg)
     run:
       using: debian-package
       tarball:
-        url: https://www.mercurial-scm.org/release/mercurial-4.5.2.tar.gz
-        sha256: a44a9ffd1c9502a4f97298a6bbcb8a79fc8192424c760c67f17b45c12114e390
+        url: https://www.mercurial-scm.org/release/mercurial-4.7.tar.gz
+        sha256: 098cb1437f77fb7f75dc2f008742933c729ec0b63cfa3e4e2f0a8fbc3d0d349f
       pre-build-command: >-
-        cp -r contrib/debian debian &&
+        cp -r contrib/packaging/debian debian &&
         sed -i -e "s/__VERSION__/$(awk -F\" '$2 {print $2}' mercurial/__version__.py)-1.deb7moz1/" \
                -e "s/__DATE__/$(date --rfc-2822)/" \
                -e "s/__CODENAME__/wheezy/" debian/changelog
 
   deb9-mercurial:
     description: "Modern Mercurial for Debian stretch"
     treeherder:
       symbol: Deb9(hg)
     run:
       using: debian-package
       dist: stretch
       tarball:
-        url: https://www.mercurial-scm.org/release/mercurial-4.5.2.tar.gz
-        sha256: a44a9ffd1c9502a4f97298a6bbcb8a79fc8192424c760c67f17b45c12114e390
+        url: https://www.mercurial-scm.org/release/mercurial-4.7.tar.gz
+        sha256: 098cb1437f77fb7f75dc2f008742933c729ec0b63cfa3e4e2f0a8fbc3d0d349f
       pre-build-command: >-
-        cp -r contrib/debian debian &&
+        cp -r contrib/packaging/debian debian &&
         sed -i -e "s/__VERSION__/$(awk -F\" '$2 {print $2}' mercurial/__version__.py)-1.deb9moz1/" \
                -e "s/__DATE__/$(date --rfc-2822)/" \
                -e "s/__CODENAME__/stretch/" debian/changelog
 
   deb7-git:
     description: "Modern git for Debian wheezy"
     treeherder:
       symbol: Deb7(git)
--- a/taskcluster/ci/test/misc.yml
+++ b/taskcluster/ci/test/misc.yml
@@ -28,16 +28,17 @@ geckoview-junit:
     treeherder-symbol: gv-junit
     instance-size: xlarge
     loopback-video: true
     e10s: true
     target: geckoview-androidTest.apk
     max-run-time: 3600
     chunks:
         by-test-platform:
+            android-em-4.3-arm7-api-16-ccov/debug: 4
             android-em-4.3-arm7-api-16/debug: 4
             android-em-4.3-arm7-api-16/opt: 2
             default: 1
     mozharness:
         script: android_emulator_unittest.py
         config:
             by-test-platform:
                 android-em-4.2-x86/opt:
--- a/taskcluster/ci/test/test-platforms.yml
+++ b/taskcluster/ci/test/test-platforms.yml
@@ -317,16 +317,21 @@ macosx64-ccov/debug:
 # android-em test platforms execute on android emulators.
 
 android-em-4.3-arm7-api-16/debug:
     build-platform: android-api-16/debug
     test-sets:
         - android-common-tests
         - android-gradle-tests
 
+android-em-4.3-arm7-api-16-ccov/debug:
+    build-platform: android-api-16-ccov/debug
+    test-sets:
+        - android-ccov-tests
+
 android-em-4.3-arm7-api-16/opt:
     build-platform: android-api-16/opt
     test-sets:
         - android-common-tests
         - android-opt-tests
         - android-gradle-tests
 
 android-em-4.2-x86/opt:
--- a/taskcluster/ci/test/test-sets.yml
+++ b/taskcluster/ci/test/test-sets.yml
@@ -386,16 +386,19 @@ android-x86-kvm-tests:
     # - mochitest-chrome
     - mochitest-clipboard
     - mochitest-gpu
     - mochitest-media
     # - mochitest-webgl1-core
     - reftest
     - test-verify
 
+android-ccov-tests:
+    - geckoview-junit
+
 devtools-tests:
     - mochitest-devtools-chrome
 
 mochitest-headless:
     - mochitest-plain-headless
 
 android-hw-arm7-opt-unittests:
     - mochitest-media
--- a/taskcluster/ci/toolchain/linux.yml
+++ b/taskcluster/ci/toolchain/linux.yml
@@ -449,16 +449,17 @@ linux64-android-gradle-dependencies:
             - 'mobile/android/config/mozconfigs/android-api-16-gradle-dependencies/**'
             - 'mobile/android/config/mozconfigs/common*'
             - 'mobile/android/gradle.configure'
         toolchain-artifact: public/build/android-gradle-dependencies.tar.xz
         toolchain-alias: android-gradle-dependencies
     toolchains:
         # Aliases aren't allowed for toolchains depending on toolchains.
         - linux64-android-sdk-linux-repack
+        - linux64-node
 
 linux64-rust-1.27:
     description: "rust repack"
     treeherder:
         kind: build
         platform: toolchains/opt
         symbol: TL(rust-1.27)
         tier: 1
@@ -489,18 +490,17 @@ linux64-rust-1.28:
     worker:
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
+            '--channel', '1.28.0',
             '--host', 'x86_64-unknown-linux-gnu',
             '--target', 'x86_64-unknown-linux-gnu',
             '--target', 'i686-unknown-linux-gnu',
         ]
         toolchain-alias: linux64-rust
         toolchain-artifact: public/build/rustc.tar.xz
 
 linux64-rust-nightly:
@@ -537,18 +537,17 @@ linux64-rust-macos-1.28:
     worker:
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
+            '--channel', '1.28.0',
             '--host', 'x86_64-unknown-linux-gnu',
             '--target', 'x86_64-unknown-linux-gnu',
             '--target', 'x86_64-apple-darwin',
         ]
         toolchain-alias: linux64-rust-macos
         toolchain-artifact: public/build/rustc.tar.xz
 
 linux64-rust-android-1.28:
@@ -562,18 +561,17 @@ linux64-rust-android-1.28:
     worker:
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
+            '--channel', '1.28.0',
             '--host', 'x86_64-unknown-linux-gnu',
             '--target', 'x86_64-unknown-linux-gnu',
             '--target', 'armv7-linux-androideabi',
             '--target', 'aarch64-linux-android',
             '--target', 'i686-linux-android',
         ]
         toolchain-alias: linux64-rust-android
         toolchain-artifact: public/build/rustc.tar.xz
--- a/taskcluster/ci/toolchain/windows.yml
+++ b/taskcluster/ci/toolchain/windows.yml
@@ -130,18 +130,17 @@ win64-rust-1.28:
         docker-image: {in-tree: toolchain-build}
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
+            '--channel', '1.28.0',
             '--host', 'x86_64-pc-windows-msvc',
             '--target', 'x86_64-pc-windows-msvc',
             '--target', 'i686-pc-windows-msvc',
         ]
         toolchain-alias: win64-rust
         toolchain-artifact: public/build/rustc.tar.bz2
 
 win64-rust-nightly:
@@ -197,18 +196,17 @@ win32-rust-1.28:
         docker-image: {in-tree: toolchain-build}
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
+            '--channel', '1.28.0',
             '--host', 'i686-pc-windows-msvc',
             '--target', 'i686-pc-windows-msvc',
         ]
         toolchain-alias: win32-rust
         toolchain-artifact: public/build/rustc.tar.bz2
 
 mingw32-rust-1.28:
     description: "rust repack"
@@ -222,22 +220,22 @@ mingw32-rust-1.28:
         docker-image: {in-tree: toolchain-build}
         max-run-time: 7200
         env:
             UPLOAD_DIR: artifacts
     run:
         using: toolchain-script
         script: repack_rust.py
         arguments: [
-            # 1.28.0-beta.6
-            '--channel', 'beta-2018-06-30',
-            '--host', 'i686-unknown-linux-gnu',
+            '--channel', '1.28.0',
+            '--host', 'x86_64-unknown-linux-gnu',
+            '--target', 'i686-unknown-linux-gnu',
             '--target', 'i686-pc-windows-gnu',
+            '--target', 'x86_64-pc-windows-gnu',
             '--target', 'x86_64-unknown-linux-gnu',
-            '--target', 'i686-unknown-linux-gnu',
         ]
         toolchain-alias: mingw32-rust
         toolchain-artifact: public/build/rustc.tar.xz
 
 win64-sccache:
     description: "sccache toolchain build"
     treeherder:
         kind: build
--- a/taskcluster/docker/recipes/install-mercurial.sh
+++ b/taskcluster/docker/recipes/install-mercurial.sh
@@ -11,23 +11,23 @@ set -e
 if [ -f /etc/lsb-release ]; then
     # Disabled so linting works on Mac
     # shellcheck disable=SC1091
     . /etc/lsb-release
 
     if [ "${DISTRIB_ID}" = "Ubuntu" ] && [[ "${DISTRIB_RELEASE}" = "16.04" || "${DISTRIB_RELEASE}" = "17.10" || "${DISTRIB_RELEASE}" = "18.04" ]]
     then
         HG_DEB=1
-        HG_DIGEST=e58ecb78fb6856161f8af1b7a50a024f1d6aa6efe50e18666aae0368ee1a44eead2f0c52e5088304b4c0e89dd74a058128e9bd184efab0142275a228aa8e0f45
-        HG_SIZE=193382
-        HG_FILENAME=mercurial_4.5.2_amd64.deb
+        HG_DIGEST=21a5ca8170bdb05527b04cbc93f00ecef39680a2c7b80aa625f9add8b7dba0a7bff0ad58e0ba40b1956ae310f681d04302d033e398eca0e001dce10d74d0dbdc
+        HG_SIZE=250748
+        HG_FILENAME=mercurial_4.7_amd64.deb
 
-        HG_COMMON_DIGEST=b69d94c91ad78a26318e3bbd2f0fda7eb3f3295755a727cde677bf143312c5dfc27ac47e800772d624ea88c6f2576fa2f585c1b8b9930ba83ddc8356661627b8
-        HG_COMMON_SIZE=2141554
-        HG_COMMON_FILENAME=mercurial-common_4.5.2_all.deb
+        HG_COMMON_DIGEST=521e0c150b142d0bbb69fa100b96bac5bee7a108605eea1c484e8544540dcf764efffab2e3a9ad640f56674ad85c1b47647e3d504c96f7567d7d7c21299c2c25
+        HG_COMMON_SIZE=2315590
+        HG_COMMON_FILENAME=mercurial-common_4.7_all.deb
     elif [ "${DISTRIB_ID}" = "Ubuntu" ] && [ "${DISTRIB_RELEASE}" = "12.04" ]
     then
         echo "Ubuntu 12.04 not supported"
         exit 1
     fi
 
     CERT_PATH=/etc/ssl/certs/ca-certificates.crt
 
@@ -101,25 +101,25 @@ tooltool_fetch <<EOF
 ]
 EOF
 
     rpm -i ${HG_FILENAME}
 elif [ -n "${PIP_PATH}" ]; then
 tooltool_fetch <<EOF
 [
   {
-    "size": 5779915,
-    "digest": "f70e40cba72b7955f0ecec9c1f53ffffac26f206188617cb182e22ce4f43dc8b970ce46d12c516ef88480c3fa076a59afcddd736dffb642d8e23befaf45b4941",
+    "size": 6476268,
+    "digest": "a08dfc4e296b5d162097769ab38ab85b7c5de16710bce0b6dce2a39f56cb517455c0ed634f689d07e9bd082fb7641501b7da51963844aee7ab28233cf721dec8",
     "algorithm": "sha512",
-    "filename": "mercurial-4.5.2.tar.gz"
+    "filename": "mercurial-4.7.tar.gz"
   }
 ]
 EOF
 
-   ${PIP_PATH} install mercurial-4.5.2.tar.gz
+   ${PIP_PATH} install mercurial-4.7.tar.gz
 else
     echo "Do not know how to install Mercurial on this OS"
     exit 1
 fi
 
 chmod 644 /usr/local/mercurial/robustcheckout.py
 
 cat >/etc/mercurial/hgrc.d/cacerts.rc <<EOF
--- a/taskcluster/taskgraph/transforms/tests.py
+++ b/taskcluster/taskgraph/transforms/tests.py
@@ -555,16 +555,17 @@ def set_treeherder_machine_platform(conf
         'macosx64/debug': 'osx-10-10/debug',
         'macosx64/opt': 'osx-10-10/opt',
         'win64-asan/opt': 'windows10-64/asan',
         'win32-pgo/opt': 'windows7-32/pgo',
         'win64-pgo/opt': 'windows10-64/pgo',
         # The build names for Android platforms have partially evolved over the
         # years and need to be translated.
         'android-api-16/debug': 'android-em-4-3-armv7-api16/debug',
+        'android-api-16-ccov/debug': 'android-em-4-3-armv7-api16-ccov/debug',
         'android-api-16/opt': 'android-em-4-3-armv7-api16/opt',
         'android-x86/opt': 'android-em-4-2-x86/opt',
         'android-api-16-gradle/opt': 'android-api-16-gradle/opt',
     }
     for test in tests:
         # For most desktop platforms, the above table is not used for "regular"
         # builds, so we'll always pick the test platform here.
         # On macOS though, the regular builds are in the table.  This causes a
@@ -726,16 +727,22 @@ def handle_suite_category(config, tests)
 def enable_code_coverage(config, tests):
     """Enable code coverage for the ccov and jsdcov build-platforms"""
     for test in tests:
         if 'ccov' in test['build-platform']:
             # do not run tests on fuzzing or opt build
             if 'opt' in test['build-platform'] or 'fuzzing' in test['build-platform']:
                 test['run-on-projects'] = []
                 continue
+            # Skip this transform for android code coverage builds.
+            if 'android' in test['build-platform']:
+                test.setdefault('fetches', {}).setdefault('fetch', []).append('grcov-linux-x86_64')
+                test['mozharness'].setdefault('extra-options', []).append('--java-code-coverage')
+                yield test
+                continue
             test['mozharness'].setdefault('extra-options', []).append('--code-coverage')
             test['instance-size'] = 'xlarge'
             # Ensure we always run on the projects defined by the build, unless the test
             # is try only or shouldn't run at all.
             if test['run-on-projects'] not in [[], ['try']]:
                 test['run-on-projects'] = 'built-projects'
 
             # Ensure we don't optimize test suites out.
--- a/testing/mochitest/runjunit.py
+++ b/testing/mochitest/runjunit.py
@@ -46,16 +46,21 @@ class JUnitTestRunner(MochitestDesktop):
         self.device = ADBAndroid(adb=options.adbPath or 'adb',
                                  device=options.deviceSerial,
                                  test_root=options.remoteTestRoot,
                                  verbose=verbose)
         self.options = options
         self.log.debug("options=%s" % vars(options))
         update_mozinfo()
         self.remote_profile = posixpath.join(self.device.test_root, 'junit-profile')
+
+        if self.options.coverage and not self.options.coverage_output_path:
+            raise Exception("--coverage-output-path is required when using --enable-coverage")
+        self.remote_coverage_output_path = posixpath.join(self.device.test_root,
+                                                          'junit-coverage.ec')
         self.server_init()
 
         self.cleanup()
         self.device.clear_logcat()
         self.build_profile()
         self.startServers(
             self.options,
             debuggerInfo=None,
@@ -140,16 +145,20 @@ class JUnitTestRunner(MochitestDesktop):
         shard = self.options.thisChunk
         if shards is not None and shard is not None:
             shard -= 1  # shard index is 0 based
             cmd = cmd + " -e numShards %d -e shardIndex %d" % (shards, shard)
         # test filters: limit run to specific test(s)
         for f in test_filters:
             # filter can be class-name or 'class-name#method-name' (single test)
             cmd = cmd + " -e class %s" % f
+        # enable code coverage reports
+        if self.options.coverage:
+            cmd = cmd + " -e coverage true"
+            cmd = cmd + " -e coverageFile %s" % self.remote_coverage_output_path
         # environment
         env = {}
         env["MOZ_CRASHREPORTER"] = "1"
         env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
         env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
         env["XPCOM_DEBUG_BREAK"] = "stack"
         env["DISABLE_UNSAFE_CPOW_WARNINGS"] = "1"
         env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
@@ -257,16 +266,19 @@ class JUnitTestRunner(MochitestDesktop):
             self.log.info("Failed: %d" % self.fail_count)
             self.log.info("Todo: %d" % self.todo_count)
         finally:
             self.log.suite_end()
 
         if self.check_for_crashes():
             self.fail_count = 1
 
+        if self.options.coverage:
+            self.device.pull(self.remote_coverage_output_path, self.options.coverage_output_path)
+
         return 1 if self.fail_count else 0
 
     def check_for_crashes(self):
         logcat = self.device.get_logcat()
         if logcat:
             if mozcrash.check_for_java_exception(logcat, self.current_full_name):
                 return True
         symbols_path = self.options.symbolsPath
@@ -359,16 +371,27 @@ class JunitArgumentParser(argparse.Argum
                           default=None,
                           help="Total number of chunks to split tests into.")
         self.add_argument("--this-chunk",
                           action="store",
                           type=int,
                           dest="thisChunk",
                           default=None,
                           help="If running tests by chunks, the chunk number to run.")
+        self.add_argument("--enable-coverage",
+                          action="store_true",
+                          dest="coverage",
+                          default=False,
+                          help="Enable code coverage collection.")
+        self.add_argument("--coverage-output-path",
+                          action="store",
+                          type=str,
+                          dest="coverage_output_path",
+                          default=None,
+                          help="If collecting code coverage, save the report file to this path.")
         # Additional options for server.
         self.add_argument("--certificate-path",
                           action="store",
                           type=str,
                           dest="certPath",
                           default=None,
                           help="Path to directory containing certificate store."),
         self.add_argument("--http-port",
new file mode 100644
--- /dev/null
+++ b/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_16_debug_ccov.py
@@ -0,0 +1,15 @@
+config = {
+    'base_name': 'Android armv7 api-16+ %(branch)s debug coverage',
+    'stage_platform': 'android-api-16-debug-ccov',
+    'src_mozconfig': 'mobile/android/config/mozconfigs/android-api-16/debug-ccov',
+    'multi_locale_config_platform': 'android',
+    'debug_build': True,
+    'postflight_build_mach_commands': [
+        ['android',
+         'archive-geckoview',
+        ],
+        ['android',
+         'archive-geckoview-coverage-artifacts',
+        ],
+    ],
+}
--- a/testing/mozharness/external_tools/robustcheckout.py
+++ b/testing/mozharness/external_tools/robustcheckout.py
@@ -44,17 +44,17 @@ try:
     from mercurial import configitems
     configitems.dynamicdefault
 except ImportError:
     configitems = None
 
 # Causes worker to purge caches on process exit and for task to retry.
 EXIT_PURGE_CACHE = 72
 
-testedwith = '3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5'
+testedwith = '3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7'
 minimumhgversion = '3.7'
 
 cmdtable = {}
 
 # TRACKING hg43 Mercurial 4.3 introduced registrar.command as a replacement for
 # cmdutil.command.
 if util.safehasattr(registrar, 'command'):
     command = registrar.command(cmdtable)
@@ -81,16 +81,27 @@ def getvfs():
         return scmutil.vfs
 
 
 def getsparse():
     from mercurial import sparse
     return sparse
 
 
+def supported_hg():
+    '''Returns True if the Mercurial version is supported for robustcheckout'''
+    return util.versiontuple(n=2) in (
+        (4, 3),
+        (4, 4),
+        (4, 5),
+        (4, 6),
+        (4, 7),
+    )
+
+
 if os.name == 'nt':
     import ctypes
 
     # Get a reference to the DeleteFileW function
     # DeleteFileW accepts filenames encoded as a null terminated sequence of
     # wide chars (UTF-16). Python's ctypes.c_wchar_p correctly encodes unicode
     # strings to null terminated UTF-16 strings.
     # However, we receive (byte) strings from mercurial. When these are passed
@@ -223,17 +234,17 @@ def robustcheckout(ui, url, dest, upstre
 
     # Sparse profile support was added in Mercurial 4.3, where it was highly
     # experimental. Because of the fragility of it, we only support sparse
     # profiles on 4.3. When 4.4 is released, we'll need to opt in to sparse
     # support. We /could/ silently fall back to non-sparse when not supported.
     # However, given that sparse has performance implications, we want to fail
     # fast if we can't satisfy the desired checkout request.
     if sparseprofile:
-        if util.versiontuple(n=2) not in ((4, 3), (4, 4), (4, 5)):
+        if not supported_hg():
             raise error.Abort('sparse profile support only available for '
                               'Mercurial versions greater than 4.3 (using %s)' % util.version())
 
         try:
             extensions.find('sparse')
         except KeyError:
             raise error.Abort('sparse extension must be enabled to use '
                               '--sparseprofile')
@@ -570,26 +581,31 @@ def _docheckout(ui, url, dest, upstream,
     # The destination .hg directory should exist. Now make sure we have the
     # wanted revision.
 
     repo = hg.repository(ui, dest)
 
     # We only pull if we are using symbolic names or the requested revision
     # doesn't exist.
     havewantedrev = False
-    if revision and revision in repo:
-        ctx = repo[revision]
+
+    if revision:
+        try:
+            ctx = scmutil.revsingle(repo, revision)
+        except error.RepoLookupError:
+            ctx = None
 
-        if not ctx.hex().startswith(revision):
-            raise error.Abort('--revision argument is ambiguous',
-                              hint='must be the first 12+ characters of a '
-                                   'SHA-1 fragment')
+        if ctx:
+            if not ctx.hex().startswith(revision):
+                raise error.Abort('--revision argument is ambiguous',
+                                  hint='must be the first 12+ characters of a '
+                                       'SHA-1 fragment')
 
-        checkoutrevision = ctx.hex()
-        havewantedrev = True
+            checkoutrevision = ctx.hex()
+            havewantedrev = True
 
     if not havewantedrev:
         ui.write('(pulling to obtain %s)\n' % (revision or branch,))
 
         remote = None
         try:
             remote = hg.peer(repo, {}, url)
             pullrevs = [remote.lookup(revision or branch)]
@@ -630,17 +646,17 @@ def _docheckout(ui, url, dest, upstream,
         purgeext = extensions.find('purge')
 
         # Mercurial 4.3 doesn't purge files outside the sparse checkout.
         # See https://bz.mercurial-scm.org/show_bug.cgi?id=5626. Force
         # purging by monkeypatching the sparse matcher.
         try:
             old_sparse_fn = getattr(repo.dirstate, '_sparsematchfn', None)
             if old_sparse_fn is not None:
-                assert util.versiontuple(n=2) in ((4, 3), (4, 4), (4, 5))
+                assert supported_hg(), 'Mercurial version not supported (must be 4.3+)'
                 repo.dirstate._sparsematchfn = lambda: matchmod.always(repo.root, '')
 
             with timeit('purge'):
                 if purgeext.purge(ui, repo, all=True, abort_on_err=True,
                                   # The function expects all arguments to be
                                   # defined.
                                   **{'print': None,
                                      'print0': None,
--- a/testing/mozharness/mozharness/mozilla/building/buildbase.py
+++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py
@@ -423,16 +423,17 @@ class BuildOptionParser(object):
         'code-coverage-debug': 'builds/releng_sub_%s_configs/%s_code_coverage_debug.py',
         'code-coverage-opt': 'builds/releng_sub_%s_configs/%s_code_coverage_opt.py',
         'source': 'builds/releng_sub_%s_configs/%s_source.py',
         'noopt-debug': 'builds/releng_sub_%s_configs/%s_noopt_debug.py',
         'api-16-gradle-dependencies': 'builds/releng_sub_%s_configs/%s_api_16_gradle_dependencies.py',
         'api-16': 'builds/releng_sub_%s_configs/%s_api_16.py',
         'api-16-artifact': 'builds/releng_sub_%s_configs/%s_api_16_artifact.py',
         'api-16-debug': 'builds/releng_sub_%s_configs/%s_api_16_debug.py',
+        'api-16-debug-ccov': 'builds/releng_sub_%s_configs/%s_api_16_debug_ccov.py',
         'api-16-debug-artifact': 'builds/releng_sub_%s_configs/%s_api_16_debug_artifact.py',
         'api-16-gradle': 'builds/releng_sub_%s_configs/%s_api_16_gradle.py',
         'api-16-gradle-artifact': 'builds/releng_sub_%s_configs/%s_api_16_gradle_artifact.py',
         'api-16-without-google-play-services': 'builds/releng_sub_%s_configs/%s_api_16_without_google_play_services.py',
         'rusttests': 'builds/releng_sub_%s_configs/%s_rusttests.py',
         'rusttests-debug': 'builds/releng_sub_%s_configs/%s_rusttests_debug.py',
         'x86': 'builds/releng_sub_%s_configs/%s_x86.py',
         'x86-artifact': 'builds/releng_sub_%s_configs/%s_x86_artifact.py',
--- a/testing/mozharness/mozharness/mozilla/testing/codecoverage.py
+++ b/testing/mozharness/mozharness/mozilla/testing/codecoverage.py
@@ -40,16 +40,22 @@ code_coverage_config_options = [
       "help": "Whether test run should package and upload code coverage data."
       }],
     [["--jsd-code-coverage"],
      {"action": "store_true",
       "dest": "jsd_code_coverage",
       "default": False,
       "help": "Whether JSDebugger code coverage should be run."
       }],
+    [["--java-code-coverage"],
+     {"action": "store_true",
+      "dest": "java_code_coverage",
+      "default": False,
+      "help": "Whether Java code coverage should be run."
+      }],
 ]
 
 
 class CodeCoverageMixin(SingleTestMixin):
     """
     Mixin for setting GCOV_PREFIX during test execution, packaging up
     the resulting .gcda files and uploading them to blobber.
     """
@@ -84,39 +90,65 @@ class CodeCoverageMixin(SingleTestMixin)
 
     @property
     def jsd_code_coverage_enabled(self):
         try:
             return bool(self.config.get('jsd_code_coverage'))
         except (AttributeError, KeyError, TypeError):
             return False
 
-    @PostScriptAction('download-and-extract')
-    def setup_coverage_tools(self, action, success=None):
-        if not self.code_coverage_enabled:
-            return
+    @property
+    def java_code_coverage_enabled(self):
+        try:
+            return bool(self.config.get('java_code_coverage'))
+        except (AttributeError, KeyError, TypeError):
+            return False
 
+    def _setup_cpp_js_coverage_tools(self):
         if mozinfo.os == 'linux' or mozinfo.os == 'mac':
             self.prefix = '/builds/worker/workspace/build/src/'
             strip_count = self.prefix.count('/')
         elif mozinfo.os == 'win':
             self.prefix = 'z:/build/build/src/'
             # Add 1 as on Windows the path where the compiler tries to write the
             # gcda files has an additional 'obj-firefox' component.
             strip_count = self.prefix.count('/') + 1
         else:
             raise Exception('Unexpected OS: {}'.format(mozinfo.os))
 
         os.environ['GCOV_PREFIX_STRIP'] = str(strip_count)
 
-        # Install grcov on the test machine
-        # Get the path to the build machines gcno files.
-        self.url_to_gcno = self.query_build_dir_url('target.code-coverage-gcno.zip')
-        self.url_to_chrome_map = self.query_build_dir_url('chrome-map.json')
+        # Download the gcno archive from the build machine.
+        url_to_gcno = self.query_build_dir_url('target.code-coverage-gcno.zip')
+        self.download_file(url_to_gcno, parent_dir=self.grcov_dir)
+
+        # Download the chrome-map.json file from the build machine.
+        url_to_chrome_map = self.query_build_dir_url('chrome-map.json')
+        self.download_file(url_to_chrome_map, parent_dir=self.grcov_dir)
+
+    def _setup_java_coverage_tools(self):
+        # Download and extract jacoco-cli from the build task.
+        url_to_jacoco = self.query_build_dir_url('target.jacoco-cli.jar')
+        self.jacoco_jar = os.path.join(tempfile.mkdtemp(), 'target.jacoco-cli.jar')
+        self.download_file(url_to_jacoco, self.jacoco_jar)
 
+        # Download and extract class files from the build task.
+        self.classfiles_dir = tempfile.mkdtemp()
+        url_to_classfiles = self.query_build_dir_url('target.geckoview_classfiles.zip')
+        classfiles_zip_path = os.path.join(self.classfiles_dir, 'target.geckoview_classfiles.zip')
+        self.download_file(url_to_classfiles, classfiles_zip_path)
+        with zipfile.ZipFile(classfiles_zip_path, 'r') as z:
+            z.extractall(self.classfiles_dir)
+        os.remove(classfiles_zip_path)
+
+        # Create the directory where the emulator coverage file will be placed.
+        self.java_coverage_output_path = os.path.join(tempfile.mkdtemp(),
+                                                      'junit-coverage.ec')
+
+    def _download_grcov(self):
         fetches_dir = os.environ.get('MOZ_FETCHES_DIR')
         if fetches_dir and os.path.isfile(os.path.join(fetches_dir, 'grcov')):
             self.grcov_dir = fetches_dir
         else:
             # Create the grcov directory, then download it.
             # TODO: use the fetch-content script to download artifacts.
             self.grcov_dir = tempfile.mkdtemp()
             ARTIFACT_URL = 'https://queue.taskcluster.net/v1/task/{task}/artifacts/{artifact}'
@@ -125,21 +157,28 @@ class CodeCoverageMixin(SingleTestMixin)
                 filename = os.path.basename(artifact)
                 url = ARTIFACT_URL.format(artifact=artifact, task=task)
                 self.download_file(url, parent_dir=self.grcov_dir)
 
                 with tarfile.open(os.path.join(self.grcov_dir, filename), 'r') as tar:
                     tar.extractall(self.grcov_dir)
                 os.remove(os.path.join(self.grcov_dir, filename))
 
-        # Download the gcno archive from the build machine.
-        self.download_file(self.url_to_gcno, parent_dir=self.grcov_dir)
+    @PostScriptAction('download-and-extract')
+    def setup_coverage_tools(self, action, success=None):
+        if not self.code_coverage_enabled and not self.java_code_coverage_enabled:
+            return
 
-        # Download the chrome-map.json file from the build machine.
-        self.download_file(self.url_to_chrome_map, parent_dir=self.grcov_dir)
+        self._download_grcov()
+
+        if self.code_coverage_enabled:
+            self._setup_cpp_js_coverage_tools()
+
+        if self.java_code_coverage_enabled:
+            self._setup_java_coverage_tools()
 
     @PostScriptAction('download-and-extract')
     def find_tests_for_coverage(self, action, success=None):
         """
            For each file modified on this push, determine if the modified file
            is a test, by searching test manifests. Populate self.verify_suites
            with test files, organized by suite.
 
@@ -435,16 +474,59 @@ class CodeCoverageMixin(SingleTestMixin)
 
             # Zip the JSVM coverage data and upload it.
             jsvm_zip_path = os.path.join(dirs['abs_blob_upload_dir'], 'code-coverage-jsvm.zip')
             with zipfile.ZipFile(jsvm_zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
                 z.write(jsvm_output_file)
 
         shutil.rmtree(self.grcov_dir)
 
+    @PostScriptAction('run-tests')
+    def process_java_coverage_data(self, action, success=None):
+        '''
+        Run JaCoCo on the coverage.ec file in order to get a XML report.
+        After that, run grcov on the XML report to get a lcov report.
+        Finally, archive the lcov file and upload it, as process_coverage_data is doing.
+        '''
+        if not self.java_code_coverage_enabled:
+            return
+
+        # If the emulator became unresponsive, the task has failed and we don't
+        # have the coverage report file, so stop running this function and
+        # allow the task to be retried automatically.
+        if not success and not os.path.exists(self.java_coverage_output_path):
+            return
+
+        dirs = self.query_abs_dirs()
+        xml_path = tempfile.mkdtemp()
+        jacoco_command = ['java', '-jar', self.jacoco_jar, 'report',
+                          self.java_coverage_output_path,
+                          '--classfiles', self.classfiles_dir,
+                          '--name', 'geckoview-junit',
+                          '--xml', os.path.join(xml_path, 'geckoview-junit.xml')]
+        self.run_command(jacoco_command, halt_on_failure=True)
+
+        grcov_command = [
+            os.path.join(self.grcov_dir, 'grcov'),
+            '-t', 'lcov',
+            xml_path,
+        ]
+        tmp_output_file, _ = self.get_output_from_command(
+            grcov_command,
+            silent=True,
+            save_tmpfiles=True,
+            return_type='files',
+            throw_exception=True,
+        )
+
+        if not self.ccov_upload_disabled:
+            grcov_zip_path = os.path.join(dirs['abs_blob_upload_dir'], 'code-coverage-grcov.zip')
+            with zipfile.ZipFile(grcov_zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
+                z.write(tmp_output_file, 'grcov_lcov_output.info')
+
 
 def rm_baseline_cov(baseline_coverage, test_coverage):
     '''
     Returns the difference between test_coverage and
     baseline_coverage, such that what is returned
     is the unique coverage for the test in question.
     '''
 
--- a/testing/mozharness/scripts/android_emulator_unittest.py
+++ b/testing/mozharness/scripts/android_emulator_unittest.py
@@ -21,17 +21,20 @@ sys.path.insert(1, os.path.dirname(sys.p
 
 from mozprocess import ProcessHandler
 
 from mozharness.base.log import FATAL
 from mozharness.base.script import BaseScript, PreScriptAction, PostScriptAction
 from mozharness.mozilla.automation import TBPL_RETRY, EXIT_STATUS_DICT
 from mozharness.mozilla.mozbase import MozbaseMixin
 from mozharness.mozilla.testing.testbase import TestingMixin, testing_config_options
-from mozharness.mozilla.testing.codecoverage import CodeCoverageMixin
+from mozharness.mozilla.testing.codecoverage import (
+    CodeCoverageMixin,
+    code_coverage_config_options
+)
 
 
 class AndroidEmulatorTest(TestingMixin, BaseScript, MozbaseMixin, CodeCoverageMixin):
     """
        A mozharness script for Android functional tests (like mochitests and reftests)
        run on an Android emulator. This script starts and manages an Android emulator
        for the duration of the required tests. This is like desktop_unittest.py, but
        for Android emulator test platforms.
@@ -72,17 +75,18 @@ class AndroidEmulatorTest(TestingMixin, 
          }
     ], [
         ["--log-tbpl-level"],
         {"action": "store",
          "dest": "log_tbpl_level",
          "default": "info",
          "help": "Set log level (debug|info|warning|error|critical|fatal)",
          }
-    ]] + copy.deepcopy(testing_config_options)
+    ]] + copy.deepcopy(testing_config_options) + \
+        copy.deepcopy(code_coverage_config_options)
 
     app_name = None
 
     def __init__(self, require_config_file=False):
         super(AndroidEmulatorTest, self).__init__(
             config_options=self.config_options,
             all_actions=['clobber',
                          'setup-avds',
@@ -482,16 +486,20 @@ class AndroidEmulatorTest(TestingMixin, 
         try_options, try_tests = self.try_args(self.test_suite)
         cmd.extend(try_options)
         if not self.verify_enabled and not self.per_test_coverage:
             cmd.extend(self.query_tests_args(
                 self.config["suite_definitions"][self.test_suite].get("tests"),
                 None,
                 try_tests))
 
+        if self.java_code_coverage_enabled:
+            cmd.extend(['--enable-coverage',
+                        '--coverage-output-path', self.java_coverage_output_path])
+
         return cmd
 
     def _get_repo_url(self, path):
         """
            Return a url for a file (typically a tooltool manifest) in this hg repo
            and using this revision (or mozilla-central/default if repo/rev cannot
            be determined).
 
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -1072,24 +1072,22 @@ class HiddenXULWindow {
     // The invisible page is currently wrapped in a XUL window to fix an issue
     // with using the canvas API from a background page (See Bug 1274775).
     let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
     this._windowlessBrowser = windowlessBrowser;
 
     // The windowless browser is a thin wrapper around a docShell that keeps
     // its related resources alive. It implements nsIWebNavigation and
     // forwards its methods to the underlying docShell, but cannot act as a
-    // docShell itself. Calling `getInterface(nsIDocShell)` gives us the
+    // docShell itself.  Getting .docShell gives us the
     // underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
     // access to the webNav methods that are already available on the
     // windowless browser, but contrary to appearances, they are not the same
     // object.
-    this.chromeShell = this._windowlessBrowser
-                           .QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIDocShell)
+    this.chromeShell = this._windowlessBrowser.docShell
                            .QueryInterface(Ci.nsIWebNavigation);
 
     if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
       let attrs = this.chromeShell.getOriginAttributes();
       attrs.privateBrowsingId = 1;
       this.chromeShell.setOriginAttributes(attrs);
     }
 
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -113,19 +113,18 @@ class ContentPage {
     this.browserReady = this._initBrowser();
   }
 
   async _initBrowser() {
     this.windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
 
     let system = Services.scriptSecurityManager.getSystemPrincipal();
 
-    let chromeShell = this.windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
-                                            .getInterface(Ci.nsIDocShell)
-                                            .QueryInterface(Ci.nsIWebNavigation);
+    let chromeShell = this.windowlessBrowser.docShell
+                          .QueryInterface(Ci.nsIWebNavigation);
 
     chromeShell.createAboutBlankContentViewer(system);
     chromeShell.useGlobalHistory = false;
     chromeShell.loadURI("chrome://extensions/content/dummy.xul", 0, null, null, null);
 
     await promiseObserved("chrome-document-global-created",
                           win => win.document == chromeShell.document);
 
--- a/toolkit/components/extensions/WebExtensionPolicy.h
+++ b/toolkit/components/extensions/WebExtensionPolicy.h
@@ -62,20 +62,20 @@ public:
   Result<nsString, nsresult> GetURL(const nsAString& aPath) const;
 
   void RegisterContentScript(WebExtensionContentScript& script,
                              ErrorResult& aRv);
 
   void UnregisterContentScript(const WebExtensionContentScript& script,
                                ErrorResult& aRv);
 
-  bool CanAccessURI(const URLInfo& aURI, bool aExplicit = false) const
+  bool CanAccessURI(const URLInfo& aURI, bool aExplicit = false, bool aCheckRestricted = true) const
   {
-    return (!IsRestrictedURI(aURI) &&
-            mHostPermissions && mHostPermissions->Matches(aURI, aExplicit));
+    return (!aCheckRestricted || !IsRestrictedURI(aURI)) &&
+            mHostPermissions && mHostPermissions->Matches(aURI, aExplicit);
   }
 
   bool IsPathWebAccessible(const nsAString& aPath) const
   {
     return mWebAccessiblePaths.Matches(aPath);
   }
 
   bool HasPermission(const nsAtom* aPermission) const
--- a/toolkit/components/extensions/parent/ext-downloads.js
+++ b/toolkit/components/extensions/parent/ext-downloads.js
@@ -703,19 +703,17 @@ this.downloads = class extends Extension
               path = Services.io.newFileURI(file).spec;
             } else {
               path = OS.Path.basename(download.target.path);
               pathPrefix = "//";
             }
 
             return new Promise((resolve, reject) => {
               let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
-              chromeWebNav
-                .QueryInterface(Ci.nsIInterfaceRequestor)
-                .getInterface(Ci.nsIDocShell)
+              chromeWebNav.docShell
                 .createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
 
               let img = chromeWebNav.document.createElement("img");
               img.width = size;
               img.height = size;
 
               let handleLoad;
               let handleError;
--- a/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_downloads_misc.js
@@ -803,18 +803,17 @@ function loadImage(img, data) {
   return new Promise((resolve) => {
     img.src = data;
     img.onload = resolve;
   });
 }
 
 add_task(async function test_getFileIcon() {
   let webNav = Services.appShell.createWindowlessBrowser(false);
-  let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
 
   let system = Services.scriptSecurityManager.getSystemPrincipal();
   docShell.createAboutBlankContentViewer(system);
 
   let img = webNav.document.createElement("img");
 
   let msg = await runInExtension("download", {url: TXT_URL});
   equal(msg.status, "success", "download() succeeded");
--- a/toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
+++ b/toolkit/components/extensions/test/xpcshell/test_proxy_listener.js
@@ -3,20 +3,20 @@
 ChromeUtils.import("resource://gre/modules/Extension.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gProxyService",
                                    "@mozilla.org/network/protocol-proxy-service;1",
                                    "nsIProtocolProxyService");
 
 const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
 
-function getProxyInfo() {
+function getProxyInfo(url = "http://www.mozilla.org/") {
   return new Promise((resolve, reject) => {
     let channel = NetUtil.newChannel({
-      uri: "http://www.mozilla.org/",
+      uri: url,
       loadUsingSystemPrincipal: true,
     });
 
     gProxyService.asyncResolve(channel, 0, {
       onProxyAvailable(req, uri, pi, status) {
         resolve(pi);
       },
     });
@@ -171,22 +171,23 @@ async function getExtension(expectedProx
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   await extension.startup();
   await extension.awaitMessage("ready");
   return extension;
 }
 
 add_task(async function test_passthrough() {
   let ext1 = await getExtension(null);
-  let ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "http"});
+  let ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "https"});
 
-  let proxyInfo = await getProxyInfo();
+  // Also use a restricted url to test the ability to proxy those.
+  let proxyInfo = await getProxyInfo("https://addons.mozilla.org/");
 
   equal(proxyInfo.host, "1.2.3.4", `second extension won`);
   equal(proxyInfo.port, "8888", `second extension won`);
-  equal(proxyInfo.type, "http", `second extension won`);
+  equal(proxyInfo.type, "https", `second extension won`);
 
   await ext2.unload();
 
   proxyInfo = await getProxyInfo();
   equal(proxyInfo, null, `expected no proxy`);
   await ext1.unload();
 });
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -12,72 +12,80 @@ skip-if = os == "android" # Android does
 [test_ext_background_runtime_connect_params.js]
 [test_ext_background_sub_windows.js]
 [test_ext_background_teardown.js]
 [test_ext_background_telemetry.js]
 [test_ext_background_window_properties.js]
 skip-if = os == "android"
 [test_ext_browserSettings.js]
 [test_ext_browserSettings_homepage.js]
-skip-if = os == "android"
+skip-if = appname == "thunderbird" || os == "android"
 [test_ext_cookieBehaviors.js]
 [test_ext_cookies_samesite.js]
 [test_ext_content_security_policy.js]
 [test_ext_contentscript_api_injection.js]
 [test_ext_contentscript_async_loading.js]
 skip-if = os == 'android' && debug # The generated script takes too long to load on Android debug
 [test_ext_contentscript_context.js]
 [test_ext_contentscript_create_iframe.js]
 [test_ext_contentscript_css.js]
 [test_ext_contentscript_exporthelpers.js]
 [test_ext_contentscript_restrictSchemes.js]
 [test_ext_contentscript_teardown.js]
 [test_ext_contextual_identities.js]
-skip-if = os == "android" # Containers are not exposed to android.
+skip-if = appname == "thunderbird" || os == "android" # Containers are not exposed to android.
 [test_ext_debugging_utils.js]
 [test_ext_dns.js]
 [test_ext_downloads.js]
 [test_ext_downloads_download.js]
+fail-if = appname == "thunderbird"
 skip-if = os == "android"
 [test_ext_downloads_misc.js]
+fail-if = appname == "thunderbird"
 skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870
 [test_ext_downloads_private.js]
-skip-if = os == "android"
+skip-if = appname == "thunderbird" || os == "android"
 [test_ext_downloads_search.js]
+fail-if = appname == "thunderbird"
 skip-if = os == "android"
 [test_ext_error_location.js]
 [test_ext_eventpage_warning.js]
 [test_ext_experiments.js]
+fail-if = appname == "thunderbird"
 [test_ext_extension.js]
 [test_ext_extensionPreferencesManager.js]
 [test_ext_extensionSettingsStore.js]
 [test_ext_extension_content_telemetry.js]
 skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
 [test_ext_extension_startup_telemetry.js]
 [test_ext_geturl.js]
 [test_ext_idle.js]
 [test_ext_legacy_extension_context.js]
 [test_ext_legacy_extension_embedding.js]
 [test_ext_localStorage.js]
 [test_ext_management.js]
 skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
 [test_ext_management_uninstall_self.js]
 [test_ext_messaging_startup.js]
+skip-if = appname == "thunderbird"
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_permission_xhr.js]
 [test_ext_persistent_events.js]
 [test_ext_privacy.js]
+skip-if = appname == "thunderbird"
 [test_ext_privacy_disable.js]
+skip-if = appname == "thunderbird"
 [test_ext_privacy_update.js]
 [test_ext_proxy_auth.js]
 [test_ext_proxy_config.js]
+skip-if = appname == "thunderbird"
 [test_ext_proxy_onauthrequired.js]
 [test_ext_proxy_settings.js]
-skip-if = os == "android" # proxy settings are not supported on android
+skip-if = appname == "thunderbird" || os == "android" # proxy settings are not supported on android
 [test_ext_proxy_socks.js]
 [test_ext_proxy_speculative.js]
 [test_ext_proxy_startup.js]
 [test_ext_redirects.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getBrowserInfo.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_id.js]
@@ -98,41 +106,43 @@ skip-if = os == "android"
 [test_ext_startup_perf.js]
 [test_ext_storage.js]
 [test_ext_storage_idb_data_migration.js]
 [test_ext_storage_content.js]
 [test_ext_storage_managed.js]
 skip-if = os == "android"
 [test_ext_storage_sync.js]
 head = head.js head_sync.js
-skip-if = os == "android"
+skip-if = appname == "thunderbird" || os == "android"
 [test_ext_storage_sync_crypto.js]
-skip-if = os == "android"
+skip-if = appname == "thunderbird" || os == "android"
 [test_ext_storage_tab.js]
 [test_ext_storage_telemetry.js]
 skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
 [test_ext_tab_teardown.js]
 skip-if = os == 'android' # Bug 1258975 on android.
 [test_ext_trustworthy_origin.js]
 [test_ext_topSites.js]
 skip-if = os == "android"
 [test_ext_unload_frame.js]
 skip-if = true # Too frequent intermittent failures
 [test_ext_webRequest_auth.js]
 [test_ext_webRequest_filterResponseData.js]
 [test_ext_webRequest_permission.js]
 [test_ext_webRequest_responseBody.js]
 [test_ext_webRequest_set_cookie.js]
+skip-if = appname == "thunderbird"
 [test_ext_webRequest_startup.js]
 [test_ext_webRequest_suspend.js]
 [test_ext_webRequest_webSocket.js]
+skip-if = appname == "thunderbird"
 [test_ext_xhr_capabilities.js]
 [test_native_manifests.js]
 subprocess = true
 skip-if = os == "android"
 [test_ext_permissions.js]
-skip-if = os == "android" # Bug 1350559
+skip-if = appname == "thunderbird" || os == "android" # Bug 1350559
 [test_proxy_listener.js]
 [test_proxy_scripts.js]
 [test_proxy_scripts_results.js]
 [test_ext_brokenlinks.js]
 [test_ext_performance_counters.js]
-skip-if = os == "android"
+skip-if = appname == "thunderbird" || os == "android"
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,12 +1,11 @@
 [DEFAULT]
 head = head.js head_telemetry.js head_storage.js
 firefox-appdir = browser
-skip-if = appname == "thunderbird"
 dupe-manifest =
 support-files =
   data/**
   head_sync.js
   xpcshell-content.ini
 tags = webextensions in-process-webextensions
 
 # This file contains tests which are not affected by multi-process
--- a/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
+++ b/toolkit/components/extensions/webrequest/ChannelWrapper.cpp
@@ -516,23 +516,24 @@ ChannelWrapper::Matches(const dom::MozRe
   }
 
   auto& urlInfo = FinalURLInfo();
   if (aFilter.mUrls && !aFilter.mUrls->Matches(urlInfo)) {
     return false;
   }
 
   if (aExtension) {
-    if (!aExtension->CanAccessURI(urlInfo)) {
+    bool isProxy = aOptions.mIsProxy && aExtension->HasPermission(nsGkAtoms::proxy);
+    // Proxies are allowed access to all urls, including restricted urls.
+    if (!aExtension->CanAccessURI(urlInfo, false, !isProxy)) {
       return false;
     }
 
     // If this isn't the proxy phase of the request, check that the extension
     // has origin permissions for origin that originated the request.
-    bool isProxy = aOptions.mIsProxy && aExtension->HasPermission(nsGkAtoms::proxy);
     if (!isProxy) {
       if (IsSystemLoad()) {
         return false;
       }
 
       if (auto origin = DocumentURLInfo()) {
         nsAutoCString baseURL;
         aExtension->GetBaseURL(baseURL);
--- a/toolkit/components/reader/AboutReader.jsm
+++ b/toolkit/components/reader/AboutReader.jsm
@@ -207,16 +207,18 @@ AboutReader.prototype = {
           let btn = this._doc.createElement("button");
           btn.dataset.buttonid = message.data.id;
           btn.className = "button " + message.data.id;
           btn.style.backgroundImage = "url('" + message.data.image + "')";
           if (message.data.title)
             btn.title = message.data.title;
           if (message.data.text)
             btn.textContent = message.data.text;
+          if (message.data.width && message.data.height)
+            btn.style.backgroundSize = `${message.data.width}px ${message.data.height}px`;
           let tb = this._toolbarElement;
           tb.appendChild(btn);
           this._setupButton(message.data.id, button => {
             this._mm.sendAsyncMessage("Reader:Clicked-" + button.dataset.buttonid, { article: this._article });
           });
         }
         break;
       }
--- a/toolkit/content/tests/chrome/bug304188_window.xul
+++ b/toolkit/content/tests/chrome/bug304188_window.xul
@@ -54,19 +54,17 @@ find-menu appears in editor element whic
       });
       gBrowser.loadURI("data:text/html;charset=utf-8,some%20random%20text");
       await promise;
       await onDocumentLoaded();
     }
 
     async function onDocumentLoaded() {
       await ContentTask.spawn(gBrowser, null, async function() {
-        var edsession = content.docShell
-                               .QueryInterface(Ci.nsIInterfaceRequestor)
-                               .getInterface(Ci.nsIEditingSession);
+        var edsession = content.docShell.editingSession;
         edsession.makeWindowEditable(content, "html", false, true, false);
         content.focus();
       });
       
       await enterStringIntoEditor("'");
       await enterStringIntoEditor("/");
 
       ok(gFindBar.hidden,
--- a/toolkit/content/widgets/editor.xml
+++ b/toolkit/content/widgets/editor.xml
@@ -126,17 +126,17 @@
       <property name="webBrowserFind"
                 readonly="true"
                 onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebBrowserFind);"/>
       <property name="markupDocumentViewer"
                 readonly="true"
                 onget="return this.docShell.contentViewer;"/>
       <property name="editingSession"
                 readonly="true"
-                onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIEditingSession);"/>
+                onget="return this.docShell.editingSession"/>
       <property name="commandManager"
                 readonly="true"
                 onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsICommandManager);"/>
       <property name="fullZoom"
                 onget="return this.markupDocumentViewer.fullZoom;"
                 onset="this.markupDocumentViewer.fullZoom = val;"/>
       <property name="textZoom"
                 onget="return this.markupDocumentViewer.textZoom;"
--- a/toolkit/modules/HiddenFrame.jsm
+++ b/toolkit/modules/HiddenFrame.jsm
@@ -99,14 +99,14 @@ HiddenFrame.prototype = {
         this._listener = null;
         this._webProgress = null;
         // Get the window reference via the document.
         this._frame = this._browser.document.ownerGlobal;
         this._deferred.resolve(this._frame);
       }
     };
     this._webProgress.addProgressListener(this._listener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
-    let docShell = this._browser.getInterface(Ci.nsIDocShell);
+    let docShell = this._browser.docShell;
     docShell.createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
     docShell.useGlobalHistory = false;
     this._browser.loadURI(XUL_PAGE, 0, null, null, null);
   }
 };
--- a/toolkit/modules/InlineSpellChecker.jsm
+++ b/toolkit/modules/InlineSpellChecker.jsm
@@ -414,19 +414,17 @@ var SpellCheckHelper = {
       }
     }
 
     if (!(flags & this.SPELLCHECKABLE)) {
       var win = element.ownerGlobal;
       if (win) {
         var isSpellcheckable = false;
         try {
-          var editingSession = win.docShell
-                                  .QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIEditingSession);
+          var editingSession = win.docShell.editingSession;
           if (editingSession.windowIsEditable(win) &&
               this.getComputedStyle(element, "-moz-user-modify") == "read-write") {
             isSpellcheckable = true;
           }
         } catch (ex) {
           // If someone built with composer disabled, we can't get an editing session.
         }
 
--- a/toolkit/modules/InlineSpellCheckerContent.jsm
+++ b/toolkit/modules/InlineSpellCheckerContent.jsm
@@ -16,19 +16,17 @@ var InlineSpellCheckerContent = {
 
   initContextMenu(event, editFlags, messageManager) {
     this._manager = messageManager;
 
     let spellChecker;
     if (!(editFlags & (SpellCheckHelper.TEXTAREA | SpellCheckHelper.INPUT))) {
       // Get the editor off the window.
       let win = event.target.ownerGlobal;
-      let editingSession = win.docShell
-                              .QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIEditingSession);
+      let editingSession = win.docShell.editingSession;
       spellChecker = this._spellChecker =
         new InlineSpellChecker(editingSession.getEditorForWindow(win));
     } else {
       // Use the element's editor.
       spellChecker = this._spellChecker =
         new InlineSpellChecker(event.target.editor);
     }
 
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -13,16 +13,36 @@ XPCOMUtils.defineLazyModuleGetters(this,
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
   Services: "resource://gre/modules/Services.jsm",
   ServiceRequest: "resource://gre/modules/ServiceRequest.jsm",
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Preferences: "resource://gre/modules/Preferences.jsm",
 });
 
+// The current platform as specified in the AMO API:
+// http://addons-server.readthedocs.io/en/latest/topics/api/addons.html#addon-detail-platform
+XPCOMUtils.defineLazyGetter(this, "PLATFORM", () => {
+  let platform = Services.appinfo.OS;
+  switch (platform) {
+    case "Darwin":
+      return "mac";
+
+    case "Linux":
+      return "linux";
+
+    case "Android":
+      return "android";
+
+    case "WINNT":
+      return "windows";
+  }
+  return platform;
+});
+
 var EXPORTED_SYMBOLS = [ "AddonRepository" ];
 
 const PREF_GETADDONS_CACHE_ENABLED       = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_TYPES         = "extensions.getAddons.cache.types";
 const PREF_GETADDONS_CACHE_ID_ENABLED    = "extensions.%ID%.getAddons.cache.enabled";
 const PREF_GETADDONS_BROWSEADDONS        = "extensions.getAddons.browseAddons";
 const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
 const PREF_COMPAT_OVERRIDES              = "extensions.getAddons.compatOverides.url";
@@ -577,17 +597,17 @@ var AddonRepository = {
   _parseAddon(aEntry) {
     let addon = new AddonSearchResult(aEntry.guid);
 
     addon.name = aEntry.name;
     if (typeof aEntry.current_version == "object") {
       addon.version = String(aEntry.current_version.version);
       if (Array.isArray(aEntry.current_version.files)) {
         for (let file of aEntry.current_version.files) {
-          if (file.platform == "all" || file.platform == Services.appinfo.OS.toLowerCase()) {
+          if (file.platform == "all" || file.platform == PLATFORM) {
             if (file.url) {
               addon.sourceURI = NetUtil.newURI(file.url);
             }
             break;
           }
         }
       }
     }
--- a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.json
@@ -55,16 +55,31 @@
             },
             "ratings_url": "http://example.com/review1.html",
             "support_url": "http://example.com/support1.html",
             "contributions_url": "http://example.com/contribution1.html",
             "weekly_downloads": 3333,
             "last_updated": "2010-02-01T14:04:05Z"
         },
         {
+            "name": "PASS",
+            "type": "extension",
+            "guid": "test2@tests.mozilla.org",
+            "current_version": {
+                "version": "2.0",
+                "files": [
+                    {
+                        "platform": "XPCShell",
+                        "url": "http://example.com/addons/bleah.xpi",
+                        "size": 1000
+                    }
+                ]
+            }
+        },
+        {
             "name": "FAIL",
             "type": "extension",
             "guid": "notRequested@tests.mozilla.org",
             "current_version": {
                 "version": "1.3",
                 "files": [
                     {
                         "platform": "all",
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -119,32 +119,40 @@ var GET_RESULTS = [{
   contributionURL:        BASE_URL + "/contribution1.html",
   averageRating:          4,
   reviewCount:            1111,
   reviewURL:              BASE_URL + "/review1.html",
   weeklyDownloads:        3333,
   sourceURI:              BASE_URL + INSTALL_URL2,
   updateDate:             new Date(1265033045000),
 }, {
+  id:                     "test2@tests.mozilla.org",
+  type:                   "extension",
+  version:                "2.0",
+  icons:                  {},
+  sourceURI:              "http://example.com/addons/bleah.xpi",
+}, {
   id:                     "test_AddonRepository_1@tests.mozilla.org",
   type:                   "theme",
   version:                "1.4",
   icons:                  {}
 }];
 
 // Values for testing AddonRepository.getAddonsByIDs()
 var GET_TEST = {
   preference:       PREF_GETADDONS_BYIDS,
   preferenceValue:  BASE_URL + "/%OS%/%VERSION%/%IDS%",
   failedIDs:      ["test1@tests.mozilla.org"],
   failedURL:        "/XPCShell/1/test1%40tests.mozilla.org",
   successfulIDs:  ["test1@tests.mozilla.org",
-                     "{00000000-1111-2222-3333-444444444444}",
-                     "test_AddonRepository_1@tests.mozilla.org"],
+                   "test2@tests.mozilla.org",
+                   "{00000000-1111-2222-3333-444444444444}",
+                   "test_AddonRepository_1@tests.mozilla.org"],
   successfulURL:    "/XPCShell/1/test1%40tests.mozilla.org%2C" +
+                    "test2%40tests.mozilla.org%2C" +
                     "%7B00000000-1111-2222-3333-444444444444%7D%2C" +
                     "test_AddonRepository_1%40tests.mozilla.org"
 };
 
 // Test that actual results and expected results are equal
 function check_results(aActualAddons, aExpectedAddons) {
   do_check_addons(aActualAddons, aExpectedAddons, ADDON_PROPERTIES);
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
@@ -39,18 +39,16 @@ const manifestSample = {
 };
 
 async function tearDownAddon(addon) {
   await addon.uninstall();
   await promiseShutdownManager();
 }
 
 add_task(async function test_reloading_a_temp_addon() {
-  if (AppConstants.MOZ_APP_NAME == "thunderbird")
-    return;
   await promiseRestartManager();
   let xpi = AddonTestUtils.createTempXPIFile(ADDONS.webextension_1);
   const addon = await AddonManager.installTemporaryAddon(xpi);
 
   var receivedOnUninstalled = false;
   var receivedOnUninstalling = false;
   var receivedOnInstalled = false;
   var receivedOnInstalling = false;
--- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -758,18 +758,17 @@ add_task(async function test_replace_per
 
   Monitor.checkNotInstalled(ID);
   Monitor.checkNotStarted(ID);
 
   await promiseRestartManager();
 });
 
 // Tests that XPIs with a .zip extension work when loaded temporarily.
-add_task({ skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
-         async function test_zip_extension() {
+add_task(async function test_zip_extension() {
   let xpi = createTempWebExtensionFile({
     background() {
       /* globals browser */
       browser.test.sendMessage("started", "Hello.");
     },
   });
   xpi.moveTo(null, xpi.leafName.replace(/\.xpi$/, ".zip"));
 
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -79,28 +79,26 @@ tags = blocklist
 [test_compatoverrides.js]
 [test_corrupt.js]
 [test_corruptfile.js]
 [test_crash_annotation_quoting.js]
 [test_db_path.js]
 head =
 [test_delay_update.js]
 [test_delay_update_webextension.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_dependencies.js]
 [test_dictionary.js]
 [test_dictionary_webextension.js]
 [test_distribution.js]
 [test_duplicateplugins.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_error.js]
 [test_ext_management.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_general.js]
 [test_getresource.js]
 [test_gfxBlacklist_Device.js]
 tags = blocklist
 [test_gfxBlacklist_DriverNew.js]
 tags = blocklist
 [test_gfxBlacklist_Equal_DriverNew.js]
@@ -217,17 +215,16 @@ skip-if = os == "mac" && debug
 [test_system_repository.js]
 [test_system_reset.js]
 [test_system_update_blank.js]
 [test_system_update_checkSizeHash.js]
 [test_system_update_custom.js]
 [test_system_update_empty.js]
 skip-if = true # Failing intermittently due to a race condition in the test, see bug 1348981
 [test_system_update_enterprisepolicy.js]
-skip-if = appname == "thunderbird"
 [test_system_update_fail.js]
 [test_system_update_newset.js]
 [test_system_update_overlapping.js]
 [test_system_update_upgrades.js]
 [test_temporary.js]
 tags = webextensions
 [test_theme_update.js]
 [test_trash_directory.js]
@@ -253,30 +250,23 @@ skip-if = os == "android"
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_upgrade.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses global XCurProcD dir.
 [test_upgrade_incompatible.js]
 [test_webextension.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_embedded.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_events.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_icons.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_install.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_install_syntax_error.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_langpack.js]
-skip-if = appname == "thunderbird"
 tags = webextensions
 [test_webextension_theme.js]
 tags = webextensions
--- a/toolkit/system/windowsDHCPClient/DHCPUtils.cpp
+++ b/toolkit/system/windowsDHCPClient/DHCPUtils.cpp
@@ -1,17 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "DHCPUtils.h"
 #include <vector>
-#include "mozilla\Logging.h"
+#include "mozilla/Logging.h"
 #include "nsString.h"
 
 
 #define MOZ_WORKING_BUFFER_SIZE_NETWORK_ADAPTERS 15000
 #define MOZ_WORKING_BUFFER_SIZE_DHCP_PARAMS 1000
 #define MOZ_MAX_TRIES 3
 namespace mozilla {
 namespace toolkit {
@@ -250,9 +250,9 @@ RetrieveOption(
       rv = NS_ERROR_FAILURE;
   }
   return rv;
 }
 
 } // namespace windowsDHCPClient
 } // namespace system
 } // namespace toolkit
-} // namespace mozilla
\ No newline at end of file
+} // namespace mozilla
--- a/toolkit/system/windowsDHCPClient/WindowsNetworkFunctionsWrapper.h
+++ b/toolkit/system/windowsDHCPClient/WindowsNetworkFunctionsWrapper.h
@@ -2,17 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef mozilla_toolkit_system_windowsDHCPClient_windowsNetworkFunctionsWrapper_h
 #define mozilla_toolkit_system_windowsDHCPClient_windowsNetworkFunctionsWrapper_h
 
-#include <Winsock2.h> // there is a compilation error if Winsock.h is not
+#include <winsock2.h> // there is a compilation error if Winsock.h is not
                       // declared before dhcpcsdk.h
 #include <dhcpcsdk.h>
 #include <iphlpapi.h>
 
 #include "nsISupports.h"
 
 // Thin wrapper around low-level network functions needed for DHCP querying for web proxy
 namespace mozilla {
@@ -52,9 +52,9 @@ class WindowsNetworkFunctionsWrapper : n
     ~WindowsNetworkFunctionsWrapper(){};
 
 };
 
 } // namespace windowsDHCPClient
 } // namespace system
 } // namespace toolkit
 } // namespace mozilla
-#endif //mozilla_toolkit_system_windowsDHCPClient_windowsNetworkFunctionsWrapper_h
\ No newline at end of file
+#endif //mozilla_toolkit_system_windowsDHCPClient_windowsNetworkFunctionsWrapper_h
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -268,16 +268,18 @@ body:not(.loaded) .toolbar:-moz-locale-d
 .toolbar .button {
   width: 40px;
   background-position: center;
   margin-right: -1px;
   border-top: 0;
   border-left: 0;
   border-right: 1px solid #b5b5b5;
   border-bottom: 1px solid #c1c1c1;
+  -moz-context-properties: fill;
+  fill: #808080;
 }
 
 .button[hidden] {
   display: none;
 }
 
 .dropdown {
   text-align: center;
@@ -496,18 +498,16 @@ body:not(.loaded) .toolbar:-moz-locale-d
   background-repeat: no-repeat;
   background-position: center;
 }
 
 /*======= Toolbar icons =======*/
 
 .close-button {
   background-image: url("chrome://global/skin/reader/RM-Close-24x24.svg");
-  -moz-context-properties: fill;
-  fill: #808080;
   height: 68px;
   background-position: center 8px;
 }
 
 .close-button:hover {
   fill: #fff;
   background-color: #d94141;
   border-bottom: 1px solid #d94141;
--- a/toolkit/themes/shared/reader/RM-Type-Controls-24x24.svg
+++ b/toolkit/themes/shared/reader/RM-Type-Controls-24x24.svg
@@ -1,7 +1,7 @@
 <?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" width="24" height="24" viewBox="0 0 24 24">
-  <path fill="#737373" d="M10.87,18.989h2.144L8.3,3.991H5.724l-4.739,15H3.044l1.115-4.171h5.6ZM4.652,12.91L6.968,5.69l2.294,7.22H4.652ZM22.1,16.515v-5.06c0-2.31-.984-3.713-3.65-3.713a10.236,10.236,0,0,0-3.7.756L15.116,9.9A9.9,9.9,0,0,1,18.1,9.317c1.533,0,1.958.627,1.958,2.223v0.975h-1.35c-3.086,0-4.871,1.125-4.871,3.5a3.217,3.217,0,0,0,3.527,3.338,3.205,3.205,0,0,0,2.945-1.659,2.573,2.573,0,0,0,2.436,1.659l0.441-1.344A1.408,1.408,0,0,1,22.1,16.515ZM17.8,17.9a1.744,1.744,0,0,1-1.911-1.995c0-1.512,1.029-2.111,3.065-2.111h1.1V16.18C19.426,17.334,18.938,17.9,17.8,17.9Z"/>
+  <path fill="context-fill" d="M10.87,18.989h2.144L8.3,3.991H5.724l-4.739,15H3.044l1.115-4.171h5.6ZM4.652,12.91L6.968,5.69l2.294,7.22H4.652ZM22.1,16.515v-5.06c0-2.31-.984-3.713-3.65-3.713a10.236,10.236,0,0,0-3.7.756L15.116,9.9A9.9,9.9,0,0,1,18.1,9.317c1.533,0,1.958.627,1.958,2.223v0.975h-1.35c-3.086,0-4.871,1.125-4.871,3.5a3.217,3.217,0,0,0,3.527,3.338,3.205,3.205,0,0,0,2.945-1.659,2.573,2.573,0,0,0,2.436,1.659l0.441-1.344A1.408,1.408,0,0,1,22.1,16.515ZM17.8,17.9a1.744,1.744,0,0,1-1.911-1.995c0-1.512,1.029-2.111,3.065-2.111h1.1V16.18C19.426,17.334,18.938,17.9,17.8,17.9Z"/>
 </svg>
--- a/widget/headless/tests/test_headless.js
+++ b/widget/headless/tests/test_headless.js
@@ -13,35 +13,32 @@ const BASE = `${ROOT}/`;
 const HEADLESS_URL = `${BASE}/headless.html`;
 const HEADLESS_BUTTON_URL = `${BASE}/headless_button.html`;
 registerCleanupFunction(() => { server.stop(() => {})});
 
 // Refrences to the progress listeners to keep them from being gc'ed
 // before they are called.
 const progressListeners = new Map();
 
-function loadContentWindow(webNavigation, uri) {
+function loadContentWindow(windowlessBrowser, uri) {
   return new Promise((resolve, reject) => {
-    webNavigation.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
-    let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
-                  .getInterface(Ci.nsIDocShell);
+    windowlessBrowser.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
+    let docShell = windowlessBrowser.docShell;
     let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsIWebProgress);
     let progressListener = {
       onLocationChange: function (progress, request, location, flags) {
         // Ignore inner-frame events
         if (progress != webProgress) {
           return;
         }
         // Ignore events that don't change the document
         if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
           return;
         }
-        let docShell = webNavigation.QueryInterface(Ci.nsIInterfaceRequestor)
-                       .getInterface(Ci.nsIDocShell);
         let contentWindow = docShell.domWindow;
         webProgress.removeProgressListener(progressListener);
         progressListeners.delete(progressListener);
         contentWindow.addEventListener("load", (event) => {
           resolve(contentWindow);
         }, { once: true });
       },
       QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener",
@@ -50,18 +47,17 @@ function loadContentWindow(webNavigation
     progressListeners.set(progressListener, progressListener);
     webProgress.addProgressListener(progressListener,
                                     Ci.nsIWebProgress.NOTIFY_LOCATION);
   });
 }
 
 add_task(async function test_snapshot() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
   const contentWidth = 400;
   const contentHeight = 300;
   // Verify dimensions.
   contentWindow.resizeTo(contentWidth, contentHeight);
   equal(contentWindow.innerWidth, contentWidth);
   equal(contentWindow.innerHeight, contentHeight);
 
   // Snapshot the test page.
@@ -88,23 +84,23 @@ add_task(async function test_snapshot() 
   for (let i = 0; i < imageData.length; i += 4) {
     if (imageData[i + 2] === 255) {
       found = true;
       break;
     }
   }
   ok(found, "Found blue text on page.");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 add_task(async function test_snapshot_widget_layers() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
   const contentWidth = 1;
   const contentHeight = 2;
   // Verify dimensions.
   contentWindow.resizeTo(contentWidth, contentHeight);
   equal(contentWindow.innerWidth, contentWidth);
   equal(contentWindow.innerHeight, contentHeight);
 
   // Snapshot the test page.
@@ -120,49 +116,49 @@ add_task(async function test_snapshot_wi
     0,
     width,
     height,
     'rgb(255, 255, 255)',
     context.DRAWWINDOW_DRAW_CARET | context.DRAWWINDOW_DRAW_VIEW | context.DRAWWINDOW_USE_WIDGET_LAYERS
   );
   ok(true, "Snapshot with widget layers didn't crash.");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 // Ensure keydown events are triggered on the windowless browser.
 add_task(async function test_keydown() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_URL);
 
   let keydown = new Promise((resolve) => {
     contentWindow.addEventListener("keydown", () => {
       resolve();
     }, { once: true });
   })
 
   let tip = Cc["@mozilla.org/text-input-processor;1"]
             .createInstance(Ci.nsITextInputProcessor);
   let begun = tip.beginInputTransactionForTests(contentWindow);
   ok(begun, "nsITextInputProcessor.beginInputTransactionForTests() should succeed");
   tip.keydown(new contentWindow.KeyboardEvent("", {key: "a", code: "KeyA", keyCode: contentWindow.KeyboardEvent.DOM_VK_A}));
 
   await keydown;
   ok(true, "Send keydown didn't crash");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
 
 // Test dragging the mouse on a button to ensure the creation of the drag
 // service doesn't crash in headless.
 add_task(async function test_mouse_drag() {
   let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
-  let webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
-  let contentWindow = await loadContentWindow(webNavigation, HEADLESS_BUTTON_URL);
+  // nsIWindowlessBrowser inherits from nsIWebNavigation.
+  let contentWindow = await loadContentWindow(windowlessBrowser, HEADLESS_BUTTON_URL);
   contentWindow.resizeTo(400, 400);
 
   let target = contentWindow.document.getElementById('btn');
   let rect = target.getBoundingClientRect();
   let left = rect.left;
   let top = rect.top;
 
   let utils = contentWindow.windowUtils;
@@ -170,10 +166,10 @@ add_task(async function test_mouse_drag(
   utils.sendMouseEvent("mousemove", left, top, 0, 1, 0, false, 0, 0);
   // Wait for a turn of the event loop since the synthetic mouse event
   // that creates the drag service is processed during the refresh driver.
   await new Promise((r) => {executeSoon(r)});
   utils.sendMouseEvent("mouseup", left, top, 0, 1, 0, false, 0, 0);
 
   ok(true, "Send mouse event didn't crash");
 
-  webNavigation.close();
+  windowlessBrowser.close();
 });
--- a/xpcom/base/MemoryInfo.cpp
+++ b/xpcom/base/MemoryInfo.cpp
@@ -3,17 +3,17 @@
 /* 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/. */
 
 #include "mozilla/MemoryInfo.h"
 
 #include "mozilla/DebugOnly.h"
 
-#include <WinBase.h>
+#include <winbase.h>
 
 namespace mozilla {
 
 /* static */ MemoryInfo
 MemoryInfo::Get(const void* aPtr, size_t aSize)
 {
   MemoryInfo result;
 
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -59,17 +59,17 @@
 #include <sys/resource.h>
 #include <sched.h>
 #include <stdio.h>
 #endif
 
 #ifdef XP_WIN
 #include "mozilla/DynamicallyLinkedFunctionPtr.h"
 
-#include <Winbase.h>
+#include <winbase.h>
 
 using GetCurrentThreadStackLimitsFn = void (WINAPI*)(
   PULONG_PTR LowLimit, PULONG_PTR HighLimit);
 #endif
 
 #define HAVE_UALARM _BSD_SOURCE || (_XOPEN_SOURCE >= 500 ||                 \
                       _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) &&           \
                       !(_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700)
--- a/xpfe/appshell/nsAppShellService.cpp
+++ b/xpfe/appshell/nsAppShellService.cpp
@@ -447,35 +447,20 @@ public:
     mBrowser(aBrowser),
     mContainer(aContainer),
     mClosed(false)
   {
     mWebNavigation = do_QueryInterface(aBrowser);
     mInterfaceRequestor = do_QueryInterface(aBrowser);
   }
   NS_DECL_ISUPPORTS
+  NS_DECL_NSIWINDOWLESSBROWSER
   NS_FORWARD_SAFE_NSIWEBNAVIGATION(mWebNavigation)
   NS_FORWARD_SAFE_NSIINTERFACEREQUESTOR(mInterfaceRequestor)
 
-  NS_IMETHOD
-  Close() override
-  {
-    NS_ENSURE_TRUE(!mClosed, NS_ERROR_UNEXPECTED);
-    NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
-                 "WindowlessBrowser::Close called when not safe to run scripts");
-
-    mClosed = true;
-
-    mWebNavigation = nullptr;
-    mInterfaceRequestor = nullptr;
-
-    nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(mBrowser);
-    return window->Destroy();
-  }
-
 protected:
   virtual ~WindowlessBrowser()
   {
     if (mClosed) {
       return;
     }
 
     NS_WARNING("Windowless browser was not closed prior to destruction");
@@ -495,16 +480,43 @@ private:
   // we don't use the container but just hold a reference to it.
   nsCOMPtr<nsISupports> mContainer;
 
   bool mClosed;
 };
 
 NS_IMPL_ISUPPORTS(WindowlessBrowser, nsIWindowlessBrowser, nsIWebNavigation, nsIInterfaceRequestor)
 
+NS_IMETHODIMP
+WindowlessBrowser::Close()
+{
+  NS_ENSURE_TRUE(!mClosed, NS_ERROR_UNEXPECTED);
+  NS_ASSERTION(nsContentUtils::IsSafeToRunScript(),
+               "WindowlessBrowser::Close called when not safe to run scripts");
+
+  mClosed = true;
+
+  mWebNavigation = nullptr;
+  mInterfaceRequestor = nullptr;
+
+  nsCOMPtr<nsIBaseWindow> window = do_QueryInterface(mBrowser);
+  return window->Destroy();
+}
+
+NS_IMETHODIMP
+WindowlessBrowser::GetDocShell(nsIDocShell** aDocShell)
+{
+  nsCOMPtr<nsIDocShell> docShell = do_GetInterface(mInterfaceRequestor);
+  if (!docShell) {
+    return NS_ERROR_NOT_INITIALIZED;
+  }
+  docShell.forget(aDocShell);
+  return NS_OK;
+}
+
 
 NS_IMETHODIMP
 nsAppShellService::CreateWindowlessBrowser(bool aIsChrome, nsIWindowlessBrowser **aResult)
 {
   /* First, we create an instance of nsWebBrowser. Instances of this class have
    * an associated doc shell, which is what we're interested in.
    */
   nsCOMPtr<nsIWebBrowser> browser = do_CreateInstance(NS_WEBBROWSER_CONTRACTID);
--- a/xpfe/appshell/nsIWindowlessBrowser.idl
+++ b/xpfe/appshell/nsIWindowlessBrowser.idl
@@ -1,16 +1,18 @@
 /* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  *
  * 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/. */
 
 #include "nsIWebNavigation.idl"
 
+interface nsIDocShell;
+
 /**
  * This interface represents a nsIWebBrowser instance with no associated OS
  * window. Its main function is to manage the lifetimes of those windows.
  * A strong reference to this object must be held until the window is
  * ready to be destroyed.
  */
 [scriptable, uuid(abb46f48-abfc-41bf-aa9a-7feccefcf977)]
 interface nsIWindowlessBrowser : nsIWebNavigation
@@ -18,10 +20,16 @@ interface nsIWindowlessBrowser : nsIWebN
   /**
    * "Closes" the windowless browser and destroys its associated nsIWebBrowser
    * and docshell.
    *
    * This method *must* be called for every windowless browser before its last
    * reference is released.
    */
   void close();
+
+  /**
+   * Get the docshell for this browser.  This is the docshell that gets
+   * navigated when the browser's nsIWebNavigation interface is used.
+   */
+  readonly attribute nsIDocShell docShell;
 };
 
--- a/xpfe/appshell/test/test_windowlessBrowser.xul
+++ b/xpfe/appshell/test/test_windowlessBrowser.xul
@@ -26,18 +26,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
 ChromeUtils.import('resource://gre/modules/Services.jsm');
 
 function testWindowlessBrowser(chromePrivileged) {
   var webNav = Services.appShell.createWindowlessBrowser(chromePrivileged);
 
   ok(webNav, "createWindowlessBrowser should return a wevNav");
 
-  let interfaceRequestor = webNav.QueryInterface(Ci.nsIInterfaceRequestor);
-  let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
+  let docShell = webNav.docShell;
 
   ok(docShell, "docShell should be defined");
   ok(docShell.contentViewer.DOMDocument.defaultView, "docShell defaultView should be defined");
 
   var win = docShell.contentViewer.DOMDocument.defaultView;
 
   ok(win.screenX == 0, "window.screenX should be 0 in a windowless browser");
   ok(win.screenY == 0, "window.screenY should be 0 in a windowless browser");