mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-10 02:08:38 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
178
mobile/android/modules/Accounts.jsm
Normal file
178
mobile/android/modules/Accounts.jsm
Normal file
|
|
@ -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"
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
92
mobile/android/modules/AndroidLog.jsm
Normal file
92
mobile/android/modules/AndroidLog.jsm
Normal file
|
|
@ -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;
|
||||
}
|
||||
177
mobile/android/modules/DelayedInit.jsm
Normal file
177
mobile/android/modules/DelayedInit.jsm
Normal file
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
291
mobile/android/modules/DownloadNotifications.jsm
Normal file
291
mobile/android/modules/DownloadNotifications.jsm
Normal file
|
|
@ -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;
|
||||
}
|
||||
394
mobile/android/modules/FxAccountsWebChannel.jsm
Normal file
394
mobile/android/modules/FxAccountsWebChannel.jsm
Normal file
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
229
mobile/android/modules/HelperApps.jsm
Normal file
229
mobile/android/modules/HelperApps.jsm
Normal file
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
487
mobile/android/modules/Home.jsm
Normal file
487
mobile/android/modules/Home.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
407
mobile/android/modules/HomeProvider.jsm
Normal file
407
mobile/android/modules/HomeProvider.jsm
Normal file
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
1167
mobile/android/modules/JNI.jsm
Normal file
1167
mobile/android/modules/JNI.jsm
Normal file
File diff suppressed because it is too large
Load diff
115
mobile/android/modules/JavaAddonManager.jsm
Normal file
115
mobile/android/modules/JavaAddonManager.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
44
mobile/android/modules/LightweightThemeConsumer.jsm
Normal file
44
mobile/android/modules/LightweightThemeConsumer.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
166
mobile/android/modules/MediaPlayerApp.jsm
Normal file
166
mobile/android/modules/MediaPlayerApp.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
183
mobile/android/modules/Messaging.jsm
Normal file
183
mobile/android/modules/Messaging.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
175
mobile/android/modules/NetErrorHelper.jsm
Normal file
175
mobile/android/modules/NetErrorHelper.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
259
mobile/android/modules/Notifications.jsm
Normal file
259
mobile/android/modules/Notifications.jsm
Normal file
|
|
@ -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);
|
||||
113
mobile/android/modules/PageActions.jsm
Normal file
113
mobile/android/modules/PageActions.jsm
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
234
mobile/android/modules/Prompt.jsm
Normal file
234
mobile/android/modules/Prompt.jsm
Normal file
|
|
@ -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);
|
||||
},
|
||||
|
||||
}
|
||||
41
mobile/android/modules/RuntimePermissions.jsm
Normal file
41
mobile/android/modules/RuntimePermissions.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
118
mobile/android/modules/SSLExceptions.jsm
Normal file
118
mobile/android/modules/SSLExceptions.jsm
Normal file
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
303
mobile/android/modules/Sanitizer.jsm
Normal file
303
mobile/android/modules/Sanitizer.jsm
Normal file
|
|
@ -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();
|
||||
254
mobile/android/modules/SharedPreferences.jsm
Normal file
254
mobile/android/modules/SharedPreferences.jsm
Normal file
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
72
mobile/android/modules/Snackbars.jsm
Normal file
72
mobile/android/modules/Snackbars.jsm
Normal file
|
|
@ -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];
|
||||
}
|
||||
153
mobile/android/modules/TabMirror.jsm
Normal file
153
mobile/android/modules/TabMirror.jsm
Normal file
|
|
@ -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"];
|
||||
475
mobile/android/modules/WebsiteMetadata.jsm
Normal file
475
mobile/android/modules/WebsiteMetadata.jsm
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
77
mobile/android/modules/dbg-browser-actors.js
Normal file
77
mobile/android/modules/dbg-browser-actors.js
Normal file
|
|
@ -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);
|
||||
};
|
||||
33
mobile/android/modules/moz.build
Normal file
33
mobile/android/modules/moz.build
Normal file
|
|
@ -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'
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue