1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8 const { XPCOMUtils } = ChromeUtils.import(
9 "resource://gre/modules/XPCOMUtils.jsm"
12 ChromeUtils.defineModuleGetter(
15 "resource://gre/modules/PushDB.jsm"
17 ChromeUtils.defineModuleGetter(
20 "resource://gre/modules/PushRecord.jsm"
22 ChromeUtils.defineModuleGetter(
25 "resource://gre/modules/PushCrypto.jsm"
27 ChromeUtils.defineModuleGetter(
30 "resource://gre/modules/Messaging.jsm"
32 ChromeUtils.defineModuleGetter(
35 "resource://gre/modules/Preferences.jsm"
38 XPCOMUtils.defineLazyGetter(this, "Log", () => {
39 return ChromeUtils.import(
40 "resource://gre/modules/AndroidLog.jsm",
42 ).AndroidLog.bind("Push");
45 const EXPORTED_SYMBOLS = ["PushServiceAndroidGCM"];
47 XPCOMUtils.defineLazyGetter(this, "console", () => {
48 let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
49 return new ConsoleAPI({
51 maxLogLevelPref: "dom.push.loglevel",
52 prefix: "PushServiceAndroidGCM",
56 const kPUSHANDROIDGCMDB_DB_NAME = "pushAndroidGCM";
57 const kPUSHANDROIDGCMDB_DB_VERSION = 5; // Change this if the IndexedDB format changes
58 const kPUSHANDROIDGCMDB_STORE_NAME = "pushAndroidGCM";
60 const FXA_PUSH_SCOPE = "chrome://fxa-push";
62 const prefs = new Preferences("dom.push.");
65 * The implementation of WebPush push backed by Android's GCM
68 var PushServiceAndroidGCM = {
69 _mainPushService: null,
74 kPUSHANDROIDGCMDB_DB_NAME,
75 kPUSHANDROIDGCMDB_DB_VERSION,
76 kPUSHANDROIDGCMDB_STORE_NAME,
82 observe(subject, topic, data) {
84 case "nsPref:changed":
85 if (data == "dom.push.debug") {
87 let debug = !!prefs.get("debug");
89 "Debug parameter changed; updating configuration with new debug",
92 this._configure(this._serverURI, debug);
95 case "PushServiceAndroidGCM:ReceivedPushMessage":
96 this._onPushMessageReceived(data);
103 _onPushMessageReceived(data) {
104 // TODO: Use Messaging.jsm for this.
105 if (this._mainPushService == null) {
106 // Shouldn't ever happen, but let's be careful.
107 console.error("No main PushService! Dropping message.");
111 console.error("No data from Java! Dropping message.");
114 data = JSON.parse(data);
115 console.debug("ReceivedPushMessage with data", data);
117 let { headers, message } = this._messageAndHeaders(data);
119 console.debug("Delivering message to main PushService:", message, headers);
120 this._mainPushService.receivedPushMessage(
126 // Always update the stored record.
132 _messageAndHeaders(data) {
133 // Default is no data (and no encryption).
138 if (data.enc && (data.enckey || data.cryptokey)) {
140 encryption_key: data.enckey,
141 crypto_key: data.cryptokey,
142 encryption: data.enc,
145 } else if (data.con == "aes128gcm") {
150 // Ciphertext is (urlsafe) Base 64 encoded.
151 message = ChromeUtils.base64URLDecode(data.message, {
152 // The Push server may append padding.
157 return { headers, message };
160 _configure(serverURL, debug) {
161 return EventDispatcher.instance.sendRequestForResult({
162 type: "PushServiceAndroidGCM:Configure",
163 endpoint: serverURL.spec,
168 init(options, mainPushService, serverURL) {
169 console.debug("init()");
170 this._mainPushService = mainPushService;
171 this._serverURI = serverURL;
173 prefs.observe("debug", this);
174 Services.obs.addObserver(this, "PushServiceAndroidGCM:ReceivedPushMessage");
176 return this._configure(serverURL, !!prefs.get("debug")).then(() => {
177 EventDispatcher.instance.sendRequestForResult({
178 type: "PushServiceAndroidGCM:Initialized",
184 console.debug("uninit()");
185 EventDispatcher.instance.sendRequestForResult({
186 type: "PushServiceAndroidGCM:Uninitialized",
189 this._mainPushService = null;
190 Services.obs.removeObserver(
192 "PushServiceAndroidGCM:ReceivedPushMessage"
194 prefs.ignore("debug", this);
198 // No action required.
201 connect(records, broadcastListeners) {
202 console.debug("connect:", records);
203 // It's possible for the registration or subscriptions backing the
204 // PushService to not be registered with the underlying AndroidPushService.
205 // Expire those that are unrecognized.
206 return EventDispatcher.instance
207 .sendRequestForResult({
208 type: "PushServiceAndroidGCM:DumpSubscriptions",
210 .then(subscriptions => {
211 subscriptions = JSON.parse(subscriptions);
212 console.debug("connect:", subscriptions);
213 // subscriptions maps chid => subscription data.
215 records.map(record => {
216 if (subscriptions.hasOwnProperty(record.keyID)) {
217 console.debug("connect:", "hasOwnProperty", record.keyID);
218 return Promise.resolve();
220 console.debug("connect:", "!hasOwnProperty", record.keyID);
221 // Subscription is known to PushService.jsm but not to AndroidPushService. Drop it.
222 return this._mainPushService
223 .dropRegistrationAndNotifyApp(record.keyID)
226 "connect: Error dropping registration",
236 async sendSubscribeBroadcast(serviceId, version) {
237 // Not implemented yet
241 return this._mainPushService != null;
245 console.debug("disconnect");
249 console.debug("register:", record);
250 let ctime = Date.now();
251 let appServerKey = record.appServerKey
252 ? ChromeUtils.base64URLEncode(record.appServerKey, {
253 // The Push server requires padding.
258 type: "PushServiceAndroidGCM:SubscribeChannel",
261 if (record.scope == FXA_PUSH_SCOPE) {
262 message.service = "fxa";
264 // Caller handles errors.
265 return EventDispatcher.instance.sendRequestForResult(message).then(data => {
266 data = JSON.parse(data);
267 console.debug("Got data:", data);
268 return PushCrypto.generateKeys().then(
270 new PushRecordAndroidGCM({
271 // Straight from autopush.
272 channelID: data.channelID,
273 pushEndpoint: data.endpoint,
274 // Common to all PushRecord implementations.
276 originAttributes: record.originAttributes,
278 systemRecord: record.systemRecord,
280 p256dhPublicKey: exportedKeys[0],
281 p256dhPrivateKey: exportedKeys[1],
282 authenticationSecret: PushCrypto.generateAuthenticationSecret(),
283 appServerKey: record.appServerKey,
290 console.debug("unregister: ", record);
291 return EventDispatcher.instance.sendRequestForResult({
292 type: "PushServiceAndroidGCM:UnsubscribeChannel",
293 channelID: record.keyID,
297 reportDeliveryError(messageID, reason) {
299 "reportDeliveryError: Ignoring message delivery error",
306 function PushRecordAndroidGCM(record) {
307 PushRecord.call(this, record);
308 this.channelID = record.channelID;
311 PushRecordAndroidGCM.prototype = Object.create(PushRecord.prototype, {
314 return this.channelID;