summaryrefslogtreecommitdiffstats
path: root/mobile/android/modules
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/modules')
-rw-r--r--mobile/android/modules/Accounts.jsm178
-rw-r--r--mobile/android/modules/AndroidLog.jsm92
-rw-r--r--mobile/android/modules/DelayedInit.jsm177
-rw-r--r--mobile/android/modules/DownloadNotifications.jsm291
-rw-r--r--mobile/android/modules/FxAccountsWebChannel.jsm394
-rw-r--r--mobile/android/modules/HelperApps.jsm229
-rw-r--r--mobile/android/modules/Home.jsm487
-rw-r--r--mobile/android/modules/HomeProvider.jsm407
-rw-r--r--mobile/android/modules/JNI.jsm1167
-rw-r--r--mobile/android/modules/JavaAddonManager.jsm115
-rw-r--r--mobile/android/modules/LightweightThemeConsumer.jsm44
-rw-r--r--mobile/android/modules/MediaPlayerApp.jsm166
-rw-r--r--mobile/android/modules/Messaging.jsm183
-rw-r--r--mobile/android/modules/NetErrorHelper.jsm175
-rw-r--r--mobile/android/modules/Notifications.jsm259
-rw-r--r--mobile/android/modules/PageActions.jsm113
-rw-r--r--mobile/android/modules/Prompt.jsm234
-rw-r--r--mobile/android/modules/RuntimePermissions.jsm41
-rw-r--r--mobile/android/modules/SSLExceptions.jsm118
-rw-r--r--mobile/android/modules/Sanitizer.jsm303
-rw-r--r--mobile/android/modules/SharedPreferences.jsm254
-rw-r--r--mobile/android/modules/Snackbars.jsm72
-rw-r--r--mobile/android/modules/TabMirror.jsm153
-rw-r--r--mobile/android/modules/WebsiteMetadata.jsm475
-rw-r--r--mobile/android/modules/dbg-browser-actors.js77
-rw-r--r--mobile/android/modules/moz.build33
26 files changed, 6237 insertions, 0 deletions
diff --git a/mobile/android/modules/Accounts.jsm b/mobile/android/modules/Accounts.jsm
new file mode 100644
index 000000000..a611f3c58
--- /dev/null
+++ b/mobile/android/modules/Accounts.jsm
@@ -0,0 +1,178 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["Accounts"];
+
+const { utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Deprecated.jsm"); /*global Deprecated */
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
+Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+
+/**
+ * A promise-based API for querying the existence of Sync accounts,
+ * and accessing the Sync setup wizard.
+ *
+ * Usage:
+ *
+ * Cu.import("resource://gre/modules/Accounts.jsm");
+ * Accounts.anySyncAccountsExist().then(
+ * (exist) => {
+ * console.log("Accounts exist? " + exist);
+ * if (!exist) {
+ * Accounts.launchSetup();
+ * }
+ * },
+ * (err) => {
+ * console.log("We failed so hard.");
+ * }
+ * );
+ */
+var Accounts = Object.freeze({
+ _accountsExist: function (kind) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:Exist",
+ kind: kind
+ }).then(data => data.exists);
+ },
+
+ firefoxAccountsExist: function () {
+ return this._accountsExist("fxa");
+ },
+
+ syncAccountsExist: function () {
+ Deprecated.warning("The legacy Sync account type has been removed from Firefox for Android. " +
+ "Please use `firefoxAccountsExist` instead.",
+ "https://developer.mozilla.org/en-US/Add-ons/Firefox_for_Android/API/Accounts.jsm");
+ return Promise.resolve(false);
+ },
+
+ anySyncAccountsExist: function () {
+ return this._accountsExist("any");
+ },
+
+ /**
+ * Fire-and-forget: open the Firefox accounts activity, which
+ * will be the Getting Started screen if FxA isn't yet set up.
+ *
+ * Optional extras are passed, as a JSON string, to the Firefox
+ * Account Getting Started activity in the extras bundle of the
+ * activity launch intent, under the key "extras".
+ *
+ * There is no return value from this method.
+ */
+ launchSetup: function (extras) {
+ Messaging.sendRequest({
+ type: "Accounts:Create",
+ extras: extras
+ });
+ },
+
+ _addDefaultEndpoints: function (json) {
+ let newData = Cu.cloneInto(json, {}, { cloneFunctions: false });
+ let associations = {
+ authServerEndpoint: 'identity.fxaccounts.auth.uri',
+ profileServerEndpoint: 'identity.fxaccounts.remote.profile.uri',
+ tokenServerEndpoint: 'identity.sync.tokenserver.uri'
+ };
+ for (let key in associations) {
+ newData[key] = newData[key] || Services.urlFormatter.formatURLPref(associations[key]);
+ }
+ return newData;
+ },
+
+ /**
+ * Create a new Android Account corresponding to the given
+ * fxa-content-server "login" JSON datum. The new account will be
+ * in the "Engaged" state, and will start syncing immediately.
+ *
+ * It is an error if an Android Account already exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ createFirefoxAccountFromJSON: function (json) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:CreateFirefoxAccountFromJSON",
+ json: this._addDefaultEndpoints(json)
+ });
+ },
+
+ /**
+ * Move an existing Android Account to the "Engaged" state with the given
+ * fxa-content-server "login" JSON datum. The account will (re)start
+ * syncing immediately, unless the user has manually configured the account
+ * to not Sync.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ updateFirefoxAccountFromJSON: function (json) {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:UpdateFirefoxAccountFromJSON",
+ json: this._addDefaultEndpoints(json)
+ });
+ },
+
+ /**
+ * Notify that profile for Android Account has updated.
+ * The account will re-fetch the profile image.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * There is no return value from this method.
+ */
+ notifyFirefoxAccountProfileChanged: function () {
+ Messaging.sendRequest({
+ type: "Accounts:ProfileUpdated",
+ });
+ },
+
+ /**
+ * Fetch information about an existing Android Firefox Account.
+ *
+ * Returns a Promise that resolves to null if no Android Firefox Account
+ * exists, or an object including at least a string-valued 'email' key.
+ */
+ getFirefoxAccount: function () {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:Exist",
+ kind: "fxa",
+ }).then(data => {
+ if (!data || !data.exists) {
+ return null;
+ }
+ delete data.exists;
+ return data;
+ });
+ },
+
+ /**
+ * Delete an existing Android Firefox Account.
+ *
+ * It is an error if no Android Account exists.
+ *
+ * Returns a Promise that resolves to a boolean indicating success.
+ */
+ deleteFirefoxAccount: function () {
+ return Messaging.sendRequestForResult({
+ type: "Accounts:DeleteFirefoxAccount",
+ });
+ },
+
+ showSyncPreferences: function () {
+ // Only show Sync preferences of an existing Android Account.
+ return Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
+ }
+ return Messaging.sendRequestForResult({
+ type: "Accounts:ShowSyncPreferences"
+ });
+ });
+ }
+});
diff --git a/mobile/android/modules/AndroidLog.jsm b/mobile/android/modules/AndroidLog.jsm
new file mode 100644
index 000000000..be6cca4e8
--- /dev/null
+++ b/mobile/android/modules/AndroidLog.jsm
@@ -0,0 +1,92 @@
+/* -*- 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/. */
+"use strict";
+
+/**
+ * Native Android logging for JavaScript. Lets you specify a priority and tag
+ * in addition to the message being logged. Resembles the android.util.Log API
+ * <http://developer.android.com/reference/android/util/Log.html>.
+ *
+ * // Import it as a JSM:
+ * let Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog;
+ *
+ * // Or require it in a chrome worker:
+ * importScripts("resource://gre/modules/workers/require.js");
+ * let Log = require("resource://gre/modules/AndroidLog.jsm");
+ *
+ * // Use Log.i, Log.v, Log.d, Log.w, and Log.e to log verbose, debug, info,
+ * // warning, and error messages, respectively.
+ * Log.v("MyModule", "This is a verbose message.");
+ * Log.d("MyModule", "This is a debug message.");
+ * Log.i("MyModule", "This is an info message.");
+ * Log.w("MyModule", "This is a warning message.");
+ * Log.e("MyModule", "This is an error message.");
+ *
+ * // Bind a function with a tag to replace a bespoke dump/log/debug function:
+ * let debug = Log.d.bind(null, "MyModule");
+ * debug("This is a debug message.");
+ * // Outputs "D/GeckoMyModule(#####): This is a debug message."
+ *
+ * // Or "bind" the module object to a tag to automatically tag messages:
+ * Log = Log.bind("MyModule");
+ * Log.d("This is a debug message.");
+ * // Outputs "D/GeckoMyModule(#####): This is a debug message."
+ *
+ * Note: the module automatically prepends "Gecko" to the tag you specify,
+ * since all tags used by Fennec code should start with that string; and it
+ * truncates tags longer than MAX_TAG_LENGTH characters (not including "Gecko").
+ */
+
+if (typeof Components != "undefined") {
+ // Specify exported symbols for JSM module loader.
+ this.EXPORTED_SYMBOLS = ["AndroidLog"];
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+}
+
+// From <https://android.googlesource.com/platform/system/core/+/master/include/android/log.h>.
+const ANDROID_LOG_VERBOSE = 2;
+const ANDROID_LOG_DEBUG = 3;
+const ANDROID_LOG_INFO = 4;
+const ANDROID_LOG_WARN = 5;
+const ANDROID_LOG_ERROR = 6;
+
+// android.util.Log.isLoggable throws IllegalArgumentException if a tag length
+// exceeds 23 characters, and we prepend five characters ("Gecko") to every tag,
+// so we truncate tags exceeding 18 characters (although __android_log_write
+// itself and other android.util.Log methods don't seem to mind longer tags).
+const MAX_TAG_LENGTH = 18;
+
+var liblog = ctypes.open("liblog.so"); // /system/lib/liblog.so
+var __android_log_write = liblog.declare("__android_log_write",
+ ctypes.default_abi,
+ ctypes.int, // return value: num bytes logged
+ ctypes.int, // priority (ANDROID_LOG_* constant)
+ ctypes.char.ptr, // tag
+ ctypes.char.ptr); // message
+
+var AndroidLog = {
+ MAX_TAG_LENGTH: MAX_TAG_LENGTH,
+ v: (tag, msg) => __android_log_write(ANDROID_LOG_VERBOSE, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ d: (tag, msg) => __android_log_write(ANDROID_LOG_DEBUG, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ i: (tag, msg) => __android_log_write(ANDROID_LOG_INFO, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ w: (tag, msg) => __android_log_write(ANDROID_LOG_WARN, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+ e: (tag, msg) => __android_log_write(ANDROID_LOG_ERROR, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+
+ bind: function(tag) {
+ return {
+ MAX_TAG_LENGTH: MAX_TAG_LENGTH,
+ v: AndroidLog.v.bind(null, tag),
+ d: AndroidLog.d.bind(null, tag),
+ i: AndroidLog.i.bind(null, tag),
+ w: AndroidLog.w.bind(null, tag),
+ e: AndroidLog.e.bind(null, tag),
+ };
+ },
+};
+
+if (typeof Components == "undefined") {
+ // Specify exported symbols for require.js module loader.
+ module.exports = AndroidLog;
+}
diff --git a/mobile/android/modules/DelayedInit.jsm b/mobile/android/modules/DelayedInit.jsm
new file mode 100644
index 000000000..7c33a3ede
--- /dev/null
+++ b/mobile/android/modules/DelayedInit.jsm
@@ -0,0 +1,177 @@
+/* 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"
+
+/*globals MessageLoop */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+this.EXPORTED_SYMBOLS = ["DelayedInit"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "MessageLoop",
+ "@mozilla.org/message-loop;1",
+ "nsIMessageLoop");
+
+/**
+ * Use DelayedInit to schedule initializers to run some time after startup.
+ * Initializers are added to a list of pending inits. Whenever the main thread
+ * message loop is idle, DelayedInit will start running initializers from the
+ * pending list. To prevent monopolizing the message loop, every idling period
+ * has a maximum duration. When that's reached, we give up the message loop and
+ * wait for the next idle.
+ *
+ * DelayedInit is compatible with lazy getters like those from XPCOMUtils. When
+ * the lazy getter is first accessed, its corresponding initializer is run
+ * automatically if it hasn't been run already. Each initializer also has a
+ * maximum wait parameter that specifies a mandatory timeout; when the timeout
+ * is reached, the initializer is forced to run.
+ *
+ * DelayedInit.schedule(() => Foo.init(), null, null, 5000);
+ *
+ * In the example above, Foo.init will run automatically when the message loop
+ * becomes idle, or when 5000ms has elapsed, whichever comes first.
+ *
+ * DelayedInit.schedule(() => Foo.init(), this, "Foo", 5000);
+ *
+ * In the example above, Foo.init will run automatically when the message loop
+ * becomes idle, when |this.Foo| is accessed, or when 5000ms has elapsed,
+ * whichever comes first.
+ *
+ * It may be simpler to have a wrapper for DelayedInit.schedule. For example,
+ *
+ * function InitLater(fn, obj, name) {
+ * return DelayedInit.schedule(fn, obj, name, 5000); // constant max wait
+ * }
+ * InitLater(() => Foo.init());
+ * InitLater(() => Bar.init(), this, "Bar");
+ */
+var DelayedInit = {
+ schedule: function (fn, object, name, maxWait) {
+ return Impl.scheduleInit(fn, object, name, maxWait);
+ },
+};
+
+// Maximum duration for each idling period. Pending inits are run until this
+// duration is exceeded; then we wait for next idling period.
+const MAX_IDLE_RUN_MS = 50;
+
+var Impl = {
+ pendingInits: [],
+
+ onIdle: function () {
+ let startTime = Cu.now();
+ let time = startTime;
+ let nextDue;
+
+ // Go through all the pending inits. Even if we don't run them,
+ // we still need to find out when the next timeout should be.
+ for (let init of this.pendingInits) {
+ if (init.complete) {
+ continue;
+ }
+
+ if (time - startTime < MAX_IDLE_RUN_MS) {
+ init.maybeInit();
+ time = Cu.now();
+ } else {
+ // We ran out of time; find when the next closest due time is.
+ nextDue = nextDue ? Math.min(nextDue, init.due) : init.due;
+ }
+ }
+
+ // Get rid of completed ones.
+ this.pendingInits = this.pendingInits.filter((init) => !init.complete);
+
+ if (nextDue !== undefined) {
+ // Schedule the next idle, if we still have pending inits.
+ MessageLoop.postIdleTask(() => this.onIdle(),
+ Math.max(0, nextDue - time));
+ }
+ },
+
+ addPendingInit: function (fn, wait) {
+ let init = {
+ fn: fn,
+ due: Cu.now() + wait,
+ complete: false,
+ maybeInit: function () {
+ if (this.complete) {
+ return false;
+ }
+ this.complete = true;
+ this.fn.call();
+ this.fn = null;
+ return true;
+ },
+ };
+
+ if (!this.pendingInits.length) {
+ // Schedule for the first idle.
+ MessageLoop.postIdleTask(() => this.onIdle(), wait);
+ }
+ this.pendingInits.push(init);
+ return init;
+ },
+
+ scheduleInit: function (fn, object, name, wait) {
+ let init = this.addPendingInit(fn, wait);
+
+ if (!object || !name) {
+ // No lazy getter needed.
+ return;
+ }
+
+ // Get any existing information about the property.
+ let prop = Object.getOwnPropertyDescriptor(object, name) ||
+ { configurable: true, enumerable: true, writable: true };
+
+ if (!prop.configurable) {
+ // Object.defineProperty won't work, so just perform init here.
+ init.maybeInit();
+ return;
+ }
+
+ // Define proxy getter/setter that will call first initializer first,
+ // before delegating the get/set to the original target.
+ Object.defineProperty(object, name, {
+ get: function proxy_getter() {
+ init.maybeInit();
+
+ // If the initializer actually ran, it may have replaced our proxy
+ // property with a real one, so we need to reload he property.
+ let newProp = Object.getOwnPropertyDescriptor(object, name);
+ if (newProp.get !== proxy_getter) {
+ // Set prop if newProp doesn't refer to our proxy property.
+ prop = newProp;
+ } else {
+ // Otherwise, reset to the original property.
+ Object.defineProperty(object, name, prop);
+ }
+
+ if (prop.get) {
+ return prop.get.call(object);
+ }
+ return prop.value;
+ },
+ set: function (newVal) {
+ init.maybeInit();
+
+ // Since our initializer already ran,
+ // we can get rid of our proxy property.
+ if (prop.get || prop.set) {
+ Object.defineProperty(object, name, prop);
+ return prop.set.call(object);
+ }
+
+ prop.value = newVal;
+ Object.defineProperty(object, name, prop);
+ return newVal;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+};
diff --git a/mobile/android/modules/DownloadNotifications.jsm b/mobile/android/modules/DownloadNotifications.jsm
new file mode 100644
index 000000000..39b520979
--- /dev/null
+++ b/mobile/android/modules/DownloadNotifications.jsm
@@ -0,0 +1,291 @@
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["DownloadNotifications"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
+ "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");
+
+var Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.i.bind(null, "DownloadNotifications");
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
+
+Object.defineProperty(this, "window",
+ { get: () => Services.wm.getMostRecentWindow("navigator:browser") });
+
+const kButtons = {
+ PAUSE: new DownloadNotificationButton("pause",
+ "drawable://pause",
+ "alertDownloadsPause"),
+ RESUME: new DownloadNotificationButton("resume",
+ "drawable://play",
+ "alertDownloadsResume"),
+ CANCEL: new DownloadNotificationButton("cancel",
+ "drawable://close",
+ "alertDownloadsCancel")
+};
+
+var notifications = new Map();
+
+var DownloadNotifications = {
+ _notificationKey: "downloads",
+
+ init: function () {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.addView(this))
+ .then(() => this._viewAdded = true, Cu.reportError);
+
+ // All click, cancel, and button presses will be handled by this handler as part of the Notifications callback API.
+ Notifications.registerHandler(this._notificationKey, this);
+ },
+
+ onDownloadAdded: function (download) {
+ // Don't create notifications for pre-existing succeeded downloads.
+ // We still add notifications for canceled downloads in case the
+ // user decides to retry the download.
+ if (download.succeeded && !this._viewAdded) {
+ return;
+ }
+
+ if (!ParentalControls.isAllowed(ParentalControls.DOWNLOAD)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ Snackbars.show(strings.GetStringFromName("downloads.disabledInGuest"), Snackbars.LENGTH_LONG);
+ return;
+ }
+
+ let notification = new DownloadNotification(download);
+ notifications.set(download, notification);
+ notification.showOrUpdate();
+
+ // If this is a new download, show a snackbar as well.
+ if (this._viewAdded) {
+ Snackbars.show(strings.GetStringFromName("alertDownloadsToast"), Snackbars.LENGTH_LONG);
+ }
+ },
+
+ onDownloadChanged: function (download) {
+ let notification = notifications.get(download);
+
+ if (download.succeeded) {
+ let file = new FileUtils.File(download.target.path);
+
+ Snackbars.show(strings.formatStringFromName("alertDownloadSucceeded", [file.leafName], 1), Snackbars.LENGTH_LONG, {
+ action: {
+ label: strings.GetStringFromName("helperapps.open"),
+ callback: () => {
+ UITelemetry.addEvent("launch.1", "toast", null, "downloads");
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ if (notification) {
+ notification.hide();
+ }
+ }
+ }});
+ }
+
+ if (notification) {
+ notification.showOrUpdate();
+ }
+ },
+
+ onDownloadRemoved: function (download) {
+ let notification = notifications.get(download);
+ if (!notification) {
+ Cu.reportError("Download doesn't have a notification.");
+ return;
+ }
+
+ notification.hide();
+ notifications.delete(download);
+ },
+
+ _findDownloadForCookie: function(cookie) {
+ return Downloads.getList(Downloads.ALL)
+ .then(list => list.getAll())
+ .then((downloads) => {
+ for (let download of downloads) {
+ let cookie2 = getCookieFromDownload(download);
+ if (cookie2 === cookie) {
+ return download;
+ }
+ }
+
+ throw "Couldn't find download for " + cookie;
+ });
+ },
+
+ onCancel: function(cookie) {
+ // TODO: I'm not sure what we do here...
+ },
+
+ showInAboutDownloads: function (download) {
+ let hash = "#" + window.encodeURIComponent(download.target.path);
+
+ // Force using string equality to find a tab
+ window.BrowserApp.selectOrAddTab("about:downloads" + hash, null, { startsWith: true });
+ },
+
+ onClick: function(cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (download.succeeded) {
+ // We don't call Download.launch(), because there's (currently) no way to
+ // tell if the file was actually launched or not, and we want to show
+ // about:downloads if the launch failed.
+ let file = new FileUtils.File(download.target.path);
+ try {
+ file.launch();
+ } catch (ex) {
+ this.showInAboutDownloads(download);
+ }
+ } else {
+ ConfirmCancelPrompt.show(download);
+ }
+ }).catch(Cu.reportError);
+ },
+
+ onButtonClick: function(button, cookie) {
+ this._findDownloadForCookie(cookie).then((download) => {
+ if (button === kButtons.PAUSE.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ } else if (button === kButtons.RESUME.buttonId) {
+ download.start().catch(Cu.reportError);
+ } else if (button === kButtons.CANCEL.buttonId) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }).catch(Cu.reportError);
+ },
+};
+
+function getCookieFromDownload(download) {
+ return download.target.path +
+ download.source.url +
+ download.startTime;
+}
+
+function DownloadNotification(download) {
+ this.download = download;
+ this._fileName = OS.Path.basename(download.target.path);
+
+ this.id = null;
+}
+
+DownloadNotification.prototype = {
+ _updateFromDownload: function () {
+ this._downloading = !this.download.stopped;
+ this._paused = this.download.canceled && this.download.hasPartialData;
+ this._succeeded = this.download.succeeded;
+
+ this._show = this._downloading || this._paused || this._succeeded;
+ },
+
+ get options() {
+ if (!this._show) {
+ return null;
+ }
+
+ let options = {
+ icon: "drawable://alert_download",
+ cookie: getCookieFromDownload(this.download),
+ handlerKey: DownloadNotifications._notificationKey
+ };
+
+ if (this._downloading) {
+ options.icon = "drawable://alert_download_animation";
+ if (this.download.currentBytes == 0) {
+ this._updateOptionsForStatic(options, "alertDownloadsStart2");
+ } else {
+ let buttons = this.download.hasPartialData ? [kButtons.PAUSE, kButtons.CANCEL] :
+ [kButtons.CANCEL]
+ this._updateOptionsForOngoing(options, buttons);
+ }
+ } else if (this._paused) {
+ this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
+ } else if (this._succeeded) {
+ options.persistent = false;
+ this._updateOptionsForStatic(options, "alertDownloadsDone2");
+ }
+
+ return options;
+ },
+
+ _updateOptionsForStatic : function (options, titleName) {
+ options.title = strings.GetStringFromName(titleName);
+ options.message = this._fileName;
+ },
+
+ _updateOptionsForOngoing: function (options, buttons) {
+ options.title = this._fileName;
+ options.message = this.download.progress + "%";
+ options.buttons = buttons;
+ options.ongoing = true;
+ options.progress = this.download.progress;
+ options.persistent = true;
+ },
+
+ showOrUpdate: function () {
+ this._updateFromDownload();
+
+ if (this._show) {
+ if (!this.id) {
+ this.id = Notifications.create(this.options);
+ } else if (!this.options.ongoing) {
+ // We need to explictly cancel ongoing notifications,
+ // since updating them to be non-ongoing doesn't seem
+ // to work. See bug 1130834.
+ Notifications.cancel(this.id);
+ this.id = Notifications.create(this.options);
+ } else {
+ Notifications.update(this.id, this.options);
+ }
+ } else {
+ this.hide();
+ }
+ },
+
+ hide: function () {
+ if (this.id) {
+ Notifications.cancel(this.id);
+ this.id = null;
+ }
+ },
+};
+
+var ConfirmCancelPrompt = {
+ show: function (download) {
+ // Open a prompt that offers a choice to cancel the download
+ let title = strings.GetStringFromName("downloadCancelPromptTitle1");
+ let message = strings.GetStringFromName("downloadCancelPromptMessage1");
+
+ if (Services.prompt.confirm(null, title, message)) {
+ download.cancel().catch(Cu.reportError);
+ download.removePartialData().catch(Cu.reportError);
+ }
+ }
+};
+
+function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
+ this.buttonId = buttonId;
+ this.title = strings.GetStringFromName(titleStringName);
+ this.icon = iconUrl;
+}
diff --git a/mobile/android/modules/FxAccountsWebChannel.jsm b/mobile/android/modules/FxAccountsWebChannel.jsm
new file mode 100644
index 000000000..6ee8fd07f
--- /dev/null
+++ b/mobile/android/modules/FxAccountsWebChannel.jsm
@@ -0,0 +1,394 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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/. */
+
+/**
+ * Firefox Accounts Web Channel.
+ *
+ * Use the WebChannel component to receive messages about account
+ * state changes.
+ */
+this.EXPORTED_SYMBOLS = ["EnsureFxAccountsWebChannel"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Accounts.jsm"); /*global Accounts */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+Cu.import("resource://gre/modules/WebChannel.jsm"); /*global WebChannel */
+Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global XPCOMUtils */
+
+const log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccounts");
+
+const WEBCHANNEL_ID = "account_updates";
+
+const COMMAND_LOADED = "fxaccounts:loaded";
+const COMMAND_CAN_LINK_ACCOUNT = "fxaccounts:can_link_account";
+const COMMAND_LOGIN = "fxaccounts:login";
+const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
+const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account";
+const COMMAND_PROFILE_CHANGE = "profile:change";
+const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
+
+const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
+
+XPCOMUtils.defineLazyGetter(this, "strings",
+ () => Services.strings.createBundle("chrome://browser/locale/aboutAccounts.properties")); /*global strings */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry", "resource://gre/modules/UITelemetry.jsm");
+
+this.FxAccountsWebChannelHelpers = function() {
+};
+
+this.FxAccountsWebChannelHelpers.prototype = {
+ /**
+ * Get the hash of account name of the previously signed in account.
+ */
+ getPreviousAccountNameHashPref() {
+ try {
+ return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
+ } catch (_) {
+ return "";
+ }
+ },
+
+ /**
+ * Given an account name, set the hash of the previously signed in account.
+ *
+ * @param acctName the account name of the user's account.
+ */
+ setPreviousAccountNameHashPref(acctName) {
+ let string = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ string.data = this.sha256(acctName);
+ Services.prefs.setComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString, string);
+ },
+
+ /**
+ * Given a string, returns the SHA265 hash in base64.
+ */
+ sha256(str) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // Data is an array of bytes.
+ let data = converter.convertToByteArray(str, {});
+ let hasher = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA256);
+ hasher.update(data, data.length);
+
+ return hasher.finish(true);
+ },
+};
+
+/**
+ * Create a new FxAccountsWebChannel to listen for account updates.
+ *
+ * @param {Object} options Options
+ * @param {Object} options
+ * @param {String} options.content_uri
+ * The FxA Content server uri
+ * @param {String} options.channel_id
+ * The ID of the WebChannel
+ * @param {String} options.helpers
+ * Helpers functions. Should only be passed in for testing.
+ * @constructor
+ */
+this.FxAccountsWebChannel = function(options) {
+ if (!options) {
+ throw new Error("Missing configuration options");
+ }
+ if (!options["content_uri"]) {
+ throw new Error("Missing 'content_uri' option");
+ }
+ this._contentUri = options.content_uri;
+
+ if (!options["channel_id"]) {
+ throw new Error("Missing 'channel_id' option");
+ }
+ this._webChannelId = options.channel_id;
+
+ // options.helpers is only specified by tests.
+ this._helpers = options.helpers || new FxAccountsWebChannelHelpers(options);
+
+ this._setupChannel();
+};
+
+this.FxAccountsWebChannel.prototype = {
+ /**
+ * WebChannel that is used to communicate with content page
+ */
+ _channel: null,
+
+ /**
+ * WebChannel ID.
+ */
+ _webChannelId: null,
+ /**
+ * WebChannel origin, used to validate origin of messages
+ */
+ _webChannelOrigin: null,
+
+ /**
+ * Release all resources that are in use.
+ */
+ tearDown() {
+ this._channel.stopListening();
+ this._channel = null;
+ this._channelCallback = null;
+ },
+
+ /**
+ * Configures and registers a new WebChannel
+ *
+ * @private
+ */
+ _setupChannel() {
+ // if this.contentUri is present but not a valid URI, then this will throw an error.
+ try {
+ this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
+ this._registerChannel();
+ } catch (e) {
+ log.e(e.toString());
+ throw e;
+ }
+ },
+
+ /**
+ * Create a new channel with the WebChannelBroker, setup a callback listener
+ * @private
+ */
+ _registerChannel() {
+ /**
+ * Processes messages that are called back from the FxAccountsChannel
+ *
+ * @param webChannelId {String}
+ * Command webChannelId
+ * @param message {Object}
+ * Command message
+ * @param sendingContext {Object}
+ * Message sending context.
+ * @param sendingContext.browser {browser}
+ * The <browser> object that captured the
+ * WebChannelMessageToChrome.
+ * @param sendingContext.eventTarget {EventTarget}
+ * The <EventTarget> where the message was sent.
+ * @param sendingContext.principal {Principal}
+ * The <Principal> of the EventTarget where the message was sent.
+ * @private
+ *
+ */
+ let listener = (webChannelId, message, sendingContext) => {
+ if (message) {
+ let command = message.command;
+ let data = message.data;
+ log.d("FxAccountsWebChannel message received, command: " + command);
+
+ // Respond to the message with true or false.
+ let respond = (data) => {
+ let response = {
+ command: command,
+ messageId: message.messageId,
+ data: data
+ };
+ log.d("Sending response to command: " + command);
+ this._channel.send(response, sendingContext);
+ };
+
+ switch (command) {
+ case COMMAND_LOADED:
+ let mm = sendingContext.browser.docShell
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+ mm.sendAsyncMessage(COMMAND_LOADED);
+ break;
+
+ case COMMAND_CAN_LINK_ACCOUNT:
+ Accounts.getFirefoxAccount().then(account => {
+ if (account) {
+ // If we /have/ an Android Account, we never allow the user to
+ // login to a different account. They need to manually delete
+ // the first Android Account and then create a new one.
+ if (account.email == data.email) {
+ // In future, we should use a UID for this comparison.
+ log.d("Relinking existing Android Account: email addresses agree.");
+ respond({ok: true});
+ } else {
+ log.w("Not relinking existing Android Account: email addresses disagree!");
+ let message = strings.GetStringFromName("relinkDenied.message");
+ let buttonLabel = strings.GetStringFromName("relinkDenied.openPrefs");
+ Snackbars.show(message, Snackbars.LENGTH_LONG, {
+ action: {
+ label: buttonLabel,
+ callback: () => {
+ // We have an account, so this opens Sync native preferences.
+ Accounts.launchSetup();
+ },
+ }
+ });
+ respond({ok: false});
+ }
+ } else {
+ // If we /don't have/ an Android Account, we warn if we're
+ // connecting to a new Account. This is to minimize surprise;
+ // we never did this when changing accounts via the native UI.
+ let prevAcctHash = this._helpers.getPreviousAccountNameHashPref();
+ let shouldShowWarning = prevAcctHash && (prevAcctHash != this._helpers.sha256(data.email));
+
+ if (shouldShowWarning) {
+ log.w("Warning about creating a new Android Account: previously linked to different email address!");
+ let message = strings.formatStringFromName("relinkVerify.message", [data.email], 1);
+ new Prompt({
+ title: strings.GetStringFromName("relinkVerify.title"),
+ message: message,
+ buttons: [
+ // This puts Cancel on the right.
+ strings.GetStringFromName("relinkVerify.cancel"),
+ strings.GetStringFromName("relinkVerify.continue"),
+ ],
+ }).show(result => respond({ok: result && result.button == 1}));
+ } else {
+ log.d("Not warning about creating a new Android Account: no previously linked email address.");
+ respond({ok: true});
+ }
+ }
+ }).catch(e => {
+ log.e(e.toString());
+ respond({ok: false});
+ });
+ break;
+
+ case COMMAND_LOGIN:
+ // Either create a new Android Account or re-connect an existing
+ // Android Account here. There's not much to be done if we don't
+ // succeed or get an error.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ return Accounts.createFirefoxAccountFromJSON(data).then(success => {
+ if (!success) {
+ throw new Error("Could not create Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-create");
+ return success;
+ });
+ } else {
+ return Accounts.updateFirefoxAccountFromJSON(data).then(success => {
+ if (!success) {
+ throw new Error("Could not update Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-login");
+ return success;
+ });
+ }
+ })
+ .then(success => {
+ if (!success) {
+ throw new Error("Could not create or update Firefox Account!");
+ }
+
+ // Remember who it is so we can show a relink warning when appropriate.
+ this._helpers.setPreviousAccountNameHashPref(data.email);
+
+ log.i("Created or updated Firefox Account.");
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_CHANGE_PASSWORD:
+ // Only update an existing Android Account.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't change password of non-existent Firefox Account!");
+ }
+ return Accounts.updateFirefoxAccountFromJSON(data);
+ })
+ .then(success => {
+ if (!success) {
+ throw new Error("Could not change Firefox Account password!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-changepassword");
+ log.i("Changed Firefox Account password.");
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_DELETE_ACCOUNT:
+ // The fxa-content-server has already confirmed the user's intent.
+ // Bombs away. There's no recovery from failure, and not even a
+ // real need to check an account exists (although we do, for error
+ // messaging only).
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't delete non-existent Firefox Account!");
+ }
+ return Accounts.deleteFirefoxAccount().then(success => {
+ if (!success) {
+ throw new Error("Could not delete Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-delete");
+ log.i("Firefox Account deleted.");
+ });
+ }).catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_PROFILE_CHANGE:
+ // Only update an existing Android Account.
+ Accounts.getFirefoxAccount().then(account => {
+ if (!account) {
+ throw new Error("Can't change profile of non-existent Firefox Account!");
+ }
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-changeprofile");
+ return Accounts.notifyFirefoxAccountProfileChanged();
+ })
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ case COMMAND_SYNC_PREFERENCES:
+ UITelemetry.addEvent("action.1", "content", null, "fxaccount-syncprefs");
+ Accounts.showSyncPreferences()
+ .catch(e => {
+ log.e(e.toString());
+ });
+ break;
+
+ default:
+ log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
+ break;
+ }
+ }
+ };
+
+ this._channelCallback = listener;
+ this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
+ this._channel.listen(listener);
+
+ log.d("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
+ }
+};
+
+var singleton;
+// The entry-point for this module, which ensures only one of our channels is
+// ever created - we require this because the WebChannel is global in scope and
+// allowing multiple channels would cause such notifications to be sent multiple
+// times.
+this.EnsureFxAccountsWebChannel = function() {
+ if (!singleton) {
+ let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri");
+ // The FxAccountsWebChannel listens for events and updates the Java layer.
+ singleton = new this.FxAccountsWebChannel({
+ content_uri: contentUri,
+ channel_id: WEBCHANNEL_ID,
+ });
+ }
+};
diff --git a/mobile/android/modules/HelperApps.jsm b/mobile/android/modules/HelperApps.jsm
new file mode 100644
index 000000000..0ac478da0
--- /dev/null
+++ b/mobile/android/modules/HelperApps.jsm
@@ -0,0 +1,229 @@
+/* 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";
+
+/* globals ContentAreaUtils */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
+ "resource://gre/modules/Prompt.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
+ "resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+this.EXPORTED_SYMBOLS = ["App","HelperApps"];
+
+function App(data) {
+ this.name = data.name;
+ this.isDefault = data.isDefault;
+ this.packageName = data.packageName;
+ this.activityName = data.activityName;
+ this.iconUri = "-moz-icon://" + data.packageName;
+}
+
+App.prototype = {
+ // callback will be null if a result is not requested
+ launch: function(uri, callback) {
+ HelperApps._launchApp(this, uri, callback);
+ return false;
+ }
+}
+
+var HelperApps = {
+ get defaultBrowsers() {
+ delete this.defaultBrowsers;
+ this.defaultBrowsers = this._getHandlers("http://www.example.com", {
+ filterBrowsers: false,
+ filterHtml: false
+ });
+ return this.defaultBrowsers;
+ },
+
+ // Finds handlers that have registered for text/html pages or urls ending in html. Some apps, like
+ // the Samsung Video player will only appear for these urls, while some Browsers (like Link Bubble)
+ // won't register here because of the text/html mime type.
+ get defaultHtmlHandlers() {
+ delete this.defaultHtmlHandlers;
+ return this.defaultHtmlHandlers = this._getHandlers("http://www.example.com/index.html", {
+ filterBrowsers: false,
+ filterHtml: false
+ });
+ },
+
+ _getHandlers: function(url, options) {
+ let values = {};
+
+ let handlers = this.getAppsForUri(Services.io.newURI(url, null, null), options);
+ handlers.forEach(function(app) {
+ values[app.name] = app;
+ }, this);
+
+ return values;
+ },
+
+ get protoSvc() {
+ delete this.protoSvc;
+ return this.protoSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].getService(Ci.nsIExternalProtocolService);
+ },
+
+ get urlHandlerService() {
+ delete this.urlHandlerService;
+ return this.urlHandlerService = Cc["@mozilla.org/uriloader/external-url-handler-service;1"].getService(Ci.nsIExternalURLHandlerService);
+ },
+
+ prompt: function showPicker(apps, promptOptions, callback) {
+ let p = new Prompt(promptOptions).addIconGrid({ items: apps });
+ p.show(callback);
+ },
+
+ getAppsForProtocol: function getAppsForProtocol(scheme) {
+ let protoHandlers = this.protoSvc.getProtocolHandlerInfoFromOS(scheme, {}).possibleApplicationHandlers;
+
+ let results = {};
+ for (let i = 0; i < protoHandlers.length; i++) {
+ try {
+ let protoApp = protoHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ results[protoApp.name] = new App({
+ name: protoApp.name,
+ description: protoApp.detailedDescription,
+ });
+ } catch(e) {}
+ }
+
+ return results;
+ },
+
+ getAppsForUri: function getAppsForUri(uri, flags = { }, callback) {
+ // Return early for well-known internal schemes
+ if (!uri || uri.schemeIs("about") || uri.schemeIs("chrome")) {
+ if (callback) {
+ callback([]);
+ }
+ return [];
+ }
+
+ flags.filterBrowsers = "filterBrowsers" in flags ? flags.filterBrowsers : true;
+ flags.filterHtml = "filterHtml" in flags ? flags.filterHtml : true;
+
+ // Query for apps that can/can't handle the mimetype
+ let msg = this._getMessage("Intent:GetHandlers", uri, flags);
+ let parseData = (d) => {
+ let apps = []
+ if (!d) {
+ return apps;
+ }
+
+ apps = this._parseApps(d.apps);
+
+ if (flags.filterBrowsers) {
+ apps = apps.filter((app) => {
+ return app.name && !this.defaultBrowsers[app.name];
+ });
+ }
+
+ // Some apps will register for html files (the Samsung Video player) but should be shown
+ // for non-HTML files (like videos). This filters them only if the page has an htm of html
+ // file extension.
+ if (flags.filterHtml) {
+ // Matches from the first '.' to the end of the string, '?', or '#'
+ let ext = /\.([^\?#]*)/.exec(uri.path);
+ if (ext && (ext[1] === "html" || ext[1] === "htm")) {
+ apps = apps.filter(function(app) {
+ return app.name && !this.defaultHtmlHandlers[app.name];
+ }, this);
+ }
+ }
+
+ return apps;
+ };
+
+ if (!callback) {
+ let data = this._sendMessageSync(msg);
+ return parseData(data);
+ } else {
+ Messaging.sendRequestForResult(msg).then(function(data) {
+ callback(parseData(data));
+ });
+ }
+ },
+
+ launchUri: function launchUri(uri) {
+ let msg = this._getMessage("Intent:Open", uri);
+ Messaging.sendRequest(msg);
+ },
+
+ _parseApps: function _parseApps(appInfo) {
+ // appInfo -> {apps: [app1Label, app1Default, app1PackageName, app1ActivityName, app2Label, app2Defaut, ...]}
+ // see GeckoAppShell.java getHandlersForIntent function for details
+ const numAttr = 4; // 4 elements per ResolveInfo: label, default, package name, activity name.
+
+ let apps = [];
+ for (let i = 0; i < appInfo.length; i += numAttr) {
+ apps.push(new App({"name" : appInfo[i],
+ "isDefault" : appInfo[i+1],
+ "packageName" : appInfo[i+2],
+ "activityName" : appInfo[i+3]}));
+ }
+
+ return apps;
+ },
+
+ _getMessage: function(type, uri, options = {}) {
+ let mimeType = options.mimeType;
+ if (uri && mimeType == undefined) {
+ mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
+ }
+
+ return {
+ type: type,
+ mime: mimeType,
+ action: options.action || "", // empty action string defaults to android.intent.action.VIEW
+ url: uri ? uri.spec : "",
+ packageName: options.packageName || "",
+ className: options.className || ""
+ };
+ },
+
+ _launchApp: function launchApp(app, uri, callback) {
+ if (callback) {
+ let msg = this._getMessage("Intent:OpenForResult", uri, {
+ packageName: app.packageName,
+ className: app.activityName
+ });
+
+ Messaging.sendRequestForResult(msg).then(callback);
+ } else {
+ let msg = this._getMessage("Intent:Open", uri, {
+ packageName: app.packageName,
+ className: app.activityName
+ });
+
+ Messaging.sendRequest(msg);
+ }
+ },
+
+ _sendMessageSync: function(msg) {
+ let res = null;
+ Messaging.sendRequestForResult(msg).then(function(data) {
+ res = data;
+ });
+
+ let thread = Services.tm.currentThread;
+ while (res == null) {
+ thread.processNextEvent(true);
+ }
+
+ return res;
+ },
+};
diff --git a/mobile/android/modules/Home.jsm b/mobile/android/modules/Home.jsm
new file mode 100644
index 000000000..e77d35dbd
--- /dev/null
+++ b/mobile/android/modules/Home.jsm
@@ -0,0 +1,487 @@
+// -*- 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Home"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/SharedPreferences.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+// Keep this in sync with the constant defined in PanelAuthCache.java
+const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
+
+// Default weight for a banner message.
+const DEFAULT_WEIGHT = 100;
+
+// See bug 915424
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+function BannerMessage(options) {
+ let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ this.id = uuidgen.generateUUID().toString();
+
+ if ("text" in options && options.text != null)
+ this.text = options.text;
+
+ if ("icon" in options && options.icon != null)
+ this.iconURI = resolveGeckoURI(options.icon);
+
+ if ("onshown" in options && typeof options.onshown === "function")
+ this.onshown = options.onshown;
+
+ if ("onclick" in options && typeof options.onclick === "function")
+ this.onclick = options.onclick;
+
+ if ("ondismiss" in options && typeof options.ondismiss === "function")
+ this.ondismiss = options.ondismiss;
+
+ let weight = parseInt(options.weight, 10);
+ this.weight = weight > 0 ? weight : DEFAULT_WEIGHT;
+}
+
+// We need this object to have access to the HomeBanner
+// private members without leaking it outside Home.jsm.
+var HomeBannerMessageHandlers;
+
+var HomeBanner = (function () {
+ // Whether there is a "HomeBanner:Get" request we couldn't fulfill.
+ let _pendingRequest = false;
+
+ // Functions used to handle messages sent from Java.
+ HomeBannerMessageHandlers = {
+ "HomeBanner:Get": function handleBannerGet(data) {
+ if (Object.keys(_messages).length > 0) {
+ _sendBannerData();
+ } else {
+ _pendingRequest = true;
+ }
+ }
+ };
+
+ // Holds the messages that will rotate through the banner.
+ let _messages = {};
+
+ // Choose a random message from the set of messages, biasing towards those with higher weight.
+ // Weight logic copied from desktop snippets:
+ // https://github.com/mozilla/snippets-service/blob/7d80edb8b1cddaed075275c2fc7cdf69a10f4003/snippets/base/templates/base/includes/snippet_js.html#L119
+ let _sendBannerData = function() {
+ let totalWeight = 0;
+ for (let key in _messages) {
+ let message = _messages[key];
+ totalWeight += message.weight;
+ message.totalWeight = totalWeight;
+ }
+
+ let threshold = Math.random() * totalWeight;
+ for (let key in _messages) {
+ let message = _messages[key];
+ if (threshold < message.totalWeight) {
+ Messaging.sendRequest({
+ type: "HomeBanner:Data",
+ id: message.id,
+ text: message.text,
+ iconURI: message.iconURI
+ });
+ return;
+ }
+ }
+ };
+
+ let _handleShown = function(id) {
+ let message = _messages[id];
+ if (message.onshown)
+ message.onshown();
+ };
+
+ let _handleClick = function(id) {
+ let message = _messages[id];
+ if (message.onclick)
+ message.onclick();
+ };
+
+ let _handleDismiss = function(id) {
+ let message = _messages[id];
+ if (message.ondismiss)
+ message.ondismiss();
+ };
+
+ return Object.freeze({
+ observe: function(subject, topic, data) {
+ switch(topic) {
+ case "HomeBanner:Shown":
+ _handleShown(data);
+ break;
+
+ case "HomeBanner:Click":
+ _handleClick(data);
+ break;
+
+ case "HomeBanner:Dismiss":
+ _handleDismiss(data);
+ break;
+ }
+ },
+
+ /**
+ * Adds a new banner message to the rotation.
+ *
+ * @return id Unique identifer for the message.
+ */
+ add: function(options) {
+ let message = new BannerMessage(options);
+ _messages[message.id] = message;
+
+ // If this is the first message we're adding, add
+ // observers to listen for requests from the Java UI.
+ if (Object.keys(_messages).length == 1) {
+ Services.obs.addObserver(this, "HomeBanner:Shown", false);
+ Services.obs.addObserver(this, "HomeBanner:Click", false);
+ Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
+
+ // Send a message to Java if there's a pending "HomeBanner:Get" request.
+ if (_pendingRequest) {
+ _pendingRequest = false;
+ _sendBannerData();
+ }
+ }
+
+ return message.id;
+ },
+
+ /**
+ * Removes a banner message from the rotation.
+ *
+ * @param id The id of the message to remove.
+ */
+ remove: function(id) {
+ if (!(id in _messages)) {
+ throw "Home.banner: Can't remove message that doesn't exist: id = " + id;
+ }
+
+ delete _messages[id];
+
+ // If there are no more messages, remove the observers.
+ if (Object.keys(_messages).length == 0) {
+ Services.obs.removeObserver(this, "HomeBanner:Shown");
+ Services.obs.removeObserver(this, "HomeBanner:Click");
+ Services.obs.removeObserver(this, "HomeBanner:Dismiss");
+ }
+ }
+ });
+})();
+
+// We need this object to have access to the HomePanels
+// private members without leaking it outside Home.jsm.
+var HomePanelsMessageHandlers;
+
+var HomePanels = (function () {
+ // Functions used to handle messages sent from Java.
+ HomePanelsMessageHandlers = {
+
+ "HomePanels:Get": function handlePanelsGet(data) {
+ data = JSON.parse(data);
+
+ let requestId = data.requestId;
+ let ids = data.ids || null;
+
+ let panels = [];
+ for (let id in _registeredPanels) {
+ // Null ids means we want to fetch all available panels
+ if (ids == null || ids.indexOf(id) >= 0) {
+ try {
+ panels.push(_generatePanel(id));
+ } catch(e) {
+ Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
+ }
+ }
+ }
+
+ Messaging.sendRequest({
+ type: "HomePanels:Data",
+ panels: panels,
+ requestId: requestId
+ });
+ },
+
+ "HomePanels:Authenticate": function handlePanelsAuthenticate(id) {
+ // Generate panel options to get auth handler.
+ let options = _registeredPanels[id]();
+ if (!options.auth) {
+ throw "Home.panels: Invalid auth for panel.id = " + id;
+ }
+ if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") {
+ throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id;
+ }
+ options.auth.authenticate();
+ },
+
+ "HomePanels:RefreshView": function handlePanelsRefreshView(data) {
+ data = JSON.parse(data);
+
+ let options = _registeredPanels[data.panelId]();
+ let view = options.views[data.viewIndex];
+
+ if (!view) {
+ throw "Home.panels: Invalid view for panel.id = " + data.panelId
+ + ", view.index = " + data.viewIndex;
+ }
+
+ if (!view.onrefresh || typeof view.onrefresh !== "function") {
+ throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
+ + ", view.index = " + data.viewIndex;
+ }
+
+ view.onrefresh();
+ },
+
+ "HomePanels:Installed": function handlePanelsInstalled(id) {
+ _assertPanelExists(id);
+
+ let options = _registeredPanels[id]();
+ if (!options.oninstall) {
+ return;
+ }
+ if (typeof options.oninstall !== "function") {
+ throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
+ }
+ options.oninstall();
+ },
+
+ "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
+ _assertPanelExists(id);
+
+ let options = _registeredPanels[id]();
+ if (!options.onuninstall) {
+ return;
+ }
+ if (typeof options.onuninstall !== "function") {
+ throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
+ }
+ options.onuninstall();
+ }
+ };
+
+ // Holds the current set of registered panels that can be
+ // installed, updated, uninstalled, or unregistered. It maps
+ // panel ids with the functions that dynamically generate
+ // their respective panel options. This is used to retrieve
+ // the current list of available panels in the system.
+ // See HomePanels:Get handler.
+ let _registeredPanels = {};
+
+ // Valid layouts for a panel.
+ let Layout = Object.freeze({
+ FRAME: "frame"
+ });
+
+ // Valid types of views for a dataset.
+ let View = Object.freeze({
+ LIST: "list",
+ GRID: "grid"
+ });
+
+ // Valid item types for a panel view.
+ let Item = Object.freeze({
+ ARTICLE: "article",
+ IMAGE: "image",
+ ICON: "icon"
+ });
+
+ // Valid item handlers for a panel view.
+ let ItemHandler = Object.freeze({
+ BROWSER: "browser",
+ INTENT: "intent"
+ });
+
+ function Panel(id, options) {
+ this.id = id;
+ this.title = options.title;
+ this.layout = options.layout;
+ this.views = options.views;
+ this.default = !!options.default;
+
+ if (!this.id || !this.title) {
+ throw "Home.panels: Can't create a home panel without an id and title!";
+ }
+
+ if (!this.layout) {
+ // Use FRAME layout by default
+ this.layout = Layout.FRAME;
+ } else if (!_valueExists(Layout, this.layout)) {
+ throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout;
+ }
+
+ for (let view of this.views) {
+ if (!_valueExists(View, view.type)) {
+ throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type;
+ }
+
+ if (!view.itemType) {
+ if (view.type == View.LIST) {
+ // Use ARTICLE item type by default in LIST views
+ view.itemType = Item.ARTICLE;
+ } else if (view.type == View.GRID) {
+ // Use IMAGE item type by default in GRID views
+ view.itemType = Item.IMAGE;
+ }
+ } else if (!_valueExists(Item, view.itemType)) {
+ throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType;
+ }
+
+ if (!view.itemHandler) {
+ // Use BROWSER item handler by default
+ view.itemHandler = ItemHandler.BROWSER;
+ } else if (!_valueExists(ItemHandler, view.itemHandler)) {
+ throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler;
+ }
+
+ if (!view.dataset) {
+ throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type;
+ }
+
+ if (view.onrefresh) {
+ view.refreshEnabled = true;
+ }
+ }
+
+ if (options.auth) {
+ if (!options.auth.messageText) {
+ throw "Home.panels: Invalid auth messageText: panel.id = " + this.id;
+ }
+ if (!options.auth.buttonText) {
+ throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id;
+ }
+
+ this.authConfig = {
+ messageText: options.auth.messageText,
+ buttonText: options.auth.buttonText
+ };
+
+ // Include optional image URL if it is specified.
+ if (options.auth.imageUrl) {
+ this.authConfig.imageUrl = options.auth.imageUrl;
+ }
+ }
+
+ if (options.position >= 0) {
+ this.position = options.position;
+ }
+ }
+
+ let _generatePanel = function(id) {
+ let options = _registeredPanels[id]();
+ return new Panel(id, options);
+ };
+
+ // Helper function used to see if a value is in an object.
+ let _valueExists = function(obj, value) {
+ for (let key in obj) {
+ if (obj[key] == value) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ let _assertPanelExists = function(id) {
+ if (!(id in _registeredPanels)) {
+ throw "Home.panels: Panel doesn't exist: id = " + id;
+ }
+ };
+
+ return Object.freeze({
+ Layout: Layout,
+ View: View,
+ Item: Item,
+ ItemHandler: ItemHandler,
+
+ register: function(id, optionsCallback) {
+ // Bail if the panel already exists
+ if (id in _registeredPanels) {
+ throw "Home.panels: Panel already exists: id = " + id;
+ }
+
+ if (!optionsCallback || typeof optionsCallback !== "function") {
+ throw "Home.panels: Panel callback must be a function: id = " + id;
+ }
+
+ _registeredPanels[id] = optionsCallback;
+ },
+
+ unregister: function(id) {
+ _assertPanelExists(id);
+
+ delete _registeredPanels[id];
+ },
+
+ install: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Install",
+ panel: _generatePanel(id)
+ });
+ },
+
+ uninstall: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Uninstall",
+ id: id
+ });
+ },
+
+ update: function(id) {
+ _assertPanelExists(id);
+
+ Messaging.sendRequest({
+ type: "HomePanels:Update",
+ panel: _generatePanel(id)
+ });
+ },
+
+ setAuthenticated: function(id, isAuthenticated) {
+ _assertPanelExists(id);
+
+ let authKey = PREFS_PANEL_AUTH_PREFIX + id;
+ let sharedPrefs = SharedPreferences.forProfile();
+ sharedPrefs.setBoolPref(authKey, isAuthenticated);
+ }
+ });
+})();
+
+// Public API
+this.Home = Object.freeze({
+ banner: HomeBanner,
+ panels: HomePanels,
+
+ // Lazy notification observer registered in browser.js
+ observe: function(subject, topic, data) {
+ if (topic in HomeBannerMessageHandlers) {
+ HomeBannerMessageHandlers[topic](data);
+ } else if (topic in HomePanelsMessageHandlers) {
+ HomePanelsMessageHandlers[topic](data);
+ } else {
+ Cu.reportError("Home.observe: message handler not found for topic: " + topic);
+ }
+ }
+});
diff --git a/mobile/android/modules/HomeProvider.jsm b/mobile/android/modules/HomeProvider.jsm
new file mode 100644
index 000000000..bca8fa526
--- /dev/null
+++ b/mobile/android/modules/HomeProvider.jsm
@@ -0,0 +1,407 @@
+// -*- 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [ "HomeProvider" ];
+
+const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
+
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+/*
+ * SCHEMA_VERSION history:
+ * 1: Create HomeProvider (bug 942288)
+ * 2: Add filter column to items table (bug 942295/975841)
+ * 3: Add background_color and background_url columns (bug 1157539)
+ */
+const SCHEMA_VERSION = 3;
+
+// The maximum number of items you can attempt to save at once.
+const MAX_SAVE_COUNT = 100;
+
+XPCOMUtils.defineLazyGetter(this, "DB_PATH", function() {
+ return OS.Path.join(OS.Constants.Path.profileDir, "home.sqlite");
+});
+
+const PREF_STORAGE_LAST_SYNC_TIME_PREFIX = "home.storage.lastSyncTime.";
+const PREF_SYNC_UPDATE_MODE = "home.sync.updateMode";
+const PREF_SYNC_CHECK_INTERVAL_SECS = "home.sync.checkIntervalSecs";
+
+XPCOMUtils.defineLazyGetter(this, "gSyncCheckIntervalSecs", function() {
+ return Services.prefs.getIntPref(PREF_SYNC_CHECK_INTERVAL_SECS);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this,
+ "gUpdateTimerManager", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
+
+/**
+ * All SQL statements should be defined here.
+ */
+const SQL = {
+ createItemsTable:
+ "CREATE TABLE items (" +
+ "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "dataset_id TEXT NOT NULL, " +
+ "url TEXT," +
+ "title TEXT," +
+ "description TEXT," +
+ "image_url TEXT," +
+ "background_color TEXT," +
+ "background_url TEXT," +
+ "filter TEXT," +
+ "created INTEGER" +
+ ")",
+
+ dropItemsTable:
+ "DROP TABLE items",
+
+ insertItem:
+ "INSERT INTO items (dataset_id, url, title, description, image_url, background_color, background_url, filter, created) " +
+ "VALUES (:dataset_id, :url, :title, :description, :image_url, :background_color, :background_url, :filter, :created)",
+
+ deleteFromDataset:
+ "DELETE FROM items WHERE dataset_id = :dataset_id",
+
+ addColumnBackgroundColor:
+ "ALTER TABLE items ADD COLUMN background_color TEXT",
+
+ addColumnBackgroundUrl:
+ "ALTER TABLE items ADD COLUMN background_url TEXT",
+}
+
+/**
+ * Technically this function checks to see if the user is on a local network,
+ * but we express this as "wifi" to the user.
+ */
+function isUsingWifi() {
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ return (network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType === Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
+}
+
+function getNowInSeconds() {
+ return Math.round(Date.now() / 1000);
+}
+
+function getLastSyncPrefName(datasetId) {
+ return PREF_STORAGE_LAST_SYNC_TIME_PREFIX + datasetId;
+}
+
+// Whether or not we've registered an update timer.
+var gTimerRegistered = false;
+
+// Map of datasetId -> { interval: <integer>, callback: <function> }
+var gSyncCallbacks = {};
+
+/**
+ * nsITimerCallback implementation. Checks to see if it's time to sync any registered datasets.
+ *
+ * @param timer The timer which has expired.
+ */
+function syncTimerCallback(timer) {
+ for (let datasetId in gSyncCallbacks) {
+ let lastSyncTime = 0;
+ try {
+ lastSyncTime = Services.prefs.getIntPref(getLastSyncPrefName(datasetId));
+ } catch(e) { }
+
+ let now = getNowInSeconds();
+ let { interval: interval, callback: callback } = gSyncCallbacks[datasetId];
+
+ if (lastSyncTime < now - interval) {
+ let success = HomeProvider.requestSync(datasetId, callback);
+ if (success) {
+ Services.prefs.setIntPref(getLastSyncPrefName(datasetId), now);
+ }
+ }
+ }
+}
+
+this.HomeStorage = function(datasetId) {
+ this.datasetId = datasetId;
+};
+
+this.ValidationError = function(message) {
+ this.name = "ValidationError";
+ this.message = message;
+};
+ValidationError.prototype = new Error();
+ValidationError.prototype.constructor = ValidationError;
+
+this.HomeProvider = Object.freeze({
+ ValidationError: ValidationError,
+
+ /**
+ * Returns a storage associated with a given dataset identifer.
+ *
+ * @param datasetId
+ * (string) Unique identifier for the dataset.
+ *
+ * @return HomeStorage
+ */
+ getStorage: function(datasetId) {
+ return new HomeStorage(datasetId);
+ },
+
+ /**
+ * Checks to see if it's an appropriate time to sync.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ *
+ * @return boolean Whether or not we were able to sync.
+ */
+ requestSync: function(datasetId, callback) {
+ // Make sure it's a good time to sync.
+ if ((Services.prefs.getIntPref(PREF_SYNC_UPDATE_MODE) === 1) && !isUsingWifi()) {
+ Cu.reportError("HomeProvider: Failed to sync because device is not on a local network");
+ return false;
+ }
+
+ callback(datasetId);
+ return true;
+ },
+
+ /**
+ * Specifies that a sync should be requested for the given dataset and update interval.
+ *
+ * @param datasetId Unique identifier for the dataset to sync.
+ * @param interval Update interval in seconds. By default, this is throttled to 3600 seconds (1 hour).
+ * @param callback Function to call when it's time to sync, called with datasetId as a parameter.
+ */
+ addPeriodicSync: function(datasetId, interval, callback) {
+ // Warn developers if they're expecting more frequent notifications that we allow.
+ if (interval < gSyncCheckIntervalSecs) {
+ Cu.reportError("HomeProvider: Warning for dataset " + datasetId +
+ " : Sync notifications are throttled to " + gSyncCheckIntervalSecs + " seconds");
+ }
+
+ gSyncCallbacks[datasetId] = {
+ interval: interval,
+ callback: callback
+ };
+
+ if (!gTimerRegistered) {
+ gUpdateTimerManager.registerTimer("home-provider-sync-timer", syncTimerCallback, gSyncCheckIntervalSecs);
+ gTimerRegistered = true;
+ }
+ },
+
+ /**
+ * Removes a periodic sync timer.
+ *
+ * @param datasetId Dataset to sync.
+ */
+ removePeriodicSync: function(datasetId) {
+ delete gSyncCallbacks[datasetId];
+ Services.prefs.clearUserPref(getLastSyncPrefName(datasetId));
+ // You can't unregister a update timer, so we don't try to do that.
+ }
+});
+
+var gDatabaseEnsured = false;
+
+/**
+ * Creates the database schema.
+ */
+function createDatabase(db) {
+ return Task.spawn(function create_database_task() {
+ yield db.execute(SQL.createItemsTable);
+ });
+}
+
+/**
+ * Migrates the database schema to a new version.
+ */
+function upgradeDatabase(db, oldVersion, newVersion) {
+ return Task.spawn(function upgrade_database_task() {
+ switch (oldVersion) {
+ case 1:
+ // Migration from v1 to latest:
+ // Recreate the items table discarding any
+ // existing data.
+ yield db.execute(SQL.dropItemsTable);
+ yield db.execute(SQL.createItemsTable);
+ break;
+
+ case 2:
+ // Migration from v2 to latest:
+ // Add new columns: background_color, background_url
+ yield db.execute(SQL.addColumnBackgroundColor);
+ yield db.execute(SQL.addColumnBackgroundUrl);
+ break;
+ }
+ });
+}
+
+/**
+ * Opens a database connection and makes sure that the database schema version
+ * is correct, performing migrations if necessary. Consumers should be sure
+ * to close any database connections they open.
+ *
+ * @return Promise
+ * @resolves Handle on an opened SQLite database.
+ */
+function getDatabaseConnection() {
+ return Task.spawn(function get_database_connection_task() {
+ let db = yield Sqlite.openConnection({ path: DB_PATH });
+ if (gDatabaseEnsured) {
+ throw new Task.Result(db);
+ }
+
+ try {
+ // Check to see if we need to perform any migrations.
+ let dbVersion = parseInt(yield db.getSchemaVersion());
+
+ // getSchemaVersion() returns a 0 int if the schema
+ // version is undefined.
+ if (dbVersion === 0) {
+ yield createDatabase(db);
+ } else if (dbVersion < SCHEMA_VERSION) {
+ yield upgradeDatabase(db, dbVersion, SCHEMA_VERSION);
+ }
+
+ yield db.setSchemaVersion(SCHEMA_VERSION);
+ } catch(e) {
+ // Close the DB connection before passing the exception to the consumer.
+ yield db.close();
+ throw e;
+ }
+
+ gDatabaseEnsured = true;
+ throw new Task.Result(db);
+ });
+}
+
+/**
+ * Validates an item to be saved to the DB.
+ *
+ * @param item
+ * (object) item object to be validated.
+ */
+function validateItem(datasetId, item) {
+ if (!item.url) {
+ throw new ValidationError('HomeStorage: All rows must have an URL: datasetId = ' +
+ datasetId);
+ }
+
+ if (!item.image_url && !item.title && !item.description) {
+ throw new ValidationError('HomeStorage: All rows must have at least an image URL, ' +
+ 'or a title or a description: datasetId = ' + datasetId);
+ }
+}
+
+var gRefreshTimers = {};
+
+/**
+ * Sends a message to Java to refresh the given dataset. Delays sending
+ * messages to avoid successive refreshes, which can result in flashing views.
+ */
+function refreshDataset(datasetId) {
+ // Bail if there's already a refresh timer waiting to fire
+ if (gRefreshTimers[datasetId]) {
+ return;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(function(timer) {
+ delete gRefreshTimers[datasetId];
+
+ Messaging.sendRequest({
+ type: "HomePanels:RefreshDataset",
+ datasetId: datasetId
+ });
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ gRefreshTimers[datasetId] = timer;
+}
+
+HomeStorage.prototype = {
+ /**
+ * Saves data rows to the DB.
+ *
+ * @param data
+ * An array of JS objects represnting row items to save.
+ * Each object may have the following properties:
+ * - url (string)
+ * - title (string)
+ * - description (string)
+ * - image_url (string)
+ * - filter (string)
+ * @param options
+ * A JS object holding additional cofiguration properties.
+ * The following properties are currently supported:
+ * - replace (boolean): Whether or not to replace existing items.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ save: function(data, options) {
+ if (data && data.length > MAX_SAVE_COUNT) {
+ throw "save failed for dataset = " + this.datasetId +
+ ": you cannot save more than " + MAX_SAVE_COUNT + " items at once";
+ }
+
+ return Task.spawn(function save_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ yield db.executeTransaction(function save_transaction() {
+ if (options && options.replace) {
+ yield db.executeCached(SQL.deleteFromDataset, { dataset_id: this.datasetId });
+ }
+
+ // Insert data into DB.
+ for (let item of data) {
+ validateItem(this.datasetId, item);
+
+ // XXX: Directly pass item as params? More validation for item?
+ let params = {
+ dataset_id: this.datasetId,
+ url: item.url,
+ title: item.title,
+ description: item.description,
+ image_url: item.image_url,
+ background_color: item.background_color,
+ background_url: item.background_url,
+ filter: item.filter,
+ created: Date.now()
+ };
+ yield db.executeCached(SQL.insertItem, params);
+ }
+ }.bind(this));
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ },
+
+ /**
+ * Deletes all rows associated with this storage.
+ *
+ * @return Promise
+ * @resolves When the operation has completed.
+ */
+ deleteAll: function() {
+ return Task.spawn(function delete_all_task() {
+ let db = yield getDatabaseConnection();
+ try {
+ let params = { dataset_id: this.datasetId };
+ yield db.executeCached(SQL.deleteFromDataset, params);
+ } finally {
+ yield db.close();
+ }
+
+ refreshDataset(this.datasetId);
+ }.bind(this));
+ }
+};
diff --git a/mobile/android/modules/JNI.jsm b/mobile/android/modules/JNI.jsm
new file mode 100644
index 000000000..1e10b9cfb
--- /dev/null
+++ b/mobile/android/modules/JNI.jsm
@@ -0,0 +1,1167 @@
+// JavaScript to Java bridge via the Java Native Interface
+// Allows calling into Android SDK from JavaScript in Firefox Add-On.
+// Released into the public domain.
+// C. Scott Ananian <cscott@laptop.org> (http://cscott.net)
+
+// NOTE: All changes to this file should first be pushed to the repo at:
+// https://github.com/cscott/skeleton-addon-fxandroid/tree/jni
+
+var EXPORTED_SYMBOLS = ["JNI","android_log"];
+
+Components.utils.import("resource://gre/modules/ctypes.jsm")
+
+var liblog = ctypes.open('liblog.so');
+var android_log = liblog.declare("__android_log_write",
+ ctypes.default_abi,
+ ctypes.int32_t,
+ ctypes.int32_t,
+ ctypes.char.ptr,
+ ctypes.char.ptr);
+
+var libxul = ctypes.open('libxul.so');
+
+var jenvptr = ctypes.voidptr_t;
+var jclass = ctypes.voidptr_t;
+var jobject = ctypes.voidptr_t;
+var jvalue = ctypes.voidptr_t;
+var jmethodid = ctypes.voidptr_t;
+var jfieldid = ctypes.voidptr_t;
+
+var jboolean = ctypes.uint8_t;
+var jbyte = ctypes.int8_t;
+var jchar = ctypes.uint16_t;
+var jshort = ctypes.int16_t;
+var jint = ctypes.int32_t;
+var jlong = ctypes.int64_t;
+var jfloat = ctypes.float32_t;
+var jdouble = ctypes.float64_t;
+
+var jsize = jint;
+var jstring = jobject;
+var jarray = jobject;
+var jthrowable = jobject;
+
+var JNINativeInterface = new ctypes.StructType(
+ "JNINativeInterface",
+ [{reserved0: ctypes.voidptr_t},
+ {reserved1: ctypes.voidptr_t},
+ {reserved2: ctypes.voidptr_t},
+ {reserved3: ctypes.voidptr_t},
+ {GetVersion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.int32_t,
+ [ctypes.voidptr_t]).ptr},
+ {DefineClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr, ctypes.char.ptr, jobject,
+ jbyte.array(), jsize]).ptr},
+ {FindClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {FromReflectedMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr, jobject]).ptr},
+ {FromReflectedField: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jobject]).ptr},
+ {ToReflectedMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jmethodid]).ptr},
+ {GetSuperclass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass, [jenvptr, jclass]).ptr},
+ {IsAssignableFrom: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass, jclass]).ptr},
+ {ToReflectedField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {Throw: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jthrowable]).ptr},
+ {ThrowNew: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jclass,
+ ctypes.char.ptr]).ptr},
+ {ExceptionOccurred: new ctypes.FunctionType(ctypes.default_abi,
+ jthrowable, [jenvptr]).ptr},
+ {ExceptionDescribe: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t, [jenvptr]).ptr},
+ {ExceptionClear: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t, [jenvptr]).ptr},
+ {FatalError: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {PushLocalFrame: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jint]).ptr},
+ {PopLocalFrame: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject]).ptr},
+ {NewGlobalRef: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jobject]).ptr},
+ {DeleteGlobalRef: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject]).ptr},
+ {DeleteLocalRef: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject]).ptr},
+ {IsSameObject: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject, jobject]).ptr},
+ {NewLocalRef: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jobject]).ptr},
+ {EnsureLocalCapacity: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jint]).ptr},
+ {AllocObject: new ctypes.FunctionType(ctypes.default_abi,
+ jobject, [jenvptr, jclass]).ptr},
+ {NewObject: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr,
+ jclass,
+ jmethodid,
+ "..."]).ptr},
+ {NewObjectV: ctypes.voidptr_t},
+ {NewObjectA: ctypes.voidptr_t},
+ {GetObjectClass: new ctypes.FunctionType(ctypes.default_abi,
+ jclass,
+ [jenvptr, jobject]).ptr},
+ {IsInstanceOf: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject, jclass]).ptr},
+ {GetMethodID: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr,
+ jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {CallObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject, jmethodid,
+ "..."]).ptr},
+ {CallObjectMethodV: ctypes.voidptr_t},
+ {CallObjectMethodA: ctypes.voidptr_t},
+ {CallBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallBooleanMethodV: ctypes.voidptr_t},
+ {CallBooleanMethodA: ctypes.voidptr_t},
+ {CallByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallByteMethodV: ctypes.voidptr_t},
+ {CallByteMethodA: ctypes.voidptr_t},
+ {CallCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallCharMethodV: ctypes.voidptr_t},
+ {CallCharMethodA: ctypes.voidptr_t},
+ {CallShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallShortMethodV: ctypes.voidptr_t},
+ {CallShortMethodA: ctypes.voidptr_t},
+ {CallIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallIntMethodV: ctypes.voidptr_t},
+ {CallIntMethodA: ctypes.voidptr_t},
+ {CallLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallLongMethodV: ctypes.voidptr_t},
+ {CallLongMethodA: ctypes.voidptr_t},
+ {CallFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallFloatMethodV: ctypes.voidptr_t},
+ {CallFloatMethodA: ctypes.voidptr_t},
+ {CallDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallDoubleMethodV: ctypes.voidptr_t},
+ {CallDoubleMethodA: ctypes.voidptr_t},
+ {CallVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr,
+ jobject,
+ jmethodid,
+ "..."]).ptr},
+ {CallVoidMethodV: ctypes.voidptr_t},
+ {CallVoidMethodA: ctypes.voidptr_t},
+ {CallNonvirtualObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualObjectMethodV: ctypes.voidptr_t},
+ {CallNonvirtualObjectMethodA: ctypes.voidptr_t},
+ {CallNonvirtualBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualBooleanMethodV: ctypes.voidptr_t},
+ {CallNonvirtualBooleanMethodA: ctypes.voidptr_t},
+ {CallNonvirtualByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualByteMethodV: ctypes.voidptr_t},
+ {CallNonvirtualByteMethodA: ctypes.voidptr_t},
+ {CallNonvirtualCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualCharMethodV: ctypes.voidptr_t},
+ {CallNonvirtualCharMethodA: ctypes.voidptr_t},
+ {CallNonvirtualShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualShortMethodV: ctypes.voidptr_t},
+ {CallNonvirtualShortMethodA: ctypes.voidptr_t},
+ {CallNonvirtualIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualIntMethodV: ctypes.voidptr_t},
+ {CallNonvirtualIntMethodA: ctypes.voidptr_t},
+ {CallNonvirtualLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualLongMethodV: ctypes.voidptr_t},
+ {CallNonvirtualLongMethodA: ctypes.voidptr_t},
+ {CallNonvirtualFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualFloatMethodV: ctypes.voidptr_t},
+ {CallNonvirtualFloatMethodA: ctypes.voidptr_t},
+ {CallNonvirtualDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualDoubleMethodV: ctypes.voidptr_t},
+ {CallNonvirtualDoubleMethodA: ctypes.voidptr_t},
+ {CallNonvirtualVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jclass, jmethodid,
+ "..."]).ptr},
+ {CallNonvirtualVoidMethodV: ctypes.voidptr_t},
+ {CallNonvirtualVoidMethodA: ctypes.voidptr_t},
+ {GetFieldID: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {GetObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetByteField: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetCharField: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetShortField: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetIntField: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetLongField: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {GetDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jobject,
+ jfieldid]).ptr},
+ {SetObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jobject]).ptr},
+ {SetBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jboolean]).ptr},
+ {SetByteField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jbyte]).ptr},
+ {SetCharField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jchar]).ptr},
+ {SetShortField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jshort]).ptr},
+ {SetIntField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jint]).ptr},
+ {SetLongField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jlong]).ptr},
+ {SetFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jfloat]).ptr},
+ {SetDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jobject,
+ jfieldid, jdouble]).ptr},
+ {GetStaticMethodID: new ctypes.FunctionType(ctypes.default_abi,
+ jmethodid,
+ [jenvptr,
+ jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {CallStaticObjectMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticObjectMethodV: ctypes.voidptr_t},
+ {CallStaticObjectMethodA: ctypes.voidptr_t},
+ {CallStaticBooleanMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticBooleanMethodV: ctypes.voidptr_t},
+ {CallStaticBooleanMethodA: ctypes.voidptr_t},
+ {CallStaticByteMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticByteMethodV: ctypes.voidptr_t},
+ {CallStaticByteMethodA: ctypes.voidptr_t},
+ {CallStaticCharMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticCharMethodV: ctypes.voidptr_t},
+ {CallStaticCharMethodA: ctypes.voidptr_t},
+ {CallStaticShortMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticShortMethodV: ctypes.voidptr_t},
+ {CallStaticShortMethodA: ctypes.voidptr_t},
+ {CallStaticIntMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticIntMethodV: ctypes.voidptr_t},
+ {CallStaticIntMethodA: ctypes.voidptr_t},
+ {CallStaticLongMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticLongMethodV: ctypes.voidptr_t},
+ {CallStaticLongMethodA: ctypes.voidptr_t},
+ {CallStaticFloatMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticFloatMethodV: ctypes.voidptr_t},
+ {CallStaticFloatMethodA: ctypes.voidptr_t},
+ {CallStaticDoubleMethod: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticDoubleMethodV: ctypes.voidptr_t},
+ {CallStaticDoubleMethodA: ctypes.voidptr_t},
+ {CallStaticVoidMethod: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jmethodid,
+ "..."]).ptr},
+ {CallStaticVoidMethodV: ctypes.voidptr_t},
+ {CallStaticVoidMethodA: ctypes.voidptr_t},
+ {GetStaticFieldID: new ctypes.FunctionType(ctypes.default_abi,
+ jfieldid,
+ [jenvptr, jclass,
+ ctypes.char.ptr,
+ ctypes.char.ptr]).ptr},
+ {GetStaticObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticByteField: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticCharField: new ctypes.FunctionType(ctypes.default_abi,
+ jchar,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticShortField: new ctypes.FunctionType(ctypes.default_abi,
+ jshort,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticIntField: new ctypes.FunctionType(ctypes.default_abi,
+ jint,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticLongField: new ctypes.FunctionType(ctypes.default_abi,
+ jlong,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {GetStaticDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble,
+ [jenvptr, jclass,
+ jfieldid]).ptr},
+ {SetStaticObjectField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jobject]).ptr},
+ {SetStaticBooleanField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jboolean]).ptr},
+ {SetStaticByteField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jbyte]).ptr},
+ {SetStaticCharField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jchar]).ptr},
+ {SetStaticShortField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jshort]).ptr},
+ {SetStaticIntField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jint]).ptr},
+ {SetStaticLongField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jlong]).ptr},
+ {SetStaticFloatField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jfloat]).ptr},
+ {SetStaticDoubleField: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jclass,
+ jfieldid, jdouble]).ptr},
+
+ {NewString: new ctypes.FunctionType(ctypes.default_abi,
+ jstring,
+ [jenvptr, jchar.ptr, jsize]).ptr},
+ {GetStringLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jstring]).ptr},
+ {GetStringChars: new ctypes.FunctionType(ctypes.default_abi,
+ jchar.ptr,
+ [jenvptr, jstring,
+ jboolean.ptr]).ptr},
+ {ReleaseStringChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jchar.ptr]).ptr},
+
+ {NewStringUTF: new ctypes.FunctionType(ctypes.default_abi,
+ jstring,
+ [jenvptr,
+ ctypes.char.ptr]).ptr},
+ {GetStringUTFLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jstring]).ptr},
+ {GetStringUTFChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.char.ptr,
+ [jenvptr, jstring,
+ jboolean.ptr]).ptr},
+ {ReleaseStringUTFChars: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ ctypes.char.ptr]).ptr},
+ {GetArrayLength: new ctypes.FunctionType(ctypes.default_abi,
+ jsize,
+ [jenvptr, jarray]).ptr},
+ {NewObjectArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize,
+ jclass, jobject]).ptr},
+ {GetObjectArrayElement: new ctypes.FunctionType(ctypes.default_abi,
+ jobject,
+ [jenvptr, jarray,
+ jsize]).ptr},
+ {SetObjectArrayElement: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jobject]).ptr},
+ {NewBooleanArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewByteArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewCharArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewShortArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewIntArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewLongArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewFloatArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {NewDoubleArray: new ctypes.FunctionType(ctypes.default_abi,
+ jarray,
+ [jenvptr, jsize]).ptr},
+ {GetBooleanArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetByteArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jbyte.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetCharArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jchar.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetShortArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jshort.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetIntArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jint.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetLongArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jlong.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetFloatArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jfloat.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {GetDoubleArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ jdouble.ptr,
+ [jenvptr, jarray,
+ jboolean.ptr]).ptr},
+ {ReleaseBooleanArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jboolean.ptr,
+ jint]).ptr},
+ {ReleaseByteArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jbyte.ptr,
+ jint]).ptr},
+ {ReleaseCharArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jchar.ptr,
+ jint]).ptr},
+ {ReleaseShortArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jshort.ptr,
+ jint]).ptr},
+ {ReleaseIntArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jint.ptr,
+ jint]).ptr},
+ {ReleaseLongArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jlong.ptr,
+ jint]).ptr},
+ {ReleaseFloatArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jfloat.ptr,
+ jint]).ptr},
+ {ReleaseDoubleArrayElements: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jdouble.ptr,
+ jint]).ptr},
+ {GetBooleanArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jboolean.array()]).ptr},
+ {GetByteArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jbyte.array()]).ptr},
+ {GetCharArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {GetShortArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jshort.array()]).ptr},
+ {GetIntArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jint.array()]).ptr},
+ {GetLongArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jlong.array()]).ptr},
+ {GetFloatArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jfloat.array()]).ptr},
+ {GetDoubleArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jdouble.array()]).ptr},
+ {SetBooleanArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jboolean.array()]).ptr},
+ {SetByteArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jbyte.array()]).ptr},
+ {SetCharArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {SetShortArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jshort.array()]).ptr},
+ {SetIntArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jint.array()]).ptr},
+ {SetLongArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jlong.array()]).ptr},
+ {SetFloatArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jfloat.array()]).ptr},
+ {SetDoubleArrayRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jarray,
+ jsize, jsize,
+ jdouble.array()]).ptr},
+ {RegisterNatives: ctypes.voidptr_t},
+ {UnregisterNatives: ctypes.voidptr_t},
+ {MonitorEnter: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jobject]).ptr},
+ {MonitorExit: new ctypes.FunctionType(ctypes.default_abi,
+ jint, [jenvptr, jobject]).ptr},
+ {GetJavaVM: ctypes.voidptr_t},
+ {GetStringRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jsize, jsize,
+ jchar.array()]).ptr},
+ {GetStringUTFRegion: new ctypes.FunctionType(ctypes.default_abi,
+ ctypes.void_t,
+ [jenvptr, jstring,
+ jsize, jsize,
+ ctypes.char.array()]).ptr},
+ {GetPrimitiveArrayCritical: ctypes.voidptr_t},
+ {ReleasePrimitiveArrayCritical: ctypes.voidptr_t},
+ {GetStringCritical: ctypes.voidptr_t},
+ {ReleaseStringCritical: ctypes.voidptr_t},
+ {NewWeakGlobalRef: ctypes.voidptr_t},
+ {DeleteWeakGlobalRef: ctypes.voidptr_t},
+ {ExceptionCheck: new ctypes.FunctionType(ctypes.default_abi,
+ jboolean, [jenvptr]).ptr},
+ {NewDirectByteBuffer: ctypes.voidptr_t},
+ {GetDirectBufferAddress: ctypes.voidptr_t},
+ {GetDirectBufferCapacity: ctypes.voidptr_t},
+ {GetObjectRefType: ctypes.voidptr_t}]
+);
+
+var GetJNIForThread = libxul.declare("GetJNIForThread",
+ ctypes.default_abi,
+ JNINativeInterface.ptr.ptr);
+
+var registry = Object.create(null);
+var classes = Object.create(null);
+
+function JNIUnloadClasses(jenv) {
+ Object.getOwnPropertyNames(registry).forEach(function(classname) {
+ var jcls = unwrap(registry[classname]);
+ jenv.contents.contents.DeleteGlobalRef(jenv, jcls);
+
+ // Purge the registry, so we don't try to reuse stale global references
+ // in JNI calls and we garbage-collect the JS global reference objects.
+ delete registry[classname];
+ });
+
+ // The refs also get added to the 'classes' object, so we should purge it too.
+ // That object is a hierarchical data structure organized by class path parts,
+ // but deleting its own properties should be sufficient to break its refs.
+ Object.getOwnPropertyNames(classes).forEach(function(topLevelPart) {
+ delete classes[topLevelPart];
+ });
+}
+
+var PREFIX = 'js#';
+// this regex matches one component of a type signature:
+// any number of array modifiers, followed by either a
+// primitive type character or L<classname>;
+var sigRegex = () => /\[*([VZBCSIJFD]|L([^.\/;]+(\/[^.\/;]+)*);)/g;
+var ensureSig = function(classname_or_signature) {
+ // convert a classname into a signature,
+ // leaving unchanged signatures. We assume that
+ // anything not a valid signature is a classname.
+ var m = sigRegex().exec(classname_or_signature);
+ return (m && m[0] === classname_or_signature) ? classname_or_signature :
+ 'L' + classname_or_signature.replace(/\./g, '/') + ';';
+};
+var wrap = function(obj, classSig) {
+ if (!classSig) { return obj; }
+ // don't wrap primitive types.
+ if (classSig.charAt(0)!=='L' &&
+ classSig.charAt(0)!=='[') { return obj; }
+ var proto = registry[classSig][PREFIX+'proto'];
+ return new proto(obj);
+};
+var unwrap = function(obj, opt_jenv, opt_ctype) {
+ if (obj && typeof(obj)==='object' && (PREFIX+'obj') in obj) {
+ return obj[PREFIX+'obj'];
+ } else if (opt_jenv && opt_ctype) {
+ if (opt_ctype !== jobject)
+ return opt_ctype(obj); // cast to given primitive ctype
+ if (typeof(obj)==='string')
+ return unwrap(JNINewString(opt_jenv, obj)); // create Java String
+ }
+ return obj;
+};
+var ensureLoaded = function(jenv, classSig) {
+ if (!Object.hasOwnProperty.call(registry, classSig)) {
+ JNILoadClass(jenv, classSig);
+ }
+ return registry[classSig];
+};
+
+function JNINewString(jenv, value) {
+ var s = jenv.contents.contents.NewStringUTF(jenv, ctypes.char.array()(value));
+ ensureLoaded(jenv, "Ljava/lang/String;");
+ return wrap(s, "Ljava/lang/String;");
+}
+
+function JNIReadString(jenv, jstring_value) {
+ var val = unwrap(jstring_value);
+ if ((!val) || val.isNull()) { return null; }
+ var chars = jenv.contents.contents.GetStringUTFChars(jenv, val, null);
+ var result = chars.readString();
+ jenv.contents.contents.ReleaseStringUTFChars(jenv, val, chars);
+ return result;
+}
+
+var sigInfo = {
+ 'V': { name: 'Void', longName: 'Void', ctype: ctypes.void_t },
+ 'Z': { name: 'Boolean', longName: 'Boolean', ctype: jboolean },
+ 'B': { name: 'Byte', longName: 'Byte', ctype: jbyte },
+ 'C': { name: 'Char', longName: 'Char', ctype: jchar },
+ 'S': { name: 'Short', longName: 'Short', ctype: jshort },
+ 'I': { name: 'Int', longName: 'Integer', ctype: jint },
+ 'J': { name: 'Long', longName: 'Long', ctype: jlong },
+ 'F': { name: 'Float', longName: 'Float', ctype: jfloat },
+ 'D': { name: 'Double', longName: 'Double', ctype: jdouble },
+ 'L': { name: 'Object', longName: 'Object', ctype: jobject },
+ '[': { name: 'Object', longName: 'Object', ctype: jarray }
+};
+
+var sig2type = function(sig) { return sigInfo[sig.charAt(0)].name; };
+var sig2ctype = function(sig) { return sigInfo[sig.charAt(0)].ctype; };
+var sig2prim = function(sig) { return sigInfo[sig.charAt(0)].longName; };
+
+// return the class object for a signature string.
+// allocates 1 or 2 local refs
+function JNIClassObj(jenv, classSig) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ // Deal with funny calling convention of JNI FindClass method.
+ // Classes get the leading & trailing chars stripped; primitives
+ // have to be looked up via their wrapper type.
+ var prim = function(ty) {
+ var jcls = jenvpp().FindClass(jenv, "java/lang/"+ty);
+ var jfld = jenvpp().GetStaticFieldID(jenv, jcls, "TYPE",
+ "Ljava/lang/Class;");
+ return jenvpp().GetStaticObjectField(jenv, jcls, jfld);
+ };
+ switch (classSig.charAt(0)) {
+ case '[':
+ return jenvpp().FindClass(jenv, classSig);
+ case 'L':
+ classSig = classSig.substring(1, classSig.indexOf(';'));
+ return jenvpp().FindClass(jenv, classSig);
+ default:
+ return prim(sig2prim(classSig));
+ }
+}
+
+// return the signature string for a Class object.
+// allocates 2 local refs
+function JNIClassSig(jenv, jcls) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ var jclscls = jenvpp().FindClass(jenv, "java/lang/Class");
+ var jmtd = jenvpp().GetMethodID(jenv, jclscls,
+ "getName", "()Ljava/lang/String;");
+ var name = jenvpp().CallObjectMethod(jenv, jcls, jmtd);
+ name = JNIReadString(jenv, name);
+ // API is weird. Make sure we're using slashes not dots
+ name = name.replace(/\./g, '/');
+ // special case primitives, arrays
+ if (name.charAt(0)==='[') return name;
+ switch(name) {
+ case 'void': return 'V';
+ case 'boolean': return 'Z';
+ case 'byte': return 'B';
+ case 'char': return 'C';
+ case 'short': return 'S';
+ case 'int': return 'I';
+ case 'long': return 'J';
+ case 'float': return 'F';
+ case 'double': return 'D';
+ default:
+ return 'L' + name + ';';
+ }
+}
+
+// create dispatch method
+// we resolve overloaded methods only by # of arguments. If you need
+// further resolution, use the 'long form' of the method name, ie:
+// obj['toString()Ljava/lang/String'].call(obj);
+var overloadFunc = function(basename) {
+ return function() {
+ return this[basename+'('+arguments.length+')'].apply(this, arguments);
+ };
+};
+
+// Create appropriate wrapper fields/methods for a Java class.
+function JNILoadClass(jenv, classSig, opt_props) {
+ var jenvpp = function() { return jenv.contents.contents; };
+ var props = opt_props || {};
+
+ // allocate a local reference frame with enough space
+ // this class (1 or 2 local refs) plus superclass (3 refs)
+ // plus array element class (1 or 2 local refs)
+ var numLocals = 7;
+ jenvpp().PushLocalFrame(jenv, numLocals);
+
+ var jcls;
+ if (Object.hasOwnProperty.call(registry, classSig)) {
+ jcls = unwrap(registry[classSig]);
+ } else {
+ jcls = jenvpp().NewGlobalRef(jenv, JNIClassObj(jenv, classSig));
+
+ // get name of superclass
+ var jsuper = jenvpp().GetSuperclass(jenv, jcls);
+ if (jsuper.isNull()) {
+ jsuper = null;
+ } else {
+ jsuper = JNIClassSig(jenv, jsuper);
+ }
+
+ registry[classSig] = Object.create(jsuper?ensureLoaded(jenv, jsuper):null);
+ registry[classSig][PREFIX+'obj'] = jcls; // global ref, persistent.
+ registry[classSig][PREFIX+'proto'] =
+ function(o) { this[PREFIX+'obj'] = o; };
+ registry[classSig][PREFIX+'proto'].prototype =
+ Object.create(jsuper ?
+ ensureLoaded(jenv, jsuper)[PREFIX+'proto'].prototype :
+ null);
+ // Add a __cast__ method to the wrapper corresponding to the class
+ registry[classSig].__cast__ = function(obj) {
+ return wrap(unwrap(obj), classSig);
+ };
+
+ // make wrapper accessible via the classes object.
+ var path = sig2type(classSig).toLowerCase();
+ if (classSig.charAt(0)==='L') {
+ path = classSig.substring(1, classSig.length-1);
+ }
+ if (classSig.charAt(0)!=='[') {
+ var root = classes, i;
+ var parts = path.split('/');
+ for (i = 0; i < parts.length-1; i++) {
+ if (!Object.hasOwnProperty.call(root, parts[i])) {
+ root[parts[i]] = Object.create(null);
+ }
+ root = root[parts[i]];
+ }
+ root[parts[parts.length-1]] = registry[classSig];
+ }
+ }
+
+ var r = registry[classSig];
+ var rpp = r[PREFIX+'proto'].prototype;
+
+ if (classSig.charAt(0)==='[') {
+ // add 'length' field for arrays
+ Object.defineProperty(rpp, 'length', {
+ get: function() {
+ return jenvpp().GetArrayLength(jenv, unwrap(this));
+ }
+ });
+ // add 'get' and 'set' methods, 'new' constructor
+ var elemSig = classSig.substring(1);
+ ensureLoaded(jenv, elemSig);
+
+ registry[elemSig].__array__ = r;
+ if (!Object.hasOwnProperty.call(registry[elemSig], 'array'))
+ registry[elemSig].array = r;
+
+ if (elemSig.charAt(0)==='L' || elemSig.charAt(0)==='[') {
+ var elemClass = unwrap(registry[elemSig]);
+
+ rpp.get = function(idx) {
+ return wrap(jenvpp().GetObjectArrayElement(jenv, unwrap(this), idx),
+ elemSig);
+ };
+ rpp.set = function(idx, value) {
+ jenvpp().SetObjectArrayElement(jenv, unwrap(this), idx,
+ unwrap(value, jenv, jobject));
+ };
+ rpp.getElements = function(start, len) {
+ var i, r=[];
+ for (i=0; i<len; i++) { r.push(this.get(start+i)); }
+ return r;
+ };
+ rpp.setElements = function(start, vals) {
+ vals.forEach(function(v, i) { this.set(start+i, v); }.bind(this));
+ };
+ r['new'] = function(length) {
+ return wrap(jenvpp().NewObjectArray(jenv, length, elemClass, null),
+ classSig);
+ };
+ } else {
+ var ty = sig2type(elemSig), ctype = sig2ctype(elemSig);
+ var constructor = "New"+ty+"Array";
+ var getter = "Get"+ty+"ArrayRegion";
+ var setter = "Set"+ty+"ArrayRegion";
+ rpp.get = function(idx) { return this.getElements(idx, 1)[0]; };
+ rpp.set = function(idx, val) { this.setElements(idx, [val]); };
+ rpp.getElements = function(start, len) {
+ var j = jenvpp();
+ var buf = new (ctype.array())(len);
+ j[getter].call(j, jenv, unwrap(this), start, len, buf);
+ return buf;
+ };
+ rpp.setElements = function(start, vals) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, unwrap(this), start, vals.length,
+ ctype.array()(vals));
+ };
+ r['new'] = function(length) {
+ var j = jenvpp();
+ return wrap(j[constructor].call(j, jenv, length), classSig);
+ };
+ }
+ }
+
+ (props.static_fields || []).forEach(function(fld) {
+ var jfld = jenvpp().GetStaticFieldID(jenv, jcls, fld.name, fld.sig);
+ var ty = sig2type(fld.sig), nm = fld.sig;
+ var getter = "GetStatic"+ty+"Field", setter = "SetStatic"+ty+"Field";
+ ensureLoaded(jenv, nm);
+ var props = {
+ get: function() {
+ var j = jenvpp();
+ return wrap(j[getter].call(j, jenv, jcls, jfld), nm);
+ },
+ set: function(newValue) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, jcls, jfld, unwrap(newValue));
+ }
+ };
+ Object.defineProperty(r, fld.name, props);
+ // add static fields to object instances, too.
+ Object.defineProperty(rpp, fld.name, props);
+ });
+ (props.static_methods || []).forEach(function(mtd) {
+ var jmtd = jenvpp().GetStaticMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+ var ty = sig2type(returnSig), nm = returnSig;
+ var call = "CallStatic"+ty+"Method";
+ ensureLoaded(jenv, nm);
+ r[mtd.name] = rpp[mtd.name] = overloadFunc(mtd.name);
+ r[mtd.name + mtd.sig] = r[mtd.name+'('+(argctypes.length-1)+')'] =
+ // add static methods to object instances, too.
+ rpp[mtd.name + mtd.sig] = rpp[mtd.name+'('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, jcls, jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j[call].apply(j, args), nm);
+ };
+ });
+ (props.constructors || []).forEach(function(mtd) {
+ mtd.name = "<init>";
+ var jmtd = jenvpp().GetMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+
+ r['new'] = overloadFunc('new');
+ r['new'+mtd.sig] = r['new('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, jcls, jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j.NewObject.apply(j, args), classSig);
+ };
+ });
+ (props.fields || []).forEach(function(fld) {
+ var jfld = jenvpp().GetFieldID(jenv, jcls, fld.name, fld.sig);
+ var ty = sig2type(fld.sig), nm = fld.sig;
+ var getter = "Get"+ty+"Field", setter = "Set"+ty+"Field";
+ ensureLoaded(jenv, nm);
+ Object.defineProperty(rpp, fld.name, {
+ get: function() {
+ var j = jenvpp();
+ return wrap(j[getter].call(j, jenv, unwrap(this), jfld), nm);
+ },
+ set: function(newValue) {
+ var j = jenvpp();
+ j[setter].call(j, jenv, unwrap(this), jfld, unwrap(newValue));
+ }
+ });
+ });
+ (props.methods || []).forEach(function(mtd) {
+ var jmtd = jenvpp().GetMethodID(jenv, jcls, mtd.name, mtd.sig);
+ var argctypes = mtd.sig.match(sigRegex()).map(s => sig2ctype(s));
+ var returnSig = mtd.sig.substring(mtd.sig.indexOf(')')+1);
+ var ty = sig2type(returnSig), nm = returnSig;
+ var call = "Call"+ty+"Method";
+ ensureLoaded(jenv, nm);
+ rpp[mtd.name] = overloadFunc(mtd.name);
+ rpp[mtd.name + mtd.sig] = rpp[mtd.name+'('+(argctypes.length-1)+')'] = function() {
+ var i, j = jenvpp();
+ var args = [jenv, unwrap(this), jmtd];
+ for (i=0; i<arguments.length; i++) {
+ args.push(unwrap(arguments[i], jenv, argctypes[i]));
+ }
+ return wrap(j[call].apply(j, args), nm);
+ };
+ });
+ jenvpp().PopLocalFrame(jenv, null);
+ return r;
+}
+
+// exported object
+var JNI = {
+ // primitive types
+ jboolean: jboolean,
+ jbyte: jbyte,
+ jchar: jchar,
+ jshort: jshort,
+ jint: jint,
+ jlong: jlong,
+ jfloat: jfloat,
+ jdouble: jdouble,
+ jsize: jsize,
+
+ // class registry
+ classes: classes,
+
+ // methods
+ GetForThread: GetJNIForThread,
+ NewString: JNINewString,
+ ReadString: JNIReadString,
+ LoadClass: function(jenv, classname_or_signature, props) {
+ return JNILoadClass(jenv, ensureSig(classname_or_signature), props);
+ },
+ UnloadClasses: JNIUnloadClasses
+};
diff --git a/mobile/android/modules/JavaAddonManager.jsm b/mobile/android/modules/JavaAddonManager.jsm
new file mode 100644
index 000000000..a24535ede
--- /dev/null
+++ b/mobile/android/modules/JavaAddonManager.jsm
@@ -0,0 +1,115 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["JavaAddonManager"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components; /*global Components */
+
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
+Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
+
+function resolveGeckoURI(uri) {
+ if (!uri) {
+ throw new Error("Can't resolve an empty uri");
+ }
+ if (uri.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(uri, null, null)).spec;
+ } else if (uri.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(uri, null, null));
+ }
+ return uri;
+}
+
+/**
+ * A promise-based API
+ */
+var JavaAddonManager = Object.freeze({
+ classInstanceFromFile: function(classname, filename) {
+ if (!classname) {
+ throw new Error("classname cannot be null");
+ }
+ if (!filename) {
+ throw new Error("filename cannot be null");
+ }
+ return Messaging.sendRequestForResult({
+ type: "JavaAddonManagerV1:Load",
+ classname: classname,
+ filename: resolveGeckoURI(filename)
+ })
+ .then((guid) => {
+ if (!guid) {
+ throw new Error("Internal error: guid should not be null");
+ }
+ return new JavaAddonV1({classname: classname, guid: guid});
+ });
+ }
+});
+
+function JavaAddonV1(options = {}) {
+ if (!(this instanceof JavaAddonV1)) {
+ return new JavaAddonV1(options);
+ }
+ if (!options.classname) {
+ throw new Error("options.classname cannot be null");
+ }
+ if (!options.guid) {
+ throw new Error("options.guid cannot be null");
+ }
+ this._classname = options.classname;
+ this._guid = options.guid;
+ this._loaded = true;
+ this._listeners = {};
+}
+
+JavaAddonV1.prototype = Object.freeze({
+ unload: function() {
+ if (!this._loaded) {
+ return;
+ }
+
+ Messaging.sendRequestForResult({
+ type: "JavaAddonManagerV1:Unload",
+ guid: this._guid
+ })
+ .then(() => {
+ this._loaded = false;
+ for (let listener of this._listeners) {
+ // If we use this.removeListener, we prefix twice.
+ Messaging.removeListener(listener);
+ }
+ this._listeners = {};
+ });
+ },
+
+ _prefix: function(message) {
+ let newMessage = Cu.cloneInto(message, {}, { cloneFunctions: false });
+ newMessage.type = this._guid + ":" + message.type;
+ return newMessage;
+ },
+
+ sendRequest: function(message) {
+ return Messaging.sendRequest(this._prefix(message));
+ },
+
+ sendRequestForResult: function(message) {
+ return Messaging.sendRequestForResult(this._prefix(message));
+ },
+
+ addListener: function(listener, message) {
+ let prefixedMessage = this._guid + ":" + message;
+ this._listeners[prefixedMessage] = listener;
+ return Messaging.addListener(listener, prefixedMessage);
+ },
+
+ removeListener: function(message) {
+ let prefixedMessage = this._guid + ":" + message;
+ delete this._listeners[prefixedMessage];
+ return Messaging.removeListener(prefixedMessage);
+ }
+});
diff --git a/mobile/android/modules/LightweightThemeConsumer.jsm b/mobile/android/modules/LightweightThemeConsumer.jsm
new file mode 100644
index 000000000..3d3ca4c0b
--- /dev/null
+++ b/mobile/android/modules/LightweightThemeConsumer.jsm
@@ -0,0 +1,44 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+function LightweightThemeConsumer(aDocument) {
+ this._doc = aDocument;
+ Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
+ Services.obs.addObserver(this, "lightweight-theme-apply", false);
+
+ this._update(LightweightThemeManager.currentThemeForDisplay);
+}
+
+LightweightThemeConsumer.prototype = {
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic == "lightweight-theme-styling-update")
+ this._update(JSON.parse(aData));
+ else if (aTopic == "lightweight-theme-apply")
+ this._update(LightweightThemeManager.currentThemeForDisplay);
+ },
+
+ destroy: function () {
+ Services.obs.removeObserver(this, "lightweight-theme-styling-update");
+ Services.obs.removeObserver(this, "lightweight-theme-apply");
+ this._doc = null;
+ },
+
+ _update: function (aData) {
+ if (!aData)
+ aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
+
+ let active = !!aData.headerURL;
+
+ let msg = active ? { type: "LightweightTheme:Update", data: aData } :
+ { type: "LightweightTheme:Disable" };
+ Services.androidBridge.handleGeckoMessage(msg);
+ }
+}
diff --git a/mobile/android/modules/MediaPlayerApp.jsm b/mobile/android/modules/MediaPlayerApp.jsm
new file mode 100644
index 000000000..949863d1f
--- /dev/null
+++ b/mobile/android/modules/MediaPlayerApp.jsm
@@ -0,0 +1,166 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* 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";
+
+this.EXPORTED_SYMBOLS = ["MediaPlayerApp"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+var log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "MediaPlayerApp");
+
+// Helper function for sending commands to Java.
+function send(type, data, callback) {
+ let msg = {
+ type: type
+ };
+
+ for (let i in data) {
+ msg[i] = data[i];
+ }
+
+ Messaging.sendRequestForResult(msg)
+ .then(result => callback(result, null),
+ error => callback(null, error));
+}
+
+/* These apps represent players supported natively by the platform. This class will proxy commands
+ * to native controls */
+function MediaPlayerApp(service) {
+ this.service = service;
+ this.location = service.location;
+ this.id = service.uuid;
+}
+
+MediaPlayerApp.prototype = {
+ start: function start(callback) {
+ send("MediaPlayer:Start", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ },
+
+ stop: function stop(callback) {
+ send("MediaPlayer:Stop", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ },
+
+ remoteMedia: function remoteMedia(callback, listener) {
+ if (callback) {
+ callback(new RemoteMedia(this.id, listener));
+ }
+ },
+
+ mirror: function mirror(callback) {
+ send("MediaPlayer:Mirror", { id: this.id }, (result, err) => {
+ if (callback) {
+ callback(err == null);
+ }
+ });
+ }
+}
+
+/* RemoteMedia provides a proxy to a native media player session.
+ */
+function RemoteMedia(id, listener) {
+ this._id = id;
+ this._listener = listener;
+
+ if ("onRemoteMediaStart" in this._listener) {
+ Services.tm.mainThread.dispatch((function() {
+ this._listener.onRemoteMediaStart(this);
+ }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ }
+}
+
+RemoteMedia.prototype = {
+ shutdown: function shutdown() {
+ Services.obs.removeObserver(this, "MediaPlayer:Playing");
+ Services.obs.removeObserver(this, "MediaPlayer:Paused");
+
+ this._send("MediaPlayer:End", {}, (result, err) => {
+ this._status = "shutdown";
+ if ("onRemoteMediaStop" in this._listener) {
+ this._listener.onRemoteMediaStop(this);
+ }
+ });
+ },
+
+ play: function play() {
+ this._send("MediaPlayer:Play", {}, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't play " + err);
+ this.shutdown();
+ return;
+ }
+
+ this._status = "started";
+ });
+ },
+
+ pause: function pause() {
+ this._send("MediaPlayer:Pause", {}, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't pause " + err);
+ this.shutdown();
+ return;
+ }
+
+ this._status = "paused";
+ });
+ },
+
+ load: function load(aData) {
+ this._send("MediaPlayer:Load", aData, (result, err) => {
+ if (err) {
+ Cu.reportError("Can't load " + err);
+ this.shutdown();
+ return;
+ }
+
+ Services.obs.addObserver(this, "MediaPlayer:Playing", false);
+ Services.obs.addObserver(this, "MediaPlayer:Paused", false);
+ this._status = "started";
+ })
+ },
+
+ get status() {
+ return this._status;
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "MediaPlayer:Playing":
+ if (this._status !== "started") {
+ this._status = "started";
+ if ("onRemoteMediaStatus" in this._listener) {
+ this._listener.onRemoteMediaStatus(this);
+ }
+ }
+ break;
+ case "MediaPlayer:Paused":
+ if (this._status !== "paused") {
+ this._status = "paused";
+ if ("onRemoteMediaStatus" in this._listener) {
+ this._listener.onRemoteMediaStatus(this);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+ },
+
+ _send: function(msg, data, callback) {
+ data.id = this._id;
+ send(msg, data, callback);
+ }
+}
diff --git a/mobile/android/modules/Messaging.jsm b/mobile/android/modules/Messaging.jsm
new file mode 100644
index 000000000..30b7f5a96
--- /dev/null
+++ b/mobile/android/modules/Messaging.jsm
@@ -0,0 +1,183 @@
+/* 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/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+this.EXPORTED_SYMBOLS = ["sendMessageToJava", "Messaging"];
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+function sendMessageToJava(aMessage, aCallback) {
+ Cu.reportError("sendMessageToJava is deprecated. Use Messaging API instead.");
+
+ if (aCallback) {
+ Messaging.sendRequestForResult(aMessage)
+ .then(result => aCallback(result, null),
+ error => aCallback(null, error));
+ } else {
+ Messaging.sendRequest(aMessage);
+ }
+}
+
+var Messaging = {
+ /**
+ * Add a listener for the given message.
+ *
+ * Only one request listener can be registered for a given message.
+ *
+ * Example usage:
+ * // aData is data sent from Java with the request. The return value is
+ * // used to respond to the request. The return type *must* be an instance
+ * // of Object.
+ * let listener = function (aData) {
+ * if (aData == "foo") {
+ * return { response: "bar" };
+ * }
+ * return {};
+ * };
+ * Messaging.addListener(listener, "Demo:Request");
+ *
+ * The listener may also be a generator function, useful for performing a
+ * task asynchronously. For example:
+ * let listener = function* (aData) {
+ * // Respond with "bar" after 2 seconds.
+ * yield new Promise(resolve => setTimeout(resolve, 2000));
+ * return { response: "bar" };
+ * };
+ * Messaging.addListener(listener, "Demo:Request");
+ *
+ * @param aListener Listener callback taking a single data parameter (see
+ * example usage above).
+ * @param aMessage Event name that this listener should observe.
+ */
+ addListener: function (aListener, aMessage) {
+ requestHandler.addListener(aListener, aMessage);
+ },
+
+ /**
+ * Removes a listener for a given message.
+ *
+ * @param aMessage The event to stop listening for.
+ */
+ removeListener: function (aMessage) {
+ requestHandler.removeListener(aMessage);
+ },
+
+ /**
+ * Sends a request to Java.
+ *
+ * @param aMessage Message to send; must be an object with a "type" property
+ */
+ sendRequest: function (aMessage) {
+ Services.androidBridge.handleGeckoMessage(aMessage);
+ },
+
+ /**
+ * Sends a request to Java, returning a Promise that resolves to the response.
+ *
+ * @param aMessage Message to send; must be an object with a "type" property
+ * @returns A Promise resolving to the response
+ */
+ sendRequestForResult: function (aMessage) {
+ return new Promise((resolve, reject) => {
+ let id = uuidgen.generateUUID().toString();
+ let obs = {
+ observe: function (aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+ if (data.__guid__ != id) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, aMessage.type + ":Response");
+
+ if (data.status === "success") {
+ resolve(data.response);
+ } else {
+ reject(data.response);
+ }
+ }
+ };
+
+ aMessage.__guid__ = id;
+ Services.obs.addObserver(obs, aMessage.type + ":Response", false);
+
+ this.sendRequest(aMessage);
+ });
+ },
+
+ /**
+ * Handles a request from Java, using the given listener method.
+ * This is mainly an internal method used by the RequestHandler object, but can be
+ * used in nsIObserver.observe implmentations that fall outside the normal usage
+ * patterns.
+ *
+ * @param aTopic The string name of the message
+ * @param aData The data sent to the observe method from Java
+ * @param aListener A function that takes a JSON data argument and returns a
+ * response which is sent to Java.
+ */
+ handleRequest: Task.async(function* (aTopic, aData, aListener) {
+ let wrapper = JSON.parse(aData);
+
+ try {
+ let response = yield aListener(wrapper.data);
+ if (typeof response !== "object" || response === null) {
+ throw new Error("Gecko request listener did not return an object");
+ }
+
+ Messaging.sendRequest({
+ type: "Gecko:Request" + wrapper.id,
+ response: response
+ });
+ } catch (e) {
+ Cu.reportError("Error in Messaging handler for " + aTopic + ": " + e);
+
+ Messaging.sendRequest({
+ type: "Gecko:Request" + wrapper.id,
+ error: {
+ message: e.message || (e && e.toString()),
+ stack: e.stack || Components.stack.formattedStack,
+ }
+ });
+ }
+ })
+};
+
+var requestHandler = {
+ _listeners: {},
+
+ addListener: function (aListener, aMessage) {
+ if (aMessage in this._listeners) {
+ throw new Error("Error in addListener: A listener already exists for message " + aMessage);
+ }
+
+ if (typeof aListener !== "function") {
+ throw new Error("Error in addListener: Listener must be a function for message " + aMessage);
+ }
+
+ this._listeners[aMessage] = aListener;
+ Services.obs.addObserver(this, aMessage, false);
+ },
+
+ removeListener: function (aMessage) {
+ if (!(aMessage in this._listeners)) {
+ throw new Error("Error in removeListener: There is no listener for message " + aMessage);
+ }
+
+ delete this._listeners[aMessage];
+ Services.obs.removeObserver(this, aMessage);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let listener = this._listeners[aTopic];
+ Messaging.handleRequest(aTopic, aData, listener);
+ }
+};
diff --git a/mobile/android/modules/NetErrorHelper.jsm b/mobile/android/modules/NetErrorHelper.jsm
new file mode 100644
index 000000000..9c74df8fe
--- /dev/null
+++ b/mobile/android/modules/NetErrorHelper.jsm
@@ -0,0 +1,175 @@
+/* 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/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/UITelemetry.jsm");
+
+this.EXPORTED_SYMBOLS = ["NetErrorHelper"];
+
+const KEY_CODE_ENTER = 13;
+
+/* Handlers is a list of objects that will be notified when an error page is shown
+ * or when an event occurs on the page that they are registered to handle. Registration
+ * is done by just adding yourself to the dictionary.
+ *
+ * handlers.myKey = {
+ * onPageShown: function(browser) { },
+ * handleEvent: function(event) { },
+ * }
+ *
+ * The key that you register yourself with should match the ID of the element you want to
+ * watch for click events on.
+ */
+
+var handlers = {};
+
+function NetErrorHelper(browser) {
+ browser.addEventListener("click", this.handleClick, true);
+
+ let listener = () => {
+ browser.removeEventListener("click", this.handleClick, true);
+ browser.removeEventListener("pagehide", listener, true);
+ };
+ browser.addEventListener("pagehide", listener, true);
+
+ // Handlers may want to customize the page
+ for (let id in handlers) {
+ if (handlers[id].onPageShown) {
+ handlers[id].onPageShown(browser);
+ }
+ }
+}
+
+NetErrorHelper.attachToBrowser = function(browser) {
+ return new NetErrorHelper(browser);
+}
+
+NetErrorHelper.prototype = {
+ handleClick: function(event) {
+ let node = event.target;
+
+ while(node) {
+ if (node.id in handlers && handlers[node.id].handleClick) {
+ handlers[node.id].handleClick(event);
+ return;
+ }
+
+ node = node.parentNode;
+ }
+ },
+}
+
+handlers.searchbutton = {
+ onPageShown: function(browser) {
+ let search = browser.contentDocument.querySelector("#searchbox");
+ if (!search) {
+ return;
+ }
+
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ let tab = browserWin.BrowserApp.getTabForBrowser(browser);
+
+ // If there is no stored userRequested, just hide the searchbox
+ if (!tab.userRequested) {
+ search.style.display = "none";
+ } else {
+ let text = browser.contentDocument.querySelector("#searchtext");
+ text.value = tab.userRequested;
+ text.addEventListener("keypress", (event) => {
+ if (event.keyCode === KEY_CODE_ENTER) {
+ this.doSearch(event.target.value);
+ }
+ });
+ }
+ },
+
+ handleClick: function(event) {
+ let value = event.target.previousElementSibling.value;
+ this.doSearch(value);
+ },
+
+ doSearch: function(value) {
+ UITelemetry.addEvent("neterror.1", "button", null, "search");
+ let engine = Services.search.defaultEngine;
+ let uri = engine.getSubmission(value).uri;
+
+ let browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ // Reset the user search to whatever the new search term was
+ browserWin.BrowserApp.loadURI(uri.spec, undefined, { isSearch: true, userRequested: value });
+ }
+};
+
+handlers.wifi = {
+ // This registers itself with the nsIObserverService as a weak ref,
+ // so we have to implement GetWeakReference as well.
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ GetWeakReference: function() {
+ return Cu.getWeakReference(this);
+ },
+
+ onPageShown: function(browser) {
+ // If we have a connection, don't bother showing the wifi toggle.
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (network.isLinkUp && network.linkStatusKnown) {
+ let nodes = browser.contentDocument.querySelectorAll("#wifi");
+ for (let i = 0; i < nodes.length; i++) {
+ nodes[i].style.display = "none";
+ }
+ }
+ },
+
+ handleClick: function(event) {
+ let node = event.target;
+ while(node && node.id !== "wifi") {
+ node = node.parentNode;
+ }
+
+ if (!node) {
+ return;
+ }
+
+ UITelemetry.addEvent("neterror.1", "button", null, "wifitoggle");
+ // Show indeterminate progress while we wait for the network.
+ node.disabled = true;
+ node.classList.add("inProgress");
+
+ this.node = Cu.getWeakReference(node);
+ Services.obs.addObserver(this, "network:link-status-changed", true);
+
+ Messaging.sendRequest({
+ type: "Wifi:Enable"
+ });
+ },
+
+ observe: function(subject, topic, data) {
+ let node = this.node.get();
+ if (!node) {
+ return;
+ }
+
+ // Remove the progress bar
+ node.disabled = false;
+ node.classList.remove("inProgress");
+
+ let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (network.isLinkUp && network.linkStatusKnown) {
+ // If everything worked, reload the page
+ UITelemetry.addEvent("neterror.1", "button", null, "wifitoggle.reload");
+ Services.obs.removeObserver(this, "network:link-status-changed");
+
+ // Even at this point, Android sometimes lies about the real state of the network and this reload request fails.
+ // Add a 500ms delay before refreshing the page.
+ node.ownerDocument.defaultView.setTimeout(function() {
+ node.ownerDocument.location.reload(false);
+ }, 500);
+ }
+ }
+}
+
diff --git a/mobile/android/modules/Notifications.jsm b/mobile/android/modules/Notifications.jsm
new file mode 100644
index 000000000..a035bb2e3
--- /dev/null
+++ b/mobile/android/modules/Notifications.jsm
@@ -0,0 +1,259 @@
+/* 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/Services.jsm");
+
+this.EXPORTED_SYMBOLS = ["Notifications"];
+
+function log(msg) {
+ // Services.console.logStringMessage(msg);
+}
+
+var _notificationsMap = {};
+var _handlersMap = {};
+
+function Notification(aId, aOptions) {
+ this._id = aId;
+ this._when = (new Date()).getTime();
+ this.fillWithOptions(aOptions);
+}
+
+Notification.prototype = {
+ fillWithOptions: function(aOptions) {
+ if ("icon" in aOptions && aOptions.icon != null)
+ this._icon = aOptions.icon;
+ else
+ throw "Notification icon is mandatory";
+
+ if ("title" in aOptions && aOptions.title != null)
+ this._title = aOptions.title;
+ else
+ throw "Notification title is mandatory";
+
+ if ("message" in aOptions && aOptions.message != null)
+ this._message = aOptions.message;
+ else
+ this._message = null;
+
+ if ("priority" in aOptions && aOptions.priority != null)
+ this._priority = aOptions.priority;
+
+ if ("buttons" in aOptions && aOptions.buttons != null) {
+ if (aOptions.buttons.length > 3)
+ throw "Too many buttons provided. The max number is 3";
+
+ this._buttons = {};
+ for (let i = 0; i < aOptions.buttons.length; i++) {
+ let button_id = aOptions.buttons[i].buttonId;
+ this._buttons[button_id] = aOptions.buttons[i];
+ }
+ } else {
+ this._buttons = null;
+ }
+
+ if ("ongoing" in aOptions && aOptions.ongoing != null)
+ this._ongoing = aOptions.ongoing;
+ else
+ this._ongoing = false;
+
+ if ("progress" in aOptions && aOptions.progress != null)
+ this._progress = aOptions.progress;
+ else
+ this._progress = null;
+
+ if ("onCancel" in aOptions && aOptions.onCancel != null)
+ this._onCancel = aOptions.onCancel;
+ else
+ this._onCancel = null;
+
+ if ("onClick" in aOptions && aOptions.onClick != null)
+ this._onClick = aOptions.onClick;
+ else
+ this._onClick = null;
+
+ if ("cookie" in aOptions && aOptions.cookie != null)
+ this._cookie = aOptions.cookie;
+ else
+ this._cookie = null;
+
+ if ("handlerKey" in aOptions && aOptions.handlerKey != null)
+ this._handlerKey = aOptions.handlerKey;
+
+ if ("persistent" in aOptions && aOptions.persistent != null)
+ this._persistent = aOptions.persistent;
+ else
+ this._persistent = false;
+ },
+
+ show: function() {
+ let msg = {
+ type: "Notification:Show",
+ id: this._id,
+ title: this._title,
+ smallIcon: this._icon,
+ ongoing: this._ongoing,
+ when: this._when,
+ persistent: this._persistent,
+ };
+
+ if (this._message)
+ msg.text = this._message;
+
+ if (this._progress) {
+ msg.progress_value = this._progress;
+ msg.progress_max = 100;
+ msg.progress_indeterminate = false;
+ } else if (Number.isNaN(this._progress)) {
+ msg.progress_value = 0;
+ msg.progress_max = 0;
+ msg.progress_indeterminate = true;
+ }
+
+ if (this._cookie)
+ msg.cookie = JSON.stringify(this._cookie);
+
+ if (this._priority)
+ msg.priority = this._priority;
+
+ if (this._buttons) {
+ msg.actions = [];
+ let buttonName;
+ for (buttonName in this._buttons) {
+ let button = this._buttons[buttonName];
+ let obj = {
+ buttonId: button.buttonId,
+ title : button.title,
+ icon : button.icon
+ };
+ msg.actions.push(obj);
+ }
+ }
+
+ if (this._light)
+ msg.light = this._light;
+
+ if (this._handlerKey)
+ msg.handlerKey = this._handlerKey;
+
+ Services.androidBridge.handleGeckoMessage(msg);
+ return this;
+ },
+
+ cancel: function() {
+ let msg = {
+ type: "Notification:Hide",
+ id: this._id,
+ handlerKey: this._handlerKey,
+ cookie: JSON.stringify(this._cookie),
+ };
+ Services.androidBridge.handleGeckoMessage(msg);
+ }
+}
+
+var Notifications = {
+ get idService() {
+ delete this.idService;
+ return this.idService = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+ },
+
+ registerHandler: function(key, handler) {
+ if (!_handlersMap[key]) {
+ _handlersMap[key] = [];
+ }
+ _handlersMap[key].push(handler);
+ },
+
+ unregisterHandler: function(key, handler) {
+ let h = _handlersMap[key];
+ if (!h) {
+ return;
+ }
+ let i = h.indexOf(handler);
+ if (i > -1) {
+ h.splice(i, 1);
+ }
+ },
+
+ create: function notif_notify(aOptions) {
+ let id = this.idService.generateUUID().toString();
+
+ let notification = new Notification(id, aOptions);
+ _notificationsMap[id] = notification;
+ notification.show();
+
+ return id;
+ },
+
+ update: function notif_update(aId, aOptions) {
+ let notification = _notificationsMap[aId];
+ if (!notification)
+ throw "Unknown notification id";
+ notification.fillWithOptions(aOptions);
+ notification.show();
+ },
+
+ cancel: function notif_cancel(aId) {
+ let notification = _notificationsMap[aId];
+ if (notification)
+ notification.cancel();
+ },
+
+ observe: function notif_observe(aSubject, aTopic, aData) {
+ Services.console.logStringMessage(aTopic + " " + aData);
+
+ let data = JSON.parse(aData);
+ let id = data.id;
+ let handlerKey = data.handlerKey;
+ let cookie = data.cookie ? JSON.parse(data.cookie) : undefined;
+ let notification = _notificationsMap[id];
+
+ switch (data.eventType) {
+ case "notification-clicked":
+ if (notification && notification._onClick)
+ notification._onClick(id, notification._cookie);
+
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onClick(cookie);
+ });
+ }
+
+ break;
+ case "notification-button-clicked":
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onButtonClick(data.buttonId, cookie);
+ });
+ }
+
+ break;
+ case "notification-cleared":
+ case "notification-closed":
+ if (handlerKey) {
+ _handlersMap[handlerKey].forEach(function(handler) {
+ handler.onCancel(cookie);
+ });
+ }
+
+ if (notification && notification._onCancel)
+ notification._onCancel(id, notification._cookie);
+ delete _notificationsMap[id]; // since the notification was dismissed, we no longer need to hold a reference.
+ break;
+ }
+ },
+
+ QueryInterface: function (aIID) {
+ if (!aIID.equals(Ci.nsISupports) &&
+ !aIID.equals(Ci.nsIObserver) &&
+ !aIID.equals(Ci.nsISupportsWeakReference))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+Services.obs.addObserver(Notifications, "Notification:Event", false);
diff --git a/mobile/android/modules/PageActions.jsm b/mobile/android/modules/PageActions.jsm
new file mode 100644
index 000000000..a66268f82
--- /dev/null
+++ b/mobile/android/modules/PageActions.jsm
@@ -0,0 +1,113 @@
+/* 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/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
+
+this.EXPORTED_SYMBOLS = ["PageActions"];
+
+// Copied from browser.js
+// TODO: We should move this method to a common importable location
+function resolveGeckoURI(aURI) {
+ if (!aURI)
+ throw "Can't resolve an empty uri";
+
+ if (aURI.startsWith("chrome://")) {
+ let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
+ return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
+ } else if (aURI.startsWith("resource://")) {
+ let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+ return handler.resolveURI(Services.io.newURI(aURI, null, null));
+ }
+ return aURI;
+}
+
+var PageActions = {
+ _items: { },
+
+ _inited: false,
+
+ _maybeInit: function() {
+ if (!this._inited && Object.keys(this._items).length > 0) {
+ this._inited = true;
+ Services.obs.addObserver(this, "PageActions:Clicked", false);
+ Services.obs.addObserver(this, "PageActions:LongClicked", false);
+ }
+ },
+
+ _maybeUninit: function() {
+ if (this._inited && Object.keys(this._items).length == 0) {
+ this._inited = false;
+ Services.obs.removeObserver(this, "PageActions:Clicked");
+ Services.obs.removeObserver(this, "PageActions:LongClicked");
+ }
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ let item = this._items[aData];
+ if (aTopic == "PageActions:Clicked") {
+ if (item.clickCallback) {
+ item.clickCallback();
+ }
+ } else if (aTopic == "PageActions:LongClicked") {
+ if (item.longClickCallback) {
+ item.longClickCallback();
+ }
+ }
+ },
+
+ isShown: function(id) {
+ return !!this._items[id];
+ },
+
+ synthesizeClick: function(id) {
+ let item = this._items[id];
+ if (item && item.clickCallback) {
+ item.clickCallback();
+ }
+ },
+
+ add: function(aOptions) {
+ let id = aOptions.id || uuidgen.generateUUID().toString()
+
+ Messaging.sendRequest({
+ type: "PageActions:Add",
+ id: id,
+ title: aOptions.title,
+ icon: resolveGeckoURI(aOptions.icon),
+ important: "important" in aOptions ? aOptions.important : false
+ });
+
+ this._items[id] = {};
+
+ if (aOptions.clickCallback) {
+ this._items[id].clickCallback = aOptions.clickCallback;
+ }
+
+ if (aOptions.longClickCallback) {
+ this._items[id].longClickCallback = aOptions.longClickCallback;
+ }
+
+ this._maybeInit();
+ return id;
+ },
+
+ remove: function(id) {
+ Messaging.sendRequest({
+ type: "PageActions:Remove",
+ id: id
+ });
+
+ delete this._items[id];
+ this._maybeUninit();
+ }
+}
diff --git a/mobile/android/modules/Prompt.jsm b/mobile/android/modules/Prompt.jsm
new file mode 100644
index 000000000..5bed87650
--- /dev/null
+++ b/mobile/android/modules/Prompt.jsm
@@ -0,0 +1,234 @@
+/* -*- 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/. */
+"use strict"
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Messaging.jsm");
+
+this.EXPORTED_SYMBOLS = ["Prompt"];
+
+function log(msg) {
+ Services.console.logStringMessage(msg);
+}
+
+function Prompt(aOptions) {
+ this.window = "window" in aOptions ? aOptions.window : null;
+
+ this.msg = { async: true };
+
+ if (this.window) {
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+ var tab = window.BrowserApp.getTabForWindow(this.window);
+ if (tab) {
+ this.msg.tabId = tab.id;
+ }
+ }
+
+ if (aOptions.priority === 1)
+ this.msg.type = "Prompt:ShowTop"
+ else
+ this.msg.type = "Prompt:Show"
+
+ if ("title" in aOptions && aOptions.title != null)
+ this.msg.title = aOptions.title;
+
+ if ("message" in aOptions && aOptions.message != null)
+ this.msg.text = aOptions.message;
+
+ if ("buttons" in aOptions && aOptions.buttons != null)
+ this.msg.buttons = aOptions.buttons;
+
+ if ("doubleTapButton" in aOptions && aOptions.doubleTapButton != null)
+ this.msg.doubleTapButton = aOptions.doubleTapButton;
+
+ if ("hint" in aOptions && aOptions.hint != null)
+ this.msg.hint = aOptions.hint;
+}
+
+Prompt.prototype = {
+ setHint: function(aHint) {
+ if (!aHint)
+ delete this.msg.hint;
+ else
+ this.msg.hint = aHint;
+ return this;
+ },
+
+ addButton: function(aOptions) {
+ if (!this.msg.buttons)
+ this.msg.buttons = [];
+ this.msg.buttons.push(aOptions.label);
+ return this;
+ },
+
+ _addInput: function(aOptions) {
+ let obj = aOptions;
+ if (this[aOptions.type + "_count"] === undefined)
+ this[aOptions.type + "_count"] = 0;
+
+ obj.id = aOptions.id || (aOptions.type + this[aOptions.type + "_count"]);
+ this[aOptions.type + "_count"]++;
+
+ if (!this.msg.inputs)
+ this.msg.inputs = [];
+ this.msg.inputs.push(obj);
+ return this;
+ },
+
+ addCheckbox: function(aOptions) {
+ return this._addInput({
+ type: "checkbox",
+ label: aOptions.label,
+ checked: aOptions.checked,
+ id: aOptions.id
+ });
+ },
+
+ addTextbox: function(aOptions) {
+ return this._addInput({
+ type: "textbox",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id: aOptions.id
+ });
+ },
+
+ addNumber: function(aOptions) {
+ return this._addInput({
+ type: "number",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id: aOptions.id
+ });
+ },
+
+ addPassword: function(aOptions) {
+ return this._addInput({
+ type: "password",
+ value: aOptions.value,
+ hint: aOptions.hint,
+ autofocus: aOptions.autofocus,
+ id : aOptions.id
+ });
+ },
+
+ addDatePicker: function(aOptions) {
+ return this._addInput({
+ type: aOptions.type || "date",
+ value: aOptions.value,
+ id: aOptions.id,
+ max: aOptions.max,
+ min: aOptions.min
+ });
+ },
+
+ addColorPicker: function(aOptions) {
+ return this._addInput({
+ type: "color",
+ value: aOptions.value,
+ id: aOptions.id
+ });
+ },
+
+ addLabel: function(aOptions) {
+ return this._addInput({
+ type: "label",
+ label: aOptions.label,
+ id: aOptions.id
+ });
+ },
+
+ addMenulist: function(aOptions) {
+ return this._addInput({
+ type: "menulist",
+ values: aOptions.values,
+ id: aOptions.id
+ });
+ },
+
+ addIconGrid: function(aOptions) {
+ return this._addInput({
+ type: "icongrid",
+ items: aOptions.items,
+ id: aOptions.id
+ });
+ },
+
+ addTabs: function(aOptions) {
+ return this._addInput({
+ type: "tabs",
+ items: aOptions.items,
+ id: aOptions.id
+ });
+ },
+
+ show: function(callback) {
+ this.callback = callback;
+ log("Sending message");
+ this._innerShow();
+ },
+
+ _innerShow: function() {
+ Messaging.sendRequestForResult(this.msg).then((data) => {
+ if (this.callback)
+ this.callback(data);
+ });
+ },
+
+ _setListItems: function(aItems) {
+ let hasSelected = false;
+ this.msg.listitems = [];
+
+ aItems.forEach(function(item) {
+ let obj = { id: item.id };
+
+ obj.label = item.label;
+
+ if (item.disabled)
+ obj.disabled = true;
+
+ if (item.selected) {
+ if (!this.msg.choiceMode) {
+ this.msg.choiceMode = "single";
+ }
+ obj.selected = item.selected;
+ }
+
+ if (item.header)
+ obj.isGroup = true;
+
+ if (item.menu)
+ obj.isParent = true;
+
+ if (item.child)
+ obj.inGroup = true;
+
+ if (item.showAsActions)
+ obj.showAsActions = item.showAsActions;
+
+ if (item.icon)
+ obj.icon = item.icon;
+
+ this.msg.listitems.push(obj);
+
+ }, this);
+ return this;
+ },
+
+ setSingleChoiceItems: function(aItems) {
+ return this._setListItems(aItems);
+ },
+
+ setMultiChoiceItems: function(aItems) {
+ this.msg.choiceMode = "multiple";
+ return this._setListItems(aItems);
+ },
+
+}
diff --git a/mobile/android/modules/RuntimePermissions.jsm b/mobile/android/modules/RuntimePermissions.jsm
new file mode 100644
index 000000000..42d8024b1
--- /dev/null
+++ b/mobile/android/modules/RuntimePermissions.jsm
@@ -0,0 +1,41 @@
+/* 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;
+
+this.EXPORTED_SYMBOLS = ["RuntimePermissions"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+
+// See: http://developer.android.com/reference/android/Manifest.permission.html
+const CAMERA = "android.permission.CAMERA";
+const WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
+const RECORD_AUDIO = "android.permission.RECORD_AUDIO";
+
+var RuntimePermissions = {
+ CAMERA: CAMERA,
+ RECORD_AUDIO: RECORD_AUDIO,
+ WRITE_EXTERNAL_STORAGE: WRITE_EXTERNAL_STORAGE,
+
+ /**
+ * Check whether the permissions have been granted or not. If needed prompt the user to accept the permissions.
+ *
+ * @returns A promise resolving to true if all the permissions have been granted or false if any of the
+ * permissions have been denied.
+ */
+ waitForPermissions: function(permission) {
+ let permissions = [].concat(permission);
+
+ let msg = {
+ type: 'RuntimePermissions:Prompt',
+ permissions: permissions
+ };
+
+ return Messaging.sendRequestForResult(msg);
+ }
+}; \ No newline at end of file
diff --git a/mobile/android/modules/SSLExceptions.jsm b/mobile/android/modules/SSLExceptions.jsm
new file mode 100644
index 000000000..48dfe8d92
--- /dev/null
+++ b/mobile/android/modules/SSLExceptions.jsm
@@ -0,0 +1,118 @@
+/* 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"
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["SSLExceptions"];
+
+/**
+ A class to add exceptions to override SSL certificate problems. The functionality
+ itself is borrowed from exceptionDialog.js.
+*/
+function SSLExceptions() {
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+}
+
+
+SSLExceptions.prototype = {
+ _overrideService: null,
+ _sslStatus: null,
+
+ getInterface: function SSLE_getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+ QueryInterface: function SSLE_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIBadCertListener2) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ /**
+ To collect the SSL status we intercept the certificate error here
+ and store the status for later use.
+ */
+ notifyCertProblem: function SSLE_notifyCertProblem(socketInfo, sslStatus, targetHost) {
+ this._sslStatus = sslStatus.QueryInterface(Ci.nsISSLStatus);
+ return true; // suppress error UI
+ },
+
+ /**
+ Attempt to download the certificate for the location specified to get the SSLState
+ for the certificate and the errors.
+ */
+ _checkCert: function SSLE_checkCert(aURI) {
+ this._sslStatus = null;
+
+ let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
+ try {
+ if (aURI) {
+ req.open("GET", aURI.prePath, false);
+ req.channel.notificationCallbacks = this;
+ req.send(null);
+ }
+ } catch (e) {
+ // We *expect* exceptions if there are problems with the certificate
+ // presented by the site. Log it, just in case, but we can proceed here,
+ // with appropriate sanity checks
+ Components.utils.reportError("Attempted to connect to a site with a bad certificate in the add exception dialog. " +
+ "This results in a (mostly harmless) exception being thrown. " +
+ "Logged for information purposes only: " + e);
+ }
+
+ return this._sslStatus;
+ },
+
+ /**
+ Internal method to create an override.
+ */
+ _addOverride: function SSLE_addOverride(aURI, aWindow, aTemporary) {
+ let SSLStatus = this._checkCert(aURI);
+ let certificate = SSLStatus.serverCert;
+
+ let flags = 0;
+
+ // in private browsing do not store exceptions permanently ever
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ aTemporary = true;
+ }
+
+ if (SSLStatus.isUntrusted)
+ flags |= this._overrideService.ERROR_UNTRUSTED;
+ if (SSLStatus.isDomainMismatch)
+ flags |= this._overrideService.ERROR_MISMATCH;
+ if (SSLStatus.isNotValidAtThisTime)
+ flags |= this._overrideService.ERROR_TIME;
+
+ this._overrideService.rememberValidityOverride(
+ aURI.asciiHost,
+ aURI.port,
+ certificate,
+ flags,
+ aTemporary);
+ },
+
+ /**
+ Creates a permanent exception to override all overridable errors for
+ the given URL.
+ */
+ addPermanentException: function SSLE_addPermanentException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, false);
+ },
+
+ /**
+ Creates a temporary exception to override all overridable errors for
+ the given URL.
+ */
+ addTemporaryException: function SSLE_addTemporaryException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, true);
+ }
+};
diff --git a/mobile/android/modules/Sanitizer.jsm b/mobile/android/modules/Sanitizer.jsm
new file mode 100644
index 000000000..014a89688
--- /dev/null
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -0,0 +1,303 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 4 -*-
+/* 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/. */
+
+/*globals LoadContextInfo, FormHistory, Accounts */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/LoadContextInfo.jsm");
+Cu.import("resource://gre/modules/FormHistory.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Downloads.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Accounts.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
+ "resource://gre/modules/DownloadIntegration.jsm");
+
+function dump(a) {
+ Services.console.logStringMessage(a);
+}
+
+this.EXPORTED_SYMBOLS = ["Sanitizer"];
+
+function Sanitizer() {}
+Sanitizer.prototype = {
+ clearItem: function (aItemName)
+ {
+ let item = this.items[aItemName];
+ let canClear = item.canClear;
+ if (typeof canClear == "function") {
+ canClear(function clearCallback(aCanClear) {
+ if (aCanClear)
+ item.clear();
+ });
+ } else if (canClear) {
+ item.clear();
+ }
+ },
+
+ items: {
+ cache: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
+ try {
+ cache.clear();
+ } catch(er) {}
+
+ let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
+ .getImgCacheForDocument(null);
+ try {
+ imageCache.clearCache(false); // true=chrome, false=content
+ } catch(er) {}
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ cookies: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ Services.cookies.removeAll();
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ siteSettings: {
+ clear: Task.async(function* () {
+ // Clear site-specific permissions like "Allow this site to open popups"
+ Services.perms.removeAll();
+
+ // Clear site-specific settings like page-zoom level
+ Cc["@mozilla.org/content-pref/service;1"]
+ .getService(Ci.nsIContentPrefService2)
+ .removeAllDomains(null);
+
+ // Clear site security settings
+ var sss = Cc["@mozilla.org/ssservice;1"]
+ .getService(Ci.nsISiteSecurityService);
+ sss.clearAll();
+
+ // Clear push subscriptions
+ yield new Promise((resolve, reject) => {
+ let push = Cc["@mozilla.org/push/Service;1"]
+ .getService(Ci.nsIPushService);
+ push.clearForDomain("*", status => {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject(new Error("Error clearing push subscriptions: " +
+ status));
+ }
+ });
+ });
+ }),
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ offlineApps: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ var cacheService = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
+ var appCacheStorage = cacheService.appCacheStorage(LoadContextInfo.default, null);
+ try {
+ appCacheStorage.asyncEvictStorage(null);
+ } catch(er) {}
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ history: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory" })
+ .catch(e => Cu.reportError("Java-side history clearing failed: " + e))
+ .then(function() {
+ try {
+ Services.obs.notifyObservers(null, "browser:purge-session-history", "");
+ }
+ catch (e) { }
+
+ try {
+ var predictor = Cc["@mozilla.org/network/predictor;1"].getService(Ci.nsINetworkPredictor);
+ predictor.reset();
+ } catch (e) { }
+ });
+ },
+
+ get canClear()
+ {
+ // bug 347231: Always allow clearing history due to dependencies on
+ // the browser:purge-session-history notification. (like error console)
+ return true;
+ }
+ },
+
+ searchHistory: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory", clearSearchHistory: true })
+ .catch(e => Cu.reportError("Java-side search history clearing failed: " + e))
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ formdata: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ FormHistory.update({ op: "remove" });
+ resolve();
+ });
+ },
+
+ canClear: function (aCallback)
+ {
+ let count = 0;
+ let countDone = {
+ handleResult: function(aResult) { count = aResult; },
+ handleError: function(aError) { Cu.reportError(aError); },
+ handleCompletion: function(aReason) { aCallback(aReason == 0 && count > 0); }
+ };
+ FormHistory.count({}, countDone);
+ }
+ },
+
+ downloadFiles: {
+ clear: Task.async(function* () {
+ let list = yield Downloads.getList(Downloads.ALL);
+ let downloads = yield list.getAll();
+ var finalizePromises = [];
+
+ // Logic copied from DownloadList.removeFinished. Ideally, we would
+ // just use that method directly, but we want to be able to remove the
+ // downloaded files as well.
+ for (let download of downloads) {
+ // Remove downloads that have been canceled, even if the cancellation
+ // operation hasn't completed yet so we don't check "stopped" here.
+ // Failed downloads with partial data are also removed.
+ if (download.stopped && (!download.hasPartialData || download.error)) {
+ // Remove the download first, so that the views don't get the change
+ // notifications that may occur during finalization.
+ yield list.remove(download);
+ // Ensure that the download is stopped and no partial data is kept.
+ // This works even if the download state has changed meanwhile. We
+ // don't need to wait for the procedure to be complete before
+ // processing the other downloads in the list.
+ finalizePromises.push(download.finalize(true).then(() => null, Cu.reportError));
+
+ // Delete the downloaded files themselves.
+ OS.File.remove(download.target.path).then(() => null, ex => {
+ if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
+ Cu.reportError(ex);
+ }
+ });
+ }
+ }
+
+ yield Promise.all(finalizePromises);
+ yield DownloadIntegration.forceSave();
+ }),
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ passwords: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ Services.logins.removeAllLogins();
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ let count = Services.logins.countLogins("", "", ""); // count all logins
+ return (count > 0);
+ }
+ },
+
+ sessions: {
+ clear: function ()
+ {
+ return new Promise(function(resolve, reject) {
+ // clear all auth tokens
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+
+ // clear FTP and plain HTTP auth sessions
+ Services.obs.notifyObservers(null, "net:clear-active-logins", null);
+
+ resolve();
+ });
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ syncedTabs: {
+ clear: function ()
+ {
+ return Messaging.sendRequestForResult({ type: "Sanitize:ClearSyncedTabs" })
+ .catch(e => Cu.reportError("Java-side synced tabs clearing failed: " + e));
+ },
+
+ canClear: function(aCallback)
+ {
+ Accounts.anySyncAccountsExist().then(aCallback)
+ .catch(function(err) {
+ Cu.reportError("Java-side synced tabs clearing failed: " + err)
+ aCallback(false);
+ });
+ }
+ }
+
+ }
+};
+
+this.Sanitizer = new Sanitizer();
diff --git a/mobile/android/modules/SharedPreferences.jsm b/mobile/android/modules/SharedPreferences.jsm
new file mode 100644
index 000000000..3f32df6ea
--- /dev/null
+++ b/mobile/android/modules/SharedPreferences.jsm
@@ -0,0 +1,254 @@
+// -*- 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["SharedPreferences"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// For adding observers.
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+var Scope = Object.freeze({
+ APP: "app",
+ PROFILE: "profile",
+ GLOBAL: "global"
+});
+
+/**
+ * Public API to getting a SharedPreferencesImpl instance. These scopes mirror GeckoSharedPrefs.
+ */
+var SharedPreferences = {
+ forApp: function() {
+ return new SharedPreferencesImpl({ scope: Scope.APP });
+ },
+
+ forProfile: function() {
+ return new SharedPreferencesImpl({ scope: Scope.PROFILE });
+ },
+
+ /**
+ * Get SharedPreferences for the named profile; if the profile name is null,
+ * returns the preferences for the current profile (just like |forProfile|).
+ */
+ forProfileName: function(profileName) {
+ return new SharedPreferencesImpl({ scope: Scope.PROFILE, profileName: profileName });
+ },
+
+ /**
+ * Get SharedPreferences for the given Android branch; if the branch is null,
+ * returns the default preferences branch for the application, which is the
+ * output of |PreferenceManager.getDefaultSharedPreferences|.
+ */
+ forAndroid: function(branch) {
+ return new SharedPreferencesImpl({ scope: Scope.GLOBAL, branch: branch });
+ }
+};
+
+/**
+ * Create an interface to an Android SharedPreferences branch.
+ *
+ * options {Object} with the following valid keys:
+ * - scope {String} (required) specifies the scope of preferences that should be accessed.
+ * - branch {String} (only when using Scope.GLOBAL) should be a string describing a preferences branch,
+ * like "UpdateService" or "background.data", or null to access the
+ * default preferences branch for the application.
+ * - profileName {String} (optional, only valid when using Scope.PROFILE)
+ */
+function SharedPreferencesImpl(options = {}) {
+ if (!(this instanceof SharedPreferencesImpl)) {
+ return new SharedPreferencesImpl(options);
+ }
+
+ if (options.scope == null || options.scope == undefined) {
+ throw "Shared Preferences must specifiy a scope.";
+ }
+
+ this._scope = options.scope;
+ this._profileName = options.profileName;
+ this._branch = options.branch;
+ this._observers = {};
+}
+
+SharedPreferencesImpl.prototype = Object.freeze({
+ _set: function _set(prefs) {
+ Messaging.sendRequest({
+ type: "SharedPreferences:Set",
+ preferences: prefs,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+
+ _setOne: function _setOne(prefName, value, type) {
+ let prefs = [];
+ prefs.push({
+ name: prefName,
+ value: value,
+ type: type,
+ });
+ this._set(prefs);
+ },
+
+ setBoolPref: function setBoolPref(prefName, value) {
+ this._setOne(prefName, value, "bool");
+ },
+
+ setCharPref: function setCharPref(prefName, value) {
+ this._setOne(prefName, value, "string");
+ },
+
+ setIntPref: function setIntPref(prefName, value) {
+ this._setOne(prefName, value, "int");
+ },
+
+ _get: function _get(prefs, callback) {
+ let result = null;
+ Messaging.sendRequestForResult({
+ type: "SharedPreferences:Get",
+ preferences: prefs,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ }).then((data) => {
+ result = data.values;
+ });
+
+ let thread = Services.tm.currentThread;
+ while (result == null)
+ thread.processNextEvent(true);
+
+ return result;
+ },
+
+ _getOne: function _getOne(prefName, type) {
+ let prefs = [];
+ prefs.push({
+ name: prefName,
+ type: type,
+ });
+ let values = this._get(prefs);
+ if (values.length != 1) {
+ throw new Error("Got too many values: " + values.length);
+ }
+ return values[0].value;
+ },
+
+ getBoolPref: function getBoolPref(prefName) {
+ return this._getOne(prefName, "bool");
+ },
+
+ getCharPref: function getCharPref(prefName) {
+ return this._getOne(prefName, "string");
+ },
+
+ getIntPref: function getIntPref(prefName) {
+ return this._getOne(prefName, "int");
+ },
+
+ /**
+ * Invoke `observer` after a change to the preference `domain` in
+ * the current branch.
+ *
+ * `observer` should implement the nsIObserver.observe interface.
+ */
+ addObserver: function addObserver(domain, observer, holdWeak) {
+ if (!domain)
+ throw new Error("domain must not be null");
+ if (!observer)
+ throw new Error("observer must not be null");
+ if (holdWeak)
+ throw new Error("Weak references not yet implemented.");
+
+ if (!this._observers.hasOwnProperty(domain))
+ this._observers[domain] = [];
+ if (this._observers[domain].indexOf(observer) > -1)
+ return;
+
+ this._observers[domain].push(observer);
+
+ this._updateAndroidListener();
+ },
+
+ /**
+ * Do not invoke `observer` after a change to the preference
+ * `domain` in the current branch.
+ */
+ removeObserver: function removeObserver(domain, observer) {
+ if (!this._observers.hasOwnProperty(domain))
+ return;
+ let index = this._observers[domain].indexOf(observer);
+ if (index < 0)
+ return;
+
+ this._observers[domain].splice(index, 1);
+ if (this._observers[domain].length < 1)
+ delete this._observers[domain];
+
+ this._updateAndroidListener();
+ },
+
+ _updateAndroidListener: function _updateAndroidListener() {
+ if (this._listening && Object.keys(this._observers).length < 1)
+ this._uninstallAndroidListener();
+ if (!this._listening && Object.keys(this._observers).length > 0)
+ this._installAndroidListener();
+ },
+
+ _installAndroidListener: function _installAndroidListener() {
+ if (this._listening)
+ return;
+ this._listening = true;
+
+ Services.obs.addObserver(this, "SharedPreferences:Changed", false);
+ Messaging.sendRequest({
+ type: "SharedPreferences:Observe",
+ enable: true,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+
+ observe: function observe(subject, topic, data) {
+ if (topic != "SharedPreferences:Changed") {
+ return;
+ }
+
+ let msg = JSON.parse(data);
+ if (msg.scope !== this._scope ||
+ ((this._scope === Scope.PROFILE) && (msg.profileName !== this._profileName)) ||
+ ((this._scope === Scope.GLOBAL) && (msg.branch !== this._branch))) {
+ return;
+ }
+
+ if (!this._observers.hasOwnProperty(msg.key)) {
+ return;
+ }
+
+ let observers = this._observers[msg.key];
+ for (let obs of observers) {
+ obs.observe(obs, msg.key, msg.value);
+ }
+ },
+
+ _uninstallAndroidListener: function _uninstallAndroidListener() {
+ if (!this._listening)
+ return;
+ this._listening = false;
+
+ Services.obs.removeObserver(this, "SharedPreferences:Changed");
+ Messaging.sendRequest({
+ type: "SharedPreferences:Observe",
+ enable: false,
+ scope: this._scope,
+ profileName: this._profileName,
+ branch: this._branch,
+ });
+ },
+});
diff --git a/mobile/android/modules/Snackbars.jsm b/mobile/android/modules/Snackbars.jsm
new file mode 100644
index 000000000..066a28c56
--- /dev/null
+++ b/mobile/android/modules/Snackbars.jsm
@@ -0,0 +1,72 @@
+/* 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;
+
+this.EXPORTED_SYMBOLS = ["Snackbars"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+
+const LENGTH_INDEFINITE = -2;
+const LENGTH_LONG = 0;
+const LENGTH_SHORT = -1;
+
+var Snackbars = {
+ LENGTH_INDEFINITE: LENGTH_INDEFINITE,
+ LENGTH_LONG: LENGTH_LONG,
+ LENGTH_SHORT: LENGTH_SHORT,
+
+ show: function(aMessage, aDuration, aOptions) {
+
+ // Takes care of the deprecated toast calls
+ if (typeof aDuration === "string") {
+ [aDuration, aOptions] = migrateToastIfNeeded(aDuration, aOptions);
+ }
+
+ let msg = {
+ type: 'Snackbar:Show',
+ message: aMessage,
+ duration: aDuration,
+ };
+
+ if (aOptions && aOptions.backgroundColor) {
+ msg.backgroundColor = aOptions.backgroundColor;
+ }
+
+ if (aOptions && aOptions.action) {
+ msg.action = {};
+
+ if (aOptions.action.label) {
+ msg.action.label = aOptions.action.label;
+ }
+
+ Messaging.sendRequestForResult(msg).then(result => aOptions.action.callback());
+ } else {
+ Messaging.sendRequest(msg);
+ }
+ }
+};
+
+function migrateToastIfNeeded(aDuration, aOptions) {
+ let duration;
+ if (aDuration === "long") {
+ duration = LENGTH_LONG;
+ }
+ else {
+ duration = LENGTH_SHORT;
+ }
+
+ let options = {};
+ if (aOptions && aOptions.button) {
+ options.action = {
+ label: aOptions.button.label,
+ callback: () => aOptions.button.callback(),
+ };
+ }
+ return [duration, options];
+} \ No newline at end of file
diff --git a/mobile/android/modules/TabMirror.jsm b/mobile/android/modules/TabMirror.jsm
new file mode 100644
index 000000000..72a640ec8
--- /dev/null
+++ b/mobile/android/modules/TabMirror.jsm
@@ -0,0 +1,153 @@
+/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 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/. */
+"use strict";
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+const CONFIG = { iceServers: [{ "urls": ["stun:stun.services.mozilla.com"] }] };
+
+var log = Cu.import("resource://gre/modules/AndroidLog.jsm",
+ {}).AndroidLog.d.bind(null, "TabMirror");
+
+var failure = function(x) {
+ log("ERROR: " + JSON.stringify(x));
+};
+
+var TabMirror = function(deviceId, window) {
+
+ this.deviceId = deviceId;
+ // Save RTCSessionDescription and RTCIceCandidate for later when the window object is not available.
+ this.RTCSessionDescription = window.RTCSessionDescription;
+ this.RTCIceCandidate = window.RTCIceCandidate;
+
+ Services.obs.addObserver((aSubject, aTopic, aData) => this._processMessage(aData), "MediaPlayer:Response", false);
+ this._sendMessage({ start: true });
+ this._window = window;
+ this._pc = new window.RTCPeerConnection(CONFIG, {});
+ if (!this._pc) {
+ throw "Failure creating Webrtc object";
+ }
+
+};
+
+TabMirror.prototype = {
+ _window: null,
+ _screenSize: { width: 1280, height: 720 },
+ _pc: null,
+ _start: function() {
+ this._pc.onicecandidate = this._onIceCandidate.bind(this);
+
+ let windowId = this._window.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ let constraints = {
+ video: {
+ mediaSource: "browser",
+ browserWindow: windowId,
+ scrollWithPage: true,
+ advanced: [
+ { width: { min: 0, max: this._screenSize.width },
+ height: { min: 0, max: this._screenSize.height }
+ },
+ { aspectRatio: this._screenSize.width / this._screenSize.height }
+ ]
+ }
+ };
+
+ this._window.navigator.mozGetUserMedia(constraints, this._onGumSuccess.bind(this), this._onGumFailure.bind(this));
+ },
+
+ _processMessage: function(data) {
+ if (!data) {
+ return;
+ }
+
+ let msg = JSON.parse(data);
+
+ if (!msg) {
+ return;
+ }
+
+ if (msg.sdp && msg.type === "answer") {
+ this._processAnswer(msg);
+ } else if (msg.type == "size") {
+ if (msg.height) {
+ this._screenSize.height = msg.height;
+ }
+ if (msg.width) {
+ this._screenSize.width = msg.width;
+ }
+ this._start();
+ } else if (msg.candidate) {
+ this._processIceCandidate(msg);
+ } else {
+ log("dropping unrecognized message: " + JSON.stringify(msg));
+ }
+ },
+
+ // Signaling methods
+ _processAnswer: function(msg) {
+ this._pc.setRemoteDescription(new this.RTCSessionDescription(msg),
+ this._setRemoteAnswerSuccess.bind(this), failure);
+ },
+
+ _processIceCandidate: function(msg) {
+ // WebRTC generates a warning if the success and fail callbacks are not passed in.
+ this._pc.addIceCandidate(new this.RTCIceCandidate(msg), () => log("Ice Candiated added successfuly"), () => log("Failed to add Ice Candidate"));
+ },
+
+ _setRemoteAnswerSuccess: function() {
+ },
+
+ _setLocalSuccessOffer: function(sdp) {
+ this._sendMessage(sdp);
+ },
+
+ _createOfferSuccess: function(sdp) {
+ this._pc.setLocalDescription(sdp, () => this._setLocalSuccessOffer(sdp), failure);
+ },
+
+ _onIceCandidate: function (msg) {
+ log("NEW Ice Candidate: " + JSON.stringify(msg.candidate));
+ this._sendMessage(msg.candidate);
+ },
+
+ _ready: function() {
+ this._pc.createOffer(this._createOfferSuccess.bind(this), failure);
+ },
+
+ _onGumSuccess: function(stream){
+ this._pc.addStream(stream);
+ this._ready();
+ },
+
+ _onGumFailure: function() {
+ log("Could not get video stream");
+ this._pc.close();
+ },
+
+ _sendMessage: function(msg) {
+ if (this.deviceId) {
+ let obj = {
+ type: "MediaPlayer:Message",
+ id: this.deviceId,
+ data: JSON.stringify(msg)
+ };
+ Messaging.sendRequest(obj);
+ }
+ },
+
+ stop: function() {
+ if (this.deviceId) {
+ let obj = {
+ type: "MediaPlayer:End",
+ id: this.deviceId
+ };
+ Services.androidBridge.handleGeckoMessage(obj);
+ }
+ },
+};
+
+
+this.EXPORTED_SYMBOLS = ["TabMirror"];
diff --git a/mobile/android/modules/WebsiteMetadata.jsm b/mobile/android/modules/WebsiteMetadata.jsm
new file mode 100644
index 000000000..39af9ddeb
--- /dev/null
+++ b/mobile/android/modules/WebsiteMetadata.jsm
@@ -0,0 +1,475 @@
+/* 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;
+
+this.EXPORTED_SYMBOLS = ["WebsiteMetadata"];
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+
+var WebsiteMetadata = {
+ /**
+ * Asynchronously parse the document extract metadata. A 'Website:Metadata' event with the metadata
+ * will be sent.
+ */
+ parseAsynchronously: function(doc) {
+ Task.spawn(function() {
+ let metadata = getMetadata(doc, doc.location.href, {
+ image_url: metadataRules['image_url']
+ });
+
+ // No metadata was extracted, so don't bother sending it.
+ if (Object.keys(metadata).length === 0) {
+ return;
+ }
+
+ let msg = {
+ type: 'Website:Metadata',
+ location: doc.location.href,
+ metadata: metadata,
+ };
+
+ Messaging.sendRequest(msg);
+ });
+ }
+};
+
+// #################################################################################################
+// # Modified version of makeUrlAbsolute() to not import url parser library (and dependencies)
+// #################################################################################################
+
+function makeUrlAbsolute(context, relative) {
+ var a = context.doc.createElement('a');
+ a.href = relative;
+ return a.href;
+}
+
+// #################################################################################################
+// # page-metadata-parser
+// # https://github.com/mozilla/page-metadata-parser/
+// # 61c58cbd0f0bf2153df832a388a79c66b288b98c
+// #################################################################################################
+
+function buildRuleset(name, rules, processors) {
+ const reversedRules = Array.from(rules).reverse();
+ const builtRuleset = ruleset(...reversedRules.map(([query, handler], order) => rule(
+ dom(query),
+ node => [{
+ score: order,
+ flavor: name,
+ notes: handler(node),
+ }]
+ )));
+
+ return (doc, context) => {
+ const kb = builtRuleset.score(doc);
+ const maxNode = kb.max(name);
+
+ if (maxNode) {
+ let value = maxNode.flavors.get(name);
+
+ if (processors) {
+ processors.forEach(processor => {
+ value = processor(value, context);
+ });
+ }
+
+ if (value) {
+ if (value.trim) {
+ return value.trim();
+ }
+ return value;
+ }
+ }
+ };
+}
+
+const metadataRules = {
+ description: {
+ rules: [
+ ['meta[property="og:description"]', node => node.element.getAttribute('content')],
+ ['meta[name="description"]', node => node.element.getAttribute('content')],
+ ],
+ },
+
+ icon_url: {
+ rules: [
+ ['link[rel="apple-touch-icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="apple-touch-icon-precomposed"]', node => node.element.getAttribute('href')],
+ ['link[rel="icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="fluid-icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="shortcut icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="Shortcut Icon"]', node => node.element.getAttribute('href')],
+ ['link[rel="mask-icon"]', node => node.element.getAttribute('href')],
+ ],
+ processors: [
+ (icon_url, context) => makeUrlAbsolute(context, icon_url)
+ ]
+ },
+
+ image_url: {
+ rules: [
+ ['meta[property="og:image:secure_url"]', node => node.element.getAttribute('content')],
+ ['meta[property="og:image:url"]', node => node.element.getAttribute('content')],
+ ['meta[property="og:image"]', node => node.element.getAttribute('content')],
+ ['meta[property="twitter:image"]', node => node.element.getAttribute('content')],
+ ['meta[name="thumbnail"]', node => node.element.getAttribute('content')],
+ ],
+ processors: [
+ (image_url, context) => makeUrlAbsolute(context, image_url)
+ ],
+ },
+
+ keywords: {
+ rules: [
+ ['meta[name="keywords"]', node => node.element.getAttribute('content')],
+ ],
+ processors: [
+ (keywords) => keywords.split(',').map((keyword) => keyword.trim()),
+ ]
+ },
+
+ title: {
+ rules: [
+ ['meta[property="og:title"]', node => node.element.getAttribute('content')],
+ ['meta[property="twitter:title"]', node => node.element.getAttribute('content')],
+ ['meta[name="hdl"]', node => node.element.getAttribute('content')],
+ ['title', node => node.element.text],
+ ],
+ },
+
+ type: {
+ rules: [
+ ['meta[property="og:type"]', node => node.element.getAttribute('content')],
+ ],
+ },
+
+ url: {
+ rules: [
+ ['meta[property="og:url"]', node => node.element.getAttribute('content')],
+ ['link[rel="canonical"]', node => node.element.getAttribute('href')],
+ ],
+ },
+};
+
+function getMetadata(doc, url, rules) {
+ const metadata = {};
+ const context = {url,doc};
+ const ruleSet = rules || metadataRules;
+
+ Object.keys(ruleSet).map(metadataKey => {
+ const metadataRule = ruleSet[metadataKey];
+
+ if(Array.isArray(metadataRule.rules)) {
+ const builtRule = buildRuleset(metadataKey, metadataRule.rules, metadataRule.processors);
+ metadata[metadataKey] = builtRule(doc, context);
+ } else {
+ metadata[metadataKey] = getMetadata(doc, url, metadataRule);
+ }
+ });
+
+ return metadata;
+}
+
+// #################################################################################################
+// # Fathom dependencies resolved
+// #################################################################################################
+
+// const {forEach} = require('wu');
+function forEach(fn, obj) {
+ for (let x of obj) {
+ fn(x);
+ }
+}
+
+function best(iterable, by, isBetter) {
+ let bestSoFar, bestKeySoFar;
+ let isFirst = true;
+ forEach(
+ function (item) {
+ const key = by(item);
+ if (isBetter(key, bestKeySoFar) || isFirst) {
+ bestSoFar = item;
+ bestKeySoFar = key;
+ isFirst = false;
+ }
+ },
+ iterable);
+ if (isFirst) {
+ throw new Error('Tried to call best() on empty iterable');
+ }
+ return bestSoFar;
+}
+
+// const {max} = require('./utils');
+function max(iterable, by = identity) {
+ return best(iterable, by, (a, b) => a > b);
+}
+
+// #################################################################################################
+// # Fathom
+// # https://github.com/mozilla/fathom
+// # cac59e470816f17fc1efd4a34437b585e3e451cd
+// #################################################################################################
+
+// Get a key of a map, first setting it to a default value if it's missing.
+function getDefault(map, key, defaultMaker) {
+ if (map.has(key)) {
+ return map.get(key);
+ }
+ const defaultValue = defaultMaker();
+ map.set(key, defaultValue);
+ return defaultValue;
+}
+
+
+// Construct a filtration network of rules.
+function ruleset(...rules) {
+ const rulesByInputFlavor = new Map(); // [someInputFlavor: [rule, ...]]
+
+ // File each rule under its input flavor:
+ forEach(rule => getDefault(rulesByInputFlavor, rule.source.inputFlavor, () => []).push(rule),
+ rules);
+
+ return {
+ // Iterate over a DOM tree or subtree, building up a knowledgebase, a
+ // data structure holding scores and annotations for interesting
+ // elements. Return the knowledgebase.
+ //
+ // This is the "rank" portion of the rank-and-yank algorithm.
+ score: function (tree) {
+ const kb = knowledgebase();
+
+ // Introduce the whole DOM into the KB as flavor 'dom' to get
+ // things started:
+ const nonterminals = [[{tree}, 'dom']]; // [[node, flavor], [node, flavor], ...]
+
+ // While there are new facts, run the applicable rules over them to
+ // generate even newer facts. Repeat until everything's fully
+ // digested. Rules run in no particular guaranteed order.
+ while (nonterminals.length) {
+ const [inNode, inFlavor] = nonterminals.pop();
+ for (let rule of getDefault(rulesByInputFlavor, inFlavor, () => [])) {
+ const outFacts = resultsOf(rule, inNode, inFlavor, kb);
+ for (let fact of outFacts) {
+ const outNode = kb.nodeForElement(fact.element);
+
+ // No matter whether or not this flavor has been
+ // emitted before for this node, we multiply the score.
+ // We want to be able to add rules that refine the
+ // scoring of a node, without having to rewire the path
+ // of flavors that winds through the ruleset.
+ //
+ // 1 score per Node is plenty. That simplifies our
+ // data, our rankers, our flavor system (since we don't
+ // need to represent score axes), and our engine. If
+ // somebody wants more score axes, they can fake it
+ // themselves with notes, thus paying only for what
+ // they eat. (We can even provide functions that help
+ // with that.) Most rulesets will probably be concerned
+ // with scoring only 1 thing at a time anyway. So,
+ // rankers return a score multiplier + 0 or more new
+ // flavors with optional notes. Facts can never be
+ // deleted from the KB by rankers (or order would start
+ // to matter); after all, they're *facts*.
+ outNode.score *= fact.score;
+
+ // Add a new annotation to a node--but only if there
+ // wasn't already one of the given flavor already
+ // there; otherwise there's no point.
+ //
+ // You might argue that we might want to modify an
+ // existing note here, but that would be a bad
+ // idea. Notes of a given flavor should be
+ // considered immutable once laid down. Otherwise, the
+ // order of execution of same-flavored rules could
+ // matter, hurting pluggability. Emit a new flavor and
+ // a new note if you want to do that.
+ //
+ // Also, choosing not to add a new fact to nonterminals
+ // when we're not adding a new flavor saves the work of
+ // running the rules against it, which would be
+ // entirely redundant and perform no new work (unless
+ // the rankers were nondeterministic, but don't do
+ // that).
+ if (!outNode.flavors.has(fact.flavor)) {
+ outNode.flavors.set(fact.flavor, fact.notes);
+ kb.indexNodeByFlavor(outNode, fact.flavor); // TODO: better encapsulation rather than indexing explicitly
+ nonterminals.push([outNode, fact.flavor]);
+ }
+ }
+ }
+ }
+ return kb;
+ }
+ };
+}
+
+
+// Construct a container for storing and querying facts, where a fact has a
+// flavor (used to dispatch further rules upon), a corresponding DOM element, a
+// score, and some other arbitrary notes opaque to fathom.
+function knowledgebase() {
+ const nodesByFlavor = new Map(); // Map{'texty' -> [NodeA],
+ // 'spiffy' -> [NodeA, NodeB]}
+ // NodeA = {element: <someElement>,
+ //
+ // // Global nodewide score. Add
+ // // custom ones with notes if
+ // // you want.
+ // score: 8,
+ //
+ // // Flavors is a map of flavor names to notes:
+ // flavors: Map{'texty' -> {ownText: 'blah',
+ // someOtherNote: 'foo',
+ // someCustomScore: 10},
+ // // This is an empty note:
+ // 'fluffy' -> undefined}}
+ const nodesByElement = new Map();
+
+ return {
+ // Return the "node" (our own data structure that we control) that
+ // corresponds to a given DOM element, creating one if necessary.
+ nodeForElement: function (element) {
+ return getDefault(nodesByElement,
+ element,
+ () => ({element,
+ score: 1,
+ flavors: new Map()}));
+ },
+
+ // Return the highest-scored node of the given flavor, undefined if
+ // there is none.
+ max: function (flavor) {
+ const nodes = nodesByFlavor.get(flavor);
+ return nodes === undefined ? undefined : max(nodes, node => node.score);
+ },
+
+ // Let the KB know that a new flavor has been added to an element.
+ indexNodeByFlavor: function (node, flavor) {
+ getDefault(nodesByFlavor, flavor, () => []).push(node);
+ },
+
+ nodesOfFlavor: function (flavor) {
+ return getDefault(nodesByFlavor, flavor, () => []);
+ }
+ };
+}
+
+
+// Apply a rule (as returned by a call to rule()) to a fact, and return the
+// new facts that result.
+function resultsOf(rule, node, flavor, kb) {
+ // If more types of rule pop up someday, do fancier dispatching here.
+ return rule.source.flavor === 'flavor' ? resultsOfFlavorRule(rule, node, flavor) : resultsOfDomRule(rule, node, kb);
+}
+
+
+// Pull the DOM tree off the special property of the root "dom" fact, and query
+// against it.
+function *resultsOfDomRule(rule, specialDomNode, kb) {
+ // Use the special "tree" property of the special starting node:
+ const matches = specialDomNode.tree.querySelectorAll(rule.source.selector);
+
+ for (let i = 0; i < matches.length; i++) { // matches is a NodeList, which doesn't conform to iterator protocol
+ const element = matches[i];
+ const newFacts = explicitFacts(rule.ranker(kb.nodeForElement(element)));
+ for (let fact of newFacts) {
+ if (fact.element === undefined) {
+ fact.element = element;
+ }
+ if (fact.flavor === undefined) {
+ throw new Error('Rankers of dom() rules must return a flavor in each fact. Otherwise, there is no way for that fact to be used later.');
+ }
+ yield fact;
+ }
+ }
+}
+
+
+function *resultsOfFlavorRule(rule, node, flavor) {
+ const newFacts = explicitFacts(rule.ranker(node));
+
+ for (let fact of newFacts) {
+ // If the ranker didn't specify a different element, assume it's
+ // talking about the one we passed in:
+ if (fact.element === undefined) {
+ fact.element = node.element;
+ }
+ if (fact.flavor === undefined) {
+ fact.flavor = flavor;
+ }
+ yield fact;
+ }
+}
+
+
+// Take the possibly abbreviated output of a ranker function, and make it
+// explicitly an iterable with a defined score.
+//
+// Rankers can return undefined, which means "no facts", a single fact, or an
+// array of facts.
+function *explicitFacts(rankerResult) {
+ const array = (rankerResult === undefined) ? [] : (Array.isArray(rankerResult) ? rankerResult : [rankerResult]);
+ for (let fact of array) {
+ if (fact.score === undefined) {
+ fact.score = 1;
+ }
+ yield fact;
+ }
+}
+
+
+// TODO: For the moment, a lot of responsibility is on the rankers to return a
+// pretty big data structure of up to 4 properties. This is a bit verbose for
+// an arrow function (as I hope we can use most of the time) and the usual case
+// will probably be returning just a score multiplier. Make that case more
+// concise.
+
+// TODO: It is likely that rankers should receive the notes of their input type
+// as a 2nd arg, for brevity.
+
+
+// Return a condition that uses a DOM selector to find its matches from the
+// original DOM tree.
+//
+// For consistency, Nodes will still be delivered to the transformers, but
+// they'll have empty flavors and score = 1.
+//
+// Condition constructors like dom() and flavor() build stupid, introspectable
+// objects that the query engine can read. They don't actually do the query
+// themselves. That way, the query planner can be smarter than them, figuring
+// out which indices to use based on all of them. (We'll probably keep a heap
+// by each dimension's score and a hash by flavor name, for starters.) Someday,
+// fancy things like this may be possible: rule(and(tag('p'), klass('snork')),
+// ...)
+function dom(selector) {
+ return {
+ flavor: 'dom',
+ inputFlavor: 'dom',
+ selector
+ };
+}
+
+
+// Return a condition that discriminates on nodes of the knowledgebase by flavor.
+function flavor(inputFlavor) {
+ return {
+ flavor: 'flavor',
+ inputFlavor
+ };
+}
+
+
+function rule(source, ranker) {
+ return {
+ source,
+ ranker
+ };
+}
diff --git a/mobile/android/modules/dbg-browser-actors.js b/mobile/android/modules/dbg-browser-actors.js
new file mode 100644
index 000000000..f96a39151
--- /dev/null
+++ b/mobile/android/modules/dbg-browser-actors.js
@@ -0,0 +1,77 @@
+/* -*- 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/. */
+
+"use strict";
+/**
+ * Fennec-specific actors.
+ */
+
+const { RootActor } = require("devtools/server/actors/root");
+const { DebuggerServer } = require("devtools/server/main");
+const { BrowserTabList, BrowserAddonList, sendShutdownEvent } =
+ require("devtools/server/actors/webbrowser");
+
+/**
+ * Construct a root actor appropriate for use in a server running in a
+ * browser on Android. The returned root actor:
+ * - respects the factories registered with DebuggerServer.addGlobalActor,
+ * - uses a MobileTabList to supply tab actors,
+ * - sends all navigator:browser window documents a Debugger:Shutdown event
+ * when it exits.
+ *
+ * * @param aConnection DebuggerServerConnection
+ * The conection to the client.
+ */
+function createRootActor(aConnection)
+{
+ let parameters = {
+ tabList: new MobileTabList(aConnection),
+ addonList: new BrowserAddonList(aConnection),
+ globalActorFactories: DebuggerServer.globalActorFactories,
+ onShutdown: sendShutdownEvent
+ };
+ return new RootActor(aConnection, parameters);
+}
+
+/**
+ * A live list of BrowserTabActors representing the current browser tabs,
+ * to be provided to the root actor to answer 'listTabs' requests.
+ *
+ * This object also takes care of listening for TabClose events and
+ * onCloseWindow notifications, and exiting the BrowserTabActors concerned.
+ *
+ * (See the documentation for RootActor for the definition of the "live
+ * list" interface.)
+ *
+ * @param aConnection DebuggerServerConnection
+ * The connection in which this list's tab actors may participate.
+ *
+ * @see BrowserTabList for more a extensive description of how tab list objects
+ * work.
+ */
+function MobileTabList(aConnection)
+{
+ BrowserTabList.call(this, aConnection);
+}
+
+MobileTabList.prototype = Object.create(BrowserTabList.prototype);
+
+MobileTabList.prototype.constructor = MobileTabList;
+
+MobileTabList.prototype._getSelectedBrowser = function(aWindow) {
+ return aWindow.BrowserApp.selectedBrowser;
+};
+
+MobileTabList.prototype._getChildren = function(aWindow) {
+ return aWindow.BrowserApp.tabs.map(tab => tab.browser);
+};
+
+exports.register = function(handle) {
+ handle.setRootActor(createRootActor);
+};
+
+exports.unregister = function(handle) {
+ handle.setRootActor(null);
+};
diff --git a/mobile/android/modules/moz.build b/mobile/android/modules/moz.build
new file mode 100644
index 000000000..479ff1f3f
--- /dev/null
+++ b/mobile/android/modules/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; 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/.
+
+EXTRA_JS_MODULES += [
+ 'Accounts.jsm',
+ 'AndroidLog.jsm',
+ 'dbg-browser-actors.js',
+ 'DelayedInit.jsm',
+ 'DownloadNotifications.jsm',
+ 'FxAccountsWebChannel.jsm',
+ 'HelperApps.jsm',
+ 'Home.jsm',
+ 'HomeProvider.jsm',
+ 'JavaAddonManager.jsm',
+ 'JNI.jsm',
+ 'LightweightThemeConsumer.jsm',
+ 'MediaPlayerApp.jsm',
+ 'Messaging.jsm',
+ 'NetErrorHelper.jsm',
+ 'Notifications.jsm',
+ 'PageActions.jsm',
+ 'Prompt.jsm',
+ 'RuntimePermissions.jsm',
+ 'Sanitizer.jsm',
+ 'SharedPreferences.jsm',
+ 'Snackbars.jsm',
+ 'SSLExceptions.jsm',
+ 'TabMirror.jsm',
+ 'WebsiteMetadata.jsm'
+]