Bug 715814 - Implement Web Activities: DOM Part [r=mounir]

This commit is contained in:
Fabrice Desré 2012-07-20 17:41:30 +02:00
parent 1ef55968bc
commit d19bb08b57
12 changed files with 625 additions and 23 deletions

View File

@ -477,8 +477,11 @@
@BINPATH@/components/SystemMessageManager.js
@BINPATH@/components/SystemMessageManager.manifest
@BINPATH@/components/ActivityProxy.js
@BINPATH@/components/Activities.manifest
@BINPATH@/components/ActivityOptions.js
@BINPATH@/components/ActivityProxy.js
@BINPATH@/components/ActivityRequestHandler.js
@BINPATH@/components/ActivityWrapper.js
@BINPATH@/components/AppProtocolHandler.js
@BINPATH@/components/AppProtocolHandler.manifest

View File

@ -17,6 +17,7 @@ XPIDLSRCS = nsIDOMActivity.idl \
nsIDOMActivityRequestHandler.idl \
nsIDOMNavigatorActivities.idl \
nsIActivityProxy.idl \
nsIActivityUIGlue.idl \
$(NULL)
include $(topsrcdir)/config/rules.mk

View File

@ -0,0 +1,25 @@
/* 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/. */
#include "nsISupports.idl"
[scriptable, function, uuid(7a16feb4-5a78-4589-9174-b728f26942e2)]
interface nsIActivityUIGlueCallback : nsISupports
{
void handleEvent(in long choice);
};
/**
* To be implemented by @mozilla.org/dom/activities/ui-glue;1
*/
[scriptable, uuid(8624ad73-937a-400f-9d93-39ab5449b867)]
interface nsIActivityUIGlue : nsISupports
{
/**
* @param name The name of the activity to handle (eg. "share", "pick").
* @param activities A json blob which is an array of { "title":"...", "icon":"..." }.
* @param onresult The callback to send the index of the choosen activity. Send -1 if no choice is made.
*/
void chooseActivity(in DOMString title, in jsval activities, in nsIActivityUIGlueCallback onresult);
};

View File

@ -1,2 +1,11 @@
component {ba9bd5cb-76a0-4ecf-a7b3-d2f7c43c5949} ActivityProxy.js
contract @mozilla.org/dom/activities/proxy;1 {ba9bd5cb-76a0-4ecf-a7b3-d2f7c43c5949}
component {5430d6f9-32d6-4924-ba39-6b6d1b093cd6} ActivityWrapper.js
contract @mozilla.org/dom/system-messages/wrapper/activity;1 {5430d6f9-32d6-4924-ba39-6b6d1b093cd6}
component {9326952a-dbe3-4d81-a51f-d9c160d96d6b} ActivityRequestHandler.js
contract @mozilla.org/dom/activities/request-handler;1 {9326952a-dbe3-4d81-a51f-d9c160d96d6b}
component {ee983dbb-d5ea-4c5b-be98-10a13cac9f9d} ActivityOptions.js
contract @mozilla.org/dom/activities/options;1 {ee983dbb-d5ea-4c5b-be98-10a13cac9f9d}

View File

