merge fx-team to m-c
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 15 Apr 2014 15:09:02 +0200
changeset 196958 89d416958eefa21312c121c115ca2e8001909b58
parent 196948 56da72c95d48718ba22cdafb95707ed12ef8dcdb (current diff)
parent 196957 12796fe3eb01e86b2d725cd617863325c0052eec (diff)
child 197004 3d34a3b6443a32b3279c7672f2c27d1c59bd135e
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone31.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
addon-sdk/source/test/fixtures/pagemod-css-include-file.css
--- a/addon-sdk/source/examples/reading-data/lib/main.js
+++ b/addon-sdk/source/examples/reading-data/lib/main.js
@@ -1,44 +1,53 @@
 /* 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";
 
 var self = require("sdk/self");
-var panels = require("sdk/panel");
-var widgets = require("sdk/widget");
+var { Panel } = require("sdk/panel");
+var { ToggleButton } = require("sdk/ui");
 
 function replaceMom(html) {
   return html.replace("World", "Mom");
 }
 exports.replaceMom = replaceMom;
 
 exports.main = function(options, callbacks) {
   console.log("My ID is " + self.id);
 
   // Load the sample HTML into a string.
   var helloHTML = self.data.load("sample.html");
 
   // Let's now modify it...
   helloHTML = replaceMom(helloHTML);
 
   // ... and then create a panel that displays it.
-  var myPanel = panels.Panel({
-    contentURL: "data:text/html," + helloHTML
+  var myPanel = Panel({
+    contentURL: "data:text/html," + helloHTML,
+    onHide: handleHide
   });
 
-  // Load the URL of the sample image.
-  var iconURL = self.data.url("mom.png");
-
   // Create a widget that displays the image.  We'll attach the panel to it.
   // When you click the widget, the panel will pop up.
-  widgets.Widget({
+  var button = ToggleButton({
     id: "test-widget",
     label: "Mom",
-    contentURL: iconURL,
-    panel: myPanel
+    icon: './mom.png',
+    onChange: handleChange
   });
 
   // If you run cfx with --static-args='{"quitWhenDone":true}' this program
   // will automatically quit Firefox when it's done.
   if (options.staticArgs.quitWhenDone)
     callbacks.quit();
 }
+
+function handleChange(state) {
+  if (state.checked) {
+    myPanel.show({ position: button });
+  }
+}
+
+function handleHide() {
+  button.state('window', { checked: false });
+}
--- a/addon-sdk/source/examples/reading-data/tests/test-main.js
+++ b/addon-sdk/source/examples/reading-data/tests/test-main.js
@@ -1,16 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-// Disable tests below for now.
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
-/*
 var m = require("main");
 var self = require("sdk/self");
 
 exports.testReplace = function(test) {
   var input = "Hello World";
   var output = m.replaceMom(input);
   test.assertEqual(output, "Hello Mom");
   var callbacks = { quit: function() {} };
@@ -21,9 +18,8 @@ exports.testReplace = function(test) {
 
 exports.testID = function(test) {
   // The ID is randomly generated during tests, so we cannot compare it against
   // anything in particular.  Just assert that it is not empty.
   test.assert(self.id.length > 0);
   test.assertEqual(self.data.url("sample.html"),
                    "resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html");
 };
-*/
--- a/addon-sdk/source/examples/reddit-panel/lib/main.js
+++ b/addon-sdk/source/examples/reddit-panel/lib/main.js
@@ -1,31 +1,49 @@
 /* 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";
 
-var data = require("sdk/self").data;
+var { data } = require("sdk/self");
+var { ToggleButton } = require("sdk/ui");
+
+var base64png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYA" +
+                "AABzenr0AAAASUlEQVRYhe3O0QkAIAwD0eyqe3Q993AQ3cBSUKpygfsNTy" +
+                "N5ugbQpK0BAADgP0BRDWXWlwEAAAAAgPsA3rzDaAAAAHgPcGrpgAnzQ2FG" +
+                "bWRR9AAAAABJRU5ErkJggg%3D%3D";
 
 var reddit_panel = require("sdk/panel").Panel({
   width: 240,
   height: 320,
   contentURL: "http://www.reddit.com/.mobile?keep_extension=True",
   contentScriptFile: [data.url("jquery-1.4.4.min.js"),
-                      data.url("panel.js")]
+                      data.url("panel.js")],
+  onHide: handleHide
 });
 
 reddit_panel.port.on("click", function(url) {
   require("sdk/tabs").open(url);
 });
 
-require("sdk/widget").Widget({
+let button = ToggleButton({
   id: "open-reddit-btn",
   label: "Reddit",
-  contentURL: "http://www.reddit.com/static/favicon.ico",
-  panel: reddit_panel
+  icon: base64png,
+  onChange: handleChange
 });
 
 exports.main = function(options, callbacks) {
   // If you run cfx with --static-args='{"quitWhenDone":true}' this program
   // will automatically quit Firefox when it's done.
   if (options.staticArgs.quitWhenDone)
     callbacks.quit();
 };
+
+function handleChange(state) {
+  if (state.checked) {
+    reddit_panel.show({ position: button });
+  }
+}
+
+function handleHide() {
+  button.state('window', { checked: false });
+}
--- a/addon-sdk/source/examples/reddit-panel/tests/test-main.js
+++ b/addon-sdk/source/examples/reddit-panel/tests/test-main.js
@@ -1,16 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
-// Disable tests below for now.
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=987348
-/*
 var m = require("main");
 var self = require("sdk/self");
 
 exports.testMain = function(test) {
   var callbacks = { quit: function() {
     test.pass();
     test.done();
   } };
@@ -18,9 +15,8 @@ exports.testMain = function(test) {
   test.waitUntilDone();
   // Make sure it doesn't crash...
   m.main({ staticArgs: {quitWhenDone: true} }, callbacks);
 };
 
 exports.testData = function(test) {
   test.assert(self.data.load("panel.js").length > 0);
 };
-*/
--- a/addon-sdk/source/lib/sdk/content/content-worker.js
+++ b/addon-sdk/source/lib/sdk/content/content-worker.js
@@ -102,17 +102,19 @@ const ContentWorker = Object.freeze({
   injectConsole: function injectConsole(exports, pipe) {
     exports.console = Object.freeze({
       log: pipe.emit.bind(null, "console", "log"),
       info: pipe.emit.bind(null, "console", "info"),
       warn: pipe.emit.bind(null, "console", "warn"),
       error: pipe.emit.bind(null, "console", "error"),
       debug: pipe.emit.bind(null, "console", "debug"),
       exception: pipe.emit.bind(null, "console", "exception"),
-      trace: pipe.emit.bind(null, "console", "trace")
+      trace: pipe.emit.bind(null, "console", "trace"),
+      time: pipe.emit.bind(null, "console", "time"),
+      timeEnd: pipe.emit.bind(null, "console", "timeEnd")
     });
   },
 
   injectTimers: function injectTimers(exports, chromeAPI, pipe, console) {
     // wrapped functions from `'timer'` module.
     // Wrapper adds `try catch` blocks to the callbacks in order to
     // emit `error` event on a symbiont if exception is thrown in
     // the Worker global scope.
--- a/addon-sdk/source/lib/sdk/content/mod.js
+++ b/addon-sdk/source/lib/sdk/content/mod.js
@@ -26,27 +26,33 @@ exports.getTargetWindow = getTargetWindo
 
 let attachTo = method("attachTo");
 exports.attachTo = attachTo;
 
 let detachFrom = method("detatchFrom");
 exports.detachFrom = detachFrom;
 
 function attach(modification, target) {
+  if (!modification)
+    return;
+
   let window = getTargetWindow(target);
 
   attachTo(modification, window);
 
   // modification are stored per content; `window` reference can still be the
   // same even if the content is changed, therefore `document` is used instead.
   add(modification, window.document);
 }
 exports.attach = attach;
 
 function detach(modification, target) {
+  if (!modification)
+    return;
+
   if (target) {
     let window = getTargetWindow(target);
     detachFrom(modification, window);
     remove(modification, window.document);
   }
   else {
     let documents = iterator(modification);
     for (let document of documents) {
--- a/addon-sdk/source/lib/sdk/loader/sandbox.js
+++ b/addon-sdk/source/lib/sdk/loader/sandbox.js
@@ -62,8 +62,13 @@ function load(sandbox, uri) {
     let source = uri.substr(uri.indexOf(',') + 1);
 
     return evaluate(sandbox, decodeURIComponent(source), '1.8', uri, 0);
   } else {
     return scriptLoader.loadSubScript(uri, sandbox, 'UTF-8');
   }
 }
 exports.load = load;
+
+/**
+ * Forces the given `sandbox` to be freed immediately.
+ */
+exports.nuke = Cu.nukeSandbox
--- a/addon-sdk/source/lib/sdk/page-mod.js
+++ b/addon-sdk/source/lib/sdk/page-mod.js
@@ -196,16 +196,20 @@ function applyOnExistingDocuments (mod) 
 }
 
 function createWorker (mod, window) {
   let worker = Worker({
     window: window,
     contentScript: mod.contentScript,
     contentScriptFile: mod.contentScriptFile,
     contentScriptOptions: mod.contentScriptOptions,
+    // Bug 980468: Syntax errors from scripts can happen before the worker
+    // can set up an error handler. They are per-mod rather than per-worker
+    // so are best handled at the mod level.
+    onError: (e) => emit(mod, 'error', e)
   });
   workers.set(mod, worker);
   pipe(worker, mod);
   emit(mod, 'attach', worker);
   once(worker, 'detach', function detach() {
     worker.destroy();
   });
 }
--- a/addon-sdk/source/lib/sdk/panel.js
+++ b/addon-sdk/source/lib/sdk/panel.js
@@ -13,32 +13,34 @@ module.metadata = {
 };
 
 const { Ci } = require("chrome");
 const { setTimeout } = require('./timers');
 const { isPrivateBrowsingSupported } = require('./self');
 const { isWindowPBSupported } = require('./private-browsing/utils');
 const { Class } = require("./core/heritage");
 const { merge } = require("./util/object");
-const { WorkerHost, detach, attach, destroy } = require("./content/utils");
+const { WorkerHost } = require("./content/utils");
 const { Worker } = require("./content/worker");
 const { Disposable } = require("./core/disposable");
 const { WeakReference } = require('./core/reference');
 const { contract: loaderContract } = require("./content/loader");
 const { contract } = require("./util/contract");
 const { on, off, emit, setListeners } = require("./event/core");
 const { EventTarget } = require("./event/target");
 const domPanel = require("./panel/utils");
 const { events } = require("./panel/events");
 const systemEvents = require("./system/events");
 const { filter, pipe, stripListeners } = require("./event/utils");
 const { getNodeView, getActiveView } = require("./view/core");
 const { isNil, isObject, isNumber } = require("./lang/type");
 const { getAttachEventType } = require("./content/utils");
 const { number, boolean, object } = require('./deprecated/api-utils');
+const { Style } = require("./stylesheet/style");
+const { attach, detach } = require("./content/mod");
 
 let isRect = ({top, right, bottom, left}) => [top, right, bottom, left].
   some(value => isNumber(value) && !isNaN(value));
 
 let isSDKObj = obj => obj instanceof Class;
 
 let rectContract = contract({
   top: number,
@@ -58,31 +60,41 @@ let position = {
 
 let displayContract = contract({
   width: number,
   height: number,
   focus: boolean,
   position: position
 });
 
-let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
+let panelContract = contract(merge({
+  // contentStyle* / contentScript* are sharing the same validation constraints,
+  // so they can be mostly reused, except for the messages.
+  contentStyle: merge(Object.create(loaderContract.rules.contentScript), {
+    msg: 'The `contentStyle` option must be a string or an array of strings.'
+  }),
+  contentStyleFile: merge(Object.create(loaderContract.rules.contentScriptFile), {
+    msg: 'The `contentStyleFile` option must be a local URL or an array of URLs'
+  })
+}, displayContract.rules, loaderContract.rules));
 
 
 function isDisposed(panel) !views.has(panel);
 
 let panels = new WeakMap();
 let models = new WeakMap();
 let views = new WeakMap();
 let workers = new WeakMap();
+let styles = new WeakMap();
 
-function viewFor(panel) views.get(panel)
-function modelFor(panel) models.get(panel)
-function panelFor(view) panels.get(view)
-function workerFor(panel) workers.get(panel)
-
+const viewFor = (panel) => views.get(panel);
+const modelFor = (panel) => models.get(panel);
+const panelFor = (view) => panels.get(view);
+const workerFor = (panel) => workers.get(panel);
+const styleFor = (panel) => styles.get(panel);
 
 // Utility function takes `panel` instance and makes sure it will be
 // automatically hidden as soon as other panel is shown.
 let setupAutoHide = new function() {
   let refs = new WeakMap();
 
   return function setupAutoHide(panel) {
     // Create system event listener that reacts to any panel showing and
@@ -120,16 +132,22 @@ const Panel = Class({
     let model = merge({
       defaultWidth: 320,
       defaultHeight: 240,
       focus: true,
       position: Object.freeze({}),
     }, panelContract(options));
     models.set(this, model);
 
+    if (model.contentStyle || model.contentStyleFile) {
+      styles.set(this, Style({
+        uri: model.contentStyleFile,
+        source: model.contentStyle
+      }));
+    }
 
     // Setup view
     let view = domPanel.make();
     panels.set(view, this);
     views.set(this, view);
 
     // Load panel content.
     domPanel.setURL(view, model.contentURL);
@@ -143,17 +161,18 @@ const Panel = Class({
 
     // pipe events from worker to a panel.
     pipe(worker, this);
   },
   dispose: function dispose() {
     this.hide();
     off(this);
 
-    destroy(workerFor(this));
+    workerFor(this).destroy();
+    detach(styleFor(this));
 
     domPanel.dispose(viewFor(this));
 
     // Release circular reference between view and panel instance. This
     // way view will be GC-ed. And panel as well once all the other refs
     // will be removed from it.
     views.delete(this);
   },
@@ -172,17 +191,17 @@ const Panel = Class({
 
   get contentURL() modelFor(this).contentURL,
   set contentURL(value) {
     let model = modelFor(this);
     model.contentURL = panelContract({ contentURL: value }).contentURL;
     domPanel.setURL(viewFor(this), model.contentURL);
     // Detach worker so that messages send will be queued until it's
     // reatached once panel content is ready.
-    detach(workerFor(this));
+    workerFor(this).detach();
   },
 
   /* Public API: Panel.isShowing */
   get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
 
   /* Public API: Panel.show */
   show: function show(options={}, anchor) {
     if (options instanceof Ci.nsIDOMElement) {
@@ -257,17 +276,30 @@ let hides = filter(panelEvents, ({type})
 
 // Panel events emitted after content inside panel is ready. For different
 // panels ready may mean different state based on `contentScriptWhen` attribute.
 // Weather given event represents readyness is detected by `getAttachEventType`
 // helper function.
 let ready = filter(panelEvents, ({type, target}) =>
   getAttachEventType(modelFor(panelFor(target))) === type);
 
+// Styles should be always added as soon as possible, and doesn't makes them
+// depends on `contentScriptWhen`
+let start = filter(panelEvents, ({type}) => type === "document-element-inserted");
+
 // Forward panel show / hide events to panel's own event listeners.
 on(shows, "data", ({target}) => emit(panelFor(target), "show"));
 
 on(hides, "data", ({target}) => emit(panelFor(target), "hide"));
 
-on(ready, "data", function({target}) {
-  let worker = workerFor(panelFor(target));
-  attach(worker, domPanel.getContentDocument(target).defaultView);
+on(ready, "data", ({target}) => {
+  let panel = panelFor(target);
+  let window = domPanel.getContentDocument(target).defaultView;
+  
+  workerFor(panel).attach(window);
 });
+
+on(start, "data", ({target}) => {
+  let panel = panelFor(target);
+  let window = domPanel.getContentDocument(target).defaultView;
+  
+  attach(styleFor(panel), window);
+});
--- a/addon-sdk/source/lib/sdk/system.js
+++ b/addon-sdk/source/lib/sdk/system.js
@@ -104,27 +104,31 @@ exports.pathFor = function pathFor(id) {
 
 /**
  * What platform you're running on (all lower case string).
  * For possible values see:
  * https://developer.mozilla.org/en/OS_TARGET
  */
 exports.platform = runtime.OS.toLowerCase();
 
+const [, architecture, compiler] = runtime.XPCOMABI ? 
+                                   runtime.XPCOMABI.match(/^([^-]*)-(.*)$/) :
+                                   [, null, null];
+
 /**
  * What processor architecture you're running on:
  * `'arm', 'ia32', or 'x64'`.
  */
-exports.architecture = runtime.XPCOMABI.split('_')[0];
+exports.architecture = architecture;
 
 /**
  * What compiler used for build:
  * `'msvc', 'n32', 'gcc2', 'gcc3', 'sunc', 'ibmc'...`
  */
-exports.compiler = runtime.XPCOMABI.split('_')[1];
+exports.compiler = compiler;
 
 /**
  * The application's build ID/date, for example "2004051604".
  */
 exports.build = appInfo.appBuildID;
 
 /**
  * The XUL application's UUID.
--- a/addon-sdk/source/lib/sdk/system/events.js
+++ b/addon-sdk/source/lib/sdk/system/events.js
@@ -3,22 +3,23 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 'use strict';
 
 module.metadata = {
   'stability': 'unstable'
 };
 
-const { Cc, Ci } = require('chrome');
+const { Cc, Ci, Cu } = require('chrome');
 const { Unknown } = require('../platform/xpcom');
 const { Class } = require('../core/heritage');
 const { ns } = require('../core/namespace');
 const { addObserver, removeObserver, notifyObservers } = 
   Cc['@mozilla.org/observer-service;1'].getService(Ci.nsIObserverService);
+const unloadSubject = require('@loader/unload');
 
 const Subject = Class({
   extends: Unknown,
   initialize: function initialize(object) {
     // Double-wrap the object and set a property identifying the
     // wrappedJSObject as one of our wrappers to distinguish between
     // subjects that are one of our wrappers (which we should unwrap
     // when notifying our observers) and those that are real JS XPCOM
@@ -89,16 +90,20 @@ function on(type, listener, strong) {
   // Take list of observers associated with given `listener` function.
   let observers = subscribers(listener);
   // If `observer` for the given `type` is not registered yet, then
   // associate an `observer` and register it.
   if (!(type in observers)) {
     let observer = Observer(listener);
     observers[type] = observer;
     addObserver(observer, type, weak);
+    // WeakRef gymnastics to remove all alive observers on unload
+    let ref = Cu.getWeakReference(observer);
+    weakRefs.set(observer, ref);
+    stillAlive.set(ref, type);
   }
 }
 exports.on = on;
 
 function once(type, listener) {
   // Note: this code assumes order in which listeners are called, which is fine
   // as long as dispatch happens in same order as listener registration which
   // is the case now. That being said we should be aware that this may break
@@ -115,11 +120,36 @@ function off(type, listener) {
   // Take list of observers as with the given `listener`.
   let observers = subscribers(listener);
   // If `observer` for the given `type` is registered, then
   // remove it & unregister.
   if (type in observers) {
     let observer = observers[type];
     delete observers[type];
     removeObserver(observer, type);
+    stillAlive.delete(weakRefs.get(observer));
   }
 }
 exports.off = off;
+
+// must use WeakMap to keep reference to all the WeakRefs (!), see bug 986115
+let weakRefs = new WeakMap();
+
+// and we're out of beta, we're releasing on time!
+let stillAlive = new Map();   
+
+on('sdk:loader:destroy', function onunload({ subject, data: reason }) {
+  // using logic from ./unload, to avoid a circular module reference
+  if (subject.wrappedJSObject === unloadSubject) {
+    off('sdk:loader:destroy', onunload);
+
+    // don't bother
+    if (reason === 'shutdown') 
+      return;
+
+    stillAlive.forEach( (type, ref) => {
+      let observer = ref.get();
+      if (observer) 
+        removeObserver(observer, type);
+    })
+  }
+  // a strong reference
+}, true);
--- a/addon-sdk/source/lib/sdk/test/loader.js
+++ b/addon-sdk/source/lib/sdk/test/loader.js
@@ -57,19 +57,21 @@ exports.LoaderWithHookedConsole = functi
     loader: CustomLoader(module, {
       console: {
         log: hook.bind("log"),
         info: hook.bind("info"),
         warn: hook.bind("warn"),
         error: hook.bind("error"),
         debug: hook.bind("debug"),
         exception: hook.bind("exception"),
+        time: hook.bind("time"),
+        timeEnd: hook.bind("timeEnd"),
         __exposedProps__: {
           log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-          exception: "rw"
+          exception: "rw", time: "rw", timeEnd: "rw"
         }
       }
     }),
     messages: messages
   };
 }
 
 // Same than LoaderWithHookedConsole with lower level, instead we get what is
@@ -100,15 +102,17 @@ exports.LoaderWithFilteredConsole = func
   return CustomLoader(module, {
     console: {
       log: hook.bind("log"),
       info: hook.bind("info"),
       warn: hook.bind("warn"),
       error: hook.bind("error"),
       debug: hook.bind("debug"),
       exception: hook.bind("exception"),
+      time: hook.bind("time"),
+      timeEnd: hook.bind("timeEnd"),
       __exposedProps__: {
         log: "rw", info: "rw", warn: "rw", error: "rw", debug: "rw",
-        exception: "rw"
+        exception: "rw", time: "rw", timeEnd: "rw"
       }
     }
   });
 }
--- a/addon-sdk/source/lib/sdk/ui/button/contract.js
+++ b/addon-sdk/source/lib/sdk/ui/button/contract.js
@@ -3,24 +3,26 @@
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 'use strict';
 
 const { contract } = require('../../util/contract');
 const { isLocalURL } = require('../../url');
 const { isNil, isObject, isString } = require('../../lang/type');
 const { required, either, string, boolean, object } = require('../../deprecated/api-utils');
 const { merge } = require('../../util/object');
+const { freeze } = Object;
 
 function isIconSet(icons) {
   return Object.keys(icons).
     every(size => String(size >>> 0) === size && isLocalURL(icons[size]))
 }
 
 let iconSet = {
   is: either(object, string),
+  map: v => isObject(v) ? freeze(merge({}, v)) : v,
   ok: v => (isString(v) && isLocalURL(v)) || (isObject(v) && isIconSet(v)),
   msg: 'The option "icon" must be a local URL or an object with ' +
     'numeric keys / local URL values pair.'
 }
 
 let id = {
   is: string,
   ok: v => /^[a-z-_][a-z0-9-_]*$/i.test(v),
--- a/addon-sdk/source/lib/sdk/ui/sidebar.js
+++ b/addon-sdk/source/lib/sdk/ui/sidebar.js
@@ -248,19 +248,21 @@ const Sidebar = Class({
   dispose: function() {
     const internals = sidebarNS(this);
 
     off(this);
 
     remove(sidebars, this);
 
     // stop tracking windows
-    internals.tracker.unload();
+    if (internals.tracker) {
+      internals.tracker.unload();
+    }
+
     internals.tracker = null;
-
     internals.windowNS = null;
 
     views.delete(this);
     models.delete(this);
   }
 });
 exports.Sidebar = Sidebar;
 
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/fixtures/css-include-file.css
@@ -0,0 +1,1 @@
+div { border: 10px solid black; }
deleted file mode 100644
--- a/addon-sdk/source/test/fixtures/pagemod-css-include-file.css
+++ /dev/null
@@ -1,1 +0,0 @@
-div { border: 10px solid black; }
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -377,38 +377,42 @@ exports["test:ensure console.xxx works i
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
     let { loader } = LoaderWithHookedConsole(module, onMessage);
 
     // Intercept all console method calls
     let calls = [];
     function onMessage(type, msg) {
       assert.equal(type, msg,
-                       "console.xxx(\"xxx\"), i.e. message is equal to the " +
-                       "console method name we are calling");
+        "console.xxx(\"xxx\"), i.e. message is equal to the " +
+        "console method name we are calling");
       calls.push(msg);
     }
 
     // Finally, create a worker that will call all console methods
     let worker =  loader.require("sdk/content/worker").Worker({
       window: browser.contentWindow,
       contentScript: "new " + function WorkerScope() {
+        console.time("time");
         console.log("log");
         console.info("info");
         console.warn("warn");
         console.error("error");
         console.debug("debug");
         console.exception("exception");
+        console.timeEnd("timeEnd");
         self.postMessage();
       },
       onMessage: function() {
         // Ensure that console methods are called in the same execution order
+        const EXPECTED_CALLS = ["time", "log", "info", "warn", "error",
+          "debug", "exception", "timeEnd"];
         assert.equal(JSON.stringify(calls),
-                         JSON.stringify(["log", "info", "warn", "error", "debug", "exception"]),
-                         "console has been called successfully, in the expected order");
+          JSON.stringify(EXPECTED_CALLS),
+          "console methods have been called successfully, in expected order");
         done();
       }
     });
   }
 );
 
 exports["test:setTimeout works with string argument"] = WorkerTest(
   "data:text/html;charset=utf-8,<script>var docVal=5;</script>",
--- a/addon-sdk/source/test/test-page-mod.js
+++ b/addon-sdk/source/test/test-page-mod.js
@@ -952,17 +952,17 @@ exports.testContentScriptOptionsOption =
   );
 };
 
 exports.testPageModCss = function(assert, done) {
   let [pageMod] = testPageMod(assert, done,
     'data:text/html;charset=utf-8,<div style="background: silver">css test</div>', [{
       include: ["*", "data:*"],
       contentStyle: "div { height: 100px; }",
-      contentStyleFile: data.url("pagemod-css-include-file.css")
+      contentStyleFile: data.url("css-include-file.css")
     }],
     function(win, done) {
       let div = win.document.querySelector("div");
       assert.equal(
         div.clientHeight,
         100,
         "PageMod contentStyle worked"
       );
@@ -1526,9 +1526,37 @@ exports.testDetachOnUnload = function(as
   });
 
   tabs.open({
     url: TEST_URL,
     onOpen: t => tab = t
   })
 }
 
+exports.testSyntaxErrorInContentScript = function(assert, done) {
+  const url = "data:text/html;charset=utf-8,testSyntaxErrorInContentScript";
+  let hitError = null;
+  let attached = false;
+
+  testPageMod(assert, done, url, [{
+      include: url,
+      contentScript: 'console.log(23',
+
+      onAttach: function() {
+        attached = true;
+      },
+
+      onError: function(e) {
+        hitError = e;
+      }
+    }],
+
+    function(win, done) {
+      assert.ok(attached, "The worker was attached.");
+      assert.notStrictEqual(hitError, null, "The syntax error was reported.");
+      if (hitError)
+        assert.equal(hitError.name, "SyntaxError", "The error thrown should be a SyntaxError");
+      done();
+    }
+  );
+};
+
 require('sdk/test').run(exports);
--- a/addon-sdk/source/test/test-panel.js
+++ b/addon-sdk/source/test/test-panel.js
@@ -20,16 +20,18 @@ const { isWindowPBSupported, isGlobalPBS
 const { defer, all } = require('sdk/core/promise');
 const { getMostRecentBrowserWindow } = require('sdk/window/utils');
 const { getWindow } = require('sdk/panel/window');
 const { pb } = require('./private-browsing/helper');
 const { URL } = require('sdk/url');
 const fixtures = require('./fixtures')
 
 const SVG_URL = fixtures.url('mofo_logo.SVG');
+const CSS_URL = fixtures.url('css-include-file.css');
+
 const Isolate = fn => '(' + fn + ')()';
 
 function ignorePassingDOMNodeWarning(type, message) {
   if (type !== 'warn' || !message.startsWith('Passing a DOM node'))
     console[type](message);
 }
 
 function makeEmptyPrivateBrowserWindow(options) {
@@ -969,16 +971,98 @@ exports['test emits on url changes'] = f
 
 exports['test panel can be constructed without any arguments'] = function (assert) {
   const { Panel } = require('sdk/panel');
 
   let panel = Panel();
   assert.ok(true, "Creating a panel with no arguments does not throw");
 };
 
+exports['test panel CSS'] = function(assert, done) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+
+  const { getActiveView } = loader.require('sdk/view/core');
+
+  const getContentWindow = panel =>
+    getActiveView(panel).querySelector('iframe').contentWindow;
+
+  let panel = Panel({
+    contentURL: 'data:text/html;charset=utf-8,' + 
+                '<div style="background: silver">css test</div>',
+    contentStyle: 'div { height: 100px; }',
+    contentStyleFile: CSS_URL,
+    onShow: () => {
+      ready(getContentWindow(panel)).then(({ document }) => {
+        let div = document.querySelector('div');
+
+        assert.equal(div.clientHeight, 100, 'Panel contentStyle worked');
+        assert.equal(div.offsetHeight, 120, 'Panel contentStyleFile worked');
+
+        loader.unload();
+        done();
+      }).then(null, assert.fail); 
+    }
+  });
+
+  panel.show();
+};
+
+exports['test panel CSS list'] = function(assert, done) {
+  const loader = Loader(module);
+  const { Panel } = loader.require('sdk/panel');
+
+  const { getActiveView } = loader.require('sdk/view/core');
+
+  const getContentWindow = panel =>
+    getActiveView(panel).querySelector('iframe').contentWindow;
+
+  let panel = Panel({
+    contentURL: 'data:text/html;charset=utf-8,' + 
+                '<div style="width:320px; max-width: 480px!important">css test</div>',
+    contentStyleFile: [
+      // Highlight evaluation order in this list
+      "data:text/css;charset=utf-8,div { border: 1px solid black; }",
+      "data:text/css;charset=utf-8,div { border: 10px solid black; }",
+      // Highlight evaluation order between contentStylesheet & contentStylesheetFile
+      "data:text/css;charset=utf-8s,div { height: 1000px; }",
+      // Highlight precedence between the author and user style sheet
+      "data:text/css;charset=utf-8,div { width: 200px; max-width: 640px!important}",
+    ],
+    contentStyle: [
+      "div { height: 10px; }",
+      "div { height: 100px; }"
+    ],
+    onShow: () => {
+      ready(getContentWindow(panel)).then(({ window, document }) => {
+        let div = document.querySelector('div');
+        let style = window.getComputedStyle(div);
+
+        assert.equal(div.clientHeight, 100,
+          'Panel contentStyle list is evaluated after contentStyleFile');
+
+        assert.equal(div.offsetHeight, 120,
+          'Panel contentStyleFile list works');
+
+        assert.equal(style.width, '320px',
+          'add-on author/page author stylesheet precedence works');
+
+        assert.equal(style.maxWidth, '480px',
+          'add-on author/page author stylesheet !important precedence works');
+
+        loader.unload();
+        done();
+      }).then(null, assert.fail); 
+    }
+  });
+
+  panel.show();
+};
+
+
 if (isWindowPBSupported) {
   exports.testGetWindow = function(assert, done) {
     let activeWindow = getMostRecentBrowserWindow();
     open(null, { features: {
       toolbar: true,
       chrome: true,
       private: true
     } }).then(function(window) {
--- a/addon-sdk/source/test/test-sandbox.js
+++ b/addon-sdk/source/test/test-sandbox.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const { sandbox, load, evaluate } = require('sdk/loader/sandbox');
+const { sandbox, load, evaluate, nuke } = require('sdk/loader/sandbox');
 const xulApp = require("sdk/system/xul-app");
 const fixturesURI = module.uri.split('test-sandbox.js')[0] + 'fixtures/';
 
 // The following adds Debugger constructor to the global namespace.
 const { Cu } = require('chrome');
 const { addDebuggerToGlobal } =
   Cu.import('resource://gre/modules/jsdebugger.jsm', {});
 addDebuggerToGlobal(this);
@@ -132,9 +132,35 @@ exports['test metadata']  = function(ass
 
     dbg.onNewGlobalObject = undefined;
   }
 
   let fixture = sandbox();
   let self = require('sdk/self');
 }
 
+exports['test nuke sandbox'] = function(assert) {
+
+  let fixture = sandbox('http://example.com');
+  fixture.foo = 'foo';
+
+  let ref = evaluate(fixture, 'let a = {bar: "bar"}; a');
+
+  nuke(fixture);
+
+  assert.ok(Cu.isDeadWrapper(fixture), 'sandbox should be dead');
+
+  assert.throws(
+    () => fixture.foo,
+    /can't access dead object/,
+    'property of nuked sandbox should not be accessible'
+  );
+
+  assert.ok(Cu.isDeadWrapper(ref), 'ref to object from sandbox should be dead');
+
+  assert.throws(
+    () => ref.bar,
+    /can't access dead object/,
+    'object from nuked sandbox should not be alive'
+  );
+}
+
 require('test').run(exports);
--- a/addon-sdk/source/test/test-system-events.js
+++ b/addon-sdk/source/test/test-system-events.js
@@ -114,16 +114,40 @@ exports["test listeners are GC-ed"] = fu
     assert.equal(receivedFromWeak.length, 1, "weak listener was GC-ed");
     assert.equal(receivedFromStrong.length, 2, "strong listener was invoked");
 
     loader.unload();
     done();
   });
 };
 
+exports["test alive listeners are removed on unload"] = function(assert) {
+  let receivedFromWeak = [];
+  let receivedFromStrong = [];
+  let loader = Loader(module);
+  let events = loader.require('sdk/system/events');
+
+  let type = 'test-alive-listeners-are-removed';
+  const handler = (event) => receivedFromStrong.push(event);
+  const weakHandler = (event) => receivedFromWeak.push(event); 
+
+  events.on(type, handler, true);
+  events.on(type, weakHandler);
+
+  events.emit(type, { data: 1 });
+  assert.equal(receivedFromStrong.length, 1, "strong listener invoked");
+  assert.equal(receivedFromWeak.length, 1, "weak listener invoked");
+
+  loader.unload();
+  events.emit(type, { data: 2 });
+
+  assert.equal(receivedFromWeak.length, 1, "weak listener was removed");
+  assert.equal(receivedFromStrong.length, 1, "strong listener was removed");
+};
+
 exports["test handle nsIObserverService notifications"] = function(assert) {
   let ios = Cc['@mozilla.org/network/io-service;1']
             .getService(Ci.nsIIOService);
 
   let uri = ios.newURI("http://www.foo.com", null, null);
 
   let type = Date.now().toString(32);
   let timesCalled = 0;
--- a/addon-sdk/source/test/test-system-runtime.js
+++ b/addon-sdk/source/test/test-system-runtime.js
@@ -1,23 +1,23 @@
 /* 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";
 
-var runtime = require("sdk/system/runtime");
+const runtime = require("sdk/system/runtime");
 
 exports["test system runtime"] = function(assert) {
   assert.equal(typeof(runtime.inSafeMode), "boolean",
                "inSafeMode is boolean");
   assert.equal(typeof(runtime.OS), "string",
                "runtime.OS is string");
   assert.equal(typeof(runtime.processType), "number",
                "runtime.processType is a number");
   assert.equal(typeof(runtime.widgetToolkit), "string",
                "runtime.widgetToolkit is string");
-  var XPCOMABI = typeof(runtime.XPCOMABI);
+  const XPCOMABI = runtime.XPCOMABI;
   assert.ok(XPCOMABI === null || typeof(XPCOMABI) === "string",
             "runtime.XPCOMABI is string or null if not supported by platform");
 };
 
 
 require("test").run(exports);
new file mode 100644
--- /dev/null
+++ b/addon-sdk/source/test/test-system.js
@@ -0,0 +1,37 @@
+/* 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 runtime = require("sdk/system/runtime");
+const system = require("sdk/system");
+
+exports["test system architecture and compiler"] = function(assert) {
+
+  if (system.architecture !== null) {
+    assert.equal(
+      runtime.XPCOMABI.indexOf(system.architecture), 0,
+      "system.architecture is starting substring of runtime.XPCOMABI"
+    );
+  }
+
+  if (system.compiler !== null) {
+    assert.equal(
+      runtime.XPCOMABI.indexOf(system.compiler),
+      runtime.XPCOMABI.length - system.compiler.length,
+      "system.compiler is trailing substring of runtime.XPCOMABI"
+    );
+  }
+
+  assert.ok(
+    system.architecture === null || typeof(system.architecture) === "string",
+    "system.architecture is string or null if not supported by platform"
+  );
+
+  assert.ok(
+    system.compiler === null || typeof(system.compiler) === "string",
+    "system.compiler is string or null if not supported by platform"
+  );
+};
+
+require("test").run(exports);
--- a/addon-sdk/source/test/test-ui-action-button.js
+++ b/addon-sdk/source/test/test-ui-action-button.js
@@ -830,16 +830,54 @@ exports['test button state are snapshot'
     'window state is not the same object of previous window state');
 
   assert.notEqual(button.state(tabs.activeTab), tabState,
     'tab state is not the same object of previous tab state');
 
   loader.unload();
 }
 
+exports['test button icon object is a snapshot'] = function(assert) {
+  let loader = Loader(module);
+  let { ActionButton } = loader.require('sdk/ui');
+
+  let icon = {
+    '16': './foo.png'
+  };
+
+  let button = ActionButton({
+    id: 'my-button-17',
+    label: 'my button',
+    icon: icon
+  });
+
+  assert.deepEqual(button.icon, icon,
+    'button.icon has the same properties of the object set in the constructor');
+
+  assert.notEqual(button.icon, icon,
+    'button.icon is not the same object of the object set in the constructor');
+
+  assert.throws(
+    () => button.icon[16] = './bar.png',
+    /16 is read-only/,
+    'properties of button.icon are ready-only'
+  );
+
+  let newIcon = {'16': './bar.png'};
+  button.icon = newIcon;
+
+  assert.deepEqual(button.icon, newIcon,
+    'button.icon has the same properties of the object set');
+
+  assert.notEqual(button.icon, newIcon,
+    'button.icon is not the same object of the object set');
+
+  loader.unload();
+}
+
 exports['test button after destroy'] = function(assert) {
   let loader = Loader(module);
   let { ActionButton } = loader.require('sdk/ui');
   let { browserWindows } = loader.require('sdk/windows');
   let { activeTab } = loader.require('sdk/tabs');
 
   let button = ActionButton({
     id: 'my-button-15',
--- a/addon-sdk/source/test/test-ui-toggle-button.js
+++ b/addon-sdk/source/test/test-ui-toggle-button.js
@@ -839,16 +839,54 @@ exports['test button state are snapshot'
     'window state is not the same object of previous window state');
 
   assert.notEqual(button.state(tabs.activeTab), tabState,
     'tab state is not the same object of previous tab state');
 
   loader.unload();
 }
 
+exports['test button icon object is a snapshot'] = function(assert) {
+  let loader = Loader(module);
+  let { ToggleButton } = loader.require('sdk/ui');
+
+  let icon = {
+    '16': './foo.png'
+  };
+
+  let button = ToggleButton({
+    id: 'my-button-17',
+    label: 'my button',
+    icon: icon
+  });
+
+  assert.deepEqual(button.icon, icon,
+    'button.icon has the same properties of the object set in the constructor');
+
+  assert.notEqual(button.icon, icon,
+    'button.icon is not the same object of the object set in the constructor');
+
+  assert.throws(
+    () => button.icon[16] = './bar.png',
+    /16 is read-only/,
+    'properties of button.icon are ready-only'
+  );
+
+  let newIcon = {'16': './bar.png'};
+  button.icon = newIcon;
+
+  assert.deepEqual(button.icon, newIcon,
+    'button.icon has the same properties of the object set');
+
+  assert.notEqual(button.icon, newIcon,
+    'button.icon is not the same object of the object set');
+
+  loader.unload();
+}
+
 exports['test button after destroy'] = function(assert) {
   let loader = Loader(module);
   let { ToggleButton } = loader.require('sdk/ui');
   let { browserWindows } = loader.require('sdk/windows');
   let { activeTab } = loader.require('sdk/tabs');
 
   let button = ToggleButton({
     id: 'my-button-15',
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -452,16 +452,18 @@
 @BINPATH@/components/txEXSLTRegExFunctions.manifest
 @BINPATH@/components/txEXSLTRegExFunctions.js
 @BINPATH@/components/toolkitplaces.manifest
 @BINPATH@/components/nsLivemarkService.js
 @BINPATH@/components/nsTaggingService.js
 @BINPATH@/components/nsPlacesDBFlush.js
 @BINPATH@/components/nsPlacesAutoComplete.manifest
 @BINPATH@/components/nsPlacesAutoComplete.js
+@BINPATH@/components/UnifiedComplete.manifest
+@BINPATH@/components/UnifiedComplete.js
 @BINPATH@/components/nsPlacesExpiration.js
 @BINPATH@/components/PlacesProtocolHandler.js
 @BINPATH@/components/PlacesCategoriesStarter.js
 @BINPATH@/components/nsDefaultCLH.manifest
 @BINPATH@/components/nsDefaultCLH.js
 @BINPATH@/components/nsContentPrefService.manifest
 @BINPATH@/components/nsContentPrefService.js
 @BINPATH@/components/nsContentDispatchChooser.manifest
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -117,34 +117,46 @@ tabbrowser {
 }
 
 .tabbrowser-tab:not([pinned]):not([fadein]) {
   max-width: 0.1px;
   min-width: 0.1px;
   visibility: hidden;
 }
 
-.tab-close-button,
 .tab-background {
   /* Explicitly set the visibility to override the value (collapsed)
    * we inherit from #TabsToolbar[collapsed] upon opening a browser window. */
   visibility: visible;
   /* The transition is only delayed for opening tabs. */
   transition: visibility 0ms 25ms;
 }
 
-.tab-close-button:not([fadein]):not([pinned]),
 .tab-background:not([fadein]):not([pinned]) {
   visibility: hidden;
   /* Closing tabs are hidden without a delay. */
   transition-delay: 0ms;
 }
 
+.tab-close-button,
+.tab-label {
+  /* Explicitly set the visibility to override the value (collapsed)
+   * we inherit from #TabsToolbar[collapsed] upon opening a browser window. */
+  visibility: visible;
+  transition: opacity 70ms 230ms,
+              visibility 0ms 230ms;
+}
+
+.tab-close-button:not([fadein]):not([pinned]),
+.tab-label:not([fadein]):not([pinned]) {
+  visibility: collapse;
+  opacity: .6;
+}
+
 .tab-throbber:not([fadein]):not([pinned]),
-.tab-label:not([fadein]):not([pinned]),
 .tab-icon-image:not([fadein]):not([pinned]) {
   display: none;
 }
 
 .tabbrowser-tabs[positionpinnedtabs] > .tabbrowser-tab[pinned] {
   position: fixed !important;
   display: block; /* position:fixed already does this (bug 579776), but let's be explicit */
 }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -76,16 +76,20 @@
       <field name="mFaviconService" readonly="true">
         Components.classes["@mozilla.org/browser/favicon-service;1"]
                   .getService(Components.interfaces.nsIFaviconService);
       </field>
       <field name="_placesAutocomplete" readonly="true">
          Components.classes["@mozilla.org/autocomplete/search;1?name=history"]
                    .getService(Components.interfaces.mozIPlacesAutoComplete);
       </field>
+      <field name="_unifiedComplete" readonly="true">
+         Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+                   .getService(Components.interfaces.mozIPlacesAutoComplete);
+      </field>
       <field name="mTabBox" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "tabbox");
       </field>
       <field name="mPanelContainer" readonly="true">
         document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer");
       </field>
       <field name="mStringBundle">
         document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle");
@@ -739,26 +743,29 @@
                 // Don't clear the favicon if this onLocationChange was
                 // triggered by a pushState or a replaceState.  See bug 550565.
                 if (aWebProgress.isLoadingDocument &&
                     !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
                   this.mBrowser.mIconURL = null;
                 }
 
                 let autocomplete = this.mTabBrowser._placesAutocomplete;
+                let unifiedComplete = this.mTabBrowser._unifiedComplete;
                 if (this.mBrowser.registeredOpenURI) {
                   autocomplete.unregisterOpenPage(this.mBrowser.registeredOpenURI);
+                  unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI);
                   delete this.mBrowser.registeredOpenURI;
                 }
                 // Tabs in private windows aren't registered as "Open" so
                 // that they don't appear as switch-to-tab candidates.
                 if (!isBlankPageURL(aLocation.spec) &&
                     (!PrivateBrowsingUtils.isWindowPrivate(window) ||
                     PrivateBrowsingUtils.permanentPrivateBrowsing)) {
                   autocomplete.registerOpenPage(aLocation);
+                  unifiedComplete.registerOpenPage(aLocation);
                   this.mBrowser.registeredOpenURI = aLocation;
                 }
               }
 
               if (!this.mBlank) {
                 this._callProgressListeners("onLocationChange",
                                             [aWebProgress, aRequest, aLocation,
                                              aFlags]);
@@ -1667,22 +1674,16 @@
             }
 
             if (animate) {
               mozRequestAnimationFrame(function () {
                 this.tabContainer._handleTabTelemetryStart(t, aURI);
 
                 // kick the animation off
                 t.setAttribute("fadein", "true");
-
-                // This call to adjustTabstrip is redundant but needed so that
-                // when opening a second tab, the first tab's close buttons
-                // appears immediately rather than when the transition ends.
-                if (this.tabs.length - this._removingTabs.length == 2)
-                  this.tabContainer.adjustTabstrip();
               }.bind(this));
             }
 
             return t;
           ]]>
         </body>
       </method>
 
@@ -1858,23 +1859,16 @@
             }
 
             this.tabContainer._handleTabTelemetryStart(aTab);
 
             this._blurTab(aTab);
             aTab.style.maxWidth = ""; // ensure that fade-out transition happens
             aTab.removeAttribute("fadein");
 
-            if (this.tabs.length - this._removingTabs.length == 1) {
-              // The second tab just got closed and we will end up with a single
-              // one. Remove the first tab's close button immediately (if needed)
-              // rather than after the tabclose animation ends.
-              this.tabContainer.adjustTabstrip();
-            }
-
             setTimeout(function (tab, tabbrowser) {
               if (tab.parentNode &&
                   window.getComputedStyle(tab).maxWidth == "0.1px") {
                 NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
                 tabbrowser._endRemoveTab(tab);
               }
             }, 3000, aTab, this);
           ]]>
@@ -1964,16 +1958,17 @@
 
             browser.webProgress.removeProgressListener(filter);
 
             filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
             this.mTabListeners[aTab._tPos].destroy();
 
             if (browser.registeredOpenURI && !aTabWillBeMoved) {
               this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
+              this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI);
               delete browser.registeredOpenURI;
             }
 
             // We are no longer the primary content area.
             browser.setAttribute("type", "content-targetable");
 
             // Remove this tab as the owner of any other tabs, since it's going away.
             Array.forEach(this.tabs, function (tab) {
@@ -2290,16 +2285,17 @@
       <method name="_swapRegisteredOpenURIs">
         <parameter name="aOurBrowser"/>
         <parameter name="aOtherBrowser"/>
         <body>
           <![CDATA[
             // If the current URI is registered as open remove it from the list.
             if (aOurBrowser.registeredOpenURI) {
               this._placesAutocomplete.unregisterOpenPage(aOurBrowser.registeredOpenURI);
+              this._unifiedComplete.unregisterOpenPage(aOurBrowser.registeredOpenURI);
               delete aOurBrowser.registeredOpenURI;
             }
 
             // If the other/new URI is registered as open then copy it over.
             if (aOtherBrowser.registeredOpenURI) {
               aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
               delete aOtherBrowser.registeredOpenURI;
             }
@@ -3099,16 +3095,17 @@
       </method>
 
       <destructor>
         <![CDATA[
           for (var i = 0; i < this.mTabListeners.length; ++i) {
             let browser = this.getBrowserAtIndex(i);
             if (browser.registeredOpenURI) {
               this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
+              this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI);
               delete browser.registeredOpenURI;
             }
             browser.webProgress.removeProgressListener(this.mTabFilters[i]);
             this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
             this.mTabFilters[i] = null;
             this.mTabListeners[i].destroy();
             this.mTabListeners[i] = null;
           }
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -63,16 +63,22 @@
 
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("mousedown", this, false);
         this.inputField.addEventListener("mousemove", this, false);
         this.inputField.addEventListener("mouseout", this, false);
         this.inputField.addEventListener("overflow", this, false);
         this.inputField.addEventListener("underflow", this, false);
 
+        try {
+          if (this._prefs.getBoolPref("unifiedcomplete")) {
+            this.setAttribute("autocompletesearch", "unifiedcomplete");
+          }
+        } catch (ex) {}
+
         const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
         var textBox = document.getAnonymousElementByAttribute(this,
                                                 "anonid", "textbox-input-box");
         var cxmenu = document.getAnonymousElementByAttribute(textBox,
                                             "anonid", "input-box-contextmenu");
         var pasteAndGo;
         cxmenu.addEventListener("popupshowing", function() {
           if (!pasteAndGo)
@@ -594,16 +600,24 @@
                 this.timeout = this._prefs.getIntPref(aData);
                 break;
               case "formatting.enabled":
                 this._formattingEnabled = this._prefs.getBoolPref(aData);
                 break;
               case "trimURLs":
                 this._mayTrimURLs = this._prefs.getBoolPref(aData);
                 break;
+              case "unifiedcomplete":
+                let useUnifiedComplete = false;
+                try {
+                  useUnifiedComplete = this._prefs.getBoolPref(aData);
+                } catch (ex) {}
+                this.setAttribute("autocompletesearch",
+                                  useUnifiedComplete ? "unifiedcomplete"
+                                                     : "urlinline history");
             }
           }
         ]]></body>
       </method>
 
       <method name="handleEvent">
         <parameter name="aEvent"/>
         <body><![CDATA[
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -381,16 +381,23 @@ let CustomizableUIInternal = {
       if (aDestroyPlacements) {
         gPlacements.delete(aName);
       } else {
         // Otherwise we need to re-set them, as removeFromArea will have emptied
         // them out:
         gPlacements.set(aName, placements);
       }
       gFuturePlacements.delete(aName);
+      let existingAreaNodes = gBuildAreas.get(aName);
+      if (existingAreaNodes) {
+        for (let areaNode of existingAreaNodes) {
+          this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget,
+                               CustomizableUI.REASON_AREA_UNREGISTERED);
+        }
+      }
       gBuildAreas.delete(aName);
     } finally {
       this.endBatchUpdate(true);
     }
   },
 
   registerToolbarNode: function(aToolbar, aExistingChildren) {
     let area = aToolbar.id;
@@ -453,16 +460,17 @@ let CustomizableUIInternal = {
       // 2) The number of children of the toolbar does not match the length of
       //    the placements array for that area.
       //
       // This notion of being "dirty" is stored in a cache which is persisted
       // in the saved state.
       if (gDirtyAreaCache.has(area)) {
         this.buildArea(area, placements, aToolbar);
       }
+      this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
       aToolbar.setAttribute("currentset", placements.join(","));
     } finally {
       this.endBatchUpdate();
     }
   },
 
   buildArea: function(aArea, aPlacements, aAreaNode) {
     let document = aAreaNode.ownerDocument;
@@ -694,16 +702,18 @@ let CustomizableUIInternal = {
 
     aPanelContents.toolbox = document.getElementById("navigator-toolbox");
     aPanelContents.customizationTarget = aPanelContents;
 
     this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
 
     let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
     this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
+    this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents);
+
     for (let child of aPanelContents.children) {
       if (child.localName != "toolbarbutton") {
         if (child.localName == "toolbaritem") {
           this.ensureButtonContextMenu(child, aPanelContents);
         }
         continue;
       }
       this.ensureButtonContextMenu(child, aPanelContents);
@@ -834,16 +844,18 @@ let CustomizableUIInternal = {
     gBuildWindows.delete(aWindow);
     gSingleWrapperCache.delete(aWindow);
     let document = aWindow.document;
 
     for (let [areaId, areaNodes] of gBuildAreas) {
       let areaProperties = gAreas.get(areaId);
       for (let node of areaNodes) {
         if (node.ownerDocument == document) {
+          this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget,
+                               CustomizableUI.REASON_WINDOW_CLOSED);
           if (areaProperties.has("overflowable")) {
             node.overflowable.uninit();
             node.overflowable = null;
           }
           areaNodes.delete(node);
         }
       }
     }
@@ -2498,16 +2510,27 @@ this.CustomizableUI = {
    */
   get WIDE_PANEL_CLASS() "panel-wide-item",
   /**
    * The (constant) number of columns in the menu panel.
    */
   get PANEL_COLUMN_COUNT() 3,
 
   /**
+   * Constant indicating the reason the event was fired was a window closing
+   */
+  get REASON_WINDOW_CLOSED() "window-closed",
+  /**
+   * Constant indicating the reason the event was fired was an area being
+   * unregistered separately from window closing mechanics.
+   */
+  get REASON_AREA_UNREGISTERED() "area-unregistered",
+
+
+  /**
    * An iteratable property of windows managed by CustomizableUI.
    * Note that this can *only* be used as an iterator. ie:
    *     for (let window of CustomizableUI.windows) { ... }
    */
   windows: {
     "@@iterator": function*() {
       for (let [window,] of gBuildWindows)
         yield window;
@@ -2598,16 +2621,25 @@ this.CustomizableUI = {
    *     Fired when a widget's DOM node is *not* overflowing its container, a
    *     toolbar, anymore.
    *   - onWindowOpened(aWindow)
    *     Fired when a window has been opened that is managed by CustomizableUI,
    *     once all of the prerequisite setup has been done.
    *   - onWindowClosed(aWindow)
    *     Fired when a window that has been managed by CustomizableUI has been
    *     closed.
+   *   - onAreaNodeRegistered(aArea, aContainer)
+   *     Fired after an area node is first built when it is registered. This
+   *     is often when the window has opened, but in the case of add-ons,
+   *     could fire when the node has just been registered with CustomizableUI
+   *     after an add-on update or disable/enable sequence.
+   *   - onAreaNodeUnregistered(aArea, aContainer, aReason)
+   *     Fired when an area node is explicitly unregistered by an API caller,
+   *     or by a window closing. The aReason parameter indicates which of
+   *     these is the case.
    */
   addListener: function(aListener) {
     CustomizableUIInternal.addListener(aListener);
   },
   /**
    * Remove a listener added with addListener
    * @param aListener the listener object to remove
    */
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -231,21 +231,17 @@ CustomizeMode.prototype = {
         mainView.removeAttribute("context");
       }
 
       this._showPanelCustomizationPlaceholders();
 
       yield this._wrapToolbarItems();
       this.populatePalette();
 
-      this.visiblePalette.addEventListener("dragstart", this, true);
-      this.visiblePalette.addEventListener("dragover", this, true);
-      this.visiblePalette.addEventListener("dragexit", this, true);
-      this.visiblePalette.addEventListener("drop", this, true);
-      this.visiblePalette.addEventListener("dragend", this, true);
+      this._addDragHandlers(this.visiblePalette);
 
       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
 
       document.getElementById("PanelUI-help").setAttribute("disabled", true);
       document.getElementById("PanelUI-quit").setAttribute("disabled", true);
 
       this._updateResetButton();
       this._updateUndoResetButton();
@@ -397,21 +393,17 @@ CustomizeMode.prototype = {
       }
       browser.parentNode.selectedPanel = browser;
       let customizer = document.getElementById("customization-container");
       customizer.hidden = true;
 
       window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
 
       DragPositionManager.stop();
-      this.visiblePalette.removeEventListener("dragstart", this, true);
-      this.visiblePalette.removeEventListener("dragover", this, true);
-      this.visiblePalette.removeEventListener("dragexit", this, true);
-      this.visiblePalette.removeEventListener("drop", this, true);
-      this.visiblePalette.removeEventListener("dragend", this, true);
+      this._removeDragHandlers(this.visiblePalette);
 
       yield this._unwrapToolbarItems();
 
       if (this._changed) {
         // XXXmconley: At first, it seems strange to also persist the old way with
         //             currentset - but this might actually be useful for switching
         //             to old builds. We might want to keep this around for a little
         //             bit.
@@ -895,60 +887,68 @@ CustomizeMode.prototype = {
 
   _wrapToolbarItems: function() {
     let window = this.window;
     // Add drag-and-drop event handlers to all of the customizable areas.
     return Task.spawn(function() {
       this.areas = [];
       for (let area of CustomizableUI.areas) {
         let target = CustomizableUI.getCustomizeTargetForArea(area, window);
-        target.addEventListener("dragstart", this, true);
-        target.addEventListener("dragover", this, true);
-        target.addEventListener("dragexit", this, true);
-        target.addEventListener("drop", this, true);
-        target.addEventListener("dragend", this, true);
+        this._addDragHandlers(target);
         for (let child of target.children) {
           if (this.isCustomizableItem(child)) {
             yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
           }
         }
         this.areas.push(target);
       }
     }.bind(this)).then(null, ERROR);
   },
 
+  _addDragHandlers: function(aTarget) {
+    aTarget.addEventListener("dragstart", this, true);
+    aTarget.addEventListener("dragover", this, true);
+    aTarget.addEventListener("dragexit", this, true);
+    aTarget.addEventListener("drop", this, true);
+    aTarget.addEventListener("dragend", this, true);
+  },
+
   _wrapItemsInArea: function(target) {
     for (let child of target.children) {
       if (this.isCustomizableItem(child)) {
         this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
       }
     }
   },
 
+  _removeDragHandlers: function(aTarget) {
+    aTarget.removeEventListener("dragstart", this, true);
+    aTarget.removeEventListener("dragover", this, true);
+    aTarget.removeEventListener("dragexit", this, true);
+    aTarget.removeEventListener("drop", this, true);
+    aTarget.removeEventListener("dragend", this, true);
+  },
+
   _unwrapItemsInArea: function(target) {
     for (let toolbarItem of target.children) {
       if (this.isWrappedToolbarItem(toolbarItem)) {
         this.unwrapToolbarItem(toolbarItem);
       }
     }
   },
 
   _unwrapToolbarItems: function() {
     return Task.spawn(function() {
       for (let target of this.areas) {
         for (let toolbarItem of target.children) {
           if (this.isWrappedToolbarItem(toolbarItem)) {
             yield this.deferredUnwrapToolbarItem(toolbarItem);
           }
         }
-        target.removeEventListener("dragstart", this, true);
-        target.removeEventListener("dragover", this, true);
-        target.removeEventListener("dragexit", this, true);
-        target.removeEventListener("drop", this, true);
-        target.removeEventListener("dragend", this, true);
+        this._removeDragHandlers(target);
       }
     }.bind(this)).then(null, ERROR);
   },
 
   _removeExtraToolbarsIfEmpty: function() {
     let toolbox = this.window.gNavToolbox;
     for (let child of toolbox.children) {
       if (child.hasAttribute("customindex")) {
@@ -1113,16 +1113,35 @@ CustomizeMode.prototype = {
         this.wrapToolbarItem(widgetNode, "palette");
       } else {
         let widget = CustomizableUI.getWidget(aWidgetId);
         this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
       }
     }
   },
 
+  onAreaNodeRegistered: function(aArea, aContainer) {
+    if (aContainer.ownerDocument == this.document) {
+      this._wrapItemsInArea(aContainer);
+      this._addDragHandlers(aContainer);
+      DragPositionManager.add(this.window, aArea, aContainer);
+      this.areas.push(aContainer);
+    }
+  },
+
+  onAreaNodeUnregistered: function(aArea, aContainer, aReason) {
+    if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
+      this._unwrapItemsInArea(aContainer);
+      this._removeDragHandlers(aContainer);
+      DragPositionManager.remove(this.window, aArea, aContainer);
+      let index = this.areas.indexOf(aContainer);
+      this.areas.splice(index, 1);
+    }
+  },
+
   _onUIChange: function() {
     this._changed = true;
     if (!this.resetting) {
       this._updateResetButton();
       this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
     }
     CustomizableUI.dispatchToolboxEvent("customizationchange");
--- a/browser/components/customizableui/src/DragPositionManager.jsm
+++ b/browser/components/customizableui/src/DragPositionManager.jsm
@@ -388,16 +388,32 @@ let DragPositionManager = {
       if (positionManager) {
         positionManager.update(areaNode);
       } else {
         gManagers.set(areaNode, new AreaPositionManager(areaNode));
       }
     }
   },
 
+  add: function(aWindow, aArea, aContainer) {
+    if (CustomizableUI.getAreaType(aArea) != "toolbar") {
+      return;
+    }
+
+    gManagers.set(aContainer, new AreaPositionManager(aContainer));
+  },
+
+  remove: function(aWindow, aArea, aContainer) {
+    if (CustomizableUI.getAreaType(aArea) != "toolbar") {
+      return;
+    }
+
+    gManagers.delete(aContainer);
+  },
+
   stop: function() {
     gManagers.clear();
   },
 
   getManagerForArea: function(aArea) {
     return gManagers.get(aArea);
   }
 };
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -99,10 +99,11 @@ skip-if = os == "linux"
 
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_987177_destroyWidget_xul.js]
 [browser_987177_xul_wrapper_updating.js]
 [browser_987492_window_api.js]
 [browser_992747_toggle_noncustomizable_toolbar.js]
 [browser_993322_widget_notoolbar.js]
+[browser_995164_registerArea_during_customize_mode.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_panel_toggle.js]
--- a/browser/components/customizableui/test/browser_993322_widget_notoolbar.js
+++ b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js
@@ -26,9 +26,11 @@ add_task(function*() {
     is(buttonNode.parentNode.getAttribute("place"), "palette", "Node should be in palette");
     is(buttonNode, gNavToolbox.palette.querySelector("#" + BUTTONID), "Node should really be in palette.");
   }
   is(currentWidget.forWindow(window).node, buttonNode, "Should have the same node for customize mode");
   yield endCustomizing();
 
   CustomizableUI.destroyWidget(BUTTONID);
   CustomizableUI.unregisterArea(TOOLBARID, true);
+  toolbar.remove();
+  gAddedToolbars.clear();
 });
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const TOOLBARID = "test-toolbar-added-during-customize-mode";
+
+add_task(function*() {
+  yield startCustomizing();
+  let toolbar = createToolbarWithPlacements(TOOLBARID, []);
+  CustomizableUI.addWidgetToArea("sync-button", TOOLBARID);
+  let syncButton = document.getElementById("sync-button");
+  ok(syncButton, "Sync button should exist.");
+  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should be a wrapper.");
+
+  simulateItemDrag(syncButton, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
+
+  simulateItemDrag(syncButton, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
+
+  yield endCustomizing();
+  isnot(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should not be a wrapper outside customize mode.");
+  yield startCustomizing();
+
+  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should be a wrapper back in customize mode.");
+
+  simulateItemDrag(syncButton, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
+
+  ok(!CustomizableUI.inDefaultState, "Not in default state while toolbar is not collapsed yet.");
+  setToolbarVisibility(toolbar, false);
+  ok(CustomizableUI.inDefaultState, "In default state while toolbar is collapsed.");
+
+  setToolbarVisibility(toolbar, true);
+
+  info("Check that removing the area registration from within customize mode works");
+  CustomizableUI.unregisterArea(TOOLBARID);
+  ok(CustomizableUI.inDefaultState, "Now that the toolbar is no longer registered, should be in default state.");
+  ok(!(new Set(gCustomizeMode.areas)).has(toolbar), "Toolbar shouldn't be known to customize mode.");
+
+  CustomizableUI.registerArea(TOOLBARID, {legacy: true, defaultPlacements: []});
+  CustomizableUI.registerToolbarNode(toolbar, []);
+  ok(!CustomizableUI.inDefaultState, "Now that the toolbar is registered again, should no longer be in default state.");
+  ok((new Set(gCustomizeMode.areas)).has(toolbar), "Toolbar should be known to customize mode again.");
+
+  simulateItemDrag(syncButton, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
+
+  let otherWin = yield openAndLoadWindow({}, true);
+  let otherTB = otherWin.document.createElementNS(kNSXUL, "toolbar");
+  otherTB.id = TOOLBARID;
+  otherTB.setAttribute("customizable", "true");
+  otherWin.gNavToolbox.appendChild(otherTB);
+  ok(otherTB.querySelector("#sync-button"), "Sync button is on other toolbar, too.");
+
+  simulateItemDrag(syncButton, gNavToolbox.palette);
+  ok(!CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved to the palette");
+  ok(gNavToolbox.palette.querySelector("#sync-button"), "Sync button really is in palette.");
+  ok(!otherTB.querySelector("#sync-button"), "Sync button is in palette in other window, too.");
+
+  simulateItemDrag(syncButton, toolbar);
+  ok(CustomizableUI.getPlacementOfWidget("sync-button"), "Button moved out of palette");
+  is(CustomizableUI.getPlacementOfWidget("sync-button").area, TOOLBARID, "Button's back on toolbar");
+  ok(toolbar.querySelector("#sync-button"), "Sync button really is on toolbar.");
+  ok(otherTB.querySelector("#sync-button"), "Sync button is on other toolbar, too.");
+
+  let wasInformedCorrectlyOfAreaDisappearing = false;
+  let listener = {
+    onAreaNodeUnregistered: function(aArea, aNode, aReason) {
+      if (aArea == TOOLBARID) {
+        is(aNode, otherTB, "Should be informed about other toolbar");
+        is(aReason, CustomizableUI.REASON_WINDOW_CLOSED, "Reason should be correct.");
+        wasInformedCorrectlyOfAreaDisappearing = (aReason === CustomizableUI.REASON_WINDOW_CLOSED);
+      }
+    }
+  };
+  CustomizableUI.addListener(listener);
+  yield promiseWindowClosed(otherWin);
+
+  ok(wasInformedCorrectlyOfAreaDisappearing, "Should be told about window closing.");
+  CustomizableUI.removeListener(listener);
+  // Closing the other window should not be counted against this window's customize mode:
+  is(syncButton.parentNode.localName, "toolbarpaletteitem", "Sync button's parent node should still be a wrapper.");
+  isnot(gCustomizeMode.areas.indexOf(toolbar), -1, "Toolbar should still be a customizable area for this customize mode instance.");
+
+  yield gCustomizeMode.reset();
+
+  yield endCustomizing();
+
+  wasInformedCorrectlyOfAreaDisappearing = false;
+  listener = {
+    onAreaNodeUnregistered: function(aArea, aNode, aReason) {
+      if (aArea == TOOLBARID) {
+        is(aNode, toolbar, "Should be informed about this window's toolbar");
+        is(aReason, CustomizableUI.REASON_AREA_UNREGISTERED, "Reason for final removal should be correct.");
+        wasInformedCorrectlyOfAreaDisappearing = (aReason === CustomizableUI.REASON_AREA_UNREGISTERED);
+      }
+    },
+  }
+  CustomizableUI.addListener(listener);
+  removeCustomToolbars();
+  ok(wasInformedCorrectlyOfAreaDisappearing, "Should be told about area being unregistered.");
+  CustomizableUI.removeListener(listener);
+  ok(CustomizableUI.inDefaultState, "Should be fine after exiting customize mode.");
+});
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -140,16 +140,18 @@ skip-if = true
 [browser_586068-reload.js]
 [browser_586068-select.js]
 [browser_586068-window_state.js]
 [browser_586068-window_state_override.js]
 [browser_588426.js]
 [browser_590268.js]
 [browser_590563.js]
 [browser_595601-restore_hidden.js]
+[browser_597071.js]
+skip-if = true # Needs to be rewritten as Marionette test, bug 995916
 [browser_599909.js]
 [browser_600545.js]
 [browser_601955.js]
 [browser_607016.js]
 [browser_615394-SSWindowState_events.js]
 [browser_618151.js]
 [browser_623779.js]
 [browser_624727.js]
@@ -179,14 +181,12 @@ skip-if = os == "linux" # Intermittent f
 
 # Disabled for frequent intermittent failures
 [browser_464620_a.js]
 skip-if = true
 [browser_464620_b.js]
 skip-if = true
 
 # Disabled on OS X:
-[browser_597071.js]
-skip-if = os == "mac" || e10s
 [browser_625016.js]
 skip-if = os == "mac" || e10s
 
 [browser_911547.js]
--- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js
+++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-const stateBackup = ss.getBrowserState();
+const stateBackup = JSON.parse(ss.getBrowserState());
 const testState = {
   windows: [{
     tabs: [
       { entries: [{ url: "about:blank" }] },
       { entries: [{ url: "about:rights" }] }
     ]
   }]
 };
@@ -71,18 +71,17 @@ function runNextTest() {
     // If we closed a window, give it time to close
     executeSoon(function() {
       let currentTest = tests.shift();
       info("prepping for " + currentTest.name);
       waitForBrowserState(testState, currentTest);
     });
   }
   else {
-    ss.setBrowserState(stateBackup);
-    finish();
+    waitForBrowserState(stateBackup, finish);
   }
 }
 
 /**  ACTUAL TESTS  **/
 
 function test_setTabState() {
   let tab = gBrowser.tabs[1];
   let newTabState = JSON.stringify({ entries: [{ url: "http://example.org" }], extData: { foo: "bar" } });
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -425,16 +425,18 @@
 @BINPATH@/browser/components/@DLL_PREFIX@browsercomps@DLL_SUFFIX@
 @BINPATH@/components/txEXSLTRegExFunctions.manifest
 @BINPATH@/components/txEXSLTRegExFunctions.js
 @BINPATH@/components/toolkitplaces.manifest
 @BINPATH@/components/nsLivemarkService.js
 @BINPATH@/components/nsTaggingService.js
 @BINPATH@/components/nsPlacesAutoComplete.manifest
 @BINPATH@/components/nsPlacesAutoComplete.js
+@BINPATH@/components/UnifiedComplete.manifest
+@BINPATH@/components/UnifiedComplete.js
 @BINPATH@/components/nsPlacesExpiration.js
 @BINPATH@/browser/components/PlacesProtocolHandler.js
 @BINPATH@/components/PlacesCategoriesStarter.js
 @BINPATH@/components/ColorAnalyzer.js
 @BINPATH@/components/PageThumbsProtocol.js
 @BINPATH@/components/nsDefaultCLH.manifest
 @BINPATH@/components/nsDefaultCLH.js
 @BINPATH@/components/nsContentPrefService.manifest
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -83,17 +83,17 @@
 /**
  * We also vertically center the window buttons.
  */
 #main-window[tabsintitlebar] > #titlebar > #titlebar-content > #titlebar-buttonbox-container,
 #main-window[tabsintitlebar] > #titlebar > #titlebar-content > #titlebar-secondary-buttonbox > #titlebar-fullscreen-button {
   margin-top: @windowButtonMarginTop@;
 }
 
-#main-window[customizing] > #titlebar {
+#main-window[customize-entered] > #titlebar {
   -moz-appearance: none;
 }
 
 /** End titlebar **/
 
 #main-window[chromehidden~="toolbar"][chromehidden~="location"][chromehidden~="directories"] {
   border-top: 1px solid rgba(0,0,0,0.65);
 }
@@ -2842,16 +2842,26 @@ toolbarbutton.chevron > .toolbarbutton-m
 #tabbrowser-tabs {
   -moz-box-align: stretch;
 }
 
 .tabs-newtab-button > .toolbarbutton-icon {
   padding: 6px 0 4px;
 }
 
+/* Background tabs:
+ *
+ * Decrease the height of the hoverable region of background tabs whenever the tabs are at the top
+ * of the window (e.g. no menubar, tabs in titlebar, etc.) to make it easier to drag the window by
+ * the titlebar. We don't need this in fullscreen since window dragging is not an issue there.
+ */
+#main-window[tabsintitlebar]:not([inFullscreen]) .tab-background-middle:not([selected=true]) {
+  clip-path: url(chrome://browser/content/browser.xul#tab-hover-clip-path);
+}
+
 /**
  * Tab Drag and Drop
  */
 
 .tab-drop-indicator {
   list-style-image: url(chrome://browser/skin/tabbrowser/tabDragIndicator.png);
   margin-top: -2px;
   z-index: 3;
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -221,30 +221,16 @@
 .tab-background-start[selected=true]:-moz-lwtheme::before,
 .tab-background-end[selected=true]:-moz-lwtheme::before,
 .tab-background-middle[selected=true]:-moz-lwtheme {
   background-color: transparent;
 }
 
 /* End selected tab */
 
-/* Background tabs */
-
-/* Decrease the height of the hoverable region of background tabs whenever the tabs are at the top
-   of the window (e.g. no menubar, tabs in titlebar, etc.) to make it easier to drag the window by
-   the titlebar. We don't need this in fullscreen since window dragging is not an issue there. */
-%ifdef XP_MACOSX
-#main-window[tabsintitlebar][sizemode="maximized"] .tab-background-middle:not([selected=true]),
-%endif
-#main-window[tabsintitlebar]:not([sizemode="maximized"]):not([inFullscreen]) #toolbar-menubar:-moz-any([autohide="true"][inactive], :not([autohide])) + #TabsToolbar .tab-background-middle:not([selected=true]) {
-  clip-path: url(chrome://browser/content/browser.xul#tab-hover-clip-path);
-}
-
-/* End background tabs */
-
 /* new tab button border and gradient on hover */
 .tabbrowser-tab:hover > .tab-stack > .tab-background:not([selected=true]),
 .tabs-newtab-button:hover {
   background-image: url(chrome://browser/skin/tabbrowser/tab-background-start.png),
                     url(chrome://browser/skin/tabbrowser/tab-background-middle.png),
                     url(chrome://browser/skin/tabbrowser/tab-background-end.png);
   background-position: left bottom, @tabCurveWidth@ bottom, right bottom;
   background-repeat: no-repeat;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1897,16 +1897,26 @@ toolbarbutton[type="socialmark"] > .tool
   -moz-image-region: rect(0, 64px, 16px, 48px) !important;
 }
 
 /* tabbrowser-tab focus ring */
 .tabbrowser-tab:focus > .tab-stack > .tab-content > .tab-label {
   outline: 1px dotted;
 }
 
+/* Background tabs:
+ *
+ * Decrease the height of the hoverable region of background tabs whenever the tabs are at the top
+ * of the window (e.g. no menubar, tabs in titlebar, etc.) to make it easier to drag the window by
+ * the titlebar. We don't need this in fullscreen since window dragging is not an issue there.
+ */
+#main-window[tabsintitlebar][sizemode=normal] #toolbar-menubar[autohide="true"][inactive] + #TabsToolbar .tab-background-middle:not([selected=true]) {
+  clip-path: url(chrome://browser/content/browser.xul#tab-hover-clip-path);
+}
+
 /* Tab DnD indicator */
 .tab-drop-indicator {
   list-style-image: url(chrome://browser/skin/tabbrowser/tabDragIndicator.png);
   margin-bottom: -9px;
   z-index: 3;
 }
 
 /* Tab close button */
--- a/toolkit/components/places/PriorityUrlProvider.jsm
+++ b/toolkit/components/places/PriorityUrlProvider.jsm
@@ -121,17 +121,17 @@ this.PriorityUrlProvider = Object.freeze
   addMatch: function (match) {
     matches.set(match.token, match);
   },
 
   removeMatchByToken: function (token) {
     matches.delete(token);
   },
 
-  getMatchingSpec: function (searchToken) {
+  getMatch: function (searchToken) {
     return Task.spawn(function* () {
       yield promiseInitialized();
       for (let [token, match] of matches.entries()) {
         // Match at the beginning for now.  In future an aOptions argument may
         // allow  to control the matching behavior.
         if (token.startsWith(searchToken)) {
           return match;
         }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -0,0 +1,1236 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * 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";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+const TOPIC_SHUTDOWN = "places-shutdown";
+const TOPIC_PREFCHANGED = "nsPref:changed";
+
+const DEFAULT_BEHAVIOR = 0;
+
+const PREF_BRANCH = "browser.urlbar";
+
+// Prefs are defined as [pref name, default value].
+const PREF_ENABLED =            [ "autocomplete.enabled", true ];
+const PREF_AUTOFILL =           [ "autoFill",             true ];
+const PREF_AUTOFILL_TYPED =     [ "autoFill.typed",       true ];
+const PREF_AUTOFILL_PRIORITY =  [ "autoFill.priority",    true ];
+const PREF_DELAY =              [ "delay",                  50 ];
+const PREF_BEHAVIOR =           [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
+const PREF_DEFAULT_BEHAVIOR =   [ "default.behavior", DEFAULT_BEHAVIOR ];
+const PREF_EMPTY_BEHAVIOR =     [ "default.behavior.emptyRestriction",
+                                  Ci.mozIPlacesAutoComplete.BEHAVIOR_HISTORY |
+                                  Ci.mozIPlacesAutoComplete.BEHAVIOR_TYPED ];
+const PREF_FILTER_JS =          [ "filter.javascript",    true ];
+const PREF_MAXRESULTS =         [ "maxRichResults",         25 ];
+const PREF_RESTRICT_HISTORY =   [ "restrict.history",      "^" ];
+const PREF_RESTRICT_BOOKMARKS = [ "restrict.bookmark",     "*" ];
+const PREF_RESTRICT_TYPED =     [ "restrict.typed",        "~" ];
+const PREF_RESTRICT_TAG =       [ "restrict.tag",          "+" ];
+const PREF_RESTRICT_SWITCHTAB = [ "restrict.openpage",     "%" ];
+const PREF_MATCH_TITLE =        [ "match.title",           "#" ];
+const PREF_MATCH_URL =          [ "match.url",             "@" ];
+
+// Match type constants.
+// These indicate what type of search function we should be using.
+const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
+const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
+const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_BEGINNING = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING;
+const MATCH_BEGINNING_CASE_SENSITIVE = Ci.mozIPlacesAutoComplete.MATCH_BEGINNING_CASE_SENSITIVE;
+
+// AutoComplete query type constants.
+// Describes the various types of queries that we can process rows for.
+const QUERYTYPE_KEYWORD       = 0;
+const QUERYTYPE_FILTERED      = 1;
+const QUERYTYPE_AUTOFILL_HOST = 2;
+const QUERYTYPE_AUTOFILL_URL  = 3;
+
+// This separator is used as an RTL-friendly way to split the title and tags.
+// It can also be used by an nsIAutoCompleteResult consumer to re-split the
+// "comment" back into the title and the tag.
+const TITLE_TAGS_SEPARATOR = " \u2013 ";
+
+// Telemetry probes.
+const TELEMETRY_1ST_RESULT = "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS";
+
+// The default frecency value used when inserting priority results.
+const FRECENCY_PRIORITY_DEFAULT = 1000;
+
+// Sqlite result row index constants.
+const QUERYINDEX_QUERYTYPE     = 0;
+const QUERYINDEX_URL           = 1;
+const QUERYINDEX_TITLE         = 2;
+const QUERYINDEX_ICONURL       = 3;
+const QUERYINDEX_BOOKMARKED    = 4;
+const QUERYINDEX_BOOKMARKTITLE = 5;
+const QUERYINDEX_TAGS          = 6;
+const QUERYINDEX_VISITCOUNT    = 7;
+const QUERYINDEX_TYPED         = 8;
+const QUERYINDEX_PLACEID       = 9;
+const QUERYINDEX_SWITCHTAB     = 10;
+const QUERYINDEX_FRECENCY      = 11;
+
+// This SQL query fragment provides the following:
+//   - whether the entry is bookmarked (QUERYINDEX_BOOKMARKED)
+//   - the bookmark title, if it is a bookmark (QUERYINDEX_BOOKMARKTITLE)
+//   - the tags associated with a bookmarked entry (QUERYINDEX_TAGS)
+const SQL_BOOKMARK_TAGS_FRAGMENT = sql(
+  "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) AS bookmarked,",
+  "( SELECT title FROM moz_bookmarks WHERE fk = h.id AND title NOTNULL",
+    "ORDER BY lastModified DESC LIMIT 1",
+  ") AS btitle,",
+  "( SELECT GROUP_CONCAT(t.title, ',')",
+    "FROM moz_bookmarks b",
+    "JOIN moz_bookmarks t ON t.id = +b.parent AND t.parent = :parent",
+    "WHERE b.fk = h.id",
+  ") AS tags");
+
+// TODO bug 412736: in case of a frecency tie, we might break it with h.typed
+// and h.visit_count.  That is slower though, so not doing it yet...
+const SQL_DEFAULT_QUERY = sql(
+  "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
+         "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
+  "FROM moz_places h",
+  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
+  "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
+  "WHERE h.frecency <> 0",
+    "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
+                           "IFNULL(btitle, h.title), tags,",
+                           "h.visit_count, h.typed,",
+                           "bookmarked, t.open_count,",
+                           ":matchBehavior, :searchBehavior)",
+    "/*CONDITIONS*/",
+  "ORDER BY h.frecency DESC, h.id DESC",
+  "LIMIT :maxResults");
+
+// Enforce ignoring the visit_count index, since the frecency one is much
+// faster in this case.  ANALYZE helps the query planner to figure out the
+// faster path, but it may not have up-to-date information yet.
+const SQL_HISTORY_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
+                                                    "AND +h.visit_count > 0", "g");
+
+const SQL_BOOKMARK_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
+                                                     "AND bookmarked", "g");
+
+const SQL_TAGS_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
+                                                 "AND tags NOTNULL", "g");
+
+const SQL_TYPED_QUERY = SQL_DEFAULT_QUERY.replace("/*CONDITIONS*/",
+                                                  "AND h.typed = 1", "g");
+
+const SQL_SWITCHTAB_QUERY = sql(
+  "SELECT :query_type, t.url, t.url, NULL, NULL, NULL, NULL, NULL, NULL, NULL,",
+         "t.open_count, NULL",
+  "FROM moz_openpages_temp t",
+  "LEFT JOIN moz_places h ON h.url = t.url",
+  "WHERE h.id IS NULL",
+    "AND AUTOCOMPLETE_MATCH(:searchString, t.url, t.url, NULL,",
+                            "NULL, NULL, NULL, t.open_count,",
+                            ":matchBehavior, :searchBehavior)",
+  "ORDER BY t.ROWID DESC",
+  "LIMIT :maxResults");
+
+const SQL_ADAPTIVE_QUERY = sql(
+  "/* do not warn (bug 487789) */",
+  "SELECT :query_type, h.url, h.title, f.url,", SQL_BOOKMARK_TAGS_FRAGMENT, ",",
+         "h.visit_count, h.typed, h.id, t.open_count, h.frecency",
+  "FROM (",
+    "SELECT ROUND(MAX(use_count) * (1 + (input = :search_string)), 1) AS rank,",
+           "place_id",
+    "FROM moz_inputhistory",
+    "WHERE input BETWEEN :search_string AND :search_string || X'FFFF'",
+    "GROUP BY place_id",
+  ") AS i",
+  "JOIN moz_places h ON h.id = i.place_id",
+  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
+  "LEFT JOIN moz_openpages_temp t ON t.url = h.url",
+  "WHERE AUTOCOMPLETE_MATCH(NULL, h.url,",
+                           "IFNULL(btitle, h.title), tags,",
+                           "h.visit_count, h.typed, bookmarked,",
+                           "t.open_count,",
+                           ":matchBehavior, :searchBehavior)",
+  "ORDER BY rank DESC, h.frecency DESC");
+
+const SQL_KEYWORD_QUERY = sql(
+  "/* do not warn (bug 487787) */",
+  "SELECT :query_type,",
+    "(SELECT REPLACE(url, '%s', :query_string) FROM moz_places WHERE id = b.fk)",
+    "AS search_url, h.title,",
+    "IFNULL(f.url, (SELECT f.url",
+                   "FROM moz_places",
+                   "JOIN moz_favicons f ON f.id = favicon_id",
+                   "WHERE rev_host = (SELECT rev_host FROM moz_places WHERE id = b.fk)",
+                   "ORDER BY frecency DESC",
+                   "LIMIT 1)",
+          "),",
+    "1, b.title, NULL, h.visit_count, h.typed, IFNULL(h.id, b.fk),",
+    "t.open_count, h.frecency",
+  "FROM moz_keywords k",
+  "JOIN moz_bookmarks b ON b.keyword_id = k.id",
+  "LEFT JOIN moz_places h ON h.url = search_url",
+  "LEFT JOIN moz_favicons f ON f.id = h.favicon_id",
+  "LEFT JOIN moz_openpages_temp t ON t.url = search_url",
+  "WHERE LOWER(k.keyword) = LOWER(:keyword)",
+  "ORDER BY h.frecency DESC");
+
+const SQL_HOST_QUERY = sql(
+  "/* do not warn (bug NA): not worth to index on (typed, frecency) */",
+  "SELECT :query_type, host || '/', prefix || host || '/',",
+         "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency",
+  "FROM moz_hosts",
+  "WHERE host BETWEEN :searchString AND :searchString || X'FFFF'",
+  "AND frecency <> 0",
+  "/*CONDITIONS*/",
+  "ORDER BY frecency DESC",
+  "LIMIT 1");
+
+const SQL_TYPED_HOST_QUERY = SQL_HOST_QUERY.replace("/*CONDITIONS*/",
+                                                    "AND typed = 1");
+const SQL_URL_QUERY = sql(
+  "/* do not warn (bug no): cannot use an index */",
+  "SELECT :query_type, h.url,",
+         "NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency",
+  "FROM moz_places h",
+  "WHERE h.frecency <> 0",
+  "/*CONDITIONS*/",
+  "AND AUTOCOMPLETE_MATCH(:searchString, h.url,",
+  "h.title, '',",
+  "h.visit_count, h.typed, 0, 0,",
+  ":matchBehavior, :searchBehavior)",
+  "ORDER BY h.frecency DESC, h.id DESC",
+  "LIMIT 1");
+
+const SQL_TYPED_URL_QUERY = SQL_URL_QUERY.replace("/*CONDITIONS*/",
+                                                  "AND typed = 1");
+
+////////////////////////////////////////////////////////////////////////////////
+//// Getters
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
+                                  "resource://gre/modules/TelemetryStopwatch.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+                                  "resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+                                  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PriorityUrlProvider",
+                                  "resource://gre/modules/PriorityUrlProvider.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
+                                   "@mozilla.org/intl/texttosuburi;1",
+                                   "nsITextToSubURI");
+
+/**
+ * Storage object for switch-to-tab entries.
+ * This takes care of caching and registering open pages, that will be reused
+ * by switch-to-tab queries.  It has an internal cache, so that the Sqlite
+ * store is lazy initialized only on first use.
+ * It has a simple API:
+ *   initDatabase(conn): initializes the temporary Sqlite entities to store data
+ *   add(uri): adds a given nsIURI to the store
+ *   delete(uri): removes a given nsIURI from the store
+ *   shutdown(): stops storing data to Sqlite
+ */
+XPCOMUtils.defineLazyGetter(this, "SwitchToTabStorage", () => Object.seal({
+  _conn: null,
+  // Temporary queue used while the database connection is not available.
+  _queue: new Set(),
+  initDatabase: Task.async(function* (conn) {
+    // To reduce IO use an in-memory table for switch-to-tab tracking.
+    // Note: this should be kept up-to-date with the definition in
+    //       nsPlacesTables.h.
+    yield conn.execute(sql(
+      "CREATE TEMP TABLE moz_openpages_temp (",
+        "url TEXT PRIMARY KEY,",
+        "open_count INTEGER",
+      ")"));
+
+    // Note: this should be kept up-to-date with the definition in
+    //       nsPlacesTriggers.h.
+    yield conn.execute(sql(
+      "CREATE TEMPORARY TRIGGER moz_openpages_temp_afterupdate_trigger",
+      "AFTER UPDATE OF open_count ON moz_openpages_temp FOR EACH ROW",
+      "WHEN NEW.open_count = 0",
+      "BEGIN",
+        "DELETE FROM moz_openpages_temp",
+        "WHERE url = NEW.url;",
+      "END"));
+
+    this._conn = conn;
+
+    // Populate the table with the current cache contents...
+    this._queue.forEach(this.add, this);
+    // ...then clear it to avoid double additions.
+    this._queue.clear();
+  }),
+
+  add: function (uri) {
+    if (!this._conn) {
+      this._queue.add(uri);
+      return;
+    }
+    this._conn.executeCached(sql(
+      "INSERT OR REPLACE INTO moz_openpages_temp (url, open_count)",
+        "VALUES ( :url, IFNULL( (SELECT open_count + 1",
+                                 "FROM moz_openpages_temp",
+                                 "WHERE url = :url),",
+                                 "1",
+                             ")",
+               ")"
+    ), { url: uri.spec });
+  },
+
+  delete: function (uri) {
+    if (!this._conn) {
+      this._queue.delete(uri);
+      return;
+    }
+    this._conn.executeCached(sql(
+      "UPDATE moz_openpages_temp",
+      "SET open_count = open_count - 1",
+      "WHERE url = :url"
+    ), { url: uri.spec });
+  },
+
+  shutdown: function () {
+    this._conn = null;
+    this._queue.clear();
+  }
+}));
+
+/**
+ * This helper keeps track of preferences and keeps their values up-to-date.
+ */
+XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
+  let prefs = new Preferences(PREF_BRANCH);
+
+  function loadPrefs() {
+    store.enabled = prefs.get(...PREF_ENABLED);
+    store.autofill = prefs.get(...PREF_AUTOFILL);
+    store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
+    store.autofillPriority = prefs.get(...PREF_AUTOFILL_PRIORITY);
+    store.delay = prefs.get(...PREF_DELAY);
+    store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
+    store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
+    store.maxRichResults = prefs.get(...PREF_MAXRESULTS);
+    store.restrictHistoryToken = prefs.get(...PREF_RESTRICT_HISTORY);
+    store.restrictBookmarkToken = prefs.get(...PREF_RESTRICT_BOOKMARKS);
+    store.restrictTypedToken = prefs.get(...PREF_RESTRICT_TYPED);
+    store.restrictTagToken = prefs.get(...PREF_RESTRICT_TAG);
+    store.restrictOpenPageToken = prefs.get(...PREF_RESTRICT_SWITCHTAB);
+    store.matchTitleToken = prefs.get(...PREF_MATCH_TITLE);
+    store.matchURLToken = prefs.get(...PREF_MATCH_URL);
+    store.defaultBehavior = prefs.get(...PREF_DEFAULT_BEHAVIOR);
+    // Further restrictions to apply for "empty searches" (i.e. searches for "").
+    store.emptySearchDefaultBehavior = store.defaultBehavior |
+                                       prefs.get(...PREF_EMPTY_BEHAVIOR);
+
+    // Validate matchBehavior; default to MATCH_BOUNDARY_ANYWHERE.
+    if (store.matchBehavior != MATCH_ANYWHERE &&
+        store.matchBehavior != MATCH_BOUNDARY &&
+        store.matchBehavior != MATCH_BEGINNING) {
+      store.matchBehavior = MATCH_BOUNDARY_ANYWHERE;
+    }
+
+    store.tokenToBehaviorMap = new Map([
+      [ store.restrictHistoryToken, "history" ],
+      [ store.restrictBookmarkToken, "bookmark" ],
+      [ store.restrictTagToken, "tag" ],
+      [ store.restrictOpenPageToken, "openpage" ],
+      [ store.matchTitleToken, "title" ],
+      [ store.matchURLToken, "url" ],
+      [ store.restrictTypedToken, "typed" ]
+    ]);
+  }
+
+  let store = {
+    observe: function (subject, topic, data) {
+      loadPrefs();
+    },
+    QueryInterface: XPCOMUtils.generateQI([ Ci.nsIObserver ])
+  };
+  loadPrefs();
+  prefs.observe("", store);
+
+  return Object.seal(store);
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Helper functions
+
+/**
+ * Joins multiple sql tokens into a single sql query.
+ */
+function sql(...parts) parts.join(" ");
+
+/**
+ * Used to unescape encoded URI strings and drop information that we do not
+ * care about.
+ *
+ * @param spec
+ *        The text to unescape and modify.
+ * @return the modified spec.
+ */
+function fixupSearchText(spec)
+  textURIService.unEscapeURIForUI("UTF-8", stripPrefix(spec));
+
+/**
+ * Generates the tokens used in searching from a given string.
+ *
+ * @param searchString
+ *        The string to generate tokens from.
+ * @return an array of tokens.
+ * @note Calling split on an empty string will return an array containing one
+ *       empty string.  We don't want that, as it'll break our logic, so return
+ *       an empty array then.
+ */
+function getUnfilteredSearchTokens(searchString)
+  searchString.length ? searchString.split(" ") : [];
+
+/**
+ * Strip prefixes from the URI that we don't care about for searching.
+ *
+ * @param spec
+ *        The text to modify.
+ * @return the modified spec.
+ */
+function stripPrefix(spec)
+{
+  ["http://", "https://", "ftp://"].some(scheme => {
+    if (spec.startsWith(scheme)) {
+      spec = spec.slice(scheme.length);
+      return true;
+    }
+    return false;
+  });
+
+  if (spec.startsWith("www.")) {
+    spec = spec.slice(4);
+  }
+  return spec;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Search Class
+//// Manages a single instance of an autocomplete search.
+
+function Search(searchString, searchParam, autocompleteListener,
+                resultListener, autocompleteSearch) {
+  // We want to store the original string with no leading or trailing
+  // whitespace for case sensitive searches.
+  this._originalSearchString = searchString.trim();
+  this._searchString = fixupSearchText(this._originalSearchString.toLowerCase());
+  this._searchTokens =
+    this.filterTokens(getUnfilteredSearchTokens(this._searchString));
+  // The protocol and the host are lowercased by nsIURI, so it's fine to
+  // lowercase the typed prefix, to add it back to the results later.
+  this._strippedPrefix = this._originalSearchString.slice(
+    0, this._originalSearchString.length - this._searchString.length
+  ).toLowerCase();
+  // The URIs in the database are fixed-up, so we can match on a lowercased
+  // host, but the path must be matched in a case sensitive way.
+  let pathIndex =
+    this._originalSearchString.indexOf("/", this._strippedPrefix.length);
+  this._autofillUrlSearchString = fixupSearchText(
+    this._originalSearchString.slice(0, pathIndex).toLowerCase() +
+    this._originalSearchString.slice(pathIndex)
+  );
+
+  this._enableActions = searchParam.split(" ").indexOf("enable-actions") != -1;
+
+  this._listener = autocompleteListener;
+  this._autocompleteSearch = autocompleteSearch;
+
+  this._matchBehavior = Prefs.matchBehavior;
+  // Set the default behavior for this search.
+  this._behavior = this._searchString ? Prefs.defaultBehavior
+                                      : Prefs.emptySearchDefaultBehavior;
+  // Create a new result to add eventual matches.  Note we need a result
+  // regardless having matches.
+  let result = Cc["@mozilla.org/autocomplete/simple-result;1"]
+                 .createInstance(Ci.nsIAutoCompleteSimpleResult);
+  result.setSearchString(searchString);
+  result.setListener(resultListener);
+  // Will be set later, if needed.
+  result.setDefaultIndex(-1);
+  this._result = result;
+
+  // These are used to avoid adding duplicate entries to the results.
+  this._usedURLs = new Set();
+  this._usedPlaceIds = new Set();
+}
+
+Search.prototype = {
+  /**
+   * Enables the desired AutoComplete behavior.
+   *
+   * @param type
+   *        The behavior type to set.
+   */
+  setBehavior: function (type) {
+    this._behavior |=
+      Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
+  },
+
+  /**
+   * Determines if the specified AutoComplete behavior is set.
+   *
+   * @param aType
+   *        The behavior type to test for.
+   * @return true if the behavior is set, false otherwise.
+   */
+  hasBehavior: function (type) {
+    return this._behavior &
+           Ci.mozIPlacesAutoComplete["BEHAVIOR_" + type.toUpperCase()];
+  },
+
+  /**
+   * Used to delay the most complex queries, to save IO while the user is
+   * typing.
+   */
+  _sleepDeferred: null,
+  _sleep: function (aTimeMs) {
+    // Reuse a single instance to try shaving off some usless work before
+    // the first query.
+    if (!this._sleepTimer)
+      this._sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+    this._sleepDeferred = Promise.defer();
+    this._sleepTimer.initWithCallback(() => this._sleepDeferred.resolve(),
+                                      aTimeMs, Ci.nsITimer.TYPE_ONE_SHOT);
+    return this._sleepDeferred.promise;
+  },
+
+  /**
+   * Given an array of tokens, this function determines which query should be
+   * ran.  It also removes any special search tokens.
+   *
+   * @param tokens
+   *        An array of search tokens.
+   * @return the filtered list of tokens to search with.
+   */
+  filterTokens: function (tokens) {
+    // Set the proper behavior while filtering tokens.
+    for (let i = tokens.length - 1; i >= 0; i--) {
+      let behavior = Prefs.tokenToBehaviorMap.get(tokens[i]);
+      // Don't remove the token if it didn't match, or if it's an action but
+      // actions are not enabled.
+      if (behavior && (behavior != "openpage" || this._enableActions)) {
+        this.setBehavior(behavior);
+        tokens.splice(i, 1);
+      }
+    }
+
+    // Set the right JavaScript behavior based on our preference.  Note that the
+    // preference is whether or not we should filter JavaScript, and the
+    // behavior is if we should search it or not.
+    if (!Prefs.filterJavaScript) {
+      this.setBehavior("javascript");
+    }
+
+    return tokens;
+  },
+
+  /**
+   * Used to cancel this search, will stop providing results.
+   */
+  cancel: function () {
+    if (this._sleepTimer)
+      this._sleepTimer.cancel();
+    if (this._sleepDeferred) {
+      this._sleepDeferred.resolve();
+      this._sleepDeferred = null;
+    }
+    delete this._pendingQuery;
+  },
+
+  /**
+   * Whether this search is running.
+   */
+  get pending() !!this._pendingQuery,
+
+  /**
+   * Execute the search and populate results.
+   * @param conn
+   *        The Sqlite connection.
+   */
+  execute: Task.async(function* (conn) {
+    this._pendingQuery = true;
+    TelemetryStopwatch.start(TELEMETRY_1ST_RESULT);
+
+    // For any given search, we run many queries:
+    // 1) priority domains
+    // 2) inline completion
+    // 3) keywords (this._keywordQuery)
+    // 4) adaptive learning (this._adaptiveQuery)
+    // 5) open pages not supported by history (this._switchToTabQuery)
+    // 6) query based on match behavior
+    //
+    // (3) only gets ran if we get any filtered tokens, since if there are no
+    // tokens, there is nothing to match.
+
+    // Get the final query, based on the tokens found in the search string.
+    let queries = [ this._adaptiveQuery,
+                    this._switchToTabQuery,
+                    this._searchQuery ];
+
+    if (this._searchTokens.length == 1) {
+      yield this._matchPriorityUrl();
+    } else if (this._searchTokens.length > 1) {
+      queries.unshift(this._keywordQuery);
+    }
+
+    if (this._shouldAutofill) {
+      // Hosts have no "/" in them.
+      let lastSlashIndex = this._searchString.lastIndexOf("/");
+      // Search only URLs if there's a slash in the search string...
+      if (lastSlashIndex != -1) {
+        // ...but not if it's exactly at the end of the search string.
+        if (lastSlashIndex < this._searchString.length - 1) {
+          queries.unshift(this._urlQuery);
+        }
+      } else if (this.pending) {
+        // The host query is executed immediately, while any other is delayed
+        // to avoid overloading the connection.
+        let [ query, params ] = this._hostQuery;
+        yield conn.executeCached(query, params, this._onResultRow.bind(this));
+      }
+    }
+
+    yield this._sleep(Prefs.delay);
+    if (!this.pending)
+      return;
+
+    for (let [query, params] of queries) {
+      yield conn.executeCached(query, params, this._onResultRow.bind(this));
+      if (!this.pending)
+        return;
+    }
+
+    // If we do not have enough results, and our match type is
+    // MATCH_BOUNDARY_ANYWHERE, search again with MATCH_ANYWHERE to get more
+    // results.
+    if (this._matchBehavior == MATCH_BOUNDARY_ANYWHERE &&
+        this._result.matchCount < Prefs.maxRichResults) {
+      this._matchBehavior = MATCH_ANYWHERE;
+      for (let [query, params] of [ this._adaptiveQuery,
+                                    this._searchQuery ]) {
+        yield conn.executeCached(query, params, this._onResultRow);
+        if (!this.pending)
+          return;
+      }
+    }
+
+    // If we didn't find enough matches and we have some frecency-driven
+    // matches, add them.
+    if (this._frecencyMatches) {
+      this._frecencyMatches.forEach(this._addMatch, this);
+    }
+  }),
+
+  _matchPriorityUrl: function* () {
+    if (!Prefs.autofillPriority)
+      return;
+    let priorityMatch = yield PriorityUrlProvider.getMatch(this._searchString);
+    if (priorityMatch) {
+      this._result.setDefaultIndex(0);
+      this._addFrecencyMatch({
+        value: priorityMatch.token,
+        comment: priorityMatch.title,
+        icon: priorityMatch.iconUrl,
+        style: "priority-" + priorityMatch.reason,
+        finalCompleteValue: priorityMatch.url,
+        frecency: FRECENCY_PRIORITY_DEFAULT
+      });
+    }
+  },
+
+  _onResultRow: function (row) {
+    TelemetryStopwatch.finish(TELEMETRY_1ST_RESULT);
+    let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
+    let match;
+    switch (queryType) {
+      case QUERYTYPE_AUTOFILL_HOST:
+        this._result.setDefaultIndex(0);
+        match = this._processHostRow(row);
+        break;
+      case QUERYTYPE_AUTOFILL_URL:
+        this._result.setDefaultIndex(0);
+        match = this._processUrlRow(row);
+        break;
+      case QUERYTYPE_FILTERED:
+      case QUERYTYPE_KEYWORD:
+        match = this._processRow(row);
+        break;
+    }
+    this._addMatch(match);
+  },
+
+  /**
+   * These matches should be mixed up with other matches, based on frecency.
+   */
+  _addFrecencyMatch: function (match) {
+    if (!this._frecencyMatches)
+      this._frecencyMatches = [];
+    this._frecencyMatches.push(match);
+    // We keep this array in reverse order, so we can walk it and remove stuff
+    // from it in one pass.  Notice that for frecency reverse order means from
+    // lower to higher.
+    this._frecencyMatches.sort((a, b) => a.frecency - b.frecency);
+  },
+
+  _addMatch: function (match) {
+    let notifyResults = false;
+
+    if (this._frecencyMatches) {
+      for (let i = this._frecencyMatches.length - 1;  i >= 0 ; i--) {
+        if (this._frecencyMatches[i].frecency > match.frecency) {
+          this._addMatch(this._frecencyMatches.splice(i, 1)[0]);
+        }
+      }
+    }
+
+    // Must check both id and url, cause keywords dinamically modify the url.
+    if ((!match.placeId || !this._usedPlaceIds.has(match.placeId)) &&
+        !this._usedURLs.has(stripPrefix(match.value))) {
+      // Add this to our internal tracker to ensure duplicates do not end up in
+      // the result.
+      // Not all entries have a place id, thus we fallback to the url for them.
+      // We cannot use only the url since keywords entries are modified to
+      // include the search string, and would be returned multiple times.  Ids
+      // are faster too.
+      if (match.placeId)
+        this._usedPlaceIds.add(match.placeId);
+      this._usedURLs.add(stripPrefix(match.value));
+
+      this._result.appendMatch(match.value,
+                               match.comment,
+                               match.icon || PlacesUtils.favicons.defaultFavicon.spec,
+                               match.style || "favicon",
+                               match.finalCompleteValue);
+      notifyResults = true;
+    }
+
+    if (this._result.matchCount == Prefs.maxRichResults || !this.pending) {
+      // We have enough results, so stop running our search.
+      this.cancel();
+      // This tells Sqlite.jsm to stop providing us results and cancel the
+      // underlying query.
+      throw StopIteration;
+    }
+
+    if (notifyResults) {
+      // Notify about results if we've gotten them.
+      this.notifyResults(true);
+    }
+  },
+
+  _processHostRow: function (row) {
+    let match = {};
+    let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
+    let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
+    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+    // If the untrimmed value doesn't preserve the user's input just
+    // ignore it and complete to the found host.
+    if (untrimmedHost &&
+        !untrimmedHost.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
+      // THIS CAUSES null TO BE SHOWN AS TITLE.
+      untrimmedHost = null;
+    }
+
+    match.value = this._strippedPrefix + trimmedHost;
+    match.comment = trimmedHost;
+    match.finalCompleteValue = untrimmedHost;
+    match.frecency = frecency;
+    return match;
+  },
+
+  _processUrlRow: function (row) {
+    let match = {};
+    let value = row.getResultByIndex(QUERYINDEX_URL);
+    let url = fixupSearchText(value);
+    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+
+    let prefix = value.slice(0, value.length - stripPrefix(value).length);
+
+    // We must complete the URL up to the next separator (which is /, ? or #).
+    let separatorIndex = url.slice(this._searchString.length)
+                            .search(/[\/\?\#]/);
+    if (separatorIndex != -1) {
+      separatorIndex += this._searchString.length;
+      if (url[separatorIndex] == "/") {
+        separatorIndex++; // Include the "/" separator
+      }
+      url = url.slice(0, separatorIndex);
+    }
+
+    // If the untrimmed value doesn't preserve the user's input just
+    // ignore it and complete to the found url.
+    let untrimmedURL = prefix + url;
+    if (untrimmedURL &&
+        !untrimmedURL.toLowerCase().contains(this._originalSearchString.toLowerCase())) {
+      // THIS CAUSES null TO BE SHOWN AS TITLE.
+      untrimmedURL = null;
+     }
+
+    match.value = this._strippedPrefix + url;
+    match.comment = url;
+    match.finalCompleteValue = untrimmedURL;
+    match.frecency = frecency;
+    return match;
+  },
+
+  _processRow: function (row) {
+    let match = {};
+    match.placeId = row.getResultByIndex(QUERYINDEX_PLACEID);
+    let queryType = row.getResultByIndex(QUERYINDEX_QUERYTYPE);
+    let escapedURL = row.getResultByIndex(QUERYINDEX_URL);
+    let openPageCount = row.getResultByIndex(QUERYINDEX_SWITCHTAB) || 0;
+    let historyTitle = row.getResultByIndex(QUERYINDEX_TITLE) || "";
+    let iconurl = row.getResultByIndex(QUERYINDEX_ICONURL) || "";
+    let bookmarked = row.getResultByIndex(QUERYINDEX_BOOKMARKED);
+    let bookmarkTitle = bookmarked ?
+      row.getResultByIndex(QUERYINDEX_BOOKMARKTITLE) : null;
+    let tags = row.getResultByIndex(QUERYINDEX_TAGS) || "";
+    let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+
+    // If actions are enabled and the page is open, add only the switch-to-tab
+    // result.  Otherwise, add the normal result.
+    let [url, action] = this._enableActions && openPageCount > 0 ?
+                        ["moz-action:switchtab," + escapedURL, "action "] :
+                        [escapedURL, ""];
+
+    // Always prefer the bookmark title unless it is empty
+    let title = bookmarkTitle || historyTitle;
+
+    if (queryType == QUERYTYPE_KEYWORD) {
+      // If we do not have a title, then we must have a keyword, so let the UI
+      // know it is a keyword.  Otherwise, we found an exact page match, so just
+      // show the page like a regular result.  Because the page title is likely
+      // going to be more specific than the bookmark title (keyword title).
+      if (!historyTitle) {
+        match.style = "keyword";
+      }
+      else {
+        title = historyTitle;
+      }
+    }
+
+    // We will always prefer to show tags if we have them.
+    let showTags = !!tags;
+
+    // However, we'll act as if a page is not bookmarked or tagged if the user
+    // only wants only history and not bookmarks or tags.
+    if (this.hasBehavior("history") &&
+        !(this.hasBehavior("bookmark") || this.hasBehavior("tag"))) {
+      showTags = false;
+      match.style = "favicon";
+    }
+
+    // If we have tags and should show them, we need to add them to the title.
+    if (showTags) {
+      title += TITLE_TAGS_SEPARATOR + tags;
+    }
+
+    // We have to determine the right style to display.  Tags show the tag icon,
+    // bookmarks get the bookmark icon, and keywords get the keyword icon.  If
+    // the result does not fall into any of those, it just gets the favicon.
+    if (!match.style) {
+      // It is possible that we already have a style set (from a keyword
+      // search or because of the user's preferences), so only set it if we
+      // haven't already done so.
+      if (showTags) {
+        match.style = "tag";
+      }
+      else if (bookmarked) {
+        match.style = "bookmark";
+      }
+    }
+
+    match.value = url;
+    match.comment = title;
+    if (iconurl) {
+      match.icon = PlacesUtils.favicons
+                              .getFaviconLinkForIcon(NetUtil.newURI(iconurl)).spec;
+    }
+    match.frecency = frecency;
+
+    return match;
+  },
+
+  /**
+   * Obtains the search query to be used based on the previously set search
+   * behaviors (accessed by this.hasBehavior).
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _searchQuery() {
+    // We use more optimized queries for restricted searches, so we will always
+    // return the most restrictive one to the least restrictive one if more than
+    // one token is found.
+    // Note: "openpages" behavior is supported by the default query.
+    //       _switchToTabQuery instead returns only pages not supported by
+    //       history and it is always executed.
+    let query = this.hasBehavior("tag") ? SQL_TAGS_QUERY :
+                this.hasBehavior("bookmark") ? SQL_BOOKMARK_QUERY :
+                this.hasBehavior("typed") ? SQL_TYPED_QUERY :
+                this.hasBehavior("history") ? SQL_HISTORY_QUERY :
+                SQL_DEFAULT_QUERY;
+
+    return [
+      query,
+      {
+        parent: PlacesUtils.tagsFolderId,
+        query_type: QUERYTYPE_FILTERED,
+        matchBehavior: this._matchBehavior,
+        searchBehavior: this._behavior,
+        // We only want to search the tokens that we are left with - not the
+        // original search string.
+        searchString: this._searchTokens.join(" "),
+        // Limit the query to the the maximum number of desired results.
+        // This way we can avoid doing more work than needed.
+        maxResults: Prefs.maxRichResults
+      }
+    ];
+  },
+
+  /**
+   * Obtains the query to search for keywords.
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _keywordQuery() {
+    // The keyword is the first word in the search string, with the parameters
+    // following it.
+    let searchString = this._originalSearchString;
+    let queryString = "";
+    let queryIndex = searchString.indexOf(" ");
+    if (queryIndex != -1) {
+      queryString = searchString.substring(queryIndex + 1);
+    }
+    // We need to escape the parameters as if they were the query in a URL
+    queryString = encodeURIComponent(queryString).replace("%20", "+", "g");
+
+    // The first word could be a keyword, so that's what we'll search.
+    let keyword = this._searchTokens[0];
+
+    return [
+      SQL_KEYWORD_QUERY,
+      {
+        keyword: keyword,
+        query_string: queryString,
+        query_type: QUERYTYPE_KEYWORD
+      }
+    ];
+  },
+
+  /**
+   * Obtains the query to search for switch-to-tab entries.
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _switchToTabQuery() [
+    SQL_SWITCHTAB_QUERY,
+    {
+      query_type: QUERYTYPE_FILTERED,
+      matchBehavior: this._matchBehavior,
+      searchBehavior: this._behavior,
+      // We only want to search the tokens that we are left with - not the
+      // original search string.
+      searchString: this._searchTokens.join(" "),
+      maxResults: Prefs.maxRichResults
+    }
+  ],
+
+  /**
+   * Obtains the query to search for adaptive results.
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _adaptiveQuery() [
+    SQL_ADAPTIVE_QUERY,
+    {
+      parent: PlacesUtils.tagsFolderId,
+      search_string: this._searchString,
+      query_type: QUERYTYPE_FILTERED,
+      matchBehavior: this._matchBehavior,
+      searchBehavior: this._behavior
+    }
+  ],
+
+  /**
+   * Whether we should try to autoFill.
+   */
+  get _shouldAutofill() {
+    // First of all, check for the autoFill pref.
+    if (!Prefs.autofill)
+      return false;
+
+    // Then, we should not try to autofill if the behavior is not the default.
+    // TODO (bug 751709): Ideally we should have a more fine-grained behavior
+    // here, but for now it's enough to just check for default behavior.
+    if (Prefs.defaultBehavior != DEFAULT_BEHAVIOR)
+      return false;
+
+    // Don't autoFill if the search term is recognized as a keyword, otherwise
+    // it will override default keywords behavior.  Note that keywords are
+    // hashed on first use, so while the first query may delay a little bit,
+    // next ones will just hit the memory hash.
+    if (this._searchString.length == 0 ||
+        PlacesUtils.bookmarks.getURIForKeyword(this._searchString)) {
+      return false;
+    }
+
+    // Don't try to autofill if the search term includes any whitespace.
+    // This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
+    // tokenizer ends up trimming the search string and returning a value
+    // that doesn't match it, or is even shorter.
+    if (/\s/.test(this._searchString)) {
+      return false;
+    }
+
+    return true;
+  },
+
+  /**
+   * Obtains the query to search for autoFill host results.
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _hostQuery() [
+    Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
+    {
+      query_type: QUERYTYPE_AUTOFILL_HOST,
+      searchString: this._searchString.toLowerCase()
+    }
+  ],
+
+  /**
+   * Obtains the query to search for autoFill url results.
+   *
+   * @return an array consisting of the correctly optimized query to search the
+   *         database with and an object containing the params to bound.
+   */
+  get _urlQuery() [
+    Prefs.autofillTyped ? SQL_TYPED_HOST_QUERY : SQL_TYPED_QUERY,
+    {
+      query_type: QUERYTYPE_AUTOFILL_URL,
+      searchString: this._autofillUrlSearchString,
+      matchBehavior: MATCH_BEGINNING_CASE_SENSITIVE,
+      searchBehavior: Ci.mozIPlacesAutoComplete.BEHAVIOR_URL
+    }
+  ],
+
+ /**
+   * Notifies the listener about results.
+   *
+   * @param searchOngoing
+   *        Indicates whether the search is ongoing.
+   */
+  notifyResults: function (searchOngoing) {
+    let result = this._result;
+    let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+    if (searchOngoing) {
+      resultCode += "_ONGOING";
+    }
+    result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+    this._listener.onSearchResult(this._autocompleteSearch, result);
+  },
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// UnifiedComplete class
+//// component @mozilla.org/autocomplete/search;1?name=unifiedcomplete
+
+function UnifiedComplete() {
+  Services.obs.addObserver(this, TOPIC_SHUTDOWN, true);
+}
+
+UnifiedComplete.prototype = {
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIObserver
+
+  observe: function (subject, topic, data) {
+    if (topic === TOPIC_SHUTDOWN) {
+      this.ensureShutdown();
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// Database handling
+
+  /**
+   * Promise resolved when the database initialization has completed, or null
+   * if it has never been requested.
+   */
+  _promiseDatabase: null,
+
+  /**
+   * Gets a Sqlite database handle.
+   *
+   * @return {Promise}
+   * @resolves to the Sqlite database handle (according to Sqlite.jsm).
+   * @rejects javascript exception.
+   */
+  getDatabaseHandle: function () {
+    if (Prefs.enabled && !this._promiseDatabase) {
+      this._promiseDatabase = Task.spawn(function* () {
+        let conn = yield Sqlite.cloneStorageConnection({
+          connection: PlacesUtils.history.DBConnection,
+          readOnly: true
+        });
+
+        // Autocomplete often fallbacks to a table scan due to lack of text
+        // indices.  A larger cache helps reducing IO and improving performance.
+        // The value used here is larger than the default Storage value defined
+        // as MAX_CACHE_SIZE_BYTES in storage/src/mozStorageConnection.cpp.
+        yield conn.execute("PRAGMA cache_size = -6144"); // 6MiB
+
+        yield SwitchToTabStorage.initDatabase(conn);
+
+        return conn;
+      }.bind(this)).then(null, Cu.reportError);
+    }
+    return this._promiseDatabase;
+  },
+
+  /**
+   * Used to stop running queries and close the database handle.
+   */
+  ensureShutdown: function () {
+    if (this._promiseDatabase) {
+      Task.spawn(function* () {
+        let conn = yield this.getDatabaseHandle();
+        SwitchToTabStorage.shutdown();
+        yield conn.close()
+      }.bind(this)).then(null, Cu.reportError);
+      this._promiseDatabase = null;
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// mozIPlacesAutoComplete
+
+  registerOpenPage: function PAC_registerOpenPage(uri) {
+    SwitchToTabStorage.add(uri);
+  },
+
+  unregisterOpenPage: function PAC_unregisterOpenPage(uri) {
+    SwitchToTabStorage.delete(uri);
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIAutoCompleteSearch
+
+  startSearch: function (searchString, searchParam, previousResult, listener) {
+    // Stop the search in case the controller has not taken care of it.
+    if (this._currentSearch) {
+      this.stopSearch();
+    }
+
+    // Note: We don't use previousResult to make sure ordering of results are
+    //       consistent.  See bug 412730 for more details.
+
+    this._currentSearch = new Search(searchString, searchParam, listener,
+                                     this, this);
+
+    // If we are not enabled, we need to return now.  Notice we need an empty
+    // result regardless, so we still create the Search object.
+    if (!Prefs.enabled) {
+      this.finishSearch(true);
+      return;
+    }
+
+    let search = this._currentSearch;
+    this.getDatabaseHandle().then(conn => search.execute(conn))
+                            .then(() => {
+                              if (search == this._currentSearch) {
+                                this.finishSearch(true);
+                              }
+                            }, Cu.reportError);
+  },
+
+  stopSearch: function () {
+    if (this._currentSearch) {
+      this._currentSearch.cancel();
+    }
+    this.finishSearch();
+  },
+
+  /**
+   * Properly cleans up when searching is completed.
+   *
+   * @param notify [optional]
+   *        Indicates if we should notify the AutoComplete listener about our
+   *        results or not.
+   */
+  finishSearch: function (notify=false) {
+    // Notify about results if we are supposed to.
+    if (notify) {
+      this._currentSearch.notifyResults(false);
+    }
+
+    // Clear our state
+    TelemetryStopwatch.cancel(TELEMETRY_1ST_RESULT);
+    delete this._currentSearch;
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIAutoCompleteSimpleResultListener
+
+  onValueRemoved: function (result, spec, removeFromDB) {
+    if (removeFromDB) {
+      PlacesUtils.history.removePage(NetUtil.newURI(spec));
+    }
+  },
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsIAutoCompleteSearchDescriptor
+
+  get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
+
+  //////////////////////////////////////////////////////////////////////////////
+  //// nsISupports
+
+  classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
+
+  _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIAutoCompleteSearch,
+    Ci.nsIAutoCompleteSimpleResultListener,
+    Ci.mozIPlacesAutoComplete,
+    Ci.nsIObserver,
+    Ci.nsISupportsWeakReference
+  ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UnifiedComplete]);
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/UnifiedComplete.manifest
@@ -0,0 +1,2 @@
+component {f964a319-397a-4d21-8be6-5cdd1ee3e3ae} UnifiedComplete.js
+contract @mozilla.org/autocomplete/search;1?name=unifiedcomplete {f964a319-397a-4d21-8be6-5cdd1ee3e3ae}
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -82,12 +82,14 @@ if CONFIG['MOZ_PLACES']:
         'nsTaggingService.js',
         'PlacesCategoriesStarter.js',
         'toolkitplaces.manifest',
     ]
     if CONFIG['MOZ_XUL']:
         EXTRA_COMPONENTS += [
             'nsPlacesAutoComplete.js',
             'nsPlacesAutoComplete.manifest',
+            'UnifiedComplete.js',
+            'UnifiedComplete.manifest',
         ]
     FINAL_LIBRARY = 'xul'
 
 include('/ipc/chromium/chromium-config.mozbuild')
--- a/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
+++ b/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
@@ -6,56 +6,56 @@ Cu.import("resource://gre/modules/Priori
 
 function run_test() {
   run_next_test();
 }
 
 add_task(function* search_engine_match() {
   let engine = yield promiseDefaultSearchEngine();
   let token = engine.getResultDomain();
-  let match = yield PriorityUrlProvider.getMatchingSpec(token.substr(0, 1));
+  let match = yield PriorityUrlProvider.getMatch(token.substr(0, 1));
   do_check_eq(match.url, engine.searchForm);
   do_check_eq(match.title, engine.name);
   do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
   do_check_eq(match.reason, "search");
 });
 
 add_task(function* no_match() {
-  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("test"));
+  do_check_eq(null, yield PriorityUrlProvider.getMatch("test"));
 });
 
 add_task(function* hide_search_engine_nomatch() {
   let engine = yield promiseDefaultSearchEngine();
   let token = engine.getResultDomain();
   let promiseTopic = promiseSearchTopic("engine-changed");
   Services.search.removeEngine(engine);
   yield promiseTopic;
   do_check_true(engine.hidden);
-  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec(token.substr(0, 1)));
+  do_check_eq(null, yield PriorityUrlProvider.getMatch(token.substr(0, 1)));
 });
 
 add_task(function* add_search_engine_match() {
   let promiseTopic = promiseSearchTopic("engine-added");
-  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("bacon"));
+  do_check_eq(null, yield PriorityUrlProvider.getMatch("bacon"));
   Services.search.addEngineWithDetails("bacon", "", "bacon", "Search Bacon",
                                        "GET", "http://www.bacon.moz/?search={searchTerms}");
   yield promiseSearchTopic;
-  let match = yield PriorityUrlProvider.getMatchingSpec("bacon");
+  let match = yield PriorityUrlProvider.getMatch("bacon");
   do_check_eq(match.url, "http://www.bacon.moz");
   do_check_eq(match.title, "bacon");
   do_check_eq(match.iconUrl, null);
   do_check_eq(match.reason, "search");
 });
 
 add_task(function* remove_search_engine_nomatch() {
   let engine = Services.search.getEngineByName("bacon");
   let promiseTopic = promiseSearchTopic("engine-removed");
   Services.search.removeEngine(engine);
   yield promiseTopic;
-  do_check_eq(null, yield PriorityUrlProvider.getMatchingSpec("bacon"));
+  do_check_eq(null, yield PriorityUrlProvider.getMatch("bacon"));
 });
 
 function promiseDefaultSearchEngine() {
   let deferred = Promise.defer();
   Services.search.init( () => {
     deferred.resolve(Services.search.defaultEngine);
   });
   return deferred.promise;
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -40,16 +40,22 @@ const PREF_EM_CERT_CHECKATTRIBUTES    = 
 const PREF_EM_HOTFIX_CERTS            = "extensions.hotfix.certs.";
 const PREF_MATCH_OS_LOCALE            = "intl.locale.matchOS";
 const PREF_SELECTED_LOCALE            = "general.useragent.locale";
 const UNKNOWN_XPCOM_ABI               = "unknownABI";
 
 const UPDATE_REQUEST_VERSION          = 2;
 const CATEGORY_UPDATE_PARAMS          = "extension-update-params";
 
+const XMLURI_BLOCKLIST                = "http://www.mozilla.org/2006/addons-blocklist";
+
+const KEY_PROFILEDIR                  = "ProfD";
+const KEY_APPDIR                      = "XCurProcD";
+const FILE_BLOCKLIST                  = "blocklist.xml";
+
 const BRANCH_REGEXP                   = /^([^\.]+\.[0-9]+[a-z]*).*/gi;
 const PREF_EM_CHECK_COMPATIBILITY_BASE = "extensions.checkCompatibility";
 #ifdef MOZ_COMPATIBILITY_NIGHTLY
 var PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly";
 #else
 var PREF_EM_CHECK_COMPATIBILITY;
 #endif
 
@@ -60,16 +66,18 @@ const VALID_TYPES_REGEXP = /^[\w\-]+$/;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+                                  "resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
   let certUtils = {};
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
 
@@ -516,16 +524,98 @@ var AddonManagerInternal = {
       return this.getPropertyNames();
     }
   }),
 
   recordTimestamp: function AMI_recordTimestamp(name, value) {
     this.TelemetryTimestamps.add(name, value);
   },
 
+  validateBlocklist: function AMI_validateBlocklist() {
+    let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+
+    // If there is no application shipped blocklist then there is nothing to do
+    if (!appBlocklist.exists())
+      return;
+
+    let profileBlocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+
+    // If there is no blocklist in the profile then copy the application shipped
+    // one there
+    if (!profileBlocklist.exists()) {
+      try {
+        appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST);
+      }
+      catch (e) {
+        logger.warn("Failed to copy the application shipped blocklist to the profile", e);
+      }
+      return;
+    }
+
+    let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+                     createInstance(Ci.nsIFileInputStream);
+    try {
+      let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].
+                    createInstance(Ci.nsIConverterInputStream);
+      fileStream.init(appBlocklist, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+      cstream.init(fileStream, "UTF-8", 0, 0);
+
+      let data = "";
+      let str = {};
+      let read = 0;
+      do {
+        read = cstream.readString(0xffffffff, str);
+        data += str.value;
+      } while (read != 0);
+
+      let parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+                   createInstance(Ci.nsIDOMParser);
+      var doc = parser.parseFromString(data, "text/xml");
+    }
+    catch (e) {
+      logger.warn("Application shipped blocklist could not be loaded", e);
+      return;
+    }
+    finally {
+      try {
+        fileStream.close();
+      }
+      catch (e) {
+        logger.warn("Unable to close blocklist file stream", e);
+      }
+    }
+
+    // If the namespace is incorrect then ignore the application shipped
+    // blocklist
+    if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
+      logger.warn("Application shipped blocklist has an unexpected namespace (" +
+                  doc.documentElement.namespaceURI + ")");
+      return;
+    }
+
+    // If there is no lastupdate information then ignore the application shipped
+    // blocklist
+    if (!doc.documentElement.hasAttribute("lastupdate"))
+      return;
+
+    // If the application shipped blocklist is older than the profile blocklist
+    // then do nothing
+    if (doc.documentElement.getAttribute("lastupdate") <=
+        profileBlocklist.lastModifiedTime)
+      return;
+
+    // Otherwise copy the application shipped blocklist to the profile
+    try {
+      appBlocklist.copyTo(profileBlocklist.parent, FILE_BLOCKLIST);
+    }
+    catch (e) {
+      logger.warn("Failed to copy the application shipped blocklist to the profile", e);
+    }
+  },
+
   /**
    * Initializes the AddonManager, loading any known providers and initializing
    * them.
    */
   startup: function AMI_startup() {
     try {
       if (gStarted)
         return;
@@ -554,16 +644,17 @@ var AddonManagerInternal = {
       if (appChanged !== false) {
         logger.debug("Application has been upgraded");
         Services.prefs.setCharPref(PREF_EM_LAST_APP_VERSION,
                                    Services.appinfo.version);
         Services.prefs.setCharPref(PREF_EM_LAST_PLATFORM_VERSION,
                                    Services.appinfo.platformVersion);
         Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION,
                                   (appChanged === undefined ? 0 : -1));
+        this.validateBlocklist();
       }
 
 #ifndef MOZ_COMPATIBILITY_NIGHTLY
       PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE + "." +
                                     Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
 #endif
 
       try {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+  <emItems>
+    <emItem  blockID="i454" id="ancient@tests.mozilla.org">
+      <versionRange  minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+  </emItems>
+</blocklist>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1396046918000">
+  <emItems>
+    <emItem  blockID="i454" id="new@tests.mozilla.org">
+      <versionRange  minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+  </emItems>
+</blocklist>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1296046918000">
+  <emItems>
+    <emItem  blockID="i454" id="old@tests.mozilla.org">
+      <versionRange  minVersion="0" maxVersion="*" severity="3"/>
+    </emItem>
+  </emItems>
+</blocklist>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const KEY_PROFILEDIR                  = "ProfD";
+const KEY_APPDIR                      = "XCurProcD";
+const FILE_BLOCKLIST                  = "blocklist.xml";
+
+const PREF_BLOCKLIST_ENABLED          = "extensions.blocklist.enabled";
+
+const OLD = do_get_file("data/test_overrideblocklist/old.xml");
+const NEW = do_get_file("data/test_overrideblocklist/new.xml");
+const ANCIENT = do_get_file("data/test_overrideblocklist/ancient.xml");
+const OLD_TSTAMP = 1296046918000;
+const NEW_TSTAMP = 1396046918000;
+
+const gAppDir = FileUtils.getFile(KEY_APPDIR, []);
+
+let oldAddon = {
+  id: "old@tests.mozilla.org",
+  version: 1
+}
+let newAddon = {
+  id: "new@tests.mozilla.org",
+  version: 1
+}
+let ancientAddon = {
+  id: "ancient@tests.mozilla.org",
+  version: 1
+}
+let invalidAddon = {
+  id: "invalid@tests.mozilla.org",
+  version: 1
+}
+
+function incrementAppVersion() {
+  gAppInfo.version = "" + (parseInt(gAppInfo.version) + 1);
+}
+
+function clearBlocklists() {
+  let blocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+  if (blocklist.exists())
+    blocklist.remove(true);
+
+  blocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+  if (blocklist.exists())
+    blocklist.remove(true);
+}
+
+function reloadBlocklist() {
+  Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, false);
+  Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, true);
+}
+
+function copyToApp(file) {
+  file.clone().copyTo(gAppDir, FILE_BLOCKLIST);
+}
+
+function copyToProfile(file, tstamp) {
+  file = file.clone();
+  file.copyTo(gProfD, FILE_BLOCKLIST);
+  file = gProfD.clone();
+  file.append(FILE_BLOCKLIST);
+  file.lastModifiedTime = tstamp;
+}
+
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+  let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+  if (appBlocklist.exists()) {
+    try {
+      appBlocklist.moveTo(gAppDir, "blocklist.old");
+    }
+    catch (e) {
+      todo(false, "Aborting test due to unmovable blocklist file: " + e);
+      return;
+    }
+    do_register_cleanup(function() {
+      clearBlocklists();
+      appBlocklist.moveTo(gAppDir, FILE_BLOCKLIST);
+    });
+  }
+
+  run_next_test();
+}
+
+// On first run whataver is in the app dir should get copied to the profile
+add_test(function test_copy() {
+  clearBlocklists();
+  copyToApp(OLD);
+
+  incrementAppVersion();
+  startupManager();
+
+  reloadBlocklist();
+  let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+                  getService(AM_Ci.nsIBlocklistService);
+  do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+  do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+  do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+  do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+  shutdownManager();
+
+  run_next_test();
+});
+
+// An ancient blocklist should be ignored
+add_test(function test_ancient() {
+  clearBlocklists();
+  copyToApp(ANCIENT);
+  copyToProfile(OLD, OLD_TSTAMP);
+
+  incrementAppVersion();
+  startupManager();
+
+  reloadBlocklist();
+  let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+                  getService(AM_Ci.nsIBlocklistService);
+  do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+  do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+  do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+  do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+  shutdownManager();
+
+  run_next_test();
+});
+
+// A new blocklist should override an old blocklist
+add_test(function test_override() {
+  clearBlocklists();
+  copyToApp(NEW);
+  copyToProfile(OLD, OLD_TSTAMP);
+
+  incrementAppVersion();
+  startupManager();
+
+  reloadBlocklist();
+  let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+                  getService(AM_Ci.nsIBlocklistService);
+  do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+  do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+  do_check_false(blocklist.isAddonBlocklisted(oldAddon));
+  do_check_true(blocklist.isAddonBlocklisted(newAddon));
+
+  shutdownManager();
+
+  run_next_test();
+});
+
+// An old blocklist shouldn't override a new blocklist
+add_test(function test_retain() {
+  clearBlocklists();
+  copyToApp(OLD);
+  copyToProfile(NEW, NEW_TSTAMP);
+
+  incrementAppVersion();
+  startupManager();
+
+  reloadBlocklist();
+  let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+                  getService(AM_Ci.nsIBlocklistService);
+  do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+  do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+  do_check_false(blocklist.isAddonBlocklisted(oldAddon));
+  do_check_true(blocklist.isAddonBlocklisted(newAddon));
+
+  shutdownManager();
+
+  run_next_test();
+});
+
+// A missing blocklist in the profile should still load an app-shipped blocklist
+add_test(function test_missing() {
+  clearBlocklists();
+  copyToApp(OLD);
+  copyToProfile(NEW, NEW_TSTAMP);
+
+  incrementAppVersion();
+  startupManager();
+  shutdownManager();
+
+  let blocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+  blocklist.remove(true);
+  startupManager(false);
+
+  reloadBlocklist();
+  let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+                  getService(AM_Ci.nsIBlocklistService);
+  do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+  do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+  do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+  do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+  shutdownManager();
+
+  run_next_test();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -257,9 +257,11 @@ run-sequentially = Uses hardcoded ports 
 [test_upgrade.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses global XCurProcD dir.
 [test_upgrade_strictcompat.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 run-sequentially = Uses global XCurProcD dir.
+[test_overrideblocklist.js]
+run-sequentially = Uses global XCurProcD dir.
 [test_sourceURI.js]