--- a/dom/interfaces/push/nsIPushService.idl
+++ b/dom/interfaces/push/nsIPushService.idl
@@ -14,16 +14,17 @@ interface nsIPrincipal;
[scriptable, uuid(1de32d5c-ea88-4c9e-9626-b032bd87f415)]
interface nsIPushSubscription : nsISupports
{
readonly attribute DOMString endpoint;
readonly attribute long long pushCount;
readonly attribute long long lastPush;
readonly attribute long quota;
readonly attribute bool isSystemSubscription;
+ readonly attribute jsval p256dhPrivateKey;
bool quotaApplies();
bool isExpired();
void getKey(in DOMString name,
[optional] out uint32_t keyLen,
[array, size_is(keyLen), retval] out uint8_t key);
};
--- a/dom/push/PushComponents.js
+++ b/dom/push/PushComponents.js
@@ -493,16 +493,21 @@ PushSubscription.prototype = {
* Indicates whether this subscription was created with the system principal.
* System subscriptions are exempt from the background message quota and
* permission checks.
*/
get isSystemSubscription() {
return !!this._props.systemRecord;
},
+ /** The private key used to decrypt incoming push messages, in JWK format */
+ get p256dhPrivateKey() {
+ return this._props.p256dhPrivateKey;
+ },
+
/**
* Indicates whether this subscription is subject to the background message
* quota.
*/
quotaApplies() {
return this.quota >= 0;
},
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -271,16 +271,17 @@ PushRecord.prototype = {
},
toSubscription() {
return {
endpoint: this.pushEndpoint,
lastPush: this.lastPush,
pushCount: this.pushCount,
p256dhKey: this.p256dhPublicKey,
+ p256dhPrivateKey: this.p256dhPrivateKey,
authenticationSecret: this.authenticationSecret,
appServerKey: this.appServerKey,
quota: this.quotaApplies() ? this.quota : -1,
systemRecord: this.systemRecord,
};
},
};
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -866,44 +866,59 @@ this.PushService = {
gPushNotifier.notifySubscriptionModified(record.scope,
record.principal);
}
return record;
});
},
/**
- * Decrypts an incoming message and notifies the associated service worker.
+ * Decrypts a message. Will resolve with null if cryptoParams is falsy.
*
* @param {PushRecord} record The receiving registration.
- * @param {String} messageID The message ID.
* @param {ArrayBuffer|Uint8Array} data The encrypted message data.
* @param {Object} cryptoParams The message encryption settings.
- * @returns {Promise} Resolves with an ack status code.
+ * @returns {Promise} Resolves with the decrypted message.
*/
- _decryptAndNotifyApp(record, messageID, data, cryptoParams) {
+ _decryptMessage(data, record, cryptoParams) {
if (!cryptoParams) {
- return this._notifyApp(record, messageID, null);
+ return Promise.resolve(null);
}
return PushCrypto.decodeMsg(
data,
record.p256dhPrivateKey,
record.p256dhPublicKey,
cryptoParams.dh,
cryptoParams.salt,
cryptoParams.rs,
record.authenticationSecret,
cryptoParams.padSize
- ).then(message => this._notifyApp(record, messageID, message), error => {
- let message = gDOMBundle.formatStringFromName(
- "PushMessageDecryptionFailure", [record.scope, String(error)], 2);
- gPushNotifier.notifyError(record.scope, record.principal, message,
- Ci.nsIScriptError.errorFlag);
- return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
- });
+ );
+ },
+
+ /**
+ * Decrypts an incoming message and notifies the associated service worker.
+ *
+ * @param {PushRecord} record The receiving registration.
+ * @param {String} messageID The message ID.
+ * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
+ * @param {Object} cryptoParams The message encryption settings.
+ * @returns {Promise} Resolves with an ack status code.
+ */
+ _decryptAndNotifyApp(record, messageID, data, cryptoParams) {
+ return this._decryptMessage(data, record, cryptoParams)
+ .then(
+ message => this._notifyApp(record, messageID, message),
+ error => {
+ let message = gDOMBundle.formatStringFromName(
+ "PushMessageDecryptionFailure", [record.scope, String(error)], 2);
+ gPushNotifier.notifyError(record.scope, record.principal, message,
+ Ci.nsIScriptError.errorFlag);
+ return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR;
+ });
},
_updateQuota: function(keyID) {
console.debug("updateQuota()");
this._db.update(keyID, record => {
// Record may have expired from an earlier quota update.
if (record.isExpired()) {
--- a/dom/push/PushServiceAndroidGCM.jsm
+++ b/dom/push/PushServiceAndroidGCM.jsm
@@ -7,45 +7,45 @@
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.jsm");
-Cu.import("resource://gre/modules/Messaging.jsm"); /*global: Services */
+const {
+ PushCrypto,
+ getCryptoParams,
+} = Cu.import("resource://gre/modules/PushCrypto.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm"); /*global: Messaging */
Cu.import("resource://gre/modules/Services.jsm"); /*global: Services */
Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
-const {
- PushCrypto,
- concatArray,
- getCryptoParams,
-} = Cu.import("resource://gre/modules/PushCrypto.jsm");
-
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
XPCOMUtils.defineLazyGetter(this, "console", () => {
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
return new ConsoleAPI({
dump: Log.i,
maxLogLevelPref: "dom.push.loglevel",
prefix: "PushServiceAndroidGCM",
});
});
const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+
const prefs = new Preferences("dom.push.");
/**
* The implementation of WebPush push backed by Android's GCM
* delivery.
*/
this.PushServiceAndroidGCM = {
_mainPushService: null,
@@ -71,67 +71,77 @@ this.PushServiceAndroidGCM = {
// Allow insecure server URLs for development and testing.
return !!prefs.get("testing.allowInsecureServerURL");
}
console.info("Unsupported Android GCM dom.push.serverURL scheme", serverURI.scheme);
return false;
},
observe: function(subject, topic, data) {
- if (topic == "nsPref:changed") {
- if (data == "dom.push.debug") {
- // Reconfigure.
- let debug = !!prefs.get("debug");
- console.info("Debug parameter changed; updating configuration with new debug", debug);
- this._configure(this._serverURI, debug);
- }
+ switch (topic) {
+ case "nsPref:changed":
+ if (data == "dom.push.debug") {
+ // Reconfigure.
+ let debug = !!prefs.get("debug");
+ console.info("Debug parameter changed; updating configuration with new debug", debug);
+ this._configure(this._serverURI, debug);
+ }
+ break;
+ case "PushServiceAndroidGCM:ReceivedPushMessage":
+ this._onPushMessageReceived(data);
+ break;
+ default:
+ break;
+ }
+ },
+
+ _onPushMessageReceived(data) {
+ // TODO: Use Messaging.jsm for this.
+ if (this._mainPushService == null) {
+ // Shouldn't ever happen, but let's be careful.
+ console.error("No main PushService! Dropping message.");
+ return;
+ }
+ if (!data) {
+ console.error("No data from Java! Dropping message.");
return;
}
+ data = JSON.parse(data);
+ console.debug("ReceivedPushMessage with data", data);
- if (topic == "PushServiceAndroidGCM:ReceivedPushMessage") {
- // TODO: Use Messaging.jsm for this.
- if (this._mainPushService == null) {
- // Shouldn't ever happen, but let's be careful.
- console.error("No main PushService! Dropping message.");
- return;
- }
- if (!data) {
- console.error("No data from Java! Dropping message.");
- return;
- }
- data = JSON.parse(data);
- console.debug("ReceivedPushMessage with data", data);
+ let { message, cryptoParams } = this._messageAndCryptoParams(data);
- // Default is no data (and no encryption).
- let message = null;
- let cryptoParams = null;
+ console.debug("Delivering message to main PushService:", message, cryptoParams);
+ this._mainPushService.receivedPushMessage(
+ data.channelID, "", message, cryptoParams, (record) => {
+ // Always update the stored record.
+ return record;
+ });
+ },
- if (data.message && data.enc && (data.enckey || data.cryptokey)) {
- let headers = {
- encryption_key: data.enckey,
- crypto_key: data.cryptokey,
- encryption: data.enc,
- encoding: data.con,
- };
- cryptoParams = getCryptoParams(headers);
- // Ciphertext is (urlsafe) Base 64 encoded.
- message = ChromeUtils.base64URLDecode(data.message, {
- // The Push server may append padding.
- padding: "ignore",
- });
- }
+ _messageAndCryptoParams(data) {
+ // Default is no data (and no encryption).
+ let message = null;
+ let cryptoParams = null;
- console.debug("Delivering message to main PushService:", message, cryptoParams);
- this._mainPushService.receivedPushMessage(
- data.channelID, "", message, cryptoParams, (record) => {
- // Always update the stored record.
- return record;
- });
- return;
+ if (data.message && data.enc && (data.enckey || data.cryptokey)) {
+ let headers = {
+ encryption_key: data.enckey,
+ crypto_key: data.cryptokey,
+ encryption: data.enc,
+ encoding: data.con,
+ };
+ cryptoParams = getCryptoParams(headers);
+ // Ciphertext is (urlsafe) Base 64 encoded.
+ message = ChromeUtils.base64URLDecode(data.message, {
+ // The Push server may append padding.
+ padding: "ignore",
+ });
}
+ return { message, cryptoParams };
},
_configure: function(serverURL, debug) {
return Messaging.sendRequestForResult({
type: "PushServiceAndroidGCM:Configure",
endpoint: serverURL.spec,
debug: debug,
});
@@ -204,32 +214,38 @@ this.PushServiceAndroidGCM = {
register: function(record) {
console.debug("register:", record);
let ctime = Date.now();
let appServerKey = record.appServerKey ?
ChromeUtils.base64URLEncode(record.appServerKey, {
// The Push server requires padding.
pad: true,
}) : null;
- // Caller handles errors.
- return Messaging.sendRequestForResult({
+ let message = {
type: "PushServiceAndroidGCM:SubscribeChannel",
appServerKey: appServerKey,
- }).then(data => {
+ }
+ if (record.scope == FXA_PUSH_SCOPE) {
+ message.service = "fxa";
+ }
+ // Caller handles errors.
+ return Messaging.sendRequestForResult(message)
+ .then(data => {
console.debug("Got data:", data);
return PushCrypto.generateKeys()
.then(exportedKeys =>
new PushRecordAndroidGCM({
// Straight from autopush.
channelID: data.channelID,
pushEndpoint: data.endpoint,
// Common to all PushRecord implementations.
scope: record.scope,
originAttributes: record.originAttributes,
ctime: ctime,
+ systemRecord: record.systemRecord,
// Cryptography!
p256dhPublicKey: exportedKeys[0],
p256dhPrivateKey: exportedKeys[1],
authenticationSecret: PushCrypto.generateAuthenticationSecret(),
appServerKey: record.appServerKey,
})
);
});
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -830,16 +830,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
'fxa/authenticator/FxAccountAuthenticatorService.java',
'fxa/authenticator/FxAccountLoginDelegate.java',
'fxa/authenticator/FxAccountLoginException.java',
'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
'fxa/FirefoxAccounts.java',
'fxa/FxAccountConstants.java',
'fxa/FxAccountDevice.java',
'fxa/FxAccountDeviceRegistrator.java',
+ 'fxa/FxAccountPushHandler.java',
'fxa/login/BaseRequestDelegate.java',
'fxa/login/Cohabiting.java',
'fxa/login/Doghouse.java',
'fxa/login/Engaged.java',
'fxa/login/FxAccountLoginStateMachine.java',
'fxa/login/FxAccountLoginTransition.java',
'fxa/login/Married.java',
'fxa/login/MigratedFromSync11.java',
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -16,16 +16,17 @@ import org.json.JSONObject;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoService;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.annotation.ReflectionTarget;
+import org.mozilla.gecko.fxa.FxAccountPushHandler;
import org.mozilla.gecko.gcm.GcmTokenClient;
import org.mozilla.gecko.push.autopush.AutopushClientException;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.File;
import java.io.IOException;
@@ -45,29 +46,31 @@ import java.util.Map;
* <p/>
* It's worth noting that we allow the DOM push API in restricted profiles.
*/
@ReflectionTarget
public class PushService implements BundleEventListener {
private static final String LOG_TAG = "GeckoPushService";
public static final String SERVICE_WEBPUSH = "webpush";
+ public static final String SERVICE_FXA = "fxa";
private static PushService sInstance;
private static final String[] GECKO_EVENTS = new String[] {
"PushServiceAndroidGCM:Configure",
"PushServiceAndroidGCM:DumpRegistration",
"PushServiceAndroidGCM:DumpSubscriptions",
"PushServiceAndroidGCM:Initialized",
"PushServiceAndroidGCM:Uninitialized",
"PushServiceAndroidGCM:RegisterUserAgent",
"PushServiceAndroidGCM:UnregisterUserAgent",
"PushServiceAndroidGCM:SubscribeChannel",
"PushServiceAndroidGCM:UnsubscribeChannel",
+ "FxAccountsPush:ReceivedPushMessageToDecode:Response",
"History:GetPrePathLastVisitedTimeMilliseconds",
};
public static synchronized PushService getInstance(Context context) {
if (sInstance == null) {
onCreate(context);
}
return sInstance;
@@ -145,83 +148,97 @@ public class PushService implements Bund
final PushSubscription subscription = registration.getSubscription(chid);
if (subscription == null) {
// This should never happen. There's not much to be done; in the future, perhaps we
// could try to drop the remote subscription?
Log.e(LOG_TAG, "No subscription found for chid: " + chid + "; ignoring message.");
return;
}
+ boolean isWebPush = SERVICE_WEBPUSH.equals(subscription.service);
+ boolean isFxAPush = SERVICE_FXA.equals(subscription.service);
+ if (!isWebPush && !isFxAPush) {
+ Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
+ return;
+ }
+
Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
- if (SERVICE_WEBPUSH.equals(subscription.service)) {
- if (subscription.serviceData == null) {
- Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
- return;
- }
+ if (subscription.serviceData == null) {
+ Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
+ return;
+ }
+
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.SERVICE, "dom-push-api");
- Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.SERVICE, "dom-push-api");
+ final String profileName = subscription.serviceData.optString("profileName", null);
+ final String profilePath = subscription.serviceData.optString("profilePath", null);
+ if (profileName == null || profilePath == null) {
+ Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message.");
+ return;
+ }
- final String profileName = subscription.serviceData.optString("profileName", null);
- final String profilePath = subscription.serviceData.optString("profilePath", null);
- if (profileName == null || profilePath == null) {
- Log.e(LOG_TAG, "Corrupt serviceData found for chid: " + chid + "; ignoring dom/push message.");
+ if (canSendPushMessagesToGecko) {
+ if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
+ Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
return;
}
-
- if (canSendPushMessagesToGecko) {
- if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
- Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
- return;
- }
- } else {
- final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
- GeckoService.setIntentProfile(intent, profileName, profilePath);
- context.startService(intent);
- }
+ } else {
+ final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
+ GeckoService.setIntentProfile(intent, profileName, profilePath);
+ context.startService(intent);
+ }
- // DELIVERANCE!
- final JSONObject data = new JSONObject();
- try {
- data.put("channelID", chid);
- data.put("con", bundle.getString("con"));
- data.put("enc", bundle.getString("enc"));
- // Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
- // Gecko handler will verify this.
- data.put("cryptokey", bundle.getString("cryptokey"));
- data.put("enckey", bundle.getString("enckey"));
- data.put("message", bundle.getString("body"));
+ final JSONObject data = new JSONObject();
+ try {
+ data.put("channelID", chid);
+ data.put("con", bundle.getString("con"));
+ data.put("enc", bundle.getString("enc"));
+ // Only one of cryptokey (newer) and enckey (deprecated) should be set, but the
+ // Gecko handler will verify this.
+ data.put("cryptokey", bundle.getString("cryptokey"));
+ data.put("enckey", bundle.getString("enckey"));
+ data.put("message", bundle.getString("body"));
- if (!canSendPushMessagesToGecko) {
- data.put("profileName", profileName);
- data.put("profilePath", profilePath);
- }
- } catch (JSONException e) {
- Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
- return;
+ if (!canSendPushMessagesToGecko) {
+ data.put("profileName", profileName);
+ data.put("profilePath", profilePath);
+ data.put("service", subscription.service);
}
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
+ return;
+ }
- if (canSendPushMessagesToGecko) {
+ if (canSendPushMessagesToGecko) {
+ if (isWebPush) {
sendMessageToGeckoService(data);
} else {
- Log.i(LOG_TAG, "Service not initialized, adding message to queue.");
- pendingPushMessages.add(data);
+ sendMessageToDecodeToGeckoService(data);
}
} else {
- Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
+ Log.i(LOG_TAG, "Service not initialized, adding message to queue.");
+ pendingPushMessages.add(data);
}
}
protected void sendMessageToGeckoService(final @NonNull JSONObject message) {
Log.i(LOG_TAG, "Delivering dom/push message to Gecko!");
GeckoAppShell.notifyObservers("PushServiceAndroidGCM:ReceivedPushMessage",
message.toString(),
GeckoThread.State.PROFILE_READY);
}
+ protected void sendMessageToDecodeToGeckoService(final @NonNull JSONObject message) {
+ Log.i(LOG_TAG, "Delivering dom/push message to decode to Gecko!");
+ GeckoAppShell.notifyObservers("FxAccountsPush:ReceivedPushMessageToDecode",
+ message.toString(),
+ GeckoThread.State.PROFILE_READY);
+ }
+
protected void registerGeckoEventListener() {
Log.d(LOG_TAG, "Registered Gecko event listener.");
EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS);
}
protected void unregisterGeckoEventListener() {
Log.d(LOG_TAG, "Unregistered Gecko event listener.");
EventDispatcher.getInstance().unregisterBackgroundThreadListener(this, GECKO_EVENTS);
@@ -278,24 +295,29 @@ public class PushService implements Bund
// Send all pending messages to Gecko and set the
// canSendPushMessageToGecko flag to true so that
// all new push messages are sent directly to Gecko
// instead of being queued.
canSendPushMessagesToGecko = true;
for (JSONObject pushMessage : pendingPushMessages) {
final String profileName = pushMessage.optString("profileName", null);
final String profilePath = pushMessage.optString("profilePath", null);
+ final String service = pushMessage.optString("service", null);
if (profileName == null || profilePath == null ||
!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
Log.e(LOG_TAG, "Mismatched profile for chid: " +
pushMessage.optString("channelID") +
"; ignoring dom/push message.");
continue;
}
- sendMessageToGeckoService(pushMessage);
+ if (SERVICE_WEBPUSH.equals(service)) {
+ sendMessageToGeckoService(pushMessage);
+ } else /* FxA Push */ {
+ sendMessageToDecodeToGeckoService(pushMessage);
+ }
}
pendingPushMessages.clear();
callback.sendSuccess(null);
return;
}
if ("PushServiceAndroidGCM:Uninitialized".equals(event)) {
canSendPushMessagesToGecko = false;
callback.sendSuccess(null);
@@ -315,17 +337,19 @@ public class PushService implements Bund
// In the future, this might be used to tell the Java Push Manager to unregister
// a User Agent entirely from JavaScript. Right now, however, everything is
// subscription based; there's no concept of unregistering all subscriptions
// simultaneously.
callback.sendError("Not yet implemented!");
return;
}
if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) {
- final String service = SERVICE_WEBPUSH;
+ final String service = SERVICE_FXA.equals(message.getString("service")) ?
+ SERVICE_FXA :
+ SERVICE_WEBPUSH;
final JSONObject serviceData;
final String appServerKey = message.getString("appServerKey");
try {
serviceData = new JSONObject();
serviceData.put("profileName", geckoProfile.getName());
serviceData.put("profilePath", geckoProfile.getDir().getAbsolutePath());
} catch (JSONException e) {
Log.e(LOG_TAG, "Got exception in " + event, e);
@@ -369,16 +393,20 @@ public class PushService implements Bund
Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "dom-push-api");
callback.sendSuccess(null);
return;
}
callback.sendError("Could not unsubscribe from channel: " + channelID);
return;
}
+ if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
+ FxAccountPushHandler.handleFxAPushMessage(context, message);
+ return;
+ }
if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
if (callback == null) {
Log.e(LOG_TAG, "callback must not be null in " + event);
return;
}
final String prePath = message.getString("prePath");
if (prePath == null) {
callback.sendError("prePath must not be null in " + event);
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/FxAccountsPush.js
@@ -0,0 +1,170 @@
+/* jshint moz: true, esnext: true */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+const {
+ PushCrypto,
+ getCryptoParams,
+} = Cu.import("resource://gre/modules/PushCrypto.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "PushService",
+ "@mozilla.org/push/Service;1", "nsIPushService");
+XPCOMUtils.defineLazyGetter(this, "_decoder", () => new TextDecoder());
+
+const FXA_PUSH_SCOPE = "chrome://fxa-push";
+const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("FxAccountsPush");
+
+function FxAccountsPush() {
+ Services.obs.addObserver(this, "FxAccountsPush:ReceivedPushMessageToDecode", false);
+}
+
+FxAccountsPush.prototype = {
+ observe: function (subject, topic, data) {
+ switch (topic) {
+ case "android-push-service":
+ if (data === "android-fxa-subscribe") {
+ this._subscribe();
+ } else if (data === "android-fxa-unsubscribe") {
+ this._unsubscribe();
+ }
+ break;
+ case "FxAccountsPush:ReceivedPushMessageToDecode":
+ this._decodePushMessage(data);
+ break;
+ }
+ },
+
+ _subscribe() {
+ Log.i("FxAccountsPush _subscribe");
+ return new Promise((resolve, reject) => {
+ PushService.subscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (Components.isSuccessCode(result)) {
+ Log.d("FxAccountsPush got subscription");
+ resolve(subscription);
+ } else {
+ Log.w("FxAccountsPush failed to subscribe", result);
+ reject(new Error("FxAccountsPush failed to subscribe"));
+ }
+ });
+ })
+ .then(subscription => {
+ Messaging.sendRequest({
+ type: "FxAccountsPush:Subscribe:Response",
+ subscription: {
+ pushCallback: subscription.endpoint,
+ pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
+ pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
+ }
+ });
+ })
+ .catch(err => {
+ Log.i("Error when registering FxA push endpoint " + err);
+ });
+ },
+
+ _unsubscribe() {
+ Log.i("FxAccountsPush _unsubscribe");
+ return new Promise((resolve) => {
+ PushService.unsubscribe(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, ok) => {
+ if (Components.isSuccessCode(result)) {
+ if (ok === true) {
+ Log.d("FxAccountsPush unsubscribed");
+ } else {
+ Log.d("FxAccountsPush had no subscription to unsubscribe");
+ }
+ } else {
+ Log.w("FxAccountsPush failed to unsubscribe", result);
+ }
+ return resolve(ok);
+ });
+ }).catch(err => {
+ Log.e("Error during unsubscribe", err);
+ });
+ },
+
+ _decodePushMessage(data) {
+ Log.i("FxAccountsPush _decodePushMessage");
+ data = JSON.parse(data);
+ let { message, cryptoParams } = this._messageAndCryptoParams(data);
+ return new Promise((resolve, reject) => {
+ PushService.getSubscription(FXA_PUSH_SCOPE,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ (result, subscription) => {
+ if (!subscription) {
+ return reject(new Error("No subscription found"));
+ }
+ return resolve(subscription);
+ });
+ }).then(subscription => {
+ if (!cryptoParams) {
+ return new Uint8Array();
+ }
+ return PushCrypto.decodeMsg(
+ message,
+ subscription.p256dhPrivateKey,
+ new Uint8Array(subscription.getKey("p256dh")),
+ cryptoParams.dh,
+ cryptoParams.salt,
+ cryptoParams.rs,
+ new Uint8Array(subscription.getKey("auth")),
+ cryptoParams.padSize
+ );
+ })
+ .then(decryptedMessage => {
+ decryptedMessage = _decoder.decode(decryptedMessage);
+ Messaging.sendRequestForResult({
+ type: "FxAccountsPush:ReceivedPushMessageToDecode:Response",
+ message: decryptedMessage
+ });
+ })
+ .catch(err => {
+ Log.d("Error while decoding incoming message : " + err);
+ });
+ },
+
+ // Copied from PushServiceAndroidGCM
+ _messageAndCryptoParams(data) {
+ // Default is no data (and no encryption).
+ let message = null;
+ let cryptoParams = null;
+
+ if (data.message && data.enc && (data.enckey || data.cryptokey)) {
+ let headers = {
+ encryption_key: data.enckey,
+ crypto_key: data.cryptokey,
+ encryption: data.enc,
+ encoding: data.con,
+ };
+ cryptoParams = getCryptoParams(headers);
+ // Ciphertext is (urlsafe) Base 64 encoded.
+ message = ChromeUtils.base64URLDecode(data.message, {
+ // The Push server may append padding.
+ padding: "ignore",
+ });
+ }
+ return { message, cryptoParams };
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ classID: Components.ID("{d1bbb0fd-1d47-4134-9c12-d7b1be20b721}")
+};
+
+function urlsafeBase64Encode(key) {
+ return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
+}
+
+var components = [ FxAccountsPush ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -91,16 +91,21 @@ contract @mozilla.org/nsClientAuthDialog
# SiteSpecificUserAgent.js
component {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6} SiteSpecificUserAgent.js
contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18be9e5cf7e6}
# FilePicker.js
component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f}
+# FxAccountsPush.js
+component {d1bbb0fd-1d47-4134-9c12-d7b1be20b721} FxAccountsPush.js
+contract @mozilla.org/fxa-push;1 {d1bbb0fd-1d47-4134-9c12-d7b1be20b721}
+category android-push-service FxAccountsPush @mozilla.org/fxa-push;1
+
#ifndef RELEASE_BUILD
# TabSource.js
component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js
contract @mozilla.org/tab-source-service;1 {5850c76e-b916-4218-b99a-31f004e0a7e7}
#endif
# Snippets.js
component {a78d7e59-b558-4321-a3d6-dffe2f1e76dd} Snippets.js
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -16,16 +16,17 @@ EXTRA_COMPONENTS += [
'AndroidActivitiesGlue.js',
'BlocklistPrompt.js',
'BrowserCLH.js',
'ColorPicker.js',
'ContentDispatchChooser.js',
'ContentPermissionPrompt.js',
'DirectoryProvider.js',
'FilePicker.js',
+ 'FxAccountsPush.js',
'HelperAppDialog.js',
'ImageBlockingPolicy.js',
'LoginManagerPrompter.js',
'NSSDialogService.js',
'PersistentNotificationHandler.js',
'PresentationDevicePrompt.js',
'PromptService.js',
'SessionStore.js',
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -1,14 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
+import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSContainer;
import org.mozilla.gecko.util.NativeJSObject;
@@ -52,16 +53,17 @@ public final class EventDispatcher {
new HashMap<String, List<NativeEventListener>>(DEFAULT_GECKO_NATIVE_EVENTS_COUNT);
private final Map<String, List<GeckoEventListener>> mGeckoThreadJSONListeners =
new HashMap<String, List<GeckoEventListener>>(DEFAULT_GECKO_JSON_EVENTS_COUNT);
private final Map<String, List<BundleEventListener>> mUiThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_UI_EVENTS_COUNT);
private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
+ @ReflectionTarget
public static EventDispatcher getInstance() {
return INSTANCE;
}
private EventDispatcher() {
}
private <T> void registerListener(final Class<?> listType,
@@ -154,16 +156,17 @@ public final class EventDispatcher {
public void registerUiThreadListener(final BundleEventListener listener,
final String... events) {
checkNotRegisteredElsewhere(mUiThreadListeners, events);
registerListener(ArrayList.class,
mUiThreadListeners, listener, events);
}
+ @ReflectionTarget
public void registerBackgroundThreadListener(final BundleEventListener listener,
final String... events) {
checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);
registerListener(ArrayList.class,
mBackgroundThreadListeners, listener, events);
}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoService.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoService.java
@@ -142,22 +142,22 @@ public class GeckoService extends Servic
private int handleIntent(final Intent intent, final int startId) {
if (DEBUG) {
Log.d(LOGTAG, "Handling " + intent.getAction());
}
final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
- if (profileName == null || profileDir == null) {
+ if (profileName == null) {
throw new IllegalArgumentException("Intent must specify profile.");
}
if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
- new File(profileDir))) {
+ profileDir != null ? new File(profileDir) : null)) {
Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
profileName + " [" + profileDir + ']');
final GeckoProfile profile = GeckoThread.getActiveProfile();
if (profile != null) {
Log.w(LOGTAG, "Current profile is " + profile.getName() +
" [" + profile.getDir().getAbsolutePath() + ']');
}
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -539,16 +539,17 @@
@BINPATH@/components/BlocklistPrompt.js
@BINPATH@/components/BrowserCLH.js
@BINPATH@/components/ColorPicker.js
@BINPATH@/components/ContentDispatchChooser.js
@BINPATH@/components/ContentPermissionPrompt.js
@BINPATH@/components/ImageBlockingPolicy.js
@BINPATH@/components/DirectoryProvider.js
@BINPATH@/components/FilePicker.js
+@BINPATH@/components/FxAccountsPush.js
@BINPATH@/components/HelperAppDialog.js
@BINPATH@/components/LoginManagerPrompter.js
@BINPATH@/components/MobileComponents.manifest
@BINPATH@/components/MobileComponents.xpt
@BINPATH@/components/NSSDialogService.js
@BINPATH@/components/PersistentNotificationHandler.js
@BINPATH@/components/PresentationDevicePrompt.js
@BINPATH@/components/PromptService.js
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountConstants.java
@@ -44,16 +44,17 @@ public class FxAccountConstants {
/**
* Version number of contents of SYNC_ACCOUNT_DELETED_ACTION intent.
*/
public static final long ACCOUNT_DELETED_INTENT_VERSION = 1;
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_KEY = "account_deleted_intent_account";
+ public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE = "account_deleted_intent_profile";
public static final String ACCOUNT_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";
/**
* This action is broadcast when an Android Firefox Account's internal state
* is changed.
* <p>
* It is protected by signing-level permission PER_ACCOUNT_TYPE_PERMISSION and
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -1,128 +1,168 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa;
import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.TokensAndKeysState;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
import java.io.UnsupportedEncodingException;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.security.GeneralSecurityException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/* This class provides a way to register the current device against FxA
* and also stores the registration details in the Android FxAccount.
* This should be used in a state where we possess a sessionToken, most likely the Married state.
*/
-public class FxAccountDeviceRegistrator {
-
- public abstract static class RegisterDelegate {
- private boolean allowRecursion = true;
- protected abstract void onComplete(String deviceId);
- }
-
- public static class InvalidFxAState extends Exception {
- private static final long serialVersionUID = -8537626959811195978L;
-
- public InvalidFxAState(String message) {
- super(message);
- }
- }
+public class FxAccountDeviceRegistrator implements BundleEventListener {
+ private static final String LOG_TAG = "FxADeviceRegistrator";
// The current version of the device registration, we use this to re-register
// devices after we update what we send on device registration.
public static final Integer DEVICE_REGISTRATION_VERSION = 1;
- private static final String LOG_TAG = "FxADeviceRegistrator";
+ private static FxAccountDeviceRegistrator instance;
+ private final WeakReference<Context> context;
- private FxAccountDeviceRegistrator() {}
+ private FxAccountDeviceRegistrator(Context appContext) {
+ this.context = new WeakReference<Context>(appContext);
+ }
- public static void register(final AndroidFxAccount fxAccount, final Context context) throws InvalidFxAState {
- register(fxAccount, context, new RegisterDelegate() {
- @Override
- public void onComplete(String deviceId) {}
- });
+ private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+ if (instance == null) {
+ FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
+ tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
+ instance = tempInstance;
+ }
+ return instance;
+ }
+
+ public static void register(Context context) {
+ Context appContext = context.getApplicationContext();
+ try {
+ getInstance(appContext).beginRegistration(appContext);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Could not start FxA device registration", e);
+ }
}
- /**
- * @throws InvalidFxAState thrown if we're not in a fxa state with a session token
- */
- public static void register(final AndroidFxAccount fxAccount, final Context context,
- final RegisterDelegate delegate) throws InvalidFxAState {
+ private void beginRegistration(Context context) {
+ // Fire up gecko and send event
+ // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
+ // because we can't import these modules (circular dependency between browser and services)
+ final Intent geckoIntent = new Intent();
+ geckoIntent.setAction("create-services");
+ geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
+ geckoIntent.putExtra("category", "android-push-service");
+ geckoIntent.putExtra("data", "android-fxa-subscribe");
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+ geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
+ context.startService(geckoIntent);
+ // -> handleMessage()
+ }
+
+ @Override
+ public void handleMessage(String event, Bundle message, EventCallback callback) {
+ if ("FxAccountsPush:Subscribe:Response".equals(event)) {
+ try {
+ doFxaRegistration(message.getBundle("subscription"));
+ } catch (InvalidFxAState e) {
+ Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
+ }
+ } else {
+ Log.e(LOG_TAG, "No action defined for " + event);
+ }
+ }
+
+ private void doFxaRegistration(Bundle subscription) throws InvalidFxAState {
+ final Context context = this.context.get();
+ if (this.context == null) {
+ throw new IllegalStateException("Application context has been gc'ed");
+ }
+ doFxaRegistration(context, subscription, true);
+ }
+
+ private static void doFxaRegistration(final Context context, final Bundle subscription, final boolean allowRecursion) throws InvalidFxAState {
+ String pushCallback = subscription.getString("pushCallback");
+ String pushPublicKey = subscription.getString("pushPublicKey");
+ String pushAuthKey = subscription.getString("pushAuthKey");
+
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
final byte[] sessionToken = getSessionToken(fxAccount);
-
final FxAccountDevice device;
String deviceId = fxAccount.getDeviceId();
String clientName = getClientName(fxAccount, context);
if (TextUtils.isEmpty(deviceId)) {
Log.i(LOG_TAG, "Attempting registration for a new device");
- device = FxAccountDevice.forRegister(clientName, "mobile");
+ device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
} else {
Log.i(LOG_TAG, "Attempting registration for an existing device");
Logger.pii(LOG_TAG, "Device ID: " + deviceId);
- device = FxAccountDevice.forUpdate(deviceId, clientName);
+ device = FxAccountDevice.forUpdate(deviceId, clientName, pushCallback, pushPublicKey, pushAuthKey);
}
ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
final FxAccountClient20 fxAccountClient =
- new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
+ new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
@Override
public void handleError(Exception e) {
Log.e(LOG_TAG, "Error while updating a device registration: ", e);
- delegate.onComplete(null);
}
@Override
public void handleFailure(FxAccountClientRemoteException error) {
Log.e(LOG_TAG, "Error while updating a device registration: ", error);
if (error.httpStatusCode == 400) {
if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
recoverFromUnknownDevice(fxAccount);
- delegate.onComplete(null);
} else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
- recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount,
- context, delegate); // Will call delegate.onComplete
+ recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
+ subscription, allowRecursion);
}
} else
if (error.httpStatusCode == 401
- && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
+ && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
handleTokenError(error, fxAccountClient, fxAccount);
- delegate.onComplete(null);
} else {
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
- delegate.onComplete(null);
}
}
@Override
public void handleSuccess(FxAccountDevice result) {
Log.i(LOG_TAG, "Device registration complete");
Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
- delegate.onComplete(result.id);
}
});
}
private static void logErrorAndResetDeviceRegistrationVersion(
final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
Log.e(LOG_TAG, "Device registration failed", error);
fxAccount.resetDeviceRegistrationVersion();
@@ -190,23 +230,23 @@ public class FxAccountDeviceRegistrator
/**
* Will call delegate#complete in all cases
*/
private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error,
final FxAccountClient fxAccountClient,
final byte[] sessionToken,
final AndroidFxAccount fxAccount,
final Context context,
- final RegisterDelegate delegate) {
+ final Bundle subscription,
+ final boolean allowRecursion) {
Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
private void onError() {
Log.e(LOG_TAG, "failed to recover from device-session conflict");
logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
- delegate.onComplete(null);
}
@Override
public void handleError(Exception e) {
onError();
}
@Override
@@ -214,27 +254,45 @@ public class FxAccountDeviceRegistrator
onError();
}
@Override
public void handleSuccess(FxAccountDevice[] devices) {
for (FxAccountDevice device : devices) {
if (device.isCurrentDevice) {
fxAccount.setFxAUserData(device.id, 0); // Reset device registration version
- if (!delegate.allowRecursion) {
+ if (!allowRecursion) {
Log.d(LOG_TAG, "Failure to register a device on the second try");
break;
}
- delegate.allowRecursion = false; // Make sure we don't fall into an infinite loop
try {
- register(fxAccount, context, delegate); // Will call delegate.onComplete()
+ doFxaRegistration(context, subscription, false);
return;
} catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
break;
}
}
}
onError();
}
});
}
+
+ private void setupListeners() throws ClassNotFoundException, NoSuchMethodException,
+ InvocationTargetException, IllegalAccessException {
+ // We have no choice but to use reflection here, sorry :(
+ Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher");
+ Method getInstance = eventDispatcher.getMethod("getInstance");
+ Object instance = getInstance.invoke(null);
+ Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener",
+ BundleEventListener.class, String[].class);
+ registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" });
+ }
+
+ public static class InvalidFxAState extends Exception {
+ private static final long serialVersionUID = -8537626959811195978L;
+
+ public InvalidFxAState(String message) {
+ super(message);
+ }
+ }
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountPushHandler.java
@@ -0,0 +1,71 @@
+package org.mozilla.gecko.fxa;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+public class FxAccountPushHandler {
+ private static final String LOG_TAG = "FxAccountPush";
+
+ private static final String COMMAND_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
+
+ // Forbid instantiation
+ private FxAccountPushHandler() {}
+
+ public static void handleFxAPushMessage(Context context, Bundle bundle) {
+ Log.i(LOG_TAG, "Handling FxA Push Message");
+ String rawMessage = bundle.getString("message");
+ JSONObject message = null;
+ if (!TextUtils.isEmpty(rawMessage)) {
+ try {
+ message = new JSONObject(rawMessage);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Could not parse JSON", e);
+ return;
+ }
+ }
+ if (message == null) {
+ // An empty body means we should check the verification state of the account (FxA sends this
+ // when the account email is verified for example).
+ // TODO: We're only registering the push endpoint when we are in the Married state, that's why we're skipping the message :(
+ Log.d(LOG_TAG, "Skipping empty message");
+ return;
+ }
+ try {
+ String command = message.getString("command");
+ JSONObject data = message.getJSONObject("data");
+ switch (command) {
+ case COMMAND_DEVICE_DISCONNECTED:
+ handleDeviceDisconnection(context, data);
+ break;
+ default:
+ Log.d(LOG_TAG, "No handler defined for FxA Push command " + command);
+ break;
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, "Error while handling FxA push notification", e);
+ }
+ }
+
+ private static void handleDeviceDisconnection(Context context, JSONObject data) throws JSONException {
+ final Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ Log.e(LOG_TAG, "The account does not exist anymore");
+ return;
+ }
+ final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+ if (!fxAccount.getDeviceId().equals(data.getString("id"))) {
+ Log.e(LOG_TAG, "The device ID to disconnect doesn't match with the local device ID.\n"
+ + "Local: " + fxAccount.getDeviceId() + ", ID to disconnect: " + data.getString("id"));
+ return;
+ }
+ AccountManager.get(context).removeAccount(account, null, null);
+ }
+}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -141,16 +141,25 @@ public class AndroidFxAccount {
* Android account to use for storage.
*/
public AndroidFxAccount(Context applicationContext, Account account) {
this.context = applicationContext;
this.account = account;
this.accountManager = AccountManager.get(this.context);
}
+ public static AndroidFxAccount fromContext(Context context) {
+ context = context.getApplicationContext();
+ Account account = FirefoxAccounts.getFirefoxAccount(context);
+ if (account == null) {
+ return null;
+ }
+ return new AndroidFxAccount(context, account);
+ }
+
/**
* Persist the Firefox account to disk as a JSON object. Note that this is a wrapper around
* {@link AccountPickler#pickle}, and is identical to calling it directly.
* <p>
* Note that pickling is different from bundling, which involves operations on a
* {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account.
* See {@link #persistBundle} and {@link #unbundle} for more.
*/
@@ -643,16 +652,17 @@ public class AndroidFxAccount {
* @return <code>Intent</code> with a deleted action and account/OAuth information extras
*/
public Intent populateDeletedAccountIntent(final Intent intent) {
final List<String> tokens = new ArrayList<>();
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name);
+ intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE, getProfile());
// Get the tokens from AccountManager. Note: currently, only reading list service supports OAuth. The following logic will
// be extended in future to support OAuth for other services.
for (String tokenKey : KNOWN_OAUTH_TOKEN_TYPES) {
final String authToken = accountManager.peekAuthToken(account, tokenKey);
if (authToken != null) {
tokens.add(authToken);
}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/FxAccountAuthenticator.java
@@ -355,27 +355,27 @@ public class FxAccountAuthenticator exte
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(context, account);
// Deleting the pickle file in a blocking manner will avoid race conditions that might happen when
// an account is unpickled while an FxAccount is being deleted.
// Also we have an assumption that this method is always called from a background thread, so we delete
// the pickle file directly without being afraid from a StrictMode violation.
ThreadUtils.assertNotOnUiThread();
- Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
- "deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
- deletePickle();
-
final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
new Intent(context, FxAccountDeletedService.class)
);
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
"starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
context.startService(serviceIntent);
+ Logger.info(LOG_TAG, "Firefox account named " + account.name + " being removed; " +
+ "deleting saved pickle file '" + FxAccountConstants.ACCOUNT_PICKLE_FILENAME + "'.");
+ deletePickle();
+
return result;
}
private void deletePickle() {
try {
AccountPickler.deletePickle(context, FxAccountConstants.ACCOUNT_PICKLE_FILENAME);
} catch (Exception e) {
// This should never happen, but we really don't want to die in a background thread.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -1,29 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.receivers;
-import java.util.concurrent.Executor;
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
-import android.app.IntentService;
-import android.content.Context;
-import android.content.Intent;
+import java.util.concurrent.Executor;
/**
* A background service to clean up after a Firefox Account is deleted.
* <p>
* Note that we specifically handle deleting the pickle file using a Service and a
* BroadcastReceiver, rather than a background thread, to allow channels sharing a Firefox account
* to delete their respective pickle files (since, if one remains, the account will be restored
* when that channel is used).
@@ -59,16 +60,27 @@ public class FxAccountDeletedService ext
FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
if (accountName == null) {
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
"deleted Account.");
return;
}
+ // Fire up gecko and unsubscribe push
+ final Intent geckoIntent = new Intent();
+ geckoIntent.setAction("create-services");
+ geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
+ geckoIntent.putExtra("category", "android-push-service");
+ geckoIntent.putExtra("data", "android-fxa-unsubscribe");
+ final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+ geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME",
+ intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE));
+ context.startService(geckoIntent);
+
// Delete client database and non-local tabs.
Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
FennecTabsRepository.deleteNonLocalClientsAndTabs(context);
// Clear Firefox Sync client tables.
try {
Logger.info(LOG_TAG, "Deleting the Firefox Sync clients database.");
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -536,17 +536,17 @@ public class FxAccountSyncAdapter extend
final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
final String clientState = married.getClientState();
syncWithAssertion(audience, assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs, syncKeyBundle, clientState, sessionCallback, extras, fxAccount);
// Register the device if necessary (asynchronous, in another thread)
if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
|| TextUtils.isEmpty(fxAccount.getDeviceId())) {
- FxAccountDeviceRegistrator.register(fxAccount, context);
+ FxAccountDeviceRegistrator.register(context);
}
// Force fetch the profile avatar information. (asynchronous, in another thread)
Logger.info(LOG_TAG, "Fetching profile avatar information.");
fxAccount.fetchProfileJSON();
} catch (Exception e) {
syncDelegate.handleError(e);
return;