Bug 1272102 - FlyWeb Android UI. r=sebastian
authorJustin D'Arcangelo <jdarcangelo@mozilla.com>
Wed, 08 Jun 2016 21:37:06 -0400
changeset 301199 8b658fb9b51049d588c3ec08d5ff01d4a3641b90
parent 301198 85bb94bb8c3e98b79c3efe3b71bd4b8d8854d0aa
child 301200 6d21ad903c7a293ffe7a76460f9f6f3fbe0b9104
push id78247
push userkvijayan@mozilla.com
push dateThu, 09 Jun 2016 01:37:17 +0000
treeherdermozilla-inbound@8b658fb9b510 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1272102
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1272102 - FlyWeb Android UI. r=sebastian
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
mobile/android/base/moz.build
mobile/android/chrome/content/aboutAddons.js
mobile/android/extensions/flyweb/bootstrap.js
mobile/android/extensions/flyweb/content/aboutFlyWeb.css
mobile/android/extensions/flyweb/content/aboutFlyWeb.js
mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml
mobile/android/extensions/flyweb/content/icon-64.png
mobile/android/extensions/flyweb/install.rdf.in
mobile/android/extensions/flyweb/jar.mn
mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd
mobile/android/extensions/flyweb/locale/en-US/flyweb.properties
mobile/android/extensions/flyweb/moz.build
mobile/android/extensions/moz.build
mobile/android/installer/package-manifest.in
mobile/android/moz.build
python/mozbuild/mozbuild/action/package_fennec_apk.py
toolkit/mozapps/installer/upload-files-APK.mk
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -80,16 +80,17 @@ import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.telemetry.TelemetryUploadService;
 import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
+import org.mozilla.gecko.updater.PostUpdateHandler;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.Experiments;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
@@ -305,16 +306,17 @@ public class BrowserApp extends GeckoApp
     private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
 
     private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
             (BrowserAppDelegate) new AddToHomeScreenPromotion(),
             (BrowserAppDelegate) new ScreenshotDelegate(),
             (BrowserAppDelegate) new BookmarkStateChangeDelegate(),
             (BrowserAppDelegate) new ReaderViewBookmarkPromotion(),
             (BrowserAppDelegate) new ContentNotificationsDelegate(),
+            (BrowserAppDelegate) new PostUpdateHandler(),
             new TelemetryCorePingDelegate()
     ));
 
     @NonNull
     private SearchEngineManager mSearchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
     private boolean mHasResumed;
 
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -154,16 +154,17 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_SCREEN_ADVANCED = NON_PREF_PREFIX + "advanced_screen";
     public static final String PREFS_HOMEPAGE = NON_PREF_PREFIX + "homepage";
     public static final String PREFS_HISTORY_SAVED_SEARCH = NON_PREF_PREFIX + "search.search_history.enabled";
     private static final String PREFS_FAQ_LINK = NON_PREF_PREFIX + "faq.link";
     private static final String PREFS_FEEDBACK_LINK = NON_PREF_PREFIX + "feedback.link";
     public static final String PREFS_NOTIFICATIONS_CONTENT = NON_PREF_PREFIX + "notifications.content";
     public static final String PREFS_NOTIFICATIONS_CONTENT_LEARN_MORE = NON_PREF_PREFIX + "notifications.content.learn_more";
     public static final String PREFS_NOTIFICATIONS_WHATS_NEW = NON_PREF_PREFIX + "notifications.whats_new";
