michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict" michael@0: michael@0: const Cu = Components.utils; michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter", michael@0: "resource://gre/modules/ActivitiesServiceFilter.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "ppmm", michael@0: "@mozilla.org/parentprocessmessagemanager;1", michael@0: "nsIMessageBroadcaster"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "NetUtil", michael@0: "@mozilla.org/network/util;1", michael@0: "nsINetUtil"); michael@0: michael@0: this.EXPORTED_SYMBOLS = []; michael@0: michael@0: function debug(aMsg) { michael@0: //dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n"); michael@0: } michael@0: michael@0: const DB_NAME = "activities"; michael@0: const DB_VERSION = 1; michael@0: const STORE_NAME = "activities"; michael@0: michael@0: function ActivitiesDb() { michael@0: michael@0: } michael@0: michael@0: ActivitiesDb.prototype = { michael@0: __proto__: IndexedDBHelper.prototype, michael@0: michael@0: init: function actdb_init() { michael@0: this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]); michael@0: }, michael@0: michael@0: /** michael@0: * Create the initial database schema. michael@0: * michael@0: * The schema of records stored is as follows: michael@0: * michael@0: * { michael@0: * id: String michael@0: * manifest: String michael@0: * name: String michael@0: * icon: String michael@0: * description: jsval michael@0: * } michael@0: */ michael@0: upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { michael@0: debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion); michael@0: let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" }); michael@0: michael@0: // indexes michael@0: objectStore.createIndex("name", "name", { unique: false }); michael@0: objectStore.createIndex("manifest", "manifest", { unique: false }); michael@0: michael@0: debug("Created object stores and indexes"); michael@0: }, michael@0: michael@0: // unique ids made of (uri, action) michael@0: createId: function actdb_createId(aObject) { michael@0: let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] michael@0: .createInstance(Ci.nsIScriptableUnicodeConverter); michael@0: converter.charset = "UTF-8"; michael@0: michael@0: let hasher = Cc["@mozilla.org/security/hash;1"] michael@0: .createInstance(Ci.nsICryptoHash); michael@0: hasher.init(hasher.SHA1); michael@0: michael@0: // add uri and action to the hash michael@0: ["manifest", "name"].forEach(function(aProp) { michael@0: let data = converter.convertToByteArray(aObject[aProp], {}); michael@0: hasher.update(data, data.length); michael@0: }); michael@0: michael@0: return hasher.finish(true); michael@0: }, michael@0: michael@0: // Add all the activities carried in the |aObjects| array. michael@0: add: function actdb_add(aObjects, aSuccess, aError) { michael@0: this.newTxn("readwrite", STORE_NAME, function (txn, store) { michael@0: aObjects.forEach(function (aObject) { michael@0: let object = { michael@0: manifest: aObject.manifest, michael@0: name: aObject.name, michael@0: icon: aObject.icon || "", michael@0: description: aObject.description michael@0: }; michael@0: object.id = this.createId(object); michael@0: debug("Going to add " + JSON.stringify(object)); michael@0: store.put(object); michael@0: }, this); michael@0: }.bind(this), aSuccess, aError); michael@0: }, michael@0: michael@0: // Remove all the activities carried in the |aObjects| array. michael@0: remove: function actdb_remove(aObjects) { michael@0: this.newTxn("readwrite", STORE_NAME, function (txn, store) { michael@0: aObjects.forEach(function (aObject) { michael@0: let object = { michael@0: manifest: aObject.manifest, michael@0: name: aObject.name michael@0: }; michael@0: debug("Going to remove " + JSON.stringify(object)); michael@0: store.delete(this.createId(object)); michael@0: }, this); michael@0: }.bind(this), function() {}, function() {}); michael@0: }, michael@0: michael@0: find: function actdb_find(aObject, aSuccess, aError, aMatch) { michael@0: debug("Looking for " + aObject.options.name); michael@0: michael@0: this.newTxn("readonly", STORE_NAME, function (txn, store) { michael@0: let index = store.index("name"); michael@0: let request = index.mozGetAll(aObject.options.name); michael@0: request.onsuccess = function findSuccess(aEvent) { michael@0: debug("Request successful. Record count: " + aEvent.target.result.length); michael@0: if (!txn.result) { michael@0: txn.result = { michael@0: name: aObject.options.name, michael@0: options: [] michael@0: }; michael@0: } michael@0: michael@0: aEvent.target.result.forEach(function(result) { michael@0: if (!aMatch(result)) michael@0: return; michael@0: michael@0: txn.result.options.push({ michael@0: manifest: result.manifest, michael@0: icon: result.icon, michael@0: description: result.description michael@0: }); michael@0: }); michael@0: } michael@0: }.bind(this), aSuccess, aError); michael@0: } michael@0: } michael@0: michael@0: let Activities = { michael@0: messages: [ michael@0: // ActivityProxy.js michael@0: "Activity:Start", michael@0: michael@0: // ActivityWrapper.js michael@0: "Activity:Ready", michael@0: michael@0: // ActivityRequestHandler.js michael@0: "Activity:PostResult", michael@0: "Activity:PostError", michael@0: michael@0: "Activities:Register", michael@0: "Activities:Unregister", michael@0: "Activities:GetContentTypes", michael@0: michael@0: "child-process-shutdown" michael@0: ], michael@0: michael@0: init: function activities_init() { michael@0: this.messages.forEach(function(msgName) { michael@0: ppmm.addMessageListener(msgName, this); michael@0: }, this); michael@0: michael@0: Services.obs.addObserver(this, "xpcom-shutdown", false); michael@0: michael@0: this.db = new ActivitiesDb(); michael@0: this.db.init(); michael@0: this.callers = {}; michael@0: }, michael@0: michael@0: observe: function activities_observe(aSubject, aTopic, aData) { michael@0: this.messages.forEach(function(msgName) { michael@0: ppmm.removeMessageListener(msgName, this); michael@0: }, this); michael@0: ppmm = null; michael@0: michael@0: if (this.db) { michael@0: this.db.close(); michael@0: this.db = null; michael@0: } michael@0: michael@0: Services.obs.removeObserver(this, "xpcom-shutdown"); michael@0: }, michael@0: michael@0: /** michael@0: * Starts an activity by doing: michael@0: * - finds a list of matching activities. michael@0: * - calls the UI glue to get the user choice. michael@0: * - fire an system message of type "activity" to this app, sending the michael@0: * activity data as a payload. michael@0: */ michael@0: startActivity: function activities_startActivity(aMsg) { michael@0: debug("StartActivity: " + JSON.stringify(aMsg)); michael@0: michael@0: let successCb = function successCb(aResults) { michael@0: debug(JSON.stringify(aResults)); michael@0: michael@0: // We have no matching activity registered, let's fire an error. michael@0: if (aResults.options.length === 0) { michael@0: Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", { michael@0: "id": aMsg.id, michael@0: "error": "NO_PROVIDER" michael@0: }); michael@0: delete Activities.callers[aMsg.id]; michael@0: return; michael@0: } michael@0: michael@0: function getActivityChoice(aChoice) { michael@0: debug("Activity choice: " + aChoice); michael@0: michael@0: // The user has cancelled the choice, fire an error. michael@0: if (aChoice === -1) { michael@0: Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", { michael@0: "id": aMsg.id, michael@0: "error": "ActivityCanceled" michael@0: }); michael@0: delete Activities.callers[aMsg.id]; michael@0: return; michael@0: } michael@0: michael@0: let sysmm = Cc["@mozilla.org/system-message-internal;1"] michael@0: .getService(Ci.nsISystemMessagesInternal); michael@0: if (!sysmm) { michael@0: // System message is not present, what should we do? michael@0: delete Activities.callers[aMsg.id]; michael@0: return; michael@0: } michael@0: michael@0: debug("Sending system message..."); michael@0: let result = aResults.options[aChoice]; michael@0: sysmm.sendMessage("activity", { michael@0: "id": aMsg.id, michael@0: "payload": aMsg.options, michael@0: "target": result.description michael@0: }, michael@0: Services.io.newURI(result.description.href, null, null), michael@0: Services.io.newURI(result.manifest, null, null), michael@0: { michael@0: "manifestURL": Activities.callers[aMsg.id].manifestURL, michael@0: "pageURL": Activities.callers[aMsg.id].pageURL michael@0: }); michael@0: michael@0: if (!result.description.returnValue) { michael@0: Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", { michael@0: "id": aMsg.id, michael@0: "result": null michael@0: }); michael@0: // No need to notify observers, since we don't want the caller michael@0: // to be raised on the foreground that quick. michael@0: delete Activities.callers[aMsg.id]; michael@0: } michael@0: }; michael@0: michael@0: let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"] michael@0: .createInstance(Ci.nsIActivityUIGlue); michael@0: glue.chooseActivity(aResults.name, aResults.options, getActivityChoice); michael@0: }; michael@0: michael@0: let errorCb = function errorCb(aError) { michael@0: // Something unexpected happened. Should we send an error back? michael@0: debug("Error in startActivity: " + aError + "\n"); michael@0: }; michael@0: michael@0: let matchFunc = function matchFunc(aResult) { michael@0: return ActivitiesServiceFilter.match(aMsg.options.data, michael@0: aResult.description.filters); michael@0: }; michael@0: michael@0: this.db.find(aMsg, successCb, errorCb, matchFunc); michael@0: }, michael@0: michael@0: receiveMessage: function activities_receiveMessage(aMessage) { michael@0: let mm = aMessage.target; michael@0: let msg = aMessage.json; michael@0: michael@0: let caller; michael@0: let obsData; michael@0: michael@0: if (aMessage.name == "Activity:PostResult" || michael@0: aMessage.name == "Activity:PostError" || michael@0: aMessage.name == "Activity:Ready") { michael@0: caller = this.callers[msg.id]; michael@0: if (!caller) { michael@0: debug("!! caller is null for msg.id=" + msg.id); michael@0: return; michael@0: } michael@0: obsData = JSON.stringify({ manifestURL: caller.manifestURL, michael@0: pageURL: caller.pageURL, michael@0: success: aMessage.name == "Activity:PostResult" }); michael@0: } michael@0: michael@0: switch(aMessage.name) { michael@0: case "Activity:Start": michael@0: this.callers[msg.id] = { mm: mm, michael@0: manifestURL: msg.manifestURL, michael@0: pageURL: msg.pageURL }; michael@0: this.startActivity(msg); michael@0: break; michael@0: michael@0: case "Activity:Ready": michael@0: caller.childMM = mm; michael@0: break; michael@0: michael@0: case "Activity:PostResult": michael@0: caller.mm.sendAsyncMessage("Activity:FireSuccess", msg); michael@0: delete this.callers[msg.id]; michael@0: break; michael@0: case "Activity:PostError": michael@0: caller.mm.sendAsyncMessage("Activity:FireError", msg); michael@0: delete this.callers[msg.id]; michael@0: break; michael@0: michael@0: case "Activities:Register": michael@0: let self = this; michael@0: this.db.add(msg, michael@0: function onSuccess(aEvent) { michael@0: mm.sendAsyncMessage("Activities:Register:OK", null); michael@0: let res = []; michael@0: msg.forEach(function(aActivity) { michael@0: self.updateContentTypeList(aActivity, res); michael@0: }); michael@0: if (res.length) { michael@0: ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes", michael@0: { contentTypes: res }); michael@0: } michael@0: }, michael@0: function onError(aEvent) { michael@0: msg.error = "REGISTER_ERROR"; michael@0: mm.sendAsyncMessage("Activities:Register:KO", msg); michael@0: }); michael@0: break; michael@0: case "Activities:Unregister": michael@0: this.db.remove(msg); michael@0: let res = []; michael@0: msg.forEach(function(aActivity) { michael@0: this.updateContentTypeList(aActivity, res); michael@0: }, this); michael@0: if (res.length) { michael@0: ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes", michael@0: { contentTypes: res }); michael@0: } michael@0: break; michael@0: case "Activities:GetContentTypes": michael@0: this.sendContentTypes(mm); michael@0: break; michael@0: case "child-process-shutdown": michael@0: for (let id in this.callers) { michael@0: if (this.callers[id].childMM == mm) { michael@0: this.callers[id].mm.sendAsyncMessage("Activity:FireError", { michael@0: "id": id, michael@0: "error": "ActivityCanceled" michael@0: }); michael@0: delete this.callers[id]; michael@0: break; michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: updateContentTypeList: function updateContentTypeList(aActivity, aResult) { michael@0: // Bail out if this is not a "view" activity. michael@0: if (aActivity.name != "view") { michael@0: return; michael@0: } michael@0: michael@0: let types = aActivity.description.filters.type; michael@0: if (typeof types == "string") { michael@0: types = [types]; michael@0: } michael@0: michael@0: // Check that this is a real content type and sanitize it. michael@0: types.forEach(function(aContentType) { michael@0: let hadCharset = { }; michael@0: let charset = { }; michael@0: let contentType = michael@0: NetUtil.parseContentType(aContentType, charset, hadCharset); michael@0: if (contentType) { michael@0: aResult.push(contentType); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: sendContentTypes: function sendContentTypes(aMm) { michael@0: let res = []; michael@0: let self = this; michael@0: this.db.find({ options: { name: "view" } }, michael@0: function() { // Success callback. michael@0: if (res.length) { michael@0: aMm.sendAsyncMessage("Activities:RegisterContentTypes", michael@0: { contentTypes: res }); michael@0: } michael@0: }, michael@0: null, // Error callback. michael@0: function(aActivity) { // Matching callback. michael@0: self.updateContentTypeList(aActivity, res) michael@0: return false; michael@0: } michael@0: ); michael@0: } michael@0: } michael@0: michael@0: Activities.init();