Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 10 Jun 2014 18:47:36 -0700
changeset 208810 75377bac231e3017bef14f1d1a5942ae67c08777
parent 208780 18293c58c89a978910bcfbb424359e5600eb8ccb (current diff)
parent 208809 ebbdf5f1a4a0955b34668ee249fc8928de0c94ed (diff)
child 208811 18c21532848a1673c6c3fe57e710fbf6e9cb35e1
child 208849 a6db5975136ad661319ca54010359a8703c571c3
child 208907 5695da975597ce941290f80e70e4df7910342e0c
child 208951 510548c5318114009c3d671d44e7e861d0726e69
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
browser/themes/linux/tabbrowser/tab-overflow-border.png
browser/themes/osx/tabbrowser/tab-overflow-border.png
browser/themes/windows/tabbrowser/tab-overflow-border.png
mobile/android/base/WebappAllocator.java
mobile/android/base/WebappImpl.java
toolkit/components/telemetry/Histograms.json
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -1924,23 +1924,31 @@ let CustomizableUIInternal = {
       if (cache) {
         cache.delete(widget.id);
       }
     }
 
     this.notifyListeners("onWidgetCreated", widget.id);
 
     if (widget.defaultArea) {
+      let addToDefaultPlacements = false;
       let area = gAreas.get(widget.defaultArea);
-      //XXXgijs this won't have any effect for legacy items. Sort of OK because
-      // consumers can modify currentset? Maybe?
-      if (area.has("defaultPlacements")) {
-        area.get("defaultPlacements").push(widget.id);
-      } else {
-        area.set("defaultPlacements", [widget.id]);
+      if (widget.source == CustomizableUI.SOURCE_BUILTIN) {
+        addToDefaultPlacements = true;
+      } else if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
+                 widget.defaultArea != CustomizableUI.AREA_PANEL) {
+        addToDefaultPlacements = true;
+      }
+
+      if (addToDefaultPlacements) {
+        if (area.has("defaultPlacements")) {
+          area.get("defaultPlacements").push(widget.id);
+        } else {
+          area.set("defaultPlacements", [widget.id]);
+        }
       }
     }
 
     // Look through previously saved state to see if we're restoring a widget.
     let seenAreas = new Set();
     let widgetMightNeedAutoAdding = true;
     for (let [area, placements] of gPlacements) {
       seenAreas.add(area);
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -660,17 +660,17 @@ CustomizeMode.prototype = {
     let numberOfAreas = areas.length;
     for (let i = 0; i < numberOfAreas; i++) {
       let area = areas[i];
       let areaNode = aNode.ownerDocument.getElementById(area);
       let customizationTarget = areaNode && areaNode.customizationTarget;
       if (customizationTarget && customizationTarget != areaNode) {
         areas.push(customizationTarget.id);
       }
-      let overflowTarget = areaNode.getAttribute("overflowtarget");
+      let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget");
       if (overflowTarget) {
         areas.push(overflowTarget);
       }
     }
     areas.push(kPaletteId);
 
     while (aNode && aNode.parentNode) {
       let parent = aNode.parentNode;
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -89,16 +89,17 @@ skip-if = e10s # Bug ?????? - test uses 
 [browser_975719_customtoolbars_behaviour.js]
 
 [browser_976792_insertNodeInWindow.js]
 skip-if = os == "linux"
 
 [browser_978084_dragEnd_after_move.js]
 [browser_980155_add_overflow_toolbar.js]
 [browser_981418-widget-onbeforecreated-handler.js]
+[browser_982656_restore_defaults_builtin_widgets.js]
 
 [browser_984455_bookmarks_items_reparenting.js]
 skip-if = os == "linux"
 
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_988072_sidebar_events.js]
 [browser_989751_subviewbutton_class.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Restoring default should not place addon widgets back in the toolbar
+add_task(function() {
+  ok(CustomizableUI.inDefaultState, "Default state to begin");
+
+  const kWidgetId = "bug982656-add-on-widget-should-not-restore-to-default-area";
+  let widgetSpec = {
+    id: kWidgetId,
+    defaultArea: CustomizableUI.AREA_NAVBAR
+  };
+  CustomizableUI.createWidget(widgetSpec);
+
+  ok(!CustomizableUI.inDefaultState, "Not in default state after widget added");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar");
+
+  yield resetCustomization();
+
+  ok(CustomizableUI.inDefaultState, "Back in default state after reset");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId), null, "Widget now in palette");
+  CustomizableUI.destroyWidget(kWidgetId);
+});
+
+
+// resetCustomization shouldn't move 3rd party widgets out of custom toolbars
+add_task(function() {
+  const kToolbarId = "bug982656-toolbar-with-defaultset";
+  const kWidgetId = "bug982656-add-on-widget-should-restore-to-default-area-when-area-is-not-builtin";
+  ok(CustomizableUI.inDefaultState, "Everything should be in its default state.");
+  let toolbar = createToolbarWithPlacements(kToolbarId);
+  ok(CustomizableUI.areas.indexOf(kToolbarId) != -1,
+     "Toolbar has been registered.");
+  is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR,
+     "Area should be registered as toolbar");
+
+  let widgetSpec = {
+    id: kWidgetId,
+    defaultArea: kToolbarId
+  };
+  CustomizableUI.createWidget(widgetSpec);
+
+  ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible.");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget should be in custom toolbar");
+
+  yield resetCustomization();
+  ok(CustomizableUI.inDefaultState, "Back in default state after reset");
+  is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget still in custom toolbar");
+  ok(toolbar.collapsed, "Custom toolbar should be collapsed after reset");
+
+  toolbar.remove();
+  CustomizableUI.destroyWidget(kWidgetId);
+  CustomizableUI.unregisterArea(kToolbarId);
+});
--- a/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
+++ b/browser/devtools/inspector/test/browser_inspector_pseudoclass_lock.js
@@ -36,35 +36,45 @@ let startTests = Task.async(function*() 
   yield performTests(inspector, view);
 
   yield finishUp(toolbox);
   finish();
 });
 
 function* performTests(inspector, ruleview) {
   yield togglePseudoClass(inspector);
-  yield testAdded(inspector, ruleview);
+  yield assertPseudoAddedToNode(inspector, ruleview);
 
   yield togglePseudoClass(inspector);
-  yield testRemoved();
-  yield testRemovedFromUI(inspector, ruleview);
+  yield assertPseudoRemovedFromNode();
+  yield assertPseudoRemovedFromView(inspector, ruleview);
 
   yield togglePseudoClass(inspector);
   yield testNavigate(inspector, ruleview);
 }
 
 function* togglePseudoClass(inspector) {
-  info("Toggle the pseudoclass, wait for the pseudoclass event and wait for the refresh of the rule view");
+  info("Toggle the pseudoclass, wait for it to be applied");
 
+  // Give the inspector panels a chance to update when the pseudoclass changes
   let onPseudo = inspector.selection.once("pseudoclass");
   let onRefresh = inspector.once("rule-view-refreshed");
-  inspector.togglePseudoClass(PSEUDO);
+  let onMutations = waitForMutation(inspector);
+
+  yield inspector.togglePseudoClass(PSEUDO);
 
   yield onPseudo;
   yield onRefresh;
+  yield onMutations;
+}
+
+function waitForMutation(inspector) {
+  let def = promise.defer();
+  inspector.walker.once("mutations", def.resolve);
+  return def.promise;
 }
 
 function* testNavigate(inspector, ruleview) {
   yield selectNode("#parent-div", inspector);
 
   info("Make sure the pseudoclass is still on after navigating to a parent");
   is(DOMUtils.hasPseudoClassLock(getNode("#div-1"), PSEUDO), true,
     "pseudo-class lock is still applied after inspecting ancestor");
@@ -82,17 +92,17 @@ function* testNavigate(inspector, rulevi
   yield inspector.once("computed-view-refreshed");
 }
 
 function showPickerOn(node, inspector) {
   let highlighter = inspector.toolbox.highlighter;
   return highlighter.showBoxModel(getNodeFront(node));
 }
 
-function* testAdded(inspector, ruleview) {
+function* assertPseudoAddedToNode(inspector, ruleview) {
   info("Make sure the pseudoclass lock is applied to #div-1 and its ancestors");
   let node = getNode("#div-1");
   do {
     is(DOMUtils.hasPseudoClassLock(node, PSEUDO), true,
       "pseudo-class lock has been applied");
     node = node.parentNode;
   } while (node.parentNode)
 
@@ -105,38 +115,38 @@ function* testAdded(inspector, ruleview)
   yield showPickerOn(getNode("#div-1"), inspector);
 
   info("Check that the infobar selector contains the pseudo-class");
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, PSEUDO, "pseudo-class in infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
-function* testRemoved() {
+function* assertPseudoRemovedFromNode() {
   info("Make sure the pseudoclass lock is removed from #div-1 and its ancestors");
   let node = getNode("#div-1");
   do {
     is(DOMUtils.hasPseudoClassLock(node, PSEUDO), false,
        "pseudo-class lock has been removed");
     node = node.parentNode;
   } while (node.parentNode)
 }
 
-function* testRemovedFromUI(inspector, ruleview) {
+function* assertPseudoRemovedFromView(inspector, ruleview) {
   info("Check that the ruleview no longer contains the pseudo-class rule");
   let rules = ruleview.element.querySelectorAll(".ruleview-rule.theme-separator");
   is(rules.length, 2, "rule view is showing 2 rules after removing lock");
 
   yield showPickerOn(getNode("#div-1"), inspector);
 
   let pseudoClassesBox = getHighlighter().querySelector(".highlighter-nodeinfobar-pseudo-classes");
   is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");
   yield inspector.toolbox.highlighter.hideBoxModel();
 }
 
 function* finishUp(toolbox) {
   let onDestroy = gDevTools.once("toolbox-destroyed");
   toolbox.destroy();
   yield onDestroy;
 
-  yield testRemoved(getNode("#div-1"));
+  yield assertPseudoRemovedFromNode(getNode("#div-1"));
   gBrowser.removeCurrentTab();
 }
--- a/browser/devtools/shared/test/browser_graphs-01.js
+++ b/browser/devtools/shared/test/browser_graphs-01.js
@@ -15,17 +15,22 @@ let test = Task.async(function*() {
   finish();
 });
 
 function* performTest() {
   let [host, win, doc] = yield createHost();
   doc.body.setAttribute("style", "position: fixed; width: 100%; height: 100%; margin: 0;");
 
   let graph = new LineGraphWidget(doc.body, "fps");
-  yield graph.once("ready");
+
+  let readyEventEmitted;
+  graph.once("ready", () => readyEventEmitted = true);
+
+  yield graph.ready();
+  ok(readyEventEmitted, "The 'ready' event should have been emitted");
 
   testGraph(host, graph);
 
   graph.destroy();
   host.destroy();
 }
 
 function testGraph(host, graph) {
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://gre/modules/devtools/event-emitter.js");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
 
 this.EXPORTED_SYMBOLS = ["LineGraphWidget"];
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
 
 // Generic constants.
 
@@ -111,16 +112,17 @@ GraphSelectionResizer.prototype = {
  *        The graph type, used for setting the correct class names.
  *        Currently supported: "line-graph" only.
  * @param number sharpness [optional]
  *        Defaults to the current device pixel ratio.
  */
 this.AbstractCanvasGraph = function(parent, name, sharpness) {
   EventEmitter.decorate(this);
 
+  this._ready = promise.defer();
   this._parent = parent;
   this._uid = "canvas-graph-" + Date.now();
 
   AbstractCanvasGraph.createIframe(GRAPH_SRC, parent, iframe => {
     this._iframe = iframe;
     this._window = iframe.contentWindow;
     this._document = iframe.contentDocument;
     this._pixelRatio = sharpness || this._window.devicePixelRatio;
@@ -160,16 +162,17 @@ this.AbstractCanvasGraph = function(pare
     container.addEventListener("MozMousePixelScroll", this._onMouseWheel);
     container.addEventListener("mouseout", this._onMouseOut);
 
     let ownerWindow = this._parent.ownerDocument.defaultView;
     ownerWindow.addEventListener("resize", this._onResize);
 
     this._animationId = this._window.requestAnimationFrame(this._onAnimationFrame);
 
+    this._ready.resolve(this);
     this.emit("ready", this);
   });
 }
 
 AbstractCanvasGraph.prototype = {
   /**
    * Read-only width and height of the canvas.
    * @return number
@@ -177,16 +180,23 @@ AbstractCanvasGraph.prototype = {
   get width() {
     return this._width;
   },
   get height() {
     return this._height;
   },
 
   /**
+   * Returns a promise resolved once this graph is ready to receive data.
+   */
+  ready: function() {
+    return this._ready.promise;
+  },
+
+  /**
    * Destroys this graph.
    */
   destroy: function() {
     let container = this._container;
     container.removeEventListener("mousemove", this._onMouseMove);
     container.removeEventListener("mousedown", this._onMouseDown);
     container.removeEventListener("mouseup", this._onMouseUp);
     container.removeEventListener("MozMousePixelScroll", this._onMouseWheel);
@@ -496,25 +506,26 @@ AbstractCanvasGraph.prototype = {
   _drawWidget: function() {
     if (!this._shouldRedraw) {
       return;
     }
 
     let ctx = this._ctx;
     ctx.clearRect(0, 0, this._width, this._height);
 
+    // Draw the graph underneath the cursor and selection.
+    if (this.hasData()) {
+      ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
+    }
     if (this.hasCursor()) {
       this._drawCliphead();
     }
     if (this.hasSelection() || this.hasSelectionInProgress()) {
       this._drawSelection();
     }
-    if (this.hasData()) {
-      ctx.drawImage(this._cachedGraphImage, 0, 0, this._width, this._height);
-    }
 
     this._shouldRedraw = false;
   },
 
   /**
    * Draws the cliphead, if available and necessary.
    */
   _drawCliphead: function() {
@@ -952,34 +963,27 @@ LineGraphWidget.prototype = Heritage.ext
    * @see AbstractCanvasGraph.prototype.buildGraphImage
    */
   buildGraphImage: function() {
     let canvas = this._document.createElementNS(HTML_NS, "canvas");
     let ctx = canvas.getContext("2d");
     let width = canvas.width = this._width;
     let height = canvas.height = this._height;
 
+    let totalTicks = this._data.length;
+    let firstTick = this._data[0].delta;
+    let lastTick = this._data[totalTicks - 1].delta;
     let maxValue = Number.MIN_SAFE_INTEGER;
     let minValue = Number.MAX_SAFE_INTEGER;
     let sumValues = 0;
-    let totalTicks = 0;
-    let firstTick;
-    let lastTick;
 
     for (let { delta, value } of this._data) {
       maxValue = Math.max(value, maxValue);
       minValue = Math.min(value, minValue);
       sumValues += value;
-      totalTicks++;
-
-      if (!firstTick) {
-        firstTick = delta;
-      } else {
-        lastTick = delta;
-      }
     }
 
     let dataScaleX = this.dataScaleX = width / lastTick;
     let dataScaleY = this.dataScaleY = height / maxValue * GRAPH_DAMPEN_VALUES;
 
     /**
      * Calculates the squared distance between two 2D points.
      */
@@ -992,17 +996,16 @@ LineGraphWidget.prototype = Heritage.ext
     // Draw the graph.
 
     let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
     gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
     gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
     ctx.fillStyle = gradient;
     ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
     ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH;
-    ctx.setLineDash([]);
     ctx.beginPath();
 
     let prevX = 0;
     let prevY = 0;
 
     for (let { delta, value } of this._data) {
       let currX = delta * dataScaleX;
       let currY = height - value * dataScaleY;
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -372,31 +372,27 @@ exports.AppManager = AppManager = {
         };
         yield AppActorFront.installHosted(client,
                                           actor,
                                           appId,
                                           metadata,
                                           project.manifest);
       }
 
-      function waitUntilProjectRuns() {
+      let manifest = self.getProjectManifestURL(project);
+      if (!self._runningApps.has(manifest)) {
         let deferred = promise.defer();
         self.on("app-manager-update", function onUpdate(event, what) {
           if (what == "project-is-running") {
             self.off("app-manager-update", onUpdate);
             deferred.resolve();
           }
         });
-        return deferred.promise;
-      }
-
-      let manifest = self.getProjectManifestURL(project);
-      if (!self._runningApps.has(manifest)) {
         yield AppActorFront.launchApp(client, actor, manifest);
-        yield waitUntilProjectRuns();
+        yield deferred.promise;
 
       } else {
         yield AppActorFront.reloadApp(client, actor, manifest);
       }
     });
   },
 
   stopRunningApp: function() {
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1824,47 +1824,26 @@ richlistitem[type~="action"][actiontype=
 
 /* Tabbrowser arrowscrollbox arrows */
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
   -moz-appearance: none;
   margin: 0 0 @tabToolbarNavbarOverlap@;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up {
-  -moz-border-start: 0;
-  -moz-border-end: 2px solid transparent;
-}
-
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
-  -moz-border-start: 2px solid transparent;
-  -moz-border-end: 0;
   transition: 1s box-shadow ease-out;
   border-radius: 4px;
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
   box-shadow: 0 0 5px 5px Highlight inset;
   transition: none;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 0 0 2px;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 #TabsToolbar .toolbarbutton-1 {
   margin-bottom: @tabToolbarNavbarOverlap@;
 }
 
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-icon,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menu-dropmarker,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
   margin-top: -2px;
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -160,17 +160,17 @@ browser.jar:
   skin/classic/browser/social/gear_default.png        (../shared/social/gear_default.png)
   skin/classic/browser/social/gear_clicked.png        (../shared/social/gear_clicked.png)
   skin/classic/browser/tabbrowser/connecting.png      (tabbrowser/connecting.png)
   skin/classic/browser/tabbrowser/loading.png         (tabbrowser/loading.png)
   skin/classic/browser/tabbrowser/tab-active-middle.png     (tabbrowser/tab-active-middle.png)
   skin/classic/browser/tabbrowser/tab-background-end.png    (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-start.png  (tabbrowser/tab-background-start.png)
-  skin/classic/browser/tabbrowser/tab-overflow-border.png   (tabbrowser/tab-overflow-border.png)
+  skin/classic/browser/tabbrowser/tab-overflow-indicator.png (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
   skin/classic/browser/tabbrowser/tab-selected-end.svg      (tab-selected-end.svg)
   skin/classic/browser/tabbrowser/tab-selected-start.svg    (tab-selected-start.svg)
 
   skin/classic/browser/tabbrowser/tab-stroke-end.png        (tabbrowser/tab-stroke-end.png)
   skin/classic/browser/tabbrowser/tab-stroke-start.png      (tabbrowser/tab-stroke-start.png)
deleted file mode 100644
index 77f2462e5bfda652074261e2e160fcbc99c3e04e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -2966,30 +2966,16 @@ toolbarbutton.chevron > .toolbarbutton-m
   }
 
   .tabbrowser-arrowscrollbox > .scrollbutton-up > .toolbarbutton-icon,
   .tabbrowser-arrowscrollbox > .scrollbutton-down > .toolbarbutton-icon {
     width: 13px;
   }
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):-moz-locale-dir(ltr),
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):-moz-locale-dir(rtl) {
-  border-width: 0 0 0 2px;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 /**
  * Tabstrip & add-on bar toolbar buttons
  */
 
 #TabsToolbar .toolbarbutton-1,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 #TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-appearance: none;
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -272,27 +272,27 @@ browser.jar:
   skin/classic/browser/tabbrowser/tab-arrow-right-inverted.png           (tabbrowser/tab-arrow-right-inverted.png)
   skin/classic/browser/tabbrowser/tab-arrow-right-inverted@2x.png        (tabbrowser/tab-arrow-right-inverted@2x.png)
   skin/classic/browser/tabbrowser/tab-background-end.png                 (tabbrowser/tab-background-end.png)
   skin/classic/browser/tabbrowser/tab-background-end@2x.png              (tabbrowser/tab-background-end@2x.png)
   skin/classic/browser/tabbrowser/tab-background-middle.png              (tabbrowser/tab-background-middle.png)
   skin/classic/browser/tabbrowser/tab-background-middle@2x.png           (tabbrowser/tab-background-middle@2x.png)
   skin/classic/browser/tabbrowser/tab-background-start.png               (tabbrowser/tab-background-start.png)
   skin/classic/browser/tabbrowser/tab-background-start@2x.png            (tabbrowser/tab-background-start@2x.png)
+  skin/classic/browser/tabbrowser/tab-overflow-indicator.png             (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
   skin/classic/browser/tabbrowser/tab-selected-end.svg                   (tab-selected-end.svg)
   skin/classic/browser/tabbrowser/tab-selected-start.svg                 (tab-selected-start.svg)
 
   skin/classic/browser/tabbrowser/tab-stroke-end.png                     (tabbrowser/tab-stroke-end.png)
   skin/classic/browser/tabbrowser/tab-stroke-end@2x.png                  (tabbrowser/tab-stroke-end@2x.png)
   skin/classic/browser/tabbrowser/tab-stroke-start.png                   (tabbrowser/tab-stroke-start.png)
   skin/classic/browser/tabbrowser/tab-stroke-start@2x.png                (tabbrowser/tab-stroke-start@2x.png)
-  skin/classic/browser/tabbrowser/tab-overflow-border.png                (tabbrowser/tab-overflow-border.png)
   skin/classic/browser/tabbrowser/tabDragIndicator.png                   (tabbrowser/tabDragIndicator.png)
   skin/classic/browser/tabbrowser/tabDragIndicator@2x.png                (tabbrowser/tabDragIndicator@2x.png)
   skin/classic/browser/tabbrowser/tab-separator.png                      (tabbrowser/tab-separator.png)
   skin/classic/browser/tabbrowser/tab-separator@2x.png                   (tabbrowser/tab-separator@2x.png)
   skin/classic/browser/tabview/close.png                    (tabview/close.png)
   skin/classic/browser/tabview/edit-light.png               (tabview/edit-light.png)
   skin/classic/browser/tabview/search.png                   (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png           (tabview/stack-expander.png)
deleted file mode 100644
index 73c778af033150dac75d515b11763bf2567395bc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..17d27c99eede5f41ab7114e3e97762c0d6daf5b1
GIT binary patch
literal 578
zc$@)30=@l-P)<h;3K|Lk000e1NJLTq000~S002k`1ONa4Pa9qS0006BNkl<Zcmb8u
zL5|xn429wMQ_f^OMK9I+14WO}Bh0RwhNUJ12seODiAYxd@JN|9|M<E8c-LKv$Fyn4
zWb5$V-yio#WEJj@{qby-S$+87yYJd%B8z6CA}`8jC8Gns=|g+K5!Ip9DmycCiz?bl
zjM}5Nq$4Ta8E;WRGqZE6lP1i>PNoH#>@D+xCTncY-J{iH?Yp&_Pa|1TokDI*W(nJ^
zzE<;c@*GgIGi>rKqO7Wsm=BP}YOBSnRT?m<6-~wktj7<O%S0p7>Zj(6$V}1@RSjsH
zZEo0+by~dz9hf2OTu;HbpaBaNyM$~EF=}sGx4MzdI$~vWzVfK7?tA=V!?7U>H)afG
zm~EBTU4Y1wAvZBrRt|zh8Q@2RI9+n0G`@JvNAK&f>5H{y1R3z;)>-nBli)R3Z*wxm
zgjs^#uEsH7GX1wXT+JBe^=Nki93+fl-OJH4@tHglHQpoSAIM%8j?=`+9GT?DJLUX6
zJ`ur=uY-#jJ$a?HFNew~+JN$Elzt+)E|?C7MxyFkSZNhO?MyMQVUYr@8BzNjZ#6$>
zJLF$N5EJ`hTN*1@JFqn4Ny0fSVC%aodH&}qel_C}v{w#MVCg;PZxx>BaRO_&{;k$$
zwccvs4mWumy`Jk2`L91hqQztSzU{YBZvndSZW|=5V6Ne>&)=Vav}y7D1M@LJXyFJ_
QS^xk507*qoM6N<$g1NyF761SM
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -125,16 +125,53 @@
   -moz-margin-start: -@tabCurveHalfWidth@;
 }
 
 .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
   -moz-padding-end: @tabCurveHalfWidth@;
   -moz-padding-start: @tabCurveHalfWidth@;
 }
 
+/* Tab Overflow */
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
+  background-image: url(chrome://browser/skin/tabbrowser/tab-overflow-indicator.png);
+  background-size: 100% 100%;
+  width: 14px;
+  margin-bottom: @tabToolbarNavbarOverlap@;
+  pointer-events: none;
+  position: relative;
+  z-index: 3; /* the selected tab's z-index + 1 */
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:-moz-locale-dir(rtl),
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:-moz-locale-dir(ltr) {
+  transform: scaleX(-1);
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]) {
+  -moz-margin-start: -2px;
+  -moz-margin-end: -12px;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
+  -moz-margin-start: -12px;
+  -moz-margin-end: -2px;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator[collapsed],
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator[collapsed] {
+  opacity: 0;
+}
+
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator,
+.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator {
+  transition: opacity 150ms ease;
+}
+
 .tab-background-start[selected=true]::after,
 .tab-background-start[selected=true]::before,
 .tab-background-start,
 .tab-background-end,
 .tab-background-end[selected=true]::after,
 .tab-background-end[selected=true]::before {
   min-height: @tabMinHeight@;
   width: @tabCurveWidth@;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1850,19 +1850,16 @@ toolbarbutton[type="socialmark"] > .tool
 }
 
 /* Tab scrollbox arrow, tabstrip new tab and all-tabs buttons */
 
 .tabbrowser-arrowscrollbox > .scrollbutton-up,
 .tabbrowser-arrowscrollbox > .scrollbutton-down {
   list-style-image: url("chrome://browser/skin/tabbrowser/tab-arrow-left.png");
   margin: 0 0 @tabToolbarNavbarOverlap@;
-  padding-right: 2px;
-  border-right: 2px solid transparent;
-  background-origin: border-box;
 }
 
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-up,
 #TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-down {
   list-style-image: url(chrome://browser/skin/tabbrowser/tab-arrow-left-inverted.png);
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-up[disabled],
@@ -1879,23 +1876,16 @@ toolbarbutton[type="socialmark"] > .tool
   transition: 1s background-color ease-out;
 }
 
 .tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
   background-color: Highlight;
   transition: none;
 }
 
-.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]),
-.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]) {
-  border-width: 0 2px 0 0;
-  border-style: solid;
-  border-image: url("chrome://browser/skin/tabbrowser/tab-overflow-border.png") 0 2 0 2 fill;
-}
-
 .tabs-newtab-button > .toolbarbutton-icon {
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .tabs-newtab-button,
 #TabsToolbar > #new-tab-button,
 #TabsToolbar > toolbarpaletteitem > #new-tab-button {
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -191,17 +191,17 @@ browser.jar:
         skin/classic/browser/tabbrowser/tab-arrow-left.png           (tabbrowser/tab-arrow-left.png)
         skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png  (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/browser/tabbrowser/tab-background-start.png     (tabbrowser/tab-background-start.png)
         skin/classic/browser/tabbrowser/tab-background-start@2x.png  (tabbrowser/tab-background-start@2x.png)
         skin/classic/browser/tabbrowser/tab-background-middle.png    (tabbrowser/tab-background-middle.png)
         skin/classic/browser/tabbrowser/tab-background-middle@2x.png (tabbrowser/tab-background-middle@2x.png)
         skin/classic/browser/tabbrowser/tab-background-end.png       (tabbrowser/tab-background-end.png)
         skin/classic/browser/tabbrowser/tab-background-end@2x.png    (tabbrowser/tab-background-end@2x.png)
-        skin/classic/browser/tabbrowser/tab-overflow-border.png      (tabbrowser/tab-overflow-border.png)
+        skin/classic/browser/tabbrowser/tab-overflow-indicator.png   (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
         skin/classic/browser/tabbrowser/tab-selected-end.svg         (tab-selected-end.svg)
         skin/classic/browser/tabbrowser/tab-selected-start.svg       (tab-selected-start.svg)
 
         skin/classic/browser/tabbrowser/tab-stroke-end.png           (tabbrowser/tab-stroke-end.png)
         skin/classic/browser/tabbrowser/tab-stroke-end@2x.png        (tabbrowser/tab-stroke-end@2x.png)
@@ -596,17 +596,17 @@ browser.jar:
         skin/classic/aero/browser/tabbrowser/tab-arrow-left.png      (tabbrowser/tab-arrow-left.png)
         skin/classic/aero/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
         skin/classic/aero/browser/tabbrowser/tab-background-start.png    (tabbrowser/tab-background-start.png)
         skin/classic/aero/browser/tabbrowser/tab-background-start@2x.png (tabbrowser/tab-background-start@2x.png)
         skin/classic/aero/browser/tabbrowser/tab-background-middle.png   (tabbrowser/tab-background-middle.png)
         skin/classic/aero/browser/tabbrowser/tab-background-middle@2x.png (tabbrowser/tab-background-middle@2x.png)
         skin/classic/aero/browser/tabbrowser/tab-background-end.png      (tabbrowser/tab-background-end.png)
         skin/classic/aero/browser/tabbrowser/tab-background-end@2x.png   (tabbrowser/tab-background-end@2x.png)
-        skin/classic/aero/browser/tabbrowser/tab-overflow-border.png (tabbrowser/tab-overflow-border.png)
+        skin/classic/aero/browser/tabbrowser/tab-overflow-indicator.png  (../shared/tabbrowser/tab-overflow-indicator.png)
 
 # NOTE: The following two files (tab-selected-end.svg, tab-selected-start.svg) get pre-processed in
 #       Makefile.in with a non-default marker of "%" and the result of that gets packaged.
         skin/classic/aero/browser/tabbrowser/tab-selected-end.svg    (tab-selected-end-aero.svg)
         skin/classic/aero/browser/tabbrowser/tab-selected-start.svg  (tab-selected-start-aero.svg)
 
         skin/classic/aero/browser/tabbrowser/tab-stroke-end.png      (tabbrowser/tab-stroke-end.png)
         skin/classic/aero/browser/tabbrowser/tab-stroke-end@2x.png   (tabbrowser/tab-stroke-end@2x.png)
deleted file mode 100644
index 77f2462e5bfda652074261e2e160fcbc99c3e04e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/configure.in
+++ b/configure.in
@@ -3936,17 +3936,16 @@ if test -n "$MOZ_RTSP"; then
   NECKO_PROTOCOLS_DEFAULT="$NECKO_PROTOCOLS_DEFAULT rtsp"
 fi
 USE_ARM_KUSER=
 BUILD_CTYPES=1
 MOZ_USE_NATIVE_POPUP_WINDOWS=
 MOZ_ANDROID_HISTORY=
 MOZ_WEBSMS_BACKEND=
 MOZ_ANDROID_BEAM=
-MOZ_ANDROID_SYNTHAPKS=
 MOZ_LOCALE_SWITCHER=
 ACCESSIBILITY=1
 MOZ_TIME_MANAGER=
 MOZ_PAY=
 MOZ_AUDIO_CHANNEL_MANAGER=
 NSS_NO_LIBPKIX=
 MOZ_CONTENT_SANDBOX=
 MOZ_CONTENT_SANDBOX_REPORTER=1
@@ -4962,28 +4961,16 @@ fi
 dnl ========================================================
 dnl = Enable NFC permission on Android
 dnl ========================================================
 if test -n "$MOZ_ANDROID_BEAM"; then
     AC_DEFINE(MOZ_ANDROID_BEAM)
 fi
 
 dnl ========================================================
-dnl = Synthesized Webapp APKs on Android
-dnl ========================================================
-MOZ_ARG_ENABLE_BOOL(android-synthapks,
-[  --enable-android-synthapks       Enable synthesized APKs],
-    MOZ_ANDROID_SYNTHAPKS=1,
-    MOZ_ANDROID_SYNTHAPKS=)
-
-if test -n "$MOZ_ANDROID_SYNTHAPKS"; then
-    AC_DEFINE(MOZ_ANDROID_SYNTHAPKS)
-fi
-
-dnl ========================================================
 dnl = JS Debugger XPCOM component (js/jsd)
 dnl ========================================================
 MOZ_ARG_DISABLE_BOOL(jsd,
 [  --disable-jsd           Disable JavaScript debug library],
     MOZ_JSDEBUGGER=,
     MOZ_JSDEBUGGER=1)
 
 
@@ -8569,17 +8556,16 @@ AC_SUBST(MOZ_DIRECTX_SDK_PATH)
 AC_SUBST(MOZ_D3DCOMPILER_XP_DLL)
 AC_SUBST(MOZ_D3DCOMPILER_XP_CAB)
 
 AC_SUBST(MOZ_METRO)
 
 AC_SUBST(MOZ_ANDROID_HISTORY)
 AC_SUBST(MOZ_WEBSMS_BACKEND)
 AC_SUBST(MOZ_ANDROID_BEAM)
-AC_SUBST(MOZ_ANDROID_SYNTHAPKS)
 AC_SUBST(MOZ_LOCALE_SWITCHER)
 AC_SUBST(MOZ_DISABLE_GECKOVIEW)
 AC_SUBST(ENABLE_STRIP)
 AC_SUBST(PKG_SKIP_STRIP)
 AC_SUBST(STRIP_FLAGS)
 AC_SUBST(USE_ELF_HACK)
 AC_SUBST(INCREMENTAL_LINKER)
 AC_SUBST(MOZ_COMPONENTS_VERSION_SCRIPT_LDFLAGS)
--- a/content/media/webrtc/nsITabSource.idl
+++ b/content/media/webrtc/nsITabSource.idl
@@ -1,17 +1,17 @@
 /* -*- Mode: IDL; tab-width: 2; 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 "nsISupports.idl"
 #include "nsIDOMWindow.idl"
 
-[scriptable,uuid(ff9c0e45-4646-45ec-b2f0-3b16d9e41875)]
+[scriptable,uuid(b3a7a402-2760-4583-b4a3-af095fe00c84)]
 interface nsITabSource : nsISupports
 {
   nsIDOMWindow getTabToStream();
   void notifyStreamStart(in nsIDOMWindow window);
   void notifyStreamStop(in nsIDOMWindow window);
 };
 
 %{C++
--- a/dom/apps/src/AppsUtils.jsm
+++ b/dom/apps/src/AppsUtils.jsm
@@ -66,17 +66,17 @@ mozIApplication.prototype = {
   }
 }
 
 function _setAppProperties(aObj, aApp) {
   aObj.name = aApp.name;
   aObj.csp = aApp.csp;
   aObj.installOrigin = aApp.installOrigin;
   aObj.origin = aApp.origin;
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
   aObj.apkPackageName = aApp.apkPackageName;
 #endif
   aObj.receipts = aApp.receipts ? JSON.parse(JSON.stringify(aApp.receipts)) : null;
   aObj.installTime = aApp.installTime;
   aObj.manifestURL = aApp.manifestURL;
   aObj.appStatus = aApp.appStatus;
   aObj.removable = aApp.removable;
   aObj.id = aApp.id;
--- a/dom/apps/src/Webapps.jsm
+++ b/dom/apps/src/Webapps.jsm
@@ -1085,17 +1085,17 @@ this.DOMApplicationRegistry = {
     }
 
     let msg = aMessage.data || {};
     let mm = aMessage.target;
     msg.mm = mm;
 
     switch (aMessage.name) {
       case "Webapps:Install": {
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
         Services.obs.notifyObservers(mm, "webapps-runtime-install", JSON.stringify(msg));
 #else
         this.doInstall(msg, mm);
 #endif
         break;
       }
       case "Webapps:GetSelf":
         this.getSelf(msg, mm);
@@ -1114,17 +1114,17 @@ this.DOMApplicationRegistry = {
         break;
       case "Webapps:GetNotInstalled":
         this.getNotInstalled(msg, mm);
         break;
       case "Webapps:GetAll":
         this.doGetAll(msg, mm);
         break;
       case "Webapps:InstallPackage": {
-#ifdef MOZ_ANDROID_SYNTHAPKS
+#ifdef MOZ_WIDGET_ANDROID
         Services.obs.notifyObservers(mm, "webapps-runtime-install-package", JSON.stringify(msg));
 #else
         this.doInstallPackage(msg, mm);
 #endif
         break;
       }
       case "Webapps:RegisterForMessages":
         this.addMessageListener(msg.messages, msg.app, mm);
@@ -2505,25 +2505,25 @@ this.DOMApplicationRegistry = {
       this.queuedDownload[app.manifestURL] = {
         manifest: manifest,
         app: appObject,
         profileDir: aProfileDir
       }
     } else if (manifest.package_path) {
       // If it is a local app then it must been installed from a local file
       // instead of web.
-#ifdef MOZ_ANDROID_SYNTHAPKS
       // In that case, we would already have the manifest, not just the update
       // manifest.
+#ifdef MOZ_WIDGET_ANDROID
       dontNeedNetwork = !!aData.app.manifest;
 #else
       if (aData.app.localInstallPath) {
         dontNeedNetwork = true;
         jsonManifest.package_path = "file://" + aData.app.localInstallPath;
-      }
+      }   
 #endif
 
       // origin for install apps is meaningless here, since it's app:// and this
       // can't be used to resolve package paths.
       manifest = new ManifestHelper(jsonManifest, app.manifestURL);
 
       this.queuedPackageDownload[app.manifestURL] = {
         manifest: manifest,
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -820,17 +820,16 @@ pref("browser.snippets.geoUrl", "https:/
 
 // URL used to ping metrics with stats about which snippets have been shown
 pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
 
 // These prefs require a restart to take effect.
 pref("browser.snippets.enabled", true);
 pref("browser.snippets.syncPromo.enabled", true);
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 // The URL of the APK factory from which we obtain APKs for webapps.
 pref("browser.webapps.apkFactoryUrl", "https://controller.apk.firefox.com/application.apk");
 
 // How frequently to check for webapp updates, in seconds (86400 is daily).
 pref("browser.webapps.updateInterval", 86400);
 
 // Whether or not to check for updates.  Enabled by default, but the runtime
 // disables it for webapp profiles on firstrun, so only the main Fennec process
@@ -845,17 +844,15 @@ pref("browser.webapps.updateInterval", 8
 //   1: do check for updates
 pref("browser.webapps.checkForUpdates", 1);
 
 // The URL of the service that checks for updates.
 // To test updates, set this to http://apk-update-checker.paas.allizom.org,
 // which is a test server that always reports all apps as having updates.
 pref("browser.webapps.updateCheckUrl", "https://controller.apk.firefox.com/app_updates");
 
-#endif
-
 // The mode of home provider syncing.
 // 0: Sync always
 // 1: Sync only when on wifi
 pref("home.sync.updateMode", 0);
 
 // How frequently to check if we should sync home provider data.
 pref("home.sync.checkIntervalSecs", 3600);
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -183,17 +183,16 @@
 
             <!-- For debugging -->
             <intent-filter>
                 <action android:name="org.mozilla.gecko.DEBUG" />
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
         <activity android:name="org.mozilla.gecko.webapp.Dispatcher"
             android:noHistory="true" >
             <intent-filter>
                 <!-- catch links from synthetic apks -->
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="application/webapp" />
             </intent-filter>
@@ -207,17 +206,16 @@
         </receiver>
 
         <receiver android:name="org.mozilla.gecko.webapp.TaskKiller">
           <intent-filter>
              <action android:name="org.mozilla.webapp.TASK_REMOVED" />
              <category android:name="android.intent.category.DEFAULT" />
           </intent-filter>
         </receiver>
-#endif
 
         <activity android:name=".Webapp"
                   android:label="@string/webapp_generic_name"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:launchMode="singleTask"
                   android:taskAffinity="org.mozilla.gecko.WEBAPP"
                   android:process=":@ANDROID_PACKAGE_NAME@.Webapp"
--- a/mobile/android/base/AppConstants.java.in
+++ b/mobile/android/base/AppConstants.java.in
@@ -144,23 +144,16 @@ public class AppConstants {
 
     public static final boolean MOZ_ANDROID_BEAM =
 #ifdef MOZ_ANDROID_BEAM
     true;
 #else
     false;
 #endif
 
-    public static final boolean MOZ_ANDROID_SYNTHAPKS =
-#ifdef MOZ_ANDROID_SYNTHAPKS
-    true;
-#else
-    false;
-#endif
-
     // See this wiki page for more details about channel specific build defines:
     // https://wiki.mozilla.org/Platform/Channel-specific_build_defines
     public static final boolean RELEASE_BUILD =
 #ifdef RELEASE_BUILD
     true;
 #else
     false;
 #endif
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -788,51 +788,29 @@ public class GeckoAppShell
     @WrapElementForJNI
     static void scheduleRestart() {
         restartScheduled = true;
     }
 
     public static Intent getWebappIntent(String aURI, String aOrigin, String aTitle, Bitmap aIcon) {
         Intent intent;
 
-        if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-            Allocator slots = Allocator.getInstance(getContext());
-            int index = slots.getIndexForOrigin(aOrigin);
-
-            if (index == -1) {
-                return null;
-            }
-            String packageName = slots.getAppForIndex(index);
-            intent = getContext().getPackageManager().getLaunchIntentForPackage(packageName);
-            if (aURI != null) {
-                intent.setData(Uri.parse(aURI));
-            }
-        } else {
-            int index;
-            if (aIcon != null && !TextUtils.isEmpty(aTitle))
-                index = WebappAllocator.getInstance(getContext()).findAndAllocateIndex(aOrigin, aTitle, aIcon);
-            else
-                index = WebappAllocator.getInstance(getContext()).getIndexForApp(aOrigin);
-
-            if (index == -1)
-                return null;
-
-            intent = getWebappIntent(index, aURI);
+        Allocator slots = Allocator.getInstance(getContext());
+        int index = slots.getIndexForOrigin(aOrigin);
+
+        if (index == -1) {
+            return null;
         }
 
-        return intent;
-    }
-
-    // The old implementation of getWebappIntent.  Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static Intent getWebappIntent(int aIndex, String aURI) {
-        Intent intent = new Intent();
-        intent.setAction(GeckoApp.ACTION_WEBAPP_PREFIX + aIndex);
-        intent.setData(Uri.parse(aURI));
-        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
-                            AppConstants.ANDROID_PACKAGE_NAME + ".WebApps$WebApp" + aIndex);
+        String packageName = slots.getAppForIndex(index);
+        intent = getContext().getPackageManager().getLaunchIntentForPackage(packageName);
+        if (aURI != null) {
+            intent.setData(Uri.parse(aURI));
+        }
+
         return intent;
     }
 
     // "Installs" an application by creating a shortcut
     // This is the entry point from AndroidBridge.h
     @WrapElementForJNI
     static void createShortcut(String aTitle, String aURI, String aIconData, String aType) {
         if ("webapp".equals(aType)) {
--- a/mobile/android/base/GlobalHistory.java
+++ b/mobile/android/base/GlobalHistory.java
@@ -1,33 +1,37 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
-import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Handler;
-import android.util.Log;
-
 import java.lang.ref.SoftReference;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Queue;
 import java.util.Set;
 
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.Log;
+
 class GlobalHistory {
     private static final String LOGTAG = "GeckoGlobalHistory";
 
-    private static GlobalHistory sInstance = new GlobalHistory();
+    private static final String TELEMETRY_HISTOGRAM_ADD = "FENNEC_GLOBALHISTORY_ADD_MS";
+    private static final String TELEMETRY_HISTOGRAM_UPDATE = "FENNEC_GLOBALHISTORY_UPDATE_MS";
+    private static final String TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK = "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS";
+
+    private static final GlobalHistory sInstance = new GlobalHistory();
 
     static GlobalHistory getInstance() {
         return sInstance;
     }
 
     // this is the delay between receiving a URI check request and processing it.
     // this allows batching together multiple requests and processing them together,
     // which is more efficient.
@@ -43,45 +47,48 @@ class GlobalHistory {
         mHandler = ThreadUtils.getBackgroundHandler();
         mPendingUris = new LinkedList<String>();
         mVisitedCache = new SoftReference<Set<String>>(null);
         mNotifierRunnable = new Runnable() {
             @Override
             public void run() {
                 Set<String> visitedSet = mVisitedCache.get();
                 if (visitedSet == null) {
-                    // the cache was wiped away, repopulate it
+                    // The cache was wiped. Repopulate it.
                     Log.w(LOGTAG, "Rebuilding visited link set...");
-                    visitedSet = new HashSet<String>();
-                    Cursor c = null;
+                    final long start = SystemClock.uptimeMillis();
+                    final Cursor c = BrowserDB.getAllVisitedHistory(GeckoAppShell.getContext().getContentResolver());
+                    if (c == null) {
+                        return;
+                    }
+
                     try {
-                        c = BrowserDB.getAllVisitedHistory(GeckoAppShell.getContext().getContentResolver());
-                        if (c == null) {
-                            return;
-                        }
-
+                        visitedSet = new HashSet<String>();
                         if (c.moveToFirst()) {
                             do {
                                 visitedSet.add(c.getString(0));
                             } while (c.moveToNext());
                         }
                         mVisitedCache = new SoftReference<Set<String>>(visitedSet);
+                        final long end = SystemClock.uptimeMillis();
+                        final long took = end - start;
+                        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
                     } finally {
-                        if (c != null)
-                            c.close();
+                        c.close();
                     }
                 }
 
-                // this runs on the same handler thread as the checkUriVisited code,
-                // so no synchronization needed
+                // This runs on the same handler thread as the checkUriVisited code,
+                // so no synchronization is needed.
                 while (true) {
-                    String uri = mPendingUris.poll();
+                    final String uri = mPendingUris.poll();
                     if (uri == null) {
                         break;
                     }
+
                     if (visitedSet.contains(uri)) {
                         GeckoAppShell.notifyUriVisited(uri);
                     }
                 }
                 mProcessing = false;
             }
         };
     }
@@ -90,22 +97,33 @@ class GlobalHistory {
         Set<String> visitedSet = mVisitedCache.get();
         if (visitedSet != null) {
             visitedSet.add(uri);
         }
         GeckoAppShell.notifyUriVisited(uri);
     }
 
     public void add(String uri) {
+        final long start = SystemClock.uptimeMillis();
         BrowserDB.updateVisitedHistory(GeckoAppShell.getContext().getContentResolver(), uri);
+        final long end = SystemClock.uptimeMillis();
+        final long took = end - start;
+        Log.d(LOGTAG, "GlobalHistory.add took " + took + "msec.");
+        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
         addToGeckoOnly(uri);
     }
 
+    @SuppressWarnings("static-method")
     public void update(String uri, String title) {
+        final long start = SystemClock.uptimeMillis();
         BrowserDB.updateHistoryTitle(GeckoAppShell.getContext().getContentResolver(), uri, title);
+        final long end = SystemClock.uptimeMillis();
+        final long took = end - start;
+        Log.d(LOGTAG, "GlobalHistory.update took " + took + "msec.");
+        Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
     }
 
     public void checkUriVisited(final String uri) {
         mHandler.post(new Runnable() {
             @Override
             public void run() {
                 // this runs on the same handler thread as the processing loop,
                 // so no synchronization needed
--- a/mobile/android/base/Webapp.java.in
+++ b/mobile/android/base/Webapp.java.in
@@ -1,18 +1,14 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #filter substitution
 package @ANDROID_PACKAGE_NAME@;
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 import org.mozilla.gecko.webapp.WebappImpl;
-#else
-import org.mozilla.gecko.WebappImpl;
-#endif
 
 /**
  * This class serves only as a namespace wrapper for WebappImpl.
  */
 public class Webapp extends WebappImpl {}
deleted file mode 100644
--- a/mobile/android/base/WebappAllocator.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/* -*- 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;
-
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
-import android.graphics.Bitmap;
-import android.util.Log;
-
-import java.util.ArrayList;
-
-public class WebappAllocator {
-    private final String LOGTAG = "GeckoWebappAllocator";
-    // The number of Webapp# and WEBAPP# activites/apps/intents
-    private final static int MAX_WEB_APPS = 100;
-
-    protected static WebappAllocator sInstance = null;
-    public static WebappAllocator getInstance() {
-        return getInstance(GeckoAppShell.getContext());
-    }
-
-    public static synchronized WebappAllocator getInstance(Context cx) {
-        if (sInstance == null) {
-            sInstance = new WebappAllocator(cx);
-        }
-
-        return sInstance;
-    }
-
-    SharedPreferences mPrefs;
-
-    protected WebappAllocator(Context context) {
-        mPrefs = context.getSharedPreferences("webapps", Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
-    }
-
-    public static String appKey(int index) {
-        return "app" + index;
-    }
-
-    static public String iconKey(int index) {
-        return "icon" + index;
-    }
-
-    public synchronized int findAndAllocateIndex(String app, String name, String aIconData) {
-        Bitmap icon = (aIconData != null) ? BitmapUtils.getBitmapFromDataURI(aIconData) : null;
-        return findAndAllocateIndex(app, name, icon);
-    }
-
-    public synchronized int findAndAllocateIndex(final String app, final String name, final Bitmap aIcon) {
-        int index = getIndexForApp(app);
-        if (index != -1)
-            return index;
-
-        for (int i = 0; i < MAX_WEB_APPS; ++i) {
-            if (!mPrefs.contains(appKey(i))) {
-                // found unused index i
-                updateAppAllocation(app, i, aIcon);
-                return i;
-            }
-        }
-
-        // no more apps!
-        return -1;
-    }
-
-    public synchronized void updateAppAllocation(final String app,
-                                                 final int index,
-                                                 final Bitmap aIcon) {
-        if (aIcon != null) {
-            ThreadUtils.getBackgroundHandler().post(new Runnable() {
-                @Override
-                public void run() {
-                    int color = 0;
-                    try {
-                        color = BitmapUtils.getDominantColor(aIcon);
-                    } catch (Exception e) {
-                        Log.e(LOGTAG, "Exception during getDominantColor", e);
-                    }
-                    mPrefs.edit()
-                          .putString(appKey(index), app)
-                          .putInt(iconKey(index), color).commit();
-                }
-            });
-        } else {
-            mPrefs.edit()
-                  .putString(appKey(index), app)
-                  .putInt(iconKey(index), 0).commit();
-        }
-    }
-
-    public synchronized int getIndexForApp(String app) {
-        for (int i = 0; i < MAX_WEB_APPS; ++i) {
-            if (mPrefs.getString(appKey(i), "").equals(app)) {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-
-    public synchronized String getAppForIndex(int index) {
-        return mPrefs.getString(appKey(index), null);
-    }
-
-    public synchronized int releaseIndexForApp(String app) {
-        int index = getIndexForApp(app);
-        if (index == -1)
-            return -1;
-
-        releaseIndex(index);
-        return index;
-    }
-
-    public synchronized void releaseIndex(final int index) {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                mPrefs.edit()
-                    .remove(appKey(index))
-                    .remove(iconKey(index))
-                    .commit();
-            }
-        });
-    }
-}
deleted file mode 100644
--- a/mobile/android/base/WebappImpl.java
+++ /dev/null
@@ -1,221 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.View;
-import android.view.MenuItem;
-import android.widget.TextView;
-import android.widget.RelativeLayout;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.view.animation.AnimationUtils;
-import android.view.animation.Animation;
-import android.widget.ImageView;
-import android.view.Display;
-
-import java.io.File;
-import java.net.URI;
-
-public class WebappImpl extends GeckoApp {
-    private static final String LOGTAG = "GeckoWebappImpl";
-
-    private URI mOrigin;
-    private TextView mTitlebarText = null;
-    private View mTitlebar = null;
-
-    private View mSplashscreen;
-
-    protected int getIndex() { return 0; }
-
-    @Override
-    public int getLayout() { return R.layout.web_app; }
-
-    @Override
-    public boolean hasTabsSideBar() { return false; }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState)
-    {
-        super.onCreate(savedInstanceState);
-
-        mSplashscreen = (RelativeLayout) findViewById(R.id.splashscreen);
-        if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
-            overridePendingTransition(R.anim.grow_fade_in_center, android.R.anim.fade_out);
-            showSplash();
-        }
-
-        String action = getIntent().getAction();
-        Bundle extras = getIntent().getExtras();
-        String title = extras != null ? extras.getString(Intent.EXTRA_SHORTCUT_NAME) : null;
-        setTitle(title != null ? title : "Web App");
-
-        mTitlebarText = (TextView)findViewById(R.id.webapp_title);
-        mTitlebar = findViewById(R.id.webapp_titlebar);
-        if (!action.startsWith(ACTION_WEBAPP_PREFIX)) {
-            Log.e(LOGTAG, "Webapp launch, but intent action is " + action + "!");
-            return;
-        }
-
-        // Try to use the origin stored in the WebappAllocator first
-        String origin = WebappAllocator.getInstance(this).getAppForIndex(getIndex());
-        try {
-            mOrigin = new URI(origin);
-        } catch (java.net.URISyntaxException ex) {
-            // If we can't parse the this is an app protocol, just settle for not having an origin
-            if (!origin.startsWith("app://")) {
-                return;
-            }
-
-            // If that failed fall back to the origin stored in the shortcut
-            Log.i(LOGTAG, "Webapp is not registered with allocator");
-            try {
-                mOrigin = new URI(getIntent().getData().toString());
-            } catch (java.net.URISyntaxException ex2) {
-                Log.e(LOGTAG, "Unable to parse intent url: ", ex);
-            }
-        }
-    }
-
-    @Override
-    protected void loadStartupTab(String uri) {
-        String action = getIntent().getAction();
-        if (GeckoApp.ACTION_WEBAPP_PREFIX.equals(action)) {
-            // This action assumes the uri is not an installed Webapp. We will
-            // use the WebappAllocator to register the uri with an Android
-            // process so it can run chromeless.
-            int index = WebappAllocator.getInstance(this).findAndAllocateIndex(uri, "App", (Bitmap) null);
-            Intent appIntent = GeckoAppShell.getWebappIntent(index, uri);
-            startActivity(appIntent);
-            finish();
-        }
-    }
-
-    private void showSplash() {
-        SharedPreferences prefs = getSharedPreferences("webapps", Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
-
-        // get the favicon dominant color, stored when the app was installed
-        int[] colors = new int[2];
-        int dominantColor = prefs.getInt(WebappAllocator.iconKey(getIndex()), -1);
-
-        // now lighten it, to ensure that the icon stands out in the center
-        float[] f = new float[3];
-        Color.colorToHSV(dominantColor, f);
-        f[2] = Math.min(f[2]*2, 1.0f);
-        colors[0] = Color.HSVToColor(255, f);
-
-        // now generate a second, slightly darker version of the same color
-        f[2] *= 0.75;
-        colors[1] = Color.HSVToColor(255, f);
-
-        // Draw the background gradient
-        GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TL_BR, colors);
-        gd.setGradientType(GradientDrawable.RADIAL_GRADIENT);
-        Display display = getWindowManager().getDefaultDisplay();
-        gd.setGradientCenter(0.5f, 0.5f);
-        gd.setGradientRadius(Math.max(display.getWidth()/2, display.getHeight()/2));
-        mSplashscreen.setBackgroundDrawable((Drawable)gd);
-
-        // look for a logo.png in the profile dir and show it. If we can't find a logo show nothing
-        File profile = getProfile().getDir();
-        File logoFile = new File(profile, "logo.png");
-        if (logoFile.exists()) {
-            ImageView image = (ImageView)findViewById(R.id.splashscreen_icon);
-            Drawable d = Drawable.createFromPath(logoFile.getPath());
-            image.setImageDrawable(d);
-
-            Animation fadein = AnimationUtils.loadAnimation(this, R.anim.grow_fade_in_center);
-            fadein.setStartOffset(500);
-            fadein.setDuration(1000);
-            image.startAnimation(fadein);
-        }
-    }
-
-    @Override
-    protected String getDefaultProfileName() {
-        String action = getIntent().getAction();
-        if (!action.startsWith(ACTION_WEBAPP_PREFIX)) {
-            Log.e(LOGTAG, "Webapp launch, but intent action is " + action + "!");
-            return null;
-        }
-
-        return "webapp" + action.substring(ACTION_WEBAPP_PREFIX.length());
-    }
-
-    @Override
-    protected boolean getSessionRestoreState(Bundle savedInstanceState) {
-        // for now webapps never restore your session
-        return false;
-    }
-
-    @Override
-    public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
-        switch(msg) {
-            case SELECTED:
-            case LOCATION_CHANGE:
-                if (Tabs.getInstance().isSelectedTab(tab)) {
-                    final String urlString = tab.getURL();
-                    final URI uri;
-
-                    try {
-                        uri = new URI(urlString);
-                    } catch (java.net.URISyntaxException ex) {
-                        mTitlebarText.setText(urlString);
-
-                        // If we can't parse the url, and its an app protocol hide
-                        // the titlebar and return, otherwise show the titlebar
-                        // and the full url
-                        if (!urlString.startsWith("app://")) {
-                            mTitlebar.setVisibility(View.VISIBLE);
-                        } else {
-                            mTitlebar.setVisibility(View.GONE);
-                        }
-                        return;
-                    }
-
-                    if (mOrigin != null && mOrigin.getHost().equals(uri.getHost())) {
-                        mTitlebar.setVisibility(View.GONE);
-                    } else {
-                        mTitlebarText.setText(uri.getScheme() + "://" + uri.getHost());
-                        mTitlebar.setVisibility(View.VISIBLE);
-                    }
-                }
-                break;
-            case LOADED:
-                if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
-                    Animation fadeout = AnimationUtils.loadAnimation(this, android.R.anim.fade_out);
-                    fadeout.setAnimationListener(new Animation.AnimationListener() {
-                        @Override
-                        public void onAnimationEnd(Animation animation) {
-                          mSplashscreen.setVisibility(View.GONE);
-                        }
-                        @Override
-                        public void onAnimationRepeat(Animation animation) { }
-                        @Override
-                        public void onAnimationStart(Animation animation) { }
-                    });
-                    mSplashscreen.startAnimation(fadeout);
-                }
-                break;
-            case START:
-                if (mSplashscreen != null && mSplashscreen.getVisibility() == View.VISIBLE) {
-                    View area = findViewById(R.id.splashscreen_progress);
-                    area.setVisibility(View.VISIBLE);
-                    Animation fadein = AnimationUtils.loadAnimation(this, android.R.anim.fade_in);
-                    fadein.setDuration(1000);
-                    area.startAnimation(fadein);
-                }
-                break;
-        }
-        super.onTabChanged(tab, msg, data);
-    }
-};
--- a/mobile/android/base/WebappManifestFragment.xml.frag.in
+++ b/mobile/android/base/WebappManifestFragment.xml.frag.in
@@ -1,22 +1,9 @@
         <activity android:name=".WebApps$WebApp@APPNUM@"
                   android:label="@string/webapp_generic_name"
                   android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize"
                   android:windowSoftInputMode="stateUnspecified|adjustResize"
                   android:process=":@ANDROID_PACKAGE_NAME@.Webapp@APPNUM@"
                   android:theme="@style/Gecko.App"
-#ifdef MOZ_ANDROID_SYNTHAPKS
                   android:launchMode="singleTop"
                   android:exported="true"
         />
-#else
-                  android:launchMode="singleTask"
-                  android:taskAffinity="org.mozilla.gecko.WEBAPP@APPNUM@"
-                  android:excludeFromRecents="true">
-            <intent-filter>
-                <action android:name="org.mozilla.gecko.WEBAPP@APPNUM@" />
-            </intent-filter>
-            <intent-filter>
-                <action android:name="org.mozilla.gecko.ACTION_ALERT_CALLBACK" />
-            </intent-filter>
-        </activity>
-#endif
--- a/mobile/android/base/home/SearchLoader.java
+++ b/mobile/android/base/home/SearchLoader.java
@@ -1,29 +1,33 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.db.BrowserDB;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
+import android.util.Log;
 
 /**
  * Encapsulates the implementation of the search cursor loader.
  */
 class SearchLoader {
-    // Key for search terms
+    public static final String LOGTAG = "GeckoSearchLoader";
+
     private static final String KEY_SEARCH_TERM = "search_term";
 
     private SearchLoader() {
     }
 
     public static Loader<Cursor> createInstance(Context context, Bundle args) {
         if (args != null) {
             final String searchTerm = args.getString(KEY_SEARCH_TERM);
@@ -48,30 +52,37 @@ class SearchLoader {
 
     public static void restart(LoaderManager manager, int loaderId,
                                LoaderCallbacks<Cursor> callbacks, String searchTerm) {
         final Bundle args = createArgs(searchTerm);
         manager.restartLoader(loaderId, args, callbacks);
     }
 
     public static class SearchCursorLoader extends SimpleCursorLoader {
+        private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS";
+
         // Max number of search results
         private static final int SEARCH_LIMIT = 100;
 
         // The target search term associated with the loader
         private final String mSearchTerm;
 
         public SearchCursorLoader(Context context, String searchTerm) {
             super(context);
             mSearchTerm = searchTerm;
         }
 
         @Override
         public Cursor loadCursor() {
-            return BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT);
+            final long start = SystemClock.uptimeMillis();
+            final Cursor cursor = BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT);
+            final long end = SystemClock.uptimeMillis();
+            final long took = end - start;
+            Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
+            return cursor;
         }
 
         public String getSearchTerm() {
             return mSearchTerm;
         }
     }
 
 }
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -26,19 +26,19 @@ import org.mozilla.gecko.home.TopSitesGr
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Configuration;
 import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.SystemClock;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.support.v4.widget.CursorAdapter;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
@@ -425,29 +425,34 @@ public class TopSitesPanel extends HomeF
         }
 
         // Once thumbnails have finished loading, the UI is ready. Reset
         // Gecko to normal priority.
         ThreadUtils.resetGeckoPriority();
     }
 
     private static class TopSitesLoader extends SimpleCursorLoader {
-        // Max number of search results
+        // Max number of search results.
         private static final int SEARCH_LIMIT = 30;
+        private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS";
         private int mMaxGridEntries;
 
         public TopSitesLoader(Context context) {
             super(context);
             mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
         }
 
         @Override
         public Cursor loadCursor() {
-            trace("TopSitesLoader.loadCursor()");
-            return BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
+            final long start = SystemClock.uptimeMillis();
+            final Cursor cursor = BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
+            final long end = SystemClock.uptimeMillis();
+            final long took = end - start;
+            Telemetry.HistogramAdd(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
+            return cursor;
         }
     }
 
     private class VisitedAdapter extends CursorAdapter {
         public VisitedAdapter(Context context, Cursor cursor) {
             super(context, cursor, 0);
         }
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -399,18 +399,16 @@ gbjar.sources += [
     'webapp/ApkResources.java',
     'webapp/Dispatcher.java',
     'webapp/EventListener.java',
     'webapp/InstallHelper.java',
     'webapp/InstallListener.java',
     'webapp/TaskKiller.java',
     'webapp/UninstallListener.java',
     'webapp/WebappImpl.java',
-    'WebappAllocator.java',
-    'WebappImpl.java',
     'widget/ActivityChooserModel.java',
     'widget/AllCapsTextView.java',
     'widget/AnimatedHeightLayout.java',
     'widget/ArrowPopup.java',
     'widget/BasicColorPicker.java',
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
--- a/mobile/android/base/webapp/EventListener.java
+++ b/mobile/android/base/webapp/EventListener.java
@@ -1,50 +1,42 @@
 /* -*- 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.webapp;
 
-import org.mozilla.gecko.ActivityHandlerHelper;
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
-import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.ActivityResultHandler;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.WebappAllocator;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.util.Log;
-
 import java.io.File;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.ActivityHandlerHelper;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.util.ActivityResultHandler;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.util.Log;
 
 public class EventListener implements NativeEventListener  {
 
     private static final String LOGTAG = "GeckoWebappEventListener";
 
     public void registerEvents() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Webapps:Preinstall",
@@ -63,81 +55,39 @@ public class EventListener implements Na
             "Webapps:Open",
             "Webapps:Uninstall",
             "Webapps:GetApkVersions");
     }
 
     @Override
     public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
         try {
-            if (AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:InstallApk")) {
+            if (event.equals("Webapps:InstallApk")) {
                 installApk(GeckoAppShell.getGeckoInterface().getActivity(), message, callback);
             } else if (event.equals("Webapps:Postinstall")) {
-                if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-                    postInstallWebapp(message.getString("apkPackageName"), message.getString("origin"));
-                } else {
-                    postInstallWebapp(message.getString("name"),
-                                      message.getString("manifestURL"),
-                                      message.getString("origin"),
-                                      message.getString("iconURL"),
-                                      message.getString("originalOrigin"));
-                }
+                postInstallWebapp(message.getString("apkPackageName"), message.getString("origin"));
             } else if (event.equals("Webapps:Open")) {
                 Intent intent = GeckoAppShell.getWebappIntent(message.getString("manifestURL"),
                                                               message.getString("origin"),
                                                               "", null);
                 if (intent == null) {
                     return;
                 }
                 GeckoAppShell.getGeckoInterface().getActivity().startActivity(intent);
-            } else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:Uninstall")) {
-                uninstallWebapp(message.getString("origin"));
-            } else if (!AppConstants.MOZ_ANDROID_SYNTHAPKS && event.equals("Webapps:Preinstall")) {
-                String name = message.getString("name");
-                String manifestURL = message.getString("manifestURL");
-                String origin = message.getString("origin");
-
-                JSONObject obj = new JSONObject();
-                obj.put("profile", preInstallWebapp(name, manifestURL, origin).toString());
-                callback.sendSuccess(obj);
             } else if (event.equals("Webapps:GetApkVersions")) {
                 JSONObject obj = new JSONObject();
                 obj.put("versions", getApkVersions(GeckoAppShell.getGeckoInterface().getActivity(),
                                                    message.getStringArray("packageNames")));
                 callback.sendSuccess(obj);
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
-    // Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static File preInstallWebapp(String aTitle, String aURI, String aOrigin) {
-        int index = WebappAllocator.getInstance(GeckoAppShell.getContext()).findAndAllocateIndex(aOrigin, aTitle, (String) null);
-        GeckoProfile profile = GeckoProfile.get(GeckoAppShell.getContext(), "webapp" + index);
-        return profile.getDir();
-    }
-
-    // Not used by MOZ_ANDROID_SYNTHAPKS.
-    public static void postInstallWebapp(String aTitle, String aURI, String aOrigin, String aIconURL, String aOriginalOrigin) {
-        WebappAllocator allocator = WebappAllocator.getInstance(GeckoAppShell.getContext());
-        int index = allocator.getIndexForApp(aOriginalOrigin);
-
-        assert aIconURL != null;
-
-        final int preferredSize = GeckoAppShell.getPreferredIconSize();
-        Bitmap icon = FaviconDecoder.getMostSuitableBitmapFromDataURI(aIconURL, preferredSize);
-
-        assert aOrigin != null && index != -1;
-        allocator.updateAppAllocation(aOrigin, index, icon);
-
-        GeckoAppShell.createShortcut(aTitle, aURI, aOrigin, icon, "webapp");
-    }
-
-    // Used by MOZ_ANDROID_SYNTHAPKS.
     public static void postInstallWebapp(String aPackageName, String aOrigin) {
         Allocator allocator = Allocator.getInstance(GeckoAppShell.getContext());
         int index = allocator.findOrAllocatePackage(aPackageName);
         allocator.putOrigin(index, aOrigin);
     }
 
     public static void uninstallWebapp(final String packageName) {
         // On uninstall, we need to do a couple of things:
--- a/mobile/android/base/webapp/UninstallListener.java
+++ b/mobile/android/base/webapp/UninstallListener.java
@@ -142,14 +142,12 @@ public class UninstallListener extends B
             mApp = app;
         }
 
         @Override
         public void run() {
             ThreadUtils.assertOnBackgroundThread();
 
             // Perform webapp uninstalls as appropiate.
-            if (AppConstants.MOZ_ANDROID_SYNTHAPKS) {
-                UninstallListener.initUninstallPackageScan(mApp.getApplicationContext());
-            }
+            UninstallListener.initUninstallPackageScan(mApp.getApplicationContext());
         }
     }
 }
--- a/mobile/android/chrome/content/WebappRT.js
+++ b/mobile/android/chrome/content/WebappRT.js
@@ -6,21 +6,19 @@ let Ci = Components.interfaces;
 let Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PermissionsInstaller.jsm");
 Cu.import("resource://gre/modules/PermissionPromptHelper.jsm");
 Cu.import("resource://gre/modules/ContactService.jsm");
-#ifdef MOZ_ANDROID_SYNTHAPKS
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
-#endif
 
 function pref(name, value) {
   return {
     name: name,
     value: value
   }
 }
 
@@ -65,24 +63,22 @@ let WebappRT = {
     if (aStatus == "new" || aStatus == "upgrade") {
       this.getManifestFor(aUrl, function (aManifest, aApp) {
         if (aManifest) {
           PermissionsInstaller.installPermissions(aApp, true);
         }
       });
     }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
     // If the app is in debug mode, configure and enable the remote debugger.
     sendMessageToJava({ type: "NativeApp:IsDebuggable" }, (response) => {
       if (response.isDebuggable) {
         this._enableRemoteDebugger(aUrl);
       }
     });
-#endif
 
     this.findManifestUrlFor(aUrl, aCallback);
   },
 
   getManifestFor: function (aUrl, aCallback) {
     DOMApplicationRegistry.registryReady.then(() => {
       let request = navigator.mozApps.mgmt.getAll();
       request.onsuccess = function() {
@@ -158,17 +154,16 @@ let WebappRT = {
         Services.prefs.setBoolPref(aPref.name, aPref.value);
         break;
       case "number":
         Services.prefs.setIntPref(aPref.name, aPref.value);
         break;
     }
   },
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   _enableRemoteDebugger: function(aUrl) {
     // Skip the connection prompt in favor of notifying the user below.
     Services.prefs.setBoolPref("devtools.debugger.prompt-connection", false);
 
     // Automagically find a free port and configure the debugger to use it.
     let serv = Cc['@mozilla.org/network/server-socket;1'].createInstance(Ci.nsIServerSocket);
     serv.init(-1, true, -1);
     let port = serv.port;
@@ -190,17 +185,16 @@ let WebappRT = {
 
       Notifications.create({
         title: Strings.browser.formatStringFromName("remoteNotificationTitle", [name], 1),
         message: Strings.browser.formatStringFromName("remoteNotificationMessage", [port], 1),
         icon: "drawable://warning_doorhanger",
       });
     });
   },
-#endif
 
   handleEvent: function(event) {
     let target = event.target;
 
     // walk up the tree to find the nearest link tag
     while (target && !(target instanceof HTMLAnchorElement)) {
       target = target.parentNode;
     }
--- a/mobile/android/chrome/content/aboutApps.js
+++ b/mobile/android/chrome/content/aboutApps.js
@@ -7,19 +7,17 @@
  * ***** END LICENSE BLOCK ***** */
 
 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm")
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", "resource://gre/modules/WebappManager.jsm");
-#endif
 
 const DEFAULT_ICON = "chrome://browser/skin/images/default-app-icon.png";
 
 let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutApps.properties");
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
   window.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIWebNavigation)
@@ -40,79 +38,34 @@ function openLink(aEvent) {
   try {
     let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
     let url = formatter.formatURLPref(aEvent.currentTarget.getAttribute("pref"));
     let BrowserApp = gChromeWin.BrowserApp;
     BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
   } catch (ex) {}
 }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 function checkForUpdates(aEvent) {
   WebappManager.checkForUpdates(true);
 }
-#endif
-
-#ifndef MOZ_ANDROID_SYNTHAPKS
-var ContextMenus = {
-  target: null,
-
-  init: function() {
-    document.addEventListener("contextmenu", this, false);
-    document.getElementById("addToHomescreenLabel").addEventListener("click", this.addToHomescreen, false);
-    document.getElementById("uninstallLabel").addEventListener("click", this.uninstall, false);
-  },
-
-  handleEvent: function(event) {
-    // store the target of context menu events so that we know which app to act on
-    this.target = event.target;
-    while (!this.target.hasAttribute("contextmenu")) {
-      this.target = this.target.parentNode;
-    }
-  },
-
-  addToHomescreen: function() {
-    let manifest = this.target.manifest;
-    gChromeWin.WebappsUI.createShortcut(manifest.name, manifest.fullLaunchPath(), manifest.biggestIconURL || DEFAULT_ICON, "webapp");
-    this.target = null;
-  },
-
-  uninstall: function() {
-    navigator.mozApps.mgmt.uninstall(this.target.app);
-
-    let manifest = this.target.manifest;
-    gChromeWin.sendMessageToJava({
-      type: "Shortcut:Remove",
-      title: manifest.name,
-      url: manifest.fullLaunchPath(),
-      origin: this.target.app.origin,
-      shortcutType: "webapp"
-    });
-    this.target = null;
-  }
-}
-#endif
 
 function onLoad(aEvent) {
   let elmts = document.querySelectorAll("[pref]");
   for (let i = 0; i < elmts.length; i++) {
     elmts[i].addEventListener("click",  openLink,  false);
   }
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   document.getElementById("update-item").addEventListener("click", checkForUpdates, false);
-#endif
 
   navigator.mozApps.mgmt.oninstall = onInstall;
   navigator.mozApps.mgmt.onuninstall = onUninstall;
   updateList();
 
-#ifndef MOZ_ANDROID_SYNTHAPKS
-  ContextMenus.init();
-#endif
+  // XXX - Hack to fix bug 985867 for now
+  document.addEventListener("touchstart", function() { });
 }
 
 function updateList() {
   let grid = document.getElementById("appgrid");
   while (grid.lastChild) {
     grid.removeChild(grid.lastChild);
   }
 
@@ -126,19 +79,16 @@ function updateList() {
 }
 
 function addApplication(aApp) {
   let list = document.getElementById("appgrid");
   let manifest = new ManifestHelper(aApp.manifest, aApp.origin);
 
   let container = document.createElement("div");
   container.className = "app list-item";
-#ifndef MOZ_ANDROID_SYNTHAPKS
-  container.setAttribute("contextmenu", "appmenu");
-#endif
   container.setAttribute("id", "app-" + aApp.origin);
   container.setAttribute("mozApp", aApp.origin);
   container.setAttribute("title", manifest.name);
 
   let img = document.createElement("img");
   img.src = manifest.biggestIconURL || DEFAULT_ICON;
   img.onerror = function() {
     // If the image failed to load, and it was not our default icon, attempt to
--- a/mobile/android/chrome/content/aboutApps.xhtml
+++ b/mobile/android/chrome/content/aboutApps.xhtml
@@ -23,24 +23,16 @@
     <meta name="viewport" content="width=device-width; user-scalable=0" />
     <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutBase.css" media="all" />
     <link rel="stylesheet" type="text/css" href="chrome://browser/skin/aboutApps.css" media="all" />
     <script type="text/javascript;version=1.8" src="chrome://browser/content/aboutApps.js"></script>
   </head>
 
   <body dir="&locale.dir;">
-
-#ifndef MOZ_ANDROID_SYNTHAPKS
-    <menu type="context" id="appmenu">
-      <menuitem id="addToHomescreenLabel" label="&aboutApps.addToHomescreen;"></menuitem>
-      <menuitem id="uninstallLabel" label="&aboutApps.uninstall;"></menuitem>
-    </menu>
-#endif
-
     <div class="header">
       <div>&aboutApps.header;</div>
       <div id="header-button" role="button" aria-label="&aboutApps.browseMarketplace;" pref="app.marketplaceURL"/>
     </div>
 
     <div id="main-container" class="hidden">
       <div>
         <div class="spacer" id="spacer1"> </div>
@@ -50,18 +42,16 @@
     </div>
 
     <div class="list-item" role="button" pref="app.marketplaceURL">
       <img class="icon" src="chrome://browser/skin/images/marketplace-logo.png" />
       <div class="inner">
         <div id="browse-title" class="title">&aboutApps.browseMarketplace;</div>
       </div>
     </div>
-#ifdef MOZ_ANDROID_SYNTHAPKS
     <div class="list-item" id="update-item" role="button">
       <img class="icon" src="chrome://browser/skin/images/update.png" />
       <div class="inner">
         <div id="browse-title" class="title">&aboutApps.checkForUpdates;</div>
       </div>
     </div>
-#endif
   </body>
 </html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -72,20 +72,18 @@ XPCOMUtils.defineLazyServiceGetter(this,
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
                                   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
                                   "resource://shumway/ShumwayUtils.jsm");
 #endif
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
                                   "resource://gre/modules/WebappManager.jsm");
-#endif
 
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 
 // Lazily-loaded browser scripts:
 [
   ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
   ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
@@ -321,26 +319,24 @@ var BrowserApp = {
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Viewport:Change", false);
     Services.obs.addObserver(this, "Viewport:Flush", false);
     Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
     Services.obs.addObserver(this, "Passwords:Init", false);
     Services.obs.addObserver(this, "FormHistory:Init", false);
     Services.obs.addObserver(this, "gather-telemetry", false);
     Services.obs.addObserver(this, "keyword-search", false);
-#ifdef MOZ_ANDROID_SYNTHAPKS
     Services.obs.addObserver(this, "webapps-runtime-install", false);
     Services.obs.addObserver(this, "webapps-runtime-install-package", false);
     Services.obs.addObserver(this, "webapps-ask-install", false);
     Services.obs.addObserver(this, "webapps-launch", false);
     Services.obs.addObserver(this, "webapps-uninstall", false);
     Services.obs.addObserver(this, "Webapps:AutoInstall", false);
     Services.obs.addObserver(this, "Webapps:Load", false);
     Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
-#endif
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
 
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
       sendMessageToJava({
@@ -372,23 +368,19 @@ var BrowserApp = {
     LightWeightThemeWebInstaller.init();
     Downloads.init();
     FormAssistant.init();
     IndexedDB.init();
     HealthReportStatusListener.init();
     XPInstallObserver.init();
     CharacterEncoding.init();
     ActivityObserver.init();
-#ifdef MOZ_ANDROID_SYNTHAPKS
     // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
     Cu.import("resource://gre/modules/Webapps.jsm");
     DOMApplicationRegistry.allAppsLaunchable = true;
-#else
-    WebappsUI.init();
-#endif
     RemoteDebugger.init();
     Reader.init();
     UserAgentOverrides.init();
     DesktopUserAgent.init();
     CastingApps.init();
     Distribution.init();
     Tabs.init();
 #ifdef ACCESSIBILITY
@@ -765,19 +757,16 @@ var BrowserApp = {
     LightWeightThemeWebInstaller.uninit();
     FormAssistant.uninit();
     IndexedDB.uninit();
     ViewportHandler.uninit();
     XPInstallObserver.uninit();
     HealthReportStatusListener.uninit();
     CharacterEncoding.uninit();
     SearchEngines.uninit();
-#ifndef MOZ_ANDROID_SYNTHAPKS
-    WebappsUI.uninit();
-#endif
     RemoteDebugger.uninit();
     Reader.uninit();
     UserAgentOverrides.uninit();
     DesktopUserAgent.uninit();
     ExternalApps.uninit();
     CastingApps.uninit();
     Distribution.uninit();
     Tabs.uninit();
@@ -956,25 +945,23 @@ var BrowserApp = {
 
     let message = {
       type: "Tab:Close",
       tabID: aTab.id
     };
     sendMessageToJava(message);
   },
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
   _loadWebapp: function(aMessage) {
 
     this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
       this.manifestUrl = aMessage.url;
       this.addTab(aUrl, { title: aMessage.name });
     });
   },
-#endif
 
   // Calling this will update the state in BrowserApp after a tab has been
   // closed in the Java UI.
   _handleTabClosed: function _handleTabClosed(aTab, aShowUndoToast) {
     if (aTab == this.selectedTab)
       this.selectedTab = null;
 
     let tabIndex = this._tabs.indexOf(aTab);
@@ -1622,17 +1609,16 @@ var BrowserApp = {
         gViewportMargins = JSON.parse(aData);
         this.selectedTab.updateViewportSize(gScreenWidth);
         break;
 
       case "nsPref:changed":
         this.notifyPrefObservers(aData);
         break;
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
       case "webapps-runtime-install":
         WebappManager.install(JSON.parse(aData), aSubject);
         break;
 
       case "webapps-runtime-install-package":
         WebappManager.installPackage(JSON.parse(aData), aSubject);
         break;
 
@@ -1656,31 +1642,38 @@ var BrowserApp = {
 
       case "Webapps:Load":
         this._loadWebapp(JSON.parse(aData));
         break;
 
       case "Webapps:AutoUninstall":
         WebappManager.autoUninstall(JSON.parse(aData));
         break;
-#endif
 
       case "Locale:Changed":
         if (aData) {
           // The value provided to Locale:Changed should be a BCP47 language tag
           // understood by Gecko -- for example, "es-ES" or "de".
           console.log("Locale:Changed: " + aData);
           Services.prefs.setCharPref("general.useragent.locale", aData);
         } else {
           // Resetting.
           console.log("Switching to system locale.");
           Services.prefs.clearUserPref("general.useragent.locale");
         }
 
         Services.prefs.setBoolPref("intl.locale.matchOS", !aData);
+
+        // Ensure that this choice is immediately persisted, because
+        // Gecko won't be told again if it forgets.
+        Services.prefs.savePrefFile(null);
+
+        // Blow away the string cache so that future lookups get the
+        // correct locale.
+        Services.strings.flushBundles();
         break;
 
       default:
         dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
         break;
 
     }
   },
@@ -2285,26 +2278,56 @@ var NativeWindow = {
         if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
           res.push(item);
         }
       }
 
       return res;
     },
 
+    _findTarget: function(x, y) {
+      let isDescendant = function(parent, child) {
+        let node = child;
+        while (node) {
+          if (node === parent) {
+            return true;
+          }
+
+          node = node.parentNode;
+        }
+
+        return false;
+      };
+
+      let target = BrowserEventHandler._highlightElement;
+      let touchTarget = ElementTouchHelper.anyElementFromPoint(x, y);
+
+      // If we have a highlighted element that has a click handler, we want to ensure our target is inside it
+      if (isDescendant(target, touchTarget)) {
+        target = touchTarget;
+      } else if (!target) {
+        // Otherwise, let's try to find something clickable
+        target = ElementTouchHelper.elementFromPoint(x, y);
+
+        // If that failed, we'll just fall back to anything under the user's finger
+        if (!target) {
+          target = touchTarget;
+        }
+      }
+
+      return target;
+    },
+
     /* Checks if there are context menu items to show, and if it finds them
      * sends a contextmenu event to content. We also send showing events to
      * any html5 context menus we are about to show, and fire some local notifications
      * for chrome consumers to do lazy menuitem construction
      */
     _sendToContent: function(x, y) {
-      let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
-      if (!target)
-        target = ElementTouchHelper.anyElementFromPoint(x, y);
-
+      let target = this._findTarget(x, y);
       if (!target)
         return;
 
       this._target = target;
 
       Services.obs.notifyObservers(null, "before-build-contextmenu", "");
       this._buildMenu(x, y);
 
@@ -7025,256 +7048,16 @@ var ActivityObserver = {
     }
 
     if (tab && tab.getActive() != isForeground) {
       tab.setActive(isForeground);
     }
   }
 };
 
-#ifndef MOZ_ANDROID_SYNTHAPKS
-var WebappsUI = {
-  init: function init() {
-    Cu.import("resource://gre/modules/Webapps.jsm");
-    Cu.import("resource://gre/modules/AppsUtils.jsm");
-    DOMApplicationRegistry.allAppsLaunchable = true;
-
-    Services.obs.addObserver(this, "webapps-ask-install", false);
-    Services.obs.addObserver(this, "webapps-launch", false);
-    Services.obs.addObserver(this, "webapps-uninstall", false);
-    Services.obs.addObserver(this, "webapps-install-error", false);
-  },
-
-  uninit: function unint() {
-    Services.obs.removeObserver(this, "webapps-ask-install");
-    Services.obs.removeObserver(this, "webapps-launch");
-    Services.obs.removeObserver(this, "webapps-uninstall");
-    Services.obs.removeObserver(this, "webapps-install-error");
-  },
-
-  DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
-  DEFAULT_PREFS_FILENAME: "default-prefs.js",
-
-  observe: function observe(aSubject, aTopic, aData) {
-    let data = {};
-    try {
-      data = JSON.parse(aData);
-      data.mm = aSubject;
-    } catch(ex) { }
-    switch (aTopic) {
-      case "webapps-install-error":
-        let msg = "";
-        switch (aData) {
-          case "INVALID_MANIFEST":
-          case "MANIFEST_PARSE_ERROR":
-            msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
-            break;
-          case "NETWORK_ERROR":
-          case "MANIFEST_URL_ERROR":
-            msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
-            break;
-          default:
-            msg = Strings.browser.GetStringFromName("webapps.installError");
-        }
-        NativeWindow.toast.show(msg, "short");
-        console.log("Error installing app: " + aData);
-        break;
-      case "webapps-ask-install":
-        this.doInstall(data);
-        break;
-      case "webapps-launch":
-        this.openURL(data.manifestURL, data.origin);
-        break;
-      case "webapps-uninstall":
-        sendMessageToJava({
-          type: "Webapps:Uninstall",
-          origin: data.origin
-        });
-        break;
-    }
-  },
-
-  doInstall: function doInstall(aData) {
-    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
-    let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
-
-    if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
-      // Get a profile for the app to be installed in. We'll download everything before creating the icons.
-      let origin = aData.app.origin;
-      sendMessageToJava({
-         type: "Webapps:Preinstall",
-         name: manifest.name,
-         manifestURL: aData.app.manifestURL,
-         origin: origin
-      }, (data) => {
-        let profilePath = data.profile;
-        if (!profilePath)
-          return;
-
-        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
-        file.initWithPath(profilePath);
-
-        let self = this;
-        DOMApplicationRegistry.confirmInstall(aData, file,
-          function (aManifest) {
-            let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
-
-            // the manifest argument is the manifest from within the zip file,
-            // TODO so now would be a good time to ask about permissions.
-            self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
-              function(scaledIcon, fullsizeIcon) {
-                // if java returned a profile path to us, try to use it to pre-populate the app cache
-                // also save the icon so that it can be used in the splash screen
-                try {
-                  let iconFile = file.clone();
-                  iconFile.append("logo.png");
-                  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
-                  persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
-                  persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
-
-                  let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
-                  persist.saveURI(source, null, null, null, null, iconFile, null);
-
-                  // aData.app.origin may now point to the app: url that hosts this app
-                  sendMessageToJava({
-                    type: "Webapps:Postinstall",
-                    name: localeManifest.name,
-                    manifestURL: aData.app.manifestURL,
-                    originalOrigin: origin,
-                    origin: aData.app.origin,
-                    iconURL: fullsizeIcon
-                  });
-                  if (!!aData.isPackage) {
-                    // For packaged apps, put a notification in the notification bar.
-                    let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
-                    let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
-                    alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
-                      observe: function () {
-                        self.openURL(aData.app.manifestURL, aData.app.origin);
-                      }
-                    }, "webapp");
-                  }
-
-                  // Create a system notification allowing the user to launch the app
-                  let observer = {
-                    observe: function (aSubject, aTopic) {
-                      if (aTopic == "alertclickcallback") {
-                        WebappsUI.openURL(aData.app.manifestURL, origin);
-                      }
-                    }
-                  };
-
-                  let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
-                  let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
-                  alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
-                } catch(ex) {
-                  console.log(ex);
-                }
-                self.writeDefaultPrefs(file, localeManifest);
-              }
-            );
-          }
-        );
-      });
-    } else {
-      DOMApplicationRegistry.denyInstall(aData);
-    }
-  },
-
-  writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
-      // build any app specific default prefs
-      let prefs = [];
-      if (aManifest.orientation) {
-        prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
-      }
-
-      // write them into the app profile
-      let defaultPrefsFile = aProfile.clone();
-      defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
-      this._writeData(defaultPrefsFile, prefs);
-  },
-
-  _writeData: function(aFile, aPrefs) {
-    if (aPrefs.length > 0) {
-      let array = new TextEncoder().encode(JSON.stringify(aPrefs));
-      OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
-        console.log("Error writing default prefs: " + reason);
-      });
-    }
-  },
-
-  openURL: function openURL(aManifestURL, aOrigin) {
-    sendMessageToJava({
-      type: "Webapps:Open",
-      manifestURL: aManifestURL,
-      origin: aOrigin
-    });
-  },
-
-  get iconSize() {
-    let iconSize = 64;
-    try {
-      let jni = new JNI();
-      let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
-      let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
-      iconSize = jni.callStaticIntMethod(cls, method);
-      jni.close();
-    } catch(ex) {
-      console.log(ex);
-    }
-
-    delete this.iconSize;
-    return this.iconSize = iconSize;
-  },
-
-  makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
-    let size = this.iconSize;
-
-    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    canvas.width = canvas.height = size;
-    let ctx = canvas.getContext("2d");
-    let favicon = new Image();
-    favicon.onload = function() {
-      ctx.drawImage(favicon, 0, 0, size, size);
-      let scaledIcon = canvas.toDataURL("image/png", "");
-
-      canvas.width = favicon.width;
-      canvas.height = favicon.height;
-      ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
-      let fullsizeIcon = canvas.toDataURL("image/png", "");
-
-      canvas = null;
-      aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
-    };
-    favicon.onerror = function() {
-      Cu.reportError("CreateShortcut: favicon image load error");
-
-      // if the image failed to load, and it was not our default icon, attempt to
-      // use our default as a fallback
-      if (favicon.src != WebappsUI.DEFAULT_ICON) {
-        favicon.src = WebappsUI.DEFAULT_ICON;
-      }
-    };
-  
-    favicon.src = aIconURL;
-  },
-
-  createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
-    this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
-      try {
-        let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
-        shell.createShortcut(aTitle, aURL, icon, aType);
-      } catch(e) {
-        Cu.reportError(e);
-      }
-    });
-  }
-}
-#endif
-
 var RemoteDebugger = {
   init: function rd_init() {
     Services.prefs.addObserver("devtools.debugger.", this, false);
 
     if (this._isEnabled())
       this._start();
   },
 
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -108,22 +108,20 @@ contract @mozilla.org/tab-source-service
 #endif
 
 # Snippets.js
 component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
 contract @mozilla.org/snippets;1 {a78d7e59-b558-4321-a3d6-dffe2f1e76dd}
 category profile-after-change Snippets @mozilla.org/snippets;1
 category update-timer Snippets @mozilla.org/snippets;1,getService,snippets-update-timer,browser.snippets.updateInterval,86400
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 # WebappsUpdateTimer.js
 component {8f7002cb-e959-4f0a-a2e8-563232564385} WebappsUpdateTimer.js
 contract @mozilla.org/webapps-update-timer;1 {8f7002cb-e959-4f0a-a2e8-563232564385}
 category update-timer WebappsUpdateTimer @mozilla.org/webapps-update-timer;1,getService,webapp-background-update-timer,browser.webapps.updateInterval,86400
-#endif
 
 # ColorPicker.js
 component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
 contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
 
 # ActivitiesGlue.js
 component {e4deb5f6-d5e3-4fce-bc53-901dd9951c48} ActivitiesGlue.js
 contract @mozilla.org/dom/activities/ui-glue;1 {e4deb5f6-d5e3-4fce-bc53-901dd9951c48}
--- a/mobile/android/confvars.sh
+++ b/mobile/android/confvars.sh
@@ -62,13 +62,10 @@ MOZ_SERVICES_HEALTHREPORT=1
 MOZ_SERVICES_FXACCOUNTS=1
 
 # Enable Wifi-AP/cell tower data reporting
 MOZ_DATA_REPORTING=1
 
 # Enable runtime locale switching.
 MOZ_LOCALE_SWITCHER=1
 
-# Enable the "synthetic APKs" implementation of Open Web Apps.
-MOZ_ANDROID_SYNTHAPKS=1
-
 # Enable second screen and casting support for external devices.
 MOZ_DEVICES=1
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -616,15 +616,12 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 
 #ifdef ENABLE_MARIONETTE
 @BINPATH@/chrome/marionette@JAREXT@ 
 @BINPATH@/chrome/marionette.manifest
 @BINPATH@/components/MarionetteComponents.manifest
 @BINPATH@/components/marionettecomponent.js
 #endif
 
-#ifdef MOZ_ANDROID_SYNTHAPKS
 @BINPATH@/components/WebappsUpdateTimer.js
-#endif
-
 @BINPATH@/components/DataStore.manifest
 @BINPATH@/components/DataStoreImpl.js
 @BINPATH@/components/dom_datastore.xpt
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -237,26 +237,16 @@ stacktrace.anonymousFunction=<anonymous>
 stacktrace.outputMessage=Stack trace from %S, function %S, line %S.
 timer.start=%S: timer started
 
 # LOCALIZATION NOTE (timer.end):
 # This string is used to display the result of the console.timeEnd() call.
 # %1$S=name of timer, %2$S=number of milliseconds
 timer.end=%1$S: %2$Sms
 
-# Webapps
-webapps.installTitle=Install Application
-webapps.alertSuccess=Successfully installed
-# Shown when there is a generic problem installing an app
-webapps.installError=Error installing application
-# Shown when there is something wrong with an apps manifest
-webapps.manifestInstallError=Invalid application manifest
-# Shown when a network error prevented installing an app
-webapps.networkInstallError=Could not download manifest
-
 # Click to play plugins
 clickToPlayPlugins.message2=%S contains plugin content. Would you like to activate it?
 clickToPlayPlugins.activate=Activate
 clickToPlayPlugins.dontActivate=Don't activate
 # LOCALIZATION NOTE (clickToPlayPlugins.dontAskAgain): This label appears next to a
 # checkbox to indicate whether or not the user wants to make a permanent decision.
 clickToPlayPlugins.dontAskAgain=Don't ask again for this site
 # LOCALIZATION NOTE (clickToPlayPlugins.playPlugins): Label that
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -32,19 +32,17 @@
   locale/@AB_CD@/browser/pippki.properties        (%chrome/pippki.properties)
   locale/@AB_CD@/browser/sync.dtd                 (%chrome/sync.dtd)
   locale/@AB_CD@/browser/sync.properties          (%chrome/sync.properties)
   locale/@AB_CD@/browser/prompt.dtd               (%chrome/prompt.dtd)
   locale/@AB_CD@/browser/feedback.dtd             (%chrome/feedback.dtd)
   locale/@AB_CD@/browser/phishing.dtd             (%chrome/phishing.dtd)
   locale/@AB_CD@/browser/payments.properties      (%chrome/payments.properties)
   locale/@AB_CD@/browser/handling.properties      (%chrome/handling.properties)
-#ifdef MOZ_ANDROID_SYNTHAPKS
   locale/@AB_CD@/browser/webapp.properties        (%chrome/webapp.properties)
-#endif
 
 # overrides for toolkit l10n, also for en-US
 relativesrcdir toolkit/locales:
   locale/@AB_CD@/browser/overrides/about.dtd                       (%chrome/global/about.dtd)
   locale/@AB_CD@/browser/overrides/aboutAbout.dtd                  (%chrome/global/aboutAbout.dtd)
   locale/@AB_CD@/browser/overrides/aboutRights.dtd                 (%chrome/global/aboutRights.dtd)
   locale/@AB_CD@/browser/overrides/charsetMenu.properties          (%chrome/global/charsetMenu.properties)
   locale/@AB_CD@/browser/overrides/commonDialogs.properties        (%chrome/global/commonDialogs.properties)
--- a/mobile/android/modules/moz.build
+++ b/mobile/android/modules/moz.build
@@ -17,21 +17,15 @@ EXTRA_JS_MODULES += [
     'Messaging.jsm',
     'Notifications.jsm',
     'OrderedBroadcast.jsm',
     'Prompt.jsm',
     'Sanitizer.jsm',
     'SharedPreferences.jsm',
     'SimpleServiceDiscovery.jsm',
     'SSLExceptions.jsm',
+    'WebappManagerWorker.js',
 ]
 
 EXTRA_PP_JS_MODULES += [
     'RokuApp.jsm',
+    'WebappManager.jsm',
 ]
-
-if CONFIG['MOZ_ANDROID_SYNTHAPKS']:
-    EXTRA_PP_JS_MODULES += [
-        'WebappManager.jsm',
-    ]
-    EXTRA_JS_MODULES += [
-        'WebappManagerWorker.js',
-    ]
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -644,16 +644,17 @@ FxAccountsInternal.prototype = {
 
       if (logPII) {
         log.debug("kB_hex: " + kB_hex);
       }
       data.kA = CommonUtils.bytesAsHex(kA);
       data.kB = CommonUtils.bytesAsHex(kB_hex);
 
       delete data.keyFetchToken;
+      delete data.unwrapBKey;
 
       log.debug("Keys Obtained: kA=" + !!data.kA + ", kB=" + !!data.kB);
       if (logPII) {
         log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
       }
 
       yield currentState.setUserAccountData(data);
       // We are now ready for business. This should only be invoked once
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -289,27 +289,29 @@ add_test(function test_getKeys() {
   // Once email has been verified, we will be able to get keys
   user.verified = true;
 
   fxa.setSignedInUser(user).then(() => {
     fxa.getSignedInUser().then((user) => {
       // Before getKeys, we have no keys
       do_check_eq(!!user.kA, false);
       do_check_eq(!!user.kB, false);
-      // And we still have a key-fetch token to use
+      // And we still have a key-fetch token and unwrapBKey to use
       do_check_eq(!!user.keyFetchToken, true);
+      do_check_eq(!!user.unwrapBKey, true);
 
       fxa.internal.getKeys().then(() => {
         fxa.getSignedInUser().then((user) => {
           // Now we should have keys
           do_check_eq(fxa.internal.isUserEmailVerified(user), true);
           do_check_eq(!!user.verified, true);
           do_check_eq(user.kA, expandHex("11"));
           do_check_eq(user.kB, expandHex("66"));
           do_check_eq(user.keyFetchToken, undefined);
+          do_check_eq(user.unwrapBKey, undefined);
           do_test_finished();
           run_next_test();
         });
       });
     });
   });
 });
 
--- a/toolkit/components/places/PlacesBackups.jsm
+++ b/toolkit/components/places/PlacesBackups.jsm
@@ -58,18 +58,17 @@ function getHashFromFilename(aFilename) 
 /**
  * Given two filenames, checks if they contain the same date.
  */
 function isFilenameWithSameDate(aSourceName, aTargetName) {
   let sourceMatches = aSourceName.match(filenamesRegex);
   let targetMatches = aTargetName.match(filenamesRegex);
 
   return sourceMatches && targetMatches &&
-         sourceMatches[1] == targetMatches[1] &&
-         sourceMatches[4] == targetMatches[4];
+         sourceMatches[1] == targetMatches[1];
 }
 
 /**
  * Given a filename, searches for another backup with the same date.
  *
  * @return OS.File path string or null.
  */
 function getBackupFileForSameDate(aFilename) {
@@ -433,16 +432,20 @@ this.PlacesBackups = {
                                                            { count: nodeCount,
                                                              hash: hash });
       } catch (ex if ex.becauseSameHash) {
         // The last backup already contained up-to-date information, just
         // rename it as if it was today's backup.
         this._backupFiles.shift();
         this._entries.shift();
         newBackupFile = mostRecentBackupFile;
+        // Ensure we retain the proper extension when renaming
+        // the most recent backup file.
+        if (/\.json$/.test(OS.Path.basename(mostRecentBackupFile)))
+          newBackupFilename = this.getFilenameForDate();
         newFilenameWithMetaData = appendMetaDataToFilename(
           newBackupFilename,
           { count: this.getBookmarkCountForFile(mostRecentBackupFile),
             hash: mostRecentHash });
       }
 
       // Append metadata to the backup filename.
       let newBackupFileWithMetadata = OS.Path.join(backupFolder, newFilenameWithMetaData);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js
@@ -0,0 +1,103 @@
+function run_test() {
+  run_next_test();
+}
+
+/* Bug 1016953 - When a previous bookmark backup exists with the same hash
+regardless of date, an automatic backup should attempt to either rename it to
+today's date if the backup was for an old date or leave it alone if it was for
+the same date. However if the file ext was json it will accidentally rename it
+to jsonlz4 while keeping the json contents
+*/
+
+add_task(function* test_same_date_same_hash() {
+  // If old file has been created on the same date and has the same hash
+  // the file should be left alone
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  // Save to profile dir to obtain hash and nodeCount to append to filename
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+
+  // Save JSON file in backup folder with hash appended
+  let dateObj = new Date();
+  let filename = "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let backupFile = OS.Path.join(backupFolder, filename);
+  yield OS.File.move(tempPath, backupFile);
+
+  // Force a compressed backup which fallbacks to rename
+  yield PlacesBackups.create();
+  let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+  // check to ensure not renamed to jsonlz4
+  Assert.equal(mostRecentBackupFile, backupFile);
+  // inspect contents and check if valid json
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                        createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let result = yield OS.File.read(mostRecentBackupFile);
+  let jsonString = converter.convertFromByteArray(result, result.length);
+  do_log_info("Check is valid JSON");
+  JSON.parse(jsonString);
+
+  // Cleanup
+  yield OS.File.remove(backupFile);
+  yield OS.File.remove(tempPath);
+  PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_same_date_diff_hash() {
+  // If the old file has been created on the same date, but has a different hash
+  // the existing file should be overwritten with the newer compressed version
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+  let dateObj = new Date();
+  let filename = "bookmarks-" + dateObj.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + "differentHash==" + ".json";
+  let backupFile = OS.Path.join(backupFolder, filename);
+  yield OS.File.move(tempPath, backupFile);
+  yield PlacesBackups.create(); // Force compressed backup
+  mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+
+  // Decode lz4 compressed file to json and check if json is valid
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+                        createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  let result = yield OS.File.read(mostRecentBackupFile, { compression: "lz4" });
+  let jsonString = converter.convertFromByteArray(result, result.length);
+  do_log_info("Check is valid JSON");
+  JSON.parse(jsonString);
+
+  // Cleanup
+  yield OS.File.remove(mostRecentBackupFile);
+  yield OS.File.remove(tempPath);
+  PlacesBackups._backupFiles = null; // To force re-cache of backupFiles
+});
+
+add_task(function* test_diff_date_same_hash() {
+  // If the old file has been created on an older day but has the same hash
+  // it should be renamed with today's date without altering the contents.
+  let backupFolder = yield PlacesBackups.getBackupFolder();
+  let tempPath = OS.Path.join(OS.Constants.Path.profileDir,
+                              "bug10169583_bookmarks.json");
+  let {count, hash} = yield BookmarkJSONUtils.exportToFile(tempPath);
+  let oldDate = new Date(2014, 1, 1);
+  let curDate = new Date();
+  let oldFilename = "bookmarks-" + oldDate.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let newFilename = "bookmarks-" + curDate.toLocaleFormat("%Y-%m-%d") + "_" +
+                  count + "_" + hash + ".json";
+  let backupFile = OS.Path.join(backupFolder, oldFilename);
+  let newBackupFile = OS.Path.join(backupFolder, newFilename);
+  yield OS.File.move(tempPath, backupFile);
+
+  // Ensure file has been renamed correctly
+  yield PlacesBackups.create();
+  let mostRecentBackupFile = yield PlacesBackups.getMostRecentBackup();
+  Assert.equal(mostRecentBackupFile, newBackupFile);
+
+  // Cleanup
+  yield OS.File.remove(mostRecentBackupFile);
+  yield OS.File.remove(tempPath);
+});
--- a/toolkit/components/places/tests/bookmarks/xpcshell.ini
+++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini
@@ -28,8 +28,9 @@ tail =
 [test_675416.js]
 [test_711914.js]
 [test_protectRoots.js]
 [test_818593-store-backup-metadata.js]
 [test_818584-discard-duplicate-backups.js]
 [test_818587_compress-bookmarks-backups.js]
 [test_992901-backup-unsorted-hierarchy.js]
 [test_997030-bookmarks-html-encode.js]
+[test_1016953-renaming-uncompressed.js]
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2943,26 +2943,26 @@
   },
   "FENNEC_FAVICONS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
-    "description": "FENNEC: (Places) Number of favicons stored"
+    "description": "Number of favicons stored in the browser DB"
   },
   "FENNEC_THUMBNAILS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "2000",
     "n_buckets": 10,
     "cpp_guard": "ANDROID",
     "extended_statistics_ok": true,
-    "description": "FENNEC: (Places) Number of thumbnails stored"
+    "description": "Number of thumbnails stored in the browser DB"
   },
   "PLACES_SORTED_BOOKMARKS_PERC": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": "100",
     "n_buckets": 10,
     "description": "PLACES: Percentage of bookmarks organized in folders"
   },
