diff --git a/addon-sdk/source/examples/reading-data/lib/main.js b/addon-sdk/source/examples/reading-data/lib/main.js index 71c8be6b6ad..468a497b12e 100644 --- a/addon-sdk/source/examples/reading-data/lib/main.js +++ b/addon-sdk/source/examples/reading-data/lib/main.js @@ -1,10 +1,11 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"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"); @@ -21,20 +22,18 @@ exports.main = function(options, callbacks) { 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 @@ -42,3 +41,13 @@ exports.main = function(options, callbacks) { if (options.staticArgs.quitWhenDone) callbacks.quit(); } + +function handleChange(state) { + if (state.checked) { + myPanel.show({ position: button }); + } +} + +function handleHide() { + button.state('window', { checked: false }); +} diff --git a/addon-sdk/source/examples/reading-data/tests/test-main.js b/addon-sdk/source/examples/reading-data/tests/test-main.js index 1e6a18e46bb..4e85f49de4a 100644 --- a/addon-sdk/source/examples/reading-data/tests/test-main.js +++ b/addon-sdk/source/examples/reading-data/tests/test-main.js @@ -3,9 +3,6 @@ * 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"); @@ -26,4 +23,3 @@ exports.testID = function(test) { test.assertEqual(self.data.url("sample.html"), "resource://reading-data-example-at-jetpack-dot-mozillalabs-dot-com/reading-data/data/sample.html"); }; -*/ diff --git a/addon-sdk/source/examples/reddit-panel/lib/main.js b/addon-sdk/source/examples/reddit-panel/lib/main.js index 95da781ca95..0d25ba65a43 100644 --- a/addon-sdk/source/examples/reddit-panel/lib/main.js +++ b/addon-sdk/source/examples/reddit-panel/lib/main.js @@ -1,26 +1,34 @@ /* 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) { @@ -29,3 +37,13 @@ exports.main = function(options, callbacks) { if (options.staticArgs.quitWhenDone) callbacks.quit(); }; + +function handleChange(state) { + if (state.checked) { + reddit_panel.show({ position: button }); + } +} + +function handleHide() { + button.state('window', { checked: false }); +} diff --git a/addon-sdk/source/examples/reddit-panel/tests/test-main.js b/addon-sdk/source/examples/reddit-panel/tests/test-main.js index f13003e7654..6d916d25c17 100644 --- a/addon-sdk/source/examples/reddit-panel/tests/test-main.js +++ b/addon-sdk/source/examples/reddit-panel/tests/test-main.js @@ -3,9 +3,6 @@ * 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"); @@ -23,4 +20,3 @@ exports.testMain = function(test) { exports.testData = function(test) { test.assert(self.data.load("panel.js").length > 0); }; -*/ diff --git a/addon-sdk/source/lib/sdk/content/content-worker.js b/addon-sdk/source/lib/sdk/content/content-worker.js index 23ddd5cafec..0d7d675c285 100644 --- a/addon-sdk/source/lib/sdk/content/content-worker.js +++ b/addon-sdk/source/lib/sdk/content/content-worker.js @@ -107,7 +107,9 @@ const ContentWorker = Object.freeze({ 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") }); }, diff --git a/addon-sdk/source/lib/sdk/content/mod.js b/addon-sdk/source/lib/sdk/content/mod.js index 6ce0c7608d3..f46ac49b1f0 100644 --- a/addon-sdk/source/lib/sdk/content/mod.js +++ b/addon-sdk/source/lib/sdk/content/mod.js @@ -31,6 +31,9 @@ let detachFrom = method("detatchFrom"); exports.detachFrom = detachFrom; function attach(modification, target) { + if (!modification) + return; + let window = getTargetWindow(target); attachTo(modification, window); @@ -42,6 +45,9 @@ function attach(modification, target) { exports.attach = attach; function detach(modification, target) { + if (!modification) + return; + if (target) { let window = getTargetWindow(target); detachFrom(modification, window); diff --git a/addon-sdk/source/lib/sdk/loader/sandbox.js b/addon-sdk/source/lib/sdk/loader/sandbox.js index 3d715ac178d..2edb71f90ce 100644 --- a/addon-sdk/source/lib/sdk/loader/sandbox.js +++ b/addon-sdk/source/lib/sdk/loader/sandbox.js @@ -67,3 +67,8 @@ function load(sandbox, uri) { } } exports.load = load; + +/** + * Forces the given `sandbox` to be freed immediately. + */ +exports.nuke = Cu.nukeSandbox diff --git a/addon-sdk/source/lib/sdk/page-mod.js b/addon-sdk/source/lib/sdk/page-mod.js index b13a176f9df..f706b1e4c10 100644 --- a/addon-sdk/source/lib/sdk/page-mod.js +++ b/addon-sdk/source/lib/sdk/page-mod.js @@ -201,6 +201,10 @@ function createWorker (mod, 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); diff --git a/addon-sdk/source/lib/sdk/panel.js b/addon-sdk/source/lib/sdk/panel.js index 274c2c50a62..5071ea828c4 100644 --- a/addon-sdk/source/lib/sdk/panel.js +++ b/addon-sdk/source/lib/sdk/panel.js @@ -18,7 +18,7 @@ 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'); @@ -34,6 +34,8 @@ 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)); @@ -63,7 +65,16 @@ let displayContract = contract({ 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); @@ -72,12 +83,13 @@ 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. @@ -125,6 +137,12 @@ const Panel = Class({ }, 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(); @@ -148,7 +166,8 @@ const Panel = Class({ this.hide(); off(this); - destroy(workerFor(this)); + workerFor(this).destroy(); + detach(styleFor(this)); domPanel.dispose(viewFor(this)); @@ -177,7 +196,7 @@ const Panel = Class({ 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 */ @@ -262,12 +281,25 @@ let hides = filter(panelEvents, ({type}) => type === "popuphidden"); 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); }); diff --git a/addon-sdk/source/lib/sdk/system.js b/addon-sdk/source/lib/sdk/system.js index f97522e14fb..afef91bab89 100644 --- a/addon-sdk/source/lib/sdk/system.js +++ b/addon-sdk/source/lib/sdk/system.js @@ -109,17 +109,21 @@ exports.pathFor = function pathFor(id) { */ 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". diff --git a/addon-sdk/source/lib/sdk/system/events.js b/addon-sdk/source/lib/sdk/system/events.js index 0e6ad8fa75c..9e2e826088b 100644 --- a/addon-sdk/source/lib/sdk/system/events.js +++ b/addon-sdk/source/lib/sdk/system/events.js @@ -8,12 +8,13 @@ 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, @@ -94,6 +95,10 @@ function on(type, listener, strong) { 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; @@ -120,6 +125,31 @@ function off(type, listener) { 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); diff --git a/addon-sdk/source/lib/sdk/test/loader.js b/addon-sdk/source/lib/sdk/test/loader.js index 26c956d9e01..71c4d363216 100644 --- a/addon-sdk/source/lib/sdk/test/loader.js +++ b/addon-sdk/source/lib/sdk/test/loader.js @@ -62,9 +62,11 @@ exports.LoaderWithHookedConsole = function (module, callback) { 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" } } }), @@ -105,9 +107,11 @@ exports.LoaderWithFilteredConsole = function (module, callback) { 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" } } }); diff --git a/addon-sdk/source/lib/sdk/ui/button/contract.js b/addon-sdk/source/lib/sdk/ui/button/contract.js index fa09a7c93f3..b6cd5e3c1d9 100644 --- a/addon-sdk/source/lib/sdk/ui/button/contract.js +++ b/addon-sdk/source/lib/sdk/ui/button/contract.js @@ -8,6 +8,7 @@ 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). @@ -16,6 +17,7 @@ function isIconSet(icons) { 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.' diff --git a/addon-sdk/source/lib/sdk/ui/sidebar.js b/addon-sdk/source/lib/sdk/ui/sidebar.js index ade6e1146de..763e984fa41 100644 --- a/addon-sdk/source/lib/sdk/ui/sidebar.js +++ b/addon-sdk/source/lib/sdk/ui/sidebar.js @@ -253,9 +253,11 @@ const Sidebar = Class({ remove(sidebars, this); // stop tracking windows - internals.tracker.unload(); - internals.tracker = null; + if (internals.tracker) { + internals.tracker.unload(); + } + internals.tracker = null; internals.windowNS = null; views.delete(this); diff --git a/addon-sdk/source/test/fixtures/pagemod-css-include-file.css b/addon-sdk/source/test/fixtures/css-include-file.css similarity index 100% rename from addon-sdk/source/test/fixtures/pagemod-css-include-file.css rename to addon-sdk/source/test/fixtures/css-include-file.css diff --git a/addon-sdk/source/test/test-content-worker.js b/addon-sdk/source/test/test-content-worker.js index fd7d7f87590..dbb514b6af9 100644 --- a/addon-sdk/source/test/test-content-worker.js +++ b/addon-sdk/source/test/test-content-worker.js @@ -382,8 +382,8 @@ exports["test:ensure console.xxx works in cs"] = WorkerTest( 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); } @@ -391,19 +391,23 @@ exports["test:ensure console.xxx works in cs"] = WorkerTest( 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(); } }); diff --git a/addon-sdk/source/test/test-page-mod.js b/addon-sdk/source/test/test-page-mod.js index 29b51e9ab78..af8e1ca00da 100644 --- a/addon-sdk/source/test/test-page-mod.js +++ b/addon-sdk/source/test/test-page-mod.js @@ -957,7 +957,7 @@ exports.testPageModCss = function(assert, done) { 'data:text/html;charset=utf-8,
css test
', [{ 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"); @@ -1531,4 +1531,32 @@ exports.testDetachOnUnload = function(assert, done) { }) } +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); diff --git a/addon-sdk/source/test/test-panel.js b/addon-sdk/source/test/test-panel.js index 6c2d21c88d5..479426060be 100644 --- a/addon-sdk/source/test/test-panel.js +++ b/addon-sdk/source/test/test-panel.js @@ -25,6 +25,8 @@ 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) { @@ -974,6 +976,88 @@ exports['test panel can be constructed without any arguments'] = function (asser 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,' + + '
css test
', + 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,' + + '
css test
', + 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(); diff --git a/addon-sdk/source/test/test-sandbox.js b/addon-sdk/source/test/test-sandbox.js index 9f5ee4746d6..d7f2e5ac6e5 100644 --- a/addon-sdk/source/test/test-sandbox.js +++ b/addon-sdk/source/test/test-sandbox.js @@ -2,7 +2,7 @@ * 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/'; @@ -137,4 +137,30 @@ exports['test metadata'] = function(assert) { 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); diff --git a/addon-sdk/source/test/test-system-events.js b/addon-sdk/source/test/test-system-events.js index 64aa5493f9d..1e66d1ae9d2 100644 --- a/addon-sdk/source/test/test-system-events.js +++ b/addon-sdk/source/test/test-system-events.js @@ -119,6 +119,30 @@ exports["test listeners are GC-ed"] = function(assert, 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); diff --git a/addon-sdk/source/test/test-system-runtime.js b/addon-sdk/source/test/test-system-runtime.js index de47fcb133a..ed911bfdc6b 100644 --- a/addon-sdk/source/test/test-system-runtime.js +++ b/addon-sdk/source/test/test-system-runtime.js @@ -3,7 +3,7 @@ * 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", @@ -14,7 +14,7 @@ exports["test system runtime"] = function(assert) { "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"); }; diff --git a/addon-sdk/source/test/test-system.js b/addon-sdk/source/test/test-system.js new file mode 100644 index 00000000000..0461d98d141 --- /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); diff --git a/addon-sdk/source/test/test-ui-action-button.js b/addon-sdk/source/test/test-ui-action-button.js index f6a766401f0..081fbb3fbfa 100644 --- a/addon-sdk/source/test/test-ui-action-button.js +++ b/addon-sdk/source/test/test-ui-action-button.js @@ -835,6 +835,44 @@ exports['test button state are snapshot'] = function(assert) { 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'); diff --git a/addon-sdk/source/test/test-ui-toggle-button.js b/addon-sdk/source/test/test-ui-toggle-button.js index 2eb66b1b181..71a6170593b 100644 --- a/addon-sdk/source/test/test-ui-toggle-button.js +++ b/addon-sdk/source/test/test-ui-toggle-button.js @@ -844,6 +844,44 @@ exports['test button state are snapshot'] = function(assert) { 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'); diff --git a/b2g/chrome/content/desktop.js b/b2g/chrome/content/desktop.js index 66256982643..3e0d0234e54 100644 --- a/b2g/chrome/content/desktop.js +++ b/b2g/chrome/content/desktop.js @@ -52,6 +52,10 @@ function checkDebuggerPort() { // DebuggerServer.openListener detects that it isn't a file path (string), // and starts listening on the tcp port given here as command line argument. + if (!window.arguments) { + return; + } + // Get the command line arguments that were passed to the b2g client let args = window.arguments[0].QueryInterface(Ci.nsICommandLine); diff --git a/b2g/chrome/content/runapp.js b/b2g/chrome/content/runapp.js index 59340763db5..6df206fb1cb 100644 --- a/b2g/chrome/content/runapp.js +++ b/b2g/chrome/content/runapp.js @@ -5,6 +5,10 @@ let runAppObj; window.addEventListener('load', function() { + if (!window.arguments) { + return; + } + // Get the command line arguments that were passed to the b2g client let args = window.arguments[0].QueryInterface(Ci.nsICommandLine); let appname; diff --git a/b2g/chrome/content/screen.js b/b2g/chrome/content/screen.js index f20f6579764..1f5eb826673 100644 --- a/b2g/chrome/content/screen.js +++ b/b2g/chrome/content/screen.js @@ -57,12 +57,19 @@ window.addEventListener('ContentStart', function() { }; // Get the command line arguments that were passed to the b2g client - let args = window.arguments[0].QueryInterface(Ci.nsICommandLine); - let screenarg; + let args; + try { + // On Firefox Mulet, we don't always have a command line argument + args = window.arguments[0].QueryInterface(Ci.nsICommandLine); + } catch(e) {} + + let screenarg = null; // Get the --screen argument from the command line try { - screenarg = args.handleFlagWithParam('screen', false); + if (args) { + screenarg = args.handleFlagWithParam('screen', false); + } // If there isn't one, use the default screen if (screenarg === null) diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 90a0aca1e3d..6cad5063ebb 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 194f6436ba7..a904da8d71e 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 0c9b7979fe0..b0dc35ad1fb 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 90a0aca1e3d..6cad5063ebb 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 2182504f9fa..cb30e848d83 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -18,7 +18,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index e1f051e5e96..e725b098f49 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "a54e097e9e3384d885c0116d9c4ca15c1e1cd75e", + "revision": "c766bc0d49af19f18788ad8ed0542b82db81f1d8", "repo_path": "/integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index b8c37aae1e8..37c2ed9c8bc 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index be3db5be4d7..76eb1e7218e 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/inari/sources.xml b/b2g/config/inari/sources.xml index d33d85aecaa..4f919d763cd 100644 --- a/b2g/config/inari/sources.xml +++ b/b2g/config/inari/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/leo/sources.xml b/b2g/config/leo/sources.xml index 27858e072bd..7b8c00a8fc6 100644 --- a/b2g/config/leo/sources.xml +++ b/b2g/config/leo/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/mako/sources.xml b/b2g/config/mako/sources.xml index 4da2cd2bd1d..4307d436530 100644 --- a/b2g/config/mako/sources.xml +++ b/b2g/config/mako/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index b18c39486fa..d0c0fb3b8ea 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index 7a39d31c7f1..7407e9c1721 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -183,7 +183,6 @@ @BINPATH@/components/dom_notification.xpt @BINPATH@/components/dom_html.xpt @BINPATH@/components/dom_indexeddb.xpt -@BINPATH@/components/dom_inputmethod.xpt @BINPATH@/components/dom_offline.xpt @BINPATH@/components/dom_payment.xpt @BINPATH@/components/dom_json.xpt @@ -457,6 +456,8 @@ @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 diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css index 52de7c0cd9c..09e7c030370 100644 --- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -122,7 +122,6 @@ tabbrowser { 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. */ @@ -131,15 +130,28 @@ tabbrowser { 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; } diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index a91f8c1ee8c..87e7a01c86a 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -81,6 +81,10 @@ Components.classes["@mozilla.org/autocomplete/search;1?name=history"] .getService(Components.interfaces.mozIPlacesAutoComplete); + + Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"] + .getService(Components.interfaces.mozIPlacesAutoComplete); + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); @@ -744,8 +748,10 @@ } 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 @@ -754,6 +760,7 @@ (!PrivateBrowsingUtils.isWindowPrivate(window) || PrivateBrowsingUtils.permanentPrivateBrowsing)) { autocomplete.registerOpenPage(aLocation); + unifiedComplete.registerOpenPage(aLocation); this.mBrowser.registeredOpenURI = aLocation; } } @@ -1672,12 +1679,6 @@ // 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)); } @@ -1863,13 +1864,6 @@ 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") { @@ -1969,6 +1963,7 @@ if (browser.registeredOpenURI && !aTabWillBeMoved) { this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI); + this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI); delete browser.registeredOpenURI; } @@ -2295,6 +2290,7 @@ // 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; } @@ -3104,6 +3100,7 @@ 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]); diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index 1468efb592c..1208022789d 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -68,6 +68,12 @@ 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"); @@ -599,6 +605,14 @@ 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"); } } ]]> diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm index b1b98af700e..4f4072b11ba 100644 --- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -386,6 +386,13 @@ let CustomizableUIInternal = { 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); @@ -458,6 +465,7 @@ let CustomizableUIInternal = { if (gDirtyAreaCache.has(area)) { this.buildArea(area, placements, aToolbar); } + this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); aToolbar.setAttribute("currentset", placements.join(",")); } finally { this.endBatchUpdate(); @@ -699,6 +707,8 @@ let CustomizableUIInternal = { 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") { @@ -839,6 +849,8 @@ let CustomizableUIInternal = { 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; @@ -2502,6 +2514,17 @@ this.CustomizableUI = { */ 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: @@ -2603,6 +2626,15 @@ this.CustomizableUI = { * - 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); diff --git a/browser/components/customizableui/src/CustomizeMode.jsm b/browser/components/customizableui/src/CustomizeMode.jsm index 64bce142782..b61ac47ed1c 100644 --- a/browser/components/customizableui/src/CustomizeMode.jsm +++ b/browser/components/customizableui/src/CustomizeMode.jsm @@ -236,11 +236,7 @@ CustomizeMode.prototype = { 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); @@ -402,11 +398,7 @@ CustomizeMode.prototype = { 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(); @@ -900,11 +892,7 @@ CustomizeMode.prototype = { 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)); @@ -915,6 +903,14 @@ CustomizeMode.prototype = { }.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)) { @@ -923,6 +919,14 @@ CustomizeMode.prototype = { } }, + _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)) { @@ -939,11 +943,7 @@ CustomizeMode.prototype = { 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); }, @@ -1118,6 +1118,25 @@ CustomizeMode.prototype = { } }, + 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) { diff --git a/browser/components/customizableui/src/DragPositionManager.jsm b/browser/components/customizableui/src/DragPositionManager.jsm index 01e34d3ac5d..d2ac98c4519 100644 --- a/browser/components/customizableui/src/DragPositionManager.jsm +++ b/browser/components/customizableui/src/DragPositionManager.jsm @@ -393,6 +393,22 @@ let DragPositionManager = { } }, + 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(); }, diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index d4c28e79516..e7ae2d498e9 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -104,5 +104,6 @@ skip-if = os == "linux" [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] diff --git a/browser/components/customizableui/test/browser_993322_widget_notoolbar.js b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js index b40e296840f..9264eb78a43 100644 --- a/browser/components/customizableui/test/browser_993322_widget_notoolbar.js +++ b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js @@ -31,4 +31,6 @@ add_task(function*() { CustomizableUI.destroyWidget(BUTTONID); CustomizableUI.unregisterArea(TOOLBARID, true); + toolbar.remove(); + gAddedToolbars.clear(); }); diff --git a/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js new file mode 100644 index 00000000000..f10f8fd17ea --- /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."); +}); diff --git a/browser/components/sessionstore/test/browser.ini b/browser/components/sessionstore/test/browser.ini index 9668f1c1a8d..2d2e125b67e 100644 --- a/browser/components/sessionstore/test/browser.ini +++ b/browser/components/sessionstore/test/browser.ini @@ -145,6 +145,8 @@ skip-if = true [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] @@ -184,8 +186,6 @@ skip-if = true skip-if = true # Disabled on OS X: -[browser_597071.js] -skip-if = os == "mac" || e10s [browser_625016.js] skip-if = os == "mac" || e10s diff --git a/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js b/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js index 23de133268c..f077a893611 100644 --- a/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js +++ b/browser/components/sessionstore/test/browser_615394-SSWindowState_events.js @@ -2,7 +2,7 @@ * 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: [ @@ -76,8 +76,7 @@ function runNextTest() { }); } else { - ss.setBrowserState(stateBackup); - finish(); + waitForBrowserState(stateBackup, finish); } } diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 292e9e9b429..be8962d4e87 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -203,7 +203,6 @@ @BINPATH@/components/dom_notification.xpt @BINPATH@/components/dom_html.xpt @BINPATH@/components/dom_indexeddb.xpt -@BINPATH@/components/dom_inputmethod.xpt @BINPATH@/components/dom_offline.xpt @BINPATH@/components/dom_json.xpt @BINPATH@/components/dom_power.xpt @@ -430,6 +429,8 @@ @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 diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index d486b5e0f6a..e904475801b 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -88,7 +88,7 @@ margin-top: @windowButtonMarginTop@; } -#main-window[customizing] > #titlebar { +#main-window[customize-entered] > #titlebar { -moz-appearance: none; } @@ -2847,6 +2847,16 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { 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 */ diff --git a/browser/themes/shared/tabs.inc.css b/browser/themes/shared/tabs.inc.css index 57ac7d9f901..4e9ad8e94e1 100644 --- a/browser/themes/shared/tabs.inc.css +++ b/browser/themes/shared/tabs.inc.css @@ -226,20 +226,6 @@ /* 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 { diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css index 4d562962cc1..08fff4b5c1d 100644 --- a/browser/themes/windows/browser.css +++ b/browser/themes/windows/browser.css @@ -1902,6 +1902,16 @@ toolbarbutton[type="socialmark"] > .toolbarbutton-icon { 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); diff --git a/dom/bluetooth/BluetoothCommon.h b/dom/bluetooth/BluetoothCommon.h index 94a75c97b50..e0aa8f5f751 100644 --- a/dom/bluetooth/BluetoothCommon.h +++ b/dom/bluetooth/BluetoothCommon.h @@ -57,6 +57,25 @@ extern bool gBluetoothDebugFlag; #define BT_WARNING(msg, ...) printf("%s: " msg, __FUNCTION__, ##__VA_ARGS__)) #endif +/** + * Wrap literal name and value into a BluetoothNamedValue + * and append it to the array. + */ +#define BT_APPEND_NAMED_VALUE(array, name, value) \ + array.AppendElement(BluetoothNamedValue(NS_LITERAL_STRING(name), value)) + +/** + * Ensure success of system message broadcast with void return. + */ +#define BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters) \ + do { \ + if (!BroadcastSystemMessage(type, parameters)) { \ + BT_WARNING("Failed to broadcast [%s]", \ + NS_ConvertUTF16toUTF8(type).get()); \ + return; \ + } \ + } while(0) + #define BEGIN_BLUETOOTH_NAMESPACE \ namespace mozilla { namespace dom { namespace bluetooth { #define END_BLUETOOTH_NAMESPACE \ diff --git a/dom/bluetooth/BluetoothHidManager.cpp b/dom/bluetooth/BluetoothHidManager.cpp index f81f3da51f8..5c8a5c7ecd7 100644 --- a/dom/bluetooth/BluetoothHidManager.cpp +++ b/dom/bluetooth/BluetoothHidManager.cpp @@ -241,18 +241,10 @@ BluetoothHidManager::NotifyStatusChanged() NS_NAMED_LITERAL_STRING(type, BLUETOOTH_HID_STATUS_CHANGED_ID); InfallibleTArray parameters; - BluetoothValue v = mConnected; - parameters.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("connected"), v)); + BT_APPEND_NAMED_VALUE(parameters, "connected", mConnected); + BT_APPEND_NAMED_VALUE(parameters, "address", mDeviceAddress); - v = mDeviceAddress; - parameters.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("address"), v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to broadcast system message to settings"); - return; - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); } void diff --git a/dom/bluetooth/bluedroid/BluetoothA2dpManager.cpp b/dom/bluetooth/bluedroid/BluetoothA2dpManager.cpp index d8fe487eb58..fd37f0552f8 100644 --- a/dom/bluetooth/bluedroid/BluetoothA2dpManager.cpp +++ b/dom/bluetooth/bluedroid/BluetoothA2dpManager.cpp @@ -266,8 +266,7 @@ A2dpConnectionStateCallback(btav_connection_state_t aState, AvStatusToSinkString(aState, a2dpState); InfallibleTArray props; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("State"), a2dpState)); + BT_APPEND_NAMED_VALUE(props, "State", a2dpState); BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"), remoteDeviceBdAddress, props); @@ -296,8 +295,7 @@ A2dpAudioStateCallback(btav_audio_state_t aState, } InfallibleTArray props; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("State"), a2dpState)); + BT_APPEND_NAMED_VALUE(props, "State", a2dpState); BluetoothSignal signal(NS_LITERAL_STRING("AudioSink"), remoteDeviceBdAddress, props); diff --git a/dom/bluetooth/bluedroid/BluetoothOppManager.cpp b/dom/bluetooth/bluedroid/BluetoothOppManager.cpp index cade98dc5c9..c6e591c003d 100644 --- a/dom/bluetooth/bluedroid/BluetoothOppManager.cpp +++ b/dom/bluetooth/bluedroid/BluetoothOppManager.cpp @@ -1265,39 +1265,17 @@ BluetoothOppManager::FileTransferComplete() return; } - nsString type, name; - BluetoothValue v; + NS_NAMED_LITERAL_STRING(type, "bluetooth-opp-transfer-complete"); InfallibleTArray parameters; - type.AssignLiteral("bluetooth-opp-transfer-complete"); - name.AssignLiteral("address"); - v = mDeviceAddress; - parameters.AppendElement(BluetoothNamedValue(name, v)); + BT_APPEND_NAMED_VALUE(parameters, "address", mDeviceAddress); + BT_APPEND_NAMED_VALUE(parameters, "success", mSuccessFlag); + BT_APPEND_NAMED_VALUE(parameters, "received", mIsServer); + BT_APPEND_NAMED_VALUE(parameters, "fileName", mFileName); + BT_APPEND_NAMED_VALUE(parameters, "fileLength", mSentFileLength); + BT_APPEND_NAMED_VALUE(parameters, "contentType", mContentType); - name.AssignLiteral("success"); - v = mSuccessFlag; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("received"); - v = mIsServer; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileName"); - v = mFileName; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileLength"); - v = mSentFileLength; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("contentType"); - v = mContentType; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to broadcast [bluetooth-opp-transfer-complete]"); - return; - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); mSendTransferCompleteFlag = true; } @@ -1305,35 +1283,16 @@ BluetoothOppManager::FileTransferComplete() void BluetoothOppManager::StartFileTransfer() { - nsString type, name; - BluetoothValue v; + NS_NAMED_LITERAL_STRING(type, "bluetooth-opp-transfer-start"); InfallibleTArray parameters; - type.AssignLiteral("bluetooth-opp-transfer-start"); - name.AssignLiteral("address"); - v = mDeviceAddress; - parameters.AppendElement(BluetoothNamedValue(name, v)); + BT_APPEND_NAMED_VALUE(parameters, "address", mDeviceAddress); + BT_APPEND_NAMED_VALUE(parameters, "received", mIsServer); + BT_APPEND_NAMED_VALUE(parameters, "fileName", mFileName); + BT_APPEND_NAMED_VALUE(parameters, "fileLength", mFileLength); + BT_APPEND_NAMED_VALUE(parameters, "contentType", mContentType); - name.AssignLiteral("received"); - v = mIsServer; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileName"); - v = mFileName; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileLength"); - v = mFileLength; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("contentType"); - v = mContentType; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to broadcast [bluetooth-opp-transfer-start]"); - return; - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); mSendTransferCompleteFlag = false; } @@ -1341,61 +1300,29 @@ BluetoothOppManager::StartFileTransfer() void BluetoothOppManager::UpdateProgress() { - nsString type, name; - BluetoothValue v; + NS_NAMED_LITERAL_STRING(type, "bluetooth-opp-update-progress"); InfallibleTArray parameters; - type.AssignLiteral("bluetooth-opp-update-progress"); - name.AssignLiteral("address"); - v = mDeviceAddress; - parameters.AppendElement(BluetoothNamedValue(name, v)); + BT_APPEND_NAMED_VALUE(parameters, "address", mDeviceAddress); + BT_APPEND_NAMED_VALUE(parameters, "received", mIsServer); + BT_APPEND_NAMED_VALUE(parameters, "processedLength", mSentFileLength); + BT_APPEND_NAMED_VALUE(parameters, "fileLength", mFileLength); - name.AssignLiteral("received"); - v = mIsServer; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("processedLength"); - v = mSentFileLength; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileLength"); - v = mFileLength; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to broadcast [bluetooth-opp-update-progress]"); - return; - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); } void BluetoothOppManager::ReceivingFileConfirmation() { - nsString type, name; - BluetoothValue v; + NS_NAMED_LITERAL_STRING(type, "bluetooth-opp-receiving-file-confirmation"); InfallibleTArray parameters; - type.AssignLiteral("bluetooth-opp-receiving-file-confirmation"); - name.AssignLiteral("address"); - v = mDeviceAddress; - parameters.AppendElement(BluetoothNamedValue(name, v)); + BT_APPEND_NAMED_VALUE(parameters, "address", mDeviceAddress); + BT_APPEND_NAMED_VALUE(parameters, "fileName", mFileName); + BT_APPEND_NAMED_VALUE(parameters, "fileLength", mFileLength); + BT_APPEND_NAMED_VALUE(parameters, "contentType", mContentType); - name.AssignLiteral("fileName"); - v = mFileName; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("fileLength"); - v = mFileLength; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - name.AssignLiteral("contentType"); - v = mContentType; - parameters.AppendElement(BluetoothNamedValue(name, v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to send [bluetooth-opp-receiving-file-confirmation]"); - return; - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); } void diff --git a/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp b/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp index 20d20f59351..872a2d1f819 100644 --- a/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp +++ b/dom/bluetooth/bluedroid/BluetoothServiceBluedroid.cpp @@ -294,15 +294,13 @@ AdapterPropertiesCallback(bt_status_t aStatus, int aNumProperties, if (p.type == BT_PROPERTY_BDADDR) { BdAddressTypeToString((bt_bdaddr_t*)p.val, sAdapterBdAddress); propertyValue = sAdapterBdAddress; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Address"), propertyValue)); + BT_APPEND_NAMED_VALUE(props, "Address", propertyValue); } else if (p.type == BT_PROPERTY_BDNAME) { // Construct nsCString here because Bd name returned from bluedroid // is missing a null terminated character after SetProperty. propertyValue = sAdapterBdName = NS_ConvertUTF8toUTF16( nsCString((char*)p.val, p.len)); - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Name"), propertyValue)); + BT_APPEND_NAMED_VALUE(props, "Name", propertyValue); } else if (p.type == BT_PROPERTY_ADAPTER_SCAN_MODE) { bt_scan_mode_t newMode = *(bt_scan_mode_t*)p.val; @@ -312,13 +310,10 @@ AdapterPropertiesCallback(bt_status_t aStatus, int aNumProperties, propertyValue = sAdapterDiscoverable = false; } - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Discoverable"), propertyValue)); + BT_APPEND_NAMED_VALUE(props, "Discoverable", propertyValue); } else if (p.type == BT_PROPERTY_ADAPTER_DISCOVERY_TIMEOUT) { propertyValue = sAdapterDiscoverableTimeout = *(uint32_t*)p.val; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("DiscoverableTimeout"), - propertyValue)); + BT_APPEND_NAMED_VALUE(props, "DiscoverableTimeout", propertyValue); } else if (p.type == BT_PROPERTY_ADAPTER_BONDED_DEVICES) { // We have to cache addresses of bonded devices. Unlike BlueZ, // bluedroid would not send an another BT_PROPERTY_ADAPTER_BONDED_DEVICES @@ -337,8 +332,7 @@ AdapterPropertiesCallback(bt_status_t aStatus, int aNumProperties, } propertyValue = sAdapterBondedAddressArray; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Devices"), propertyValue)); + BT_APPEND_NAMED_VALUE(props, "Devices", propertyValue); } else if (p.type == BT_PROPERTY_UUIDS) { //FIXME: This will be implemented in the later patchset continue; @@ -389,25 +383,21 @@ RemoteDevicePropertiesCallback(bt_status_t aStatus, bt_bdaddr_t *aBdAddress, nsString remoteDeviceBdAddress; BdAddressTypeToString(aBdAddress, remoteDeviceBdAddress); - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Address"), remoteDeviceBdAddress)); + BT_APPEND_NAMED_VALUE(props, "Address", remoteDeviceBdAddress); for (int i = 0; i < aNumProperties; ++i) { bt_property_t p = aProperties[i]; if (p.type == BT_PROPERTY_BDNAME) { BluetoothValue propertyValue = NS_ConvertUTF8toUTF16((char*)p.val); - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Name"), propertyValue)); + BT_APPEND_NAMED_VALUE(props, "Name", propertyValue); } else if (p.type == BT_PROPERTY_CLASS_OF_DEVICE) { uint32_t cod = *(uint32_t*)p.val; - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Class"), BluetoothValue(cod))); + BT_APPEND_NAMED_VALUE(props, "Class", cod); nsString icon; ClassToIcon(cod, icon); - props.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Icon"), BluetoothValue(icon))); + BT_APPEND_NAMED_VALUE(props, "Icon", icon); } else { BT_LOGD("Other non-handled device properties. Type: %d", p.type); } @@ -460,22 +450,20 @@ DeviceFoundCallback(int aNumProperties, bt_property_t *aProperties) nsString remoteDeviceBdAddress; BdAddressTypeToString((bt_bdaddr_t*)p.val, remoteDeviceBdAddress); propertyValue = remoteDeviceBdAddress; - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Address"), propertyValue)); + + BT_APPEND_NAMED_VALUE(propertiesArray, "Address", propertyValue); } else if (p.type == BT_PROPERTY_BDNAME) { propertyValue = NS_ConvertUTF8toUTF16((char*)p.val); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Name"), propertyValue)); + BT_APPEND_NAMED_VALUE(propertiesArray, "Name", propertyValue); } else if (p.type == BT_PROPERTY_CLASS_OF_DEVICE) { uint32_t cod = *(uint32_t*)p.val; propertyValue = cod; - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Class"), propertyValue)); + BT_APPEND_NAMED_VALUE(propertiesArray, "Class", propertyValue); + nsString icon; ClassToIcon(cod, icon); propertyValue = icon; - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Icon"), propertyValue)); + BT_APPEND_NAMED_VALUE(propertiesArray, "Icon", propertyValue); } else { BT_LOGD("Not handled remote device property: %d", p.type); } @@ -515,15 +503,12 @@ PinRequestCallback(bt_bdaddr_t* aRemoteBdAddress, nsAutoString remoteAddress; BdAddressTypeToString(aRemoteBdAddress, remoteAddress); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("address"), remoteAddress)); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("method"), - NS_LITERAL_STRING("pincode"))); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("name"), + BT_APPEND_NAMED_VALUE(propertiesArray, "address", remoteAddress); + BT_APPEND_NAMED_VALUE(propertiesArray, "method", + NS_LITERAL_STRING("pincode")); + BT_APPEND_NAMED_VALUE(propertiesArray, "name", NS_ConvertUTF8toUTF16( - (const char*)aRemoteBdName->name))); + (const char*)aRemoteBdName->name)); BluetoothValue value = propertiesArray; BluetoothSignal signal(NS_LITERAL_STRING("RequestPinCode"), @@ -546,17 +531,13 @@ SspRequestCallback(bt_bdaddr_t* aRemoteBdAddress, bt_bdname_t* aRemoteBdName, nsAutoString remoteAddress; BdAddressTypeToString(aRemoteBdAddress, remoteAddress); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("address"), remoteAddress)); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("method"), - NS_LITERAL_STRING("confirmation"))); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("name"), + BT_APPEND_NAMED_VALUE(propertiesArray, "address", remoteAddress); + BT_APPEND_NAMED_VALUE(propertiesArray, "method", + NS_LITERAL_STRING("confirmation")); + BT_APPEND_NAMED_VALUE(propertiesArray, "name", NS_ConvertUTF8toUTF16( - (const char*)aRemoteBdName->name))); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("passkey"), aPasskey)); + (const char*)aRemoteBdName->name)); + BT_APPEND_NAMED_VALUE(propertiesArray, "passkey", aPasskey); BluetoothValue value = propertiesArray; BluetoothSignal signal(NS_LITERAL_STRING("RequestConfirmation"), @@ -592,9 +573,9 @@ BondStateChangedCallback(bt_status_t aStatus, bt_bdaddr_t* aRemoteBdAddress, // Update bonded address list to BluetoothAdapter InfallibleTArray propertiesChangeArray; - propertiesChangeArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Devices"), - sAdapterBondedAddressArray)); + BT_APPEND_NAMED_VALUE(propertiesChangeArray, "Devices", + sAdapterBondedAddressArray); + BluetoothValue value(propertiesChangeArray); BluetoothSignal signal(NS_LITERAL_STRING("PropertyChanged"), NS_LITERAL_STRING(KEY_ADAPTER), @@ -603,10 +584,9 @@ BondStateChangedCallback(bt_status_t aStatus, bt_bdaddr_t* aRemoteBdAddress, // Update bonding status to gaia InfallibleTArray propertiesArray; - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("address"), remoteAddress)); - propertiesArray.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("status"), bonded)); + BT_APPEND_NAMED_VALUE(propertiesArray, "address", remoteAddress); + BT_APPEND_NAMED_VALUE(propertiesArray, "status", bonded); + BluetoothSignal newSignal(NS_LITERAL_STRING(PAIRED_STATUS_CHANGED_ID), NS_LITERAL_STRING(KEY_ADAPTER), BluetoothValue(propertiesArray)); @@ -796,23 +776,20 @@ BluetoothServiceBluedroid::GetDefaultAdapterPathInternal( BluetoothValue v = InfallibleTArray(); - v.get_ArrayOfBluetoothNamedValue().AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Address"), sAdapterBdAddress)); + BT_APPEND_NAMED_VALUE(v.get_ArrayOfBluetoothNamedValue(), + "Address", sAdapterBdAddress); - v.get_ArrayOfBluetoothNamedValue().AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Name"), sAdapterBdName)); + BT_APPEND_NAMED_VALUE(v.get_ArrayOfBluetoothNamedValue(), + "Name", sAdapterBdName); - v.get_ArrayOfBluetoothNamedValue().AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Discoverable"), - sAdapterDiscoverable)); + BT_APPEND_NAMED_VALUE(v.get_ArrayOfBluetoothNamedValue(), + "Discoverable", sAdapterDiscoverable); - v.get_ArrayOfBluetoothNamedValue().AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("DiscoverableTimeout"), - sAdapterDiscoverableTimeout)); + BT_APPEND_NAMED_VALUE(v.get_ArrayOfBluetoothNamedValue(), + "DiscoverableTimeout", sAdapterDiscoverableTimeout); - v.get_ArrayOfBluetoothNamedValue().AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("Devices"), - sAdapterBondedAddressArray)); + BT_APPEND_NAMED_VALUE(v.get_ArrayOfBluetoothNamedValue(), + "Devices", sAdapterBondedAddressArray); nsAutoString replyError; DispatchBluetoothReply(runnable.get(), v, replyError); diff --git a/dom/bluetooth/bluedroid/BluetoothUtils.cpp b/dom/bluetooth/bluedroid/BluetoothUtils.cpp index f04f4b5722a..7be8881b88d 100644 --- a/dom/bluetooth/bluedroid/BluetoothUtils.cpp +++ b/dom/bluetooth/bluedroid/BluetoothUtils.cpp @@ -176,10 +176,8 @@ DispatchStatusChangedEvent(const nsAString& aType, MOZ_ASSERT(NS_IsMainThread()); InfallibleTArray data; - data.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("address"), nsString(aAddress))); - data.AppendElement( - BluetoothNamedValue(NS_LITERAL_STRING("status"), aStatus)); + BT_APPEND_NAMED_VALUE(data, "address", nsString(aAddress)); + BT_APPEND_NAMED_VALUE(data, "status", aStatus); BluetoothSignal signal(nsString(aType), NS_LITERAL_STRING(KEY_ADAPTER), data); diff --git a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp index a9cdbefb2dd..c9b1c8bb1a6 100644 --- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp +++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp @@ -811,18 +811,12 @@ BluetoothHfpManager::NotifyConnectionStateChanged(const nsAString& aType) void BluetoothHfpManager::NotifyDialer(const nsAString& aCommand) { - BluetoothValue v; + NS_NAMED_LITERAL_STRING(type, "bluetooth-dialer-command"); InfallibleTArray parameters; - NS_NAMED_LITERAL_STRING(type, "bluetooth-dialer-command"); - NS_NAMED_LITERAL_STRING(name, "command"); + BT_APPEND_NAMED_VALUE(parameters, "command", nsString(aCommand)); - v = nsString(aCommand); - parameters.AppendElement(BluetoothNamedValue(name, v)); - - if (!BroadcastSystemMessage(type, parameters)) { - BT_WARNING("Failed to broadcast system message to dialer"); - } + BT_ENSURE_TRUE_VOID_BROADCAST_SYSMSG(type, parameters); } void diff --git a/dom/inputmethod/InputMethod.manifest b/dom/inputmethod/InputMethod.manifest index 77fd07313b3..5dc073508d9 100644 --- a/dom/inputmethod/InputMethod.manifest +++ b/dom/inputmethod/InputMethod.manifest @@ -1,6 +1,2 @@ -component {397a7fdf-2254-47be-b74e-76625a1a66d5} MozKeyboard.js -contract @mozilla.org/b2g-keyboard;1 {397a7fdf-2254-47be-b74e-76625a1a66d5} -category JavaScript-navigator-property mozKeyboard @mozilla.org/b2g-keyboard;1 - component {4607330d-e7d2-40a4-9eb8-43967eae0142} MozKeyboard.js contract @mozilla.org/b2g-inputmethod;1 {4607330d-e7d2-40a4-9eb8-43967eae0142} diff --git a/dom/inputmethod/Keyboard.jsm b/dom/inputmethod/Keyboard.jsm index 41b14b4b4ee..7a59d938907 100644 --- a/dom/inputmethod/Keyboard.jsm +++ b/dom/inputmethod/Keyboard.jsm @@ -27,7 +27,6 @@ this.Keyboard = { ], _messageNames: [ - 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions', 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker', 'SwitchToNextInputMethod', 'HideInputMethod', 'GetText', 'SendKey', 'GetContext', @@ -170,7 +169,6 @@ this.Keyboard = { this.forwardEvent(name, msg); break; - case 'Keyboard:SetValue': case 'System:SetValue': this.setValue(msg); break; @@ -178,11 +176,9 @@ this.Keyboard = { case 'System:RemoveFocus': this.removeFocus(); break; - case 'Keyboard:SetSelectedOption': case 'System:SetSelectedOption': this.setSelectedOption(msg); break; - case 'Keyboard:SetSelectedOptions': case 'System:SetSelectedOptions': this.setSelectedOption(msg); break; diff --git a/dom/inputmethod/MozKeyboard.js b/dom/inputmethod/MozKeyboard.js index 12d8e312ddc..1435b11dcec 100644 --- a/dom/inputmethod/MozKeyboard.js +++ b/dom/inputmethod/MozKeyboard.js @@ -18,187 +18,6 @@ XPCOMUtils.defineLazyServiceGetter(this, "cpmm", XPCOMUtils.defineLazyServiceGetter(this, "tm", "@mozilla.org/thread-manager;1", "nsIThreadManager"); -// ----------------------------------------------------------------------- -// MozKeyboard -// ----------------------------------------------------------------------- - -function MozKeyboard() { } - -MozKeyboard.prototype = { - classID: Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"), - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIB2GKeyboard, Ci.nsIDOMGlobalPropertyInitializer, Ci.nsIObserver - ]), - - classInfo: XPCOMUtils.generateCI({ - "classID": Components.ID("{397a7fdf-2254-47be-b74e-76625a1a66d5}"), - "contractID": "@mozilla.org/b2g-keyboard;1", - "interfaces": [Ci.nsIB2GKeyboard], - "flags": Ci.nsIClassInfo.DOM_OBJECT, - "classDescription": "B2G Virtual Keyboard" - }), - - init: function mozKeyboardInit(win) { - let principal = win.document.nodePrincipal; - // Limited the deprecated mozKeyboard API to certified apps only - let perm = Services.perms.testExactPermissionFromPrincipal(principal, - "input-manage"); - if (perm != Ci.nsIPermissionManager.ALLOW_ACTION) { - dump("No permission to use the keyboard API for " + - principal.origin + "\n"); - return null; - } - - Services.obs.addObserver(this, "inner-window-destroyed", false); - cpmm.addMessageListener('Keyboard:FocusChange', this); - cpmm.addMessageListener('Keyboard:SelectionChange', this); - - this._window = win; - this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - this.innerWindowID = this._utils.currentInnerWindowID; - this._focusHandler = null; - this._selectionHandler = null; - this._selectionStart = -1; - this._selectionEnd = -1; - }, - - uninit: function mozKeyboardUninit() { - Services.obs.removeObserver(this, "inner-window-destroyed"); - cpmm.removeMessageListener('Keyboard:FocusChange', this); - cpmm.removeMessageListener('Keyboard:SelectionChange', this); - - this._window = null; - this._utils = null; - this._focusHandler = null; - this._selectionHandler = null; - }, - - sendKey: function mozKeyboardSendKey(keyCode, charCode) { - charCode = (charCode == undefined) ? keyCode : charCode; - - let mainThread = tm.mainThread; - let utils = this._utils; - - function send(type) { - mainThread.dispatch(function() { - utils.sendKeyEvent(type, keyCode, charCode, null); - }, mainThread.DISPATCH_NORMAL); - } - - send("keydown"); - send("keypress"); - send("keyup"); - }, - - setSelectedOption: function mozKeyboardSetSelectedOption(index) { - cpmm.sendAsyncMessage('Keyboard:SetSelectedOption', { - 'index': index - }); - }, - - setValue: function mozKeyboardSetValue(value) { - cpmm.sendAsyncMessage('Keyboard:SetValue', { - 'value': value - }); - }, - - setSelectedOptions: function mozKeyboardSetSelectedOptions(indexes) { - cpmm.sendAsyncMessage('Keyboard:SetSelectedOptions', { - 'indexes': indexes - }); - }, - - set onselectionchange(val) { - this._selectionHandler = val; - }, - - get onselectionchange() { - return this._selectionHandler; - }, - - get selectionStart() { - return this._selectionStart; - }, - - get selectionEnd() { - return this._selectionEnd; - }, - - setSelectionRange: function mozKeyboardSetSelectionRange(start, end) { - cpmm.sendAsyncMessage('Keyboard:SetSelectionRange', { - 'selectionStart': start, - 'selectionEnd': end - }); - }, - - removeFocus: function mozKeyboardRemoveFocus() { - cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {}); - }, - - set onfocuschange(val) { - this._focusHandler = val; - }, - - get onfocuschange() { - return this._focusHandler; - }, - - replaceSurroundingText: function mozKeyboardReplaceSurroundingText( - text, beforeLength, afterLength) { - cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', { - 'text': text || '', - 'beforeLength': (typeof beforeLength === 'number' ? beforeLength : 0), - 'afterLength': (typeof afterLength === 'number' ? afterLength: 0) - }); - }, - - receiveMessage: function mozKeyboardReceiveMessage(msg) { - if (msg.name == "Keyboard:FocusChange") { - let msgJson = msg.json; - if (msgJson.type != "blur") { - this._selectionStart = msgJson.selectionStart; - this._selectionEnd = msgJson.selectionEnd; - } else { - this._selectionStart = 0; - this._selectionEnd = 0; - } - - let handler = this._focusHandler; - if (!handler || !(handler instanceof Ci.nsIDOMEventListener)) - return; - - let detail = { - "detail": msgJson - }; - - let evt = new this._window.CustomEvent("focuschanged", - Cu.cloneInto(detail, this._window)); - handler.handleEvent(evt); - } else if (msg.name == "Keyboard:SelectionChange") { - let msgJson = msg.json; - - this._selectionStart = msgJson.selectionStart; - this._selectionEnd = msgJson.selectionEnd; - - let handler = this._selectionHandler; - if (!handler || !(handler instanceof Ci.nsIDOMEventListener)) - return; - - let evt = new this._window.CustomEvent("selectionchange", - Cu.cloneInto({}, this._window)); - handler.handleEvent(evt); - } - }, - - observe: function mozKeyboardObserve(subject, topic, data) { - let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; - if (wId == this.innerWindowID) - this.uninit(); - } -}; - /* * A WeakMap to map input method iframe window to its active status. */ @@ -288,6 +107,7 @@ MozInputMethod.prototype = { _layouts: {}, _window: null, _isSystem: false, + _isKeyboard: true, classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"), @@ -304,6 +124,8 @@ MozInputMethod.prototype = { .getInterface(Ci.nsIDOMWindowUtils) .currentInnerWindowID; + Services.obs.addObserver(this, "inner-window-destroyed", false); + let principal = win.document.nodePrincipal; let perm = Services.perms.testExactPermissionFromPrincipal(principal, "input-manage"); @@ -311,7 +133,18 @@ MozInputMethod.prototype = { this._isSystem = true; } - Services.obs.addObserver(this, "inner-window-destroyed", false); + // Check if we can use keyboard related APIs. + let testing = false; + try { + testing = Services.prefs.getBoolPref("dom.mozInputMethod.testing"); + } catch (e) { + } + perm = Services.perms.testExactPermissionFromPrincipal(principal, "input"); + if (!testing && perm !== Ci.nsIPermissionManager.ALLOW_ACTION) { + this._isKeyboard = false; + return; + } + cpmm.addWeakMessageListener('Keyboard:FocusChange', this); cpmm.addWeakMessageListener('Keyboard:SelectionChange', this); cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this); @@ -319,15 +152,18 @@ MozInputMethod.prototype = { }, uninit: function mozInputMethodUninit() { - this.setActive(false); + this._window = null; + this._mgmt = null; Services.obs.removeObserver(this, "inner-window-destroyed"); + if (!this._isKeyboard) { + return; + } + cpmm.removeWeakMessageListener('Keyboard:FocusChange', this); cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this); cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this); cpmm.removeWeakMessageListener('Keyboard:LayoutsChange', this); - - this._window = null; - this._mgmt = null; + this.setActive(false); }, receiveMessage: function mozInputMethodReceiveMsg(msg) { @@ -769,5 +605,4 @@ MozInputContext.prototype = { } }; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory( - [MozKeyboard, MozInputMethod]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]); diff --git a/dom/inputmethod/moz.build b/dom/inputmethod/moz.build index 3f68aa509c4..f156f761d13 100644 --- a/dom/inputmethod/moz.build +++ b/dom/inputmethod/moz.build @@ -6,12 +6,6 @@ TEST_DIRS += ['mochitest'] -XPIDL_SOURCES += [ - 'nsIB2GKeyboard.idl', -] - -XPIDL_MODULE = 'dom_inputmethod' - EXTRA_COMPONENTS += [ 'InputMethod.manifest', 'MozKeyboard.js', @@ -21,4 +15,4 @@ EXTRA_JS_MODULES += [ 'Keyboard.jsm', ] -JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file +JAR_MANIFESTS += ['jar.mn'] diff --git a/dom/inputmethod/nsIB2GKeyboard.idl b/dom/inputmethod/nsIB2GKeyboard.idl deleted file mode 100644 index be205282384..00000000000 --- a/dom/inputmethod/nsIB2GKeyboard.idl +++ /dev/null @@ -1,71 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#include "domstubs.idl" - -[scriptable, uuid(40ad96b2-9efa-41fb-84c7-fbcec9b153f0)] -interface nsIB2GKeyboard : nsISupports -{ - void sendKey(in long keyCode, in long charCode); - - // Select the that support multiple - // selection, then the option specified by index will be added to - // the selection. - // If this method is called for a select that does not support multiple - // selection the previous element will be unselected. - void setSelectedOption(in jsval index); - - // Select the that does not support multiple - // selection, then the last index specified in indexes will be selected. - void setSelectedOptions(in jsval indexes); - - // Set the value on the currently focused element. This has to be used - // for special situations where the value had to be chosen amongst a - // list (type=month) or a widget (type=date, time, etc.). - // If the value passed in parameter isn't valid (in the term of HTML5 - // Forms Validation), the value will simply be ignored by the element. - void setValue(in jsval value); - - void removeFocus(); - - attribute nsIDOMEventListener onfocuschange; - - // Fires when user moves the cursor, changes the selection, or alters the - // composing text length - attribute nsIDOMEventListener onselectionchange; - - // The start position of the selection. - readonly attribute long selectionStart; - - // The stop position of the selection. - readonly attribute long selectionEnd; - - /* - * Set the selection range of the the editable text. - * - * @param start The beginning of the selected text. - * @param end The end of the selected text. - * - * Note that the start position should be less or equal to the end position. - * To move the cursor, set the start and end position to the same value. - */ - void setSelectionRange(in long start, in long end); - - /* - * Replace text around the beginning of the current selection range of the - * editable text. - * - * @param text The string to be replaced with. - * @param beforeLength The number of characters to be deleted before the - * beginning of the current selection range. Defaults to 0. - * @param afterLength The number of characters to be deleted after the - * beginning of the current selection range. Defaults to 0. - */ - void replaceSurroundingText(in DOMString text, - [optional] in long beforeLength, - [optional] in long afterLength); -}; diff --git a/dom/nfc/nsNfc.js b/dom/nfc/nsNfc.js index 1d1d08a21cc..8690cfba47b 100644 --- a/dom/nfc/nsNfc.js +++ b/dom/nfc/nsNfc.js @@ -166,8 +166,9 @@ mozNfc.prototype = { this._window = aWindow; }, - // Only System Process can call the following interfaces - // 'checkP2PRegistration' , 'notifyUserAcceptedP2P' , 'notifySendFileStatus' + // Only apps which have nfc-manager permission can call the following interfaces + // 'checkP2PRegistration' , 'notifyUserAcceptedP2P' , 'notifySendFileStatus', + // 'startPoll', 'stopPoll', and 'powerOff'. checkP2PRegistration: function checkP2PRegistration(manifestUrl) { // Get the AppID and pass it to ContentHelper let appID = appsService.getAppLocalIdByManifestURL(manifestUrl); @@ -185,6 +186,18 @@ mozNfc.prototype = { status, requestId); }, + startPoll: function startPoll() { + return this._nfcContentHelper.startPoll(this._window); + }, + + stopPoll: function stopPoll() { + return this._nfcContentHelper.stopPoll(this._window); + }, + + powerOff: function powerOff() { + return this._nfcContentHelper.powerOff(this._window); + }, + getNFCTag: function getNFCTag(sessionToken) { let obj = new MozNFCTag(); let nfcTag = this._window.MozNFCTag._create(this._window, obj); diff --git a/dom/nfc/tests/marionette/head.js b/dom/nfc/tests/marionette/head.js index 479cff0f916..458f5bc3059 100644 --- a/dom/nfc/tests/marionette/head.js +++ b/dom/nfc/tests/marionette/head.js @@ -3,40 +3,35 @@ let pendingEmulatorCmdCount = 0; +SpecialPowers.addPermission("nfc-manager", true, document); + function toggleNFC(enabled, callback) { isnot(callback, null); - var settings = window.navigator.mozSettings; - isnot(settings, null); - ok(settings instanceof SettingsManager, - 'settings instanceof ' + settings.constructor + - ', expected SettingsManager'); - let req = settings.createLock().get('nfc.enabled'); + let nfc = window.navigator.mozNfc; + let req; + if (enabled) { + req = nfc.startPoll(); + } else { + req = nfc.powerOff(); + } + req.onsuccess = function() { - if (req.result['nfc.enabled'] === enabled) { - callback(); - } else { - let req = settings.createLock().set({'nfc.enabled': enabled}); - req.onsuccess = function() { - window.setTimeout(callback, 5000); // give emulator time to toggle NFC - }; - req.onerror = function() { - ok(false, - 'Setting \'nfc.enabled\' to \'' + enabled + - '\' failed, error ' + req.error.name); - finish(); - }; - } + callback(); }; + req.onerror = function() { - ok(false, 'Getting \'nfc.enabled\' failed, error ' + req.error.name); + ok(false, 'operation failed, error ' + req.error.name); finish(); }; } function cleanUp() { log('Cleaning up'); - waitFor(finish(), + waitFor(function() { + SpecialPowers.removePermission("nfc-manager", document); + finish() + }, function() { return pendingEmulatorCmdCount === 0; }); diff --git a/dom/nfc/tests/marionette/test_ndef.js b/dom/nfc/tests/marionette/test_ndef.js index b89a3e37dc7..0d22734cc34 100644 --- a/dom/nfc/tests/marionette/test_ndef.js +++ b/dom/nfc/tests/marionette/test_ndef.js @@ -33,5 +33,4 @@ let tests = [ testConstructNDEF ]; -SpecialPowers.pushPermissions( - [{'type': 'settings', 'allow': true, 'context': document}], runTests); +runTests(); diff --git a/dom/nfc/tests/marionette/test_nfc_enabled.js b/dom/nfc/tests/marionette/test_nfc_enabled.js index 8ef07a46be0..ee49beb8dfa 100644 --- a/dom/nfc/tests/marionette/test_nfc_enabled.js +++ b/dom/nfc/tests/marionette/test_nfc_enabled.js @@ -4,20 +4,52 @@ MARIONETTE_TIMEOUT = 30000; MARIONETTE_HEAD_JS = 'head.js'; +let nfc = window.navigator.mozNfc; function testEnableNFC() { log('Running \'testEnableNFC\''); - toggleNFC(true, runNextTest); + let req = nfc.startPoll(); + req.onsuccess = function () { + ok(true); + runNextTest(); + }; + req.onerror = function () { + ok(false, "startPoll failed"); + runNextTest(); + }; } function testDisableNFC() { log('Running \'testDisableNFC\''); - toggleNFC(false, runNextTest); + let req = nfc.powerOff(); + req.onsuccess = function () { + ok(true); + runNextTest(); + }; + req.onerror = function () { + ok(false, "powerOff failed"); + runNextTest(); + }; +} + +function testStopPollNFC() { + log('Running \'testStopPollNFC\''); + let req = nfc.stopPoll(); + req.onsuccess = function () { + ok(true); + runNextTest(); + }; + req.onerror = function () { + ok(false, "stopPoll failed"); + runNextTest(); + }; } let tests = [ testEnableNFC, + testStopPollNFC, testDisableNFC ]; SpecialPowers.pushPermissions( - [{'type': 'settings', 'allow': true, 'context': document}], runTests); + [{'type': 'nfc-manager', 'allow': true, 'context': document}], + runTests); diff --git a/dom/nfc/tests/marionette/test_nfc_manager_tech_discovered.js b/dom/nfc/tests/marionette/test_nfc_manager_tech_discovered.js index b8d4d62575c..ac21a7c381c 100644 --- a/dom/nfc/tests/marionette/test_nfc_manager_tech_discovered.js +++ b/dom/nfc/tests/marionette/test_nfc_manager_tech_discovered.js @@ -33,5 +33,4 @@ let tests = [ ]; SpecialPowers.pushPermissions( - [{'type': 'nfc-manager', 'allow': true, context: document}, - {'type': 'settings', 'allow': true, context: document}], runTests); + [{'type': 'nfc-manager', 'allow': true, context: document}], runTests); diff --git a/dom/permission/tests/mochitest.ini b/dom/permission/tests/mochitest.ini index 30799ed5814..2b096ac8050 100644 --- a/dom/permission/tests/mochitest.ini +++ b/dom/permission/tests/mochitest.ini @@ -20,6 +20,8 @@ skip-if = buildapp == 'b2g' || toolkit == 'android' # b2g(https not working, bug [test_camera.html] disabled = disabled until bug 859593 is fixed [test_keyboard.html] -skip-if = buildapp != 'b2g' +skip-if = toolkit == 'android' +[test_input-manage.html] +skip-if = toolkit == 'android' [test_wifi-manage.html] skip-if = (buildapp != 'b2g') || (buildapp == 'b2g' && toolkit != 'gonk') #b2g-desktop(Bug 931116, b2g desktop specific, initial triage) diff --git a/dom/permission/tests/test_input-manage.html b/dom/permission/tests/test_input-manage.html new file mode 100644 index 00000000000..db5d8a70493 --- /dev/null +++ b/dom/permission/tests/test_input-manage.html @@ -0,0 +1,68 @@ + + + + + + Test for Bug 920977 + + + + +Mozilla Bug 920977 +

