browser/modules/BrowserNewTabPreloader.jsm
author Boris Zbarsky <bzbarsky@mit.edu>
Thu, 10 Jan 2013 10:49:07 -0500
changeset 118414 1a7ca1f81bd21b1d678762a9586287294b25d378
parent 111947 5ce71981e005a52d4cb0b831ad3db9284f2fb356
child 132825 08ee9cb4412b77adb8fb940d01bfd4b60ba97bcc
permissions -rw-r--r--
Bug 824237. Add support for "partial interface" to WebIDL parser. r=khuey

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];

const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const PREF_BRANCH = "browser.newtab.";
const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
const PRELOADER_INIT_DELAY_MS = 5000;

this.BrowserNewTabPreloader = {
  init: function Preloader_init() {
    Initializer.start();
  },

  uninit: function Preloader_uninit() {
    Initializer.stop();
    HostFrame.destroy();
    Preferences.uninit();
    HiddenBrowser.destroy();
  },

  newTab: function Preloader_newTab(aTab) {
    HiddenBrowser.swapWithNewTab(aTab);
  }
};

Object.freeze(BrowserNewTabPreloader);

let Initializer = {
  _timer: null,
  _observing: false,

  start: function Initializer_start() {
    Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
    this._observing = true;
  },

  stop: function Initializer_stop() {
    if (this._timer) {
      this._timer.cancel();
      this._timer = null;
    }

    if (this._observing) {
      Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
      this._observing = false;
    }
  },

  observe: function Initializer_observe(aSubject, aTopic, aData) {
    if (aTopic == TOPIC_DELAYED_STARTUP) {
      Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
      this._observing = false;
      this._startTimer();
    } else if (aTopic == "timer-callback") {
      this._timer = null;
      this._startPreloader();
    }
  },

  _startTimer: function Initializer_startTimer() {
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._timer.init(this, PRELOADER_INIT_DELAY_MS, Ci.nsITimer.TYPE_ONE_SHOT);
  },

  _startPreloader: function Initializer_startPreloader() {
    Preferences.init();
    if (Preferences.enabled) {
      HiddenBrowser.create();
    }
  }
};

let Preferences = {
  _enabled: null,
  _branch: null,
  _url: null,

  get enabled() {
    if (this._enabled === null) {
      this._enabled = this._branch.getBoolPref("preload") &&
                      !this._branch.prefHasUserValue("url") &&
                      this.url && this.url != "about:blank";
    }

    return this._enabled;
  },

  get url() {
    if (this._url === null) {
      this._url = this._branch.getCharPref("url");
    }

    return this._url;
  },

  init: function Preferences_init() {
    this._branch = Services.prefs.getBranch(PREF_BRANCH);
    this._branch.addObserver("", this, false);
  },

  uninit: function Preferences_uninit() {
    if (this._branch) {
      this._branch.removeObserver("", this);
      this._branch = null;
    }
  },

  observe: function Preferences_observe(aSubject, aTopic, aData) {
    let {url, enabled} = this;
    this._url = this._enabled = null;

    if (enabled && !this.enabled) {
      HiddenBrowser.destroy();
    } else if (!enabled && this.enabled) {
      HiddenBrowser.create();
    } else if (this._browser && url != this.url) {
      HiddenBrowser.update(this.url);
    }
  },
};

let HiddenBrowser = {
  get isPreloaded() {
    return this._browser &&
           this._browser.contentDocument &&
           this._browser.contentDocument.readyState == "complete" &&
           this._browser.currentURI.spec == Preferences.url;
  },

  swapWithNewTab: function HiddenBrowser_swapWithNewTab(aTab) {
    if (this.isPreloaded) {
      let tabbrowser = aTab.ownerDocument.defaultView.gBrowser;
      if (tabbrowser) {
        tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
      }
    }
  },

  create: function HiddenBrowser_create() {
    HostFrame.getFrame(function (aFrame) {
      let doc = aFrame.document;
      this._browser = doc.createElementNS(XUL_NS, "browser");
      this._browser.setAttribute("type", "content");
      this._browser.setAttribute("src", Preferences.url);
      doc.documentElement.appendChild(this._browser);
    }.bind(this));
  },

  update: function HiddenBrowser_update(aURL) {
    this._browser.setAttribute("src", aURL);
  },

  destroy: function HiddenBrowser_destroy() {
    if (this._browser) {
      this._browser.parentNode.removeChild(this._browser);
      this._browser = null;
    }
  }
};

let HostFrame = {
  _listener: null,
  _privilegedFrame: null,

  _privilegedContentTypes: {
    "application/vnd.mozilla.xul+xml": true,
    "application/xhtml+xml": true
  },

  get _frame() {
    delete this._frame;
    return this._frame = Services.appShell.hiddenDOMWindow;
  },

  get _isReady() {
    let readyState = this._frame.document.readyState;
    return (readyState == "complete" || readyState == "interactive");
  },

  get _isPrivileged() {
    return (this._frame.location.protocol == "chrome:" &&
            this._frame.document.contentType in this._privilegedContentTypes);
  },

  getFrame: function HostFrame_getFrame(aCallback) {
    if (this._isReady && !this._isPrivileged) {
      this._createPrivilegedFrame();
    }

    if (this._isReady) {
      aCallback(this._frame);
    } else {
      this._waitUntilLoaded(aCallback);
    }
  },

  destroy: function HostFrame_destroy() {
    delete this._frame;
    this._listener = null;
  },

  _createPrivilegedFrame: function HostFrame_createPrivilegedFrame() {
    let doc = this._frame.document;
    let iframe = doc.createElement("iframe");
    iframe.setAttribute("src", "chrome://browser/content/newtab/preload.xhtml");
    doc.documentElement.appendChild(iframe);
    this._frame = iframe.contentWindow;
  },

  _waitUntilLoaded: function HostFrame_waitUntilLoaded(aCallback) {
    this._listener = new HiddenWindowLoadListener(this._frame, function () {
      HostFrame.getFrame(aCallback);
    });
  }
};


function HiddenWindowLoadListener(aWindow, aCallback) {
  this._window = aWindow;
  this._callback = aCallback;

  let docShell = Services.appShell.hiddenWindow.docShell;
  this._webProgress = docShell.QueryInterface(Ci.nsIWebProgress);
  this._webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_ALL);
}

HiddenWindowLoadListener.prototype = {
  _window: null,
  _callback: null,
  _webProgress: null,

  _destroy: function HiddenWindowLoadListener_destroy() {
    this._webProgress.removeProgressListener(this);
    this._window = null;
    this._callback = null;
    this._webProgress = null;
  },

  onStateChange:
  function HiddenWindowLoadListener_onStateChange(aWebProgress, aRequest,
                                                  aStateFlags, aStatus) {
    if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
        aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW &&
        this._window == aWebProgress.DOMWindow) {
      this._callback();
      this._destroy();
    }
  },

  onStatusChange: function () {},
  onLocationChange: function () {},
  onProgressChange: function () {},
  onSecurityChange: function () {},

  QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
                                         Ci.nsISupportsWeakReference])
};