Please note, this is a STATIC archive of website developer.mozilla.org from 03 Nov 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

This article needs a technical review. How you can help.

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="https://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="https://www.mozilla.org/2004/em-rdf#">
    <Description about="urn:mozilla:install-manifest">
        <em:id>[email protected]</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 onLoad() {
      domWindow.removeEventListener("load", onLoad, 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.hide("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);
}

Showing Dialogs and Prompts

You can show native Android dialogs and prompt using the same nsIPromptService interface used on desktop Firefox. The implementation uses native Android dialogs instead of the XUL dialogs used on desktop.

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.

Installing addons on Android can be a bit tricky. There are a few different approaches that developers take. If you're comfortable putting your extension up on a public server, you can just follow a link. There are also extensions to simplify the process. If you have the Android SDK installed, you can also run a few simple adb commands to copy the extension to your phone and load it in Fennec:

adb push myAddon.xpi /sdcard/myAddon.xpi
adb shell am start -a android.intent.action.VIEW \
                   -c android.intent.category.DEFAULT \
                   -d file:///mnt/sdcard/myAddon.xpi \
                   -n org.mozilla.firefox/.App
Replace org.mozilla.firefox/.App with org.mozilla.firefox_beta for beta releases, org.mozilla.fennec_auora for Aurora builds, org.mozilla.fennec for Nightly, and org.mozilla.fennec_<your_username> for private builds.
It may occur when installing the way mentioned, the plugin be installed but does not show the changes on the browser. If this happens, try uploading the XPI to a website and accessing it from your mobile browser.

Document Tags and Contributors

Tags: 
 Last updated by: wbamberg,