diff options
Diffstat (limited to 'mobile/android/modules')
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' +] |