@@ -4276,46 +4276,71 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "1000000",
     "n_buckets": 20,
     "extended_statistics_ok": true,
     "description": "Number of history entries in the original XUL places database",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_AWESOMEBAR_ALLPAGES_EMPTY_TIME": {
+  "FENNEC_GLOBALHISTORY_ADD_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": "20000",
     "n_buckets": 20,
-    "description": "Fennec: Time for the Awesomebar Top Sites query to return with no filter set (ms)",
+    "description": "Time for a record to be added to history (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_GLOBALHISTORY_UPDATE_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time for a record to be updated in history (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_GLOBALHISTORY_VISITED_BUILD_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time to update the visited link set (ms)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_LOWMEM_TAB_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 100,
     "n_buckets": 30,
     "description": "How many tabs were open when a low-memory event was received",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_RESTORING_ACTIVITY": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Fennec is starting up but the Gecko thread was still running",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_STARTUP_TIME_JAVAUI": {
-    "expires_in_version": "never",
-    "kind": "exponential",
-    "low": 100,
-    "high": "5000",
+  "FENNEC_SEARCH_LOADER_TIME_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
     "n_buckets": 20,
-    "description": "Time for the Java UI to load (ms)",
+    "description": "Time for a URL bar DB search to return (ms)",
+    "cpp_guard": "ANDROID"
+  },
+  "FENNEC_STARTUP_GECKOAPP_ACTION": {
+    "expires_in_version": "never",
+    "kind": "enumerated",
+    "n_values": 4,
+    "description": "The way the GeckoApp was launched. (Normal, URL, Prefetch, Redirector)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_STARTUP_TIME_ABOUTHOME": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 100,
     "high": "10000",
     "n_buckets": 20,
@@ -4326,21 +4351,23 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 500,
     "high": "20000",
     "n_buckets": 20,
     "description": "Time for the Gecko:Ready message to arrive (ms)",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_STARTUP_GECKOAPP_ACTION": {
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 4,
-    "description": "The way the GeckoApp was launched. (Normal, URL, Prefetch, Redirector)",
+  "FENNEC_STARTUP_TIME_JAVAUI": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 100,
+    "high": "5000",
+    "n_buckets": 20,
+    "description": "Time for the Java UI to load (ms)",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_TAB_EXPIRED": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": 604800,
     "n_buckets": 20,
@@ -4353,16 +4380,25 @@
     "kind": "exponential",
     "low": 10,
     "high": 604800,
     "n_buckets": 20,
     "extended_statistics_ok": true,
     "description": "How long (in seconds) a tab was inactive when it was OOM-zombified",
     "cpp_guard": "ANDROID"
   },
