Bug 1287643 - FxA Push registration and handling of device disconnection message. r=kitcambridge,nalexander

MozReview-Commit-ID: 8IH3kBivp26

--HG--
extra : rebase_source : b3e2d2587f111c9d2c8d0eab887027caf92fe859
This commit is contained in:
Edouard Oger 2016-07-20 10:47:04 -07:00
Родитель c5539ef310
Коммит 83666a61d8
20 изменённых файлов: 578 добавлений и 179 удалений

Просмотреть файл

@ -19,6 +19,7 @@ interface nsIPushSubscription : nsISupports
readonly attribute long long lastPush; readonly attribute long long lastPush;
readonly attribute long quota; readonly attribute long quota;
readonly attribute bool isSystemSubscription; readonly attribute bool isSystemSubscription;
readonly attribute jsval p256dhPrivateKey;
bool quotaApplies(); bool quotaApplies();
bool isExpired(); bool isExpired();

Просмотреть файл

@ -498,6 +498,11 @@ PushSubscription.prototype = {
return !!this._props.systemRecord; 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 * Indicates whether this subscription is subject to the background message
* quota. * quota.

Просмотреть файл

@ -276,6 +276,7 @@ PushRecord.prototype = {
lastPush: this.lastPush, lastPush: this.lastPush,
pushCount: this.pushCount, pushCount: this.pushCount,
p256dhKey: this.p256dhPublicKey, p256dhKey: this.p256dhPublicKey,
p256dhPrivateKey: this.p256dhPrivateKey,
authenticationSecret: this.authenticationSecret, authenticationSecret: this.authenticationSecret,
appServerKey: this.appServerKey, appServerKey: this.appServerKey,
quota: this.quotaApplies() ? this.quota : -1, quota: this.quotaApplies() ? this.quota : -1,

Просмотреть файл

@ -871,17 +871,16 @@ this.PushService = {
}, },
/** /**
* 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 {PushRecord} record The receiving registration.
* @param {String} messageID The message ID.
* @param {ArrayBuffer|Uint8Array} data The encrypted message data. * @param {ArrayBuffer|Uint8Array} data The encrypted message data.
* @param {Object} cryptoParams The message encryption settings. * @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) { if (!cryptoParams) {
return this._notifyApp(record, messageID, null); return Promise.resolve(null);
} }
return PushCrypto.decodeMsg( return PushCrypto.decodeMsg(
data, data,
@ -892,13 +891,29 @@ this.PushService = {
cryptoParams.rs, cryptoParams.rs,
record.authenticationSecret, record.authenticationSecret,
cryptoParams.padSize 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); * Decrypts an incoming message and notifies the associated service worker.
return Ci.nsIPushErrorReporter.ACK_DECRYPTION_ERROR; *
}); * @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) { _updateQuota: function(keyID) {

Просмотреть файл

@ -12,7 +12,11 @@ const Cr = Components.results;
const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm"); const {PushDB} = Cu.import("resource://gre/modules/PushDB.jsm");
const {PushRecord} = Cu.import("resource://gre/modules/PushRecord.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/Services.jsm"); /*global: Services */
Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */ Cu.import("resource://gre/modules/Preferences.jsm"); /*global: Preferences */
Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */ Cu.import("resource://gre/modules/Promise.jsm"); /*global: Promise */
@ -20,12 +24,6 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push"); 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"]; this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
XPCOMUtils.defineLazyGetter(this, "console", () => { XPCOMUtils.defineLazyGetter(this, "console", () => {
@ -41,6 +39,8 @@ const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM"; const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
const FXA_PUSH_SCOPE = "chrome://fxa-push";
const prefs = new Preferences("dom.push."); const prefs = new Preferences("dom.push.");
/** /**
@ -76,57 +76,67 @@ this.PushServiceAndroidGCM = {
}, },
observe: function(subject, topic, data) { observe: function(subject, topic, data) {
if (topic == "nsPref:changed") { switch (topic) {
if (data == "dom.push.debug") { case "nsPref:changed":
// Reconfigure. if (data == "dom.push.debug") {
let debug = !!prefs.get("debug"); // Reconfigure.
console.info("Debug parameter changed; updating configuration with new debug", debug); let debug = !!prefs.get("debug");
this._configure(this._serverURI, 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; return;
} }
if (!data) {
if (topic == "PushServiceAndroidGCM:ReceivedPushMessage") { console.error("No data from Java! Dropping message.");
// 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);
// 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",
});
}
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; return;
} }
data = JSON.parse(data);
console.debug("ReceivedPushMessage with data", data);
let { message, cryptoParams } = this._messageAndCryptoParams(data);
console.debug("Delivering message to main PushService:", message, cryptoParams);
this._mainPushService.receivedPushMessage(
data.channelID, "", message, cryptoParams, (record) => {
// Always update the stored record.
return record;
});
},
_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 };
}, },
_configure: function(serverURL, debug) { _configure: function(serverURL, debug) {
@ -209,11 +219,16 @@ this.PushServiceAndroidGCM = {
// The Push server requires padding. // The Push server requires padding.
pad: true, pad: true,
}) : null; }) : null;
// Caller handles errors. let message = {
return Messaging.sendRequestForResult({
type: "PushServiceAndroidGCM:SubscribeChannel", type: "PushServiceAndroidGCM:SubscribeChannel",
appServerKey: appServerKey, 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); console.debug("Got data:", data);
return PushCrypto.generateKeys() return PushCrypto.generateKeys()
.then(exportedKeys => .then(exportedKeys =>
@ -225,6 +240,7 @@ this.PushServiceAndroidGCM = {
scope: record.scope, scope: record.scope,
originAttributes: record.originAttributes, originAttributes: record.originAttributes,
ctime: ctime, ctime: ctime,
systemRecord: record.systemRecord,
// Cryptography! // Cryptography!
p256dhPublicKey: exportedKeys[0], p256dhPublicKey: exportedKeys[0],
p256dhPrivateKey: exportedKeys[1], p256dhPrivateKey: exportedKeys[1],

Просмотреть файл

@ -835,6 +835,7 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
'fxa/FxAccountConstants.java', 'fxa/FxAccountConstants.java',
'fxa/FxAccountDevice.java', 'fxa/FxAccountDevice.java',
'fxa/FxAccountDeviceRegistrator.java', 'fxa/FxAccountDeviceRegistrator.java',
'fxa/FxAccountPushHandler.java',
'fxa/login/BaseRequestDelegate.java', 'fxa/login/BaseRequestDelegate.java',
'fxa/login/Cohabiting.java', 'fxa/login/Cohabiting.java',
'fxa/login/Doghouse.java', 'fxa/login/Doghouse.java',

Просмотреть файл

@ -21,6 +21,7 @@ import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.annotation.ReflectionTarget; import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.fxa.FxAccountPushHandler;
import org.mozilla.gecko.gcm.GcmTokenClient; import org.mozilla.gecko.gcm.GcmTokenClient;
import org.mozilla.gecko.push.autopush.AutopushClientException; import org.mozilla.gecko.push.autopush.AutopushClientException;
import org.mozilla.gecko.util.BundleEventListener; import org.mozilla.gecko.util.BundleEventListener;
@ -50,6 +51,7 @@ public class PushService implements BundleEventListener {
private static final String LOG_TAG = "GeckoPushService"; private static final String LOG_TAG = "GeckoPushService";
public static final String SERVICE_WEBPUSH = "webpush"; public static final String SERVICE_WEBPUSH = "webpush";
public static final String SERVICE_FXA = "fxa";
private static PushService sInstance; private static PushService sInstance;
@ -63,6 +65,7 @@ public class PushService implements BundleEventListener {
"PushServiceAndroidGCM:UnregisterUserAgent", "PushServiceAndroidGCM:UnregisterUserAgent",
"PushServiceAndroidGCM:SubscribeChannel", "PushServiceAndroidGCM:SubscribeChannel",
"PushServiceAndroidGCM:UnsubscribeChannel", "PushServiceAndroidGCM:UnsubscribeChannel",
"FxAccountsPush:ReceivedPushMessageToDecode:Response",
"History:GetPrePathLastVisitedTimeMilliseconds", "History:GetPrePathLastVisitedTimeMilliseconds",
}; };
@ -150,63 +153,70 @@ public class PushService implements BundleEventListener {
return; 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); Log.i(LOG_TAG, "Message directed to service: " + subscription.service);
if (SERVICE_WEBPUSH.equals(subscription.service)) { if (subscription.serviceData == null) {
if (subscription.serviceData == null) { Log.e(LOG_TAG, "No serviceData found for chid: " + chid + "; ignoring dom/push message.");
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");
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;
}
if (canSendPushMessagesToGecko) {
if (!GeckoThread.canUseProfile(profileName, new File(profilePath))) {
Log.e(LOG_TAG, "Mismatched profile for chid: " + chid + "; ignoring dom/push message.");
return; return;
} }
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;
}
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);
}
// 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"));
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) {
sendMessageToGeckoService(data);
} else {
Log.i(LOG_TAG, "Service not initialized, adding message to queue.");
pendingPushMessages.add(data);
}
} else { } else {
Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service); final Intent intent = GeckoService.getIntentToCreateServices(context, "android-push-service");
GeckoService.setIntentProfile(intent, profileName, profilePath);
context.startService(intent);
}
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);
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 (isWebPush) {
sendMessageToGeckoService(data);
} else {
sendMessageToDecodeToGeckoService(data);
}
} else {
Log.i(LOG_TAG, "Service not initialized, adding message to queue.");
pendingPushMessages.add(data);
} }
} }
@ -217,6 +227,13 @@ public class PushService implements BundleEventListener {
GeckoThread.State.PROFILE_READY); 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() { protected void registerGeckoEventListener() {
Log.d(LOG_TAG, "Registered Gecko event listener."); Log.d(LOG_TAG, "Registered Gecko event listener.");
EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS); EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS);
@ -283,6 +300,7 @@ public class PushService implements BundleEventListener {
for (JSONObject pushMessage : pendingPushMessages) { for (JSONObject pushMessage : pendingPushMessages) {
final String profileName = pushMessage.optString("profileName", null); final String profileName = pushMessage.optString("profileName", null);
final String profilePath = pushMessage.optString("profilePath", null); final String profilePath = pushMessage.optString("profilePath", null);
final String service = pushMessage.optString("service", null);
if (profileName == null || profilePath == null || if (profileName == null || profilePath == null ||
!GeckoThread.canUseProfile(profileName, new File(profilePath))) { !GeckoThread.canUseProfile(profileName, new File(profilePath))) {
Log.e(LOG_TAG, "Mismatched profile for chid: " + Log.e(LOG_TAG, "Mismatched profile for chid: " +
@ -290,7 +308,11 @@ public class PushService implements BundleEventListener {
"; ignoring dom/push message."); "; ignoring dom/push message.");
continue; continue;
} }
sendMessageToGeckoService(pushMessage); if (SERVICE_WEBPUSH.equals(service)) {
sendMessageToGeckoService(pushMessage);
} else /* FxA Push */ {
sendMessageToDecodeToGeckoService(pushMessage);
}
} }
pendingPushMessages.clear(); pendingPushMessages.clear();
callback.sendSuccess(null); callback.sendSuccess(null);
@ -320,7 +342,9 @@ public class PushService implements BundleEventListener {
return; return;
} }
if ("PushServiceAndroidGCM:SubscribeChannel".equals(event)) { 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 JSONObject serviceData;
final String appServerKey = message.getString("appServerKey"); final String appServerKey = message.getString("appServerKey");
try { try {
@ -374,6 +398,10 @@ public class PushService implements BundleEventListener {
callback.sendError("Could not unsubscribe from channel: " + channelID); callback.sendError("Could not unsubscribe from channel: " + channelID);
return; return;
} }
if ("FxAccountsPush:ReceivedPushMessageToDecode:Response".equals(event)) {
FxAccountPushHandler.handleFxAPushMessage(context, message);
return;
}
if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) { if ("History:GetPrePathLastVisitedTimeMilliseconds".equals(event)) {
if (callback == null) { if (callback == null) {
Log.e(LOG_TAG, "callback must not be null in " + event); Log.e(LOG_TAG, "callback must not be null in " + event);

Просмотреть файл

@ -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);

Просмотреть файл

@ -96,6 +96,11 @@ contract @mozilla.org/dom/site-specific-user-agent;1 {d5234c9d-0ee2-4b3c-9da3-18
component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js component {18a4e042-7c7c-424b-a583-354e68553a7f} FilePicker.js
contract @mozilla.org/filepicker;1 {18a4e042-7c7c-424b-a583-354e68553a7f} 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 #ifndef RELEASE_BUILD
# TabSource.js # TabSource.js
component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js component {5850c76e-b916-4218-b99a-31f004e0a7e7} TabSource.js

Просмотреть файл

@ -21,6 +21,7 @@ EXTRA_COMPONENTS += [
'ContentPermissionPrompt.js', 'ContentPermissionPrompt.js',
'DirectoryProvider.js', 'DirectoryProvider.js',
'FilePicker.js', 'FilePicker.js',
'FxAccountsPush.js',
'HelperAppDialog.js', 'HelperAppDialog.js',
'ImageBlockingPolicy.js', 'ImageBlockingPolicy.js',
'LoginManagerPrompter.js', 'LoginManagerPrompter.js',

Просмотреть файл

@ -4,6 +4,7 @@
package org.mozilla.gecko; package org.mozilla.gecko;
import org.mozilla.gecko.annotation.ReflectionTarget;
import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.util.BundleEventListener; import org.mozilla.gecko.util.BundleEventListener;
@ -57,6 +58,7 @@ public final class EventDispatcher {
private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners = private final Map<String, List<BundleEventListener>> mBackgroundThreadListeners =
new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT); new HashMap<String, List<BundleEventListener>>(DEFAULT_BACKGROUND_EVENTS_COUNT);
@ReflectionTarget
public static EventDispatcher getInstance() { public static EventDispatcher getInstance() {
return INSTANCE; return INSTANCE;
} }
@ -159,6 +161,7 @@ public final class EventDispatcher {
mUiThreadListeners, listener, events); mUiThreadListeners, listener, events);
} }
@ReflectionTarget
public void registerBackgroundThreadListener(final BundleEventListener listener, public void registerBackgroundThreadListener(final BundleEventListener listener,
final String... events) { final String... events) {
checkNotRegisteredElsewhere(mBackgroundThreadListeners, events); checkNotRegisteredElsewhere(mBackgroundThreadListeners, events);

Просмотреть файл

@ -147,12 +147,12 @@ public class GeckoService extends Service {
final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME); final String profileName = intent.getStringExtra(INTENT_PROFILE_NAME);
final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR); final String profileDir = intent.getStringExtra(INTENT_PROFILE_DIR);
if (profileName == null || profileDir == null) { if (profileName == null) {
throw new IllegalArgumentException("Intent must specify profile."); throw new IllegalArgumentException("Intent must specify profile.");
} }
if (!GeckoThread.initWithProfile(profileName != null ? profileName : "", if (!GeckoThread.initWithProfile(profileName != null ? profileName : "",
new File(profileDir))) { profileDir != null ? new File(profileDir) : null)) {
Log.w(LOGTAG, "Ignoring due to profile mismatch: " + Log.w(LOGTAG, "Ignoring due to profile mismatch: " +
profileName + " [" + profileDir + ']'); profileName + " [" + profileDir + ']');

Просмотреть файл

@ -544,6 +544,7 @@
@BINPATH@/components/ImageBlockingPolicy.js @BINPATH@/components/ImageBlockingPolicy.js
@BINPATH@/components/DirectoryProvider.js @BINPATH@/components/DirectoryProvider.js
@BINPATH@/components/FilePicker.js @BINPATH@/components/FilePicker.js
@BINPATH@/components/FxAccountsPush.js
@BINPATH@/components/HelperAppDialog.js @BINPATH@/components/HelperAppDialog.js
@BINPATH@/components/LoginManagerPrompter.js @BINPATH@/components/LoginManagerPrompter.js
@BINPATH@/components/MobileComponents.manifest @BINPATH@/components/MobileComponents.manifest

Просмотреть файл

@ -49,6 +49,7 @@ public class FxAccountConstants {
public static final String ACCOUNT_DELETED_INTENT_VERSION_KEY = "account_deleted_intent_version"; 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_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_OAUTH_SERVICE_ENDPOINT_KEY = "account_oauth_service_endpoint";
public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens"; public static final String ACCOUNT_DELETED_INTENT_ACCOUNT_AUTH_TOKENS = "account_deleted_intent_auth_tokens";

Просмотреть файл

@ -5,6 +5,8 @@
package org.mozilla.gecko.fxa; package org.mozilla.gecko.fxa;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
@ -12,8 +14,8 @@ import android.util.Log;
import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient; import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20; import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse; import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError; import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
@ -21,8 +23,13 @@ import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel; import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.login.TokensAndKeysState; import org.mozilla.gecko.fxa.login.TokensAndKeysState;
import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import java.io.UnsupportedEncodingException; 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.security.GeneralSecurityException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -31,63 +38,100 @@ import java.util.concurrent.Executors;
* and also stores the registration details in the Android FxAccount. * 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. * This should be used in a state where we possess a sessionToken, most likely the Married state.
*/ */
public class FxAccountDeviceRegistrator { public class FxAccountDeviceRegistrator implements BundleEventListener {
private static final String LOG_TAG = "FxADeviceRegistrator";
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);
}
}
// The current version of the device registration, we use this to re-register // The current version of the device registration, we use this to re-register
// devices after we update what we send on device registration. // devices after we update what we send on device registration.
public static final Integer DEVICE_REGISTRATION_VERSION = 1; 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 {
* @throws InvalidFxAState thrown if we're not in a fxa state with a session token if (instance == null) {
*/ FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
public static void register(final AndroidFxAccount fxAccount, final Context context, tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
final RegisterDelegate delegate) throws InvalidFxAState { instance = tempInstance;
final byte[] sessionToken = getSessionToken(fxAccount); }
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);
}
}
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; final FxAccountDevice device;
String deviceId = fxAccount.getDeviceId(); String deviceId = fxAccount.getDeviceId();
String clientName = getClientName(fxAccount, context); String clientName = getClientName(fxAccount, context);
if (TextUtils.isEmpty(deviceId)) { if (TextUtils.isEmpty(deviceId)) {
Log.i(LOG_TAG, "Attempting registration for a new device"); Log.i(LOG_TAG, "Attempting registration for a new device");
device = FxAccountDevice.forRegister(clientName, "mobile"); device = FxAccountDevice.forRegister(clientName, "mobile", pushCallback, pushPublicKey, pushAuthKey);
} else { } else {
Log.i(LOG_TAG, "Attempting registration for an existing device"); Log.i(LOG_TAG, "Attempting registration for an existing device");
Logger.pii(LOG_TAG, "Device ID: " + deviceId); 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 ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
final FxAccountClient20 fxAccountClient = final FxAccountClient20 fxAccountClient =
new FxAccountClient20(fxAccount.getAccountServerURI(), executor); new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() { fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
@Override @Override
public void handleError(Exception e) { public void handleError(Exception e) {
Log.e(LOG_TAG, "Error while updating a device registration: ", e); Log.e(LOG_TAG, "Error while updating a device registration: ", e);
delegate.onComplete(null);
} }
@Override @Override
@ -96,19 +140,16 @@ public class FxAccountDeviceRegistrator {
if (error.httpStatusCode == 400) { if (error.httpStatusCode == 400) {
if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) { if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
recoverFromUnknownDevice(fxAccount); recoverFromUnknownDevice(fxAccount);
delegate.onComplete(null);
} else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) { } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
context, delegate); // Will call delegate.onComplete subscription, allowRecursion);
} }
} else } else
if (error.httpStatusCode == 401 if (error.httpStatusCode == 401
&& error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) { && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
handleTokenError(error, fxAccountClient, fxAccount); handleTokenError(error, fxAccountClient, fxAccount);
delegate.onComplete(null);
} else { } else {
logErrorAndResetDeviceRegistrationVersion(error, fxAccount); logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
delegate.onComplete(null);
} }
} }
@ -117,7 +158,6 @@ public class FxAccountDeviceRegistrator {
Log.i(LOG_TAG, "Device registration complete"); Log.i(LOG_TAG, "Device registration complete");
Logger.pii(LOG_TAG, "Registered device ID: " + result.id); Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION); fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
delegate.onComplete(result.id);
} }
}); });
} }
@ -195,13 +235,13 @@ public class FxAccountDeviceRegistrator {
final byte[] sessionToken, final byte[] sessionToken,
final AndroidFxAccount fxAccount, final AndroidFxAccount fxAccount,
final Context context, 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"); Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() { fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
private void onError() { private void onError() {
Log.e(LOG_TAG, "failed to recover from device-session conflict"); Log.e(LOG_TAG, "failed to recover from device-session conflict");
logErrorAndResetDeviceRegistrationVersion(error, fxAccount); logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
delegate.onComplete(null);
} }
@Override @Override
@ -219,13 +259,12 @@ public class FxAccountDeviceRegistrator {
for (FxAccountDevice device : devices) { for (FxAccountDevice device : devices) {
if (device.isCurrentDevice) { if (device.isCurrentDevice) {
fxAccount.setFxAUserData(device.id, 0); // Reset device registration version 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"); Log.d(LOG_TAG, "Failure to register a device on the second try");
break; break;
} }
delegate.allowRecursion = false; // Make sure we don't fall into an infinite loop
try { try {
register(fxAccount, context, delegate); // Will call delegate.onComplete() doFxaRegistration(context, subscription, false);
return; return;
} catch (InvalidFxAState e) { } catch (InvalidFxAState e) {
Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e); Log.d(LOG_TAG, "Invalid state when trying to recover from a session conflict ", e);
@ -237,4 +276,23 @@ public class FxAccountDeviceRegistrator {
} }
}); });
} }
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);
}
}
} }

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -146,6 +146,15 @@ public class AndroidFxAccount {
this.accountManager = AccountManager.get(this.context); 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 * 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. * {@link AccountPickler#pickle}, and is identical to calling it directly.
@ -648,6 +657,7 @@ public class AndroidFxAccount {
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY, intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION_KEY,
Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION)); Long.valueOf(FxAccountConstants.ACCOUNT_DELETED_INTENT_VERSION));
intent.putExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY, account.name); 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 // 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. // be extended in future to support OAuth for other services.

