mobile/android/modules/Home.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:2c5ab003350d
1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 "use strict";
7
8 this.EXPORTED_SYMBOLS = ["Home"];
9
10 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
11
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/SharedPreferences.jsm");
14 Cu.import("resource://gre/modules/Messaging.jsm");
15
16 // Keep this in sync with the constant defined in PanelAuthCache.java
17 const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_";
18
19 // See bug 915424
20 function resolveGeckoURI(aURI) {
21 if (!aURI)
22 throw "Can't resolve an empty uri";
23
24 if (aURI.startsWith("chrome://")) {
25 let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
26 return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
27 } else if (aURI.startsWith("resource://")) {
28 let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
29 return handler.resolveURI(Services.io.newURI(aURI, null, null));
30 }
31 return aURI;
32 }
33
34 function BannerMessage(options) {
35 let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
36 this.id = uuidgen.generateUUID().toString();
37
38 if ("text" in options && options.text != null)
39 this.text = options.text;
40
41 if ("icon" in options && options.icon != null)
42 this.iconURI = resolveGeckoURI(options.icon);
43
44 if ("onshown" in options && typeof options.onshown === "function")
45 this.onshown = options.onshown;
46
47 if ("onclick" in options && typeof options.onclick === "function")
48 this.onclick = options.onclick;
49
50 if ("ondismiss" in options && typeof options.ondismiss === "function")
51 this.ondismiss = options.ondismiss;
52 }
53
54 // We need this object to have access to the HomeBanner
55 // private members without leaking it outside Home.jsm.
56 let HomeBannerMessageHandlers;
57
58 let HomeBanner = (function () {
59 // Whether there is a "HomeBanner:Get" request we couldn't fulfill.
60 let _pendingRequest = false;
61
62 // Functions used to handle messages sent from Java.
63 HomeBannerMessageHandlers = {
64 "HomeBanner:Get": function handleBannerGet(data) {
65 if (!_sendBannerData()) {
66 _pendingRequest = true;
67 }
68 }
69 };
70
71 // Holds the messages that will rotate through the banner.
72 let _messages = {};
73
74 let _sendBannerData = function() {
75 let keys = Object.keys(_messages);
76 if (!keys.length) {
77 return false;
78 }
79
80 // Choose a message at random.
81 let randomId = keys[Math.floor(Math.random() * keys.length)];
82 let message = _messages[randomId];
83
84 sendMessageToJava({
85 type: "HomeBanner:Data",
86 id: message.id,
87 text: message.text,
88 iconURI: message.iconURI
89 });
90 return true;
91 };
92
93 let _handleShown = function(id) {
94 let message = _messages[id];
95 if (message.onshown)
96 message.onshown();
97 };
98
99 let _handleClick = function(id) {
100 let message = _messages[id];
101 if (message.onclick)
102 message.onclick();
103 };
104
105 let _handleDismiss = function(id) {
106 let message = _messages[id];
107 if (message.ondismiss)
108 message.ondismiss();
109 };
110
111 return Object.freeze({
112 observe: function(subject, topic, data) {
113 switch(topic) {
114 case "HomeBanner:Shown":
115 _handleShown(data);
116 break;
117
118 case "HomeBanner:Click":
119 _handleClick(data);
120 break;
121
122 case "HomeBanner:Dismiss":
123 _handleDismiss(data);
124 break;
125 }
126 },
127
128 /**
129 * Adds a new banner message to the rotation.
130 *
131 * @return id Unique identifer for the message.
132 */
133 add: function(options) {
134 let message = new BannerMessage(options);
135 _messages[message.id] = message;
136
137 // If this is the first message we're adding, add
138 // observers to listen for requests from the Java UI.
139 if (Object.keys(_messages).length == 1) {
140 Services.obs.addObserver(this, "HomeBanner:Shown", false);
141 Services.obs.addObserver(this, "HomeBanner:Click", false);
142 Services.obs.addObserver(this, "HomeBanner:Dismiss", false);
143
144 // Send a message to Java if there's a pending "HomeBanner:Get" request.
145 if (_pendingRequest) {
146 _pendingRequest = false;
147 _sendBannerData();
148 }
149 }
150
151 return message.id;
152 },
153
154 /**
155 * Removes a banner message from the rotation.
156 *
157 * @param id The id of the message to remove.
158 */
159 remove: function(id) {
160 if (!(id in _messages)) {
161 throw "Home.banner: Can't remove message that doesn't exist: id = " + id;
162 }
163
164 delete _messages[id];
165
166 // If there are no more messages, remove the observers.
167 if (Object.keys(_messages).length == 0) {
168 Services.obs.removeObserver(this, "HomeBanner:Shown");
169 Services.obs.removeObserver(this, "HomeBanner:Click");
170 Services.obs.removeObserver(this, "HomeBanner:Dismiss");
171 }
172 }
173 });
174 })();
175
176 // We need this object to have access to the HomePanels
177 // private members without leaking it outside Home.jsm.
178 let HomePanelsMessageHandlers;
179
180 let HomePanels = (function () {
181 // Functions used to handle messages sent from Java.
182 HomePanelsMessageHandlers = {
183
184 "HomePanels:Get": function handlePanelsGet(data) {
185 data = JSON.parse(data);
186
187 let requestId = data.requestId;
188 let ids = data.ids || null;
189
190 let panels = [];
191 for (let id in _registeredPanels) {
192 // Null ids means we want to fetch all available panels
193 if (ids == null || ids.indexOf(id) >= 0) {
194 try {
195 panels.push(_generatePanel(id));
196 } catch(e) {
197 Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e);
198 }
199 }
200 }
201
202 sendMessageToJava({
203 type: "HomePanels:Data",
204 panels: panels,
205 requestId: requestId
206 });
207 },
208
209 "HomePanels:Authenticate": function handlePanelsAuthenticate(id) {
210 // Generate panel options to get auth handler.
211 let options = _registeredPanels[id]();
212 if (!options.auth) {
213 throw "Home.panels: Invalid auth for panel.id = " + id;
214 }
215 if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") {
216 throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id;
217 }
218 options.auth.authenticate();
219 },
220
221 "HomePanels:RefreshView": function handlePanelsRefreshView(data) {
222 data = JSON.parse(data);
223
224 let options = _registeredPanels[data.panelId]();
225 let view = options.views[data.viewIndex];
226
227 if (!view) {
228 throw "Home.panels: Invalid view for panel.id = " + data.panelId
229 + ", view.index = " + data.viewIndex;
230 }
231
232 if (!view.onrefresh || typeof view.onrefresh !== "function") {
233 throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId
234 + ", view.index = " + data.viewIndex;
235 }
236
237 view.onrefresh();
238 },
239
240 "HomePanels:Installed": function handlePanelsInstalled(id) {
241 _assertPanelExists(id);
242
243 let options = _registeredPanels[id]();
244 if (!options.oninstall) {
245 return;
246 }
247 if (typeof options.oninstall !== "function") {
248 throw "Home.panels: Invalid oninstall function: panel.id = " + this.id;
249 }
250 options.oninstall();
251 },
252
253 "HomePanels:Uninstalled": function handlePanelsUninstalled(id) {
254 _assertPanelExists(id);
255
256 let options = _registeredPanels[id]();
257 if (!options.onuninstall) {
258 return;
259 }
260 if (typeof options.onuninstall !== "function") {
261 throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id;
262 }
263 options.onuninstall();
264 }
265 };
266
267 // Holds the current set of registered panels that can be
268 // installed, updated, uninstalled, or unregistered. It maps
269 // panel ids with the functions that dynamically generate
270 // their respective panel options. This is used to retrieve
271 // the current list of available panels in the system.
272 // See HomePanels:Get handler.
273 let _registeredPanels = {};
274
275 // Valid layouts for a panel.
276 let Layout = Object.freeze({
277 FRAME: "frame"
278 });
279
280 // Valid types of views for a dataset.
281 let View = Object.freeze({
282 LIST: "list",
283 GRID: "grid"
284 });
285
286 // Valid item types for a panel view.
287 let Item = Object.freeze({
288 ARTICLE: "article",
289 IMAGE: "image"
290 });
291
292 // Valid item handlers for a panel view.
293 let ItemHandler = Object.freeze({
294 BROWSER: "browser",
295 INTENT: "intent"
296 });
297
298 function Panel(id, options) {
299 this.id = id;
300 this.title = options.title;
301 this.layout = options.layout;
302 this.views = options.views;
303
304 if (!this.id || !this.title) {
305 throw "Home.panels: Can't create a home panel without an id and title!";
306 }
307
308 if (!this.layout) {
309 // Use FRAME layout by default
310 this.layout = Layout.FRAME;
311 } else if (!_valueExists(Layout, this.layout)) {
312 throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout;
313 }
314
315 for (let view of this.views) {
316 if (!_valueExists(View, view.type)) {
317 throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type;
318 }
319
320 if (!view.itemType) {
321 if (view.type == View.LIST) {
322 // Use ARTICLE item type by default in LIST views
323 view.itemType = Item.ARTICLE;
324 } else if (view.type == View.GRID) {
325 // Use IMAGE item type by default in GRID views
326 view.itemType = Item.IMAGE;
327 }
328 } else if (!_valueExists(Item, view.itemType)) {
329 throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType;
330 }
331
332 if (!view.itemHandler) {
333 // Use BROWSER item handler by default
334 view.itemHandler = ItemHandler.BROWSER;
335 } else if (!_valueExists(ItemHandler, view.itemHandler)) {
336 throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler;
337 }
338
339 if (!view.dataset) {
340 throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type;
341 }
342
343 if (view.onrefresh) {
344 view.refreshEnabled = true;
345 }
346 }
347
348 if (options.auth) {
349 if (!options.auth.messageText) {
350 throw "Home.panels: Invalid auth messageText: panel.id = " + this.id;
351 }
352 if (!options.auth.buttonText) {
353 throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id;
354 }
355
356 this.authConfig = {
357 messageText: options.auth.messageText,
358 buttonText: options.auth.buttonText
359 };
360
361 // Include optional image URL if it is specified.
362 if (options.auth.imageUrl) {
363 this.authConfig.imageUrl = options.auth.imageUrl;
364 }
365 }
366 }
367
368 let _generatePanel = function(id) {
369 let options = _registeredPanels[id]();
370 return new Panel(id, options);
371 };
372
373 // Helper function used to see if a value is in an object.
374 let _valueExists = function(obj, value) {
375 for (let key in obj) {
376 if (obj[key] == value) {
377 return true;
378 }
379 }
380 return false;
381 };
382
383 let _assertPanelExists = function(id) {
384 if (!(id in _registeredPanels)) {
385 throw "Home.panels: Panel doesn't exist: id = " + id;
386 }
387 };
388
389 return Object.freeze({
390 Layout: Layout,
391 View: View,
392 Item: Item,
393 ItemHandler: ItemHandler,
394
395 register: function(id, optionsCallback) {
396 // Bail if the panel already exists
397 if (id in _registeredPanels) {
398 throw "Home.panels: Panel already exists: id = " + id;
399 }
400
401 if (!optionsCallback || typeof optionsCallback !== "function") {
402 throw "Home.panels: Panel callback must be a function: id = " + id;
403 }
404
405 _registeredPanels[id] = optionsCallback;
406 },
407
408 unregister: function(id) {
409 _assertPanelExists(id);
410
411 delete _registeredPanels[id];
412 },
413
414 install: function(id) {
415 _assertPanelExists(id);
416
417 sendMessageToJava({
418 type: "HomePanels:Install",
419 panel: _generatePanel(id)
420 });
421 },
422
423 uninstall: function(id) {
424 _assertPanelExists(id);
425
426 sendMessageToJava({
427 type: "HomePanels:Uninstall",
428 id: id
429 });
430 },
431
432 update: function(id) {
433 _assertPanelExists(id);
434
435 sendMessageToJava({
436 type: "HomePanels:Update",
437 panel: _generatePanel(id)
438 });
439 },
440
441 setAuthenticated: function(id, isAuthenticated) {
442 _assertPanelExists(id);
443
444 let authKey = PREFS_PANEL_AUTH_PREFIX + id;
445 let sharedPrefs = new SharedPreferences();
446 sharedPrefs.setBoolPref(authKey, isAuthenticated);
447 }
448 });
449 })();
450
451 // Public API
452 this.Home = Object.freeze({
453 banner: HomeBanner,
454 panels: HomePanels,
455
456 // Lazy notification observer registered in browser.js
457 observe: function(subject, topic, data) {
458 if (topic in HomeBannerMessageHandlers) {
459 HomeBannerMessageHandlers[topic](data);
460 } else if (topic in HomePanelsMessageHandlers) {
461 HomePanelsMessageHandlers[topic](data);
462 } else {
463 Cu.reportError("Home.observe: message handler not found for topic: " + topic);
464 }
465 }
466 });

mercurial