Merge m-c to inbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 05 Apr 2016 16:39:24 -0700
changeset 291797 b70ae970d45dcf9c8be267fbe3a61114a59cd8ef
parent 291782 ffff3be72a1dd4117ac1880acad402caf517b5f2 (current diff)
parent 291796 a235bfcc8c411169b82420c503775c1a3e7edad5 (diff)
child 291798 566412e70f27f9e77030c028cdb3d47c99bfe931
push id74679
push userkwierso@gmail.com
push dateTue, 05 Apr 2016 23:39:26 +0000
treeherdermozilla-inbound@b70ae970d45d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.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
Merge m-c to inbound, a=merge MozReview-Commit-ID: 1Q56H3tR0mI
browser/base/content/browser.js
mobile/android/app/mobile.js
--- 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": {