+  "FENNEC_TOPSITES_LOADER_TIME_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": "20000",
+    "n_buckets": 20,
+    "description": "Time for the home screen Top Sites query to return with no filter set (ms)",
+    "cpp_guard": "ANDROID"
+  },
   "FENNEC_WAS_KILLED": {
     "expires_in_version": "never",
     "kind": "flag",
     "description": "Killed, likely due to an OOM condition",
     "cpp_guard": "ANDROID"
   },
   "SECURITY_UI": {
     "expires_in_version": "never",
--- a/toolkit/content/widgets/scrollbox.xml
+++ b/toolkit/content/widgets/scrollbox.xml
@@ -22,33 +22,40 @@
       </xul:box>
     </content>
   </binding>
 
   <binding id="arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
     <content>
       <xul:autorepeatbutton class="autorepeatbutton-up"
                             anonid="scrollbutton-up"
-                            collapsed="true"
-                            xbl:inherits="orient"
+                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                             oncommand="_autorepeatbuttonScroll(event);"/>
+      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+                  xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
                      xbl:inherits="orient,align,pack,dir">
         <children/>
       </xul:scrollbox>
+      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+                  xbl:inherits="collapsed=scrolledtoend"/>
       <xul:autorepeatbutton class="autorepeatbutton-down"
                             anonid="scrollbutton-down"
-                            collapsed="true"
-                            xbl:inherits="orient"
+                            xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                             oncommand="_autorepeatbuttonScroll(event);"/>
     </content>
 
     <implementation>
+      <constructor><![CDATA[
+        this.setAttribute("notoverflowing", "true");
+        this._updateScrollButtonsDisabledState();
+      ]]></constructor>
+
       <destructor><![CDATA[
         this._stopSmoothScroll();
       ]]></destructor>
 
       <field name="_scrollbox">
         document.getAnonymousElementByAttribute(this, "anonid", "scrollbox");
       </field>
       <field name="_scrollButtonUp">
@@ -449,38 +456,49 @@
             this._isScrolling = 0;
             this._scrollTarget = null;
           }
         ]]></body>
       </method>
 
       <method name="_updateScrollButtonsDisabledState">
         <body><![CDATA[
-          var disableUpButton = false;
-          var disableDownButton = false;
+          var scrolledToStart = false;
+          var scrolledToEnd = false;
 
-          if (this.scrollPosition == 0) {
+          if (this.hasAttribute("notoverflowing")) {
+            scrolledToStart = true;
+            scrolledToEnd = true;
+          }
+          else if (this.scrollPosition == 0) {
             // In the RTL case, this means the _last_ element in the
             // scrollbox is visible
             if (this._isRTLScrollbox) 
-              disableDownButton = true;
+              scrolledToEnd = true;
             else
-              disableUpButton = true;
+              scrolledToStart = true;
           }
           else if (this.scrollClientSize + this.scrollPosition == this.scrollSize) {
             // In the RTL case, this means the _first_ element in the
             // scrollbox is visible
             if (this._isRTLScrollbox)
-              disableUpButton = true;
+              scrolledToStart = true;
             else
-              disableDownButton = true;
+              scrolledToEnd = true;
           }
 
-          this._scrollButtonUp.disabled = disableUpButton;
-          this._scrollButtonDown.disabled = disableDownButton;
+          if (scrolledToEnd)
+            this.setAttribute("scrolledtoend", "true");
+          else
+            this.removeAttribute("scrolledtoend");
+
+          if (scrolledToStart)
+            this.setAttribute("scrolledtostart", "true");
+          else
+            this.removeAttribute("scrolledtostart");
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="DOMMouseScroll"><![CDATA[
         if (this.orient == "vertical") {
           // prevent horizontal scrolling from scrolling a vertical scrollbox
@@ -530,28 +548,27 @@
           if (event.detail == 1)
             return;
         }
         else {    // horizontal scrollbox
           if (event.detail == 0)
             return;
         }
 
-        this._scrollButtonUp.collapsed = true;
-        this._scrollButtonDown.collapsed = true;
+        this.setAttribute("notoverflowing", "true");
+
         try {
           // See bug 341047 and comments in overflow handler as to why 
           // try..catch is needed here
           let childNodes = this._getScrollableElements();
           if (childNodes && childNodes.length)
             this.ensureElementIsVisible(childNodes[0], false);
         }
         catch(e) {
-          this._scrollButtonUp.collapsed = false;
-          this._scrollButtonDown.collapsed = false;
+          this.removeAttribute("notoverflowing");
         }
       ]]></handler>
 
       <handler event="overflow" phase="capturing"><![CDATA[
         // filter underflow events which were dispatched on nested scrollboxes
         if (event.target != this)
           return;
 
@@ -564,61 +581,63 @@
           if (event.detail == 1)
             return;
         }
         else {    // horizontal scrollbox
           if (event.detail == 0)
             return;
         }
 
-        this._scrollButtonUp.collapsed = false;
-        this._scrollButtonDown.collapsed = false;
+        this.removeAttribute("notoverflowing");
+
         try {
           // See bug 341047, the overflow event is dispatched when the 
           // scrollbox already is mostly destroyed. This causes some code in
           // _updateScrollButtonsDisabledState() to throw an error. It also
-          // means that the scrollbarbuttons were uncollapsed when that should
-          // not be happening, because the whole overflow event should not be
-          // happening in that case.
+          // means that the notoverflowing attribute was removed erroneously,
+          // as the whole overflow event should not be happening in that case.
           this._updateScrollButtonsDisabledState();
         } 
         catch(e) {
-          this._scrollButtonUp.collapsed = true;
-          this._scrollButtonDown.collapsed = true;
+          this.setAttribute("notoverflowing", "true");
         }
       ]]></handler>
 
       <handler event="scroll" action="this._updateScrollButtonsDisabledState()"/>
     </handlers>
   </binding>
 
   <binding id="autorepeatbutton" extends="chrome://global/content/bindings/scrollbox.xml#scrollbox-base">
     <content repeat="hover">
       <xul:image class="autorepeatbutton-icon"/>
     </content>
   </binding>
 
   <binding id="arrowscrollbox-clicktoscroll" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox">
     <content>
-      <xul:toolbarbutton class="scrollbutton-up" collapsed="true"
-                         xbl:inherits="orient"
+      <xul:toolbarbutton class="scrollbutton-up"
+                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtostart"
                          anonid="scrollbutton-up"
                          onclick="_distanceScroll(event);"
                          onmousedown="if (event.button == 0) _startScroll(-1);"
                          onmouseup="if (event.button == 0) _stopScroll();"
                          onmouseover="_continueScroll(-1);"
                          onmouseout="_pauseScroll();"/>
+      <xul:spacer class="arrowscrollbox-overflow-start-indicator"
+                  xbl:inherits="collapsed=scrolledtostart"/>
       <xul:scrollbox class="arrowscrollbox-scrollbox"
                      anonid="scrollbox"
                      flex="1"
                      xbl:inherits="orient,align,pack,dir">
         <children/>
       </xul:scrollbox>
-      <xul:toolbarbutton class="scrollbutton-down" collapsed="true"
-                         xbl:inherits="orient"
+      <xul:spacer class="arrowscrollbox-overflow-end-indicator"
+                  xbl:inherits="collapsed=scrolledtoend"/>
+      <xul:toolbarbutton class="scrollbutton-down"
+                         xbl:inherits="orient,collapsed=notoverflowing,disabled=scrolledtoend"
                          anonid="scrollbutton-down"
                          onclick="_distanceScroll(event);"
                          onmousedown="if (event.button == 0) _startScroll(1);"
                          onmouseup="if (event.button == 0) _stopScroll();"
                          onmouseover="_continueScroll(1);"
                          onmouseout="_pauseScroll();"/>
     </content>
     <implementation implements="nsITimerCallback, nsIDOMEventListener">
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -285,16 +285,22 @@ exports.hasSafeGetter = function hasSafe
  * See bugs 945920 and 946752 for discussion.
  *
  * @type Object aObj
  *       The object to check.
  * @return Boolean
  *         True if it is safe to read properties from aObj, or false otherwise.
  */
 exports.isSafeJSObject = function isSafeJSObject(aObj) {
+  // If we are running on a worker thread, Cu is not available. In this case,
+  // we always return false, just to be on the safe side.
+  if (!Cu) {
+    return false;
+  }
+
   if (Cu.getGlobalForObject(aObj) ==
       Cu.getGlobalForObject(exports.isSafeJSObject)) {
     return true; // aObj is not a cross-compartment wrapper.
   }
 
   let principal = Cu.getObjectPrincipal(aObj);
   if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
     return true; // allow chrome objects
--- a/toolkit/devtools/event-emitter.js
+++ b/toolkit/devtools/event-emitter.js
@@ -1,29 +1,33 @@
 /* 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/. */
 
 /**
  * EventEmitter.
  */
 
-this.EventEmitter = function EventEmitter() {};
+(function (factory) { // Module boilerplate
+  if (this.module && module.id.indexOf("event-emitter") >= 0) { // require
+    factory.call(this, require, exports, module);
+  } else { // Cu.import
+      const Cu = Components.utils;
+      const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+      this.promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
+      factory.call(this, devtools.require, this, { exports: this });
+      this.EXPORTED_SYMBOLS = ["EventEmitter"];
+  }
+}).call(this, function (require, exports, module) {
 
-if (typeof(require) === "function") {
-   module.exports = EventEmitter;
-   var {Cu, components} = require("chrome");
-} else {
-  var EXPORTED_SYMBOLS = ["EventEmitter"];
-  var Cu = this["Components"].utils;
-  var components = Components;
-}
+this.EventEmitter = function EventEmitter() {};
+module.exports = EventEmitter;
 
-const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
-const { Services } = Cu.import("resource://gre/modules/Services.jsm");
+const { Cu, components } = require("chrome");
+const Services = require("Services");
 
 /**
  * Decorate an object with event emitter functionality.
  *
  * @param Object aObjectToDecorate
  *        Bind all public methods of EventEmitter to
  *        the aObjectToDecorate object.
  */
@@ -185,8 +189,10 @@ EventEmitter.prototype = {
 
       argOut += ")";
       out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
 
       dump(out);
     }
   },
 };
+
+});
--- a/toolkit/devtools/server/actors/framerate.js
+++ b/toolkit/devtools/server/actors/framerate.js
@@ -49,28 +49,32 @@ let FramerateActor = exports.FramerateAc
     this._startTime = this._contentWin.performance.now();
     this._contentWin.requestAnimationFrame(this._onRefreshDriverTick);
   }, {
   }),
 
   /**
    * Stops monitoring framerate, returning the recorded values.
    */