@ -0,0 +1,292 @@
/* 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 Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
XPCOMUtils.defineLazyGetter(this, "ppmm", function() {
return Cc["@mozilla.org/parentprocessmessagemanager;1"]
.getService(Ci.nsIFrameMessageManager);
});
const EXPORTED_SYMBOLS = [];
let idbGlobal = this;
function debug(aMsg) {
//dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n");
}
const DB_NAME = "activities";
const DB_VERSION = 1;
const STORE_NAME = "activities";
function ActivitiesDb() {
}
ActivitiesDb.prototype = {
__proto__: IndexedDBHelper.prototype,
init: function actdb_init() {
let idbManager = Cc["@mozilla.org/dom/indexeddb/manager;1"]
.getService(Ci.nsIIndexedDatabaseManager);
idbManager.initWindowless(idbGlobal);
this.initDBHelper(DB_NAME, DB_VERSION, STORE_NAME, idbGlobal);
},
/**
* Create the initial database schema.
*
* The schema of records stored is as follows:
*
* {
* id: String
* manifest: String
* name: String
* title: String
* icon: String
* description: jsval
* }
*/
upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion);
let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
// indexes
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("manifest", "manifest", { unique: false });
debug("Created object stores and indexes");
},
// unique ids made of (uri, action)
createId: function actdb_createId(aObject) {
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA1);
// add uri and action to the hash
["manifest", "name"].forEach(function(aProp) {
let data = converter.convertToByteArray(aObject[aProp], {});
hasher.update(data, data.length);
});
return hasher.finish(true);
},
add: function actdb_add(aObject, aSuccess, aError) {
this.newTxn("readwrite", function (txn, store) {
let object = {
manifest: aObject.manifest,
name: aObject.name,
title: aObject.title || "",
icon: aObject.icon || "",
description: aObject.description
};
object.id = this.createId(object);
debug("Going to add " + JSON.stringify(object));
store.put(object);
}.bind(this), aSuccess, aError);
},
// we want to remove all activities for (manifest, name)
remove: function actdb_remove(aObject) {
this.newTxn("readwrite", function (txn, store) {
let object = {
manifest: aObject.manifest,
name: aObject.name
};
debug("Going to remove " + JSON.stringify(object));
store.delete(this.createId(object));
}.bind(this), function() {}, function() {});
},
find: function actdb_find(aObject, aSuccess, aError, aMatch) {
debug("Looking for " + aObject.options.name);
this.newTxn("readonly", function (txn, store) {
let index = store.index("name");
let request = index.mozGetAll(aObject.options.name);
request.onsuccess = function findSuccess(aEvent) {
debug("Request successful. Record count: " + aEvent.target.result.length);
if (!txn.result) {
txn.result = {
name: aObject.options.name,
options: []
};
}
aEvent.target.result.forEach(function(result) {
if (!aMatch(result))
return;
txn.result.options.push({
manifest: result.manifest,
title: result.title,
icon: result.icon,
description: result.description
});
});
}
}.bind(this), aSuccess, aError);
}
}
let Activities = {
messages: [
// ActivityProxy.js
"Activity:Start",
// ActivityRequestHandler.js
"Activity:PostResult",
"Activity:PostError",
"Activities:Register",
"Activities:Unregister",
],
init: function activities_init() {
this.messages.forEach(function(msgName) {
ppmm.addMessageListener(msgName, this);
}, this);
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.db = new ActivitiesDb();
this.db.init();
},
observe: function activities_observe(aSubject, aTopic, aData) {
this.messages.forEach(function(msgName) {
ppmm.removeMessageListener(msgName, this);
}, this);
ppmm = null;
Services.obs.removeObserver(this, "xpcom-shutdown");
},
/**
* Starts an activity by doing:
* - finds a list of matching activities.
* - calls the UI glue to get the user choice.
* - fire an system message of type "activity" to this app, sending the
* activity data as a payload.
*/
startActivity: function activities_startActivity(aMsg) {
debug("StartActivity: " + JSON.stringify(aMsg));
let successCb = function successCb(aResults) {
debug(JSON.stringify(aResults));
// We have no matching activity registered, let's fire an error.
if (aResults.length === 0) {
ppmm.sendAsyncMessage("Activity:FireError", {
"id": aMsg.id,
"error": "NO_PROVIDER"
});
return;
}
function getActivityChoice(aChoice) {
debug("Activity choice: " + aChoice);
// The user has cancelled the choice, fire an error.
if (aChoice === -1) {
ppmm.sendAsyncMessage("Activity:FireError", {
"id": aMsg.id,
"error": "USER_ABORT"
});
return;
}
let sysmm = Cc["@mozilla.org/system-message-internal;1"]
.getService(Ci.nsISystemMessagesInternal);
if (!sysmm) {
// System message is not present, what should we do?
return;
}
debug("Sending system message...");
let result = aResults.options[aChoice];
sysmm.sendMessage("activity", {
"id": aMsg.id,
"payload": aMsg.options
}, Services.io.newURI(result.manifest, null, null));
if (!result.description.returnValue) {
ppmm.sendAsyncMessage("Activity:FireSuccess", {
"id": aMsg.id,
"result": null
});
}
};
let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"]
.createInstance(Ci.nsIActivityUIGlue);
glue.chooseActivity(aResults.name, aResults.options, getActivityChoice);
};
let errorCb = function errorCb(aError) {
// Something unexpected happened. Should we send an error back?
debug("Error in startActivity: " + aError + "\n");
};
let matchFunc = function matchFunc(aResult) {
// Bug 773383: arrays of strings / regexp.
for (let prop in aResult.description.filters) {
if (aMsg.options.data[prop] !== aResult.description.filters[prop]) {
return false;
}
}
return true;
};
this.db.find(aMsg, successCb, errorCb, matchFunc);
},
receiveMessage: function activities_receiveMessage(aMessage) {
let mm = aMessage.target.QueryInterface(Ci.nsIFrameMessageManager);
let msg = aMessage.json;
switch(aMessage.name) {
case "Activity:Start":
this.startActivity(msg);
break;
case "Activity:PostResult":
ppmm.sendAsyncMessage("Activity:FireSuccess", msg);
break;
case "Activity:PostError":
ppmm.sendAsyncMessage("Activity:FireError", msg);
break;
case "Activities:Register":
this.db.add(msg, function onSuccess(aEvent) {
mm.sendAsyncMessage("Activities:Register:OK", msg);
},
function onError(aEvent) {
msg.error = "REGISTER_ERROR";
mm.sendAsyncMessage("Activities:Register:KO", msg);
});
break;
case "Activities:Unregister":
this.db.remove(msg);
break;
}
}
}
Activities.init();