+ +
+
+
+
+ + + diff --git a/dom/permission/tests/test_keyboard.html b/dom/permission/tests/test_keyboard.html index bec15d4acd4..493a6d80418 100644 --- a/dom/permission/tests/test_keyboard.html +++ b/dom/permission/tests/test_keyboard.html @@ -16,17 +16,32 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=920977
 
 
diff --git a/dom/system/gonk/Nfc.js b/dom/system/gonk/Nfc.js
index 47522f266ce..2147adab217 100644
--- a/dom/system/gonk/Nfc.js
+++ b/dom/system/gonk/Nfc.js
@@ -66,7 +66,10 @@ const NFC_IPC_WRITE_PERM_MSG_NAMES = [
 const NFC_IPC_MANAGER_PERM_MSG_NAMES = [
   "NFC:CheckP2PRegistration",
   "NFC:NotifyUserAcceptedP2P",
-  "NFC:NotifySendFileStatus"
+  "NFC:NotifySendFileStatus",
+  "NFC:StartPoll",
+  "NFC:StopPoll",
+  "NFC:PowerOff"
 ];
 
 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
@@ -420,6 +423,7 @@ function Nfc() {
   lock.get(NFC.SETTING_NFC_ENABLED, this);
   // Maps sessionId (that are generated from nfcd) with a unique guid : 'SessionToken'
   this.sessionTokenMap = {};
+  this.targetsByRequestId = {};
 
   gSystemWorkerManager.registerNfcWorker(this.worker);
 }
@@ -516,7 +520,14 @@ Nfc.prototype = {
         this.currentPeerAppId = null;
         break;
      case "ConfigResponse":
-        gSystemMessenger.broadcastMessage("nfc-powerlevel-change", message);
+        let target = this.targetsByRequestId[message.requestId];
+        if (!target) {
+          debug("No target for requestId: " + message.requestId);
+          return;
+        }
+        delete this.targetsByRequestId[message.requestId];
+
+        target.sendAsyncMessage("NFC:ConfigResponse", message);
         break;
       case "ConnectResponse": // Fall through.
       case "CloseResponse":
@@ -539,12 +550,32 @@ Nfc.prototype = {
 
   sessionTokenMap: null,
 
+  targetsByRequestId: null,
+
   /**
    * Process a message from the content process.
    */
   receiveMessage: function receiveMessage(message) {
     debug("Received '" + JSON.stringify(message) + "' message from content process");
 
+    // Handle messages without sessionToken.
+    if (message.name == "NFC:StartPoll") {
+      this.targetsByRequestId[message.json.requestId] = message.target;
+      this.setConfig({powerLevel: NFC.NFC_POWER_LEVEL_ENABLED,
+                      requestId: message.json.requestId});
+      return null;
+    } else if (message.name == "NFC:StopPoll") {
+      this.targetsByRequestId[message.json.requestId] = message.target;
+      this.setConfig({powerLevel: NFC.NFC_POWER_LEVEL_LOW,
+                      requestId: message.json.requestId});
+      return null;
+    } else if (message.name == "NFC:PowerOff") {
+      this.targetsByRequestId[message.json.requestId] = message.target;
+      this.setConfig({powerLevel: NFC.NFC_POWER_LEVEL_DISABLED,
+                      requestId: message.json.requestId});
+      return null;
+    }
+
     if (!this._enabled) {
       debug("NFC is not enabled.");
       this.sendNfcErrorResponse(message);
diff --git a/dom/system/gonk/NfcContentHelper.js b/dom/system/gonk/NfcContentHelper.js
index d025fc04069..beda247fec9 100644
--- a/dom/system/gonk/NfcContentHelper.js
+++ b/dom/system/gonk/NfcContentHelper.js
@@ -54,7 +54,7 @@ const NFC_IPC_MSG_NAMES = [
   "NFC:CheckP2PRegistrationResponse",
   "NFC:PeerEvent",
   "NFC:NotifySendFileStatusResponse",
-  "NFC:SendFileResponse"
+  "NFC:ConfigResponse"
 ];
 
 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
@@ -314,6 +314,51 @@ NfcContentHelper.prototype = {
     });
   },
 
+  startPoll: function startPoll(window) {
+    if (window == null) {
+      throw Components.Exception("Can't get window object",
+                                  Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    let request = Services.DOMRequest.createRequest(window);
+    let requestId = btoa(this.getRequestId(request));
+    this._requestMap[requestId] = window;
+
+    cpmm.sendAsyncMessage("NFC:StartPoll",
+                          {requestId: requestId});
+    return request;
+  },
+
+  stopPoll: function stopPoll(window) {
+    if (window == null) {
+      throw Components.Exception("Can't get window object",
+                                  Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    let request = Services.DOMRequest.createRequest(window);
+    let requestId = btoa(this.getRequestId(request));
+    this._requestMap[requestId] = window;
+
+    cpmm.sendAsyncMessage("NFC:StopPoll",
+                          {requestId: requestId});
+    return request;
+  },
+
+  powerOff: function powerOff(window) {
+    if (window == null) {
+      throw Components.Exception("Can't get window object",
+                                  Cr.NS_ERROR_UNEXPECTED);
+    }
+
+    let request = Services.DOMRequest.createRequest(window);
+    let requestId = btoa(this.getRequestId(request));
+    this._requestMap[requestId] = window;
+
+    cpmm.sendAsyncMessage("NFC:PowerOff",
+                          {requestId: requestId});
+    return request;
+  },
+
   // nsIObserver
   observe: function observe(subject, topic, data) {
     if (topic == "xpcom-shutdown") {
@@ -368,6 +413,7 @@ NfcContentHelper.prototype = {
       case "NFC:MakeReadOnlyNDEFResponse":
       case "NFC:CheckP2PRegistrationResponse":
       case "NFC:NotifySendFileStatusResponse":
+      case "NFC:ConfigResponse":
         if (result.status !== NFC.GECKO_NFC_ERROR_SUCCESS) {
           this.fireRequestError(atob(result.requestId), result.status);
         } else {
diff --git a/dom/system/gonk/nsINfcContentHelper.idl b/dom/system/gonk/nsINfcContentHelper.idl
index bb25d0661ea..91672408012 100644
--- a/dom/system/gonk/nsINfcContentHelper.idl
+++ b/dom/system/gonk/nsINfcContentHelper.idl
@@ -24,7 +24,7 @@ interface nsINfcPeerCallback : nsISupports
                          in DOMString sessionToken);
 };
 
-[scriptable, uuid(70cac000-7e3c-11e3-baa7-0800200c9a66)]
+[scriptable, uuid(10b2eb1b-3fe0-4c98-9c67-9e4c2274cd78)]
 interface nsINfcContentHelper : nsISupports
 {
   const long NFC_EVENT_PEER_READY = 0x01;
@@ -135,4 +135,19 @@ interface nsINfcContentHelper : nsISupports
   void notifySendFileStatus(in nsIDOMWindow window,
                             in octet status,
                             in DOMString requestId);
+
+  /**
+   * Power on the NFC hardware and start polling for NFC tags or devices.
+   */
+  nsIDOMDOMRequest startPoll(in nsIDOMWindow window);
+
+  /**
+   * Stop polling for NFC tags or devices. i.e. enter low power mode.
+   */
+  nsIDOMDOMRequest stopPoll(in nsIDOMWindow window);
+
+  /**
+   * Power off the NFC hardware.
+   */
+  nsIDOMDOMRequest powerOff(in nsIDOMWindow window);
 };
diff --git a/dom/webidl/MozNfc.webidl b/dom/webidl/MozNfc.webidl
index c5533302992..91a30382e55 100644
--- a/dom/webidl/MozNfc.webidl
+++ b/dom/webidl/MozNfc.webidl
@@ -25,6 +25,21 @@ interface MozNfcManager {
     * Notify the status of sendFile operation
     */
    void notifySendFileStatus(octet status, DOMString requestId);
+
+   /**
+    * Power on the NFC hardware and start polling for NFC tags or devices.
+    */
+   DOMRequest startPoll();
+
+   /**
+    * Stop polling for NFC tags or devices. i.e. enter low power mode.
+    */
+   DOMRequest stopPoll();
+
+   /**
+    * Power off the NFC hardware.
+    */
+   DOMRequest powerOff();
 };
 
 [JSImplementation="@mozilla.org/navigatorNfc;1",
diff --git a/toolkit/components/places/PriorityUrlProvider.jsm b/toolkit/components/places/PriorityUrlProvider.jsm
index 2befbf2f86d..dad4c179920 100644
--- a/toolkit/components/places/PriorityUrlProvider.jsm
+++ b/toolkit/components/places/PriorityUrlProvider.jsm
@@ -126,7 +126,7 @@ this.PriorityUrlProvider = Object.freeze({
     matches.delete(token);
   },
 
-  getMatchingSpec: function (searchToken) {
+  getMatch: function (searchToken) {
     return Task.spawn(function* () {
       yield promiseInitialized();
       for (let [token, match] of matches.entries()) {
diff --git a/toolkit/components/places/UnifiedComplete.js b/toolkit/components/places/UnifiedComplete.js
new file mode 100644
index 00000000000..1c1fd4e1281
--- /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]);
diff --git a/toolkit/components/places/UnifiedComplete.manifest b/toolkit/components/places/UnifiedComplete.manifest
new file mode 100644
index 00000000000..9ee03b60d6f
--- /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}
diff --git a/toolkit/components/places/moz.build b/toolkit/components/places/moz.build
index 9acb3f7d205..6e9537031ca 100644
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -87,6 +87,8 @@ if CONFIG['MOZ_PLACES']:
         EXTRA_COMPONENTS += [
             'nsPlacesAutoComplete.js',
             'nsPlacesAutoComplete.manifest',
+            'UnifiedComplete.js',
+            'UnifiedComplete.manifest',
         ]
     FINAL_LIBRARY = 'xul'
 
diff --git a/toolkit/components/places/tests/unit/test_priorityUrlProvider.js b/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
index 40e62260bc5..80cbd515464 100644
--- a/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
+++ b/toolkit/components/places/tests/unit/test_priorityUrlProvider.js
@@ -11,7 +11,7 @@ function run_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);
@@ -19,7 +19,7 @@ add_task(function* search_engine_match() {
 });
 
 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() {
@@ -29,16 +29,16 @@ add_task(function* hide_search_engine_nomatch() {
   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);
@@ -50,7 +50,7 @@ add_task(function* remove_search_engine_nomatch() {
   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() {
diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
index e6b44e7f78a..81f26706881 100644
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -45,6 +45,12 @@ 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
@@ -65,6 +71,8 @@ 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 = {};
@@ -521,6 +529,88 @@ var AddonManagerInternal = {
     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.
@@ -559,6 +649,7 @@ var AddonManagerInternal = {
                                    Services.appinfo.platformVersion);
         Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION,
                                   (appChanged === undefined ? 0 : -1));
+        this.validateBlocklist();
       }
 
 #ifndef MOZ_COMPATIBILITY_NIGHTLY
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
new file mode 100644
index 00000000000..699257f87e0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
@@ -0,0 +1,8 @@
+
+
+  
+    
+      
+    
+  
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
new file mode 100644
index 00000000000..8cbfb5d6a00
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
@@ -0,0 +1,8 @@
+
+
+  
+    
+      
+    
+  
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
new file mode 100644
index 00000000000..75bd6e934cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
@@ -0,0 +1,8 @@
+
+
+  
+    
+      
+    
+  
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js b/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js
new file mode 100644
index 00000000000..81cf2c9aaf1
--- /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();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
index 8c6cc19bc6b..d7dab014b1f 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -262,4 +262,6 @@ run-sequentially = Uses global XCurProcD dir.
 # 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]