author | Wes Kocher <wkocher@mozilla.com> |
Tue, 05 Apr 2016 16:25:35 -0700 | |
changeset 291796 | a235bfcc8c411169b82420c503775c1a3e7edad5 |
parent 291735 | 17a0ded9bb99c05c25729c306b91771483109067 (current diff) |
parent 291795 | 71d3427c683190cdb409063ad67e24d5926f4591 (diff) |
child 291797 | b70ae970d45dcf9c8be267fbe3a61114a59cd8ef |
push id | 74679 |
push user | kwierso@gmail.com |
push date | Tue, 05 Apr 2016 23:39:26 +0000 |
treeherder | mozilla-inbound@b70ae970d45d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 48.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/addon-sdk/source/python-lib/cuddlefish/prefs.py +++ b/addon-sdk/source/python-lib/cuddlefish/prefs.py @@ -107,17 +107,16 @@ DEFAULT_FENNEC_PREFS = { 'browser.firstrun.show.uidiscovery': False } # When launching a temporary new Firefox profile, use these preferences. 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: # http://hg.mozilla.org/mozilla-central/file/1dd81c324ac7/build/automation.py.in#l388 # Make url-classifier updates so rare that they won't affect tests. 'urlclassifier.updateinterval' : 172800, # Point the url-classifier to a nonexistent local URL for fast failures. 'browser.safebrowsing.provider.google.gethashURL' : 'http://localhost/safebrowsing-dummy/gethash',
--- a/addon-sdk/source/test/preferences/firefox.json +++ b/addon-sdk/source/test/preferences/firefox.json @@ -1,12 +1,11 @@ { "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", "browser.safebrowsing.provider.google.updateURL": "http://localhost/safebrowsing-dummy/update", "browser.safebrowsing.provider.mozilla.gethashURL": "http://localhost/safebrowsing-dummy/gethash", "browser.safebrowsing.provider.mozilla.updateURL": "http://localhost/safebrowsing-dummy/update" }
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -152,22 +152,16 @@ XPCOMUtils.defineLazyGetter(this, "Popup document.getElementById("notification-popup"), document.getElementById("notification-popup-box")); } catch (ex) { Cu.reportError(ex); return null; } }); -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); return tmp.BrowserToolboxProcess; }); XPCOMUtils.defineLazyModuleGetter(this, "Social", "resource:///modules/Social.jsm"); @@ -1395,21 +1389,16 @@ var gBrowserInit = { onUnload: function() { // In certain scenarios it's possible for unload to be fired before onload, // (e.g. if the window is being closed after browser.js loads but before the // load completes). In that case, there's nothing to do here. 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). CombinedStopReload.uninit(); gGestureSupport.init(false); gHistorySwipeAnimation.uninit();
--- a/browser/modules/ContentWebRTC.jsm +++ b/browser/modules/ContentWebRTC.jsm @@ -125,16 +125,21 @@ function handlePCRequest(aSubject, aTopi function handleGUMRequest(aSubject, aTopic, aData) { let constraints = aSubject.getConstraints(); let secure = aSubject.isSecure; let contentWindow = Services.wm.getOuterWindowWithId(aSubject.windowID); 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); }, function (error) { // bug 827146 -- In the future, the UI should catch NotFoundError // and allow the user to plug in a device, instead of immediately failing. denyGUMRequest({callID: aSubject.callID}, error); },
--- a/devtools/bootstrap.js +++ b/devtools/bootstrap.js @@ -107,34 +107,16 @@ function reload(event) { // We have to use a frame script to query "baseURI" mm.loadFrameScript("data:text/javascript,new " + function () { let isJSONView = content.document.baseURI.startsWith("resource://devtools/"); if (isJSONView) { content.location.reload(); } }, 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") { // Browser console document can't just be reloaded. // HUDService is going to close it on unload. // Instead we have to manually toggle it. let HUDService = devtools.require("devtools/client/webconsole/hudservice"); HUDService.toggleBrowserConsole()
--- a/devtools/client/animationinspector/components/animation-details.js +++ b/devtools/client/animationinspector/components/animation-details.js @@ -75,24 +75,22 @@ AnimationDetails.prototype = { * that returns the animated css properties of the animation and their * keyframes values. * If the animation actor has the getProperties function, we use it, and if * not, we fall back to getFrames, which then returns values we used to * handle. */ 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}); } } } else { let frames = yield this.animation.getFrames(); for (let frame of frames) { for (let name in frame) { if (this.NON_PROPERTIES.indexOf(name) != -1) {
--- a/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js +++ b/devtools/client/animationinspector/test/browser_animation_keyframe_markers.js @@ -55,23 +55,20 @@ add_task(function*() { function* getExpectedKeyframesData(animation) { // We're testing the UI state here, so it's fine to get the list of expected // properties from the animation actor. let properties = yield animation.getProperties(); let data = {}; 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}); } } } return data; }
--- a/devtools/client/framework/browser-menus.js +++ b/devtools/client/framework/browser-menus.js @@ -11,156 +11,101 @@ * - devtools/client/menus for top level entires * - devtools/client/definitions for tool-specifics entries */ 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 const FragmentsCache = new Map(); function l10n(key) { return MenuStrings.GetStringFromName(key); } /** * Create a xul:key element * * @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 (modifiers) { + k.setAttribute("modifiers", modifiers); } - if (key.modifiers) { - k.setAttribute("modifiers", key.modifiers); - } + + // Bug 371900: command event is fired only if "oncommand" attribute is set. + k.setAttribute("oncommand", ";"); + k.addEventListener("command", oncommand); + return k; } /** * Create a xul:menuitem element * * @param {XULDocument} doc * The document to which keys are to be added. * @param {String} id * 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); } if (isCheckbox) { menuitem.setAttribute("type", "checkbox"); menuitem.setAttribute("autocheck", "false"); } 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 * to be detached and reattached to make sure the <key> is taken into * account (see bug 832984). * * @param {XULDocument} doc * The document to which keys are to be added * @param {XULElement} or {DocumentFragment} keys @@ -183,175 +128,139 @@ function attachKeybindingsToBrowser(doc, * * @param {Object} toolDefinition * Tool definition of the tool to add a menu entry. * @param {XULDocument} doc * The document to which the tool menu item is to be added. */ 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 * The document to which the tool is to be registered. * @param {Object} toolDefinition * Tool definition of the tool to register. * @param {Object} prevDef * The tool definition after which the tool menu item is to be added. */ function insertToolMenuElements(doc, toolDefinition, prevDef) { - let elements = createToolMenuElements(toolDefinition, doc); - - doc.getElementById("mainCommandSet").appendChild(elements.cmd); + let { key, menuitem } = createToolMenuElements(toolDefinition, doc); - 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); ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null; } else { ref = doc.getElementById("menu_devtools_separator"); } if (ref) { - ref.parentNode.insertBefore(elements.menuitem, ref); + ref.parentNode.insertBefore(menuitem, ref); } } exports.insertToolMenuElements = insertToolMenuElements; /** * Remove a tool's menuitem from a window * * @param {string} toolId * Id of the tool to add a menu entry for * @param {XULDocument} doc * 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; /** * Add all tools to the developer tools menu of a window. * * @param {XULDocument} doc * 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()) { if (!toolDefinition.inMenu) { continue; } let elements = createToolMenuElements(toolDefinition, doc); if (!elements) { 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); } } /** * Add global menus and shortcuts that are not panel specific. @@ -380,31 +289,41 @@ function addTopLevelItems(doc) { accesskey: l10n(l10nKey + ".accesskey"), isCheckbox: item.checkbox }); menuitem.addEventListener("command", item.oncommand); menuItems.appendChild(menuitem); 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); } 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); } } } } // Cache all nodes before insertion to be able to remove them on unload let nodes = [];
--- a/devtools/client/framework/devtools-browser.js +++ b/devtools/client/framework/devtools-browser.js @@ -117,20 +117,16 @@ var gDevToolsBrowser = exports.gDevTools // Enable Browser Toolbox? let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled"); let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled"); let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled; 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); }, observe: function(subject, topic, prefName) { switch (topic) { case "browser-delayed-startup-finished": this._registerBrowserWindow(subject); @@ -351,16 +347,23 @@ var gDevToolsBrowser = exports.gDevTools */ _registerBrowserWindow: function(win) { if (gDevToolsBrowser._trackedBrowserWindows.has(win)) { return; } 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); let tabContainer = win.gBrowser.tabContainer; tabContainer.addEventListener("TabSelect", this, false); tabContainer.addEventListener("TabOpen", this, false); tabContainer.addEventListener("TabClose", this, false); @@ -550,28 +553,37 @@ var gDevToolsBrowser = exports.gDevTools /** * Called on browser unload to remove menu entries, toolboxes and event * listeners from the closed browser window. * * @param {XULWindow} win * The window containing the menu entry */ _forgetBrowserWindow: function(win) { + if (!gDevToolsBrowser._trackedBrowserWindows.has(win)) { + return; + } gDevToolsBrowser._trackedBrowserWindows.delete(win); win.removeEventListener("unload", this); BrowserMenus.removeMenus(win.document); // Destroy toolboxes for closed window for (let [target, toolbox] of gDevTools._toolboxes) { if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) { toolbox.destroy(); } } + // 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); tabContainer.removeEventListener("TabClose", this, false); tabContainer.removeEventListener("TabPinned", this, false); tabContainer.removeEventListener("TabUnpinned", this, false); },
--- a/devtools/client/framework/test/browser_dynamic_tool_enabling.js +++ b/devtools/client/framework/test/browser_dynamic_tool_enabling.js @@ -3,17 +3,16 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // Tests that toggling prefs immediately (de)activates the relevant menuitem 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", }; function expectedAttributeValueFromPrefs(prefs) { return prefs.every((pref) => Services.prefs.getBoolPref(pref)) ? "" : "true"; }
--- a/devtools/client/framework/test/browser_toolbox_dynamic_registration.js +++ b/devtools/client/framework/test/browser_toolbox_dynamic_registration.js @@ -23,17 +23,18 @@ function testRegister(aToolbox) toolbox = aToolbox gDevTools.once("tool-registered", toolRegistered); gDevTools.registerTool({ id: "test-tool", label: "Test Tool", inMenu: true, isTargetSupported: () => true, - build: function() {} + build: function() {}, + key: "t" }); } function toolRegistered(event, toolId) { is(toolId, "test-tool", "tool-registered event handler sent tool id"); ok(gDevTools.getToolDefinitionMap().has(toolId), "tool added to map"); @@ -42,18 +43,18 @@ function toolRegistered(event, toolId) let doc = toolbox.frame.contentDocument; let tab = doc.getElementById("toolbox-tab-" + toolId); ok(tab, "new tool's tab exists in toolbox UI"); let panel = doc.getElementById("toolbox-panel-" + 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"); } // then unregister it testUnregister(); } @@ -84,18 +85,18 @@ function toolUnregistered(event, toolDef let doc = toolbox.frame.contentDocument; let tab = doc.getElementById("toolbox-tab-" + toolId); ok(!tab, "tool's tab was removed from the toolbox UI"); let panel = doc.getElementById("toolbox-panel-" + toolId); 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"); } cleanup(); } function cleanup()
--- a/devtools/client/locales/en-US/menus.properties +++ b/devtools/client/locales/en-US/menus.properties @@ -6,19 +6,16 @@ devToolsCmd.key = VK_F12 devToolsCmd.keytext = F12 devtoolsServiceWorkers.label = Service Workers 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 responsiveDesignMode.label = Responsive Design Mode responsiveDesignMode.accesskey = R responsiveDesignMode.key = M
--- a/devtools/client/menus.js +++ b/devtools/client/menus.js @@ -165,24 +165,16 @@ exports.menuitems = [ oncommand() { ScratchpadManager.openScratchpad(); }, key: { id: "scratchpad", 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, oncommand(event) { let window = event.target.ownerDocument.defaultView; gDevToolsBrowser.openAboutDebugging(window.gBrowser, "workers"); } },
--- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -9,19 +9,16 @@ pref("devtools.devedition.promo.url", "h // Only potentially show in beta release #if MOZ_UPDATE_CHANNEL == beta pref("devtools.devedition.promo.enabled", true); #else pref("devtools.devedition.promo.enabled", false); #endif -// Disable the error console -pref("devtools.errorconsole.enabled", false); - // DevTools development workflow pref("devtools.loader.hotreload", false); // Developer toolbar preferences pref("devtools.toolbar.enabled", true); pref("devtools.toolbar.visible", false); // Enable DevTools WebIDE by default
--- a/devtools/client/shared/developer-toolbar.js +++ b/devtools/client/shared/developer-toolbar.js @@ -493,20 +493,16 @@ DeveloperToolbar.prototype.show = functi let tabbrowser = this._chromeWindow.gBrowser; tabbrowser.tabContainer.addEventListener("TabSelect", this, false); tabbrowser.tabContainer.addEventListener("TabClose", this, false); tabbrowser.addEventListener("load", this, true); 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; if (focus) { // If the toolbar was just inserted, the <textbox> may still have // its binding in process of being applied and not be focusable yet let waitForBinding = () => { // Bail out if the toolbar has been destroyed in the meantime @@ -567,34 +563,16 @@ DeveloperToolbar.prototype.hide = functi this._hidePromise = null; }); 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. * * @private * @param nsIDOMNode tab the xul:tab for which you want to track the number of * errors. */ DeveloperToolbar.prototype._initErrorsCount = function(tab) { @@ -652,18 +630,16 @@ DeveloperToolbar.prototype.destroy = fun } let tabbrowser = this._chromeWindow.gBrowser; tabbrowser.tabContainer.removeEventListener("TabSelect", this, false); tabbrowser.tabContainer.removeEventListener("TabClose", this, false); 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); this.focusManager.removeMonitoredElement(this._element); this.focusManager.onVisibilityChange.remove(this.outputPanel._visibilityChanged, this.outputPanel); this.focusManager.onVisibilityChange.remove(this.tooltipPanel._visibilityChanged,
--- a/devtools/server/actors/animation.js +++ b/devtools/server/actors/animation.js @@ -439,25 +439,27 @@ var AnimationPlayerActor = ActorClass({ request: {}, response: { frames: RetVal("json") } }), /** * 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") } }) }); exports.AnimationPlayerActor = AnimationPlayerActor; var AnimationPlayerFront = FrontClass(AnimationPlayerActor, { initialize: function(conn, form, detail, ctx) {
--- a/devtools/server/tests/browser/browser_animation_getProperties.js +++ b/devtools/server/tests/browser/browser_animation_getProperties.js @@ -5,30 +5,29 @@ "use strict"; // Check that the AnimationPlayerActor exposes a getProperties method that // returns the list of animated properties in the animation. 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"); let [player] = yield animations.getAnimationPlayersForNode(node); ok(player.getProperties, "The front has the getProperties method"); let properties = yield player.getProperties(); 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"); // Note that we don't really test the content of the frame object here on // purpose. This object comes straight out of the web animations API // unmodified.
--- a/devtools/shared/Loader.jsm +++ b/devtools/shared/Loader.jsm @@ -262,18 +262,16 @@ DevToolsLoader.prototype = { * Override the provider used to load the tools. */ setProvider: function(provider) { if (provider === this._provider) { return; } if (this._provider) { - var events = this.require("sdk/system/events"); - events.emit("devtools-unloaded", {}); delete this.require; this._provider.unload("newprovider"); } this._provider = provider; // Pass through internal loader settings specific to this loader instance this._provider.invisibleToDebugger = this.invisibleToDebugger; // Changes here should be mirrored to devtools/.eslintrc. @@ -325,17 +323,16 @@ DevToolsLoader.prototype = { }, /** * Reload the current provider. */ reload: function() { var events = this.require("sdk/system/events"); events.emit("startupcache-invalidate", {}); - events.emit("devtools-unloaded", {}); this._provider.unload("reload"); delete this._provider; let mainid = this._mainid; delete this._mainid; this._loadProvider(); this.main(mainid); },
--- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -421,18 +421,16 @@ pref("javascript.options.mem.gc_min_empt pref("javascript.options.mem.gc_max_empty_chunk_count", 2); #else pref("javascript.options.mem.high_water_mark", 32); #endif 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"); pref("devtools.remote.usb.enabled", false); pref("devtools.remote.wifi.enabled", false); pref("font.size.inflation.minTwips", 0);
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java +++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java @@ -812,37 +812,32 @@ public class BrowserApp extends GeckoApp private void initSwitchboard(Intent intent) { if (Experiments.isDisabled(new SafeIntent(intent)) || !AppConstants.MOZ_SWITCHBOARD) { return; } 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() { SnackbarHelper.SnackbarCallback allowCallback = new SnackbarHelper.SnackbarCallback() { @Override public void onClick(View v) { Permissions.from(BrowserApp.this) .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java +++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java @@ -1,28 +1,28 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * 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/. */ package org.mozilla.gecko.preferences; +import org.json.JSONArray; import org.mozilla.gecko.AboutPages; import org.mozilla.gecko.AdjustConstants; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.BrowserApp; import org.mozilla.gecko.BrowserLocaleManager; import org.mozilla.gecko.DataReportingNotification; import org.mozilla.gecko.DynamicToolbar; 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; import org.mozilla.gecko.Locales; import org.mozilla.gecko.PrefsHelper; import org.mozilla.gecko.R; import org.mozilla.gecko.SnackbarHelper; import org.mozilla.gecko.Telemetry; @@ -35,17 +35,16 @@ import org.mozilla.gecko.feeds.action.Ch import org.mozilla.gecko.permissions.Permissions; import org.mozilla.gecko.restrictions.Restrictable; import org.mozilla.gecko.restrictions.Restrictions; import org.mozilla.gecko.tabqueue.TabQueueHelper; 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; import org.mozilla.gecko.util.NativeEventListener; import org.mozilla.gecko.util.NativeJSObject; import org.mozilla.gecko.util.ThreadUtils; import android.annotation.TargetApi; @@ -85,18 +84,16 @@ import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; 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; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -1149,22 +1146,38 @@ OnSharedPreferenceChangeListener } @SuppressWarnings("serial") private final Map<String, PrefHandler> handlers = new HashMap<String, PrefHandler>() {{ put(ClearOnShutdownPref.PREF, new ClearOnShutdownPref()); 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); // We don't want the "use master password" pref to change until the // user has gone through the dialog. return false; }
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/background/junit4/src/com/keepsafe/switchboard/TestSwitchboard.java @@ -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")); + } + +}
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java +++ b/mobile/android/thirdparty/com/keepsafe/switchboard/AsyncConfigLoader.java @@ -13,72 +13,45 @@ See the License for the specific language governing permissions and limitations under the License. */ 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. * * Call <code>AsyncConfigLoader.execute()</code> to load SwitchBoard.loadConfig() with own ID. * To use your custom UUID call <code>AsyncConfigLoader.execute(uuid)</code> with uuid being your unique user id * as a String * * @author Philipp Berner * */ 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; - } - -} \ No newline at end of file + 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; + } +}
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java +++ b/mobile/android/thirdparty/com/keepsafe/switchboard/DeviceUuidFactory.java @@ -14,67 +14,57 @@ limitations under the License. */ package com.keepsafe.switchboard; 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. * * @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; - - public DeviceUuidFactory(Context context) { + private static UUID uuid = 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); + 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 (id != null) { - // Use the ids previously computed and stored in the - // prefs file - uuid = UUID.fromString(id); - - } else { + if (id != null) { + // Use the ids previously computed and stored in the prefs file + uuid = UUID.fromString(id); + } else { + uuid = UUID.randomUUID(); - UUID newId = UUID.randomUUID(); - uuid = newId; - - // Write the value out to the prefs file - prefs.edit() - .putString(PREFS_DEVICE_ID, newId.toString()) - .commit(); - - } - } - } - } - } + // Write the value out to the prefs file + prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString()).apply(); + } + } + } + } + } - /** - * 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; - } - + /** + * 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; + } + } \ No newline at end of file
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java +++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Preferences.java @@ -13,104 +13,66 @@ See the License for the specific language governing permissions and limitations under the License. */ 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. * @author Philipp Berner * */ 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"; - - + + private static final String switchBoardSettings = "com.keepsafe.switchboard.settings"; + + private static final String kDynamicConfigServerUrl = "dynamic-config-server-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); - } + /** + * 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); + } - /** - * 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); - } + /** + * 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(); + } - /** - * 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(); - } + /** + * 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); + } - 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; - } + /** + * 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(); + } }
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java +++ b/mobile/android/thirdparty/com/keepsafe/switchboard/Switch.java @@ -22,61 +22,51 @@ import android.content.Context; /** * Single instance of an existing experiment for easier and cleaner code. * * @author Philipp Berner * */ 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; + } }
--- a/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java +++ b/mobile/android/thirdparty/com/keepsafe/switchboard/SwitchBoard.java @@ -15,35 +15,36 @@ */ package com.keepsafe.switchboard; import java.io.BufferedReader; 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; import java.util.List; import java.util.Locale; import java.util.MissingResourceException; import java.util.zip.CRC32; 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. * * The SwitchBoard supports production and staging environment. * * For usage <code>initDefaultServerUrls</code> for first time usage. Server URLs can be updates from * a remote location with <code>initConfigServerUrl</code>. @@ -52,402 +53,259 @@ import android.util.Log; * setup on the server. * All functions are design to be safe for programming mistakes and network connection issues. If the * experiment does not exists it will return false and pretend the user is not part of it. * * @author Philipp Berner * */ 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 TAG = "SwitchBoard"; + + /** Set if the application is run in debug mode. */ + public static boolean DEBUG = true; + + private static final String IS_EXPERIMENT_ACTIVE = "isActive"; + private static final String EXPERIMENT_VALUES = "values"; + + private static final String KEY_SERVER_URL = "mainServerUrl"; + private static final String KEY_CONFIG_RESULTS = "results"; + + private static String uuidExtra = null; + + public static void setUUIDFromExtra(String uuid) { + uuidExtra = uuid; + } + + /** + * 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) { - 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"; + // 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; + + final URL requestUrl = buildConfigRequestUrl(c, uuid, serverUrl); + if (requestUrl == null) { + return; + } + + if (DEBUG) Log.d(TAG, requestUrl.toString()); + + final String result = readFromUrlGET(requestUrl); + if (DEBUG) Log.d(TAG, result); + + if (result == null) { + return; + } + + try { + final JSONObject json = new JSONObject(result); + + // Update the server URL if necessary. + final String newServerUrl = json.getString(KEY_SERVER_URL); + if (!defaultServerUrl.equals(newServerUrl)) { + Preferences.setDynamicConfigServerUrl(c, newServerUrl); + } - 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); - } + // 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(); + } - /** - * 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(); - } + 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; + } + } - //notify listeners that the config fetch has completed - Intent i = new Intent(ACTION_CONFIG_FETCHED); - LocalBroadcastManager.getInstance(c).sendBroadcast(i); - } + public static boolean isInBucket(Context c, int low, int high) { + int userBucket = getUserBucket(c); + if (userBucket >= low && userBucket < high) + return true; + else + return false; + } - 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; + } - /** - * 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>(); + 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; + } - // lookup experiment in config - String config = Preferences.getDynamicConfigJson(c); + 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! + } - // if it does not exist - if (config == null) { - return returnList; - } + 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! - } + /** + * 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; + } - return returnList; - } + /** + * 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); - /** - * 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); + 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); - // 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(); - } + 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 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(); - } + /** + * 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); - } + CRC32 crc = new CRC32(); + crc.update(uuid.getBytes()); + long checksum = crc.getValue(); + return (int)(checksum % 100L); + } }
--- a/testing/profiles/prefs_general.js +++ b/testing/profiles/prefs_general.js @@ -23,17 +23,16 @@ user_pref("signed.applets.codebase_princ user_pref("browser.shell.checkDefaultBrowser", false); user_pref("shell.checkDefaultClient", false); user_pref("browser.warnOnQuit", false); user_pref("accessibility.typeaheadfind.autostart", false); 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); // Disable speculative connections so they aren't reported as leaking when they're hanging around. user_pref("network.http.speculative-parallel-limit", 0); user_pref("dom.min_background_timeout_value", 1000); user_pref("test.mousescroll", true); user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
--- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -2734,16 +2734,17 @@ SearchService.prototype = { gInitialized = true; this._cacheFileJSON = null; this._initObservers.resolve(this._initRV); Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "init-complete"); Services.telemetry.getHistogramById("SEARCH_SERVICE_INIT_SYNC").add(true); + this._recordEnginesWithUpdate(); LOG("_syncInit end"); }, /** * Asynchronous implementation of the initializer. * * @returns {Promise} A promise, resolved successfully if the initialization @@ -2776,16 +2777,17 @@ SearchService.prototype = { LOG("_asyncInit: failure loading engines: " + ex); } this._addObservers(); gInitialized = true; this._cacheFileJSON = null; 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)); }, _metaData: { }, setGlobalAttr(name, val) { this._metaData[name] = val; @@ -3144,16 +3146,17 @@ SearchService.prototype = { // Due to the HTTP requests done by ensureKnownCountryCode, it's possible that // at this point a synchronous init has been forced by other code. if (!gInitialized) yield this._asyncLoadEngines(cache); // 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); Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-failed"); } finally { Services.obs.notifyObservers(null, SEARCH_SERVICE_TOPIC, "reinit-complete"); } }.bind(this)); @@ -4228,16 +4231,33 @@ SearchService.prototype = { uri.userPass = ""; // Avoid reporting a username or password. result.submissionURL = uri.spec; } } 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 * search engine name and original terms. * * The keys are strings containing the domain name and lowercase path of the * engine submission, for example "www.google.com/search". *
new file mode 100644 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/data/engine-update.xml @@ -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>
new file mode 100644 --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_update_telemetry.js @@ -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); +});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini @@ -12,16 +12,17 @@ support-files = data/engine-app.xml data/engine-fr.xml data/engineMaker.sjs data/engine-pref.xml data/engine-rel-searchform.xml 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 data/install.rdf data/list.txt data/langpack-metadata.json data/metadata.json data/search-metadata.json @@ -84,9 +85,10 @@ tags = addons [test_sync_profile_engine.js] [test_rel_searchform.js] [test_remove_profile_engine.js] [test_selectedEngine.js] [test_geodefaults.js] [test_hidden.js] [test_currentEngine_fallback.js] [test_require_engines_in_cache.js] +[test_update_telemetry.js] [test_svg_icon.js]
--- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -5370,16 +5370,32 @@ "description": "Time (ms) it takes to initialize the search service" }, "SEARCH_SERVICE_INIT_SYNC": { "alert_emails": ["rvitillo@mozilla.com", "gavin@mozilla.com"], "expires_in_version": "never", "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", "high": 1000, "n_buckets": 15, "description": "Time (ms) it takes to build the cache of the search service" }, "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS": {