+    public static final String PREFS_APP_UPDATE_LAST_BUILD_ID = "app.update.last_build_id";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/PostUpdateHandler.java
@@ -0,0 +1,154 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.updater;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.keepsafe.switchboard.SwitchBoard;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.BrowserApp;
+import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Perform tasks in the background after the app has been installed/updated.
+ */
+public class PostUpdateHandler extends BrowserAppDelegateWithReference {
+    private static final String LOGTAG = "PostUpdateHandler";
+
+    @Override
+    public void onStart(BrowserApp browserApp) {
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
+
+        // Check if this is a new installation or if the app has been updated since the last start.
+        if (!AppConstants.MOZ_APP_BUILDID.equals(prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null))) {
+            Log.d(LOGTAG, "Build ID changed since last start: '" + AppConstants.MOZ_APP_BUILDID + "', '" + prefs.getString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, null) + "'");
+
+            // Copy the bundled system add-ons from the APK to the data directory.
+            copyFeaturesFromAPK();
+        }
+    }
+
+    /**
+     * Copies the /assets/features folder out of the APK and into the app's data directory.
+     */
+    private void copyFeaturesFromAPK() {
+        final BrowserApp browserApp = getBrowserApp();
+        if (browserApp == null) {
+            return;
+        }
+
+        final String dataDir = browserApp.getApplicationInfo().dataDir;
+        final String sourceDir = browserApp.getApplicationInfo().sourceDir;
+        final File applicationPackage = new File(sourceDir);
+
+        final String assetsPrefix = "assets/";
+        final String fullPrefix = assetsPrefix + "features/";
+
+        final SharedPreferences prefs = GeckoSharedPrefs.forApp(browserApp);
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Log.d(LOGTAG, "Copying system add-ons from APK to dataDir");
+
+                try {
+                    final ZipFile zip = new ZipFile(applicationPackage);
+                    final Enumeration<? extends ZipEntry> zipEntries = zip.entries();
+                    
+                    final byte[] buffer = new byte[1024];
+
+                    while (zipEntries.hasMoreElements()) {
+                        final ZipEntry fileEntry = zipEntries.nextElement();
+                        final String name = fileEntry.getName();
+
+                        if (fileEntry.isDirectory()) {
+                            // We'll let getDataFile deal with creating the directory hierarchy.
+                            continue;
+                        }
+
+                        // Read from "assets/features/**".
+                        if (!name.startsWith(fullPrefix)) {
+                            continue;
+                        }
+
+                        // Write to "features/**".
+                        final String nameWithoutPrefix = name.substring(assetsPrefix.length());
+                        final File outFile = getDataFile(dataDir, nameWithoutPrefix);
+                        if (outFile == null) {
+                            continue;
+                        }
+
+                        final InputStream fileStream = zip.getInputStream(fileEntry);
+                        try {
+                            writeStream(fileStream, outFile, fileEntry.getTime(), buffer);
+                        } finally {
+                            fileStream.close();
+                        }
+                    }
+
+                    zip.close();
+                } catch (IOException e) {
+                    Log.e(LOGTAG, "Error copying system add-ons from APK.", e);
+                }
+
+                // Save the Build ID so we don't perform post-update operations again until the app is updated.
+                prefs.edit().putString(GeckoPreferences.PREFS_APP_UPDATE_LAST_BUILD_ID, AppConstants.MOZ_APP_BUILDID).apply();
+            }
+        });
+    }
+
+    /**
+     * Return a File instance in the data directory, ensuring
+     * that the parent exists.
+     *
+     * @return null if the parents could not be created.
+     */
+    private File getDataFile(final String dataDir, final String name) {
+        File outFile = new File(dataDir, name);
+        File dir = outFile.getParentFile();
+
+        if (!dir.exists()) {
+            Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
+            if (!dir.mkdirs()) {
+                Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
+                return null;
+            }
+        }
+
+        return outFile;
+    }
+
+    private void writeStream(InputStream fileStream, File outFile, final long modifiedTime, byte[] buffer)
+            throws FileNotFoundException, IOException {
+        final OutputStream outStream = new FileOutputStream(outFile);
+        try {
+            int count;
+            while ((count = fileStream.read(buffer)) > 0) {
+                outStream.write(buffer, 0, count);
+            }
+
+            outFile.setLastModified(modifiedTime);
+        } finally {
+            outStream.close();
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -619,16 +619,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'toolbar/TabCounter.java',
     'toolbar/ToolbarDisplayLayout.java',
     'toolbar/ToolbarEditLayout.java',
     'toolbar/ToolbarEditText.java',
     'toolbar/ToolbarPrefs.java',
     'toolbar/ToolbarProgressView.java',
     'TouchEventInterceptor.java',
     'trackingprotection/TrackingProtectionPrompt.java',
+    'updater/PostUpdateHandler.java',
     'updater/UpdateService.java',
     'updater/UpdateServiceHelper.java',
     'util/Experiments.java',
     'widget/ActivityChooserModel.java',
     'widget/AllCapsTextView.java',
     'widget/AnchoredPopup.java',
     'widget/AnimatedHeightLayout.java',
     'widget/BasicColorPicker.java',
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -261,16 +261,20 @@ var Addons = {
       // Clear all content before filling the addons
       let list = document.getElementById("addons-list");
       list.innerHTML = "";
 
       aAddons.sort(function(a,b) {
         return a.name.localeCompare(b.name);
       });
       for (let i=0; i<aAddons.length; i++) {
+        // Don't create item for system add-ons.
+        if (aAddons[i].isSystem)
+          continue;
+
         let item = self._createItemForAddon(aAddons[i]);
         list.appendChild(item);
       }
 
       // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
       let browseItem = self._createBrowseItem();
       list.appendChild(browseItem);
     });
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/bootstrap.js
@@ -0,0 +1,151 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, manager: Cm, results: Cr, utils: Cu, Constructor: CC} = Components;
+
+Cm.QueryInterface(Ci.nsIComponentRegistrar);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Console",
+                                  "resource://gre/modules/Console.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gFlyWebBundle", function() {
+  return Services.strings.createBundle("chrome://flyweb/locale/flyweb.properties");
+});
+
+const FLYWEB_ENABLED_PREF = "dom.flyweb.enabled";
+
+let factory, menuID;
+
+function AboutFlyWeb() {}
+
+AboutFlyWeb.prototype = Object.freeze({
+  classDescription: "About page for displaying nearby FlyWeb services",
+  contractID: "@mozilla.org/network/protocol/about;1?what=flyweb",
+  classID: Components.ID("{baa04ff0-08b5-11e6-a837-0800200c9a66}"),
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+  getURIFlags: function(aURI) {
+    return Ci.nsIAboutModule.ALLOW_SCRIPT;
+  },
+
+  newChannel: function(aURI, aLoadInfo) {
+    let uri = Services.io.newURI("chrome://flyweb/content/aboutFlyWeb.xhtml", null, null);
+    let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
+    channel.originalURI = aURI;
+    return channel;
+  }
+});
+
+function Factory(component) {
+  this.createInstance = function(outer, iid) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return new component();
+  };
+  this.register = function() {
+    Cm.registerFactory(component.prototype.classID, component.prototype.classDescription, component.prototype.contractID, this);
+  };
+  this.unregister = function() {
+    Cm.unregisterFactory(component.prototype.classID, this);
+  }
+  Object.freeze(this);
+  this.register();
+}
+
+let windowListener = {
+  onOpenWindow: function(aWindow) {
+    // Wait for the window to finish loading
+    let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+    domWindow.addEventListener("UIReady", function onLoad() {
+      domWindow.removeEventListener("UIReady", onLoad, false);
+      loadIntoWindow(domWindow);
+    }, false);
+  },
+
+  onCloseWindow: function(aWindow) {},
+  onWindowTitleChange: function(aWindow, aTitle) {}
+};
+
+let FlyWebUI = {
+  init() {
+    factory = new Factory(AboutFlyWeb);
+
+    // Load into any existing windows
+    let windows = Services.wm.getEnumerator("navigator:browser");
+    while (windows.hasMoreElements()) {
+      let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+      loadIntoWindow(domWindow);
+    }
+
+    // Load into any new windows
+    Services.wm.addListener(windowListener);
+  },
+
+  uninit() {
+    factory.unregister();
+
+    // Stop listening for new windows
+    Services.wm.removeListener(windowListener);
+
+    // Unload from any existing windows
+    let windows = Services.wm.getEnumerator("navigator:browser");
+    while (windows.hasMoreElements()) {
+      let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+      unloadFromWindow(domWindow);
+    }
+  }
+};
+
+function loadIntoWindow(aWindow) {
+  menuID = aWindow.NativeWindow.menu.add({
+    name: gFlyWebBundle.GetStringFromName("flyweb-menu.name"),
+    callback() {
+      aWindow.BrowserApp.addTab("about:flyweb");
+    }
+  });
+}
+
+function unloadFromWindow(aWindow) {
+  if (!aWindow) {
+    return;
+  }
+
+  aWindow.NativeWindow.menu.remove(menuID);
+}
+
+function prefObserver(aSubject, aTopic, aData) {
+  let enabled = Services.prefs.getBoolPref(FLYWEB_ENABLED_PREF);
+  if (enabled) {
+    FlyWebUI.init();
+  } else {
+    FlyWebUI.uninit();
+  }
+}
+
+function install(aData, aReason) {}
+
+function uninstall(aData, aReason) {}
+
+function startup(aData, aReason) {
+  // Observe pref changes and enable/disable as necessary.
+  Services.prefs.addObserver(FLYWEB_ENABLED_PREF, prefObserver, false);
+
+  // Only initialize if pref is enabled.
+  let enabled = Services.prefs.getBoolPref(FLYWEB_ENABLED_PREF);
+  if (enabled) {
+    FlyWebUI.init();
+  }
+}
+
+function shutdown(aData, aReason) {
+  Services.prefs.removeObserver(FLYWEB_ENABLED_PREF, prefObserver);
+
+  FlyWebUI.uninit();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.css
@@ -0,0 +1,29 @@
+/* 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/. */
+
+include "defines.css"
+
+.list-item > a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.details {
+  -moz-margin-start: calc(var(--icon-size) + var(--icon-margin) * 2 - 1em);
+  padding: 1em;
+}
+
+#flyweb-item-template {
+  display: none;
+}
+
+#flyweb-list-empty {
+  display: none;
+}
+
+#flyweb-list:empty + #flyweb-list-empty {
+  display: block;
+  text-align: center;
+  padding-top: 3.9em;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.js
@@ -0,0 +1,73 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Console.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gFlyWebBundle", function() {
+  return Services.strings.createBundle("chrome://flyweb/locale/flyweb.properties");
+});
+
+let discoveryManager = new FlyWebDiscoveryManager();
+
+let discoveryCallback = {
+  onDiscoveredServicesChanged(services) {
+    if (!this.id) {
+      return;
+    }
+
+    let list = document.getElementById("flyweb-list");
+    while (list.firstChild) {
+      list.firstChild.remove();
+    }
+
+    let template = document.getElementById("flyweb-item-template");
+
+    for (let service of services) {
+      let item = template.cloneNode(true);
+      item.removeAttribute("id");
+
+      item.setAttribute("data-service-id", service.serviceId);
+      item.querySelector(".title").setAttribute("value", service.displayName);
+      item.querySelector(".icon").src = "chrome://flyweb/content/icon-64.png";
+
+      list.appendChild(item);
+    }
+  },
+  start() {
+    this.id = discoveryManager.startDiscovery(this);
+  },
+  stop() {
+    discoveryManager.stopDiscovery(this.id);
+    this.id = undefined;
+  }
+};
+
+window.addEventListener("DOMContentLoaded", () => {
+  let list = document.getElementById("flyweb-list");
+  list.addEventListener("click", (evt) => {
+    let serviceId = evt.target.closest("[data-service-id]").getAttribute("data-service-id");
+
+    discoveryManager.pairWithService(serviceId, {
+      pairingSucceeded(service) {
+        window.open(service.uiUrl, "FlyWebWindow_" + serviceId);
+      },
+
+      pairingFailed(error) {
+        console.error("FlyWeb failed to connect to service " + serviceId, error);
+      }
+    });
+  });
+
+  discoveryCallback.start();
+});
+
+window.addEventListener("unload", () => {
+  discoveryCallback.stop();
+});
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/content/aboutFlyWeb.xhtml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % flywebDTD SYSTEM "chrome://flyweb/locale/aboutFlyWeb.dtd" >
+%flywebDTD;
+]>
+
+<!-- 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/.  -->
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+  <title>&aboutFlyWeb.title;</title>
+  <meta name="viewport" content="width=device-width; user-scalable=0" />
+  <link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
+  <link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
+  <link rel="stylesheet" href="chrome://flyweb/content/aboutFlyWeb.css" type="text/css"/>
+</head>
+
+<body dir="&locale.dir;">
+  <!--template id="flyweb-item-template"-->
+    <li id="flyweb-item-template" class="list-item" role="button">
+      <img class="icon" src=""/>
+      <div class="details">
+        <div class="row">
+          <!-- This is a hack so that we can crop this label in its center -->
+          <xul:label class="title" crop="center" value=""/>
+        </div>
+      </div>
+    </li>
+  <!--/template-->
+
+  <div class="header">
+    <div>&aboutFlyWeb.header;</div>
+  </div>
+  <ul id="flyweb-list" class="list"></ul>
+  <span id="flyweb-list-empty">&aboutFlyWeb.empty;</span>
+  <script type="application/javascript;version=1.8" src="chrome://flyweb/content/aboutFlyWeb.js"/>
+</body>
+</html>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..be8ece467b7b1df0683febca9253079af55abad2
GIT binary patch
literal 1311
zc$@(r1>pLLP)<h;3K|Lk000e1NJLTq002M$002M;1^@s6s%dfF0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU$!AV3xRCwC#TFq|SKoGWR4m~<gQ1S#x
z9-tLG0p!?I@e_m`D^&udN<9Kk5ck%eAWr~Py~nxr2D$Z??Gv;+(Ky{q#`dheSde(6
z6_UNb-_CsV?HbRkt+v`~tL^2QU#9WLPs2d}oanOo@_D#<F#za=_w>&|m!|Pp=`z)2
zqQOw4K7f>f6$pk{LVvoSc;^7ni@DT4y^Ja_>9z@ippBQ)ACwya#x*DhmWv+>76Q9G
z0}za7(-HujU`gh%K@YjoV9YD?0F3uLi@w1NmPY$E2^kj#U;qfyq8!w)sM}?pO>{Zb
z<$`(FVdC1g0ib<d)(~d8w6+-nboXM7db03|9{iZN`3b3ii$oa<Ts|lO28`X)^8lbP
zq+D@O0)4AV>IngV3@t(~iG2XJ#Q>USQ1@E5j}QpuiUYbxZl=wV;98g#ABBhEA*BsD
zsEeW16*hy#hgoFlXACyhWt5N$K3iMt{38-{X$BYrA+gR82$c%}y50;s1{azb@A?Q#
zo<+J{ScQ^9;v1kB=6OS#a)Zw(yi1a1PKl@)BlBjdxU;<2E5HR!>^tWAMj*xlt^iMn
zo&b=c)<#HRbg*+0i!tWo$l=ajGA^!goMB44K6*qT@lanVRZGV+?qoA%haywRFrH`?
zX1aY7+qd%C`+~)f($E&{U<VhRH4!*T9+Amk3&}QfZUWAVKnJW+08kjqOUqqOLPMG@
zX#eLaZ8uCfoM&kZAaFgW7)hP$bJf6@4O32ZOX+UwvcR3|S&F1vM##{)9HN(us2IzW
zzqp1rqd@P;uz{6K5MnM}<Scw4NJCN&pd2wxSieXpV!N1t@Q?`_MkRP~M$htTLIl%c
zk%cG6*`z4uy=liJ#JqwaC<Nh*O@j{7Q<e%_NiQ>W|1(DFMi?p_KOB|0j<dZGkt#vh
z*{=mP!IS|YElVv)`h<2(Q@uL755ROe7VI`C$97Rcgrp^Ds~GYI4;kZJ-Q5TY?*T9J
zvIYvA4)R8T?UtC&mBpAFjOCbw=EC+o;tOMbQAQ}?>1~!lnC!6?$NoV0<_Z_5NdF5a
zgXE@isRwY<R9U1;NwyI8$Mnw~&_YQVHiU6W#Zrvy8w<gcq{=`@u&fsnp%Y3us9`})
z2?()7D;U;EnQ+LcvlgBOisty(X7Pn|Kmqr>Bf0zGstgyq&Wc;SP9M2CW*7E=5=Mo~
zjj?kQ0ds$}1s*A*1S<gmgc5!|%F_CS#LaE;wfFzFx4)AB7((d{J#sQT3Li55GI@SO
z+@B-Qu_bWZ2Y`qcDq|<ifhS7_=Mm_=G<F4oD-T$aGkT9<=sXZ2M#KLRj#=1zhaq5!
zC51Xck)!OG(FB_;mQOi-hOV-L(==#+kg64s0F%Wip_nqIZn&+K=n8}`PS|z70Umd2
ztF5-$oS9$sfS3ga-s6uakS*q(@j3ok0)Sdz;iZ2#DSRoi^8i?U^8*l0tMY_Kg$^+A
zWS`6e@9E!do&^BiE!Eu{0D>*m-5UUQ(y9bNZK>|w05IQD-Ms-|B-~l$J^drZ{sVw{
zJ?~~H5ki%z!B<rh&d(pq-*4Zwz9Y+nFnVn){F^TC_2PfkCzRT1tF5-$s<D3p3;^ms
VNp_0}C&vH)002ovPDHLkV1l>jR9XN4
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/install.rdf.in
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+#filter substitution
+
+<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>flyweb@mozilla.org</em:id>
+    <em:version>1.0.0</em:version>
+    <em:type>2</em:type>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Target Application this theme can install into,
+        with minimum and maximum supported versions. -->
+    <em:targetApplication>
+      <Description>
+        <em:id>{aa3c5121-dab2-40e2-81ca-7ea25febc110}</em:id>
+        <em:minVersion>@FIREFOX_VERSION@</em:minVersion>
+        <em:maxVersion>@FIREFOX_VERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>FlyWeb</em:name>
+    <em:description>Discover nearby services in the browser</em:description>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/jar.mn
@@ -0,0 +1,10 @@
+#filter substitution
+# 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/.
+
+[features/flyweb@mozilla.org] chrome.jar:
+% content   flyweb              %content/        contentaccessible=yes
+  content/                      (content/*)
+% locale    flyweb en-US        %locale/en-US/
+  locale/                       (locale/*)
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/locale/en-US/aboutFlyWeb.dtd
@@ -0,0 +1,7 @@
+<!-- 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/. -->
+
+<!ENTITY aboutFlyWeb.title  "FlyWeb">
+<!ENTITY aboutFlyWeb.header "Nearby FlyWeb Services">
+<!ENTITY aboutFlyWeb.empty  "No FlyWeb Services Found">
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/locale/en-US/flyweb.properties
@@ -0,0 +1,5 @@
+# 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/.
+
+flyweb-menu.name = FlyWeb
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/flyweb/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# DIRS += ['locales']
+
+FINAL_TARGET_FILES.features['flyweb@mozilla.org'] += [
+  'bootstrap.js'
+]
+
+FINAL_TARGET_PP_FILES.features['flyweb@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+JAR_MANIFESTS += ['jar.mn']
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+# Only include the following system add-ons if building Aurora or Nightly
+if 'a' in CONFIG['GRE_MILESTONE']:
+    DIRS += [
+        'flyweb',
+    ]
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -444,16 +444,19 @@
 ; Security Reports
 @BINPATH@/components/SecurityReporter.manifest
 @BINPATH@/components/SecurityReporter.js
 
 ; [Browser Chrome Files]
 @BINPATH@/chrome/toolkit@JAREXT@
 @BINPATH@/chrome/toolkit.manifest
 
+; Features
+@BINPATH@/features/*
+
 ; DevTools
 @BINPATH@/chrome/devtools@JAREXT@
 @BINPATH@/chrome/devtools.manifest
 
 ; [Default Preferences]
 ; All the pref files must be part of base to prevent migration bugs
 @BINPATH@/@PREF_DIR@/mobile.js
 @BINPATH@/@PREF_DIR@/channel-prefs.js
--- a/mobile/android/moz.build
+++ b/mobile/android/moz.build
@@ -14,16 +14,17 @@ DIRS += [
 if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
     DIRS += ['stumbler']
 
 DIRS += [
     'javaaddons', # Must be built before base.
     'base',
     'chrome',
     'components',
+    'extensions',
     'modules',
     'themes/core',
     'app',
     'fonts',
     'geckoview_library',
 ]
 
 if CONFIG['MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER']:
--- a/python/mozbuild/mozbuild/action/package_fennec_apk.py
+++ b/python/mozbuild/mozbuild/action/package_fennec_apk.py
@@ -22,16 +22,17 @@ from mozpack.files import (
 )
 from mozpack.mozjar import JarReader
 import mozpack.path as mozpath
 
 
 def package_fennec_apk(inputs=[], omni_ja=None, classes_dex=None,
                        lib_dirs=[],
                        assets_dirs=[],
+                       features_dirs=[],
                        szip_assets_libs_with=None,
                        root_files=[],
                        verbose=False):
     jarrer = Jarrer(optimize=False)
 
     # First, take input files.  The contents of the later files overwrites the
     # content of earlier files.
     for input in inputs:
@@ -48,16 +49,21 @@ def package_fennec_apk(inputs=[], omni_j
             print('Packaging %s from %s' % (path, file.path))
         if not os.path.exists(abspath):
             raise ValueError('File %s not found (looked for %s)' % \
                              (file.path, abspath))
         if jarrer.contains(path):
             jarrer.remove(path)
         jarrer.add(path, file, compress=compress)
 
+    for features_dir in features_dirs:
+        finder = FileFinder(features_dir, find_executables=False)
+        for p, f in finder.find('**'):
+            add(mozpath.join('assets', 'features', p), f, False)
+
     for assets_dir in assets_dirs:
         finder = FileFinder(assets_dir, find_executables=False)
         for p, f in finder.find('**'):
             compress = None  # Take default from Jarrer.
             if p.endswith('.so'):
                 # Asset libraries are special.
                 if szip_assets_libs_with:
                     # We need to szip libraries before packing.  The file
@@ -100,32 +106,35 @@ def main(args):
     parser.add_argument('--omnijar', default=None,
                         help='Optional omni.ja to pack into APK file.')
     parser.add_argument('--classes-dex', default=None,
                         help='Optional classes.dex to pack into APK file.')
     parser.add_argument('--lib-dirs', nargs='*', default=[],
                         help='Optional lib/ dirs to pack into APK file.')
     parser.add_argument('--assets-dirs', nargs='*', default=[],
                         help='Optional assets/ dirs to pack into APK file.')
+    parser.add_argument('--features-dirs', nargs='*', default=[],
+                        help='Optional features/ dirs to pack into APK file.')
     parser.add_argument('--szip-assets-libs-with', default=None,
                         help='IN PLACE szip assets/**/*.so BEFORE packing '
                         'into APK file using the given szip executable.')
     parser.add_argument('--root-files', nargs='*', default=[],
                         help='Optional files to pack into APK file root.')
     args = parser.parse_args(args)
 
     if buildconfig.substs.get('OMNIJAR_NAME') != 'assets/omni.ja':
         raise ValueError("Don't know how package Fennec APKs when "
                          " OMNIJAR_NAME is not 'assets/omni.jar'.")
 
     jarrer = package_fennec_apk(inputs=args.inputs,
                                 omni_ja=args.omnijar,
                                 classes_dex=args.classes_dex,
                                 lib_dirs=args.lib_dirs,
                                 assets_dirs=args.assets_dirs,
+                                features_dirs=args.features_dirs,
                                 szip_assets_libs_with=args.szip_assets_libs_with,
                                 root_files=args.root_files,
                                 verbose=args.verbose)
     jarrer.copy(args.output)
 
     return 0
 
 
--- a/toolkit/mozapps/installer/upload-files-APK.mk
+++ b/toolkit/mozapps/installer/upload-files-APK.mk
@@ -145,16 +145,17 @@ INNER_FENNEC_PACKAGE = \
   $(PYTHON) -m mozbuild.action.package_fennec_apk \
     --verbose \
     --inputs \
       $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ \
     --omnijar $(STAGEPATH)$(MOZ_PKG_DIR)/$(OMNIJAR_NAME) \
     --classes-dex $(GECKO_APP_AP_PATH)/classes.dex \
     --lib-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/lib \
     --assets-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/assets \
+    --features-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/features \
     $(if $(COMPILE_ENVIRONMENT),$(if $(MOZ_ENABLE_SZIP),--szip-assets-libs-with $(ABS_DIST)/host/bin/szip)) \
     --root-files $(foreach f,$(ROOT_FILES),$(STAGEPATH)$(MOZ_PKG_DIR)/$(f)) \
     --output $(PACKAGE:.apk=-unsigned-unaligned.apk) && \
   $(call RELEASE_SIGN_ANDROID_APK,$(PACKAGE:.apk=-unsigned-unaligned.apk),$(PACKAGE))
 
 # Packaging produces many optional artifacts.
 package_fennec = \
   $(INNER_FENNEC_PACKAGE) && \