diff options
Diffstat (limited to 'mobile/android/modules/Home.jsm')
-rw-r--r-- | mobile/android/modules/Home.jsm | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/mobile/android/modules/Home.jsm b/mobile/android/modules/Home.jsm new file mode 100644 index 000000000..e77d35dbd --- /dev/null +++ b/mobile/android/modules/Home.jsm @@ -0,0 +1,487 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Home"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SharedPreferences.jsm"); +Cu.import("resource://gre/modules/Messaging.jsm"); + +// Keep this in sync with the constant defined in PanelAuthCache.java +const PREFS_PANEL_AUTH_PREFIX = "home_panels_auth_"; + +// Default weight for a banner message. +const DEFAULT_WEIGHT = 100; + +// See bug 915424 +function resolveGeckoURI(aURI) { + if (!aURI) + throw "Can't resolve an empty uri"; + + if (aURI.startsWith("chrome://")) { + let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); + return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; + } else if (aURI.startsWith("resource://")) { + let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); + return handler.resolveURI(Services.io.newURI(aURI, null, null)); + } + return aURI; +} + +function BannerMessage(options) { + let uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); + this.id = uuidgen.generateUUID().toString(); + + if ("text" in options && options.text != null) + this.text = options.text; + + if ("icon" in options && options.icon != null) + this.iconURI = resolveGeckoURI(options.icon); + + if ("onshown" in options && typeof options.onshown === "function") + this.onshown = options.onshown; + + if ("onclick" in options && typeof options.onclick === "function") + this.onclick = options.onclick; + + if ("ondismiss" in options && typeof options.ondismiss === "function") + this.ondismiss = options.ondismiss; + + let weight = parseInt(options.weight, 10); + this.weight = weight > 0 ? weight : DEFAULT_WEIGHT; +} + +// We need this object to have access to the HomeBanner +// private members without leaking it outside Home.jsm. +var HomeBannerMessageHandlers; + +var HomeBanner = (function () { + // Whether there is a "HomeBanner:Get" request we couldn't fulfill. + let _pendingRequest = false; + + // Functions used to handle messages sent from Java. + HomeBannerMessageHandlers = { + "HomeBanner:Get": function handleBannerGet(data) { + if (Object.keys(_messages).length > 0) { + _sendBannerData(); + } else { + _pendingRequest = true; + } + } + }; + + // Holds the messages that will rotate through the banner. + let _messages = {}; + + // Choose a random message from the set of messages, biasing towards those with higher weight. + // Weight logic copied from desktop snippets: + // https://github.com/mozilla/snippets-service/blob/7d80edb8b1cddaed075275c2fc7cdf69a10f4003/snippets/base/templates/base/includes/snippet_js.html#L119 + let _sendBannerData = function() { + let totalWeight = 0; + for (let key in _messages) { + let message = _messages[key]; + totalWeight += message.weight; + message.totalWeight = totalWeight; + } + + let threshold = Math.random() * totalWeight; + for (let key in _messages) { + let message = _messages[key]; + if (threshold < message.totalWeight) { + Messaging.sendRequest({ + type: "HomeBanner:Data", + id: message.id, + text: message.text, + iconURI: message.iconURI + }); + return; + } + } + }; + + let _handleShown = function(id) { + let message = _messages[id]; + if (message.onshown) + message.onshown(); + }; + + let _handleClick = function(id) { + let message = _messages[id]; + if (message.onclick) + message.onclick(); + }; + + let _handleDismiss = function(id) { + let message = _messages[id]; + if (message.ondismiss) + message.ondismiss(); + }; + + return Object.freeze({ + observe: function(subject, topic, data) { + switch(topic) { + case "HomeBanner:Shown": + _handleShown(data); + break; + + case "HomeBanner:Click": + _handleClick(data); + break; + + case "HomeBanner:Dismiss": + _handleDismiss(data); + break; + } + }, + + /** + * Adds a new banner message to the rotation. + * + * @return id Unique identifer for the message. + */ + add: function(options) { + let message = new BannerMessage(options); + _messages[message.id] = message; + + // If this is the first message we're adding, add + // observers to listen for requests from the Java UI. + if (Object.keys(_messages).length == 1) { + Services.obs.addObserver(this, "HomeBanner:Shown", false); + Services.obs.addObserver(this, "HomeBanner:Click", false); + Services.obs.addObserver(this, "HomeBanner:Dismiss", false); + + // Send a message to Java if there's a pending "HomeBanner:Get" request. + if (_pendingRequest) { + _pendingRequest = false; + _sendBannerData(); + } + } + + return message.id; + }, + + /** + * Removes a banner message from the rotation. + * + * @param id The id of the message to remove. + */ + remove: function(id) { + if (!(id in _messages)) { + throw "Home.banner: Can't remove message that doesn't exist: id = " + id; + } + + delete _messages[id]; + + // If there are no more messages, remove the observers. + if (Object.keys(_messages).length == 0) { + Services.obs.removeObserver(this, "HomeBanner:Shown"); + Services.obs.removeObserver(this, "HomeBanner:Click"); + Services.obs.removeObserver(this, "HomeBanner:Dismiss"); + } + } + }); +})(); + +// We need this object to have access to the HomePanels +// private members without leaking it outside Home.jsm. +var HomePanelsMessageHandlers; + +var HomePanels = (function () { + // Functions used to handle messages sent from Java. + HomePanelsMessageHandlers = { + + "HomePanels:Get": function handlePanelsGet(data) { + data = JSON.parse(data); + + let requestId = data.requestId; + let ids = data.ids || null; + + let panels = []; + for (let id in _registeredPanels) { + // Null ids means we want to fetch all available panels + if (ids == null || ids.indexOf(id) >= 0) { + try { + panels.push(_generatePanel(id)); + } catch(e) { + Cu.reportError("Home.panels: Invalid options, panel.id = " + id + ": " + e); + } + } + } + + Messaging.sendRequest({ + type: "HomePanels:Data", + panels: panels, + requestId: requestId + }); + }, + + "HomePanels:Authenticate": function handlePanelsAuthenticate(id) { + // Generate panel options to get auth handler. + let options = _registeredPanels[id](); + if (!options.auth) { + throw "Home.panels: Invalid auth for panel.id = " + id; + } + if (!options.auth.authenticate || typeof options.auth.authenticate !== "function") { + throw "Home.panels: Invalid auth authenticate function: panel.id = " + this.id; + } + options.auth.authenticate(); + }, + + "HomePanels:RefreshView": function handlePanelsRefreshView(data) { + data = JSON.parse(data); + + let options = _registeredPanels[data.panelId](); + let view = options.views[data.viewIndex]; + + if (!view) { + throw "Home.panels: Invalid view for panel.id = " + data.panelId + + ", view.index = " + data.viewIndex; + } + + if (!view.onrefresh || typeof view.onrefresh !== "function") { + throw "Home.panels: Invalid onrefresh for panel.id = " + data.panelId + + ", view.index = " + data.viewIndex; + } + + view.onrefresh(); + }, + + "HomePanels:Installed": function handlePanelsInstalled(id) { + _assertPanelExists(id); + + let options = _registeredPanels[id](); + if (!options.oninstall) { + return; + } + if (typeof options.oninstall !== "function") { + throw "Home.panels: Invalid oninstall function: panel.id = " + this.id; + } + options.oninstall(); + }, + + "HomePanels:Uninstalled": function handlePanelsUninstalled(id) { + _assertPanelExists(id); + + let options = _registeredPanels[id](); + if (!options.onuninstall) { + return; + } + if (typeof options.onuninstall !== "function") { + throw "Home.panels: Invalid onuninstall function: panel.id = " + this.id; + } + options.onuninstall(); + } + }; + + // Holds the current set of registered panels that can be + // installed, updated, uninstalled, or unregistered. It maps + // panel ids with the functions that dynamically generate + // their respective panel options. This is used to retrieve + // the current list of available panels in the system. + // See HomePanels:Get handler. + let _registeredPanels = {}; + + // Valid layouts for a panel. + let Layout = Object.freeze({ + FRAME: "frame" + }); + + // Valid types of views for a dataset. + let View = Object.freeze({ + LIST: "list", + GRID: "grid" + }); + + // Valid item types for a panel view. + let Item = Object.freeze({ + ARTICLE: "article", + IMAGE: "image", + ICON: "icon" + }); + + // Valid item handlers for a panel view. + let ItemHandler = Object.freeze({ + BROWSER: "browser", + INTENT: "intent" + }); + + function Panel(id, options) { + this.id = id; + this.title = options.title; + this.layout = options.layout; + this.views = options.views; + this.default = !!options.default; + + if (!this.id || !this.title) { + throw "Home.panels: Can't create a home panel without an id and title!"; + } + + if (!this.layout) { + // Use FRAME layout by default + this.layout = Layout.FRAME; + } else if (!_valueExists(Layout, this.layout)) { + throw "Home.panels: Invalid layout for panel: panel.id = " + this.id + ", panel.layout =" + this.layout; + } + + for (let view of this.views) { + if (!_valueExists(View, view.type)) { + throw "Home.panels: Invalid view type: panel.id = " + this.id + ", view.type = " + view.type; + } + + if (!view.itemType) { + if (view.type == View.LIST) { + // Use ARTICLE item type by default in LIST views + view.itemType = Item.ARTICLE; + } else if (view.type == View.GRID) { + // Use IMAGE item type by default in GRID views + view.itemType = Item.IMAGE; + } + } else if (!_valueExists(Item, view.itemType)) { + throw "Home.panels: Invalid item type: panel.id = " + this.id + ", view.itemType = " + view.itemType; + } + + if (!view.itemHandler) { + // Use BROWSER item handler by default + view.itemHandler = ItemHandler.BROWSER; + } else if (!_valueExists(ItemHandler, view.itemHandler)) { + throw "Home.panels: Invalid item handler: panel.id = " + this.id + ", view.itemHandler = " + view.itemHandler; + } + + if (!view.dataset) { + throw "Home.panels: No dataset provided for view: panel.id = " + this.id + ", view.type = " + view.type; + } + + if (view.onrefresh) { + view.refreshEnabled = true; + } + } + + if (options.auth) { + if (!options.auth.messageText) { + throw "Home.panels: Invalid auth messageText: panel.id = " + this.id; + } + if (!options.auth.buttonText) { + throw "Home.panels: Invalid auth buttonText: panel.id = " + this.id; + } + + this.authConfig = { + messageText: options.auth.messageText, + buttonText: options.auth.buttonText + }; + + // Include optional image URL if it is specified. + if (options.auth.imageUrl) { + this.authConfig.imageUrl = options.auth.imageUrl; + } + } + + if (options.position >= 0) { + this.position = options.position; + } + } + + let _generatePanel = function(id) { + let options = _registeredPanels[id](); + return new Panel(id, options); + }; + + // Helper function used to see if a value is in an object. + let _valueExists = function(obj, value) { + for (let key in obj) { + if (obj[key] == value) { + return true; + } + } + return false; + }; + + let _assertPanelExists = function(id) { + if (!(id in _registeredPanels)) { + throw "Home.panels: Panel doesn't exist: id = " + id; + } + }; + + return Object.freeze({ + Layout: Layout, + View: View, + Item: Item, + ItemHandler: ItemHandler, + + register: function(id, optionsCallback) { + // Bail if the panel already exists + if (id in _registeredPanels) { + throw "Home.panels: Panel already exists: id = " + id; + } + + if (!optionsCallback || typeof optionsCallback !== "function") { + throw "Home.panels: Panel callback must be a function: id = " + id; + } + + _registeredPanels[id] = optionsCallback; + }, + + unregister: function(id) { + _assertPanelExists(id); + + delete _registeredPanels[id]; + }, + + install: function(id) { + _assertPanelExists(id); + + Messaging.sendRequest({ + type: "HomePanels:Install", + panel: _generatePanel(id) + }); + }, + + uninstall: function(id) { + _assertPanelExists(id); + + Messaging.sendRequest({ + type: "HomePanels:Uninstall", + id: id + }); + }, + + update: function(id) { + _assertPanelExists(id); + + Messaging.sendRequest({ + type: "HomePanels:Update", + panel: _generatePanel(id) + }); + }, + + setAuthenticated: function(id, isAuthenticated) { + _assertPanelExists(id); + + let authKey = PREFS_PANEL_AUTH_PREFIX + id; + let sharedPrefs = SharedPreferences.forProfile(); + sharedPrefs.setBoolPref(authKey, isAuthenticated); + } + }); +})(); + +// Public API +this.Home = Object.freeze({ + banner: HomeBanner, + panels: HomePanels, + + // Lazy notification observer registered in browser.js + observe: function(subject, topic, data) { + if (topic in HomeBannerMessageHandlers) { + HomeBannerMessageHandlers[topic](data); + } else if (topic in HomePanelsMessageHandlers) { + HomePanelsMessageHandlers[topic](data); + } else { + Cu.reportError("Home.observe: message handler not found for topic: " + topic); + } + } +}); |