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); }); }
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
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.