View File

@ -0,0 +1,56 @@
/* 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 Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function debug(aMsg) {
//dump("-- ActivityOptions.js " + Date.now() + " : " + aMsg + "\n");
}
/**
* nsIDOMMozActivityOptions implementation.
*/
function ActivityOptions() {
debug("ActivityOptions");
this.wrappedJSObject = this;
// When a system message of type 'activity' is emitted, it forces the
// creation of an ActivityWrapper which in turns replace the default
// system message callback. The newly created wrapper then create a
// nsIDOMActivityRequestHandler object and fills up the properties of
// this object as well as the properties of the nsIDOMActivityOptions
// object contains by the request handler.
this._name = null;
this._data = null;
}
ActivityOptions.prototype = {
get name() {
return this._name;
},
get data() {
return this._data;
},
classID: Components.ID("{ee983dbb-d5ea-4c5b-be98-10a13cac9f9d}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMMozActivityOptions]),
classInfo: XPCOMUtils.generateCI({
classID: Components.ID("{ee983dbb-d5ea-4c5b-be98-10a13cac9f9d}"),
contractID: "@mozilla.org/dom/activities/options;1",
interfaces: [Ci.nsIDOMMozActivityOptions],
flags: Ci.nsIClassInfo.DOM_OBJECT,
classDescription: "Activity Options"
})
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([ActivityOptions]);

View File

@ -0,0 +1,76 @@
/* 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;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "cpmm", function() {
return Cc["@mozilla.org/childprocessmessagemanager;1"]
.getService(Ci.nsIFrameMessageManager)
.QueryInterface(Ci.nsISyncMessageSender);
});
function debug(aMsg) {
//dump("-- ActivityRequestHandler.js " + Date.now() + " : " + aMsg + "\n");
}
/**
* nsIDOMMozActivityRequestHandler implementation.
*/
function ActivityRequestHandler() {
debug("ActivityRequestHandler");
this.wrappedJSObject = this;
// When a system message of type 'activity' is emitted, it forces the
// creation of an ActivityWrapper which in turns replace the default
// system message callback. The newly created wrapper then create a
// nsIDOMActivityRequestHandler object and fills up the properties of
// this object as well as the properties of the nsIDOMActivityOptions
// object contains by the request handler.
this._id = null;
this._options = Cc["@mozilla.org/dom/activities/options;1"]
.createInstance(Ci.nsIDOMMozActivityOptions);
}
ActivityRequestHandler.prototype = {
get source() {
return this._options;
},
postResult: function arh_postResult(aResult) {
cpmm.sendAsyncMessage("Activity:PostResult", {
"id": this._id,
"result": aResult
});
},
postError: function arh_postError(aError) {
cpmm.sendAsyncMessage("Activity:PostError", {
"id": this._id,
"error": aError
});
},
classID: Components.ID("{9326952a-dbe3-4d81-a51f-d9c160d96d6b}"),
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIDOMMozActivityRequestHandler
]),
classInfo: XPCOMUtils.generateCI({
classID: Components.ID("{9326952a-dbe3-4d81-a51f-d9c160d96d6b}"),
contractID: "@mozilla.org/dom/activities/request-handler;1",
interfaces: [Ci.nsIDOMMozActivityRequestHandler],
flags: Ci.nsIClassInfo.DOM_OBJECT,
classDescription: "Activity Request Handler"
})
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([ActivityRequestHandler]);

View File

@ -0,0 +1,45 @@
/* 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;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function debug(aMsg) {
//dump("-- ActivityWrapper.js " + Date.now() + " : " + aMsg + "\n");
}
/**
* nsISystemMessagesWrapper implementation. Will return a
* nsIDOMMozActivityRequestHandler
*/
function ActivityWrapper() {
debug("ActivityWrapper");
}
ActivityWrapper.prototype = {
wrapMessage: function wrapMessage(aMessage) {
debug("Wrapping " + JSON.stringify(aMessage));
let handler = Cc["@mozilla.org/dom/activities/request-handler;1"]
.createInstance(Ci.nsIDOMMozActivityRequestHandler);
handler.wrappedJSObject._id = aMessage.id;
// options is an nsIDOMActivityOptions object.
var options = handler.wrappedJSObject._options;
options.wrappedJSObject._name = aMessage.payload.name;
options.wrappedJSObject._data = aMessage.payload.data;
return handler;
},
classID: Components.ID("{5430d6f9-32d6-4924-ba39-6b6d1b093cd6}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsISystemMessagesWrapper])
}
const NSGetFactory = XPCOMUtils.generateNSGetFactory([ActivityWrapper]);

View File

@ -26,9 +26,16 @@ EXPORTS_mozilla/dom = \
$(NULL)
EXTRA_COMPONENTS = \
ActivityOptions.js \
ActivityProxy.js \
ActivityRequestHandler.js \
ActivityWrapper.js \
Activities.manifest \
$(NULL)
EXTRA_JS_MODULES = \
ActivitiesService.jsm \
$(NULL)
include $(topsrcdir)/config/config.mk
include $(topsrcdir)/config/rules.mk

View File

@ -14,6 +14,7 @@ let EXPORTED_SYMBOLS = ["DOMApplicationRegistry", "DOMApplicationManifest"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import('resource://gre/modules/ActivitiesService.jsm');
const WEBAPP_RUNTIME = Services.appinfo.ID == "webapprt@mozilla.org";
@ -27,6 +28,10 @@ XPCOMUtils.defineLazyGetter(this, "ppmm", function() {
.getService(Ci.nsIFrameMessageManager);
});
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIFrameMessageManager");
XPCOMUtils.defineLazyGetter(this, "msgmgr", function() {
return Cc["@mozilla.org/system-message-internal;1"]
.getService(Ci.nsISystemMessagesInternal);
@ -67,7 +72,7 @@ let DOMApplicationRegistry = {
this.webapps = aData;
for (let id in this.webapps) {
#ifdef MOZ_SYS_MSG
this._registerSystemMessagesForId(id);
this._processManifestForId(id);
#endif
if (!this.webapps[id].localId) {
this.webapps[id].localId = this._nextLocalId();
@ -99,10 +104,51 @@ let DOMApplicationRegistry = {
}
},
_registerSystemMessagesForId: function(aId) {
_registerActivities: function(aManifest, aApp) {
if (!aManifest.activities) {
return;
}
let manifest = new DOMApplicationManifest(aManifest, aApp.origin);
for (let activity in aManifest.activities) {
let description = aManifest.activities[activity];
let json = {
"manifest": aApp.manifestURL,
"name": activity,
"title": manifest.name,
"icon": manifest.iconURLForSize(128),
"description": description
}
cpmm.sendAsyncMessage("Activities:Register", json);
let launchPath =
Services.io.newURI(manifest.fullLaunchPath(description.href), null, null);
let manifestURL = Services.io.newURI(aApp.manifestURL, null, null);
msgmgr.registerPage("activity", launchPath, manifestURL);
}
},
_unregisterActivities: function(aManifest, aApp) {
if (!aManifest.activities) {
return;
}
for (let activity in aManifest.activities) {
let description = aManifest.activities[activity];
let json = {
"manifest": aApp.manifestURL,
"name": activity
}
cpmm.sendAsyncMessage("Activities:Unregister", json);
}
},
_processManifestForId: function(aId) {
let app = this.webapps[aId];
this._readManifests([{ id: aId }], (function registerManifest(aResult) {
this._registerSystemMessages(aResult[0].manifest, app);
let manifest = aResult[0].manifest;
this._registerSystemMessages(manifest, app);
this._registerActivities(manifest, app);
}).bind(this));
},
#endif
@ -489,26 +535,36 @@ let DOMApplicationRegistry = {
let found = false;
for (let id in this.webapps) {
let app = this.webapps[id];
if (app.origin == aData.origin) {
found = true;
let appNote = JSON.stringify(this._cloneAppObject(app));
appNote.id = id;
delete this.webapps[id];
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
try {
dir.remove(true);
} catch (e) {
}
this._saveApps((function() {
ppmm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData);
Services.obs.notifyObservers(this, "webapps-sync-uninstall", appNote);
}).bind(this));
if (app.origin != aData.origin) {
continue;
}
found = true;
let appNote = JSON.stringify(this._cloneAppObject(app));
appNote.id = id;
this._readManifests([{ id: id }], (function unregisterManifest(aResult) {
#ifdef MOZ_SYS_MSG
this._unregisterActivities(aResult[0].manifest, app);
#endif
}).bind(this));
let dir = FileUtils.getDir(DIRECTORY_NAME, ["webapps", id], true, true);
try {
dir.remove(true);
} catch (e) {}
delete this.webapps[id];
this._saveApps((function() {
ppmm.sendAsyncMessage("Webapps:Uninstall:Return:OK", aData);
Services.obs.notifyObservers(this, "webapps-sync-uninstall", appNote);
}).bind(this));
}
if (!found)
if (!found) {
ppmm.sendAsyncMessage("Webapps:Uninstall:Return:KO", aData);
}
},
getSelf: function(aData) {

View File

@ -45,6 +45,27 @@ function SystemMessageManager() {
SystemMessageManager.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
_dispatchMessage: function sysMessMgr_dispatchMessage(aType, aHandler, aMessage) {
// We get a json blob, but in some cases we want another kind of object
// to be dispatched.
// To do so, we check if we have a with a contract ID of
// "@mozilla.org/dom/system-messages/wrapper/TYPE;1" component implementing
// nsISystemMessageWrapper.
debug("Dispatching " + JSON.stringify(aMessage) + "\n");
let contractID = "@mozilla.org/dom/system-messages/wrapper/" + aType + ";1";
if (contractID in Cc) {
debug(contractID + " is registered, creating an instance");
let wrapper = Cc[contractID].createInstance(Ci.nsISystemMessagesWrapper);
if (wrapper) {
aMessage = wrapper.wrapMessage(aMessage);
debug("wrapped = " + aMessage);
}
}
aHandler.handleMessage(aMessage);
},
mozSetMessageHandler: function sysMessMgr_setMessageHandler(aType, aHandler) {
debug("setMessage handler for [" + aType + "] " + aHandler);
if (!aType) {
@ -68,10 +89,11 @@ SystemMessageManager.prototype = {
let thread = Services.tm.mainThread;
let pending = this._pendings[aType];
this._pendings[aType] = [];
let self = this;
pending.forEach(function dispatch_pending(aPending) {
thread.dispatch({
run: function run() {
aHandler.handleMessage(aPending);
self._dispatchMessage(aType, aHandler, aPending);
}
}, Ci.nsIEventTarget.DISPATCH_NORMAL);
});
@ -139,7 +161,7 @@ SystemMessageManager.prototype = {
return;
}
this._handlers[msg.type].handleMessage(msg.msg);
this._dispatchMessage(msg.type, this._handlers[msg.type], msg.msg);
},
// nsIDOMGlobalPropertyInitializer implementation.

View File

@ -27,3 +27,13 @@ interface nsISystemMessagesInternal : nsISupports
*/
void registerPage(in DOMString type, in nsIURI pageURI, in nsIURI manifestURI);
};
[scriptable, uuid(b43c74ec-1b64-49fb-b552-aadd9d827eec)]
interface nsISystemMessagesWrapper: nsISupports
{
/*
* Wrap a message and gives back any kind of object.
* @param message The json blob to wrap.
*/
jsval wrapMessage(in jsval message);
};