gecko/mobile/android/modules/Home.jsm
Jim Blandy 4d6a633bba Bug 914753: Make Emacs file variable header lines correct, or at least consistent. DONTBUILD r=ehsan
The -*- file variable lines -*- establish per-file settings that Emacs will
pick up. This patch makes the following changes to those lines (and touches
nothing else):

 - Never set the buffer's mode.

   Years ago, Emacs did not have a good JavaScript mode, so it made sense
   to use Java or C++ mode in .js files. However, Emacs has had js-mode for
   years now; it's perfectly serviceable, and is available and enabled by
   default in all major Emacs packagings.

   Selecting a mode in the -*- file variable line -*- is almost always the
   wrong thing to do anyway. It overrides Emacs's default choice, which is
   (now) reasonable; and even worse, it overrides settings the user might
   have made in their '.emacs' file for that file extension. It's only
   useful when there's something specific about that particular file that
   makes a particular mode appropriate.

 - Correctly propagate settings that establish the correct indentation
   level for this file: c-basic-offset and js2-basic-offset should be
   js-indent-level. Whatever value they're given should be preserved;
   different parts of our tree use different indentation styles.

 - We don't use tabs in Mozilla JS code. Always set indent-tabs-mode: nil.
   Remove tab-width: settings, at least in files that don't contain tab
   characters.

 - Remove js2-mode settings that belong in the user's .emacs file, like
   js2-skip-preprocessor-directives.
2014-06-24 22:12:07 -07:00

482 lines
14 KiB
JavaScript

// -*- 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.
let HomeBannerMessageHandlers;
let 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) {
sendMessageToJava({
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.
let HomePanelsMessageHandlers;
let 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);
}
}
}
sendMessageToJava({
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"
});
// 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;
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;
}
}
}
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);
sendMessageToJava({
type: "HomePanels:Install",
panel: _generatePanel(id)
});
},
uninstall: function(id) {
_assertPanelExists(id);
sendMessageToJava({
type: "HomePanels:Uninstall",
id: id
});
},
update: function(id) {
_assertPanelExists(id);
sendMessageToJava({
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);
}
}
});