dom/activities/src/ActivitiesService.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:c5ec7db019b0
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/. */
4
5 "use strict"
6
7 const Cu = Components.utils;
8 const Cc = Components.classes;
9 const Ci = Components.interfaces;
10
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
14
15 XPCOMUtils.defineLazyModuleGetter(this, "ActivitiesServiceFilter",
16 "resource://gre/modules/ActivitiesServiceFilter.jsm");
17
18 XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
19 "@mozilla.org/parentprocessmessagemanager;1",
20 "nsIMessageBroadcaster");
21
22 XPCOMUtils.defineLazyServiceGetter(this, "NetUtil",
23 "@mozilla.org/network/util;1",
24 "nsINetUtil");
25
26 this.EXPORTED_SYMBOLS = [];
27
28 function debug(aMsg) {
29 //dump("-- ActivitiesService.jsm " + Date.now() + " " + aMsg + "\n");
30 }
31
32 const DB_NAME = "activities";
33 const DB_VERSION = 1;
34 const STORE_NAME = "activities";
35
36 function ActivitiesDb() {
37
38 }
39
40 ActivitiesDb.prototype = {
41 __proto__: IndexedDBHelper.prototype,
42
43 init: function actdb_init() {
44 this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]);
45 },
46
47 /**
48 * Create the initial database schema.
49 *
50 * The schema of records stored is as follows:
51 *
52 * {
53 * id: String
54 * manifest: String
55 * name: String
56 * icon: String
57 * description: jsval
58 * }
59 */
60 upgradeSchema: function actdb_upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
61 debug("Upgrade schema " + aOldVersion + " -> " + aNewVersion);
62 let objectStore = aDb.createObjectStore(STORE_NAME, { keyPath: "id" });
63
64 // indexes
65 objectStore.createIndex("name", "name", { unique: false });
66 objectStore.createIndex("manifest", "manifest", { unique: false });
67
68 debug("Created object stores and indexes");
69 },
70
71 // unique ids made of (uri, action)
72 createId: function actdb_createId(aObject) {
73 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
74 .createInstance(Ci.nsIScriptableUnicodeConverter);
75 converter.charset = "UTF-8";
76
77 let hasher = Cc["@mozilla.org/security/hash;1"]
78 .createInstance(Ci.nsICryptoHash);
79 hasher.init(hasher.SHA1);
80
81 // add uri and action to the hash
82 ["manifest", "name"].forEach(function(aProp) {
83 let data = converter.convertToByteArray(aObject[aProp], {});
84 hasher.update(data, data.length);
85 });
86
87 return hasher.finish(true);
88 },
89
90 // Add all the activities carried in the |aObjects| array.
91 add: function actdb_add(aObjects, aSuccess, aError) {
92 this.newTxn("readwrite", STORE_NAME, function (txn, store) {
93 aObjects.forEach(function (aObject) {
94 let object = {
95 manifest: aObject.manifest,
96 name: aObject.name,
97 icon: aObject.icon || "",
98 description: aObject.description
99 };
100 object.id = this.createId(object);
101 debug("Going to add " + JSON.stringify(object));
102 store.put(object);
103 }, this);
104 }.bind(this), aSuccess, aError);
105 },
106
107 // Remove all the activities carried in the |aObjects| array.
108 remove: function actdb_remove(aObjects) {
109 this.newTxn("readwrite", STORE_NAME, function (txn, store) {
110 aObjects.forEach(function (aObject) {
111 let object = {
112 manifest: aObject.manifest,
113 name: aObject.name
114 };
115 debug("Going to remove " + JSON.stringify(object));
116 store.delete(this.createId(object));
117 }, this);
118 }.bind(this), function() {}, function() {});
119 },
120
121 find: function actdb_find(aObject, aSuccess, aError, aMatch) {
122 debug("Looking for " + aObject.options.name);
123
124 this.newTxn("readonly", STORE_NAME, function (txn, store) {
125 let index = store.index("name");
126 let request = index.mozGetAll(aObject.options.name);
127 request.onsuccess = function findSuccess(aEvent) {
128 debug("Request successful. Record count: " + aEvent.target.result.length);
129 if (!txn.result) {
130 txn.result = {
131 name: aObject.options.name,
132 options: []
133 };
134 }
135
136 aEvent.target.result.forEach(function(result) {
137 if (!aMatch(result))
138 return;
139
140 txn.result.options.push({
141 manifest: result.manifest,
142 icon: result.icon,
143 description: result.description
144 });
145 });
146 }
147 }.bind(this), aSuccess, aError);
148 }
149 }
150
151 let Activities = {
152 messages: [
153 // ActivityProxy.js
154 "Activity:Start",
155
156 // ActivityWrapper.js
157 "Activity:Ready",
158
159 // ActivityRequestHandler.js
160 "Activity:PostResult",
161 "Activity:PostError",
162
163 "Activities:Register",
164 "Activities:Unregister",
165 "Activities:GetContentTypes",
166
167 "child-process-shutdown"
168 ],
169
170 init: function activities_init() {
171 this.messages.forEach(function(msgName) {
172 ppmm.addMessageListener(msgName, this);
173 }, this);
174
175 Services.obs.addObserver(this, "xpcom-shutdown", false);
176
177 this.db = new ActivitiesDb();
178 this.db.init();
179 this.callers = {};
180 },
181
182 observe: function activities_observe(aSubject, aTopic, aData) {
183 this.messages.forEach(function(msgName) {
184 ppmm.removeMessageListener(msgName, this);
185 }, this);
186 ppmm = null;
187
188 if (this.db) {
189 this.db.close();
190 this.db = null;
191 }
192
193 Services.obs.removeObserver(this, "xpcom-shutdown");
194 },
195
196 /**
197 * Starts an activity by doing:
198 * - finds a list of matching activities.
199 * - calls the UI glue to get the user choice.
200 * - fire an system message of type "activity" to this app, sending the
201 * activity data as a payload.
202 */
203 startActivity: function activities_startActivity(aMsg) {
204 debug("StartActivity: " + JSON.stringify(aMsg));
205
206 let successCb = function successCb(aResults) {
207 debug(JSON.stringify(aResults));
208
209 // We have no matching activity registered, let's fire an error.
210 if (aResults.options.length === 0) {
211 Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
212 "id": aMsg.id,
213 "error": "NO_PROVIDER"
214 });
215 delete Activities.callers[aMsg.id];
216 return;
217 }
218
219 function getActivityChoice(aChoice) {
220 debug("Activity choice: " + aChoice);
221
222 // The user has cancelled the choice, fire an error.
223 if (aChoice === -1) {
224 Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireError", {
225 "id": aMsg.id,
226 "error": "ActivityCanceled"
227 });
228 delete Activities.callers[aMsg.id];
229 return;
230 }
231
232 let sysmm = Cc["@mozilla.org/system-message-internal;1"]
233 .getService(Ci.nsISystemMessagesInternal);
234 if (!sysmm) {
235 // System message is not present, what should we do?
236 delete Activities.callers[aMsg.id];
237 return;
238 }
239
240 debug("Sending system message...");
241 let result = aResults.options[aChoice];
242 sysmm.sendMessage("activity", {
243 "id": aMsg.id,
244 "payload": aMsg.options,
245 "target": result.description
246 },
247 Services.io.newURI(result.description.href, null, null),
248 Services.io.newURI(result.manifest, null, null),
249 {
250 "manifestURL": Activities.callers[aMsg.id].manifestURL,
251 "pageURL": Activities.callers[aMsg.id].pageURL
252 });
253
254 if (!result.description.returnValue) {
255 Activities.callers[aMsg.id].mm.sendAsyncMessage("Activity:FireSuccess", {
256 "id": aMsg.id,
257 "result": null
258 });
259 // No need to notify observers, since we don't want the caller
260 // to be raised on the foreground that quick.
261 delete Activities.callers[aMsg.id];
262 }
263 };
264
265 let glue = Cc["@mozilla.org/dom/activities/ui-glue;1"]
266 .createInstance(Ci.nsIActivityUIGlue);
267 glue.chooseActivity(aResults.name, aResults.options, getActivityChoice);
268 };
269
270 let errorCb = function errorCb(aError) {
271 // Something unexpected happened. Should we send an error back?
272 debug("Error in startActivity: " + aError + "\n");
273 };
274
275 let matchFunc = function matchFunc(aResult) {
276 return ActivitiesServiceFilter.match(aMsg.options.data,
277 aResult.description.filters);
278 };
279
280 this.db.find(aMsg, successCb, errorCb, matchFunc);
281 },
282
283 receiveMessage: function activities_receiveMessage(aMessage) {
284 let mm = aMessage.target;
285 let msg = aMessage.json;
286
287 let caller;
288 let obsData;
289
290 if (aMessage.name == "Activity:PostResult" ||
291 aMessage.name == "Activity:PostError" ||
292 aMessage.name == "Activity:Ready") {
293 caller = this.callers[msg.id];
294 if (!caller) {
295 debug("!! caller is null for msg.id=" + msg.id);
296 return;
297 }
298 obsData = JSON.stringify({ manifestURL: caller.manifestURL,
299 pageURL: caller.pageURL,
300 success: aMessage.name == "Activity:PostResult" });
301 }
302
303 switch(aMessage.name) {
304 case "Activity:Start":
305 this.callers[msg.id] = { mm: mm,
306 manifestURL: msg.manifestURL,
307 pageURL: msg.pageURL };
308 this.startActivity(msg);
309 break;
310
311 case "Activity:Ready":
312 caller.childMM = mm;
313 break;
314
315 case "Activity:PostResult":
316 caller.mm.sendAsyncMessage("Activity:FireSuccess", msg);
317 delete this.callers[msg.id];
318 break;
319 case "Activity:PostError":
320 caller.mm.sendAsyncMessage("Activity:FireError", msg);
321 delete this.callers[msg.id];
322 break;
323
324 case "Activities:Register":
325 let self = this;
326 this.db.add(msg,
327 function onSuccess(aEvent) {
328 mm.sendAsyncMessage("Activities:Register:OK", null);
329 let res = [];
330 msg.forEach(function(aActivity) {
331 self.updateContentTypeList(aActivity, res);
332 });
333 if (res.length) {
334 ppmm.broadcastAsyncMessage("Activities:RegisterContentTypes",
335 { contentTypes: res });
336 }
337 },
338 function onError(aEvent) {
339 msg.error = "REGISTER_ERROR";
340 mm.sendAsyncMessage("Activities:Register:KO", msg);
341 });
342 break;
343 case "Activities:Unregister":
344 this.db.remove(msg);
345 let res = [];
346 msg.forEach(function(aActivity) {
347 this.updateContentTypeList(aActivity, res);
348 }, this);
349 if (res.length) {
350 ppmm.broadcastAsyncMessage("Activities:UnregisterContentTypes",
351 { contentTypes: res });
352 }
353 break;
354 case "Activities:GetContentTypes":
355 this.sendContentTypes(mm);
356 break;
357 case "child-process-shutdown":
358 for (let id in this.callers) {
359 if (this.callers[id].childMM == mm) {
360 this.callers[id].mm.sendAsyncMessage("Activity:FireError", {
361 "id": id,
362 "error": "ActivityCanceled"
363 });
364 delete this.callers[id];
365 break;
366 }
367 }
368 break;
369 }
370 },
371
372 updateContentTypeList: function updateContentTypeList(aActivity, aResult) {
373 // Bail out if this is not a "view" activity.
374 if (aActivity.name != "view") {
375 return;
376 }
377
378 let types = aActivity.description.filters.type;
379 if (typeof types == "string") {
380 types = [types];
381 }
382
383 // Check that this is a real content type and sanitize it.
384 types.forEach(function(aContentType) {
385 let hadCharset = { };
386 let charset = { };
387 let contentType =
388 NetUtil.parseContentType(aContentType, charset, hadCharset);
389 if (contentType) {
390 aResult.push(contentType);
391 }
392 });
393 },
394
395 sendContentTypes: function sendContentTypes(aMm) {
396 let res = [];
397 let self = this;
398 this.db.find({ options: { name: "view" } },
399 function() { // Success callback.
400 if (res.length) {
401 aMm.sendAsyncMessage("Activities:RegisterContentTypes",
402 { contentTypes: res });
403 }
404 },
405 null, // Error callback.
406 function(aActivity) { // Matching callback.
407 self.updateContentTypeList(aActivity, res)
408 return false;
409 }
410 );
411 }
412 }
413
414 Activities.init();

mercurial