-  stopRecording: method(function() {
+  stopRecording: method(function(beginAt = 0, endAt = Number.MAX_SAFE_INTEGER) {
     if (!this._recording) {
       return [];
     }
     this._recording = false;
 
     // We don't need to store the ticks array for future use, release it.
-    let ticks = this._ticks;
+    let ticks = this._ticks.filter(e => e >= beginAt && e <= endAt);
     this._ticks = null;
     return ticks;
   }, {
-    response: { timeline: RetVal("array:number") }
+    request: {
+      beginAt: Arg(0, "nullable:number"),
+      endAt: Arg(1, "nullable:number")
+    },
+    response: { ticks: RetVal("array:number") }
   }),
 
   /**
    * Function invoked along with the refresh driver.
    */
   _onRefreshDriverTick: function() {
     if (!this._recording) {
       return;
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -930,19 +930,22 @@ var WalkerActor = protocol.ActorClass({
     }
   },
 
   toString: function() {
     return "[WalkerActor " + this.actorID + "]";
   },
 
   destroy: function() {
-    this._hoveredNode = null;
+    this._destroyed = true;
+
     this.clearPseudoClassLocks();
     this._activePseudoClassLocks = null;
+
+    this._hoveredNode = null;
     this.rootDoc = null;
 
     this.reflowObserver.off("reflows", this._onReflows);
     this.reflowObserver = null;
     releaseLayoutChangesObserver(this.tabActor);
 
     events.emit(this, "destroyed");
     protocol.Actor.prototype.destroy.call(this);
@@ -1706,23 +1709,24 @@ var WalkerActor = protocol.ActorClass({
   _removePseudoClassLock: function(node, pseudo) {
     if (node.rawNode.nodeType != Ci.nsIDOMNode.ELEMENT_NODE) {
       return false;
     }
     DOMUtils.removePseudoClassLock(node.rawNode, pseudo);
     if (!node.writePseudoClassLocks()) {
       this._activePseudoClassLocks.delete(node);
     }
+
     this._queuePseudoClassMutation(node);
     return true;
   },
 
   /**
-   * Clear all the pseudo-classes on a given node
-   * or all nodes.
+   * Clear all the pseudo-classes on a given node or all nodes.
+   * @param {NodeActor} node Optional node to clear pseudo-classes on
    */
   clearPseudoClassLocks: method(function(node) {
     if (node) {
       DOMUtils.clearPseudoClassLocks(node.rawNode);
       this._activePseudoClassLocks.delete(node);
       this._queuePseudoClassMutation(node);
     } else {
       for (let locked of this._activePseudoClassLocks) {
@@ -1924,17 +1928,17 @@ var WalkerActor = protocol.ActorClass({
       cleanup: Option(0)
     },
     response: {
       mutations: RetVal("array:dommutation")
     }
   }),
 
   queueMutation: function(mutation) {
-    if (!this.actorID) {
+    if (!this.actorID || this._destroyed) {
       // We've been destroyed, don't bother queueing this mutation.
       return;
     }
     // We only send the `new-mutations` notification once, until the client
     // fetches mutations with the `getMutations` packet.
     let needEvent = this._pendingMutations.length === 0;
 
     this._pendingMutations.push(mutation);
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -16,16 +16,17 @@ support-files =
 [test_Debugger.Source.prototype.introductionType.html]
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
 [test_css-logic.html]
 [test_device.html]
 [test_framerate_01.html]
 [test_framerate_02.html]
+[test_framerate_03.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
 [test_inspector-mutations-value.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_framerate_03.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Bug 1023018 - Tests whether or not the framerate actor can handle time ranges.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Framerate actor test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+<script>
+
+window.onload = function() {
+  var Cu = Components.utils;
+  var Cc = Components.classes;
+  var Ci = Components.interfaces;
+
+  Cu.import("resource://gre/modules/Services.jsm");
+
+  // Always log packets when running tests.
+  Services.prefs.setBoolPref("devtools.debugger.log", true);
+  SimpleTest.registerCleanupFunction(function() {
+    Services.prefs.clearUserPref("devtools.debugger.log");
+  });
+
+  Cu.import("resource://gre/modules/devtools/Loader.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+  Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+
+  SimpleTest.waitForExplicitFinish();
+
+  var {FramerateFront} = devtools.require("devtools/server/actors/framerate");
+  var START_TICK = 2000;
+  var STOP_TICK = 3000;
+  var TOTAL_TIME = 5000;
+
+  DebuggerServer.init(function () { return true; });
+  DebuggerServer.addBrowserActors();
+
+  var client = new DebuggerClient(DebuggerServer.connectPipe());
+  client.connect(function onConnect() {
+    client.listTabs(function onListTabs(aResponse) {
+      var form = aResponse.tabs[aResponse.selected];
+      var front = FramerateFront(client, form);
+
+      front.startRecording().then(() => {
+        window.setTimeout(() => {
+          front.stopRecording(START_TICK, STOP_TICK).then(rawData => {
+            onRecordingStopped(front, rawData);
+          });
+        }, TOTAL_TIME);
+      });
+    });
+  });
+
+  function onRecordingStopped(front, rawData) {
+    ok(rawData, "There should be a recording available.");
+
+    ok(!rawData.find(e => e < START_TICK),
+      "There should be no tick before 2000ms.");
+    ok(!rawData.find(e => e > STOP_TICK),
+      "There should be no tick after 3000ms.");
+
+    for (var tick of rawData) {
+      info("Testing tick: " + tick);
+      is(typeof tick, "number", "All values should be numbers.");
+    }
+
+    client.close(() => {
+      DebuggerServer.destroy();
+      SimpleTest.finish()
+    });
+  }
+}
+</script>
+</pre>
+</body>
+</html>
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-01.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-01.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -33,17 +42,17 @@ function test_object_grip()
     let objClient = gThreadClient.pauseGrip(args[0]);
     objClient.getOwnPropertyNames(function(aResponse) {
       do_check_eq(aResponse.ownPropertyNames.length, 3);
       do_check_eq(aResponse.ownPropertyNames[0], "a");
       do_check_eq(aResponse.ownPropertyNames[1], "b");
       do_check_eq(aResponse.ownPropertyNames[2], "c");
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ a: 1, b: true, c: 'foo' })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-02.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-02.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -36,17 +45,17 @@ function test_object_grip()
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function(aResponse) {
         do_check_eq(aResponse.ownPropertyNames.length, 2);
         do_check_eq(aResponse.ownPropertyNames[0], "b");
         do_check_eq(aResponse.ownPropertyNames[1], "c");
 
         gThreadClient.resume(function() {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval(function Constr() {
     this.a = 1;
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-03.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-03.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -46,17 +55,17 @@ function test_object_grip()
         objClient.getProperty("a", function(aResponse) {
           do_check_eq(aResponse.descriptor.configurable, true);
           do_check_eq(aResponse.descriptor.enumerable, true);
           do_check_eq(aResponse.descriptor.get.type, "object");
           do_check_eq(aResponse.descriptor.get.class, "Function");
           do_check_eq(aResponse.descriptor.set.type, "undefined");
 
           gThreadClient.resume(function() {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-04.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-04.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -50,17 +59,17 @@ function test_object_grip()
 
       do_check_true(aResponse.prototype != undefined);
 
       let protoClient = gThreadClient.pauseGrip(aResponse.prototype);
       protoClient.getOwnPropertyNames(function(aResponse) {
         do_check_true(aResponse.ownPropertyNames.toString != undefined);
 
         gThreadClient.resume(function() {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju', get a() { return 42; } })");
 }
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-05.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-05.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that frozen objects report themselves as frozen in their
  * grip.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let obj1 = aPacket.frame.arguments[0];
     do_check_true(obj1.frozen);
 
@@ -39,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.frozen);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isFrozen);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.freeze(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-06.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-06.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that sealed objects report themselves as sealed in their
  * grip.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let obj1 = aPacket.frame.arguments[0];
     do_check_true(obj1.sealed);
 
@@ -39,17 +48,17 @@ function test_object_grip()
 
     let obj2 = aPacket.frame.arguments[1];
     do_check_false(obj2.sealed);
 
     let obj2Client = gThreadClient.pauseGrip(obj2);
     do_check_false(obj2Client.isSealed);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let obj1 = {};
     Object.seal(obj1);
     stopMe(obj1, {});
   } + "())");
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-07.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-07.js
@@ -4,33 +4,42 @@
 /**
  * This test checks that objects which are not extensible report themselves as
  * such.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2, arg3, arg4) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let [f, s, ne, e] = aPacket.frame.arguments;
     let [fClient, sClient, neClient, eClient] = aPacket.frame.arguments.map(
       a => gThreadClient.pauseGrip(a));
@@ -43,17 +52,17 @@ function test_object_grip()
 
     do_check_false(ne.extensible);
     do_check_false(neClient.isExtensible);
 
     do_check_true(e.extensible);
     do_check_true(eClient.isExtensible);
 
     gThreadClient.resume(_ => {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("(" + function () {
     let f = {};
     Object.freeze(f);
     let s = {};
     Object.seal(s);
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-08.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-08.js
@@ -1,31 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     do_check_eq(args[0].class, "Object");
@@ -48,16 +57,16 @@ function test_object_grip()
       do_check_eq(aResponse.ownProperties.c.value.type, "NaN");
 
       do_check_eq(aResponse.ownProperties.d.configurable, true);
       do_check_eq(aResponse.ownProperties.d.enumerable, true);
       do_check_eq(aResponse.ownProperties.d.writable, true);
       do_check_eq(aResponse.ownProperties.d.value.type, "-0");
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
   });
 
   gDebuggee.eval("stopMe({ a: Infinity, b: -Infinity, c: NaN, d: -0 })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
+++ b/toolkit/devtools/server/tests/unit/test_objectgrips-09.js
@@ -3,33 +3,42 @@
 /**
  * This tests exercises getProtypesAndProperties message accepted
  * by a thread actor.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-grips");
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-grips", aServer);
   gDebuggee.eval(function stopMe(arg1, arg2) {
     debugger;
   }.toString());
 
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-grips", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_object_grip();
     });
   });
-  do_test_pending();
 }
 
 function test_object_grip()
 {
   gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
     let args = aPacket.frame.arguments;
 
     gThreadClient.getPrototypesAndProperties([args[0].actor, args[1].actor], function(aResponse) {
@@ -49,17 +58,17 @@ function test_object_grip()
       do_check_eq(obj2.ownProperties.z.enumerable, true);
       do_check_eq(obj2.ownProperties.z.writable, true);
       do_check_eq(obj2.ownProperties.z.value, 123);
 
       do_check_true(obj1.prototype != undefined);
       do_check_true(obj2.prototype != undefined);
 
       gThreadClient.resume(function() {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
 
   });
 
   gDebuggee.eval("stopMe({ x: 10, y: 'kaiju'}, { z: 123 })");
 }
 
--- a/toolkit/devtools/server/tests/unit/test_stepping-01.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-01.js
@@ -3,29 +3,38 @@
 
 /**
  * Check basic step-over functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
         gThreadClient.stepOver();
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
--- a/toolkit/devtools/server/tests/unit/test_stepping-02.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-02.js
@@ -3,29 +3,38 @@
 
 /**
  * Check basic step-in functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
           // When leaving a stack frame the line number doesn't change.
           do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 3);
           do_check_eq(aPacket.why.type, "resumeLimit");
           // Check that stepping worked.
           do_check_eq(gDebuggee.a, 1);
           do_check_eq(gDebuggee.b, 2);
 
           gThreadClient.resume(function () {
-            finishClient(gClient);
+            gClient.close(gCallback);
           });
         });
         gThreadClient.stepIn();
       });
       gThreadClient.stepIn();
 
     });
     gThreadClient.stepIn();
--- a/toolkit/devtools/server/tests/unit/test_stepping-03.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-03.js
@@ -3,45 +3,54 @@
 
 /**
  * Check basic step-out functionality.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
       do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 4);
       do_check_eq(aPacket.why.type, "resumeLimit");
       // Check that stepping worked.
       do_check_eq(gDebuggee.a, 1);
       do_check_eq(gDebuggee.b, 2);
 
       gThreadClient.resume(function () {
-        finishClient(gClient);
+        gClient.close(gCallback);
       });
     });
     gThreadClient.stepOut();
 
   });
 
   gDebuggee.eval("var line0 = Error().lineNumber;\n" +
                  "function f() {\n" + // line0 + 1
--- a/toolkit/devtools/server/tests/unit/test_stepping-04.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-04.js
@@ -3,29 +3,38 @@
 
 /**
  * Check that stepping over a function call does not pause inside the function.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_simple_stepping();
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -40,17 +49,17 @@ function test_simple_stepping()
         do_check_eq(aPacket.type, "paused");
         do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 6);
         do_check_eq(aPacket.why.type, "resumeLimit");
         // Check that stepping worked.
         do_check_eq(gDebuggee.a, 1);
         do_check_eq(gDebuggee.b, undefined);
 
         gThreadClient.resume(function () {
-          finishClient(gClient);
+          gClient.close(gCallback);
         });
       });
       gThreadClient.stepOver();
 
     });
     gThreadClient.stepOver();
 
   });
--- a/toolkit/devtools/server/tests/unit/test_stepping-05.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-05.js
@@ -5,29 +5,38 @@
  * Make sure that stepping in the last statement of the last frame doesn't
  * cause an unexpected pause, when another JS frame is pushed on the stack
  * (bug 785689).
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       test_stepping_last();
     });
   });
-  do_test_pending();
 }
 
 function test_stepping_last()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check the return value.
       do_check_eq(aPacket.type, "paused");
@@ -79,14 +88,14 @@ function test_next_pause()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     // Check the return value.
     do_check_eq(aPacket.type, "paused");
     // Before fixing bug 785689, the type was resumeLimit.
     do_check_eq(aPacket.why.type, "debuggerStatement");
 
     gThreadClient.resume(function () {
-      finishClient(gClient);
+      gClient.close(gCallback);
     });
   });
 
   gDebuggee.eval("debugger;");
 }
--- a/toolkit/devtools/server/tests/unit/test_stepping-06.js
+++ b/toolkit/devtools/server/tests/unit/test_stepping-06.js
@@ -3,34 +3,43 @@
 
 /**
  * Check that stepping out of a function returns the right return value.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gCallback;
 
 function run_test()
 {
-  initTestDebuggerServer();
-  gDebuggee = addTestGlobal("test-stack");
-  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  run_test_with_server(DebuggerServer, function () {
+    run_test_with_server(WorkerDebuggerServer, do_test_finished);
+  });
+  do_test_pending();
+};
+
+function run_test_with_server(aServer, aCallback)
+{
+  gCallback = aCallback;
+  initTestDebuggerServer(aServer);
+  gDebuggee = addTestGlobal("test-stack", aServer);
+  gClient = new DebuggerClient(aServer.connectPipe());
   gClient.connect(function () {
     attachTestTabAndResume(gClient, "test-stack", function (aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
       // XXX: We have to do an executeSoon so that the error isn't caught and
       // reported by DebuggerClient.requester (because we are using the local
       // transport and share a stack) which causes the test to fail.
       Services.tm.mainThread.dispatch({
         run: test_simple_stepping
       }, Ci.nsIThread.DISPATCH_NORMAL);
     });
   });
-  do_test_pending();
 }
 
 function test_simple_stepping()
 {
   gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
     gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
       // Check that the return value is 10.
       do_check_eq(aPacket.type, "paused");
@@ -50,17 +59,17 @@ function test_simple_stepping()
             gThreadClient.addOneTimeListener("paused", function (aEvent, aPacket) {
               // Check that the exception was thrown.
               do_check_eq(aPacket.type, "paused");
               do_check_eq(aPacket.frame.where.line, gDebuggee.line0 + 12);
               do_check_eq(aPacket.why.type, "resumeLimit");
               do_check_eq(aPacket.why.frameFinished.throw, "ah");
 
               gThreadClient.resume(function () {
-                finishClient(gClient);
+                gClient.close(gCallback);
               });
             });
             gThreadClient.stepOut();
           });
           gThreadClient.resume();
         });
         gThreadClient.stepOut();
       });
--- a/toolkit/devtools/transport/transport.js
+++ b/toolkit/devtools/transport/transport.js
@@ -255,18 +255,17 @@ DebuggerTransport.prototype = {
    */
   onOutputStreamReady: DevToolsUtils.makeInfallible(function(stream) {
     if (this._outgoing.length === 0) {
       return;
     }
 
     try {
       this._currentOutgoing.write(stream);
-    } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED ||
-                 e.result == Cr.NS_ERROR_NET_RESET) {
+    } catch(e if e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
       this.close(e.result);
       return;
     }
 
     this._flushOutgoing();
   }, "DebuggerTransport.prototype.onOutputStreamReady"),
 
   /**
@@ -333,19 +332,17 @@ DebuggerTransport.prototype = {
    * Called when the stream is either readable or closed.
    */
   onInputStreamReady:
   DevToolsUtils.makeInfallible(function(stream) {
     try {
       while(stream.available() && this._incomingEnabled &&
             this._processIncoming(stream, stream.available())) {}
       this._waitForIncoming();
-    } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED ||
-                 e.result == Cr.NS_ERROR_CONNECTION_REFUSED ||
-                 e.result == Cr.NS_ERROR_OFFLINE) {
+    } catch(e if e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) {
       this.close(e.result);
     }
   }, "DebuggerTransport.prototype.onInputStreamReady"),
 
   /**
    * Process the incoming data.  Will create a new currently incoming Packet if
    * needed.  Tells the incoming Packet to read as much data as it can, but
    * reading may not complete.  The Packet signals that its data is ready for
--- a/toolkit/devtools/worker-loader.js
+++ b/toolkit/devtools/worker-loader.js
@@ -89,28 +89,16 @@ function createModule(id) {
       configurable: false,
       enumerable: true,
       value: Object.create(null),
       writable: true
     }
   });
 };
 
-// A whitelist of modules from which the built-in chrome module may be
-// required. The idea is add all modules that depend on chrome to the whitelist
-// initially, and then remove them one by one, fixing any errors as we go along.
-// Once the whitelist is empty, we can remove the built-in chrome module from
-// the loader entirely.
-//
-// TODO: Remove this when the whitelist becomes empty
-let chromeWhitelist = [
-  "devtools/toolkit/DevToolsUtils",
-  "devtools/toolkit/event-emitter",
-];
-
 // Create a CommonJS loader with the following options:
 // - createSandbox:
 //     A function that will be used to create sandboxes. It takes the name and
 //     prototype of the sandbox to be created, and should return the newly
 //     created sandbox as result. This option is mandatory.
 // - globals:
 //     A map of built-in globals that will be exposed to every module. Defaults
 //     to the empty map.
@@ -183,27 +171,16 @@ function WorkerDebuggerLoader(options) {
   // create a require function for top-level modules instead.
   function createRequire(requirer) {
     return function require(id) {
       // Make sure an id was passed.
       if (id === undefined) {
         throw new Error("can't require module without id!");
       }
 
-      // If the module to be required is the built-in chrome module, and the
-      // requirer is not in the whitelist, return a vacuous object as if the
-      // module was unavailable.
-      //
-      // TODO: Remove this when the whitelist becomes empty
-      if (id === "chrome" && chromeWhitelist.indexOf(requirer.id) < 0) {
-        return { CC: undefined, Cc: undefined,
-                 ChromeWorker: undefined, Cm: undefined, Ci: undefined, Cu: undefined,
-                 Cr: undefined, components: undefined };
-      }
-
       // Built-in modules are cached by id rather than URL, so try to find the
       // module to be required by id first.
       let module = modules[id];
       if (module === undefined) {
         // Failed to find the module to be required by id, so convert the id to
         // a URL and try again.
 
         // If the id is relative, resolve it to an absolute id.
@@ -327,19 +304,19 @@ if (typeof Components === "object") {
 
     // TODO: Either replace these built-in modules with vacuous objects, or
     // provide them in a way that does not depend on the use of Components.
     const { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});;
     const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});;
     let SourceMap = {};
     Cu.import("resource://gre/modules/devtools/SourceMap.jsm", SourceMap);
     const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
-    const chrome = { CC: Function.bind.call(CC, Components), Cc: Cc,
-                     ChromeWorker: ChromeWorker, Cm: Cm, Ci: Ci, Cu: Cu,
-                     Cr: Cr, components: Components };
+    const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
+                     Cm: undefined, Ci: undefined, Cu: undefined,
+                     Cr: undefined, components: undefined };
     const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
                          getService(Ci.nsIJSInspector);
 
     this.worker = new WorkerDebuggerLoader({
       createSandbox: createSandbox,
       globals: {
         "promise": Promise,
         "reportError": Cu.reportError,
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -912,68 +912,65 @@ let Links = {
     let links = this._providerLinks.get(aProvider);
     if (!links)
       // This is not an error, it just means that between the time the provider
       // was added and the future time we call getLinks on it, it notified us of
       // a change.
       return;
 
     let { sortedLinks, linkMap } = links;
-
-    // Nothing to do if the list is full and the link isn't in it and shouldn't
-    // be in it.
-    if (!linkMap.has(aLink.url) &&
-        sortedLinks.length &&
-        sortedLinks.length == aProvider.maxNumLinks) {
-      let lastLink = sortedLinks[sortedLinks.length - 1];
-      if (this.compareLinks(lastLink, aLink) < 0)
-        return;
-    }
-
+    let existingLink = linkMap.get(aLink.url);
+    let insertionLink = null;
     let updatePages = false;
 
-    // Update the title in O(1).
-    if ("title" in aLink) {
-      let link = linkMap.get(aLink.url);
-      if (link && link.title != aLink.title) {
-        link.title = aLink.title;
+    if (existingLink) {
+      // Update our copy's position in O(lg n) by first removing it from its
+      // list.  It's important to do this before modifying its properties.
+      if (this._sortProperties.some(prop => prop in aLink)) {
+        let idx = this._indexOf(sortedLinks, existingLink);
+        if (idx < 0) {
+          throw new Error("Link should be in _sortedLinks if in _linkMap");
+        }
+        sortedLinks.splice(idx, 1);
+        // Update our copy's properties.
+        for (let prop of this._sortProperties) {
+          if (prop in aLink) {
+            existingLink[prop] = aLink[prop];
+          }
+        }
+        // Finally, reinsert our copy below.
+        insertionLink = existingLink;
+      }
+      // Update our copy's title in O(1).
+      if ("title" in aLink && aLink.title != existingLink.title) {
+        existingLink.title = aLink.title;
         updatePages = true;
       }
     }
-
-    // Update the link's position in O(lg n).
-    if (this._sortProperties.some((prop) => prop in aLink)) {
-      let link = linkMap.get(aLink.url);
-      if (link) {
-        // The link is already in the list.
-        let idx = this._indexOf(sortedLinks, link);
-        if (idx < 0)
-          throw new Error("Link should be in _sortedLinks if in _linkMap");
-        sortedLinks.splice(idx, 1);
-        for (let prop of this._sortProperties) {
-          if (prop in aLink)
-            link[prop] = aLink[prop];
+    else if (this._sortProperties.every(prop => prop in aLink)) {
+      // Before doing the O(lg n) insertion below, do an O(1) check for the
+      // common case where the new link is too low-ranked to be in the list.
+      if (sortedLinks.length && sortedLinks.length == aProvider.maxNumLinks) {
+        let lastLink = sortedLinks[sortedLinks.length - 1];
+        if (this.compareLinks(lastLink, aLink) < 0) {
+          return;
         }
       }
-      else {
-        // The link is new.
-        for (let prop of this._sortProperties) {
-          if (!(prop in aLink))
-            throw new Error("New link missing required sort property: " + prop);
-        }
-        // Copy the link object so that if the caller changes it, it doesn't
-        // screw up our bookkeeping.
-        link = {};
-        for (let [prop, val] of Iterator(aLink)) {
-          link[prop] = val;
-        }
-        linkMap.set(link.url, link);
+      // Copy the link object so that changes later made to it by the caller
+      // don't affect our copy.
+      insertionLink = {};
+      for (let prop in aLink) {
+        insertionLink[prop] = aLink[prop];
       }
-      let idx = this._insertionIndexOf(sortedLinks, link);
-      sortedLinks.splice(idx, 0, link);
+      linkMap.set(aLink.url, insertionLink);
+    }
+
+    if (insertionLink) {
+      let idx = this._insertionIndexOf(sortedLinks, insertionLink);
+      sortedLinks.splice(idx, 0, insertionLink);
       if (sortedLinks.length > aProvider.maxNumLinks) {
         let lastLink = sortedLinks.pop();
         linkMap.delete(lastLink.url);
       }
       updatePages = true;
     }
 
     if (updatePages)
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -46,22 +46,17 @@ add_test(function changeLinks() {
   NewTabUtils.links.addProvider(provider);
 
   // This is sync since the provider's getLinks is sync.
   NewTabUtils.links.populateCache(function () {}, false);
 
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a new link.
-  let newLink = {
-    url: "http://example.com/19",
-    title: "My frecency is 19",
-    frecency: 19,
-    lastVisitDate: 0,
-  };
+  let newLink = makeLink(19);
   expectedLinks.splice(1, 0, newLink);
   provider.notifyLinkChanged(newLink);
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a link that's changed sort criteria.
   newLink.frecency = 17;
   expectedLinks.splice(1, 1);
   expectedLinks.splice(2, 0, newLink);
@@ -76,21 +71,17 @@ add_test(function changeLinks() {
   provider.notifyLinkChanged({
     url: newLink.url,
     title: newLink.title,
   });
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of a new link again, but this time make it overflow maxNumLinks.
   provider.maxNumLinks = expectedLinks.length;
-  newLink = {
-    url: "http://example.com/21",
-    frecency: 21,
-    lastVisitDate: 0,
-  };
+  newLink = makeLink(21);
   expectedLinks.unshift(newLink);
   expectedLinks.pop();
   do_check_eq(expectedLinks.length, provider.maxNumLinks); // Sanity check.
   provider.notifyLinkChanged(newLink);
   do_check_links(NewTabUtils.links.getLinks(), expectedLinks);
 
   // Notify of many links changed.
   expectedLinks = makeLinks(0, 3, 1);
@@ -120,16 +111,44 @@ add_task(function oneProviderAlreadyCach
 
   NewTabUtils.links.populateCache(function () {}, false);
   do_check_links(NewTabUtils.links.getLinks(), links2.concat(links1));
 
   NewTabUtils.links.removeProvider(provider1);
   NewTabUtils.links.removeProvider(provider2);
 });
 
+add_task(function newLowRankedLink() {
+  // Init a provider with 10 links and make its maximum number also 10.
+  let links = makeLinks(0, 10, 1);
+  let provider = new TestProvider(done => done(links));
+  provider.maxNumLinks = links.length;
+
+  NewTabUtils.initWithoutProviders();
+  NewTabUtils.links.addProvider(provider);
+
+  // This is sync since the provider's getLinks is sync.
+  NewTabUtils.links.populateCache(function () {}, false);
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  // Notify of a new link that's low-ranked enough not to make the list.
+  let newLink = makeLink(0);
+  provider.notifyLinkChanged(newLink);
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  // Notify about the new link's title change.
+  provider.notifyLinkChanged({
+    url: newLink.url,
+    title: "a new title",
+  });
+  do_check_links(NewTabUtils.links.getLinks(), links);
+
+  NewTabUtils.links.removeProvider(provider);
+});
+
 function TestProvider(getLinksFn) {
   this.getLinks = getLinksFn;
   this._observers = new Set();
 }
 
 TestProvider.prototype = {
   addObserver: function (observer) {
     this._observers.add(observer);
@@ -160,17 +179,21 @@ function do_check_links(actualLinks, exp
     do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
   }
 }
 
 function makeLinks(frecRangeStart, frecRangeEnd, step) {
   let links = [];
   // Remember, links are ordered by frecency descending.
   for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
-    links.push({
-      url: "http://example.com/" + i,
-      title: "My frecency is " + i,
-      frecency: i,
-      lastVisitDate: 0,
-    });
+    links.push(makeLink(i));
   }
   return links;
 }
+
+function makeLink(frecency) {
+  return {
+    url: "http://example.com/" + frecency,
+    title: "My frecency is " + frecency,
+    frecency: frecency,
+    lastVisitDate: 0,
+  };
+}