Bug 1150683 - Implement nsIPushNotificationService. r=dougt

---
 b2g/installer/package-manifest.in     |   2 +-
 browser/installer/package-manifest.in |   2 +-
 dom/push/Push.manifest                |  11 +-
 dom/push/PushNotificationService.js   |  81 +++++++++
 dom/push/PushService.jsm              | 324 ++++++++++++++++++++--------------
 dom/push/PushServiceLauncher.js       |  50 ------
 dom/push/moz.build                    |   2 +-
 7 files changed, 285 insertions(+), 187 deletions(-)
 create mode 100644 dom/push/PushNotificationService.js
 delete mode 100644 dom/push/PushServiceLauncher.js
This commit is contained in:
Kit Cambridge 2015-04-21 20:10:50 +02:00
parent 6855cac604
commit 86e4e52d97
7 changed files with 285 additions and 187 deletions

View File

@ -630,7 +630,7 @@
@RESPATH@/components/AppsService.manifest
@RESPATH@/components/Push.js
@RESPATH@/components/Push.manifest
@RESPATH@/components/PushServiceLauncher.js
@RESPATH@/components/PushNotificationService.js
@RESPATH@/components/InterAppComm.manifest
@RESPATH@/components/InterAppCommService.js

View File

@ -557,7 +557,7 @@
@RESPATH@/components/AlarmsManager.manifest
@RESPATH@/components/Push.js
@RESPATH@/components/Push.manifest
@RESPATH@/components/PushServiceLauncher.js
@RESPATH@/components/PushNotificationService.js
@RESPATH@/components/SlowScriptDebug.manifest
@RESPATH@/components/SlowScriptDebug.js

View File

@ -5,7 +5,10 @@ contract @mozilla.org/push/PushManager;1 {cde1d019-fad8-4044-b141-65fb4fb7a245}
component {CA86B665-BEDA-4212-8D0F-5C9F65270B58} Push.js
contract @mozilla.org/push/PushSubscription;1 {CA86B665-BEDA-4212-8D0F-5C9F65270B58}
# Component to initialize PushService on startup.
component {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d} PushServiceLauncher.js
contract @mozilla.org/push/ServiceLauncher;1 {4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d}
category app-startup PushServiceLauncher @mozilla.org/push/ServiceLauncher;1
# XPCOM component; initializes the PushService on startup.
component {32028e38-903b-4a64-a180-5857eb4cb3dd} PushNotificationService.js
contract @mozilla.org/push/NotificationService;1 {32028e38-903b-4a64-a180-5857eb4cb3dd}
category app-startup PushNotificationService @mozilla.org/push/NotificationService;1
component {66a87970-6dc9-46e0-ac61-adb4a13791de} PushNotificationService.js
contract @mozilla.org/push/ObserverNotification;1 {66a87970-6dc9-46e0-ac61-adb4a13791de}

View File

@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
let isParent = Cc["@mozilla.org/xre/runtime;1"]
.getService(Ci.nsIXULRuntime)
.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
XPCOMUtils.defineLazyGetter(this, "PushService", function() {
// Lazily initialize the PushService on
// `sessionstore-windows-restored` or first use.
const {PushService} = Cu.import("resource://gre/modules/PushService.jsm", {});
if (isParent) {
PushService.init();
}
return PushService;
});
this.PushNotificationService = function PushNotificationService() {};
PushNotificationService.prototype = {
classID: Components.ID("{32028e38-903b-4a64-a180-5857eb4cb3dd}"),
contractID: "@mozilla.org/push/NotificationService;1",
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PushNotificationService),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference,
Ci.nsIPushNotificationService]),
register: function register(scope, pageURL) {
return PushService._register({scope, pageURL});
},
unregister: function unregister(scope) {
return PushService._unregister({scope});
},
registration: function registration(scope) {
return PushService._registration({scope});
},
observe: function observe(subject, topic, data) {
switch (topic) {
case "app-startup":
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
break;
case "sessionstore-windows-restored":
Services.obs.removeObserver(this, "sessionstore-windows-restored");
if (isParent) {
PushService.init();
}
break;
}
}
};
this.PushObserverNotification = function PushObserverNotification() {};
PushObserverNotification.prototype = {
classID: Components.ID("{66a87970-6dc9-46e0-ac61-adb4a13791de}"),
contractID: "@mozilla.org/push/ObserverNotification;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPushObserverNotification])
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
PushNotificationService,
PushObserverNotification
]);