Просмотреть файл

@ -360,10 +360,6 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
// the pickle file directly without being afraid from a StrictMode violation. // the pickle file directly without being afraid from a StrictMode violation.
ThreadUtils.assertNotOnUiThread(); 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( final Intent serviceIntent = androidFxAccount.populateDeletedAccountIntent(
new Intent(context, FxAccountDeletedService.class) new Intent(context, FxAccountDeletedService.class)
); );
@ -371,6 +367,10 @@ public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
"starting FxAccountDeletedService with action: " + serviceIntent.getAction() + "."); "starting FxAccountDeletedService with action: " + serviceIntent.getAction() + ".");
context.startService(serviceIntent); 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; return result;
} }

Просмотреть файл

@ -4,21 +4,22 @@
package org.mozilla.gecko.fxa.receivers; 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.common.log.Logger;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient; import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException; import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10; import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
import org.mozilla.gecko.fxa.FxAccountConstants; 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.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter; import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase; import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository; import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
import android.app.IntentService; import java.util.concurrent.Executor;
import android.content.Context;
import android.content.Intent;
/** /**
* A background service to clean up after a Firefox Account is deleted. * A background service to clean up after a Firefox Account is deleted.
@ -64,6 +65,17 @@ public class FxAccountDeletedService extends IntentService {
} }
// 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. // Delete client database and non-local tabs.
Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs"); Logger.info(LOG_TAG, "Deleting the entire Fennec clients database and non-local tabs");
FennecTabsRepository.deleteNonLocalClientsAndTabs(context); FennecTabsRepository.deleteNonLocalClientsAndTabs(context);

Просмотреть файл

@ -541,7 +541,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
// Register the device if necessary (asynchronous, in another thread) // Register the device if necessary (asynchronous, in another thread)
if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
|| TextUtils.isEmpty(fxAccount.getDeviceId())) { || TextUtils.isEmpty(fxAccount.getDeviceId())) {
FxAccountDeviceRegistrator.register(fxAccount, context); FxAccountDeviceRegistrator.register(context);
} }
// Force fetch the profile avatar information. (asynchronous, in another thread) // Force fetch the profile avatar information. (asynchronous, in another thread)