michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CustomizationTabPreloader"]; michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Promise.jsm"); michael@0: michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,"; michael@0: const CUSTOMIZATION_URL = "about:customizing"; michael@0: michael@0: // The interval between swapping in a preload docShell and kicking off the michael@0: // next preload in the background. michael@0: const PRELOADER_INTERVAL_MS = 600; michael@0: michael@0: function createTimer(obj, delay) { michael@0: let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT); michael@0: return timer; michael@0: } michael@0: michael@0: function clearTimer(timer) { michael@0: if (timer) { michael@0: timer.cancel(); michael@0: } michael@0: return null; michael@0: } michael@0: michael@0: this.CustomizationTabPreloader = { michael@0: uninit: function () { michael@0: CustomizationTabPreloaderInternal.uninit(); michael@0: }, michael@0: michael@0: newTab: function (aTab) { michael@0: return CustomizationTabPreloaderInternal.newTab(aTab); michael@0: }, michael@0: michael@0: /** michael@0: * ensurePreloading starts the preloading of the about:customizing michael@0: * content page. This function is idempotent (until a call to uninit), michael@0: * so multiple calls to it are fine. michael@0: */ michael@0: ensurePreloading: function() { michael@0: CustomizationTabPreloaderInternal.ensurePreloading(); michael@0: }, michael@0: }; michael@0: michael@0: Object.freeze(CustomizationTabPreloader); michael@0: michael@0: this.CustomizationTabPreloaderInternal = { michael@0: _browser: null, michael@0: michael@0: uninit: function () { michael@0: HostFrame.destroy(); michael@0: michael@0: if (this._browser) { michael@0: this._browser.destroy(); michael@0: this._browser = null; michael@0: } michael@0: }, michael@0: michael@0: newTab: function (aTab) { michael@0: let win = aTab.ownerDocument.defaultView; michael@0: if (win.gBrowser && this._browser) { michael@0: return this._browser.swapWithNewTab(aTab); michael@0: } michael@0: michael@0: return false; michael@0: }, michael@0: michael@0: ensurePreloading: function () { michael@0: if (!this._browser) { michael@0: this._browser = new HiddenBrowser(); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: function HiddenBrowser() { michael@0: this._createBrowser(); michael@0: } michael@0: michael@0: HiddenBrowser.prototype = { michael@0: _timer: null, michael@0: michael@0: get isPreloaded() { michael@0: return this._browser && michael@0: this._browser.contentDocument && michael@0: this._browser.contentDocument.readyState === "complete" && michael@0: this._browser.currentURI.spec === CUSTOMIZATION_URL; michael@0: }, michael@0: michael@0: swapWithNewTab: function (aTab) { michael@0: if (!this.isPreloaded || this._timer) { michael@0: return false; michael@0: } michael@0: michael@0: let win = aTab.ownerDocument.defaultView; michael@0: let tabbrowser = win.gBrowser; michael@0: michael@0: if (!tabbrowser) { michael@0: return false; michael@0: } michael@0: michael@0: // Swap docShells. michael@0: tabbrowser.swapNewTabWithBrowser(aTab, this._browser); michael@0: michael@0: // Load all default frame scripts attached to the target window. michael@0: let mm = aTab.linkedBrowser.messageManager; michael@0: let scripts = win.messageManager.getDelayedFrameScripts(); michael@0: Array.forEach(scripts, ([script, runGlobal]) => mm.loadFrameScript(script, true, runGlobal)); michael@0: michael@0: // Remove the browser, it will be recreated by a timer. michael@0: this._removeBrowser(); michael@0: michael@0: // Start a timer that will kick off preloading the next page. michael@0: this._timer = createTimer(this, PRELOADER_INTERVAL_MS); michael@0: michael@0: // Signal that we swapped docShells. michael@0: return true; michael@0: }, michael@0: michael@0: observe: function () { michael@0: this._timer = null; michael@0: michael@0: // Start pre-loading the customization page. michael@0: this._createBrowser(); michael@0: }, michael@0: michael@0: destroy: function () { michael@0: this._removeBrowser(); michael@0: this._timer = clearTimer(this._timer); michael@0: }, michael@0: michael@0: _createBrowser: function () { michael@0: HostFrame.get().then(aFrame => { michael@0: let doc = aFrame.document; michael@0: this._browser = doc.createElementNS(XUL_NS, "browser"); michael@0: this._browser.setAttribute("type", "content"); michael@0: this._browser.setAttribute("src", CUSTOMIZATION_URL); michael@0: this._browser.style.width = "400px"; michael@0: this._browser.style.height = "400px"; michael@0: doc.getElementById("win").appendChild(this._browser); michael@0: }); michael@0: }, michael@0: michael@0: _removeBrowser: function () { michael@0: if (this._browser) { michael@0: this._browser.remove(); michael@0: this._browser = null; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: let HostFrame = { michael@0: _frame: null, michael@0: _deferred: null, michael@0: michael@0: get hiddenDOMDocument() { michael@0: return Services.appShell.hiddenDOMWindow.document; michael@0: }, michael@0: michael@0: get isReady() { michael@0: return this.hiddenDOMDocument.readyState === "complete"; michael@0: }, michael@0: michael@0: get: function () { michael@0: if (!this._deferred) { michael@0: this._deferred = Promise.defer(); michael@0: this._create(); michael@0: } michael@0: michael@0: return this._deferred.promise; michael@0: }, michael@0: michael@0: destroy: function () { michael@0: if (this._frame) { michael@0: if (!Cu.isDeadWrapper(this._frame)) { michael@0: this._frame.removeEventListener("load", this, true); michael@0: this._frame.remove(); michael@0: } michael@0: michael@0: this._frame = null; michael@0: this._deferred = null; michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function () { michael@0: let contentWindow = this._frame.contentWindow; michael@0: if (contentWindow.location.href === XUL_PAGE) { michael@0: this._frame.removeEventListener("load", this, true); michael@0: this._deferred.resolve(contentWindow); michael@0: } else { michael@0: contentWindow.location = XUL_PAGE; michael@0: } michael@0: }, michael@0: michael@0: _create: function () { michael@0: if (this.isReady) { michael@0: let doc = this.hiddenDOMDocument; michael@0: this._frame = doc.createElementNS(HTML_NS, "iframe"); michael@0: this._frame.addEventListener("load", this, true); michael@0: doc.documentElement.appendChild(this._frame); michael@0: } else { michael@0: let flags = Ci.nsIThread.DISPATCH_NORMAL; michael@0: Services.tm.currentThread.dispatch(() => this._create(), flags); michael@0: } michael@0: } michael@0: };