Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 02 Jun 2015 13:13:15 +0200
changeset 246725 3bd03f40fa950bb76f92228dc7abde1726ac1552
parent 246724 3c8ed81098ddbe4a4c09e7aa652b5288dc4ce0d3 (current diff)
parent 246644 9eae3880b132898a96a80d497cba2b9523e049a4 (diff)
child 246726 b4d93e7f990aff68c20ca7b31531b2ce4941917b
push id28839
push userkwierso@gmail.com
push dateWed, 03 Jun 2015 01:20:15 +0000
treeherdermozilla-central@20a96e15631a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound
modules/libpref/init/all.js
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -430,18 +430,18 @@ loop.store.ActiveRoomStore = (function()
       // Reset the failure reason if necessary.
       if (this.getStoreState().failureReason) {
         this.setStoreState({failureReason: undefined});
       }
 
       // XXX Ideally we'd do this check before joining a room, but we're waiting
       // for the UX for that. See bug 1166824. In the meantime this gives us
       // additional information for analysis.
-      loop.shared.utils.hasAudioOrVideoDevices(function(hasAudio) {
-        if (hasAudio) {
+      loop.shared.utils.hasAudioOrVideoDevices(function(hasDevices) {
+        if (hasDevices) {
           // MEDIA_WAIT causes the views to dispatch sharedActions.SetupStreamElements,
           // which in turn starts the sdk obtaining the device permission.
           this.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
         } else {
           this.dispatchAction(new sharedActions.ConnectionFailure({
             reason: FAILURE_DETAILS.NO_MEDIA
           }));
         }
@@ -566,17 +566,29 @@ loop.store.ActiveRoomStore = (function()
         this.getStoreState().windowId,
         actionData.state === SCREEN_SHARE_STATES.ACTIVE);
     },
 
     /**
      * Used to note the current state of receiving screenshare data.
      */
     receivingScreenShare: function(actionData) {
-      this.setStoreState({receivingScreenShare: actionData.receiving});
+      if (!actionData.receiving &&
+          this.getStoreState().remoteVideoDimensions.screen) {
+        // Remove the remote video dimensions for type screen as we're not
+        // getting the share anymore.
+        var newDimensions = _.extend(this.getStoreState().remoteVideoDimensions);
+        delete newDimensions.screen;
+        this.setStoreState({
+          receivingScreenShare: actionData.receiving,
+          remoteVideoDimensions: newDimensions
+        });
+      } else {
+        this.setStoreState({receivingScreenShare: actionData.receiving});
+      }
     },
 
     /**
      * Handles switching browser (aka tab) sharing to a new window. Should
      * only be used for browser sharing.
      *
      * @param {Number} windowId  The new windowId to start sharing.
      */
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -332,16 +332,23 @@ loop.shared.mixins = (function() {
         if (cache[videoType].height !== newDimensions[videoType].height) {
           cache[videoType].height = newDimensions[videoType].height;
           changed = true;
         }
         if (changed) {
           cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]);
         }
       }, this);
+      // Remove any streams that are no longer being published.
+      cacheKeys.forEach(function(videoType) {
+        if (!(videoType in newDimensions)) {
+          delete cache[videoType];
+          changed = true;
+        }
+      });
       return changed;
     },
 
     /**
      * Whenever the dimensions change of a video stream, this function is called
      * to process these changes and possibly trigger an update to the video
      * container elements.
      *
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -309,29 +309,32 @@ var inChrome = typeof Components != "und
   /**
    * Determines if the user has any audio devices installed.
    *
    * @param  {Function} callback Called with a boolean which is true if there
    *                             are audio devices present.
    */
   function hasAudioOrVideoDevices(callback) {
     // mediaDevices is the official API for the spec.
-    if ("mediaDevices" in rootNavigator) {
+    // Older versions of FF had mediaDevices but not enumerateDevices.
+    if ("mediaDevices" in rootNavigator &&
+        "enumerateDevices" in rootNavigator.mediaDevices) {
       rootNavigator.mediaDevices.enumerateDevices().then(function(result) {
         function checkForInput(device) {
           return device.kind === "audioinput" || device.kind === "videoinput";
         }
 
         callback(result.some(checkForInput));
       }).catch(function() {
         callback(false);
       });
     // MediaStreamTrack is the older version of the API, implemented originally
     // by Google Chrome.
-    } else if ("MediaStreamTrack" in rootObject) {
+    } else if ("MediaStreamTrack" in rootObject &&
+               "getSources" in rootObject.MediaStreamTrack) {
       rootObject.MediaStreamTrack.getSources(function(result) {
         function checkForInput(device) {
           return device.kind === "audio" || device.kind === "video";
         }
 
         callback(result.some(checkForInput));
       });
     } else {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/standalone/content/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -1013,16 +1013,33 @@ describe("loop.store.ActiveRoomStore", f
   describe("#receivingScreenShare", function() {
     it("should save the state", function() {
       store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
         receiving: true
       }));
 
       expect(store.getStoreState().receivingScreenShare).eql(true);
     });
+
+    it("should delete the screen remote video dimensions if screen sharing is not active", function() {
+      store.setStoreState({
+        remoteVideoDimensions: {
+          screen: {fake: 10},
+          camera: {fake: 20}
+        }
+      });
+
+      store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
+        receiving: false
+      }));
+
+      expect(store.getStoreState().remoteVideoDimensions).eql({
+        camera: {fake: 20}
+      });
+    });
   });
 
   describe("#startScreenShare", function() {
     it("should set the state to 'pending'", function() {
       store.startScreenShare(new sharedActions.StartScreenShare({
         type: "window"
       }));
 
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -460,37 +460,44 @@ describe("loop.shared.mixins", function(
         };
         var remoteVideoDimensions = {
           camera: {
             width: 420,
             height: 138
           }
         };
 
-        beforeEach(function() {
+        it("should register video dimension updates correctly", function() {
           view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
-        });
 
-        it("should register video dimension updates correctly", function() {
           expect(view._videoDimensionsCache.local.camera.width)
             .eql(localVideoDimensions.camera.width);
           expect(view._videoDimensionsCache.local.camera.height)
             .eql(localVideoDimensions.camera.height);
           expect(view._videoDimensionsCache.local.camera.aspectRatio.width).eql(1);
           expect(view._videoDimensionsCache.local.camera.aspectRatio.height).eql(0.75);
           expect(view._videoDimensionsCache.remote.camera.width)
             .eql(remoteVideoDimensions.camera.width);
           expect(view._videoDimensionsCache.remote.camera.height)
             .eql(remoteVideoDimensions.camera.height);
           expect(view._videoDimensionsCache.remote.camera.aspectRatio.width).eql(1);
           expect(view._videoDimensionsCache.remote.camera.aspectRatio.height)
             .eql(0.32857142857142857);
         });
 
+        it("should unregister video dimension updates correctly", function() {
+          view.updateVideoDimensions(localVideoDimensions, {});
+
+          expect("camera" in view._videoDimensionsCache.local).eql(true);
+          expect("camera" in view._videoDimensionsCache.remote).eql(false);
+        });
+
         it("should not populate the cache on another component instance", function() {
+            view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
+
             var view2 =
               TestUtils.renderIntoDocument(React.createElement(TestComp));
 
             expect(view2._videoDimensionsCache.local).to.be.empty;
             expect(view2._videoDimensionsCache.remote).to.be.empty;
         });
 
       });
--- a/browser/components/loop/test/shared/utils_test.js
+++ b/browser/components/loop/test/shared/utils_test.js
@@ -169,16 +169,40 @@ describe("loop.shared.utils", function()
       delete fakeWindowObject.MediaStreamTrack;
 
       sharedUtils.hasAudioOrVideoDevices(function(result) {
         expect(result).eql(true);
         done();
       });
     });
 
+    it("should return true if enumerateDevices doesn't exist in navigator.mediaDevices", function(done) {
+      sharedUtils.setRootObjects(fakeWindowObject, {
+        mediaDevices: {}
+      });
+      delete fakeWindowObject.MediaStreamTrack;
+
+      sharedUtils.hasAudioOrVideoDevices(function(result) {
+        expect(result).eql(true);
+        done();
+      });
+    });
+
+    it("should return true if getSources doesn't exist in window.MediaStreamTrack", function(done) {
+      sharedUtils.setRootObjects({
+        MediaStreamTrack: {}
+      }, fakeNavigatorObject);
+      delete fakeNavigatorObject.mediaDevices;
+
+      sharedUtils.hasAudioOrVideoDevices(function(result) {
+        expect(result).eql(true);
+        done();
+      });
+    });
+
     it("should return false if no audio nor video devices exist according to navigator.mediaDevices", function(done) {
       delete fakeWindowObject.MediaStreamTrack;
 
       fakeNavigatorObject.mediaDevices.enumerateDevices.returns(Promise.resolve([]));
 
       sharedUtils.hasAudioOrVideoDevices(function(result) {
         try {
           expect(result).eql(false);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1108,17 +1108,17 @@ BrowserGlue.prototype = {
       try {
         // Report default browser status on startup to telemetry
         // so we can track whether we are the default.
         Services.telemetry.getHistogramById("BROWSER_IS_USER_DEFAULT")
                           .add(isDefault);
       }
       catch (ex) { /* Don't break the default prompt if telemetry is broken. */ }
 
-      Services.setBoolPref("browser.shell.isSetAsDefaultBrowser", isDefault);
+      Services.prefs.setBoolPref("browser.shell.isSetAsDefaultBrowser", isDefault);
 
       if (shouldCheck && !isDefault && !willRecoverSession) {
         Services.tm.mainThread.dispatch(function() {
           DefaultBrowserCheck.prompt(RecentWindow.getMostRecentBrowserWindow());
         }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
       }
     }
 
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -12,16 +12,17 @@ support-files =
   doc_inspector_highlighter-geometry_01.html
   doc_inspector_highlighter-geometry_02.html
   doc_inspector_highlighter_csstransform.html
   doc_inspector_highlighter_dom.html
   doc_inspector_highlighter_inline.html
   doc_inspector_highlighter.html
   doc_inspector_highlighter_rect.html
   doc_inspector_highlighter_rect_iframe.html
+  doc_inspector_highlighter_xbl.xul
   doc_inspector_infobar_01.html
   doc_inspector_infobar_02.html
   doc_inspector_menu.html
   doc_inspector_remove-iframe-during-load.html
   doc_inspector_search.html
   doc_inspector_search-reserved.html
   doc_inspector_search-suggestions.html
   doc_inspector_select-last-selected-01.html
@@ -62,16 +63,17 @@ skip-if = e10s # GCLI isn't e10s compati
 [browser_inspector_highlighter-keybinding_04.js]
 [browser_inspector_highlighter-options.js]
 [browser_inspector_highlighter-rect_01.js]
 [browser_inspector_highlighter-rect_02.js]
 [browser_inspector_highlighter-rulers_01.js]
 [browser_inspector_highlighter-rulers_02.js]
 [browser_inspector_highlighter-selector_01.js]
 [browser_inspector_highlighter-selector_02.js]
+[browser_inspector_highlighter-xbl.js]
 [browser_inspector_highlighter-zoom.js]
 [browser_inspector_iframe-navigation.js]
 [browser_inspector_infobar_01.js]
 [browser_inspector_initialization.js]
 [browser_inspector_inspect-object-element.js]
 [browser_inspector_invalidate.js]
 [browser_inspector_keyboard-shortcuts.js]
 [browser_inspector_menu-01-sensitivity.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_highlighter-xbl.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the picker works correctly with XBL anonymous nodes
+
+const TEST_URL = TEST_URL_ROOT + "doc_inspector_highlighter_xbl.xul";
+
+add_task(function*() {
+  let {inspector, toolbox} = yield openInspectorForURL(TEST_URL);
+
+  info("Starting element picker");
+  yield toolbox.highlighterUtils.startPicker();
+
+  info("Selecting the scale");
+  yield moveMouseOver("#scale");
+  yield doKeyPick({key: "VK_RETURN", options: {}});
+  is(inspector.selection.nodeFront.className, "scale-slider",
+     "The .scale-slider inside the scale was selected");
+
+  function doKeyPick(msg) {
+    info("Key pressed. Waiting for element to be picked");
+    executeInContent("Test:SynthesizeKey", msg);
+    return promise.all([
+      toolbox.selection.once("new-node-front"),
+      inspector.once("inspector-updated")
+    ]);
+  }
+
+  function moveMouseOver(selector) {
+    info("Waiting for element " + selector + " to be highlighted");
+    executeInContent("Test:SynthesizeMouse", {
+      options: {type: "mousemove"},
+      center: true,
+      selector: selector
+    }, null, false);
+    return inspector.toolbox.once("picker-node-hovered");
+  }
+
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/doc_inspector_highlighter_xbl.xul
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+
+<window title="Test that the picker works correctly with XBL anonymous nodes"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<scale id="scale"/>
+
+</window>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -856,17 +856,17 @@ var interfaceNamesInGlobalScope =
     {name: "PermissionSettings", b2g: true, permission: ["permissions"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PhoneNumberService", permission: ["phonenumberservice"]},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "Plugin",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PluginArray",
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    {name: "PointerEvent", disabled: true},
+    {name: "PointerEvent", nightly: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopStateEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "PopupBlockedEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PopupBoxObject", xbl: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "PositionSensorVRDevice", disabled: true},
--- a/gfx/thebes/gfxWindowsPlatform.cpp
+++ b/gfx/thebes/gfxWindowsPlatform.cpp
@@ -527,19 +527,17 @@ gfxWindowsPlatform::UpdateRenderMode()
 
     // Do not ever try if d2d is explicitly disabled,
     // or if we're not using DWrite fonts.
     if (d2dDisabled || mUsingGDIFonts) {
         tryD2D = false;
     }
 
     ID3D11Device *device = GetD3D11Device();
-    if (isVistaOrHigher && !InSafeMode() && tryD2D &&
-        device &&
-        device->GetFeatureLevel() >= D3D_FEATURE_LEVEL_10_0 &&
+    if (isVistaOrHigher && !InSafeMode() && tryD2D && device &&
         DoesD3D11TextureSharingWork(device)) {
 
         VerifyD2DDevice(d2dForceEnabled);
         if (mD2DDevice && GetD3D11Device()) {
             mRenderMode = RENDER_DIRECT2D;
             mUseDirectWrite = true;
         }
     } else {
@@ -1732,16 +1730,22 @@ bool DoesD3D11DeviceWork(ID3D11Device *d
   result = true;
   return true;
 }
 
 // See bug 1083071. On some drivers, Direct3D 11 CreateShaderResourceView fails
 // with E_OUTOFMEMORY.
 bool DoesD3D11TextureSharingWorkInternal(ID3D11Device *device, DXGI_FORMAT format, UINT bindflags)
 {
+  // CreateTexture2D is known to crash on lower feature levels, see bugs
+  // 1170211 and 1089413.
+  if (device->GetFeatureLevel() < D3D_FEATURE_LEVEL_10_0) {
+    return false;
+  }
+
   if (gfxPrefs::Direct2DForceEnabled() ||
       gfxPrefs::LayersAccelerationForceEnabled())
   {
     return true;
   }
 
   if (GetModuleHandleW(L"atidxx32.dll")) {
     nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
--- a/layout/xul/test/test_bug477754.xul
+++ b/layout/xul/test/test_bug477754.xul
@@ -33,17 +33,17 @@ https://bugzilla.mozilla.org/show_bug.cg
 
       testPopup = document.getElementById("testPopup");
       testAnchor = document.getElementById("anchor");
 
       testPopup.openPopup(testAnchor, "after_start", 10, 0, false, false);
     }, false);
 
     function doTest() {
-      is(Math.round(testAnchor.getBoundingClientRect().right) -
-         Math.round(testPopup.getBoundingClientRect().right), 10,
+      is(Math.round(testAnchor.getBoundingClientRect().right -
+                    testPopup.getBoundingClientRect().right), 10,
          "RTL popup's right offset should be equal to the x offset passed to openPopup");
       testPopup.hidePopup();
       SimpleTest.finish();
     }
 
    ]]></script>
 </window>
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -175,16 +175,18 @@ pref("dom.experimental_forms", true);
 pref("dom.forms.number", true);
 
 /* extension manager and xpinstall */
 pref("xpinstall.whitelist.directRequest", false);
 pref("xpinstall.whitelist.fileRequest", false);
 pref("xpinstall.whitelist.add", "addons.mozilla.org");
 pref("xpinstall.whitelist.add.180", "marketplace.firefox.com");
 
+pref("xpinstall.signatures.required", false);
+
 pref("extensions.enabledScopes", 1);
 pref("extensions.autoupdate.enabled", true);
 pref("extensions.autoupdate.interval", 86400);
 pref("extensions.update.enabled", false);
 pref("extensions.update.interval", 86400);
 pref("extensions.dss.enabled", false);
 pref("extensions.dss.switchPending", false);
 pref("extensions.ignoreMTimeChanges", false);
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -718,20 +718,20 @@ public class BrowserApp extends GeckoApp
         mBrowserToolbar.setTabHistoryController(tabHistoryController);
 
         final String action = intent.getAction();
         if (Intent.ACTION_VIEW.equals(action)) {
             // Show the target URL immediately in the toolbar.
             mBrowserToolbar.setTitle(intent.getDataString());
 
             showTabQueuePromptIfApplicable(intent);
-
-            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT);
         } else if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
             GuestSession.handleIntent(this, intent);
+        } else if (TabQueueHelper.LOAD_URLS_ACTION.equals(action)) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
         }
 
         if (HardwareUtils.isTablet()) {
             mTabStrip = (Refreshable) (((ViewStub) findViewById(R.id.new_tablet_tab_strip)).inflate());
         }
 
         ((GeckoApp.MainLayout) mMainLayout).setTouchEventInterceptor(new HideOnTouchListener());
         ((GeckoApp.MainLayout) mMainLayout).setMotionEventInterceptor(new MotionEventInterceptor() {
@@ -932,16 +932,20 @@ public class BrowserApp extends GeckoApp
         }
     }
 
     @Override
     protected void openQueuedTabs() {
         ThreadUtils.assertNotOnUiThread();
 
         int queuedTabCount = TabQueueHelper.getTabQueueLength(BrowserApp.this);
+
+        Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-delayed");
+
         TabQueueHelper.openQueuedUrls(BrowserApp.this, mProfile, TabQueueHelper.FILE_NAME, false);
 
         // If there's more than one tab then also show the tabs panel.
         if (queuedTabCount > 1) {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
                     showNormalTabs();
@@ -1719,16 +1723,17 @@ public class BrowserApp extends GeckoApp
             final BrowserDB db = getProfile().getDB();
             final ContentResolver cr = getContentResolver();
             Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
             Telemetry.addToHistogram("PLACES_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
             Telemetry.addToHistogram("FENNEC_FAVICONS_COUNT", db.getCount(cr, "favicons"));
             Telemetry.addToHistogram("FENNEC_THUMBNAILS_COUNT", db.getCount(cr, "thumbnails"));
             Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
             Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
+            Telemetry.addToHistogram("FENNEC_TABQUEUE_ENABLED", (TabQueueHelper.isTabQueueEnabled(BrowserApp.this) ? 1 : 0));
             if (Versions.feature16Plus) {
                 Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
             }
         } else if ("Updater:Launch".equals(event)) {
             handleUpdaterLaunch();
         } else {
             super.handleMessage(event, message, callback);
         }
@@ -3489,22 +3494,21 @@ public class BrowserApp extends GeckoApp
 
         if (mInitialized && (isViewAction || isBookmarkAction)) {
             // Dismiss editing mode if the user is loading a URL from an external app.
             mBrowserToolbar.cancelEdit();
 
             // Hide firstrun-pane if the user is loading a URL from an external app.
             hideFirstrunPager();
 
-            // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
-            // was added to Android's homescreen.
-            final TelemetryContract.Method method =
-                (isViewAction ? TelemetryContract.Method.INTENT : TelemetryContract.Method.HOMESCREEN);
-
-            Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method);
+            if (isBookmarkAction) {
+                // GeckoApp.ACTION_HOMESCREEN_SHORTCUT means we're opening a bookmark that
+                // was added to Android's homescreen.
+                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.HOMESCREEN);
+            }
         }
 
         showTabQueuePromptIfApplicable(intent);
 
         super.onNewIntent(intent);
 
         if (AppConstants.MOZ_ANDROID_BEAM && NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
             String uri = intent.getDataString();
@@ -3513,16 +3517,17 @@ public class BrowserApp extends GeckoApp
 
         // Only solicit feedback when the app has been launched from the icon shortcut.
         if (GuestSession.NOTIFICATION_INTENT.equals(action)) {
             GuestSession.handleIntent(this, intent);
         }
 
         // If the user has clicked the tab queue notification then load the tabs.
         if(AppConstants.NIGHTLY_BUILD  && AppConstants.MOZ_ANDROID_TAB_QUEUE && mInitialized && isTabQueueAction) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "tabqueue");
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     openQueuedTabs();
                 }
             });
         }
 
--- a/mobile/android/base/TelemetryContract.java
+++ b/mobile/android/base/TelemetryContract.java
@@ -152,16 +152,19 @@ public interface TelemetryContract {
         LIST_ITEM("listitem"),
 
         // Action occurred via the main menu.
         MENU("menu"),
 
         // No method is specified.
         NONE(null),
 
+        // Action triggered from a notification in the Android notification bar.
+        NOTIFICATION("notification"),
+
         // Action triggered from a pageaction in the URLBar.
         // Note: Only used in JavaScript for now, but here for completeness.
         PAGEACTION("pageaction"),
 
         // Action triggered from a settings screen.
         SETTINGS("settings"),
 
         // Actions triggered from the share overlay.
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -190,20 +190,20 @@
 <!ENTITY pref_cookies_not_accept_foreign "Enabled, excluding 3rd party">
 <!ENTITY pref_cookies_disabled "Disabled">
 
 <!ENTITY pref_tracking_protection_title "Tracking protection">
 <!ENTITY pref_tracking_protection_summary "&brandShortName; will prevent sites from tracking you">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
-<!ENTITY tab_queue_toast_message2 "Tab queued in &brandShortName;">
+<!ENTITY tab_queue_toast_message3 "Tab saved in &brandShortName;">
 <!ENTITY tab_queue_toast_action "Open now">
 <!ENTITY tab_queue_prompt_title "Opening multiple links?">
-<!ENTITY tab_queue_prompt_text2 "Open them without switching to &brandShortName; each time.">
+<!ENTITY tab_queue_prompt_text3 "Save them until the next time you open &brandShortName;">
 <!ENTITY tab_queue_prompt_tip_text "you can change this later in Settings">
 <!ENTITY tab_queue_prompt_positive_action_button "Enable">
 <!ENTITY tab_queue_prompt_negative_action_button "Not now">
 <!ENTITY tab_queue_notification_title "&brandShortName;">
 <!-- Localization note (tab_queue_notification_text_plural2) : The
      formatD is replaced with the number of tabs queued.  The
      number of tabs queued is always more than one.  We can't use
      Android plural forms, sadly. See Bug #753859. -->
@@ -397,17 +397,17 @@ size. -->
 <!ENTITY pref_titlebar_mode_url "Show page address">
 
 <!-- Localization note (pref_scroll_title_bar2): Label for setting that controls
      whether or not the dynamic toolbar is enabled. -->
 <!ENTITY pref_scroll_title_bar2 "Full-screen browsing">
 <!ENTITY pref_scroll_title_bar_summary "Hide the &brandShortName; title bar when scrolling down a page">
 
 <!ENTITY pref_tab_queue_title2 "Open multiple links">
-<!ENTITY pref_tab_queue_summary2 "Queue links for later instead of switching to &brandShortName; each time">
+<!ENTITY pref_tab_queue_summary3 "Save them until the next time you open &brandShortName;">
 
 <!-- Localization note (page_removed): This string appears in a toast message when
      any page is removed frome about:home. This includes pages that are in history,
      bookmarks, or reading list. -->
 <!ENTITY page_removed "Page removed">
 
 <!ENTITY bookmark_edit_title "Edit Bookmark">
 <!ENTITY bookmark_edit_name "Name">
--- a/mobile/android/base/resources/layout/tab_queue_prompt.xml
+++ b/mobile/android/base/resources/layout/tab_queue_prompt.xml
@@ -41,17 +41,17 @@
             android:layout_gravity="center"
             android:gravity="center"
             android:lineSpacingMultiplier="1.25"
             android:paddingTop="20dp"
             android:text="@string/tab_queue_prompt_text"
             android:textColor="@color/placeholder_grey"
             android:textSize="16sp"
 
-            tools:text="Open them without switching to Firefox each time." />
+            tools:text="Save them until the next time you open Firefox." />
 
         <TextView
             android:id="@+id/tip_text"
             android:layout_width="@dimen/tab_queue_content_width"
             android:layout_height="wrap_content"
             android:layout_gravity="center"
             android:gravity="center"
             android:paddingBottom="30dp"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -238,23 +238,23 @@
   <string name="pref_import_android">&pref_import_android;</string>
   <string name="pref_import_android_summary">&pref_import_android_summary;</string>
   <string name="pref_update_autodownload">&pref_update_autodownload2;</string>
   <string name="pref_update_autodownload_wifi">&pref_update_autodownload_wifi;</string>
   <string name="pref_update_autodownload_disabled">&pref_update_autodownload_never;</string>
   <string name="pref_update_autodownload_enabled">&pref_update_autodownload_always;</string>
 
   <string name="pref_tab_queue_title">&pref_tab_queue_title2;</string>
-  <string name="pref_tab_queue_summary">&pref_tab_queue_summary2;</string>
+  <string name="pref_tab_queue_summary">&pref_tab_queue_summary3;</string>
   <string name="tab_queue_prompt_title">&tab_queue_prompt_title;</string>
-  <string name="tab_queue_prompt_text">&tab_queue_prompt_text2;</string>
+  <string name="tab_queue_prompt_text">&tab_queue_prompt_text3;</string>
   <string name="tab_queue_prompt_tip_text">&tab_queue_prompt_tip_text;</string>
   <string name="tab_queue_prompt_positive_action_button">&tab_queue_prompt_positive_action_button;</string>
   <string name="tab_queue_prompt_negative_action_button">&tab_queue_prompt_negative_action_button;</string>
-  <string name="tab_queue_toast_message">&tab_queue_toast_message2;</string>
+  <string name="tab_queue_toast_message">&tab_queue_toast_message3;</string>
   <string name="tab_queue_toast_action">&tab_queue_toast_action;</string>
   <string name="tab_queue_notification_text_singular">&tab_queue_notification_text_singular2;</string>
   <string name="tab_queue_notification_text_plural">&tab_queue_notification_text_plural2;</string>
   <string name="tab_queue_notification_title">&tab_queue_notification_title;</string>
 
   <string name="pref_about_firefox">&pref_about_firefox;</string>
   <string name="pref_vendor_faqs">&pref_vendor_faqs;</string>
   <string name="pref_vendor_feedback">&pref_vendor_feedback;</string>
--- a/mobile/android/base/tabqueue/TabQueueDispatcher.java
+++ b/mobile/android/base/tabqueue/TabQueueDispatcher.java
@@ -4,16 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.content.Intent;
 import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -50,17 +52,17 @@ public class TabQueueDispatcher extends 
 
         // The URL is usually hiding somewhere in the extra text. Extract it.
         final String dataString = safeIntent.getDataString();
         if (TextUtils.isEmpty(dataString)) {
             abortDueToNoURL(dataString);
             return;
         }
 
-        boolean shouldShowOpenInBackgroundToast = GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
+        boolean shouldShowOpenInBackgroundToast = TabQueueHelper.isTabQueueEnabled(this);
 
         if (shouldShowOpenInBackgroundToast) {
             showToast(safeIntent.getUnsafe());
         } else {
             loadNormally(safeIntent.getUnsafe());
         }
     }
 
@@ -71,16 +73,17 @@ public class TabQueueDispatcher extends 
     }
 
     /**
      * Start fennec with the supplied intent.
      */
     private void loadNormally(Intent intent) {
         intent.setClassName(getApplicationContext(), AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
         startActivity(intent);
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "");
         finish();
     }
 
     /**
      * Abort as we were started with no URL.
      * @param dataString
      */
     private void abortDueToNoURL(String dataString) {
--- a/mobile/android/base/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/tabqueue/TabQueueHelper.java
@@ -49,22 +49,21 @@ public class TabQueueHelper {
      * Check if we should show the tab queue prompt
      *
      * @param context
      * @return true if we should display the prompt, false if not.
      */
     public static boolean shouldShowTabQueuePrompt(Context context) {
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
 
-        boolean isTabQueueEnabled = prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
         int numberOfTimesTabQueuePromptSeen = prefs.getInt(PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
 
         // Exit early if the feature is already enabled or the user has seen the
         // prompt more than MAX_TIMES_TO_SHOW_PROMPT times.
-        if (isTabQueueEnabled || numberOfTimesTabQueuePromptSeen >= MAX_TIMES_TO_SHOW_PROMPT) {
+        if (isTabQueueEnabled(prefs) || numberOfTimesTabQueuePromptSeen >= MAX_TIMES_TO_SHOW_PROMPT) {
             return false;
         }
 
         final int viewActionIntentLaunches = prefs.getInt(PREF_TAB_QUEUE_LAUNCHES, 0) + 1;
         if (viewActionIntentLaunches < EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
             // Allow a few external links to open before we prompt the user.
             prefs.edit().putInt(PREF_TAB_QUEUE_LAUNCHES, viewActionIntentLaunches).apply();
         } else if (viewActionIntentLaunches == EXTERNAL_LAUNCHES_BEFORE_SHOWING_PROMPT) {
@@ -179,20 +178,19 @@ public class TabQueueHelper {
     }
 
     public static boolean shouldOpenTabQueueUrls(final Context context) {
         ThreadUtils.assertNotOnUiThread();
 
         // TODO: Use profile shared prefs when bug 1147925 gets fixed.
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
 
-        boolean tabQueueEnabled = prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
         int tabsQueued = prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
 
-        return tabQueueEnabled && tabsQueued > 0;
+        return isTabQueueEnabled(prefs) && tabsQueued > 0;
     }
 
     public static int getTabQueueLength(final Context context) {
         ThreadUtils.assertNotOnUiThread();
 
         // TODO: Use profile shared prefs when bug 1147925 gets fixed.
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
         return prefs.getInt(PREF_TAB_QUEUE_COUNT, 0);
@@ -265,9 +263,17 @@ public class TabQueueHelper {
 
             default:
                 // We shouldn't ever get here.
                 Log.w(LOGTAG, "Unrecognized result code received from the tab queue prompt: " + resultCode);
         }
 
         editor.apply();
     }
-}
\ No newline at end of file
+
+    public static boolean isTabQueueEnabled(Context context) {
+        return isTabQueueEnabled(GeckoSharedPrefs.forApp(context));
+    }
+
+    public static boolean isTabQueueEnabled(SharedPreferences prefs) {
+        return prefs.getBoolean(GeckoPreferences.PREFS_TAB_QUEUE, false);
+    }
+}
--- a/mobile/android/base/tabqueue/TabQueuePrompt.java
+++ b/mobile/android/base/tabqueue/TabQueuePrompt.java
@@ -1,17 +1,19 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.animation.TransitionsTracker;
 
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.MotionEvent;
 import android.view.View;
 import com.nineoldandroids.animation.Animator;
 import com.nineoldandroids.animation.AnimatorListenerAdapter;
@@ -34,25 +36,29 @@ public class TabQueuePrompt extends Loca
         super.onCreate(savedInstanceState);
 
         showTabQueueEnablePrompt();
     }
 
     private void showTabQueueEnablePrompt() {
         setContentView(R.layout.tab_queue_prompt);
 
+        final int numberOfTimesTabQueuePromptSeen = GeckoSharedPrefs.forApp(this).getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
+
         findViewById(R.id.ok_button).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 onConfirmButtonPressed();
+                Telemetry.addToHistogram("FENNEC_TABQUEUE_PROMPT_ENABLE_YES", numberOfTimesTabQueuePromptSeen);
             }
         });
         findViewById(R.id.cancel_button).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
+                Telemetry.addToHistogram("FENNEC_TABQUEUE_PROMPT_ENABLE_NO", numberOfTimesTabQueuePromptSeen);
                 setResult(TabQueueHelper.TAB_QUEUE_NO);
                 finish();
             }
         });
 
         containerView = findViewById(R.id.tab_queue_container);
         buttonContainer = findViewById(R.id.button_container);
         enabledConfirmation = findViewById(R.id.enabled_confirmation);
--- a/mobile/android/base/tabqueue/TabQueueService.java
+++ b/mobile/android/base/tabqueue/TabQueueService.java
@@ -4,16 +4,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 
 import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Resources;
@@ -24,16 +26,17 @@ import android.os.IBinder;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.TextView;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 
 /**
  * On launch this Service displays a View over the currently running process with an action to open the url in Fennec
  * immediately.  If the user takes no action, allowing the runnable to be processed after the specified
@@ -194,16 +197,27 @@ public class TabQueueService extends Ser
         forwardIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         startActivity(forwardIntent);
 
         TabQueueHelper.removeNotification(getApplicationContext());
 
         GeckoSharedPrefs.forApp(getApplicationContext()).edit().remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_SITE)
                                                                .remove(GeckoPreferences.PREFS_TAB_QUEUE_LAST_TIME)
                                                                .apply();
+
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "tabqueue-now");
+
+        executorService.submit(new Runnable() {
+            @Override
+            public void run() {
+                int queuedTabCount = TabQueueHelper.getTabQueueLength(TabQueueService.this);
+                Telemetry.addToHistogram("FENNEC_TABQUEUE_QUEUESIZE", queuedTabCount);
+            }
+        });
+
     }
 
     private void removeView() {
         try {
             windowManager.removeView(toastLayout);
         } catch (IllegalArgumentException | IllegalStateException e) {
             // This can happen if the Service is killed by the system.  If this happens the View will have already
             // been removed but the runnable will have been kept alive.
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -101,8 +101,17 @@ MOZ_ANDROID_DOWNLOADS_INTEGRATION=1
 
 # Enable Tab Queue
 if test "$NIGHTLY_BUILD"; then
   MOZ_ANDROID_TAB_QUEUE=1
 fi
 
 # Use the low-memory GC tuning.
 export JS_GC_SMALL_CHUNK_SIZE=1
+
+# Enable checking that add-ons are signed by the trusted root
+MOZ_ADDON_SIGNING=1
+if test "$MOZ_OFFICIAL_BRANDING"; then
+  if test "$MOZ_UPDATE_CHANNEL" = "beta" -o \
+          "$MOZ_UPDATE_CHANNEL" = "release"; then
+    MOZ_REQUIRE_SIGNING=1
+  fi
+fi
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4418,25 +4418,34 @@ pref("dom.mozSettings.enabled", false);
 pref("dom.mozPermissionSettings.enabled", false);
 
 // W3C touch events
 // 0 - disabled, 1 - enabled, 2 - autodetect (win)
 #ifdef XP_WIN
 pref("dom.w3c_touch_events.enabled", 2);
 #endif
 
+#ifdef NIGHTLY_BUILD
+#if defined(XP_WIN) || defined(XP_LINUX) || defined(XP_MACOSX)
 // W3C draft pointer events
+pref("dom.w3c_pointer_events.enabled", true);
+// W3C touch-action css property (related to touch and pointer events)
+pref("layout.css.touch_action.enabled", true);
+#else
 pref("dom.w3c_pointer_events.enabled", false);
+pref("layout.css.touch_action.enabled", false);
+#endif
+#else
+pref("dom.w3c_pointer_events.enabled", false);
+pref("layout.css.touch_action.enabled", false);
+#endif
 
 // W3C draft ImageCapture API
 pref("dom.imagecapture.enabled", false);
 
-// W3C touch-action css property (related to touch and pointer events)
-pref("layout.css.touch_action.enabled", false);
-
 // Enables some assertions in nsStyleContext that are too expensive
 // for general use, but might be useful to enable for specific tests.
 // This only has an effect in DEBUG-builds.
 pref("layout.css.expensive-style-struct-assertions.enabled", false);
 
 // enable JS dump() function.
 pref("browser.dom.window.dump.enabled", false);
 
--- a/testing/taskcluster/scripts/phone-builder/build-spark-ota.sh
+++ b/testing/taskcluster/scripts/phone-builder/build-spark-ota.sh
@@ -13,25 +13,33 @@ if [ $TARGET == "aries" -o $TARGET == "s
 fi
 
 aws s3 cp s3://b2g-nightly-credentials/balrog_credentials .
 mar_file=b2g-${TARGET%%-*}-gecko-update.mar
 
 # We need different platform names for each variant (user, userdebug and
 # eng). We do not append variant suffix for "user" to keep compability with
 # verions already installed in the phones.
-if [ $VARIANT == "user" ]; then
+if [ 0$DOGFOOD -ne 0 -o $VARIANT == "user" ]; then
   PLATFORM=$TARGET
 else
   PLATFORM=$TARGET-$VARIANT
 fi
 
+if ! test $MOZHARNESS_CONFIG; then
+  MOZHARNESS_CONFIG=b2g/taskcluster-spark-nightly.py
+fi
+
+if ! test $BALROG_SERVER_CONFIG; then
+  BALROG_SERVER_CONFIG=balrog/docker-worker.py
+fi
+
 ./mozharness/scripts/b2g_lightsaber.py \
-  --config b2g/taskcluster-lightsaber-nightly.py \
-  --config balrog/docker-worker.py \
+  --config $MOZHARNESS_CONFIG \
+  --config $BALROG_SERVER_CONFIG \
   "$debug_flag" \
   --disable-mock \
   --variant=$VARIANT \
   --work-dir=$WORKSPACE/B2G \
   --gaia-languages-file locales/languages_all.json \
   --log-level=debug \
   --target=$TARGET \
   --b2g-config-dir=$TARGET \
--- a/testing/taskcluster/tasks/branches/base_job_flags.yml
+++ b/testing/taskcluster/tasks/branches/base_job_flags.yml
@@ -21,16 +21,17 @@ flags:
     - flame-kk-eng   # b2g flame eng build
     - dolphin
     - dolphin-eng
     - dolphin-512
     - dolphin-512-eng
     - aries
     - aries-ota
     - aries-eng
+    - aries-dogfood
     - android-api-11
     - linux64
 
   tests:
     - cppunit
     - crashtest
     - crashtest-ipc
     - gaia-build
--- a/testing/taskcluster/tasks/branches/mozilla-central/job_flags.yml
+++ b/testing/taskcluster/tasks/branches/mozilla-central/job_flags.yml
@@ -9,16 +9,22 @@ builds:
   aries-ota:
     platforms:
       - b2g
     types:
       opt:
         task: tasks/builds/b2g_aries_spark_ota_opt.yml
       debug:
         task: tasks/builds/b2g_aries_spark_ota_debug.yml
+  aries-dogfood:
+    platforms:
+      - b2g
+    types:
+      opt:
+        task: tasks/builds/b2g_aries_spark_dogfood.yml
   flame-kk-ota:
     platforms:
       - b2g
     types:
       opt:
         task: tasks/builds/b2g_flame_kk_ota_opt.yml
       debug:
         task: tasks/builds/b2g_flame_kk_ota_debug.yml
new file mode 100644
--- /dev/null
+++ b/testing/taskcluster/tasks/builds/b2g_aries_spark_dogfood.yml
@@ -0,0 +1,29 @@
+$inherits:
+  from: 'tasks/builds/b2g_aries_spark_ota_base.yml'
+  variables:
+    build_name: 'aries-dogfood'
+    build_type: 'debug'
+task:
+  metadata:
+    name: '[TC] B2G Aries Dogfood'
+  scopes:
+    - 'docker-worker:cache:build-aries-spark-dogfood'
+  payload:
+    cache:
+      build-aries-spark-dogfood: /home/worker/workspace
+    env:
+      VARIANT: userdebug
+      DOGFOOD: 1
+      MOZHARNESS_CONFIG: b2g/taskcluster-spark-dogfood.py
+  extra:
+    treeherderEnv:
+      - production
+      - staging
+    treeherder:
+      symbol: B
+      groupSymbol: Aries-DogFood
+      groupName: Aries Device Image
+      machine:
+        platform: b2g-device-image
+      collection:
+        debug: true
--- a/testing/web-platform/meta/pointerevents/pointerevent_touch-action-illegal.html.ini
+++ b/testing/web-platform/meta/pointerevents/pointerevent_touch-action-illegal.html.ini
@@ -1,12 +1,3 @@
 [pointerevent_touch-action-illegal.html]
   type: testharness
-  prefs: [dom.w3c_pointer_events.enabled:true]
-  ['pan-x none' is corrected properly]
-    expected: FAIL
-
-  ['pan-y none' is corrected properly]
-    expected: FAIL
-
-  ['auto none' is corrected properly]
-    expected: FAIL
-
+  prefs: [layout.css.touch_action.enabled:true]
--- a/testing/web-platform/meta/pointerevents/pointerevent_touch-action-verification.html.ini
+++ b/testing/web-platform/meta/pointerevents/pointerevent_touch-action-verification.html.ini
@@ -1,18 +1,3 @@
 [pointerevent_touch-action-verification.html]
   type: testharness
-  prefs: [dom.w3c_pointer_events.enabled:true]
-  ['auto' is set properly]
-    expected: FAIL
-
-  ['pan-x' is corrected properly]
-    expected: FAIL
-
-  ['pan-y' is set properly]
-    expected: FAIL
-
-  ['none' is set properly]
-    expected: FAIL
-
-  ['manipulation' is set properly]
-    expected: FAIL
-
+  prefs: [layout.css.touch_action.enabled:true]
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -310,27 +310,27 @@ class XPCShellTestThread(Thread):
         """
           Build the command line arguments for the test file.
           On a remote system, this may be overloaded to use a remote path structure.
         """
         return ['-e', 'const _TEST_FILE = ["%s"];' %
                   name.replace('\\', '/')]
 
     def setupTempDir(self):
-        tempDir = mkdtemp()
+        tempDir = mkdtemp(prefix='xpc-other-')
         self.env["XPCSHELL_TEST_TEMP_DIR"] = tempDir
         if self.interactive:
             self.log.info("temp dir is %s" % tempDir)
         return tempDir
 
     def setupPluginsDir(self):
         if not os.path.isdir(self.pluginsPath):
             return None
 
-        pluginsDir = mkdtemp()
+        pluginsDir = mkdtemp(prefix='xpc-plugins-')
         # shutil.copytree requires dst to not exist. Deleting the tempdir
         # would make a race condition possible in a concurrent environment,
         # so we are using dir_utils.copy_tree which accepts an existing dst
         dir_util.copy_tree(self.pluginsPath, pluginsDir)
         if self.interactive:
             self.log.info("plugins dir is %s" % pluginsDir)
         return pluginsDir
 
@@ -346,17 +346,17 @@ class XPCShellTestThread(Thread):
             profileDir = os.path.join(gettempdir(), self.profileName, "xpcshellprofile")
             try:
                 # This could be left over from previous runs
                 self.removeDir(profileDir)
             except:
                 pass
             os.makedirs(profileDir)
         else:
-            profileDir = mkdtemp()
+            profileDir = mkdtemp(prefix='xpc-profile-')
         self.env["XPCSHELL_TEST_PROFILE_DIR"] = profileDir
         if self.interactive or self.singleFile:
             self.log.info("profile dir is %s" % profileDir)
         return profileDir
 
     def buildCmdHead(self, headfiles, tailfiles, xpcscmd):
         """
           Build the command line arguments for the head and tail files,
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -1,9 +1,8 @@
-/* vim: set ts=2 sts=2 sw=2 et 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/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
                           "UserAutoCompleteResult" ];
@@ -447,58 +446,57 @@ var LoginManagerContent = {
             this._fillForm(form, true, false, true, true, loginsFound, recipes);
           })
           .then(null, Cu.reportError);
     } else {
       // Ignore the event, it's for some input we don't care about.
     }
   },
 
-
-  /*
-   * _getPasswordFields
-   *
-   * Returns an array of password field elements for the specified form.
-   * If no pw fields are found, or if more than 3 are found, then null
-   * is returned.
-   *
-   * skipEmptyFields can be set to ignore password fields with no value.
+  /**
+   * @param {FormLike} form - the FormLike to look for password fields in.
+   * @param {bool} [skipEmptyFields=false] - Whether to ignore password fields with no value.
+   *                                         Used at capture time since saving empty values isn't
+   *                                         useful.
+   * @return {Array|null} Array of password field elements for the specified form.
+   *                      If no pw fields are found, or if more than 3 are found, then null
+   *                      is returned.
    */
-  _getPasswordFields : function (form, skipEmptyFields) {
+  _getPasswordFields(form, skipEmptyFields = false) {
     // Locate the password fields in the form.
-    var pwFields = [];
-    for (var i = 0; i < form.elements.length; i++) {
-      var element = form.elements[i];
+    let pwFields = [];
+    for (let i = 0; i < form.elements.length; i++) {
+      let element = form.elements[i];
       if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
-          element.type != "password")
+          element.type != "password") {
         continue;
+      }
 
-      if (skipEmptyFields && !element.value)
+      if (skipEmptyFields && !element.value) {
         continue;
+      }
 
       pwFields[pwFields.length] = {
                                     index   : i,
                                     element : element
                                   };
     }
 
     // If too few or too many fields, bail out.
     if (pwFields.length == 0) {
       log("(form ignored -- no password fields.)");
       return null;
     } else if (pwFields.length > 3) {
-      log("(form ignored -- too many password fields. [ got ",
-                  pwFields.length, "])");
+      log("(form ignored -- too many password fields. [ got ", pwFields.length, "])");
       return null;
     }
 
     return pwFields;
   },
 
-
   _isUsernameFieldType: function(element) {
     if (!(element instanceof Ci.nsIDOMHTMLInputElement))
       return false;
 
     let fieldType = (element.hasAttribute("type") ?
                      element.getAttribute("type").toLowerCase() :
                      element.type);
     if (fieldType == "text"  ||
@@ -1086,8 +1084,81 @@ UserAutoCompleteResult.prototype = {
 
     if (removeFromDB) {
       var pwmgr = Cc["@mozilla.org/login-manager;1"].
                   getService(Ci.nsILoginManager);
       pwmgr.removeLogin(removedLogin);
     }
   }
 };
+
+/**
+ * A factory to generate FormLike objects that represent a set of login fields
+ * which aren't necessarily marked up with a <form> element.
+ */
+let FormLikeFactory = {
+  _propsFromForm: [
+      "action",
+      "autocomplete",
+  ],
+
+  /**
+   * Create a FormLike object from a <form>.
+   *
+   * @param {HTMLFormElement} aForm
+   * @return {FormLike}
+   * @throws Error if aForm isn't an HTMLFormElement
+   */
+  createFromForm(aForm) {
+    if (!(aForm instanceof Ci.nsIDOMHTMLFormElement)) {
+      throw new Error("createFromForm: aForm must be a nsIDOMHTMLFormElement");
+    }
+
+    let formLike = {
+      elements: [...aForm.elements],
+      ownerDocument: aForm.ownerDocument,
+      rootElement: aForm,
+    };
+
+    for (let prop of this._propsFromForm) {
+      formLike[prop] = aForm[prop];
+    }
+
+    return formLike;
+  },
+
+  /**
+   * Create a FormLike object from an <input type=password>.
+   *
+   * If the <input> is in a <form>, construct the FormLike from the form.
+   * Otherwise, create a FormLike with a rootElement (wrapper) according to
+   * heuristics. Currently all <input> not in a <form> are one FormLike but this
+   * shouldn't be relied upon as the heuristics may change to detect multiple
+   * "forms" (e.g. registration and login) on one page with a <form>.
+   *
+   * @param {HTMLInputElement} aPasswordField - a password field in a document
+   * @return {FormLike}
+   * @throws Error if aPasswordField isn't a password input in a document
+   */
+  createFromPasswordField(aPasswordField) {
+    if (!(aPasswordField instanceof Ci.nsIDOMHTMLInputElement) ||
+        aPasswordField.type != "password" ||
+        !aPasswordField.ownerDocument) {
+      throw new Error("createFromPasswordField requires a password field in a document");
+    }
+
+    if (aPasswordField.form) {
+      return this.createFromForm(aPasswordField.form);
+    }
+
+    let doc = aPasswordField.ownerDocument;
+    log("Created non-form FormLike for rootElement:", doc.documentElement);
+    return {
+      action: "",
+      autocomplete: "on",
+      // Exclude elements inside the rootElement that are already in a <form> as
+      // they will be handled by their own FormLike.
+      elements: [for (el of doc.querySelectorAll("input")) if (!el.form) el],
+      ownerDocument: doc,
+      rootElement: doc.documentElement,
+    };
+  },
+};
--- a/toolkit/components/passwordmgr/test/unit/head.js
+++ b/toolkit/components/passwordmgr/test/unit/head.js
@@ -119,20 +119,19 @@ function newPropertyBag(aProperties)
 ////////////////////////////////////////////////////////////////////////////////
 
 const RecipeHelpers = {
   initNewParent() {
     return (new LoginRecipesParent({ defaults: false })).initializationPromise;
   },
 
   /**
-   * Create a document for the given URL containing the given HTML containing a
-   * form and return the <form>.
+   * Create a document for the given URL containing the given HTML with the ownerDocument of all <form>s having a mocked location.
    */
-  createTestForm(aDocumentURL, aHTML = "<form>") {
+  createTestDocument(aDocumentURL, aHTML = "<form>") {
     let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                  createInstance(Ci.nsIDOMParser);
     parser.init();
     let parsedDoc = parser.parseFromString(aHTML, "text/html");
 
     // Mock the document.location object so we can unit test without a frame. We use a proxy
     // instead of just assigning to the property since it's not configurable or writable.
     let document = new Proxy(parsedDoc, {
@@ -141,24 +140,23 @@ const RecipeHelpers = {
         // See https://html.spec.whatwg.org/#the-location-interface
         if (property == "location") {
           return new URL(aDocumentURL);
         }
         return target[property];
       },
     });
 
-    let form = parsedDoc.forms[0];
-
-    // Assign form.ownerDocument to the proxy so document.location works.
-    Object.defineProperty(form, "ownerDocument", {
-      value: document,
-    });
-
-    return form;
+    for (let form of parsedDoc.forms) {
+      // Assign form.ownerDocument to the proxy so document.location works.
+      Object.defineProperty(form, "ownerDocument", {
+        value: document,
+      });
+    }
+    return parsedDoc;
   }
 };
 
 //// Initialization functions common to all tests
 
 add_task(function test_common_initialize()
 {
   // Before initializing the service for the first time, we should copy the key
@@ -171,8 +169,28 @@ add_task(function test_common_initialize
   yield Services.logins.initializationPromise;
 
   // Ensure that every test file starts with an empty database.
   LoginTestUtils.clearData();
 
   // Clean up after every test.
   do_register_cleanup(() => LoginTestUtils.clearData());
 });
+
+/**
+ * Compare two FormLike to see if they represent the same information. Elements
+ * are compared using their @id attribute.
+ */
+function formLikeEqual(a, b) {
+  Assert.strictEqual(Object.keys(a).length, Object.keys(b).length,
+                     "Check the formLikes have the same number of properties");
+
+  for (let propName of Object.keys(a)) {
+    if (propName == "elements") {
+      Assert.strictEqual(a.elements.length, b.elements.length, "Check element count");
+      for (let i = 0; i < a.elements.length; i++) {
+        Assert.strictEqual(a.elements[i].id, b.elements[i].id, "Check element " + i + " id");
+      }
+      continue;
+    }
+    Assert.strictEqual(a[propName], b[propName], "Compare formLike " + propName + " property");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/components/passwordmgr/test/unit/test_getPasswordFields.js
@@ -0,0 +1,149 @@
+/*
+ * Test for LoginManagerContent._getPasswordFields using FormLikeFactory.
+ */
+
+"use strict";
+
+const LMCBackstagePass = Cu.import("resource://gre/modules/LoginManagerContent.jsm");
+const { LoginManagerContent, FormLikeFactory } = LMCBackstagePass;
+const TESTCASES = [
+  {
+    description: "Empty document",
+    document: ``,
+    returnedFieldIDsByFormLike: [],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "Non-password input with no <form> present",
+    document: `<input>`,
+    returnedFieldIDsByFormLike: [],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "1 password field outside of a <form>",
+    document: `<input id="pw1" type=password>`,
+    returnedFieldIDsByFormLike: [["pw1"]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "4 empty password fields outside of a <form>",
+    document: `<input id="pw1" type=password>
+      <input id="pw2" type=password>
+      <input id="pw3" type=password>
+      <input id="pw4" type=password>`,
+    returnedFieldIDsByFormLike: [[]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "4 password fields outside of a <form> (1 empty, 3 full) with skipEmpty",
+    document: `<input id="pw1" type=password>
+      <input id="pw2" type=password value="pass2">
+      <input id="pw3" type=password value="pass3">
+      <input id="pw4" type=password value="pass4">`,
+    returnedFieldIDsByFormLike: [["pw2", "pw3", "pw4"]],
+    skipEmptyFields: true,
+  },
+  {
+    description: "Form with 1 password field",
+    document: `<form><input id="pw1" type=password></form>`,
+    returnedFieldIDsByFormLike: [["pw1"]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "Form with 2 password fields",
+    document: `<form><input id="pw1" type=password><input id='pw2' type=password></form>`,
+    returnedFieldIDsByFormLike: [["pw1", "pw2"]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "1 password field in a form, 1 outside",
+    document: `<form><input id="pw1" type=password></form><input id="pw2" type=password>`,
+    returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "2 password fields outside of a <form> with 1 linked via @form",
+    document: `<input id="pw1" type=password><input id="pw2" type=password form='form1'>
+      <form id="form1"></form>`,
+    returnedFieldIDsByFormLike: [["pw1"], ["pw2"]],
+    skipEmptyFields: undefined,
+  },
+  {
+    description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty",
+    document: `<input id="pw1" type=password><input id="pw2" type=password form="form1">
+      <form id="form1"></form>`,
+    returnedFieldIDsByFormLike: [[],[]],
+    skipEmptyFields: true,
+  },
+  {
+    description: "2 password fields outside of a <form> with 1 linked via @form + skipEmpty with 1 empty",
+    document: `<input id="pw1" type=password value="pass1"><input id="pw2" type=password form="form1">
+      <form id="form1"></form>`,
+    returnedFieldIDsByFormLike: [["pw1"],[]],
+    skipEmptyFields: true,
+  },
+];
+
+for (let tc of TESTCASES) {
+  do_print("Sanity checking the testcase: " + tc.description);
+
+  (function() {
+    let testcase = tc;
+    add_task(function*() {
+      do_print("Starting testcase: " + testcase.description);
+      let document = RecipeHelpers.createTestDocument("http://localhost:8080/test/",
+                                                      testcase.document);
+
+      let mapRootElementToFormLike = new Map();
+      for (let input of document.querySelectorAll("input")) {
+        if (input.type != "password") {
+          continue;
+        }
+
+        let formLike = FormLikeFactory.createFromPasswordField(input);
+        let existingFormLike = mapRootElementToFormLike.get(formLike.rootElement);
+        if (!existingFormLike) {
+          mapRootElementToFormLike.set(formLike.rootElement, formLike);
+          continue;
+        }
+
+        // If the formLike is already present, ensure that the properties are the same.
+        do_print("Checking if the new FormLike for the same root has the same properties");
+        formLikeEqual(formLike, existingFormLike);
+      }
+
+      Assert.strictEqual(mapRootElementToFormLike.size, testcase.returnedFieldIDsByFormLike.length,
+                         "Check the correct number of different formLikes were returned");
+
+      let formLikeIndex = -1;
+      for (let formLikeFromInput of mapRootElementToFormLike.values()) {
+        formLikeIndex++;
+        let pwFields = LoginManagerContent._getPasswordFields(formLikeFromInput,
+                                                              testcase.skipEmptyFields);
+
+        if (formLikeFromInput.rootElement instanceof Ci.nsIDOMHTMLFormElement) {
+          let formLikeFromForm = FormLikeFactory.createFromForm(formLikeFromInput.rootElement);
+          do_print("Checking that the FormLike created for the <form> matches" +
+                   " the one from a password field");
+          formLikeEqual(formLikeFromInput, formLikeFromForm);
+        }
+
+
+        if (testcase.returnedFieldIDsByFormLike[formLikeIndex].length === 0) {
+          Assert.strictEqual(pwFields, null,
+                             "If no password fields were found null should be returned");
+        } else {
+          Assert.strictEqual(pwFields.length,
+                             testcase.returnedFieldIDsByFormLike[formLikeIndex].length,
+                             "Check the # of password fields for formLike #" + formLikeIndex);
+        }
+
+        for (let i = 0; i < testcase.returnedFieldIDsByFormLike[formLikeIndex].length; i++) {
+          let expectedID = testcase.returnedFieldIDsByFormLike[formLikeIndex][i];
+          Assert.strictEqual(pwFields[i].element.id, expectedID,
+                             "Check password field " + i + " ID");
+        }
+      }
+    });
+  })();
+}
--- a/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
+++ b/toolkit/components/passwordmgr/test/unit/test_recipes_content.js
@@ -2,21 +2,18 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test filtering recipes in LoginRecipesContent.
  */
 
 "use strict";
 
-Cu.import("resource://testing-common/httpd.js");
 Cu.importGlobalProperties(["URL"]);
 
-Cu.import("resource://gre/modules/devtools/Console.jsm");
-
 add_task(function* test_getFieldOverrides() {
   let recipes = new Set([
     { // path doesn't match but otherwise good
       hosts: ["example.com:8080"],
       passwordSelector: "#password",
       pathRegex: /^\/$/,
       usernameSelector: ".username",
     },
@@ -27,15 +24,16 @@ add_task(function* test_getFieldOverride
       description: "best match",
       hosts: ["a.invalid", "example.com:8080", "other.invalid"],
       passwordSelector: "#password",
       pathRegex: /^\/first\/second\/$/,
       usernameSelector: ".username",
     },
   ]);
 
-  let form = RecipeHelpers.createTestForm("http://localhost:8080/first/second/");
+  let form = RecipeHelpers.createTestDocument("http://localhost:8080/first/second/", "<form>").
+             forms[0];
   let override = LoginRecipesContent.getFieldOverrides(recipes, form);
   Assert.strictEqual(override.description, "best match",
                      "Check the best field override recipe was returned");
   Assert.strictEqual(override.usernameSelector, ".username", "Check usernameSelector");
   Assert.strictEqual(override.passwordSelector, "#password", "Check passwordSelector");
 });
--- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini
+++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini
@@ -11,16 +11,17 @@ skip-if = os == "android"
 skip-if = os == "android"
 
 # Test SQLite database backup and migration, applicable to Android only.
 [test_storage_mozStorage.js]
 skip-if = os != "android"
 
 # The following tests apply to any storage back-end.
 [test_disabled_hosts.js]
+[test_getPasswordFields.js]
 [test_legacy_empty_formSubmitURL.js]
 [test_legacy_validation.js]
 [test_logins_change.js]
 [test_logins_decrypt_failure.js]
 [test_logins_metainfo.js]
 [test_logins_search.js]
 [test_notifications.js]
 [test_recipes_add.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8065,10 +8065,34 @@
     "expires_in_version": "never",
     "kind": "count",
     "description": "Record the permissions.sqlite init failure"
   },
   "DEFECTIVE_PERMISSIONS_SQL_REMOVED": {
     "expires_in_version": "never",
     "kind": "count",
     "description": "Record the removal of defective permissions.sqlite"
+  },
+  "FENNEC_TABQUEUE_QUEUESIZE" : {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "high": "50",
+    "n_buckets": 10,
+    "description": "The number of tabs queued when opened."
+  },
+  "FENNEC_TABQUEUE_PROMPT_ENABLE_YES" : {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 3,
+    "description": "The number of times the tab queue prompt was seen before the user selected YES."
+  },
+  "FENNEC_TABQUEUE_PROMPT_ENABLE_NO" : {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 3,
+    "description": "The number of times the tab queue prompt was seen before the user selected NO."
+  },
+  "FENNEC_TABQUEUE_ENABLED": {
+    "expires_in_version": "never",
+    "kind": "boolean",
+    "description": "Has the tab queue functionality been enabled."
   }
 }
--- a/toolkit/devtools/server/actors/highlighter.js
+++ b/toolkit/devtools/server/actors/highlighter.js
@@ -344,22 +344,20 @@ let HighlighterActor = exports.Highlight
 
     this._tabActor.window.focus();
     this._startPickerListeners();
 
     return null;
   }),
 
   _findAndAttachElement: function(event) {
-    let doc = event.target.ownerDocument;
-
-    let x = event.clientX;
-    let y = event.clientY;
-
-    let node = doc.elementFromPoint(x, y);
+    // originalTarget allows access to the "real" element before any retargeting
+    // is applied, such as in the case of XBL anonymous elements.  See also
+    // https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
+    let node = event.originalTarget || event.target;
     return this._walker.attachElement(node);
   },
 
   _startPickerListeners: function() {
     let target = getPageListenerTarget(this._tabActor);
     target.addEventListener("mousemove", this._onHovered, true);
     target.addEventListener("click", this._onPick, true);
     target.addEventListener("mousedown", this._preventContentEvent, true);
--- a/toolkit/devtools/transport/transport.js
+++ b/toolkit/devtools/transport/transport.js
@@ -749,9 +749,125 @@ ChildDebuggerTransport.prototype = {
 
   startBulkSend: function() {
     throw new Error("Can't send bulk data to child processes.");
   }
 };
 
 exports.ChildDebuggerTransport = ChildDebuggerTransport;
 
+// WorkerDebuggerTransport is defined differently depending on whether we are
+// on the main thread or a worker thread. In the former case, we are required
+// by the devtools loader, and isWorker will be false. Otherwise, we are
+// required by the worker loader, and isWorker will be true.
+//
+// Each worker debugger supports only a single connection to the main thread.
+// However, its theoretically possible for multiple servers to connect to the
+// same worker. Consequently, each transport has a connection id, to allow
+// messages from multiple connections to be multiplexed on a single channel.
+
+if (!this.isWorker) {
+  (function () { // Main thread
+    /**
+     * A transport that uses a WorkerDebugger to send packets from the main
+     * thread to a worker thread.
+     */
+    function WorkerDebuggerTransport(dbg, id) {
+      this._dbg = dbg;
+      this._id = id;
+      this.onMessage = this._onMessage.bind(this);
+    }
+
+    WorkerDebuggerTransport.prototype = {
+      constructor: WorkerDebuggerTransport,
+
+      ready: function () {
+        this._dbg.addListener(this);
+      },
+
+      close: function () {
+        this._dbg.removeListener(this);
+        if (this.hooks) {
+          this.hooks.onClosed();
+        }
+      },
+
+      send: function (packet) {
+        this._dbg.postMessage(JSON.stringify({
+          type: "message",
+          id: this._id,
+          message: packet
+        }));
+      },
+
+      startBulkSend: function () {
+        throw new Error("Can't send bulk data from worker threads!");
+      },
+
+      _onMessage: function (message) {
+        let packet = JSON.parse(message);
+        if (packet.type !== "message" || packet.id !== this._id) {
+          return;
+        }
+
+        if (this.hooks) {
+          this.hooks.onPacket(packet.message);
+        }
+      }
+    };
+
+    exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
+  }).call(this);
+} else {
+  (function () { // Worker thread
+    /*
+     * A transport that uses a WorkerDebuggerGlobalScope to send packets from a
+     * worker thread to the main thread.
+     */
+    function WorkerDebuggerTransport(scope, id) {
+      this._scope = scope;
+      this._id = id;
+      this._onMessage = this._onMessage.bind(this);
+    }
+
+    WorkerDebuggerTransport.prototype = {
+      constructor: WorkerDebuggerTransport,
+
+      ready: function () {
+        this._scope.addEventListener("message", this._onMessage);
+      },
+
+      close: function () {
+        this._scope.removeEventListener("message", this._onMessage);
+        if (this.hooks) {
+          this.hooks.onClosed();
+        }
+      },
+
+      send: function (packet) {
+        this._scope.postMessage(JSON.stringify({
+          type: "message",
+          id: this._id,
+          message: packet
+        }));
+      },
+
+      startBulkSend: function () {
+        throw new Error("Can't send bulk data from worker threads!");
+      },
+
+      _onMessage: function (event) {
+        let packet = JSON.parse(event.data);
+        if (packet.type !== "message" || packet.id !== this._id) {
+          return;
+        }
+
+        if (this.hooks) {
+          this.hooks.onPacket(packet.message);
+        }
+      }
+    };
+
+    exports.WorkerDebuggerTransport = WorkerDebuggerTransport;
+  }).call(this);
+}
+
 });
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -302,18 +302,20 @@ static inline size_t
 mmin(size_t a, size_t b)
 {
   return (a > b) ? b : a;
 }
 
 static NS_tchar*
 mstrtok(const NS_tchar *delims, NS_tchar **str)
 {
-  if (!*str || !**str)
+  if (!*str || !**str) {
+    *str = nullptr;
     return nullptr;
+  }
 
   // skip leading "whitespace"
   NS_tchar *ret = *str;
   const NS_tchar *d;
   do {
     for (d = delims; *d != NS_T('\0'); ++d) {
       if (*ret == *d) {
         ++ret;
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -171,20 +171,20 @@ separator.groove[orient="vertical"] {
   margin: 0 !important;
   border: none;
   padding: 0;
 }
 
 description,
 label {
   cursor: default;
-  /* FIXME: On Windows and Linux, we're using -moz-margin-end: 5px, but for
-            unknown reasons this breaks test_bug477754.xul on OS X.
-            See bug 1169606. */
-  margin: 1px 6px 2px;
+  margin-top: 1px;
+  margin-bottom: 2px;
+  -moz-margin-start: 6px;
+  -moz-margin-end: 5px;
 }
 
 description {
   margin-bottom: 4px;
 }
 
 label[disabled="true"] {
   color: GrayText;