зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1287643 - FxA Push registration and handling of device disconnection message. r=nalexander
MozReview-Commit-ID: BYFMeQNgumu= --HG-- extra : rebase_source : c17f04277e1c6840e601954c1207a2a00586864d
This commit is contained in:
Родитель
d21f10814a
Коммит
f416df2a70
|
@ -88,6 +88,11 @@ PushServiceBase.prototype = {
|
||||||
if (topic === "android-push-service") {
|
if (topic === "android-push-service") {
|
||||||
// Load PushService immediately.
|
// Load PushService immediately.
|
||||||
this._handleReady();
|
this._handleReady();
|
||||||
|
if (data === "android-fxa-subscribe") {
|
||||||
|
Services.obs.notifyObservers(null, "PushServiceAndroidGCM:FxASubscribe", null);
|
||||||
|
} else if (data === "android-fxa-unsubscribe") {
|
||||||
|
Services.obs.notifyObservers(null, "PushServiceAndroidGCM:FxAUnsubscribe", null);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,7 +891,8 @@ this.PushService = {
|
||||||
cryptoParams.rs,
|
cryptoParams.rs,
|
||||||
record.authenticationSecret,
|
record.authenticationSecret,
|
||||||
cryptoParams.padSize
|
cryptoParams.padSize
|
||||||
).then(message => this._notifyApp(record, messageID, message), error => {
|
)
|
||||||
|
.catch(error => {
|
||||||
let message = gDOMBundle.formatStringFromName(
|
let message = gDOMBundle.formatStringFromName(
|
||||||
"PushMessageDecryptionFailure", [record.scope, String(error)], 2);
|
"PushMessageDecryptionFailure", [record.scope, String(error)], 2);
|
||||||
gPushNotifier.notifyError(record.scope, record.principal, message,
|
gPushNotifier.notifyError(record.scope, record.principal, message,
|
||||||
|
@ -901,6 +901,20 @@ this.PushService = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
|
this.decryptMessage(data, record, cryptoParams)
|
||||||
|
.then(message => this._notifyApp(record, messageID, message));
|
||||||
|
},
|
||||||
|
|
||||||
_updateQuota: function(keyID) {
|
_updateQuota: function(keyID) {
|
||||||
console.debug("updateQuota()");
|
console.debug("updateQuota()");
|
||||||
|
|
||||||
|
|
|
@ -12,19 +12,29 @@ 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 */
|
|
||||||
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 {
|
const {
|
||||||
PushCrypto,
|
PushCrypto,
|
||||||
concatArray,
|
concatArray,
|
||||||
getCryptoParams,
|
getCryptoParams,
|
||||||
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
} = Cu.import("resource://gre/modules/PushCrypto.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Messaging.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/Promise.jsm"); /*global: Promise */
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); /*global: XPCOMUtils */
|
||||||
|
XPCOMUtils.defineLazyServiceGetter(this, "PushService",
|
||||||
|
"@mozilla.org/push/Service;1", "nsIPushService");
|
||||||
|
|
||||||
|
const Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("Push");
|
||||||
|
const FXA_PUSH_SCOPE = "chrome://fxa-push";
|
||||||
|
const LOG_TAG = "PushServiceAndroidGCM";
|
||||||
|
|
||||||
|
const topics = [
|
||||||
|
"PushServiceAndroidGCM:ReceivedPushMessage",
|
||||||
|
"PushServiceAndroidGCM:ReceivedPushMessageToDecode",
|
||||||
|
"PushServiceAndroidGCM:FxASubscribe",
|
||||||
|
"PushServiceAndroidGCM:FxAUnsubscribe",
|
||||||
|
]
|
||||||
|
|
||||||
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
|
this.EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
|
||||||
|
|
||||||
|
@ -43,6 +53,10 @@ const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
|
||||||
|
|
||||||
const prefs = new Preferences("dom.push.");
|
const prefs = new Preferences("dom.push.");
|
||||||
|
|
||||||
|
function urlsafeBase64Encode(key) {
|
||||||
|
return ChromeUtils.base64URLEncode(new Uint8Array(key), { pad: false });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The implementation of WebPush push backed by Android's GCM
|
* The implementation of WebPush push backed by Android's GCM
|
||||||
* delivery.
|
* delivery.
|
||||||
|
@ -50,6 +64,7 @@ const prefs = new Preferences("dom.push.");
|
||||||
this.PushServiceAndroidGCM = {
|
this.PushServiceAndroidGCM = {
|
||||||
_mainPushService: null,
|
_mainPushService: null,
|
||||||
_serverURI: null,
|
_serverURI: null,
|
||||||
|
_decoder: new TextDecoder(),
|
||||||
|
|
||||||
newPushDB: function() {
|
newPushDB: function() {
|
||||||
return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
|
return new PushDB(kPUSHANDROIDGCMDB_DB_NAME,
|
||||||
|
@ -76,59 +91,101 @@ 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:FxASubscribe":
|
||||||
|
this._subscribeFxAPush();
|
||||||
|
break;
|
||||||
|
case "PushServiceAndroidGCM:FxAUnsubscribe":
|
||||||
|
this._unsubscribeFxAPush().catch(err => {
|
||||||
|
Log.e(LOG_TAG, "Error during unsubscribe", err);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "PushServiceAndroidGCM:ReceivedPushMessage":
|
||||||
|
this._onPushMessageReceived(data);
|
||||||
|
break;
|
||||||
|
case "PushServiceAndroidGCM:ReceivedPushMessageToDecode":
|
||||||
|
this._onPushMessageReceived(data, true);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_onPushMessageReceived(data, sendToChrome = false) {
|
||||||
|
// 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) {
|
||||||
|
console.error("No data from Java! Dropping message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = JSON.parse(data);
|
||||||
|
console.debug("ReceivedPushMessage with data", data);
|
||||||
|
|
||||||
if (topic == "PushServiceAndroidGCM:ReceivedPushMessage") {
|
let { message, cryptoParams } = this._messageAndCryptoParams(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);
|
|
||||||
|
|
||||||
// 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!sendToChrome) {
|
||||||
console.debug("Delivering message to main PushService:", message, cryptoParams);
|
console.debug("Delivering message to main PushService:", message, cryptoParams);
|
||||||
this._mainPushService.receivedPushMessage(
|
this._mainPushService.receivedPushMessage(
|
||||||
data.channelID, "", message, cryptoParams, (record) => {
|
data.channelID, "", message, cryptoParams, (record) => {
|
||||||
// Always update the stored record.
|
// Always update the stored record.
|
||||||
return record;
|
return record;
|
||||||
});
|
});
|
||||||
return;
|
} else {
|
||||||
|
console.debug("Delivering message back to chrome:", message, cryptoParams);
|
||||||
|
this._mainPushService.getByKeyID(data.channelID)
|
||||||
|
.then(record => {
|
||||||
|
if (!cryptoParams) {
|
||||||
|
return new Uint8Array();
|
||||||
|
}
|
||||||
|
return this._mainPushService.decryptMessage(message, record, cryptoParams);
|
||||||
|
})
|
||||||
|
.then(decryptedMessage => {
|
||||||
|
decryptedMessage = this._decoder.decode(decryptedMessage);
|
||||||
|
Messaging.sendRequestForResult({
|
||||||
|
type: "PushServiceAndroidGCM:ReceivedPushMessageToDecode:Response",
|
||||||
|
decryptedMessage
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Log.d(LOG_TAG, "Error while decoding incoming message : " + err);
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_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) {
|
||||||
return Messaging.sendRequestForResult({
|
return Messaging.sendRequestForResult({
|
||||||
type: "PushServiceAndroidGCM:Configure",
|
type: "PushServiceAndroidGCM:Configure",
|
||||||
|
@ -143,7 +200,9 @@ this.PushServiceAndroidGCM = {
|
||||||
this._serverURI = serverURL;
|
this._serverURI = serverURL;
|
||||||
|
|
||||||
prefs.observe("debug", this);
|
prefs.observe("debug", this);
|
||||||
Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage", false);
|
for (let topic of topics) {
|
||||||
|
Services.obs.addObserver(this, topic, false);
|
||||||
|
}
|
||||||
|
|
||||||
return this._configure(serverURL, !!prefs.get("debug")).then(() => {
|
return this._configure(serverURL, !!prefs.get("debug")).then(() => {
|
||||||
Messaging.sendRequestForResult({
|
Messaging.sendRequestForResult({
|
||||||
|
@ -159,7 +218,9 @@ this.PushServiceAndroidGCM = {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._mainPushService = null;
|
this._mainPushService = null;
|
||||||
Services.obs.removeObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
|
for (let topic of topics) {
|
||||||
|
Services.obs.removeObserver(this, topic);
|
||||||
|
}
|
||||||
prefs.ignore("debug", this);
|
prefs.ignore("debug", this);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -209,11 +270,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 +291,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],
|
||||||
|
@ -247,6 +314,56 @@ this.PushServiceAndroidGCM = {
|
||||||
console.warn("reportDeliveryError: Ignoring message delivery error",
|
console.warn("reportDeliveryError: Ignoring message delivery error",
|
||||||
messageID, reason);
|
messageID, reason);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_subscribeFxAPush() {
|
||||||
|
Log.i(LOG_TAG, "PushServiceAndroidGCM _subscribeFxAPush");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
PushService.subscribe(FXA_PUSH_SCOPE,
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
(result, subscription) => {
|
||||||
|
if (Components.isSuccessCode(result)) {
|
||||||
|
Log.d(LOG_TAG, "FxA Push got subscription");
|
||||||
|
resolve(subscription);
|
||||||
|
} else {
|
||||||
|
Log.w(LOG_TAG, "FxA Push failed to subscribe", result);
|
||||||
|
reject(new Error("FxA Push failed to subscribe"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(subscription => {
|
||||||
|
Messaging.sendRequest({
|
||||||
|
type: "PushServiceAndroidGCM:FxASubscribe:Response",
|
||||||
|
subscription: {
|
||||||
|
pushCallback: subscription.endpoint,
|
||||||
|
pushPublicKey: urlsafeBase64Encode(subscription.getKey('p256dh')),
|
||||||
|
pushAuthKey: urlsafeBase64Encode(subscription.getKey('auth'))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
Log.i(LOG_TAG, "Error when registering FxA push endpoint " + err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_unsubscribeFxAPush() {
|
||||||
|
Log.i(LOG_TAG, "PushServiceAndroidGCM _subscribeFxAPush");
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
PushService.unsubscribe(FXA_PUSH_SCOPE,
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
(result, ok) => {
|
||||||
|
if (Components.isSuccessCode(result)) {
|
||||||
|
if (ok === true) {
|
||||||
|
Log.d(LOG_TAG, "FxA Push unsubscribed");
|
||||||
|
} else {
|
||||||
|
Log.d(LOG_TAG, "FxA Push had no subscription to unsubscribe");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(LOG_TAG, "FxA Push failed to unsubscribe", result);
|
||||||
|
}
|
||||||
|
return resolve(ok);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function PushRecordAndroidGCM(record) {
|
function PushRecordAndroidGCM(record) {
|
||||||
|
|
|
@ -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",
|
||||||
|
"PushServiceAndroidGCM: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("PushServiceAndroidGCM: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 ("PushServiceAndroidGCM: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);
|
||||||
|
|
|
@ -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.GeckoEvent;
|
import org.mozilla.gecko.GeckoEvent;
|
||||||
|
@ -58,6 +59,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;
|
||||||
}
|
}
|
||||||
|
@ -160,6 +162,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 + ']');
|
||||||
|
|
||||||
|
|
|
@ -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 ("PushServiceAndroidGCM:FxASubscribe: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[] { "PushServiceAndroidGCM:FxASubscribe: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(bundle.getString("message"));
|
||||||
|
} 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)
|
||||||
|
|
Загрузка…
Ссылка в новой задаче