1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/activities/src/ActivitiesService.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,414 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict" 1.9 + 1.10 +const Cu = Components.utils; 1.11 +const Cc = Components.classes; 1.12 +const Ci = Components.interfaces; 1.13 + 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); 1.17 + 1.18 +XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter", 1.19 + "resource://gre/modules/ActivitiesServiceFilter.jsm"); 1.20 + 1.21 +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", 1.22 + "@mozilla.org/parentprocessmessagemanager;1", 1.23 + "nsIMessageBroadcaster"); 1.24 + 1.25 +XPCOMUtils.defineLazyServiceGetter(this, "NetUtil", 1.26 + "@mozilla.org/network/util;1", 1.27 + "nsINetUtil"); 1.28 + 1.29 +this.EXPORTED_SYMBOLS = []; 1.30 + 1.31 +function debug(aMsg) { 1.32 + //dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n"); 1.33 +} 1.34 + 1.35 +const DB_NAME = "activities"; 1.36 +const DB_VERSION = 1; 1.37 +const STORE_NAME = "activities"; 1.38 + 1.39 +function ActivitiesDb() { 1.40 + 1.41 +} 1.42 + 1.43 +ActivitiesDb.prototype = { 1.44 + __proto__: IndexedDBHelper.prototype, 1.45 + 1.46 + init: function actdb_init() { 1.47 + this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]); 1.48 + }, 1.49 + 1.50 + /** 1.51 + * Create the initial database schema. 1.52 + * 1.53 + * The schema of records stored is as follows: 1.54 + * 1.55 + * { 1.56 + * id: String 1.57 + * manifest: String 1.58 + * name: String 1.59 + * icon: String 1.60 + * description: jsval 1.61 + * } 1.62 + */ 1.63 + upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { 1.64 + debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion); 1.65 + let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" }); 1.66 + 1.67 + // indexes 1.68 + objectStore.createIndex("name", "name", { unique: false }); 1.69 + objectStore.createIndex("manifest", "manifest", { unique: false }); 1.70 + 1.71 + debug("Created object stores and indexes"); 1.72 + }, 1.73 + 1.74 + // unique ids made of (uri, action) 1.75 + createId: function actdb_createId(aObject) { 1.76 + let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.77 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.78 + converter.charset = "UTF-8"; 1.79 + 1.80 + let hasher = Cc["@mozilla.org/security/hash;1"] 1.81 + .createInstance(Ci.nsICryptoHash); 1.82 + hasher.init(hasher.SHA1); 1.83 + 1.84 + // add uri and action to the hash 1.85 + ["manifest", "name"].forEach(function(aProp) { 1.86 + let data = converter.convertToByteArray(aObject[aProp], {}); 1.87 + hasher.update(data, data.length); 1.88 + }); 1.89 + 1.90 + return hasher.finish(true); 1.91 + }, 1.92 + 1.93 + // Add all the activities carried in the |aObjects| array. 1.94 + add: function actdb_add(aObjects, aSuccess, aError) { 1.95 + this.newTxn("readwrite", STORE_NAME, function (txn, store) { 1.96 + aObjects.forEach(function (aObject) { 1.97 + let object = { 1.98 + manifest: aObject.manifest, 1.99 + name: aObject.name, 1.100 + icon: aObject.icon || "", 1.101 + description: aObject.description 1.102 + }; 1.103 + object.id = this.createId(object); 1.104 + debug("Going to add " + JSON.stringify(object)); 1.105 + store.put(object); 1.106 + }, this); 1.107 + }.bind(this), aSuccess, aError); 1.108 + }, 1.109 + 1.110 + // Remove all the activities carried in the |aObjects| array. 1.111 + remove: function actdb_remove(aObjects) { 1.112 + this.newTxn("readwrite", STORE_NAME, function (txn, store) { 1.113 + aObjects.forEach(function (aObject) { 1.114 + let object = { 1.115 + manifest: aObject.manifest, 1.116 + name: aObject.name 1.117 + }; 1.118 + debug("Going to remove " + JSON.stringify(object)); 1.119 + store.delete(this.createId(object)); 1.120 + }, this); 1.121 + }.bind(this), function() {}, function() {}); 1.122 + }, 1.123 + 1.124 + find: function actdb_find(aObject, aSuccess, aError, aMatch) { 1.125 + debug("Looking for " + aObject.options.name); 1.126 + 1.127 + this.newTxn("readonly", STORE_NAME, function (txn, store) { 1.128 + let index = store.index("name"); 1.129 + let request = index.mozGetAll(aObject.options.name); 1.130 + request.onsuccess = function findSuccess(aEvent) { 1.131 + debug("Request successful. Record count: " + aEvent.target.result.length); 1.132 + if (!txn.result) { 1.133 + txn.result = { 1.134 + name: aObject.options.name, 1.135 + options: [] 1.136 + }; 1.137 + } 1.138 + 1.139 + aEvent.target.result.forEach(function(result) { 1.140 + if (!aMatch(result)) 1.141 + return; 1.142 + 1.143 + txn.result.options.push({ 1.144 + manifest: result.manifest, 1.145 + icon: result.icon, 1.146 + description: result.description 1.147 + }); 1.148 + }); 1.149 + } 1.150 + }.bind(this), aSuccess, aError); 1.151 + } 1.152 +} 1.153 + 1.154 +let Activities = { 1.155 + messages: [ 1.156 + // ActivityProxy.js 1.157 + "Activity:Start", 1.158 + 1.159 + // ActivityWrapper.js 1.160 + "Activity:Ready", 1.161 + 1.162 + // ActivityRequestHandler.js 1.163 + "Activity:PostResult", 1.164 + "Activity:PostError", 1.165 + 1.166 + "Activities:Register", 1.167 + "Activities:Unregister", 1.168 + "Activities:GetContentTypes", 1.169 + 1.170 + "child-process-shutdown" 1.171 + ], 1.172 + 1.173 + init: function activities_init() { 1.174 + this.messages.forEach(function(msgName) { 1.175 + ppmm.addMessageListener(msgName, this); 1.176 + }, this); 1.177 + 1.178 + Services.obs.addObserver(this, "xpcom-shutdown", false); 1.179 + 1.180 + this.db = new ActivitiesDb(); 1.181 + this.db.init(); 1.182 + this.callers = {}; 1.183 + }, 1.184 + 1.185 + observe: function activities_observe(aSubject, aTopic, aData) { 1.186 + this.messages.forEach(function(msgName) { 1.187 + ppmm.removeMessageListener(msgName, this); 1.188 + }, this); 1.189 + ppmm = null; 1.190 + 1.191 + if (this.db) { 1.192 + this.db.close(); 1.193 + this.db = null; 1.194 + } 1.195 + 1.196 + Services.obs.removeObserver(this, "xpcom-shutdown"); 1.197 + }, 1.198 + 1.199 + /** 1.200 + * Starts an activity by doing: 1.201 + * - finds a list of matching activities. 1.202 + * - calls the UI glue to get the user choice. 1.203 + * - fire an system message of type "activity" to this app, sending the 1.204 + * activity data as a payload. 1.205 + */ 1.206 + startActivity: function activities_startActivity(aMsg) { 1.207 + debug("StartActivity: " + JSON.stringify(aMsg)); 1.208 + 1.209 + let successCb = function successCb(aResults) { 1.210 + debug(JSON.stringify(aResults)); 1.211 + 1.212 + // We have no matching activity registered, let's fire an error. 1.213 + if (aResults.options.length === 0) { 1.214 + Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", { 1.215 + "id": aMsg.id, 1.216 + "error": "NO_PROVIDER" 1.217 + }); 1.218 + delete Activities.callers[aMsg.id]; 1.219 + return; 1.220 + } 1.221 + 1.222 + function getActivityChoice(aChoice) { 1.223 + debug("Activity choice: " + aChoice); 1.224 + 1.225 + // The user has cancelled the choice, fire an error. 1.226 + if (aChoice === -1) { 1.227 + Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", { 1.228 + "id": aMsg.id, 1.229 + "error": "ActivityCanceled" 1.230 + }); 1.231 + delete Activities.callers[aMsg.id]; 1.232 + return; 1.233 + } 1.234 + 1.235 + let sysmm = Cc["@mozilla.org/system-message-internal;1"] 1.236 + .getService(Ci.nsISystemMessagesInternal); 1.237 + if (!sysmm) { 1.238 + // System message is not present, what should we do? 1.239 + delete Activities.callers[aMsg.id]; 1.240 + return; 1.241 + } 1.242 + 1.243 + debug("Sending system message..."); 1.244 + let result = aResults.options[aChoice]; 1.245 + sysmm.sendMessage("activity", { 1.246 + "id": aMsg.id, 1.247 + "payload": aMsg.options, 1.248 + "target": result.description 1.249 + }, 1.250 + Services.io.newURI(result.description.href, null, null), 1.251 + Services.io.newURI(result.manifest, null, null), 1.252 + { 1.253 + "manifestURL": Activities.callers[aMsg.id].manifestURL, 1.254 + "pageURL": Activities.callers[aMsg.id].pageURL 1.255 + }); 1.256 + 1.257 + if (!result.description.returnValue) { 1.258 + Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", { 1.259 + "id": aMsg.id, 1.260 + "result": null 1.261 + }); 1.262 + // No need to notify observers, since we don't want the caller 1.263 + // to be raised on the foreground that quick. 1.264 + delete Activities.callers[aMsg.id]; 1.265 + } 1.266 + }; 1.267 + 1.268 + let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"] 1.269 + .createInstance(Ci.nsIActivityUIGlue); 1.270 + glue.chooseActivity(aResults.name, aResults.options, getActivityChoice); 1.271 + }; 1.272 + 1.273 + let errorCb = function errorCb(aError) { 1.274 + // Something unexpected happened. Should we send an error back? 1.275 + debug("Error in startActivity: " + aError + "\n"); 1.276 + }; 1.277 + 1.278 + let matchFunc = function matchFunc(aResult) { 1.279 + return ActivitiesServiceFilter.match(aMsg.options.data, 1.280 + aResult.description.filters); 1.281 + }; 1.282 + 1.283 + this.db.find(aMsg, successCb, errorCb, matchFunc); 1.284 + }, 1.285 + 1.286 + receiveMessage: function activities_receiveMessage(aMessage) { 1.287 + let mm = aMessage.target; 1.288 + let msg = aMessage.json; 1.289 + 1.290 + let caller; 1.291 + let obsData; 1.292 + 1.293 + if (aMessage.name == "Activity:PostResult" || 1.294 + aMessage.name == "Activity:PostError" || 1.295 + aMessage.name == "Activity:Ready") { 1.296 + caller = this.callers[msg.id]; 1.297 + if (!caller) { 1.298 + debug("!! caller is null for msg.id=" + msg.id); 1.299 + return; 1.300 + } 1.301 + obsData = JSON.stringify({ manifestURL: caller.manifestURL, 1.302 + pageURL: caller.pageURL, 1.303 + success: aMessage.name == "Activity:PostResult" }); 1.304 + } 1.305 + 1.306 + switch(aMessage.name) { 1.307 + case "Activity:Start": 1.308 + this.callers[msg.id] = { mm: mm, 1.309 + manifestURL: msg.manifestURL, 1.310 + pageURL: msg.pageURL }; 1.311 + this.startActivity(msg); 1.312 + break; 1.313 + 1.314 + case "Activity:Ready": 1.315 + caller.childMM = mm; 1.316 + break; 1.317 + 1.318 + case "Activity:PostResult": 1.319 + caller.mm.sendAsyncMessage("Activity:FireSuccess", msg); 1.320 + delete this.callers[msg.id]; 1.321 + break; 1.322 + case "Activity:PostError": 1.323 + caller.mm.sendAsyncMessage("Activity:FireError", msg); 1.324 + delete this.callers[msg.id]; 1.325 + break; 1.326 + 1.327 + case "Activities:Register": 1.328 + let self = this; 1.329 + this.db.add(msg, 1.330 + function onSuccess(aEvent) { 1.331 + mm.sendAsyncMessage("Activities:Register:OK", null); 1.332 + let res = []; 1.333 + msg.forEach(function(aActivity) { 1.334 + self.updateContentTypeList(aActivity, res); 1.335 + }); 1.336 + if (res.length) { 1.337 + ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes", 1.338 + { contentTypes: res }); 1.339 + } 1.340 + }, 1.341 + function onError(aEvent) { 1.342 + msg.error = "REGISTER_ERROR"; 1.343 + mm.sendAsyncMessage("Activities:Register:KO", msg); 1.344 + }); 1.345 + break; 1.346 + case "Activities:Unregister": 1.347 + this.db.remove(msg); 1.348 + let res = []; 1.349 + msg.forEach(function(aActivity) { 1.350 + this.updateContentTypeList(aActivity, res); 1.351 + }, this); 1.352 + if (res.length) { 1.353 + ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes", 1.354 + { contentTypes: res }); 1.355 + } 1.356 + break; 1.357 + case "Activities:GetContentTypes": 1.358 + this.sendContentTypes(mm); 1.359 + break; 1.360 + case "child-process-shutdown": 1.361 + for (let id in this.callers) { 1.362 + if (this.callers[id].childMM == mm) { 1.363 + this.callers[id].mm.sendAsyncMessage("Activity:FireError", { 1.364 + "id": id, 1.365 + "error": "ActivityCanceled" 1.366 + }); 1.367 + delete this.callers[id]; 1.368 + break; 1.369 + } 1.370 + } 1.371 + break; 1.372 + } 1.373 + }, 1.374 + 1.375 + updateContentTypeList: function updateContentTypeList(aActivity, aResult) { 1.376 + // Bail out if this is not a "view" activity. 1.377 + if (aActivity.name != "view") { 1.378 + return; 1.379 + } 1.380 + 1.381 + let types = aActivity.description.filters.type; 1.382 + if (typeof types == "string") { 1.383 + types = [types]; 1.384 + } 1.385 + 1.386 + // Check that this is a real content type and sanitize it. 1.387 + types.forEach(function(aContentType) { 1.388 + let hadCharset = { }; 1.389 + let charset = { }; 1.390 + let contentType = 1.391 + NetUtil.parseContentType(aContentType, charset, hadCharset); 1.392 + if (contentType) { 1.393 + aResult.push(contentType); 1.394 + } 1.395 + }); 1.396 + }, 1.397 + 1.398 + sendContentTypes: function sendContentTypes(aMm) { 1.399 + let res = []; 1.400 + let self = this; 1.401 + this.db.find({ options: { name: "view" } }, 1.402 + function() { // Success callback. 1.403 + if (res.length) { 1.404 + aMm.sendAsyncMessage("Activities:RegisterContentTypes", 1.405 + { contentTypes: res }); 1.406 + } 1.407 + }, 1.408 + null, // Error callback. 1.409 + function(aActivity) { // Matching callback. 1.410 + self.updateContentTypeList(aActivity, res) 1.411 + return false; 1.412 + } 1.413 + ); 1.414 + } 1.415 +} 1.416 + 1.417 +Activities.init();