Bootstrap Jones - Adventures in Restartless Add-ons

We recently landed some code in Firefox Mobile to better handle restartless (bootstrapped) add-ons. Turns out, Dietrich made a non-Jetpack add-on using the SDK and tried unsuccessfully to install it into Firefox Mobile. In the process of getting everything working, I decided to make a simple restartless add-on we could use as a testcase. It’s a copy of Dietrich’s original add-on, without any SDK usage. I’ve not taken the SDK plunge yet. The experience was interesting, educational and slightly enjoyable. The process of testing the install / uninstall and enable / disable mechanism without a restart left me as giddy as a schoolboy.

I’ve decided to approach any future add-on development using the bootstrapped system. If I find a reason why it’s not possible, them I’ll switch back to the traditional XUL overlay approach. Below is the code from the testcase I created. There are only two (2) files! install.rdf and bootstrap.js. That’s it!

The install.rdf has the basic stuff along with the em:bootstrap flag that turns on the magic. My bootstrap.js file has the basic bootstrap methods, but only two, startup and shutdown, needed to be implemented. I added some code to handle adding and removing my add-on UI on demand. All I do is implement the loadIntoWindow and unloadFromWindow methods. Pretty easy, especially for lightweight add-ons.

Notice how I am using a data: URI for a button image, so I don’t need to package the file and add a chrome.manifest.

Just to be clear, I am not using Jetpack or the SDK. Firefox Mobile doesn’t really support Jetpack yet. This is just a simple bootstrapped, restartless add-on. I’m in the process of using this system for a bigger add-on, so I might need to make some adjustments. I’ll let you know how it goes.

install.rdf


    
        bootstrapdemo@starkravingfinkle.org
        2
        Bootstrap Jones
        1.0
        true
        Mark 'Bootstrap' Finkle
        
            
                {a23983c0-fd0e-11dc-95ff-0800200c9a66}
                4.0b4pre
                4.*
            
        
    

bootstrap.js


var Cc = Components.classes;
var Ci = Components.interfaces;

function loadIntoWindow(window) {
  if (!window) return;

  // Get the anchor for "overlaying" but make sure the UI is loaded
  let forward = window.document.getElementById("tool-forward");
  if (!forward) return;

  // Place the new button after the last button in the top set
  let anchor = forward.nextSibling;

  let button = window.document.createElement("toolbarbutton");
  button.setAttribute("id", "tool-homebutton");
  button.setAttribute("label", "Home");
  button.setAttribute("class", "button-control");
  button.style.listStyleImage = "url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAo0lEQVR42u3VMQrCMBSA4Q7uOnsNB4%2FR0cM6ZPYyyRRIGhAzly5CNQrfD49Alhe%2BJdMkSZIk6d1ijMdSyryelNJp710hhMPWrpzzeRjAsiyX1tpzPf3%2B%2BgnsrV211hsAAAAAAAAAAAAAAAAAAAAAAACA0QDfHAAAAPweQH%2FUo5%2F3PafvCn4BAAAAAAAAAAAAAAAAAAAAAAAAAADGAkiSJOmPewEpGDS2TaImnAAAAABJRU5ErkJggg%3D%3D)";

  button.addEventListener("click", function() {
    window.Browser.loadURI("about:home");
  }, false);

  anchor.parentNode.insertBefore(button, anchor);
}

function unloadFromWindow(window) {
  if (!window) return;
  let button = window.document.getElementById("tool-homebutton");
  if (button)
    button.parentNode.removeChild(button);
}

/*
 bootstrap.js API
*/

var windowListener = {
  onOpenWindow: function(aWindow) {
    // Wait for the window to finish loading
    let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
    domWindow.addEventListener("load", function() {
      domWindow.removeEventListener("load", arguments.callee, false);
      loadIntoWindow(domWindow);
    }, false);
  },
  onCloseWindow: function(aWindow) { },
  onWindowTitleChange: function(aWindow, aTitle) { }
};

function startup(aData, aReason) {
  let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);

  // Load into any existing windows
  let enumerator = wm.getEnumerator("navigator:browser");
  while (enumerator.hasMoreElements()) {
    let win = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
    loadIntoWindow(win);
  }

  // Load into any new windows
  wm.addListener(windowListener);
}

function shutdown(aData, aReason) {
  // When the application is shutting down we normally don't have to clean up any UI changes
  if (aReason == APP_SHUTDOWN) return;

  let wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);

  // Stop watching for new windows
  wm.removeListener(windowListener);

  // Unload from any existing windows
  let enumerator = wm.getEnumerator("navigator:browser");
  while (enumerator.hasMoreElements()) {
    let win = enumerator.getNext().QueryInterface(Ci.nsIDOMWindow);
    unloadFromWindow(win);
  }
}

function install(aData, aReason) { }

function uninstall(aData, aReason) { }