User:MarkFinkle

Getting Started

This guide walks you through the steps needed to make a restartless add-on for Firefox on Android. If you are familiar with building add-ons for desktop Firefox, much of this guide will be review. Firefox on Android uses native Android widgets for the browser UI so the big difference is how you interact with the browser UI.

This add-on won't do anything in particular, just show you how to hook into the new APIs. Building something useful is an exercise left to the reader.

Boilerplate

As always, we need an install.rdf file. There is nothing new here except for the Firefox on Android app-id, but note that we do set the bootstrap flag:

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
    <Description about="urn:mozilla:install-manifest">
        <em:id>simplenativedemo@mydomain.org</em:id>
        <em:type>2</em:type>
        <em:name>Simple Native Firefox Add-on</em:name>
        <em:version>1.0</em:version>
        <em:bootstrap>true</em:bootstrap>
        <em:description>A small demo add-on for Firefox on Android (Native)</em:description>
        <em:creator>John Smith</em:creator>
 
        <!-- Mobile Native -->
        <em:targetApplication>
            <Description>
                <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
                <em:minVersion>10.0</em:minVersion>
                <em:maxVersion>15.*</em:maxVersion>
            </Description>
        </em:targetApplication>
    </Description>
</RDF>

Since we are making a restartless add-on, we also need a bootstrap.js file. This file is the main entry point for the add-on. The requirements for the file are simple, but you need to add some of your own code to make sure you manage DOM windows. Here is a simple example:

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

function isNativeUI() {
  let appInfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
  return (appInfo.ID == "{aa3c5121-dab2-40e2-81ca-7ea25febc110}");
}

function loadIntoWindow(window) {
  if (!window)
    return;
  // DO SOMETHING HERE (create UI)
}

function unloadFromWindow(window) {
  if (!window)
    return;
  // CLEAN UP HERE (remove the UI)
}

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 windows = wm.getEnumerator("navigator:browser");
  while (windows.hasMoreElements()) {
    let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
    loadIntoWindow(domWindow);
  }

  // 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 made
  if (aReason == APP_SHUTDOWN)
    return;

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

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

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

function install(aData, aReason) {}
function uninstall(aData, aReason) {}

This basic code has two window-based entry points: loadIntoWindow and unloadFromWindow. Think of these methods like you would window.onload and window.onunload for non-restartless add-ons. These are the places where we can add our add-on specific UI and behavior. There are many more features and utilities you can use with restartless add-ons.

I also added a helper method, isNativeUI, in case you need to check to see if the add-on is running in Native Firefox on Android or the older XUL-based version.

Adding UI

To add UI widgets in traditional add-ons, you could use XUL overlays or dynamically create XUL elements and add them to the DOM document. You can't do that in Firefox on Android since the UI is entirely native widgets. Instead, you can use an API to manipulate the native UI. The entry point for the API is a JavaScript object called NativeWindow. The API allows you to interact with several UI widgets:

  • Android Menu
  • Popup (doorhanger) Notifications
  • Context Menus (in web content)
  • Android Toast alerts

Adding/Removing a Menu Item

You can add and remove a menu item from the Android application main menu. When adding a menu, you are given a token which you can use to remove the menu:

var menuId;
function loadIntoWindow(window) {
  if (!window)
    return;
  menuId = window.NativeWindow.menu.add("Show Toast", null, function() { showToast(window); });
}
The bootstrap script is not attached to a DOM window, so you need to pass the reference to the window to other functions if you need access to it.

Removing a menu is just as easy:

function unloadFromWindow(window) {
  if (!window)
    return;
  window.NativeWindow.menu.remove(menuId);
}

Showing a Toast Alert

A toast alert is a simple short lived notification popup. It is useful for showing an action has completed without causing the user too much distraction. The popup closes on its own after a short or long duration.

function showToast(aWindow) {
  aWindow.NativeWindow.toast.show("Showing you a toast", "short");
}

Showing a Notification

Doorhanger notifications are slightly more complicated, and allow the user to tap buttons to make a choice. The popups are modeless so they don't require the user to explicitly close them. They are assoicated with a tab too, so switching to a different tab will hide any open notifications. Conversely, when switching back to a tab with active doorhanger notifications causes them to become visible again.

function showDoorhanger(aWindow) {
  buttons = [
    {
      label: "Button 1",
      callback: function() {
        aWindow.NativeWindow.toast.show("Button 1 was tapped", "short");
      }
    } , {
      label: "Button 2",
      callback: function() {
        aWindow.NativeWindow.toast.show("Button 2 was tapped", "short");
      }
    }];

  aWindow.NativeWindow.doorhanger.show("Showing a doorhanger with two button choices.", "doorhanger-test", buttons);
}

If you don't explicitly pass a tab ID along with the doorhanger, the selected tab is assumed. You can also pass a name along with the doorhanger, "doorhanger-test" in this example. You can use the name to hide the doorhanger programmatically, if needed:

function hideDoorhanger(aWindow) {
  aWindow.NativeWindow.doorhanger.show("doorhanger-test", aWindow.BrowserApp.selectedTab.id);
}

Adding/Removing a Context Menu

You can add and remove a menu item from the web content context menu. A context menu appears when you long-tap on an element in the webpage. Firefox already has some built-in context menus, but you can create your won too. When adding a context menu, you are given a token which you can use to remove the context menu:

var contextId;
function loadIntoWindow(window) {
  if (!window)
    return;
  contextId = window.NativeWindow.contextmenus.add(
    "Copy Link",
    window.NativeWindow.contextmenus.linkOpenableContext,
    function(aTarget) { copyLink(window, aTarget); }
  );
}

You can see that a context menu is a bit more complicated than a simple menu. The second parameter is a selector function (think CSS selector) that let's Firefox know what type of element triggers your context menu. In the example I use the built-in link (<a>) selector. The selector is simply an object with a matches method like this:

{
  matches: function(aElement) {
    // return true if the element triggers your context menu or false if it doesn't
    let name = aElement.localName;
    return name == "p";
  }
}

Removing a context menu is just as easy and can be done at any time, not just when unloading the add-on from a window:

function unloadFromWindow(window) {
  if (!window)
    return;
  window.NativeWindow.contextmenus.remove(contextId);
}

 Wrapping It Up

As with any other restartless add-on, you need to bundle the install.rdf and bootstrap.js (along with any other files you added) into an XPI file. You are now ready to install your add-on and try it out.

Document Tags and Contributors

Contributors to this page: mdnwebdocs-bot, MarkFinkle, Admin
Last updated by: mdnwebdocs-bot,