View File

@ -294,6 +294,7 @@ this.PushService = {
*/
case "xpcom-shutdown":
this.uninit();
break;
case "network-active-changed": /* On B2G. */
case "network:offline-status-changed": /* On desktop. */
// In case of network-active-changed, always disconnect existing
@ -348,9 +349,12 @@ this.PushService = {
.deferred.reject({status: 0, error: "TimeoutError"});
delete this._pendingRequests[channelID];
for (let i = this._requestQueue.length - 1; i >= 0; --i)
if (this._requestQueue[i].channelID == channelID)
for (let i = this._requestQueue.length - 1; i >= 0; --i) {
let [, data] = this._requestQueue[i];
if (data && data.channelID == channelID) {
this._requestQueue.splice(i, 1);
}
}
}
}
@ -493,6 +497,9 @@ this.PushService = {
init: function() {
debug("init()");
if (this._started) {
return;
}
var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIFrameScriptLoader);
@ -564,6 +571,7 @@ this.PushService = {
this._waitingForPong = false;
this._stopAlarm();
this._cancelPendingRequests();
},
uninit: function() {
@ -603,6 +611,7 @@ this.PushService = {
this._requestTimeoutTimer.cancel();
}
this._started = false;
debug("shutdown complete!");
},
@ -880,6 +889,10 @@ this.PushService = {
_startListeningIfChannelsPresent: function() {
// Check to see if we need to do anything.
if (this._requestQueue.length > 0) {
this._beginWSSetup();
return;
}
this._db.getAllChannelIDs(function(channelIDs) {
if (channelIDs.length > 0) {
this._beginWSSetup();
@ -1163,11 +1176,9 @@ this.PushService = {
debug("sendRequest() " + action);
if (typeof data.channelID !== "string") {
debug("Received non-string channelID");
return Promise.reject("Received non-string channelID");
return Promise.reject({error: "Received non-string channelID"});
}
let deferred = Promise.defer();
if (Object.keys(this._pendingRequests).length == 0) {
// start the timer since we now have at least one request
if (!this._requestTimeoutTimer)
@ -1178,8 +1189,18 @@ this.PushService = {
Ci.nsITimer.TYPE_REPEATING_SLACK);
}
this._pendingRequests[data.channelID] = { deferred: deferred,
ctime: Date.now() };
let deferred;
let request = this._pendingRequests[data.channelID];
if (request) {
// If a request is already pending for this channel ID, assume it's a
// retry. Use the existing deferred, but update the send time and re-send
// the request.
deferred = request.deferred;
} else {
deferred = Promise.defer();
request = this._pendingRequests[data.channelID] = {deferred};
}
request.ctime = Date.now();
this._send(action, data);
return deferred.promise;
@ -1201,6 +1222,11 @@ this.PushService = {
}
if (this._currentState != STATE_READY) {
if (!this._started) {
// The component hasn't been initialized yet. Return early; init()
// will dequeue all pending requests.
return;
}
if (!this._ws) {
// This will end up calling processNextRequestInQueue().
this._beginWSSetup();
@ -1269,52 +1295,49 @@ this.PushService = {
// registration.
_notifyAllAppsRegister: function() {
debug("notifyAllAppsRegister()");
let deferred = Promise.defer();
// records are objects describing the registration as stored in IndexedDB.
function wakeupRegisteredApps(records) {
// Pages to be notified.
// wakeupTable[scope] -> [ pageURL ]
let wakeupTable = {};
for (let i = 0; i < records.length; i++) {
let record = records[i];
if (!(record.scope in wakeupTable))
wakeupTable[record.scope] = [];
wakeupTable[record.scope].push(record.pageURL);
}
// TODO -- test needed. E10s support needed.
let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
for (let scope in wakeupTable) {
wakeupTable[scope].forEach(function(pageURL) {
globalMM.broadcastAsyncMessage('pushsubscriptionchanged', aPushRecord.scope);
});
}
deferred.resolve();
}
this._db.getAllChannelIDs(wakeupRegisteredApps, deferred.reject);
return deferred.promise;
return new Promise((resolve, reject) => {
// records are objects describing the registration as stored in IndexedDB.
this._db.getAllChannelIDs(records => {
let scopes = new Set();
for (let record of records) {
scopes.add(record.scope);
}
let globalMM = Cc['@mozilla.org/globalmessagemanager;1'].getService(Ci.nsIMessageListenerManager);
for (let scope of scopes) {
// Notify XPCOM observers.
Services.obs.notifyObservers(
null,
"push-subscription-change",
scope
);
// TODO -- test needed. E10s support needed.
globalMM.broadcastAsyncMessage('pushsubscriptionchanged', scope);
}
resolve();
}, reject);
});
},
_notifyApp: function(aPushRecord) {
if (!aPushRecord || !aPushRecord.pageURL || !aPushRecord.scope) {
if (!aPushRecord || !aPushRecord.scope) {
debug("notifyApp() something is undefined. Dropping notification: "
+ JSON.stringify(aPushRecord) );
return;
}
debug("notifyApp() " + aPushRecord.pageURL +
" " + aPushRecord.scope);
let pageURI = Services.io.newURI(aPushRecord.pageURL, null, null);
debug("notifyApp() " + aPushRecord.scope);
let scopeURI = Services.io.newURI(aPushRecord.scope, null, null);
let message = {
pushEndpoint: aPushRecord.pushEndpoint,
version: aPushRecord.version
};
// Notify XPCOM observers.
let notification = Cc["@mozilla.org/push/ObserverNotification;1"]
.createInstance(Ci.nsIPushObserverNotification);
notification.pushEndpoint = aPushRecord.pushEndpoint;
notification.version = aPushRecord.version;
notification.data = "";
Services.obs.notifyObservers(
notification,
"push-notification",
aPushRecord.scope
);
// If permission has been revoked, trash the message.
if(Services.perms.testExactPermission(scopeURI, "push") != Ci.nsIPermissionManager.ALLOW_ACTION) {
@ -1363,24 +1386,38 @@ this.PushService = {
* Called on message from the child process. aPageRecord is an object sent by
* navigator.push, identifying the sending page and other fields.
*/
_registerWithServer: function(channelID, aPageRecord) {
debug("registerWithServer()");
_registerWithServer: function(aPageRecord, aMessageManager) {
return this._sendRequest("register", {channelID: channelID})
.then(
this._onRegisterSuccess.bind(this, aPageRecord, channelID),
this._onRegisterError.bind(this)
);
},
_generateID: function() {
let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator);
// generateUUID() gives a UUID surrounded by {...}, slice them off.
let channelID = uuidGenerator.generateUUID().toString().slice(1, -1);
return uuidGenerator.generateUUID().toString().slice(1, -1);
},
this._sendRequest("register", {channelID: channelID})
.then(
this._onRegisterSuccess.bind(this, aPageRecord, channelID),
this._onRegisterError.bind(this, aPageRecord, aMessageManager)
)
.then(
function(message) {
aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
},
function(message) {
aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
_register: function(aPageRecord) {
let recordPromise = new Promise((resolve, reject) =>
this._db.getByScope(aPageRecord.scope, resolve, reject));
return recordPromise.then(
pushRecord => {
if (pushRecord == null) {
let channelID = this._generateID();
return this._registerWithServer(channelID, aPageRecord);
}
return pushRecord;
},
error => {
debug("getByScope failed");
throw "Database error";
}
);
},
@ -1388,17 +1425,20 @@ this.PushService = {
register: function(aPageRecord, aMessageManager) {
debug("register(): " + JSON.stringify(aPageRecord));
this._db.getByScope(aPageRecord.scope,
this._register(aPageRecord).then(
function(aPageRecord, aMessageManager, pushRecord) {
if (pushRecord == null) {
this._registerWithServer(aPageRecord, aMessageManager);
}
else {
this._onRegistrationSuccess(aPageRecord, aMessageManager, pushRecord);
}
let message = {
requestID: aPageRecord.requestID,
pushEndpoint: pushRecord.pushEndpoint
};
aMessageManager.sendAsyncMessage("PushService:Register:OK", message);
}.bind(this, aPageRecord, aMessageManager),
function () {
debug("getByScope failed");
function(error) {
let message = {
requestID: aPageRecord.requestID,
error
};
aMessageManager.sendAsyncMessage("PushService:Register:KO", message);
}
);
},
@ -1409,19 +1449,15 @@ this.PushService = {
*/
_onRegisterSuccess: function(aPageRecord, generatedChannelID, data) {
debug("_onRegisterSuccess()");
let deferred = Promise.defer();
let message = { requestID: aPageRecord.requestID };
if (typeof data.channelID !== "string") {
debug("Invalid channelID " + message);
message["error"] = "Invalid channelID received";
throw message;
debug("Invalid channelID " + data.channelID);
throw "Invalid channelID received";
}
else if (data.channelID != generatedChannelID) {
debug("Server replied with different channelID " + data.channelID +
" than what UA generated " + generatedChannelID);
message["error"] = "Server sent 200 status code but different channelID";
throw message;
throw "Server sent 200 status code but different channelID";
}
try {
@ -1429,8 +1465,7 @@ this.PushService = {
}
catch (e) {
debug("Invalid pushEndpoint " + data.pushEndpoint);
message["error"] = "Invalid pushEndpoint " + data.pushEndpoint;
throw message;
throw "Invalid pushEndpoint " + data.pushEndpoint;
}
let record = {
@ -1443,33 +1478,30 @@ this.PushService = {
debug("scope in _onRegisterSuccess: " + aPageRecord.scope)
this._updatePushRecord(record)
return this._updatePushRecord(record)
.then(
function() {
message["pushEndpoint"] = data.pushEndpoint;
deferred.resolve(message);
return record;
},
function(error) {
// Unable to save.
this._send("unregister", {channelID: record.channelID});
message["error"] = error;
deferred.reject(message);
throw error;
}.bind(this)
);
return deferred.promise;
},
/**
* Exceptions thrown in _onRegisterError are caught by the promise obtained
* from _sendRequest, causing the promise to be rejected instead.
*/
_onRegisterError: function(aPageRecord, aMessageManager, reply) {
_onRegisterError: function(reply) {
debug("_onRegisterError()");
if (!reply.error) {
debug("Called without valid error message!");
throw "Registration error";
}
throw { requestID: aPageRecord.requestID, error: reply.error };
throw reply.error;
},
/**
@ -1496,78 +1528,99 @@ this.PushService = {
* messages from the server, and have the client acknowledge. On a server,
* data is cheap, reliable notification is not.
*/
unregister: function(aPageRecord, aMessageManager) {
debug("unregister() " + JSON.stringify(aPageRecord));
_unregister: function(aPageRecord) {
debug("unregisterWithServer()");
let deferred = Promise.defer();
let fail = function(error) {
debug("unregister() fail() error " + error);
let message = {requestID: aPageRecord.requestID, error: error};
aMessageManager.sendAsyncMessage("PushService:Unregister:KO", message);
deferred.reject(error);
};
if (!aPageRecord.scope) {
fail("NotFoundError");
return deferred.promise;
}
this._db.getByPushEndpoint(aPageRecord.pushEndpoint, function(record) {
this._db.getByScope(aPageRecord.scope, function(record) {
// If the endpoint didn't exist, let's just fail.
if (record === undefined) {
fail("NotFoundError");
return;
}
// Non-owner tried to unregister, say success, but don't do anything.
if (record.scope !== aPageRecord.scope) {
aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
requestID: aPageRecord.requestID,
pushEndpoint: aPageRecord.pushEndpoint
});
return;
}
this._db.delete(record.channelID, function() {
// Let's be nice to the server and try to inform it, but we don't care
// about the reply.
this._send("unregister", {channelID: record.channelID});
deferred.resolve();
}.bind(this), fail);
}.bind(this), fail);
return deferred.promise;
},
unregister: function(aPageRecord, aMessageManager) {
debug("unregister() " + JSON.stringify(aPageRecord));
this._unregister(aPageRecord).then(
() => {
aMessageManager.sendAsyncMessage("PushService:Unregister:OK", {
requestID: aPageRecord.requestID,
pushEndpoint: aPageRecord.pushEndpoint
});
}.bind(this), fail);
}.bind(this), fail);
},
error => {
aMessageManager.sendAsyncMessage("PushService:Unregister:KO", {
requestID: aPageRecord.requestID,
error
});
}
);
},
/**
* Called on message from the child process
*/
_registration: function(aPageRecord) {
return new Promise((resolve, reject) => {
if (!aPageRecord.scope) {
reject("Database error");
return;
}
this._db.getByScope(aPageRecord.scope,
pushRecord => {
let registration = null;
if (pushRecord) {
registration = {
pushEndpoint: pushRecord.pushEndpoint,
version: pushRecord.version
};
}
resolve(registration);
},
() => reject("Database error")
);
});
},
registration: function(aPageRecord, aMessageManager) {
debug("registration()");
this._db.getByScope(aPageRecord.scope,
this._onRegistrationSuccess.bind(this, aPageRecord, aMessageManager),
this._onRegistrationError.bind(this, aPageRecord, aMessageManager));
},
_onRegistrationSuccess: function(aPageRecord,
aMessageManager,
pushRecord) {
let registration = null;
if (pushRecord) {
registration = {
pushEndpoint: pushRecord.pushEndpoint,
version: pushRecord.version
};
}
aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
requestID: aPageRecord.requestID,
registration: registration
});
},
_onRegistrationError: function(aPageRecord, aMessageManager) {
aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
requestID: aPageRecord.requestID,
error: "Database error"
});
return this._registration(aPageRecord).then(
registration => {
aMessageManager.sendAsyncMessage("PushService:Registration:OK", {
requestID: aPageRecord.requestID,
registration
});
},
error => {
aMessageManager.sendAsyncMessage("PushService:Registration:KO", {
requestID: aPageRecord.requestID,
error
});
}
);
},
// begin Push protocol handshake
@ -1730,6 +1783,17 @@ this.PushService = {
}
},
/**
* Rejects all pending requests with errors.
*/
_cancelPendingRequests: function() {
for (let channelID in this._pendingRequests) {
let request = this._pendingRequests[channelID];
delete this._pendingRequests[channelID];
request.deferred.reject({status: 0, error: "CancelledError"});
}
},
/**
* This method should be called only if the device is on a mobile network!
*/
@ -1904,4 +1968,4 @@ this.PushService = {
".mcc" + ("00" + networkInfo.mcc).slice(-3) + ".3gppnetwork.org";
queryDNSForDomain(netidAddress, callback);
}
}
};

View File

@ -1,50 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function PushServiceLauncher() {
};
PushServiceLauncher.prototype = {
classID: Components.ID("{4b8caa3b-3c58-4f3c-a7f5-7bd9cb24c11d}"),
contractID: "@mozilla.org/push/ServiceLauncher;1",
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
observe: function observe(subject, topic, data) {
switch (topic) {
case "app-startup":
Services.obs.addObserver(this, "final-ui-startup", true);
break;
case "final-ui-startup":
Services.obs.removeObserver(this, "final-ui-startup");
if (!Services.prefs.getBoolPref("dom.push.enabled")) {
return;
}
let isParent = Cc["@mozilla.org/xre/runtime;1"]
.getService(Ci.nsIXULRuntime)
.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
if (isParent) {
Cu.import("resource://gre/modules/PushService.jsm");
PushService.init();
}
break;
}
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PushServiceLauncher]);

View File

@ -6,7 +6,7 @@
EXTRA_COMPONENTS += [
'Push.js',
'Push.manifest',
'PushServiceLauncher.js',
'PushNotificationService.js',
]
EXTRA_JS_MODULES += [