Merge m-c to inbound, a=merge

MozReview-Commit-ID: 1Q56H3tR0mI
This commit is contained in:
Wes Kocher 2016-04-05 16:39:24 -07:00
commit 83be08e7e5
33 changed files with 680 additions and 889 deletions

View File

@ -112,7 +112,6 @@ DEFAULT_FIREFOX_PREFS = {
'browser.startup.homepage' : 'about:blank',
'startup.homepage_welcome_url' : 'about:blank',
'devtools.browsertoolbox.panel': 'jsdebugger',
'devtools.errorconsole.enabled' : True,
'devtools.chrome.enabled' : True,
# From:

View File

@ -2,7 +2,6 @@
"browser.startup.homepage": "about:blank",
"startup.homepage_welcome_url": "about:blank",
"devtools.browsertoolbox.panel": "jsdebugger",
"devtools.errorconsole.enabled": true,
"devtools.chrome.enabled": true,
"urlclassifier.updateinterval": 172800,
"browser.safebrowsing.provider.google.gethashURL": "http://localhost/safebrowsing-dummy/gethash",

View File

@ -157,12 +157,6 @@ XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () {
}
});
XPCOMUtils.defineLazyGetter(this, "DeveloperToolbar", function() {
let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
return new DeveloperToolbar(window);
});
XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() {
let tmp = {};
Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", tmp);
@ -1400,11 +1394,6 @@ var gBrowserInit = {
if (!this._loadHandled)
return;
let desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar");
if (desc && !desc.get) {
DeveloperToolbar.destroy();
}
// First clean up services initialized in gBrowserInit.onLoad (or those whose
// uninit methods don't depend on the services having been initialized).

View File

@ -130,6 +130,11 @@ function handleGUMRequest(aSubject, aTopic, aData) {
contentWindow.navigator.mozGetUserMediaDevices(
constraints,
function (devices) {
// If the window has been closed while we were waiting for the list of
// devices, there's nothing to do in the callback anymore.
if (contentWindow.closed)
return;
prompt(contentWindow, aSubject.windowID, aSubject.callID,
constraints, devices, secure);
},

18
devtools/bootstrap.js vendored
View File

@ -112,24 +112,6 @@ function reload(event) {
}
}, false);
}
// Manually reload gcli if it has been used
// Bug 1248348: Inject the developer toolbar dynamically within browser/
// so that we can easily remove/reinject it
const desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar");
if (desc && !desc.get) {
let wasVisible = window.DeveloperToolbar.visible;
window.DeveloperToolbar.hide()
.then(() => {
window.DeveloperToolbar.destroy();
let { DeveloperToolbar } = devtools.require("devtools/client/shared/developer-toolbar");
window.DeveloperToolbar = new DeveloperToolbar(window, window.document.getElementById("developer-toolbar"));
if (wasVisible) {
window.DeveloperToolbar.show();
}
});
}
} else if (windowtype === "devtools:webide") {
window.location.reload();
} else if (windowtype === "devtools:webconsole") {

View File

@ -80,14 +80,12 @@ AnimationDetails.prototype = {
*/
if (this.serverTraits.hasGetProperties) {
let properties = yield this.animation.getProperties();
for (let propertyObject of properties) {
let name = propertyObject.property;
for (let {name, values} of properties) {
if (!tracks[name]) {
tracks[name] = [];
}
for (let {value, offset} of propertyObject.values) {
for (let {value, offset} of values) {
tracks[name].push({value, offset});
}
}

View File

@ -60,15 +60,12 @@ function* getExpectedKeyframesData(animation) {
for (let expectedProperty of EXPECTED_PROPERTIES) {
data[expectedProperty] = [];
for (let propertyObject of properties) {
if (propertyObject.property !== expectedProperty) {
for (let {name, values} of properties) {
if (name !== expectedProperty) {
continue;
}
for (let valueObject of propertyObject.values) {
data[expectedProperty].push({
offset: valueObject.offset,
value: valueObject.value
});
for (let {offset, value} of values) {
data[expectedProperty].push({offset, value});
}
}
}

View File

@ -16,6 +16,7 @@ const Services = require("Services");
const MenuStrings = Services.strings.createBundle("chrome://devtools/locale/menus.properties");
loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
// Keep list of inserted DOM Elements in order to remove them on unload
// Maps browser xul document => list of DOM Elements
@ -30,40 +31,41 @@ function l10n(key) {
*
* @param {XULDocument} doc
* The document to which keys are to be added.
* @param {String} l10nKey
* Prefix of the properties entry to look for key shortcut in
* localization file. We will look for {property}.key and
* {property}.keytext for non-character shortcuts like F12.
* @param {String} command
* Id of the xul:command to map to.
* @param {Object} key definition dictionnary
* Definition with following attributes:
* - {String} id
* xul:key's id, automatically prefixed with "key_",
* - {String} modifiers
* Space separater list of modifier names,
* - {Boolean} keytext
* If true, consider the shortcut as a characther one,
* otherwise a non-character one like F12.
* @param {String} id
* key's id, automatically prefixed with "key_".
* @param {String} shortcut
* The key shortcut value.
* @param {String} keytext
* If `shortcut` refers to a function key, refers to the localized
* string to describe a non-character shortcut.
* @param {String} modifiers
* Space separated list of modifier names.
* @param {Function} oncommand
* The function to call when the shortcut is pressed.
*
* @return XULKeyElement
*/
function createKey(doc, l10nKey, command, key) {
function createKey({ doc, id, shortcut, keytext, modifiers, oncommand }) {
let k = doc.createElement("key");
k.id = "key_" + key.id;
let shortcut = l10n(l10nKey + ".key");
k.id = "key_" + id;
if (shortcut.startsWith("VK_")) {
k.setAttribute("keycode", shortcut);
k.setAttribute("keytext", l10n(l10nKey + ".keytext"));
if (keytext) {
k.setAttribute("keytext", keytext);
}
} else {
k.setAttribute("key", shortcut);
}
if (command) {
k.setAttribute("command", command);
}
if (key.modifiers) {
k.setAttribute("modifiers", key.modifiers);
if (modifiers) {
k.setAttribute("modifiers", modifiers);
}
// Bug 371900: command event is fired only if "oncommand" attribute is set.
k.setAttribute("oncommand", ";");
k.addEventListener("command", oncommand);
return k;
}
@ -76,25 +78,18 @@ function createKey(doc, l10nKey, command, key) {
* Element id.
* @param {String} label
* Menu label.
* @param {String} broadcasterId (optional)
* Id of the xul:broadcaster to map to.
* @param {String} accesskey (optional)
* Access key of the menuitem, used as shortcut while opening the menu.
* @param {Boolean} isCheckbox
* @param {Boolean} isCheckbox (optional)
* If true, the menuitem will act as a checkbox and have an optional
* tick on its left.
*
* @return XULMenuItemElement
*/
function createMenuItem({ doc, id, label, broadcasterId, accesskey, isCheckbox }) {
function createMenuItem({ doc, id, label, accesskey, isCheckbox }) {
let menuitem = doc.createElement("menuitem");
menuitem.id = id;
if (label) {
menuitem.setAttribute("label", label);
}
if (broadcasterId) {
menuitem.setAttribute("observes", broadcasterId);
}
menuitem.setAttribute("label", label);
if (accesskey) {
menuitem.setAttribute("accesskey", accesskey);
}
@ -105,56 +100,6 @@ function createMenuItem({ doc, id, label, broadcasterId, accesskey, isCheckbox }
return menuitem;
}
/**
* Create a xul:broadcaster element
*
* @param {XULDocument} doc
* The document to which keys are to be added.
* @param {String} id
* Element id.
* @param {String} label
* Broadcaster label.
* @param {Boolean} isCheckbox
* If true, the broadcaster is a checkbox one.
*
* @return XULMenuItemElement
*/
function createBroadcaster({ doc, id, label, isCheckbox }) {
let broadcaster = doc.createElement("broadcaster");
broadcaster.id = id;
broadcaster.setAttribute("label", label);
if (isCheckbox) {
broadcaster.setAttribute("type", "checkbox");
broadcaster.setAttribute("autocheck", "false");
}
return broadcaster;
}
/**
* Create a xul:command element
*
* @param {XULDocument} doc
* The document to which keys are to be added.
* @param {String} id
* Element id.
* @param {String} oncommand
* JS String to run when the command is fired.
* @param {Boolean} disabled
* If true, the command is disabled and hidden.
*
* @return XULCommandElement
*/
function createCommand({ doc, id, oncommand, disabled }) {
let command = doc.createElement("command");
command.id = id;
command.setAttribute("oncommand", oncommand);
if (disabled) {
command.setAttribute("disabled", "true");
command.setAttribute("hidden", "true");
}
return command;
}
/**
* Add a <key> to <keyset id="devtoolsKeyset">.
* Appending a <key> element is not always enough. The <keyset> needs
@ -188,61 +133,49 @@ function attachKeybindingsToBrowser(doc, keys) {
*/
function createToolMenuElements(toolDefinition, doc) {
let id = toolDefinition.id;
let menuId = "menuitem_" + id;
// Prevent multiple entries for the same tool.
if (doc.getElementById("Tools:" + id)) {
if (doc.getElementById(menuId)) {
return;
}
let cmd = createCommand({
doc,
id: "Tools:" + id,
oncommand: 'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");',
});
let oncommand = function (id, event) {
let window = event.target.ownerDocument.defaultView;
gDevToolsBrowser.selectToolCommand(window.gBrowser, id);
}.bind(null, id);
let key = null;
if (toolDefinition.key) {
key = doc.createElement("key");
key.id = "key_" + id;
if (toolDefinition.key.startsWith("VK_")) {
key.setAttribute("keycode", toolDefinition.key);
} else {
key.setAttribute("key", toolDefinition.key);
}
key.setAttribute("command", cmd.id);
key.setAttribute("modifiers", toolDefinition.modifiers);
}
let bc = createBroadcaster({
doc,
id: "devtoolsMenuBroadcaster_" + id,
label: toolDefinition.menuLabel || toolDefinition.label
});
bc.setAttribute("command", cmd.id);
if (key) {
bc.setAttribute("key", "key_" + id);
key = createKey({
doc,
id,
shortcut: toolDefinition.key,
modifiers: toolDefinition.modifiers,
oncommand: oncommand
});
}
let menuitem = createMenuItem({
doc,
id: "menuitem_" + id,
broadcasterId: "devtoolsMenuBroadcaster_" + id,
label: toolDefinition.menuLabel || toolDefinition.label,
accesskey: toolDefinition.accesskey
});
if (key) {
// Refer to the key in order to display the key shortcut at menu ends
menuitem.setAttribute("key", key.id);
}
menuitem.addEventListener("command", oncommand);
return {
cmd: cmd,
key: key,
bc: bc,
menuitem: menuitem
key,
menuitem
};
}
/**
* Create xul menuitem, command, broadcaster and key elements for a given tool.
* Create xul menuitem, key elements for a given tool.
* And then insert them into browser DOM.
*
* @param {XULDocument} doc
@ -253,16 +186,12 @@ function createToolMenuElements(toolDefinition, doc) {
* The tool definition after which the tool menu item is to be added.
*/
function insertToolMenuElements(doc, toolDefinition, prevDef) {
let elements = createToolMenuElements(toolDefinition, doc);
let { key, menuitem } = createToolMenuElements(toolDefinition, doc);
doc.getElementById("mainCommandSet").appendChild(elements.cmd);
if (elements.key) {
attachKeybindingsToBrowser(doc, elements.key);
if (key) {
attachKeybindingsToBrowser(doc, key);
}
doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
let ref;
if (prevDef) {
let menuitem = doc.getElementById("menuitem_" + prevDef.id);
@ -272,7 +201,7 @@ function insertToolMenuElements(doc, toolDefinition, prevDef) {
}
if (ref) {
ref.parentNode.insertBefore(elements.menuitem, ref);
ref.parentNode.insertBefore(menuitem, ref);
}
}
exports.insertToolMenuElements = insertToolMenuElements;
@ -286,24 +215,14 @@ exports.insertToolMenuElements = insertToolMenuElements;
* The document to which the tool menu item is to be removed from
*/
function removeToolFromMenu(toolId, doc) {
let command = doc.getElementById("Tools:" + toolId);
if (command) {
command.parentNode.removeChild(command);
}
let key = doc.getElementById("key_" + toolId);
if (key) {
key.parentNode.removeChild(key);
}
let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
if (bc) {
bc.parentNode.removeChild(bc);
key.remove();
}
let menuitem = doc.getElementById("menuitem_" + toolId);
if (menuitem) {
menuitem.parentNode.removeChild(menuitem);
menuitem.remove();
}
}
exports.removeToolFromMenu = removeToolFromMenu;
@ -315,9 +234,7 @@ exports.removeToolFromMenu = removeToolFromMenu;
* The document to which the tool items are to be added.
*/
function addAllToolsToMenu(doc) {
let fragCommands = doc.createDocumentFragment();
let fragKeys = doc.createDocumentFragment();
let fragBroadcasters = doc.createDocumentFragment();
let fragMenuItems = doc.createDocumentFragment();
for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
@ -331,22 +248,14 @@ function addAllToolsToMenu(doc) {
continue;
}
fragCommands.appendChild(elements.cmd);
if (elements.key) {
fragKeys.appendChild(elements.key);
}
fragBroadcasters.appendChild(elements.bc);
fragMenuItems.appendChild(elements.menuitem);
}
let mcs = doc.getElementById("mainCommandSet");
mcs.appendChild(fragCommands);
attachKeybindingsToBrowser(doc, fragKeys);
let mbs = doc.getElementById("mainBroadcasterSet");
mbs.appendChild(fragBroadcasters);
let mps = doc.getElementById("menu_devtools_separator");
if (mps) {
mps.parentNode.insertBefore(fragMenuItems, mps);
@ -385,10 +294,15 @@ function addTopLevelItems(doc) {
if (item.key && l10nKey) {
// Create a <key>
let key = createKey(doc, l10nKey, null, item.key);
// Bug 371900: command event is fired only if "oncommand" attribute is set.
key.setAttribute("oncommand", ";");
key.addEventListener("command", item.oncommand);
let shortcut = l10n(l10nKey + ".key");
let key = createKey({
doc,
id: item.key.id,
shortcut: shortcut,
keytext: shortcut.startsWith("VK_") ? l10n(l10nKey + ".keytext") : null,
modifiers: item.key.modifiers,
oncommand: item.oncommand
});
// Refer to the key in order to display the key shortcut at menu ends
menuitem.setAttribute("key", key.id);
keys.appendChild(key);
@ -396,10 +310,15 @@ function addTopLevelItems(doc) {
if (item.additionalKeys) {
// Create additional <key>
for (let key of item.additionalKeys) {
let node = createKey(doc, key.l10nKey, null, key);
// Bug 371900: command event is fired only if "oncommand" attribute is set.
node.setAttribute("oncommand", ";");
node.addEventListener("command", item.oncommand);
let shortcut = l10n(key.l10nKey + ".key");
let node = createKey({
doc,
id: key.id,
shortcut: shortcut,
keytext: shortcut.startsWith("VK_") ? l10n(key.l10nKey + ".keytext") : null,
modifiers: key.modifiers,
oncommand: item.oncommand
});
keys.appendChild(node);
}
}

View File

@ -122,10 +122,6 @@ var gDevToolsBrowser = exports.gDevToolsBrowser = {
toggleMenuItem("menu_browserToolbox", remoteEnabled);
toggleMenuItem("menu_browserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
// Enable Error Console?
let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
toggleMenuItem("javascriptConsole", consoleEnabled);
// Enable DevTools connection screen, if the preference allows this.
toggleMenuItem("menu_devtools_connect", devtoolsRemoteEnabled);
},
@ -356,6 +352,13 @@ var gDevToolsBrowser = exports.gDevToolsBrowser = {
gDevToolsBrowser._trackedBrowserWindows.add(win);
BrowserMenus.addMenus(win.document);
// Inject lazily DeveloperToolbar on the chrome window
loader.lazyGetter(win, "DeveloperToolbar", function() {
let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar");
return new DeveloperToolbar(win);
});
this.updateCommandAvailability(win);
this.ensurePrefObserver();
win.addEventListener("unload", this);
@ -555,6 +558,9 @@ var gDevToolsBrowser = exports.gDevToolsBrowser = {
* The window containing the menu entry
*/
_forgetBrowserWindow: function(win) {
if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) {
return;
}
gDevToolsBrowser._trackedBrowserWindows.delete(win);
win.removeEventListener("unload", this);
@ -567,6 +573,12 @@ var gDevToolsBrowser = exports.gDevToolsBrowser = {
}
}
// Destroy the Developer toolbar if it has been accessed
let desc = Object.getOwnPropertyDescriptor(win, "DeveloperToolbar");
if (desc && !desc.get) {
win.DeveloperToolbar.destroy();
}
let tabContainer = win.gBrowser.tabContainer;
tabContainer.removeEventListener("TabSelect", this, false);
tabContainer.removeEventListener("TabOpen", this, false);

View File

@ -8,7 +8,6 @@
var gItemsToTest = {
"menu_devToolbar": "devtools.toolbar.enabled",
"menu_browserToolbox": ["devtools.chrome.enabled", "devtools.debugger.remote-enabled"],
"javascriptConsole": "devtools.errorconsole.enabled",
"menu_devtools_connect": "devtools.debugger.remote-enabled",
};

View File

@ -28,7 +28,8 @@ function testRegister(aToolbox)
label: "Test Tool",
inMenu: true,
isTargetSupported: () => true,
build: function() {}
build: function() {},
key: "t"
});
}
@ -47,8 +48,8 @@ function toolRegistered(event, toolId)
ok(panel, "new tool's panel exists in toolbox UI");
for (let win of getAllBrowserWindows()) {
let command = win.document.getElementById("Tools:" + toolId);
ok(command, "command for new tool added to every browser window");
let key = win.document.getElementById("key_" + toolId);
ok(key, "key for new tool added to every browser window");
let menuitem = win.document.getElementById("menuitem_" + toolId);
ok(menuitem, "menu item of new tool added to every browser window");
}
@ -89,8 +90,8 @@ function toolUnregistered(event, toolDefinition)
ok(!panel, "tool's panel was removed from toolbox UI");
for (let win of getAllBrowserWindows()) {
let command = win.document.getElementById("Tools:" + toolId);
ok(!command, "command removed from every browser window");
let key = win.document.getElementById("key_" + toolId);
ok(!key , "key removed from every browser window");
let menuitem = win.document.getElementById("menuitem_" + toolId);
ok(!menuitem, "menu item removed from every browser window");
}

View File

@ -11,9 +11,6 @@ devtoolsServiceWorkers.accesskey = k
devtoolsConnect.label = Connect…
devtoolsConnect.accesskey = C
errorConsoleCmd.label = Error Console
errorConsoleCmd.accesskey = C
browserConsoleCmd.label = Browser Console
browserConsoleCmd.accesskey = B
browserConsoleCmd.key = j

View File

@ -170,14 +170,6 @@ exports.menuitems = [
modifiers: "shift"
}
},
{ id: "javascriptConsole",
l10nKey: "errorConsoleCmd",
disabled: true,
oncommand(event) {
let window = event.target.ownerDocument.defaultView;
window.toJavaScriptConsole();
}
},
{ id: "menu_devtools_serviceworkers",
l10nKey: "devtoolsServiceWorkers",
disabled: true,

View File

@ -14,9 +14,6 @@ pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer
pref("devtools.devedition.promo.enabled", false);
#endif
// Disable the error console
pref("devtools.errorconsole.enabled", false);
// DevTools development workflow
pref("devtools.loader.hotreload", false);

View File

@ -498,10 +498,6 @@ DeveloperToolbar.prototype.show = function(focus) {
tabbrowser.addEventListener("beforeunload", this, true);
this._initErrorsCount(tabbrowser.selectedTab);
this._devtoolsUnloaded = this._devtoolsUnloaded.bind(this);
this._devtoolsLoaded = this._devtoolsLoaded.bind(this);
Services.obs.addObserver(this._devtoolsUnloaded, "devtools-unloaded", false);
Services.obs.addObserver(this._devtoolsLoaded, "devtools-loaded", false);
this._element.hidden = false;
@ -571,24 +567,6 @@ DeveloperToolbar.prototype.hide = function() {
return this._hidePromise;
};
/**
* The devtools-unloaded event handler.
* @private
*/
DeveloperToolbar.prototype._devtoolsUnloaded = function() {
let tabbrowser = this._chromeWindow.gBrowser;
Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
};
/**
* The devtools-loaded event handler.
* @private
*/
DeveloperToolbar.prototype._devtoolsLoaded = function() {
let tabbrowser = this._chromeWindow.gBrowser;
this._initErrorsCount(tabbrowser.selectedTab);
};
/**
* Initialize the listeners needed for tracking the number of errors for a given
* tab.
@ -657,8 +635,6 @@ DeveloperToolbar.prototype.destroy = function() {
tabbrowser.removeEventListener("load", this, true);
tabbrowser.removeEventListener("beforeunload", this, true);
Services.obs.removeObserver(this._devtoolsUnloaded, "devtools-unloaded");
Services.obs.removeObserver(this._devtoolsLoaded, "devtools-loaded");
Array.prototype.forEach.call(tabbrowser.tabs, this._stopErrorsCount, this);
this.focusManager.removeMonitoredElement(this.outputPanel._frame);

View File

@ -444,15 +444,17 @@ var AnimationPlayerActor = ActorClass({
/**
* Get data about the animated properties of this animation player.
* @return {Object} Returns a list of animated properties.
* @return {Array} Returns a list of animated properties.
* Each property contains a list of values and their offsets
*/
getProperties: method(function() {
return this.player.effect.getProperties();
return this.player.effect.getProperties().map(property => {
return {name: property.property, values: property.values};
});
}, {
request: {},
response: {
frames: RetVal("json")
properties: RetVal("array:json")
}
})
});

View File

@ -10,8 +10,7 @@
const URL = MAIN_DOMAIN + "animation.html";
add_task(function*() {
let {client, walker, animations} =
yield initAnimationsFrontForUrl(MAIN_DOMAIN + "animation.html");
let {client, walker, animations} = yield initAnimationsFrontForUrl(URL);
info("Get the test node and its animation front");
let node = yield walker.querySelector(walker.rootNode, ".simple-animation");
@ -23,7 +22,7 @@ add_task(function*() {
is(properties.length, 1, "The correct number of properties was retrieved");
let propertyObject = properties[0];
is(propertyObject.property, "transform", "Property 0 is transform");
is(propertyObject.name, "transform", "Property 0 is transform");
is(propertyObject.values.length, 2,
"The correct number of property values was retrieved");

View File

@ -267,8 +267,6 @@ DevToolsLoader.prototype = {
}
if (this._provider) {
var events = this.require("sdk/system/events");
events.emit("devtools-unloaded", {});
delete this.require;
this._provider.unload("newprovider");
}
@ -330,7 +328,6 @@ DevToolsLoader.prototype = {
reload: function() {
var events = this.require("sdk/system/events");
events.emit("startupcache-invalidate", {});
events.emit("devtools-unloaded", {});
this._provider.unload("reload");
delete this._provider;

View File

@ -426,8 +426,6 @@ pref("javascript.options.mem.high_water_mark", 32);
pref("dom.max_chrome_script_run_time", 0); // disable slow script dialog for chrome
pref("dom.max_script_run_time", 20);
// JS error console
pref("devtools.errorconsole.enabled", false);
// Absolute path to the devtools unix domain socket file used
// to communicate with a usb cable via adb forward.
pref("devtools.debugger.unix-domain-socket", "/data/data/@ANDROID_PACKAGE_NAME@/firefox-debugger-socket");

View File

@ -817,27 +817,22 @@ public class BrowserApp extends GeckoApp
final String hostExtra = ContextUtils.getStringExtra(intent, INTENT_KEY_SWITCHBOARD_HOST);
final String host = TextUtils.isEmpty(hostExtra) ? DEFAULT_SWITCHBOARD_HOST : hostExtra;
final String configServerUpdateUrl;
final String configServerUrl;
final String serverUrl;
try {
configServerUpdateUrl = new URL("https", host, "urls").toString();
configServerUrl = new URL("https", host, "v1").toString();
serverUrl = new URL("https", host, "v2").toString();
} catch (MalformedURLException e) {
Log.e(LOGTAG, "Error creating Switchboard server URL", e);
return;
}
SwitchBoard.initDefaultServerUrls(configServerUpdateUrl, configServerUrl, true);
final String switchboardUUID = ContextUtils.getStringExtra(intent, INTENT_KEY_SWITCHBOARD_UUID);
SwitchBoard.setUUIDFromExtra(switchboardUUID);
// Looks at the server if there are changes in the server URL that should be used in the future
new AsyncConfigLoader(this, AsyncConfigLoader.UPDATE_SERVER, switchboardUUID).execute();
// Loads the actual config. This can be done on app start or on app onResume() depending
// how often you want to update the config.
new AsyncConfigLoader(this, AsyncConfigLoader.CONFIG_SERVER, switchboardUUID).execute();
// Loads the Switchboard config from the specified server URL. Eventually, we
// should use the endpoint returned by the server URL, to support migrating
// to a new endpoint. However, if we want to do that, we'll need to find a different
// solution for dynamically changing the server URL from the intent.
new AsyncConfigLoader(this, switchboardUUID, serverUrl).execute();
}
private void showUpdaterPermissionSnackbar() {

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.preferences;
import org.json.JSONArray;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.AdjustConstants;
import org.mozilla.gecko.AppConstants;
@ -17,7 +18,6 @@ import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoActivityStatus;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.LocaleManager;
@ -40,7 +40,6 @@ import org.mozilla.gecko.tabqueue.TabQueuePrompt;
import org.mozilla.gecko.updater.UpdateService;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.Experiments;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.InputOptionsUtils;
@ -90,8 +89,6 @@ import android.widget.LinearLayout;
import android.widget.ListAdapter;
import android.widget.ListView;
import com.keepsafe.switchboard.SwitchBoard;
import org.json.JSONObject;
import java.util.ArrayList;
@ -1154,12 +1151,28 @@ OnSharedPreferenceChangeListener
put(AndroidImportPreference.PREF_KEY, new AndroidImportPreference.Handler());
}};
private void recordSettingChangeTelemetry(String prefName, Object newValue) {
final String value;
if (newValue instanceof Boolean) {
value = (Boolean) newValue ? "1" : "0";
} else if (prefName.equals(PREFS_HOMEPAGE)) {
// Don't record the user's homepage preference.
value = "*";
} else {
value = newValue.toString();
}
final JSONArray extras = new JSONArray();
extras.put(prefName);
extras.put(value);
Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, Method.SETTINGS, extras.toString());
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final String prefName = preference.getKey();
Log.i(LOGTAG, "Changed " + prefName + " = " + newValue);
Telemetry.sendUIEvent(TelemetryContract.Event.EDIT, Method.SETTINGS, prefName);
recordSettingChangeTelemetry(prefName, newValue);
if (PREFS_MP_ENABLED.equals(prefName)) {
showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD);

View File

@ -0,0 +1,69 @@
package com.keepsafe.switchboard;
import android.content.Context;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.robolectric.RuntimeEnvironment;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.*;
@RunWith(TestRunner.class)
public class TestSwitchboard {
private static final String TEST_JSON = "{\"active-experiment\":{\"isActive\":true,\"values\":{\"foo\": true}},\"inactive-experiment\":{\"isActive\":false,\"values\":null}}";
@Before
public void setUp() throws IOException {
final Context c = RuntimeEnvironment.application;
// Avoid hitting the network by setting a config directly.
Preferences.setDynamicConfigJson(c, TEST_JSON);
}
@Test
public void testDeviceUuidFactory() {
final Context c = RuntimeEnvironment.application;
final DeviceUuidFactory df = new DeviceUuidFactory(c);
final UUID uuid = df.getDeviceUuid();
assertNotNull("UUID is not null", uuid);
assertEquals("DeviceUuidFactory always returns the same UUID", df.getDeviceUuid(), uuid);
}
@Test
public void testIsInExperiment() {
final Context c = RuntimeEnvironment.application;
assertTrue("active-experiment is active", SwitchBoard.isInExperiment(c, "active-experiment"));
assertFalse("inactive-experiment is inactive", SwitchBoard.isInExperiment(c, "inactive-experiment"));
}
@Test
public void testExperimentValues() throws JSONException {
final Context c = RuntimeEnvironment.application;
assertTrue("active-experiment has values", SwitchBoard.hasExperimentValues(c, "active-experiment"));
assertFalse("inactive-experiment doesn't have values", SwitchBoard.hasExperimentValues(c, "inactive-experiment"));
final JSONObject values = SwitchBoard.getExperimentValuesFromJson(c, "active-experiment");
assertNotNull("active-experiment values are not null", values);
assertTrue("\"foo\" extra value is true", values.getBoolean("foo"));
}
@Test
public void testGetActiveExperiments() {
final Context c = RuntimeEnvironment.application;
final List<String> experiments = SwitchBoard.getActiveExperiments(c);
assertNotNull("List of active experiments is not null", experiments);
assertTrue("List of active experiments contains active-experiemnt", experiments.contains("active-experiment"));
assertFalse("List of active experiments does not contain inactive-experiemnt", experiments.contains("inactive-experiment"));
}
}

View File

@ -18,7 +18,6 @@ package com.keepsafe.switchboard;
import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;
/**
* An async loader to load user config in background thread based on internal generated UUID.
@ -32,53 +31,27 @@ import android.util.Log;
*/
public class AsyncConfigLoader extends AsyncTask<Void, Void, Void> {
private String TAG = "AsyncConfigLoader";
public static final int UPDATE_SERVER = 1;
public static final int CONFIG_SERVER = 2;
private Context context;
private int configToLoad;
private String uuid;
/**
* Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
* or SwitchBoard.loadConfig.
* @param c Application context
* @param configType Either UPDATE_SERVER or CONFIG_SERVER
*/
public AsyncConfigLoader(Context c, int configType) {
this(c, configType, null);
}
/**
* Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
* or SwitchBoard.loadConfig.
* Loads config with a custom UUID
* @param c Application context
* @param configType Either UPDATE_SERVER or CONFIG_SERVER
* @param uuid Custom UUID
*/
public AsyncConfigLoader(Context c, int configType, String uuid) {
this.context = c;
this.configToLoad = configType;
this.uuid = uuid;
}
@Override
protected Void doInBackground(Void... params) {
if(configToLoad == UPDATE_SERVER) {
SwitchBoard.updateConfigServerUrl(context);
}
else {
if(uuid == null)
SwitchBoard.loadConfig(context);
else
SwitchBoard.loadConfig(context, uuid);
}
return null;
}
}
private Context context;
private String uuid;
private String defaultServerUrl;
/**
* Sets the params for async loading either SwitchBoard.updateConfigServerUrl()
* or SwitchBoard.loadConfig.
* Loads config with a custom UUID
* @param c Application context
* @param uuid Custom UUID
* @param defaultServerUrl Default URL endpoint for Switchboard config.
*/
public AsyncConfigLoader(Context c, String uuid, String defaultServerUrl) {
this.context = c;
this.uuid = uuid;
this.defaultServerUrl = defaultServerUrl;
}
@Override
protected Void doInBackground(Void... params) {
SwitchBoard.loadConfig(context, uuid, defaultServerUrl);
return null;
}
}

View File

@ -19,8 +19,6 @@ import java.util.UUID;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.Preference;
/**
* Generates a UUID and stores is persistent as in the apps shared preferences.
@ -28,53 +26,45 @@ import android.preference.Preference;
* @author Philipp Berner
*/
public class DeviceUuidFactory {
protected static final String PREFS_FILE = "com.keepsafe.switchboard.uuid";
protected static final String PREFS_DEVICE_ID = "device_id";
protected static final String PREFS_FILE = "com.keepsafe.switchboard.uuid";
protected static final String PREFS_DEVICE_ID = "device_id";
private static UUID uuid = null;
private static UUID uuid = null;
public DeviceUuidFactory(Context context) {
public DeviceUuidFactory(Context context) {
if (uuid == null) {
synchronized (DeviceUuidFactory.class) {
if (uuid == null) {
final SharedPreferences prefs = context
.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
final String id = prefs.getString(PREFS_DEVICE_ID, null);
if (uuid == null) {
synchronized (DeviceUuidFactory.class) {
if (uuid == null) {
final SharedPreferences prefs = context
.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
final String id = prefs.getString(PREFS_DEVICE_ID, null);
if (id != null) {
// Use the ids previously computed and stored in the prefs file
uuid = UUID.fromString(id);
} else {
uuid = UUID.randomUUID();
if (id != null) {
// Use the ids previously computed and stored in the
// prefs file
uuid = UUID.fromString(id);
// Write the value out to the prefs file
prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).apply();
}
}
}
}
}
} else {
/**
* Returns a unique UUID for the current android device. As with all UUIDs,
* this unique ID is "very highly likely" to be unique across all Android
* devices. Much more so than ANDROID_ID is.
*
* The UUID is generated with <code>UUID.randomUUID()</code>.
*
* @return a UUID that may be used to uniquely identify your device for most
* purposes.
*/
public UUID getDeviceUuid() {
return uuid;
}
UUID newId = UUID.randomUUID();
uuid = newId;
// Write the value out to the prefs file
prefs.edit()
.putString(PREFS_DEVICE_ID, newId.toString())
.commit();
}
}
}
}
}
/**
* Returns a unique UUID for the current android device. As with all UUIDs,
* this unique ID is "very highly likely" to be unique across all Android
* devices. Much more so than ANDROID_ID is.
*
* The UUID is generated with <code>UUID.randomUUID()</code>.
*
* @return a UUID that may be used to uniquely identify your device for most
* purposes.
*/
public UUID getDeviceUuid() {
return uuid;
}
}

View File

@ -18,7 +18,7 @@ package com.keepsafe.switchboard;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.support.annotation.Nullable;
/**
* Application preferences for SwitchBoard.
@ -26,91 +26,53 @@ import android.content.SharedPreferences.Editor;
*
*/
public class Preferences {
private static final String TAG = "Preferences";
private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
//dynamic config
private static final String kDynamicConfigServerUrl = "dynamic-config-server-url";
private static final String kDynamicConfigServerUpdateUrl = "dynamic-config-server-update-url";
private static final String kDynamicConfig = "dynamic-config";
//dynamic config
/** TODO check this!!!
* Returns a JSON string array with <br />
* position 0 = updateserverUrl <br />
* Fields a null if not existent.
* @param c
* @return
*/
public static String getDynamicUpdateServerUrl(Context c) {
SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
return settings.getString(kDynamicConfigServerUpdateUrl, null);
}
/**
* Returns a JSON string array with <br />
* postiion 1 = configServerUrl <br />
* Fields a null if not existent.
* @param c
* @return
*/
public static String getDynamicConfigServerUrl(Context c) {
SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
return settings.getString(kDynamicConfigServerUrl, null);
}
private static final String switchBoardSettings = "com.keepsafe.switchboard.settings";
/**
* Stores the config servers URL.
* @param c
* @param updateServerUrl Url end point to get the current config server location
* @param configServerUrl UR: end point to get the current endpoint for the apps config file
* @return true if saved successful
*/
public static boolean setDynamicConfigServerUrl(Context c, String updateServerUrl, String configServerUrl) {
SharedPreferences.Editor settings = (Editor) Preferences.getPreferenceObject(c, true);
settings.putString(kDynamicConfigServerUpdateUrl, updateServerUrl);
settings.putString(kDynamicConfigServerUrl, configServerUrl);
return settings.commit();
}
/**
* Gets the user config as a JSON string.
* @param c
* @return
*/
public static String getDynamicConfigJson(Context c) {
SharedPreferences settings = (SharedPreferences) Preferences.getPreferenceObject(c, false);
return settings.getString(kDynamicConfig, null);
}
private static final String kDynamicConfigServerUrl = "dynamic-config-server-url";
private static final String kDynamicConfig = "dynamic-config";
/**
* Saves the user config as a JSON sting.
* @param c
* @param configJson
* @return
*/
public static boolean setDynamicConfigJson(Context c, String configJson) {
SharedPreferences.Editor settings = (Editor) Preferences.getPreferenceObject(c, true);
settings.putString(kDynamicConfig, configJson);
return settings.commit();
}
/**
* Returns the stored config server URL.
* @param c Context
* @return URL for config endpoint.
*/
@Nullable public static String getDynamicConfigServerUrl(Context c) {
final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
return prefs.getString(kDynamicConfigServerUrl, null);
}
static private Object getPreferenceObject(Context ctx, boolean writeable) {
Object returnValue = null;
Context sharedDelegate = ctx.getApplicationContext();
if(!writeable) {
returnValue = sharedDelegate.getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
} else {
returnValue = sharedDelegate.getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
}
return returnValue;
}
/**
* Stores the config servers URL.
* @param c Context
* @param configServerUrl URL for config endpoint.
*/
public static void setDynamicConfigServerUrl(Context c, String configServerUrl) {
final SharedPreferences.Editor editor = c.getApplicationContext().
getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
editor.putString(kDynamicConfigServerUrl, configServerUrl);
editor.apply();
}
/**
* Gets the user config as a JSON string.
* @param c Context
* @return Config JSON
*/
@Nullable public static String getDynamicConfigJson(Context c) {
final SharedPreferences prefs = c.getApplicationContext().getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE);
return prefs.getString(kDynamicConfig, null);
}
/**
* Saves the user config as a JSON sting.
* @param c Context
* @param configJson Config JSON
*/
public static void setDynamicConfigJson(Context c, String configJson) {
final SharedPreferences.Editor editor = c.getApplicationContext().
getSharedPreferences(switchBoardSettings, Context.MODE_PRIVATE).edit();
editor.putString(kDynamicConfig, configJson);
editor.apply();
}
}

View File

@ -27,56 +27,46 @@ import android.content.Context;
*/
public class Switch {
private Context context;
private String experimentName;
/**
* Creates an instance of a single experiment to give more convenient access to its values.
* When the given experiment does not exist, it will give back default valued that can be found
* in <code>Switchboard</code>. Developer has to know that experiment exists when using it.
* @param c Application context
* @param experimentName Name of the experiment as defined on the server
*/
public Switch(Context c, String experimentName) {
this.context = c;
this.experimentName = experimentName;
}
/**
* Returns true if the experiment is active for this particular user.
* @return Status of the experiment and false when experiment does not exist.
*/
public boolean isActive() {
return SwitchBoard.isInExperiment(context, experimentName);
}
/**
* Returns the status of the experiment or the given default value when experiment
* does not exist.
* @param defaultValue Value to return when experiment does not exist.
* @return Experiment status
*/
public boolean isActive(boolean defaultValue) {
return SwitchBoard.isInExperiment(context, experimentName, defaultValue);
}
/**
* Returns true if the experiment has aditional values.
* @return true when values exist
*/
public boolean hasValues() {
return SwitchBoard.hasExperimentValues(context, experimentName);
}
/**
* Gives back all the experiment values in a JSONObject. This function checks if
* values exists. If no values exist, it returns null.
* @return Values in JSONObject or null if non
*/
public JSONObject getValues() {
if(hasValues())
return SwitchBoard.getExperimentValueFromJson(context, experimentName);
else
return null;
}
private Context context;
private String experimentName;
/**
* Creates an instance of a single experiment to give more convenient access to its values.
* When the given experiment does not exist, it will give back default valued that can be found
* in <code>Switchboard</code>. Developer has to know that experiment exists when using it.
* @param c Application context
* @param experimentName Name of the experiment as defined on the server
*/
public Switch(Context c, String experimentName) {
this.context = c;
this.experimentName = experimentName;
}
/**
* Returns true if the experiment is active for this particular user.
* @return Status of the experiment and false when experiment does not exist.
*/
public boolean isActive() {
return SwitchBoard.isInExperiment(context, experimentName);
}
/**
* Returns true if the experiment has additional values.
* @return true when values exist
*/
public boolean hasValues() {
return SwitchBoard.hasExperimentValues(context, experimentName);
}
/**
* Gives back all the experiment values in a JSONObject. This function checks if
* values exists. If no values exist, it returns null.
* @return Values in JSONObject or null if non
*/
public JSONObject getValues() {
if(hasValues())
return SwitchBoard.getExperimentValuesFromJson(context, experimentName);
else
return null;
}
}

View File

@ -20,7 +20,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
@ -33,12 +33,13 @@ import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.support.v4.content.LocalBroadcastManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
/**
* SwitchBoard is the core class of the KeepSafe Switchboard mobile A/B testing framework.
* This class provides a bunch of static methods that can be used in your app to run A/B tests.
@ -57,397 +58,254 @@ import android.util.Log;
*
*/
public class SwitchBoard {
private static final String TAG = "SwitchBoard";
/** Set if the application is run in debug mode. DynamicConfig runs against staging server when in debug and production when not */
public static boolean DEBUG = true;
/** Production server to update the remote server URLs. http://staging.domain/path_to/SwitchboardURLs.php */
private static String DYNAMIC_CONFIG_SERVER_URL_UPDATE;
/** Production server for getting the actual config file. http://staging.domain/path_to/SwitchboardDriver.php */
private static String DYNAMIC_CONFIG_SERVER_DEFAULT_URL;
public static final String ACTION_CONFIG_FETCHED = ".SwitchBoard.CONFIG_FETCHED";
private static final String kUpdateServerUrl = "updateServerUrl";
private static final String kConfigServerUrl = "configServerUrl";
private static final String IS_EXPERIMENT_ACTIVE = "isActive";
private static final String EXPERIMENT_VALUES = "values";
private static final String TAG = "SwitchBoard";
private static String uuidExtra = null;
/**
* Basic initialization with one server.
* @param configServerUpdateUrl Url to: http://staging.domain/path_to/SwitchboardURLs.php
* @param configServerUrl Url to: http://staging.domain/path_to/SwitchboardDriver.php - the acutall config
* @param isDebug Is the application running in debug mode. This will add log messages.
*/
public static void initDefaultServerUrls(String configServerUpdateUrl, String configServerUrl,
boolean isDebug) {
DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrl;
DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrl;
DEBUG = isDebug;
}
public static void setUUIDFromExtra(String uuid) {
uuidExtra = uuid;
}
/**
* Advanced initialization that supports a production and staging environment without changing the server URLs manually.
* SwitchBoard will connect to the staging environment in debug mode. This makes it very simple to test new experiements
* during development.
* @param configServerUpdateUrlStaging Url to http://staging.domain/path_to/SwitchboardURLs.php in staging environment
* @param configServerUrlStaging Url to: http://staging.domain/path_to/SwitchboardDriver.php in production - the acutall config
* @param configServerUpdateUrl Url to http://staging.domain/path_to/SwitchboardURLs.php in production environment
* @param configServerUrl Url to: http://staging.domain/path_to/SwitchboardDriver.php in production - the acutall config
* @param isDebug Defines if the app runs in debug.
*/
public static void initDefaultServerUrls(String configServerUpdateUrlStaging, String configServerUrlStaging,
String configServerUpdateUrl, String configServerUrl,
boolean isDebug) {
if(isDebug) {
DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrlStaging;
DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrlStaging;
} else {
DYNAMIC_CONFIG_SERVER_URL_UPDATE = configServerUpdateUrl;
DYNAMIC_CONFIG_SERVER_DEFAULT_URL = configServerUrl;
}
DEBUG = isDebug;
}
/**
* Updates the server URLs from remote and stores it locally in the app. This allows to move the server side
* whith users already using Switchboard.
* When there is no internet connection it will continue to use the URLs from the last time or
* default URLS that have been set with <code>initDefaultServerUrls</code>.
*
* This methode should always be executed in a background thread to not block the UI.
*
* @param c Application context
*/
public static void updateConfigServerUrl(Context c) {
if(DEBUG) Log.d(TAG, "start initConfigServerUrl");
if(DEBUG) {
//set default value that is set in code for debug mode.
Preferences.setDynamicConfigServerUrl(c, DYNAMIC_CONFIG_SERVER_URL_UPDATE, DYNAMIC_CONFIG_SERVER_DEFAULT_URL);
return;
}
//lookup new config server url from the one that is in shared prefs
String updateServerUrl = Preferences.getDynamicUpdateServerUrl(c);
//set to default when not set in preferences
if(updateServerUrl == null)
updateServerUrl = DYNAMIC_CONFIG_SERVER_URL_UPDATE;
try {
String result = readFromUrlGET(updateServerUrl, "");
if(DEBUG) Log.d(TAG, "Result String: " + result);
if(result != null){
JSONObject a = new JSONObject(result);
Preferences.setDynamicConfigServerUrl(c, (String)a.get(kUpdateServerUrl), (String)a.get(kConfigServerUrl));
if(DEBUG) Log.d(TAG, "Update Server Url: " + (String)a.get(kUpdateServerUrl));
if(DEBUG) Log.d(TAG, "Config Server Url: " + (String)a.get(kConfigServerUrl));
} else {
storeDefaultUrlsInPreferences(c);
}
} catch (JSONException e) {
e.printStackTrace();
}
if(DEBUG) Log.d(TAG, "end initConfigServerUrl");
}
/**
* Loads a new config file for the specific user from current config server. Uses internal unique user ID.
* Use this method only in background thread as network connections are involved that block UI thread.
* Use AsyncConfigLoader() for easy background threading.
* @param c ApplicationContext
*/
public static void loadConfig(Context c) {
loadConfig(c, null);
}
/** Set if the application is run in debug mode. */
public static boolean DEBUG = true;
/**
* Loads a new config for a user. This method allows you to pass your own unique user ID instead of using
* the SwitchBoard internal user ID.
* Don't call method direct for background threading reasons.
* @param c ApplicationContext
* @param uuid Custom unique user ID
*/
public static void loadConfig(Context c, String uuid) {
try {
//get uuid
if(uuid == null) {
DeviceUuidFactory df = new DeviceUuidFactory(c);
uuid = df.getDeviceUuid().toString();
}
String device = Build.DEVICE;
String manufacturer = Build.MANUFACTURER;
String lang = "unknown";
try {
lang = Locale.getDefault().getISO3Language();
} catch (MissingResourceException e) {
e.printStackTrace();
}
String country = "unknown";
try {
country = Locale.getDefault().getISO3Country();
} catch (MissingResourceException e) {
e.printStackTrace();
}
String packageName = c.getPackageName();
String versionName = "none";
try {
versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
//load config, includes all experiments
String serverUrl = Preferences.getDynamicConfigServerUrl(c);
if(serverUrl != null) {
String params = "uuid="+uuid+"&device="+device+"&lang="+lang+"&country="+country
+"&manufacturer="+manufacturer+"&appId="+packageName+"&version="+versionName;
if(DEBUG) Log.d(TAG, "Read from server URL: " + serverUrl + "?" + params);
String serverConfig = readFromUrlGET(serverUrl, params);
if(DEBUG) Log.d(TAG, serverConfig);
//store experiments in shared prefs (one variable)
if(serverConfig != null)
Preferences.setDynamicConfigJson(c, serverConfig);
}
} catch (NullPointerException e) {
e.printStackTrace();
}
private static final String IS_EXPERIMENT_ACTIVE = "isActive";
private static final String EXPERIMENT_VALUES = "values";
//notify listeners that the config fetch has completed
Intent i = new Intent(ACTION_CONFIG_FETCHED);
LocalBroadcastManager.getInstance(c).sendBroadcast(i);
}
private static final String KEY_SERVER_URL = "mainServerUrl";
private static final String KEY_CONFIG_RESULTS = "results";
public static boolean isInBucket(Context c, int low, int high) {
int userBucket = getUserBucket(c);
if (userBucket >= low && userBucket < high)
return true;
else
return false;
}
private static String uuidExtra = null;
/**
* Looks up in config if user is in certain experiment. Returns false as a default value when experiment
* does not exist.
* Experiment names are defined server side as Key in array for return values.
* @param experimentName Name of the experiment to lookup
* @return returns value for experiment or false if experiment does not exist.
*/
public static boolean isInExperiment(Context c, String experimentName) {
return isInExperiment(c, experimentName, false);
}
/**
* Looks up in config if user is in certain experiment.
* Experiment names are defined server side as Key in array for return values.
* @param experimentName Name of the experiment to lookup
* @param defaultReturnVal The return value that should be return when experiment does not exist
* @return returns value for experiment or defaultReturnVal if experiment does not exist.
*/
public static boolean isInExperiment(Context c, String experimentName, boolean defaultReturnVal) {
//lookup experiment in config
String config = Preferences.getDynamicConfigJson(c);
//if it does not exist
if(config == null)
return false;
else {
try {
JSONObject experiment = (JSONObject) new JSONObject(config).get(experimentName);
if(DEBUG) Log.d(TAG, "experiment " + experimentName + " JSON object: " + experiment.toString());
if(experiment == null)
return defaultReturnVal;
boolean returnValue = defaultReturnVal;
returnValue = experiment.getBoolean(IS_EXPERIMENT_ACTIVE);
return returnValue;
} catch (JSONException e) {
Log.e(TAG, "Config: " + config);
e.printStackTrace();
}
//return false when JSON fails
return defaultReturnVal;
}
}
/**
* @returns a list of all active experiments.
*/
public static List<String> getActiveExperiments(Context c) {
ArrayList<String> returnList = new ArrayList<String>();
public static void setUUIDFromExtra(String uuid) {
uuidExtra = uuid;
}
// lookup experiment in config
String config = Preferences.getDynamicConfigJson(c);
/**
* Loads a new config for a user. This method allows you to pass your own unique user ID instead of using
* the SwitchBoard internal user ID.
* Don't call method direct for background threading reasons.
* @param c ApplicationContext
* @param uuid Custom unique user ID
* @param defaultServerUrl Default server URL endpoint.
*/
static void loadConfig(Context c, String uuid, @NonNull String defaultServerUrl) {
// if it does not exist
if (config == null) {
return returnList;
}
// Eventually, we want to check `Preferences.getDynamicConfigServerUrl(c);` before
// falling back to the default server URL. However, this will require figuring
// out a new solution for dynamically specifying a new server from the intent.
String serverUrl = defaultServerUrl;
try {
JSONObject experiments = new JSONObject(config);
Iterator<?> iter = experiments.keys();
while (iter.hasNext()) {
String key = (String)iter.next();
JSONObject experiment = experiments.getJSONObject(key);
if (experiment.getBoolean(IS_EXPERIMENT_ACTIVE)) {
returnList.add(key);
}
}
} catch (JSONException e) {
// Something went wrong!
}
final URL requestUrl = buildConfigRequestUrl(c, uuid, serverUrl);
if (requestUrl == null) {
return;
}
return returnList;
}
if (DEBUG) Log.d(TAG, requestUrl.toString());
/**
* Checks if a certain experiment exists.
* @param c ApplicationContext
* @param experimentName Name of the experiment
* @return true when experiment exists
*/
public static boolean hasExperimentValues(Context c, String experimentName) {
if(getExperimentValueFromJson(c, experimentName) == null)
return false;
else
return true;
}
/**
* Returns the experiment value as a JSONObject. Depending on what experiment is has to be converted to the right type.
* Typcasting is by convention. You have to know what it's in there. Use <code>hasExperiment()</code>
* before this to avoid NullPointerExceptions.
* @param experimentName Name of the experiment to lookup
* @return Experiment value as String, null if experiment does not exist.
*/
public static JSONObject getExperimentValueFromJson(Context c, String experimentName) {
String config = Preferences.getDynamicConfigJson(c);
if(config == null)
return null;
try {
JSONObject experiment = (JSONObject) new JSONObject(config).get(experimentName);
JSONObject values = experiment.getJSONObject(EXPERIMENT_VALUES);
return values;
} catch (JSONException e) {
Log.e(TAG, "Config: " + config);
e.printStackTrace();
Log.e(TAG, "Could not create JSON object from config string", e);
}
return null;
}
/**
* Sets config server URLs in shared prefs to defaul when not set already. It keeps
* URLs when already set in shared preferences.
* @param c
*/
private static void storeDefaultUrlsInPreferences(Context c) {
String configUrl = Preferences.getDynamicConfigServerUrl(c);
String updateUrl = Preferences.getDynamicUpdateServerUrl(c);
if(configUrl == null)
configUrl = DYNAMIC_CONFIG_SERVER_DEFAULT_URL;
if(updateUrl == null)
updateUrl = DYNAMIC_CONFIG_SERVER_URL_UPDATE;
Preferences.setDynamicConfigServerUrl(c, updateUrl, configUrl);
}
/**
* Returns a String containing the server response from a GET request
* @param address Valid http addess.
* @param params String of params. Multiple params seperated with &. No leading ? in string
* @return Returns String from server or null when failed.
*/
private static String readFromUrlGET(String address, String params) {
if(address == null || params == null)
return null;
String completeUrl = address + "?" + params;
if(DEBUG) Log.d(TAG, "readFromUrl(): " + completeUrl);
try {
URL url = new URL(completeUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(false);
final String result = readFromUrlGET(requestUrl);
if (DEBUG) Log.d(TAG, result);
// get response
InputStream is = connection.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(is);
BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
String line = "";
StringBuffer resultContent = new StringBuffer();
while ((line = bufferReader.readLine()) != null) {
if(DEBUG) Log.d(TAG, line);
resultContent.append(line);
}
bufferReader.close();
if(DEBUG) Log.d(TAG, "readFromUrl() result: " + resultContent.toString());
return resultContent.toString();
} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
if (result == null) {
return;
}
return null;
}
try {
final JSONObject json = new JSONObject(result);
/**
* Return the bucket number of the user. There are 100 possible buckets.
*/
private static int getUserBucket(Context c) {
//get uuid
String uuid = uuidExtra;
if (uuid == null) {
DeviceUuidFactory df = new DeviceUuidFactory(c);
uuid = df.getDeviceUuid().toString();
}
// Update the server URL if necessary.
final String newServerUrl = json.getString(KEY_SERVER_URL);
if (!defaultServerUrl.equals(newServerUrl)) {
Preferences.setDynamicConfigServerUrl(c, newServerUrl);
}
CRC32 crc = new CRC32();
crc.update(uuid.getBytes());
long checksum = crc.getValue();
return (int)(checksum % 100L);
}
// Store the config in shared prefs.
final String config = json.getString(KEY_CONFIG_RESULTS);
Preferences.setDynamicConfigJson(c, config);
} catch (JSONException e) {
Log.e(TAG, "Exception parsing server result", e);
}
}
@Nullable private static URL buildConfigRequestUrl(Context c, String uuid, String serverUrl) {
if (uuid == null) {
DeviceUuidFactory df = new DeviceUuidFactory(c);
uuid = df.getDeviceUuid().toString();
}
final String device = Build.DEVICE;
final String manufacturer = Build.MANUFACTURER;
String lang = "unknown";
try {
lang = Locale.getDefault().getISO3Language();
} catch (MissingResourceException e) {
e.printStackTrace();
}
String country = "unknown";
try {
country = Locale.getDefault().getISO3Country();
} catch (MissingResourceException e) {
e.printStackTrace();
}
final String packageName = c.getPackageName();
String versionName = "none";
try {
versionName = c.getPackageManager().getPackageInfo(c.getPackageName(), 0).versionName;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
final String params = "uuid="+uuid+"&device="+device+"&lang="+lang+"&country="+country
+"&manufacturer="+manufacturer+"&appId="+packageName+"&version="+versionName;
try {
return new URL(serverUrl + "?" + params);
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}
public static boolean isInBucket(Context c, int low, int high) {
int userBucket = getUserBucket(c);
if (userBucket >= low && userBucket < high)
return true;
else
return false;
}
/**
* Looks up in config if user is in certain experiment. Returns false as a default value when experiment
* does not exist.
* Experiment names are defined server side as Key in array for return values.
* @param experimentName Name of the experiment to lookup
* @return returns value for experiment or false if experiment does not exist.
*/
public static boolean isInExperiment(Context c, String experimentName) {
final String config = Preferences.getDynamicConfigJson(c);
if (config == null) {
return false;
}
try {
final JSONObject experiment = new JSONObject(config).getJSONObject(experimentName);
if(DEBUG) Log.d(TAG, "experiment " + experimentName + " JSON object: " + experiment.toString());
return experiment != null && experiment.getBoolean(IS_EXPERIMENT_ACTIVE);
} catch (JSONException e) {
Log.e(TAG, "Error getting experiment from config", e);
return false;
}
}
/**
* @returns a list of all active experiments.
*/
public static List<String> getActiveExperiments(Context c) {
ArrayList<String> returnList = new ArrayList<String>();
// lookup experiment in config
String config = Preferences.getDynamicConfigJson(c);
// if it does not exist
if (config == null) {
return returnList;
}
try {
JSONObject experiments = new JSONObject(config);
Iterator<?> iter = experiments.keys();
while (iter.hasNext()) {
String key = (String)iter.next();
JSONObject experiment = experiments.getJSONObject(key);
if (experiment.getBoolean(IS_EXPERIMENT_ACTIVE)) {
returnList.add(key);
}
}
} catch (JSONException e) {
// Something went wrong!
}
return returnList;
}
/**
* Checks if a certain experiment has additional values.
* @param c ApplicationContext
* @param experimentName Name of the experiment
* @return true when experiment exists
*/
public static boolean hasExperimentValues(Context c, String experimentName) {
return getExperimentValuesFromJson(c, experimentName) != null;
}
/**
* Returns the experiment value as a JSONObject.
* @param experimentName Name of the experiment
* @return Experiment value as String, null if experiment does not exist.
*/
public static JSONObject getExperimentValuesFromJson(Context c, String experimentName) {
final String config = Preferences.getDynamicConfigJson(c);
if (config == null) {
return null;
}
try {
final JSONObject experiment = new JSONObject(config).getJSONObject(experimentName);
return experiment.getJSONObject(EXPERIMENT_VALUES);
} catch (JSONException e) {
Log.e(TAG, "Could not create JSON object from config string", e);
}
return null;
}
/**
* Returns a String containing the server response from a GET request
* @param url URL for GET request.
* @return Returns String from server or null when failed.
*/
@Nullable private static String readFromUrlGET(URL url) {
if (DEBUG) Log.d(TAG, "readFromUrl(): " + url);
try {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setUseCaches(false);
InputStream is = connection.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(is);
BufferedReader bufferReader = new BufferedReader(inputStreamReader, 8192);
String line = "";
StringBuilder resultContent = new StringBuilder();
while ((line = bufferReader.readLine()) != null) {
if(DEBUG) Log.d(TAG, line);
resultContent.append(line);
}
bufferReader.close();
if(DEBUG) Log.d(TAG, "readFromUrl() result: " + resultContent.toString());
return resultContent.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* Return the bucket number of the user. There are 100 possible buckets.
*/
private static int getUserBucket(Context c) {
//get uuid
String uuid = uuidExtra;
if (uuid == null) {
DeviceUuidFactory df = new DeviceUuidFactory(c);
uuid = df.getDeviceUuid().toString();
}
CRC32 crc = new CRC32();
crc.update(uuid.getBytes());
long checksum = crc.getValue();
return (int)(checksum % 100L);
}
}

View File

@ -28,7 +28,6 @@ user_pref("javascript.options.showInConsole", true);
user_pref("devtools.browsertoolbox.panel", "jsdebugger");
user_pref("devtools.debugger.remote-port", 6023);
user_pref("devtools.devedition.promo.enabled", false);
user_pref("devtools.errorconsole.enabled", true);
user_pref("browser.EULA.override", true);
user_pref("gfx.color_management.force_srgb", true);
user_pref("network.manage-offline-status", false);

View File

@ -2739,6 +2739,7 @@ SearchService.prototype = {
Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
Services.telemetry.getHistogramById("SEARCH_SERVICE_INIT_SYNC").add(true);
this._recordEnginesWithUpdate();
LOG("_syncInit end");
},
@ -2781,6 +2782,7 @@ SearchService.prototype = {
this._initObservers.resolve(this._initRV);
Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
Services.telemetry.getHistogramById("SEARCH_SERVICE_INIT_SYNC").add(false);
this._recordEnginesWithUpdate();
LOG("_asyncInit: Completed _asyncInit");
}.bind(this));
@ -3149,6 +3151,7 @@ SearchService.prototype = {
// Typically we'll re-init as a result of a pref observer,
// so signal to 'callers' that we're done.
Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete");
this._recordEnginesWithUpdate();
gInitialized = true;
} catch (err) {
LOG("Reinit failed: " + err);
@ -4233,6 +4236,23 @@ SearchService.prototype = {
return result;
},
_recordEnginesWithUpdate: function() {
let hasUpdates = false;
let hasIconUpdates = false;
for (let name in this._engines) {
let engine = this._engines[name];
if (engine._hasUpdates) {
hasUpdates = true;
if (engine._iconUpdateURL) {
hasIconUpdates = true;
break;
}
}
}
Services.telemetry.getHistogramById("SEARCH_SERVICE_HAS_UPDATES").add(hasUpdates);
Services.telemetry.getHistogramById("SEARCH_SERVICE_HAS_ICON_UPDATES").add(hasIconUpdates);
},
/**
* This map is built lazily after the available search engines change. It
* allows quick parsing of an URL representing a search submission into the

View File

@ -0,0 +1,10 @@
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>update</ShortName>
<Description>update</Description>
<InputEncoding>UTF-8</InputEncoding>
<Url type="text/html" method="GET" template="http://searchtest.local">
<Param name="search" value="{searchTerms}"/>
</Url>
<UpdateUrl>http://searchtest.local/opensearch.xml</UpdateUrl>
<IconUpdateUrl>http://searchtest.local/favicon.ico</IconUpdateUrl>
</SearchPlugin>

View File

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test() {
do_check_false(Services.search.isInitialized);
useHttpServer();
run_next_test();
}
function checkTelemetry(histogramName, expected) {
let histogram = Services.telemetry.getHistogramById(histogramName);
let snapshot = histogram.snapshot();
let expectedCounts = [0, 0, 0];
expectedCounts[expected ? 1 : 0] = 1;
Assert.deepEqual(snapshot.counts, expectedCounts,
"histogram has expected content");
histogram.clear();
}
add_task(function* ignore_cache_files_without_engines() {
yield asyncInit();
checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", false);
checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", false);
// Add an engine with update urls and re-init, as we record the presence of
// engine update urls only while initializing the search service.
yield addTestEngines([
{ name: "update", xmlFileName: "engine-update.xml" },
]);
yield asyncReInit();
checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", true);
checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", true);
});

View File

@ -17,6 +17,7 @@ support-files =
data/engine-rel-searchform-post.xml
data/engine-rel-searchform-purpose.xml
data/engine-system-purpose.xml
data/engine-update.xml
data/engineImages.xml
data/ico-size-16x16-png.ico
data/invalid-engine.xml
@ -89,4 +90,5 @@ tags = addons
[test_hidden.js]
[test_currentEngine_fallback.js]
[test_require_engines_in_cache.js]
[test_update_telemetry.js]
[test_svg_icon.js]

View File

@ -5375,6 +5375,22 @@
"kind": "boolean",
"description": "search service has been initialized synchronously"
},
"SEARCH_SERVICE_HAS_UPDATES": {
"alert_emails": ["florian@mozilla.com"],
"expires_in_version": "50",
"kind": "boolean",
"bug_numbers": [1259510],
"description": "Recorded once per session near startup: records true/false whether the search service has engines with update URLs.",
"releaseChannelCollection": "opt-out"
},
"SEARCH_SERVICE_HAS_ICON_UPDATES": {
"alert_emails": ["florian@mozilla.com"],
"expires_in_version": "50",
"kind": "boolean",
"bug_numbers": [1259510],
"description": "Recorded once per session near startup: records true/false whether the search service has engines with icon update URLs.",
"releaseChannelCollection": "opt-out"
},
"SEARCH_SERVICE_BUILD_CACHE_MS": {
"expires_in_version": "40",
"kind": "exponential",