michael@0: // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 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: this.EXPORTED_SYMBOLS = ["Home"]; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/SharedPreferences.jsm"); michael@0: Cu.import("resource://gre/modules/Messaging.jsm"); michael@0: michael@0: // Keep this in sync with the constant defined in PanelAuthCache.java michael@0: const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_"; michael@0: michael@0: // See bug 915424 michael@0: function resolveGeckoURI(aURI) { michael@0: if (!aURI) michael@0: throw "Can't resolve an empty uri"; michael@0: michael@0: if (aURI.startsWith("chrome://")) { michael@0: let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); michael@0: return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; michael@0: } else if (aURI.startsWith("resource://")) { michael@0: let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); michael@0: return handler.resolveURI(Services.io.newURI(aURI, null, null)); michael@0: } michael@0: return aURI; michael@0: } michael@0: michael@0: function BannerMessage(options) { michael@0: let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); michael@0: this.id = uuidgen.generateUUID().toString(); michael@0: michael@0: if ("text" in options && options.text != null) michael@0: this.text = options.text; michael@0: michael@0: if ("icon" in options && options.icon != null) michael@0: this.iconURI = resolveGeckoURI(options.icon); michael@0: michael@0: if ("onshown" in options && typeof options.onshown === "function") michael@0: this.onshown = options.onshown; michael@0: michael@0: if ("onclick" in options && typeof options.onclick === "function") michael@0: this.onclick = options.onclick; michael@0: michael@0: if ("ondismiss" in options && typeof options.ondismiss === "function") michael@0: this.ondismiss = options.ondismiss; michael@0: } michael@0: michael@0: // We need this object to have access to the HomeBanner michael@0: // private members without leaking it outside Home.jsm. michael@0: let HomeBannerMessageHandlers; michael@0: michael@0: let HomeBanner = (function () { michael@0: // Whether there is a "HomeBanner:Get" request we couldn't fulfill. michael@0: let _pendingRequest = false; michael@0: michael@0: // Functions used to handle messages sent from Java. michael@0: HomeBannerMessageHandlers = { michael@0: "HomeBanner:Get": function handleBannerGet(data) { michael@0: if (!_sendBannerData()) { michael@0: _pendingRequest = true; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Holds the messages that will rotate through the banner. michael@0: let _messages = {}; michael@0: michael@0: let _sendBannerData = function() { michael@0: let keys = Object.keys(_messages); michael@0: if (!keys.length) { michael@0: return false; michael@0: } michael@0: michael@0: // Choose a message at random. michael@0: let randomId = keys[Math.floor(Math.random() * keys.length)]; michael@0: let message = _messages[randomId]; michael@0: michael@0: sendMessageToJava({ michael@0: type: "HomeBanner:Data", michael@0: id: message.id, michael@0: text: message.text, michael@0: iconURI: message.iconURI michael@0: }); michael@0: return true; michael@0: }; michael@0: michael@0: let _handleShown = function(id) { michael@0: let message = _messages[id]; michael@0: if (message.onshown) michael@0: message.onshown(); michael@0: }; michael@0: michael@0: let _handleClick = function(id) { michael@0: let message = _messages[id]; michael@0: if (message.onclick) michael@0: message.onclick(); michael@0: }; michael@0: michael@0: let _handleDismiss = function(id) { michael@0: let message = _messages[id]; michael@0: if (message.ondismiss) michael@0: message.ondismiss(); michael@0: }; michael@0: michael@0: return Object.freeze({ michael@0: observe: function(subject, topic, data) { michael@0: switch(topic) { michael@0: case "HomeBanner:Shown": michael@0: _handleShown(data); michael@0: break; michael@0: michael@0: case "HomeBanner:Click": michael@0: _handleClick(data); michael@0: break; michael@0: michael@0: case "HomeBanner:Dismiss": michael@0: _handleDismiss(data); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds a new banner message to the rotation. michael@0: * michael@0: * @return id Unique identifer for the message. michael@0: */ michael@0: add: function(options) { michael@0: let message = new BannerMessage(options); michael@0: _messages[message.id] = message; michael@0: michael@0: // If this is the first message we're adding, add michael@0: // observers to listen for requests from the Java UI. michael@0: if (Object.keys(_messages).length == 1) { michael@0: Services.obs.addObserver(this, "HomeBanner:Shown", false); michael@0: Services.obs.addObserver(this, "HomeBanner:Click", false); michael@0: Services.obs.addObserver(this, "HomeBanner:Dismiss", false); michael@0: michael@0: // Send a message to Java if there's a pending "HomeBanner:Get" request. michael@0: if (_pendingRequest) { michael@0: _pendingRequest = false; michael@0: _sendBannerData(); michael@0: } michael@0: } michael@0: michael@0: return message.id; michael@0: }, michael@0: michael@0: /** michael@0: * Removes a banner message from the rotation. michael@0: * michael@0: * @param id The id of the message to remove. michael@0: */ michael@0: remove: function(id) { michael@0: if (!(id in _messages)) { michael@0: throw "Home.banner: Can't remove message that doesn't exist: id = " + id; michael@0: } michael@0: michael@0: delete _messages[id]; michael@0: michael@0: // If there are no more messages, remove the observers. michael@0: if (Object.keys(_messages).length == 0) { michael@0: Services.obs.removeObserver(this, "HomeBanner:Shown"); michael@0: Services.obs.removeObserver(this, "HomeBanner:Click"); michael@0: Services.obs.removeObserver(this, "HomeBanner:Dismiss"); michael@0: } michael@0: } michael@0: }); michael@0: })(); michael@0: michael@0: // We need this object to have access to the HomePanels michael@0: // private members without leaking it outside Home.jsm. michael@0: let HomePanelsMessageHandlers; michael@0: michael@0: let HomePanels = (function () { michael@0: // Functions used to handle messages sent from Java. michael@0: HomePanelsMessageHandlers = { michael@0: michael@0: "HomePanels:Get": function handlePanelsGet(data) { michael@0: data = JSON.parse(data); michael@0: michael@0: let requestId = data.requestId; michael@0: let ids = data.ids || null; michael@0: michael@0: let panels = []; michael@0: for (let id in _registeredPanels) { michael@0: // Null ids means we want to fetch all available panels michael@0: if (ids == null || ids.indexOf(id) >= 0) { michael@0: try { michael@0: panels.push(_generatePanel(id)); michael@0: } catch(e) { michael@0: Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); michael@0: } michael@0: } michael@0: } michael@0: michael@0: sendMessageToJava({ michael@0: type: "HomePanels:Data", michael@0: panels: panels, michael@0: requestId: requestId michael@0: }); michael@0: }, michael@0: michael@0: "HomePanels:Authenticate": function handlePanelsAuthenticate(id) { michael@0: // Generate panel options to get auth handler. michael@0: let options = _registeredPanels[id](); michael@0: if (!options.auth) { michael@0: throw "Home.panels: Invalid auth for panel.id = " + id; michael@0: } michael@0: if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") { michael@0: throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id; michael@0: } michael@0: options.auth.authenticate(); michael@0: }, michael@0: michael@0: "HomePanels:RefreshView": function handlePanelsRefreshView(data) { michael@0: data = JSON.parse(data); michael@0: michael@0: let options = _registeredPanels[data.panelId](); michael@0: let view = options.views[data.viewIndex]; michael@0: michael@0: if (!view) { michael@0: throw "Home.panels: Invalid view for panel.id = " + data.panelId michael@0: + ", view.index = " + data.viewIndex; michael@0: } michael@0: michael@0: if (!view.onrefresh || typeof view.onrefresh !== "function") { michael@0: throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId michael@0: + ", view.index = " + data.viewIndex; michael@0: } michael@0: michael@0: view.onrefresh(); michael@0: }, michael@0: michael@0: "HomePanels:Installed": function handlePanelsInstalled(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: let options = _registeredPanels[id](); michael@0: if (!options.oninstall) { michael@0: return; michael@0: } michael@0: if (typeof options.oninstall !== "function") { michael@0: throw "Home.panels: Invalid oninstall function: panel.id = " + this.id; michael@0: } michael@0: options.oninstall(); michael@0: }, michael@0: michael@0: "HomePanels:Uninstalled": function handlePanelsUninstalled(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: let options = _registeredPanels[id](); michael@0: if (!options.onuninstall) { michael@0: return; michael@0: } michael@0: if (typeof options.onuninstall !== "function") { michael@0: throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id; michael@0: } michael@0: options.onuninstall(); michael@0: } michael@0: }; michael@0: michael@0: // Holds the current set of registered panels that can be michael@0: // installed, updated, uninstalled, or unregistered. It maps michael@0: // panel ids with the functions that dynamically generate michael@0: // their respective panel options. This is used to retrieve michael@0: // the current list of available panels in the system. michael@0: // See HomePanels:Get handler. michael@0: let _registeredPanels = {}; michael@0: michael@0: // Valid layouts for a panel. michael@0: let Layout = Object.freeze({ michael@0: FRAME: "frame" michael@0: }); michael@0: michael@0: // Valid types of views for a dataset. michael@0: let View = Object.freeze({ michael@0: LIST: "list", michael@0: GRID: "grid" michael@0: }); michael@0: michael@0: // Valid item types for a panel view. michael@0: let Item = Object.freeze({ michael@0: ARTICLE: "article", michael@0: IMAGE: "image" michael@0: }); michael@0: michael@0: // Valid item handlers for a panel view. michael@0: let ItemHandler = Object.freeze({ michael@0: BROWSER: "browser", michael@0: INTENT: "intent" michael@0: }); michael@0: michael@0: function Panel(id, options) { michael@0: this.id = id; michael@0: this.title = options.title; michael@0: this.layout = options.layout; michael@0: this.views = options.views; michael@0: michael@0: if (!this.id || !this.title) { michael@0: throw "Home.panels: Can't create a home panel without an id and title!"; michael@0: } michael@0: michael@0: if (!this.layout) { michael@0: // Use FRAME layout by default michael@0: this.layout = Layout.FRAME; michael@0: } else if (!_valueExists(Layout, this.layout)) { michael@0: throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout; michael@0: } michael@0: michael@0: for (let view of this.views) { michael@0: if (!_valueExists(View, view.type)) { michael@0: throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type; michael@0: } michael@0: michael@0: if (!view.itemType) { michael@0: if (view.type == View.LIST) { michael@0: // Use ARTICLE item type by default in LIST views michael@0: view.itemType = Item.ARTICLE; michael@0: } else if (view.type == View.GRID) { michael@0: // Use IMAGE item type by default in GRID views michael@0: view.itemType = Item.IMAGE; michael@0: } michael@0: } else if (!_valueExists(Item, view.itemType)) { michael@0: throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType; michael@0: } michael@0: michael@0: if (!view.itemHandler) { michael@0: // Use BROWSER item handler by default michael@0: view.itemHandler = ItemHandler.BROWSER; michael@0: } else if (!_valueExists(ItemHandler, view.itemHandler)) { michael@0: throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler; michael@0: } michael@0: michael@0: if (!view.dataset) { michael@0: throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type; michael@0: } michael@0: michael@0: if (view.onrefresh) { michael@0: view.refreshEnabled = true; michael@0: } michael@0: } michael@0: michael@0: if (options.auth) { michael@0: if (!options.auth.messageText) { michael@0: throw "Home.panels: Invalid auth messageText: panel.id = " + this.id; michael@0: } michael@0: if (!options.auth.buttonText) { michael@0: throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id; michael@0: } michael@0: michael@0: this.authConfig = { michael@0: messageText: options.auth.messageText, michael@0: buttonText: options.auth.buttonText michael@0: }; michael@0: michael@0: // Include optional image URL if it is specified. michael@0: if (options.auth.imageUrl) { michael@0: this.authConfig.imageUrl = options.auth.imageUrl; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let _generatePanel = function(id) { michael@0: let options = _registeredPanels[id](); michael@0: return new Panel(id, options); michael@0: }; michael@0: michael@0: // Helper function used to see if a value is in an object. michael@0: let _valueExists = function(obj, value) { michael@0: for (let key in obj) { michael@0: if (obj[key] == value) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }; michael@0: michael@0: let _assertPanelExists = function(id) { michael@0: if (!(id in _registeredPanels)) { michael@0: throw "Home.panels: Panel doesn't exist: id = " + id; michael@0: } michael@0: }; michael@0: michael@0: return Object.freeze({ michael@0: Layout: Layout, michael@0: View: View, michael@0: Item: Item, michael@0: ItemHandler: ItemHandler, michael@0: michael@0: register: function(id, optionsCallback) { michael@0: // Bail if the panel already exists michael@0: if (id in _registeredPanels) { michael@0: throw "Home.panels: Panel already exists: id = " + id; michael@0: } michael@0: michael@0: if (!optionsCallback || typeof optionsCallback !== "function") { michael@0: throw "Home.panels: Panel callback must be a function: id = " + id; michael@0: } michael@0: michael@0: _registeredPanels[id] = optionsCallback; michael@0: }, michael@0: michael@0: unregister: function(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: delete _registeredPanels[id]; michael@0: }, michael@0: michael@0: install: function(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: sendMessageToJava({ michael@0: type: "HomePanels:Install", michael@0: panel: _generatePanel(id) michael@0: }); michael@0: }, michael@0: michael@0: uninstall: function(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: sendMessageToJava({ michael@0: type: "HomePanels:Uninstall", michael@0: id: id michael@0: }); michael@0: }, michael@0: michael@0: update: function(id) { michael@0: _assertPanelExists(id); michael@0: michael@0: sendMessageToJava({ michael@0: type: "HomePanels:Update", michael@0: panel: _generatePanel(id) michael@0: }); michael@0: }, michael@0: michael@0: setAuthenticated: function(id, isAuthenticated) { michael@0: _assertPanelExists(id); michael@0: michael@0: let authKey = PREFS_PANEL_AUTH_PREFIX + id; michael@0: let sharedPrefs = new SharedPreferences(); michael@0: sharedPrefs.setBoolPref(authKey, isAuthenticated); michael@0: } michael@0: }); michael@0: })(); michael@0: michael@0: // Public API michael@0: this.Home = Object.freeze({ michael@0: banner: HomeBanner, michael@0: panels: HomePanels, michael@0: michael@0: // Lazy notification observer registered in browser.js michael@0: observe: function(subject, topic, data) { michael@0: if (topic in HomeBannerMessageHandlers) { michael@0: HomeBannerMessageHandlers[topic](data); michael@0: } else if (topic in HomePanelsMessageHandlers) { michael@0: HomePanelsMessageHandlers[topic](data); michael@0: } else { michael@0: Cu.reportError("Home.observe: message handler not found for topic: " + topic); michael@0: } michael@0: } michael@0: });