1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/modules/Home.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,466 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +this.EXPORTED_SYMBOLS = ["Home"]; 1.12 + 1.13 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.14 + 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/SharedPreferences.jsm"); 1.17 +Cu.import("resource://gre/modules/Messaging.jsm"); 1.18 + 1.19 +// Keep this in sync with the constant defined in PanelAuthCache.java 1.20 +const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_"; 1.21 + 1.22 +// See bug 915424 1.23 +function resolveGeckoURI(aURI) { 1.24 + if (!aURI) 1.25 + throw "Can't resolve an empty uri"; 1.26 + 1.27 + if (aURI.startsWith("chrome://")) { 1.28 + let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); 1.29 + return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; 1.30 + } else if (aURI.startsWith("resource://")) { 1.31 + let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); 1.32 + return handler.resolveURI(Services.io.newURI(aURI, null, null)); 1.33 + } 1.34 + return aURI; 1.35 +} 1.36 + 1.37 +function BannerMessage(options) { 1.38 + let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); 1.39 + this.id = uuidgen.generateUUID().toString(); 1.40 + 1.41 + if ("text" in options && options.text != null) 1.42 + this.text = options.text; 1.43 + 1.44 + if ("icon" in options && options.icon != null) 1.45 + this.iconURI = resolveGeckoURI(options.icon); 1.46 + 1.47 + if ("onshown" in options && typeof options.onshown === "function") 1.48 + this.onshown = options.onshown; 1.49 + 1.50 + if ("onclick" in options && typeof options.onclick === "function") 1.51 + this.onclick = options.onclick; 1.52 + 1.53 + if ("ondismiss" in options && typeof options.ondismiss === "function") 1.54 + this.ondismiss = options.ondismiss; 1.55 +} 1.56 + 1.57 +// We need this object to have access to the HomeBanner 1.58 +// private members without leaking it outside Home.jsm. 1.59 +let HomeBannerMessageHandlers; 1.60 + 1.61 +let HomeBanner = (function () { 1.62 + // Whether there is a "HomeBanner:Get" request we couldn't fulfill. 1.63 + let _pendingRequest = false; 1.64 + 1.65 + // Functions used to handle messages sent from Java. 1.66 + HomeBannerMessageHandlers = { 1.67 + "HomeBanner:Get": function handleBannerGet(data) { 1.68 + if (!_sendBannerData()) { 1.69 + _pendingRequest = true; 1.70 + } 1.71 + } 1.72 + }; 1.73 + 1.74 + // Holds the messages that will rotate through the banner. 1.75 + let _messages = {}; 1.76 + 1.77 + let _sendBannerData = function() { 1.78 + let keys = Object.keys(_messages); 1.79 + if (!keys.length) { 1.80 + return false; 1.81 + } 1.82 + 1.83 + // Choose a message at random. 1.84 + let randomId = keys[Math.floor(Math.random() * keys.length)]; 1.85 + let message = _messages[randomId]; 1.86 + 1.87 + sendMessageToJava({ 1.88 + type: "HomeBanner:Data", 1.89 + id: message.id, 1.90 + text: message.text, 1.91 + iconURI: message.iconURI 1.92 + }); 1.93 + return true; 1.94 + }; 1.95 + 1.96 + let _handleShown = function(id) { 1.97 + let message = _messages[id]; 1.98 + if (message.onshown) 1.99 + message.onshown(); 1.100 + }; 1.101 + 1.102 + let _handleClick = function(id) { 1.103 + let message = _messages[id]; 1.104 + if (message.onclick) 1.105 + message.onclick(); 1.106 + }; 1.107 + 1.108 + let _handleDismiss = function(id) { 1.109 + let message = _messages[id]; 1.110 + if (message.ondismiss) 1.111 + message.ondismiss(); 1.112 + }; 1.113 + 1.114 + return Object.freeze({ 1.115 + observe: function(subject, topic, data) { 1.116 + switch(topic) { 1.117 + case "HomeBanner:Shown": 1.118 + _handleShown(data); 1.119 + break; 1.120 + 1.121 + case "HomeBanner:Click": 1.122 + _handleClick(data); 1.123 + break; 1.124 + 1.125 + case "HomeBanner:Dismiss": 1.126 + _handleDismiss(data); 1.127 + break; 1.128 + } 1.129 + }, 1.130 + 1.131 + /** 1.132 + * Adds a new banner message to the rotation. 1.133 + * 1.134 + * @return id Unique identifer for the message. 1.135 + */ 1.136 + add: function(options) { 1.137 + let message = new BannerMessage(options); 1.138 + _messages[message.id] = message; 1.139 + 1.140 + // If this is the first message we're adding, add 1.141 + // observers to listen for requests from the Java UI. 1.142 + if (Object.keys(_messages).length == 1) { 1.143 + Services.obs.addObserver(this, "HomeBanner:Shown", false); 1.144 + Services.obs.addObserver(this, "HomeBanner:Click", false); 1.145 + Services.obs.addObserver(this, "HomeBanner:Dismiss", false); 1.146 + 1.147 + // Send a message to Java if there's a pending "HomeBanner:Get" request. 1.148 + if (_pendingRequest) { 1.149 + _pendingRequest = false; 1.150 + _sendBannerData(); 1.151 + } 1.152 + } 1.153 + 1.154 + return message.id; 1.155 + }, 1.156 + 1.157 + /** 1.158 + * Removes a banner message from the rotation. 1.159 + * 1.160 + * @param id The id of the message to remove. 1.161 + */ 1.162 + remove: function(id) { 1.163 + if (!(id in _messages)) { 1.164 + throw "Home.banner: Can't remove message that doesn't exist: id = " + id; 1.165 + } 1.166 + 1.167 + delete _messages[id]; 1.168 + 1.169 + // If there are no more messages, remove the observers. 1.170 + if (Object.keys(_messages).length == 0) { 1.171 + Services.obs.removeObserver(this, "HomeBanner:Shown"); 1.172 + Services.obs.removeObserver(this, "HomeBanner:Click"); 1.173 + Services.obs.removeObserver(this, "HomeBanner:Dismiss"); 1.174 + } 1.175 + } 1.176 + }); 1.177 +})(); 1.178 + 1.179 +// We need this object to have access to the HomePanels 1.180 +// private members without leaking it outside Home.jsm. 1.181 +let HomePanelsMessageHandlers; 1.182 + 1.183 +let HomePanels = (function () { 1.184 + // Functions used to handle messages sent from Java. 1.185 + HomePanelsMessageHandlers = { 1.186 + 1.187 + "HomePanels:Get": function handlePanelsGet(data) { 1.188 + data = JSON.parse(data); 1.189 + 1.190 + let requestId = data.requestId; 1.191 + let ids = data.ids || null; 1.192 + 1.193 + let panels = []; 1.194 + for (let id in _registeredPanels) { 1.195 + // Null ids means we want to fetch all available panels 1.196 + if (ids == null || ids.indexOf(id) >= 0) { 1.197 + try { 1.198 + panels.push(_generatePanel(id)); 1.199 + } catch(e) { 1.200 + Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); 1.201 + } 1.202 + } 1.203 + } 1.204 + 1.205 + sendMessageToJava({ 1.206 + type: "HomePanels:Data", 1.207 + panels: panels, 1.208 + requestId: requestId 1.209 + }); 1.210 + }, 1.211 + 1.212 + "HomePanels:Authenticate": function handlePanelsAuthenticate(id) { 1.213 + // Generate panel options to get auth handler. 1.214 + let options = _registeredPanels[id](); 1.215 + if (!options.auth) { 1.216 + throw "Home.panels: Invalid auth for panel.id = " + id; 1.217 + } 1.218 + if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") { 1.219 + throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id; 1.220 + } 1.221 + options.auth.authenticate(); 1.222 + }, 1.223 + 1.224 + "HomePanels:RefreshView": function handlePanelsRefreshView(data) { 1.225 + data = JSON.parse(data); 1.226 + 1.227 + let options = _registeredPanels[data.panelId](); 1.228 + let view = options.views[data.viewIndex]; 1.229 + 1.230 + if (!view) { 1.231 + throw "Home.panels: Invalid view for panel.id = " + data.panelId 1.232 + + ", view.index = " + data.viewIndex; 1.233 + } 1.234 + 1.235 + if (!view.onrefresh || typeof view.onrefresh !== "function") { 1.236 + throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId 1.237 + + ", view.index = " + data.viewIndex; 1.238 + } 1.239 + 1.240 + view.onrefresh(); 1.241 + }, 1.242 + 1.243 + "HomePanels:Installed": function handlePanelsInstalled(id) { 1.244 + _assertPanelExists(id); 1.245 + 1.246 + let options = _registeredPanels[id](); 1.247 + if (!options.oninstall) { 1.248 + return; 1.249 + } 1.250 + if (typeof options.oninstall !== "function") { 1.251 + throw "Home.panels: Invalid oninstall function: panel.id = " + this.id; 1.252 + } 1.253 + options.oninstall(); 1.254 + }, 1.255 + 1.256 + "HomePanels:Uninstalled": function handlePanelsUninstalled(id) { 1.257 + _assertPanelExists(id); 1.258 + 1.259 + let options = _registeredPanels[id](); 1.260 + if (!options.onuninstall) { 1.261 + return; 1.262 + } 1.263 + if (typeof options.onuninstall !== "function") { 1.264 + throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id; 1.265 + } 1.266 + options.onuninstall(); 1.267 + } 1.268 + }; 1.269 + 1.270 + // Holds the current set of registered panels that can be 1.271 + // installed, updated, uninstalled, or unregistered. It maps 1.272 + // panel ids with the functions that dynamically generate 1.273 + // their respective panel options. This is used to retrieve 1.274 + // the current list of available panels in the system. 1.275 + // See HomePanels:Get handler. 1.276 + let _registeredPanels = {}; 1.277 + 1.278 + // Valid layouts for a panel. 1.279 + let Layout = Object.freeze({ 1.280 + FRAME: "frame" 1.281 + }); 1.282 + 1.283 + // Valid types of views for a dataset. 1.284 + let View = Object.freeze({ 1.285 + LIST: "list", 1.286 + GRID: "grid" 1.287 + }); 1.288 + 1.289 + // Valid item types for a panel view. 1.290 + let Item = Object.freeze({ 1.291 + ARTICLE: "article", 1.292 + IMAGE: "image" 1.293 + }); 1.294 + 1.295 + // Valid item handlers for a panel view. 1.296 + let ItemHandler = Object.freeze({ 1.297 + BROWSER: "browser", 1.298 + INTENT: "intent" 1.299 + }); 1.300 + 1.301 + function Panel(id, options) { 1.302 + this.id = id; 1.303 + this.title = options.title; 1.304 + this.layout = options.layout; 1.305 + this.views = options.views; 1.306 + 1.307 + if (!this.id || !this.title) { 1.308 + throw "Home.panels: Can't create a home panel without an id and title!"; 1.309 + } 1.310 + 1.311 + if (!this.layout) { 1.312 + // Use FRAME layout by default 1.313 + this.layout = Layout.FRAME; 1.314 + } else if (!_valueExists(Layout, this.layout)) { 1.315 + throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout; 1.316 + } 1.317 + 1.318 + for (let view of this.views) { 1.319 + if (!_valueExists(View, view.type)) { 1.320 + throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type; 1.321 + } 1.322 + 1.323 + if (!view.itemType) { 1.324 + if (view.type == View.LIST) { 1.325 + // Use ARTICLE item type by default in LIST views 1.326 + view.itemType = Item.ARTICLE; 1.327 + } else if (view.type == View.GRID) { 1.328 + // Use IMAGE item type by default in GRID views 1.329 + view.itemType = Item.IMAGE; 1.330 + } 1.331 + } else if (!_valueExists(Item, view.itemType)) { 1.332 + throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType; 1.333 + } 1.334 + 1.335 + if (!view.itemHandler) { 1.336 + // Use BROWSER item handler by default 1.337 + view.itemHandler = ItemHandler.BROWSER; 1.338 + } else if (!_valueExists(ItemHandler, view.itemHandler)) { 1.339 + throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler; 1.340 + } 1.341 + 1.342 + if (!view.dataset) { 1.343 + throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type; 1.344 + } 1.345 + 1.346 + if (view.onrefresh) { 1.347 + view.refreshEnabled = true; 1.348 + } 1.349 + } 1.350 + 1.351 + if (options.auth) { 1.352 + if (!options.auth.messageText) { 1.353 + throw "Home.panels: Invalid auth messageText: panel.id = " + this.id; 1.354 + } 1.355 + if (!options.auth.buttonText) { 1.356 + throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id; 1.357 + } 1.358 + 1.359 + this.authConfig = { 1.360 + messageText: options.auth.messageText, 1.361 + buttonText: options.auth.buttonText 1.362 + }; 1.363 + 1.364 + // Include optional image URL if it is specified. 1.365 + if (options.auth.imageUrl) { 1.366 + this.authConfig.imageUrl = options.auth.imageUrl; 1.367 + } 1.368 + } 1.369 + } 1.370 + 1.371 + let _generatePanel = function(id) { 1.372 + let options = _registeredPanels[id](); 1.373 + return new Panel(id, options); 1.374 + }; 1.375 + 1.376 + // Helper function used to see if a value is in an object. 1.377 + let _valueExists = function(obj, value) { 1.378 + for (let key in obj) { 1.379 + if (obj[key] == value) { 1.380 + return true; 1.381 + } 1.382 + } 1.383 + return false; 1.384 + }; 1.385 + 1.386 + let _assertPanelExists = function(id) { 1.387 + if (!(id in _registeredPanels)) { 1.388 + throw "Home.panels: Panel doesn't exist: id = " + id; 1.389 + } 1.390 + }; 1.391 + 1.392 + return Object.freeze({ 1.393 + Layout: Layout, 1.394 + View: View, 1.395 + Item: Item, 1.396 + ItemHandler: ItemHandler, 1.397 + 1.398 + register: function(id, optionsCallback) { 1.399 + // Bail if the panel already exists 1.400 + if (id in _registeredPanels) { 1.401 + throw "Home.panels: Panel already exists: id = " + id; 1.402 + } 1.403 + 1.404 + if (!optionsCallback || typeof optionsCallback !== "function") { 1.405 + throw "Home.panels: Panel callback must be a function: id = " + id; 1.406 + } 1.407 + 1.408 + _registeredPanels[id] = optionsCallback; 1.409 + }, 1.410 + 1.411 + unregister: function(id) { 1.412 + _assertPanelExists(id); 1.413 + 1.414 + delete _registeredPanels[id]; 1.415 + }, 1.416 + 1.417 + install: function(id) { 1.418 + _assertPanelExists(id); 1.419 + 1.420 + sendMessageToJava({ 1.421 + type: "HomePanels:Install", 1.422 + panel: _generatePanel(id) 1.423 + }); 1.424 + }, 1.425 + 1.426 + uninstall: function(id) { 1.427 + _assertPanelExists(id); 1.428 + 1.429 + sendMessageToJava({ 1.430 + type: "HomePanels:Uninstall", 1.431 + id: id 1.432 + }); 1.433 + }, 1.434 + 1.435 + update: function(id) { 1.436 + _assertPanelExists(id); 1.437 + 1.438 + sendMessageToJava({ 1.439 + type: "HomePanels:Update", 1.440 + panel: _generatePanel(id) 1.441 + }); 1.442 + }, 1.443 + 1.444 + setAuthenticated: function(id, isAuthenticated) { 1.445 + _assertPanelExists(id); 1.446 + 1.447 + let authKey = PREFS_PANEL_AUTH_PREFIX + id; 1.448 + let sharedPrefs = new SharedPreferences(); 1.449 + sharedPrefs.setBoolPref(authKey, isAuthenticated); 1.450 + } 1.451 + }); 1.452 +})(); 1.453 + 1.454 +// Public API 1.455 +this.Home = Object.freeze({ 1.456 + banner: HomeBanner, 1.457 + panels: HomePanels, 1.458 + 1.459 + // Lazy notification observer registered in browser.js 1.460 + observe: function(subject, topic, data) { 1.461 + if (topic in HomeBannerMessageHandlers) { 1.462 + HomeBannerMessageHandlers[topic](data); 1.463 + } else if (topic in HomePanelsMessageHandlers) { 1.464 + HomePanelsMessageHandlers[topic](data); 1.465 + } else { 1.466 + Cu.reportError("Home.observe: message handler not found for topic: " + topic); 1.467 + } 1.468 + } 1.469 +});