Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 06 Feb 2013 08:20:00 -0500
changeset 121008 ed29092600501b9b2798c6a9c0ee643eed8281ef
parent 121007 2e891e0db39737c5a9928feebde42ea10d1792fe (current diff)
parent 120942 bc108d2ce8d17a560e2fc2affa6867ca9418448d (diff)
child 121009 35d48d9d80d542075bee8ecec666ce7bcb14ac8a
push id24271
push userryanvm@gmail.com
push dateWed, 06 Feb 2013 21:55:32 +0000
treeherdermozilla-central@325397090d12 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone21.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.
services/aitc/Aitc.js
services/aitc/AitcComponents.manifest
services/aitc/Makefile.in
services/aitc/modules/browserid.js
services/aitc/modules/client.js
services/aitc/modules/main.js
services/aitc/modules/manager.js
services/aitc/modules/storage.js
services/aitc/services-aitc.js
services/aitc/tests/Makefile.in
services/aitc/tests/mochitest/browser_id_simple.js
services/aitc/tests/mochitest/file_browser_id_mock.html
services/aitc/tests/mochitest/head.js
services/aitc/tests/unit/test_aitc_client.js
services/aitc/tests/unit/test_aitc_manager.js
services/aitc/tests/unit/test_load_modules.js
services/aitc/tests/unit/test_storage_queue.js
services/aitc/tests/unit/test_storage_registry.js
services/aitc/tests/unit/xpcshell.ini
services/common/modules-testing/aitcserver.js
services/common/tests/run_aitc_server.js
services/common/tests/unit/test_aitc_server.js
--- a/browser/base/content/abouthealthreport/abouthealth.js
+++ b/browser/base/content/abouthealthreport/abouthealth.js
@@ -67,34 +67,35 @@ function refreshDataView(data) {
 /**
  * Ensure the page has the latest version of the uploaded JSON payload.
  */
 function refreshJSONPayload() {
   reporter.getLastPayload().then(refreshDataView);
 }
 
 function onOptInClick() {
-  policy.healthReportUploadEnabled = true;
+  policy.recordHealthReportUploadEnabled(true,
+                                         "Clicked opt in button on about page.");
   refreshWithDataSubmissionFlag(true);
 }
 
 function onOptOutClick() {
   let prompts = Cc["@mozilla.org/embedcomp/prompt-service;1"]
                   .getService(Ci.nsIPromptService);
 
   let messages = document.getElementById("optout-confirmationPrompt");
   let title = messages.getAttribute("confirmationPrompt_title");
   let message = messages.getAttribute("confirmationPrompt_message");
 
   if (!prompts.confirm(window, title, message)) {
     return;
   }
 
-  policy.healthReportUploadEnabled = false;
-  reporter.requestDeleteRemoteData("Clicked opt out button on about page.");
+  policy.recordHealthReportUploadEnabled(false,
+                                         "Clicked opt out button on about page.");
   refreshWithDataSubmissionFlag(false);
   updateView("disabled");
 }
 
 function onShowRawDataClick() {
   updateView("showDetails");
   refreshJSONPayload();
 }
--- a/browser/components/preferences/advanced.js
+++ b/browser/components/preferences/advanced.js
@@ -233,17 +233,18 @@ var gAdvancedPane = {
                                    .wrappedJSObject
                                    .policy;
 
     if (!policy) {
       return;
     }
 
     let checkbox = document.getElementById("submitHealthReportBox");
-    policy.healthReportUploadEnabled = checkbox.checked;
+    policy.recordHealthReportUploadEnabled(checkbox.checked,
+                                           "Checkbox from preferences pane");
   },
 #endif
 
   // NETWORK TAB
 
   /*
    * Preferences:
    *
--- a/browser/components/preferences/in-content/advanced.js
+++ b/browser/components/preferences/in-content/advanced.js
@@ -222,17 +222,18 @@ var gAdvancedPane = {
                                    .wrappedJSObject
                                    .policy;
 
     if (!policy) {
       return;
     }
 
     let checkbox = document.getElementById("submitHealthReportBox");
-    policy.healthReportUploadEnabled = checkbox.checked;
+    policy.recordHealthReportUploadEnabled(checkbox.checked,
+                                           "Checkbox from preferences pane");
   },
 #endif
 
   // NETWORK TAB
 
   /*
    * Preferences:
    *
--- a/browser/confvars.sh
+++ b/browser/confvars.sh
@@ -20,17 +20,16 @@ if test "$OS_ARCH" = "WINNT"; then
         MOZ_STUB_INSTALLER=1
       fi
     fi
   fi
 fi
 
 MOZ_CHROME_FILE_FORMAT=omni
 MOZ_SAFE_BROWSING=1
-MOZ_SERVICES_AITC=1
 MOZ_SERVICES_COMMON=1
 MOZ_SERVICES_CRYPTO=1
 MOZ_SERVICES_HEALTHREPORT=1
 MOZ_SERVICES_METRICS=1
 MOZ_SERVICES_SYNC=1
 MOZ_APP_VERSION=$FIREFOX_VERSION
 MOZ_EXTENSIONS_DEFAULT=" gio"
 # MOZ_APP_DISPLAYNAME will be set by branding/configure.sh
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -457,20 +457,16 @@
 #endif
 #ifdef MOZ_ENABLE_DBUS
 @BINPATH@/components/@DLL_PREFIX@dbusservice@DLL_SUFFIX@
 #endif
 @BINPATH@/components/nsINIProcessor.manifest
 @BINPATH@/components/nsINIProcessor.js
 @BINPATH@/components/nsPrompter.manifest
 @BINPATH@/components/nsPrompter.js
-#ifdef MOZ_SERVICES_AITC
-@BINPATH@/components/AitcComponents.manifest
-@BINPATH@/components/Aitc.js
-#endif
 #ifdef MOZ_DATA_REPORTING
 @BINPATH@/components/DataReporting.manifest
 @BINPATH@/components/DataReportingService.js
 #endif
 #ifdef MOZ_SERVICES_HEALTHREPORT
 @BINPATH@/components/HealthReportComponents.manifest
 #endif
 #ifdef MOZ_SERVICES_SYNC
@@ -579,19 +575,16 @@
 ; gre location for now.
 @BINPATH@/defaults/pref/channel-prefs.js
 #else
 ; For Fx-on-xr, channel-prefs lives with the app preferences. (Bug 762588)
 @BINPATH@/@PREF_DIR@/channel-prefs.js
 #endif
 
 ; Services (gre) prefs
-#ifdef MOZ_SERVICES_AITC
-@BINPATH@/defaults/pref/services-aitc.js
-#endif
 #ifdef MOZ_SERVICES_NOTIFICATIONS
 @BINPATH@/defaults/pref/services-notifications.js
 #endif
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/defaults/pref/services-sync.js
 #endif
 
 ; [Layout Engine Resources]
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -92,16 +92,17 @@ components/nsProxyAutoConfig.js
 D3DCompiler_42.dll
 d3dx9_42.dll
 d3dx9_43.dll
 defaults/pref/all.js
 defaults/pref/bug259708.js
 defaults/pref/bug307259.js
 defaults/pref/reporter.js
 defaults/pref/security-prefs.js
+defaults/pref/services-aitc.js
 defaults/pref/winpref.js
 defaults/pref/xpinstall.js
 defaults/preferences/services-aitc.js
 defaults/preferences/services-notifications.js
 defaults/preferences/services-sync.js
 defaults/preferences/healthreport-prefs.js
 defaults/profile/US/
 defaults/profile/extensions/
@@ -878,16 +879,18 @@ xpicleanup@BIN_SUFFIX@
   chrome/zh-TW.jar
   chrome/zh-TW.manifest
   chrome/browser.jar
   chrome/localized.manifest
   chrome/nonlocalized.manifest
   chrome/pippki.jar
   chrome/toolkit.jar
   components/addonManager.js
+  components/Aitc.js
+  components/AitcComponents.manifest
   components/amContentHandler.js
   components/amWebInstallListener.js
   components/binary.manifest
   components/browser.xpt
   components/BrowserElementParent.js
   components/BrowserElementParent.manifest
   components/BrowserElementPromptService.jsm
   components/BrowserElementParent.jsm
--- a/configure.in
+++ b/configure.in
@@ -8333,22 +8333,16 @@ if test "$BUILD_CTYPES"; then
     AC_DEFINE(BUILD_CTYPES)
 fi
 
 dnl Build Places if required
 if test "$MOZ_PLACES"; then
   AC_DEFINE(MOZ_PLACES)
 fi
 
-dnl Build Apps in the Cloud (AITC) if required
-AC_SUBST(MOZ_SERVICES_AITC)
-if test -n "$MOZ_SERVICES_AITC"; then
-  AC_DEFINE(MOZ_SERVICES_AITC)
-fi
-
 dnl Build Common JS modules provided by services.
 AC_SUBST(MOZ_SERVICES_COMMON)
 if test -n "$MOZ_SERVICES_COMMON"; then
   AC_DEFINE(MOZ_SERVICES_COMMON)
 fi
 
 dnl Build Services crypto component (used by Sync)
 AC_SUBST(MOZ_SERVICES_CRYPTO)
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -9,20 +9,16 @@ VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 PARALLEL_DIRS += \
   common \
   crypto \
   $(NULL)
 
-ifdef MOZ_SERVICES_AITC
-PARALLEL_DIRS += aitc
-endif
-
 ifdef MOZ_SERVICES_HEALTHREPORT
 PARALLEL_DIRS += healthreport
 endif
 
 ifdef MOZ_DATA_REPORTING
 PARALLEL_DIRS += datareporting
 endif
 
deleted file mode 100644
--- a/services/aitc/Aitc.js
+++ /dev/null
@@ -1,104 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/preferences.js");
-
-function AitcService() {
-  this.aitc = null;
-  this.wrappedJSObject = this;
-}
-AitcService.prototype = {
-  classID: Components.ID("{a3d387ca-fd26-44ca-93be-adb5fda5a78d}"),
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
-                                         Ci.nsISupportsWeakReference]),
-
-  observe: function observe(subject, topic, data) {
-    switch (topic) {
-      case "app-startup":
-        // We listen for this event beacause Aitc won't work until there is
-        // atleast 1 visible top-level XUL window.
-        Services.obs.addObserver(this, "sessionstore-windows-restored", true);
-        break;
-      case "sessionstore-windows-restored":
-        Services.obs.removeObserver(this, "sessionstore-windows-restored");
-
-        // Don't start AITC if classic sync is on.
-        if (Preferences.get("services.sync.engine.apps", false)) {
-          return;
-        }
-        // Start AITC only if it is enabled.
-        if (!Preferences.get("services.aitc.enabled", false)) {
-          return;
-        }
-
-        // Start AITC service only if apps.enabled is true. If false, setup
-        // an observer in case the value changes as a result of an access to
-        // the DOM API.
-        if (Preferences.get("dom.mozApps.used", false)) {
-          this.start();
-          return;
-        }
-
-        // Wait and see if the user wants anything apps related.
-        Preferences.observe("dom.mozApps.used", function checkIfEnabled() {
-          if (Preferences.get("dom.mozApps.used", false)) {
-            Preferences.ignore("dom.mozApps.used", checkIfEnabled, this);
-            this.start();
-          }
-        }, this);
-        break;
-    }
-  },
-
-  start: function start() {
-    if (this.aitc) {
-      return;
-    }
-
-    // Log to stdout if enabled.
-    Cu.import("resource://services-aitc/main.js");
-    Cu.import("resource://services-common/log4moz.js");
-    let root = Log4Moz.repository.getLogger("Service.AITC");
-    root.level = Log4Moz.Level[Preferences.get("services.aitc.log.level")];
-    if (Preferences.get("services.aitc.log.dump")) {
-      root.addAppender(new Log4Moz.DumpAppender());
-    }
-    this.aitc = new Aitc();
-    Services.obs.notifyObservers(null, "service:aitc:started", null);
-  },
-
-};
-
-function AboutApps() {
-}
-AboutApps.prototype = {
-  classID: Components.ID("{1de7cbe8-60f1-493e-b56b-9d099b3c018e}"),
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
-                                         Ci.nsIAboutModule]),
-
-  getURIFlags: function(aURI) {
-    return Ci.nsIAboutModule.ALLOW_SCRIPT;
-  },
-
-  newChannel: function(aURI) {
-    let channel = Services.io.newChannel(
-      Preferences.get("services.aitc.dashboard.url"), null, null
-    );
-    channel.originalURI = aURI;
-    return channel;
-  }
-};
-
-const components = [AitcService, AboutApps];
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
deleted file mode 100644
--- a/services/aitc/AitcComponents.manifest
+++ /dev/null
@@ -1,10 +0,0 @@
-# Aitc.js
-component {a3d387ca-fd26-44ca-93be-adb5fda5a78d} Aitc.js
-contract @mozilla.org/services/aitc;1 {a3d387ca-fd26-44ca-93be-adb5fda5a78d}
-category app-startup AitcService service,@mozilla.org/services/aitc;1
-
-component {1de7cbe8-60f1-493e-b56b-9d099b3c018e} Aitc.js
-contract @mozilla.org/network/protocol/about;1?what=apps {1de7cbe8-60f1-493e-b56b-9d099b3c018e}
-
-# Register resource aliases
-resource services-aitc resource://gre/modules/services-aitc/
deleted file mode 100644
--- a/services/aitc/Makefile.in
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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/.
-
-DEPTH     = @DEPTH@
-topsrcdir = @top_srcdir@
-srcdir    = @srcdir@
-VPATH     = @srcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-aitc_modules := \
-  browserid.js \
-  client.js \
-  main.js \
-  manager.js \
-  storage.js \
-  $(NULL)
-
-EXTRA_COMPONENTS = \
-  AitcComponents.manifest \
-  Aitc.js \
-  $(NULL)
-
-PREF_JS_EXPORTS = $(srcdir)/services-aitc.js
-
-AITC_MODULE_FILES := $(addprefix modules/,$(aitc_modules))
-AITC_MODULE_DEST = $(FINAL_TARGET)/modules/services-aitc
-INSTALL_TARGETS += AITC_MODULE
-
-TEST_DIRS += tests
-
-include $(topsrcdir)/config/rules.mk
deleted file mode 100644
--- a/services/aitc/modules/browserid.js
+++ /dev/null
@@ -1,472 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["BrowserID"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/preferences.js");
-
-const PREFS = new Preferences("services.aitc.browserid.");
-
-/**
- * This implementation will be replaced with native crypto and assertion
- * generation goodness. See bug 753238.
- */
-function BrowserIDService() {
-  this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID");
-  this._log.level = Log4Moz.Level[PREFS.get("log")];
-}
-BrowserIDService.prototype = {
-  /**
-   * Getter that returns the freshest value for ID_URI.
-   */
-  get ID_URI() {
-    return PREFS.get("url");
-  },
-
-  /**
-   * Obtain a BrowserID assertion with the specified characteristics.
-   *
-   * @param cb
-   *        (Function) Callback to be called with (err, assertion) where 'err'
-   *        can be an Error or NULL, and 'assertion' can be NULL or a valid
-   *        BrowserID assertion. If no callback is provided, an exception is
-   *        thrown.
-   *
-   * @param options
-   *        (Object) An object that may contain the following properties:
-   *
-   *          "requiredEmail" : An email for which the assertion is to be
-   *                            issued. If one could not be obtained, the call
-   *                            will fail. If this property is not specified,
-   *                            the default email as set by the user will be
-   *                            chosen. If both this property and "sameEmailAs"
-   *                            are set, an exception will be thrown.
-   *
-   *          "sameEmailAs"   : If set, instructs the function to issue an
-   *                            assertion for the same email that was provided
-   *                            to the domain specified by this value. If this
-   *                            information could not be obtained, the call
-   *                            will fail. If both this property and
-   *                            "requiredEmail" are set, an exception will be
-   *                            thrown.
-   *
-   *          "audience"      : The audience for which the assertion is to be
-   *                            issued. If this property is not set an exception
-   *                            will be thrown.
-   *
-   *        Any properties not listed above will be ignored.
-   *
-   * (This function could use some love in terms of what arguments it accepts.
-   * See bug 746401.)
-   */
-  getAssertion: function getAssertion(cb, options) {
-    if (!cb) {
-      throw new Error("getAssertion called without a callback");
-    }
-    if (!options) {
-      throw new Error("getAssertion called without any options");
-    }
-    if (!options.audience) {
-      throw new Error("getAssertion called without an audience");
-    }
-    if (options.sameEmailAs && options.requiredEmail) {
-      throw new Error(
-        "getAssertion sameEmailAs and requiredEmail are mutually exclusive"
-      );
-    }
-
-    new Sandbox(this._getEmails.bind(this, cb, options), this.ID_URI);
-  },
-
-  /**
-   * Obtain a BrowserID assertion by asking the user to login and select an
-   * email address.
-   *
-   * @param cb
-   *        (Function) Callback to be called with (err, assertion) where 'err'
-   *        can be an Error or NULL, and 'assertion' can be NULL or a valid
-   *        BrowserID assertion. If no callback is provided, an exception is
-   *        thrown.
-   *
-   * @param win
-   *        (Window) A contentWindow that has a valid document loaded. If this
-   *        argument is provided the user will be asked to login in the context
-   *        of the document currently loaded in this window.
-   *
-   *        The audience of the assertion will be set to the domain of the
-   *        loaded document, and the "audience" property in the "options"
-   *        argument (if provided), will be ignored. The email to which this
-   *        assertion issued will be selected by the user when they login (and
-   *        "requiredEmail" or "sameEmailAs", if provided, will be ignored). If
-   *        the user chooses to not login, this call will fail.
-   *
-   *        Be aware! The provided contentWindow must also have loaded the
-   *        BrowserID include.js shim for this to work! This behavior is
-   *        temporary until we implement native support for navigator.id.
-   *
-   * @param options
-   *        (Object) Currently an empty object. Present for future compatiblity
-   *        when options for a login case may be added. Any properties, if
-   *        present, are ignored.
-   */
-  getAssertionWithLogin: function getAssertionWithLogin(cb, win, options) {
-    if (!cb) {
-      throw new Error("getAssertionWithLogin called without a callback");
-    }
-    if (!win) {
-      throw new Error("getAssertionWithLogin called without a window");
-    }
-    this._getAssertionWithLogin(cb, win);
-  },
-
-
-  /**
-   * Internal implementation methods begin here
-   */
-
-  // Try to get the user's email(s). If user isn't logged in, this will be empty
-  _getEmails: function _getEmails(cb, options, sandbox) {
-    let self = this;
-
-    if (!sandbox) {
-      cb(new Error("Sandbox not created"), null);
-      return;
-    }
-
-    function callback(res) {
-      let emails = {};
-      try {
-        emails = JSON.parse(res);
-      } catch (e) {
-        self._log.error("Exception in JSON.parse for _getAssertion: " + e);
-      }
-      self._gotEmails(emails, sandbox, cb, options);
-    }
-    sandbox.box.importFunction(callback);
-
-    let scriptText =
-      "var list = window.BrowserID.User.getStoredEmailKeypairs();" +
-      "callback(JSON.stringify(list));";
-    Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
-  },
-
-  // Received a list of emails from BrowserID for current user
-  _gotEmails: function _gotEmails(emails, sandbox, cb, options) {
-    let keys = Object.keys(emails);
-
-    // If list is empty, user is not logged in, or doesn't have a default email.
-    if (!keys.length) {
-      sandbox.free();
-
-      let err = "User is not logged in, or no emails were found";
-      this._log.error(err);
-      try {
-        cb(new Error(err), null);
-      } catch(e) {
-        this._log.warn("Callback threw in _gotEmails " +
-                       CommonUtils.exceptionStr(e));
-      }
-      return;
-    }
-
-    // User is logged in. For which email shall we get an assertion?
-
-    // Case 1: Explicitely provided
-    if (options.requiredEmail) {
-      this._getAssertionWithEmail(
-        sandbox, cb, options.requiredEmail, options.audience
-      );
-      return;
-    }
-
-    // Case 2: Derive from a given domain
-    if (options.sameEmailAs) {
-      this._getAssertionWithDomain(
-        sandbox, cb, options.sameEmailAs, options.audience
-      );
-      return;
-    }
-
-    // Case 3: Default email
-    this._getAssertionWithEmail(
-      sandbox, cb, keys[0], options.audience
-    );
-    return;
-  },
-
-  /**
-   * Open a login window and ask the user to login, returning the assertion
-   * generated as a result to the caller.
-   */
-  _getAssertionWithLogin: function _getAssertionWithLogin(cb, win) {
-    // We're executing navigator.id.get as a content script in win.
-    // This results in a popup that we will temporarily unblock.
-    let pm = Services.perms;
-    let principal = win.document.nodePrincipal;
-
-    let oldPerm = pm.testExactPermissionFromPrincipal(principal, "popup");
-    try {
-      pm.addFromPrincipal(principal, "popup", pm.ALLOW_ACTION);
-    } catch(e) {
-      this._log.warn("Setting popup blocking to false failed " + e);
-    }
-
-    // Open sandbox and execute script. This sandbox will be GC'ed.
-    let sandbox = new Cu.Sandbox(win, {
-      wantXrays:        false,
-      sandboxPrototype: win
-    });
-
-    let self = this;
-    function callback(val) {
-      // Set popup blocker permission to original value.
-      try {
-        pm.addFromPrincipal(principal, "popup", oldPerm);
-      } catch(e) {
-        this._log.warn("Setting popup blocking to original value failed " + e);
-      }
-
-      if (val) {
-        self._log.info("_getAssertionWithLogin succeeded");
-        try {
-          cb(null, val);
-        } catch(e) {
-          self._log.warn("Callback threw in _getAssertionWithLogin " +
-                         CommonUtils.exceptionStr(e));
-        }
-      } else {
-        let msg = "Could not obtain assertion in _getAssertionWithLogin";
-        self._log.error(msg);
-        try {
-          cb(new Error(msg), null);
-        } catch(e) {
-          self._log.warn("Callback threw in _getAssertionWithLogin " +
-                         CommonUtils.exceptionStr(e));
-        }
-      }
-    }
-    sandbox.importFunction(callback);
-
-    function doGetAssertion() {
-      self._log.info("_getAssertionWithLogin Started");
-      let scriptText = "window.navigator.id.get(" +
-                       "  callback, {allowPersistent: true}" +
-                       ");";
-      Cu.evalInSandbox(scriptText, sandbox, "1.8", self.ID_URI, 1);
-    }
-
-    // Sometimes the provided win hasn't fully loaded yet
-    if (!win.document || (win.document.readyState != "complete")) {
-      win.addEventListener("DOMContentLoaded", function _contentLoaded() {
-        win.removeEventListener("DOMContentLoaded", _contentLoaded, false);
-        doGetAssertion();
-      }, false);
-    } else {
-      doGetAssertion();
-    }
-  },
-
-  /**
-   * Gets an assertion for the specified 'email' and 'audience'
-   */
-  _getAssertionWithEmail: function _getAssertionWithEmail(sandbox, cb, email,
-                                                          audience) {
-    let self = this;
-
-    function onSuccess(res) {
-      // Cleanup first.
-      sandbox.free();
-
-      // The internal API sometimes calls onSuccess even though no assertion
-      // could be obtained! Double check:
-      if (!res) {
-        let msg = "BrowserID.User.getAssertion empty assertion for " + email;
-        self._log.error(msg);
-        try {
-          cb(new Error(msg), null);
-        } catch(e) {
-          self._log.warn("Callback threw in _getAssertionWithEmail " +
-                         CommonUtils.exceptionStr(e));
-        }
-        return;
-      }
-
-      // Success
-      self._log.info("BrowserID.User.getAssertion succeeded");
-      try {
-        cb(null, res);
-      } catch(e) {
-        self._log.warn("Callback threw in _getAssertionWithEmail " +
-                       CommonUtils.exceptionStr(e));
-      }
-    }
-
-    function onError(err) {
-      sandbox.free();
-
-      self._log.info("BrowserID.User.getAssertion failed");
-      try {
-        cb(err, null);
-      } catch(e) {
-        self._log.warn("Callback threw in _getAssertionWithEmail " +
-                       CommonUtils.exceptionStr(e));
-      }
-    }
-    sandbox.box.importFunction(onSuccess);
-    sandbox.box.importFunction(onError);
-
-    self._log.info("_getAssertionWithEmail Started");
-    let scriptText =
-      "window.BrowserID.User.getAssertion(" +
-        "'" + email + "', "     +
-        "'" + audience + "', "  +
-        "onSuccess, "           +
-        "onError"               +
-      ");";
-    Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
-  },
-
-  /**
-   * Gets the email which was used to login to 'domain'. If one was found,
-   * _getAssertionWithEmail is called to obtain the assertion.
-   */
-  _getAssertionWithDomain: function _getAssertionWithDomain(sandbox, cb, domain,
-                                                            audience) {
-    let self = this;
-
-    function onDomainSuccess(email) {
-      if (email) {
-        self._getAssertionWithEmail(sandbox, cb, email, audience);
-      } else {
-        sandbox.free();
-        try {
-          cb(new Error("No email found for _getAssertionWithDomain"), null);
-        } catch(e) {
-          self._log.warn("Callback threw in _getAssertionWithDomain " +
-                         CommonUtils.exceptionStr(e));
-        }
-      }
-    }
-    sandbox.box.importFunction(onDomainSuccess);
-
-    // This wil tell us which email was used to login to "domain", if any.
-    self._log.info("_getAssertionWithDomain Started");
-    let scriptText =
-      "onDomainSuccess(window.BrowserID.Storage.site.get(" +
-        "'" + domain + "', "  +
-        "'email'"             +
-      "));";
-    Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
-  },
-};
-
-/**
- * An object that represents a sandbox in an iframe loaded with uri. The
- * callback provided to the constructor will be invoked when the sandbox is
- * ready to be used. The callback will receive this object as its only argument
- * and the prepared sandbox may be accessed via the "box" property.
- *
- * Please call free() when you are finished with the sandbox to explicitely free
- * up all associated resources.
- *
- * @param cb
- *        (function) Callback to be invoked with a Sandbox, when ready.
- * @param uri
- *        (String) URI to be loaded in the Sandbox.
- */
-function Sandbox(cb, uri) {
-  this._uri = uri;
-
-  // Put in a try/catch block because Services.wm.getMostRecentWindow, called in
-  // _createFrame will be null in XPCShell.
-  try {
-    this._createFrame();
-    this._createSandbox(cb, uri);
-  } catch(e) {
-    this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID.Sandbox");
-    this._log.level = Log4Moz.Level[PREFS.get("log")];
-    this._log.error("Could not create Sandbox " + e);
-    cb(null);
-  }
-}
-Sandbox.prototype = {
-  /**
-   * Frees the sandbox and releases the iframe created to host it.
-   */
-  free: function free() {
-    delete this.box;
-    this._container.removeChild(this._frame);
-    this._frame = null;
-    this._container = null;
-  },
-
-  /**
-   * Creates an empty, hidden iframe and sets it to the _iframe
-   * property of this object.
-   *
-   * @return frame
-   *         (iframe) An empty, hidden iframe
-   */
-  _createFrame: function _createFrame() {
-    let doc = Services.wm.getMostRecentWindow("navigator:browser").document;
-
-    // Insert iframe in to create docshell.
-    let frame = doc.createElement("iframe");
-    frame.setAttribute("type", "content");
-    frame.setAttribute("collapsed", "true");
-    doc.documentElement.appendChild(frame);
-
-    // Stop about:blank from being loaded.
-    let webNav = frame.docShell.QueryInterface(Ci.nsIWebNavigation);
-    webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);
-
-    // Set instance properties.
-    this._frame = frame;
-    this._container = doc.documentElement;
-  },
-
-  _createSandbox: function _createSandbox(cb, uri) {
-    let self = this;
-    this._frame.addEventListener(
-      "DOMContentLoaded",
-      function _makeSandboxContentLoaded(event) {
-        if (event.target.location.toString() != uri) {
-          return;
-        }
-        event.target.removeEventListener(
-          "DOMContentLoaded", _makeSandboxContentLoaded, false
-        );
-        let workerWindow = self._frame.contentWindow;
-        self.box = new Cu.Sandbox(workerWindow, {
-          wantXrays:        false,
-          sandboxPrototype: workerWindow
-        });
-        cb(self);
-      },
-      true
-    );
-
-    // Load the iframe.
-    this._frame.docShell.loadURI(
-      uri,
-      this._frame.docShell.LOAD_FLAGS_NONE,
-      null, // referrer
-      null, // postData
-      null  // headers
-    );
-  },
-};
-
-XPCOMUtils.defineLazyGetter(this, "BrowserID", function() {
-  return new BrowserIDService();
-});
deleted file mode 100644
--- a/services/aitc/modules/client.js
+++ /dev/null
@@ -1,417 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["AitcClient"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/Webapps.jsm");
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/preferences.js");
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-crypto/utils.js");
-
-const DEFAULT_INITIAL_BACKOFF = 600000; // 10min
-const DEFAULT_MAX_BACKOFF = 21600000;   // 6h
-const DEFAULT_REQUEST_FAILURE_THRESHOLD = 3;
-
-/**
- * First argument is a token as returned by CommonUtils.TokenServerClient.
- * This is a convenience, so you don't have to call updateToken immediately
- * after making a new client (though you must when the token expires and you
- * intend to reuse the same client instance).
- *
- * Second argument is a key-value store object that exposes two methods:
- *   set(key, value);     Sets the value of a given key
- *   get(key, default);   Returns the value of key, if it doesn't exist,
- *                        return default
- * The values should be stored persistently. The Preferences object from
- * services-common/preferences.js is an example of such an object.
- */
-this.AitcClient = function AitcClient(token, state) {
-  this.updateToken(token);
-
-  this._log = Log4Moz.repository.getLogger("Service.AITC.Client");
-  this._log.level = Log4Moz.Level[
-    Preferences.get("services.aitc.client.log.level")
-  ];
-
-  this._state = state;
-  this._backoff = !!state.get("backoff", false);
-  this._backoffTime = 0;
-  this._consecutiveFailures = 0;
-  this._maxFailures = state.get("requestFailureThreshold",
-    DEFAULT_REQUEST_FAILURE_THRESHOLD);
-
-  this._timeout = state.get("timeout", 120);
-  this._appsLastModified = parseInt(state.get("lastModified", "0"), 10);
-  this._log.info("Client initialized with token endpoint: " + this.uri);
-}
-AitcClient.prototype = {
-  _requiredLocalKeys: [
-    "origin", "receipts", "manifestURL", "installOrigin"
-  ],
-  _requiredRemoteKeys: [
-    "origin", "name", "receipts", "manifestPath", "installOrigin",
-    "installedAt", "modifiedAt"
-  ],
-
-  /**
-   * Updates the token the client must use to authenticate outgoing requests.
-   *
-   * @param token
-   *        (Object) A token as returned by CommonUtils.TokenServerClient.
-   */
-  updateToken: function updateToken(token) {
-    this.uri = token.endpoint.replace(/\/+$/, "");
-    this.token = {id: token.id, key: token.key};
-  },
-
-  /**
-   * Initiates an update of a newly installed app to the AITC server. Call this
-   * when an application is installed locally.
-   *
-   * @param app
-   *        (Object) The app record of the application that was just installed.
-   */
-  remoteInstall: function remoteInstall(app, cb) {
-    if (!cb) {
-      throw new Error("remoteInstall called without callback");
-    }
-
-    // Fetch the name of the app because it's not in the event app object.
-    let self = this;
-    DOMApplicationRegistry.getManifestFor(app.origin, function gotManifest(m) {
-      if (m) {
-        app.name = m.name;
-      }
-      self._putApp(self._makeRemoteApp(app), cb);
-    });
-  },
-
-  /**
-   * Initiates an update of an uinstalled app to the AITC server. Call this
-   * when an application is uninstalled locally.
-   *
-   * @param app
-   *        (Object) The app record of the application that was uninstalled.
-   */
-  remoteUninstall: function remoteUninstall(app, cb) {
-    if (!cb) {
-      throw new Error("remoteUninstall called without callback");
-    }
-
-    app.name = "Uninstalled"; // Bug 760262
-    let record = this._makeRemoteApp(app);
-    record.hidden = true;
-    this._putApp(record, cb);
-  },
-
-  /**
-   * Fetch remote apps from server with GET. The provided callback will receive
-   * an array of app objects in the format expected by DOMApplicationRegistry,
-   * if successful, or an Error; in the usual (err, result) way.
-   */
-  getApps: function getApps(cb) {
-    if (!cb) {
-      throw new Error("getApps called but no callback provided");
-    }
-
-    if (!this._isRequestAllowed()) {
-      cb(null, null);
-      return;
-    }
-
-    let uri = this.uri + "/apps/?full=1";
-    let req = new TokenAuthenticatedRESTRequest(uri, this.token);
-    req.timeout = this._timeout;
-    req.setHeader("Content-Type", "application/json");
-
-    if (this._appsLastModified) {
-      req.setHeader("X-If-Modified-Since", this._appsLastModified);
-    }
-
-    let self = this;
-    req.get(function _getAppsCb(err) {
-      self._processGetApps(err, cb, req);
-    });
-  },
-
-  /**
-   * GET request returned from getApps, process.
-   */
-  _processGetApps: function _processGetApps(err, cb, req) {
-    // Set X-Backoff or Retry-After, if needed.
-    this._setBackoff(req);
-
-    if (err) {
-      this._log.error("getApps request error " + err);
-      cb(err, null);
-      return;
-    }
-
-    // Bubble auth failure back up so new token can be acquired.
-    if (req.response.status == 401) {
-      let msg = new Error("getApps failed due to 401 authentication failure");
-      this._log.info(msg);
-      msg.authfailure = true;
-      cb(msg, null);
-      return;
-    }
-    // Process response.
-    if (req.response.status == 304) {
-      this._log.info("getApps returned 304");
-      cb(null, null);
-      return;
-    }
-    if (req.response.status != 200) {
-      this._log.error(req);
-      cb(new Error("Unexpected error with getApps"), null);
-      return;
-    }
-
-    let apps;
-    try {
-      let tmp = JSON.parse(req.response.body);
-      tmp = tmp["apps"];
-      // Convert apps from remote to local format.
-      apps = tmp.map(this._makeLocalApp, this);
-      this._log.info("getApps succeeded and got " + apps.length);
-    } catch (e) {
-      this._log.error(CommonUtils.exceptionStr(e));
-      cb(new Error("Exception in getApps " + e), null);
-      return;
-    }
-
-    // Return success.
-    try {
-      cb(null, apps);
-      // Don't update lastModified until we know cb succeeded.
-      this._appsLastModified = parseInt(req.response.headers["x-timestamp"], 10);
-      this._state.set("lastModified", ""  + this._appsLastModified);
-    } catch (e) {
-      this._log.error("Exception in getApps callback " + e);
-    }
-  },
-
-  /**
-   * Change a given app record to match what the server expects.
-   * Change manifestURL to manifestPath, and trim out manifests since we
-   * don't store them on the server.
-   */
-  _makeRemoteApp: function _makeRemoteApp(app) {
-    for each (let key in this.requiredLocalKeys) {
-      if (!(key in app)) {
-        throw new Error("Local app missing key " + key);
-      }
-    }
-
-    let record = {
-      name:          app.name,
-      origin:        app.origin,
-      receipts:      app.receipts,
-      manifestPath:  app.manifestURL,
-      installOrigin: app.installOrigin
-    };
-    if ("modifiedAt" in app) {
-      record.modifiedAt = app.modifiedAt;
-    }
-    if ("installedAt" in app) {
-      record.installedAt = app.installedAt;
-    }
-    return record;
-  },
-
-  /**
-   * Change a given app record received from the server to match what the local
-   * registry expects. (Inverse of _makeRemoteApp)
-   */
-  _makeLocalApp: function _makeLocalApp(app) {
-    for each (let key in this._requiredRemoteKeys) {
-      if (!(key in app)) {
-        throw new Error("Remote app missing key " + key);
-      }
-    }
-
-    let record = {
-      origin:         app.origin,
-      installOrigin:  app.installOrigin,
-      installedAt:    app.installedAt,
-      modifiedAt:     app.modifiedAt,
-      manifestURL:    app.manifestPath,
-      receipts:       app.receipts
-    };
-    if ("hidden" in app) {
-      record.hidden = app.hidden;
-    }
-    return record;
-  },
-
-  /**
-   * Try PUT for an app on the server and determine if we should retry
-   * if it fails.
-   */
-  _putApp: function _putApp(app, cb) {
-    if (!this._isRequestAllowed()) {
-      // PUT requests may qualify as the "minimum number of additional requests
-      // required to maintain consistency of their stored data". However, it's
-      // better to keep server load low, even if it means user's apps won't
-      // reach their other devices during the early days of AITC. We should
-      // revisit this when we have a better of idea of server load curves.
-      let err = new Error("Backoff in effect, aborting PUT");
-      err.processed = false;
-      cb(err, null);
-      return;
-    }
-
-    let uri = this._makeAppURI(app.origin);
-    let req = new TokenAuthenticatedRESTRequest(uri, this.token);
-    req.timeout = this._timeout;
-    req.setHeader("Content-Type", "application/json");
-
-    if (app.modifiedAt) {
-      req.setHeader("X-If-Unmodified-Since", "" + app.modified);
-    }
-
-    let self = this;
-    this._log.info("Trying to _putApp to " + uri);
-    req.put(JSON.stringify(app), function _putAppCb(err) {
-      self._processPutApp(err, cb, req);
-    });
-  },
-
-  /**
-   * PUT from _putApp finished, process.
-   */
-  _processPutApp: function _processPutApp(error, cb, req) {
-    this._setBackoff(req);
-
-    if (error) {
-      this._log.error("_putApp request error " + error);
-      cb(error, null);
-      return;
-    }
-
-    let err = null;
-    switch (req.response.status) {
-      case 201:
-      case 204:
-        this._log.info("_putApp succeeded");
-        cb(null, true);
-        break;
-
-      case 401:
-        // Bubble auth failure back up so new token can be acquired.
-        err = new Error("_putApp failed due to 401 authentication failure");
-        this._log.warn(err);
-        err.authfailure = true;
-        cb(err, null);
-        break;
-
-      case 409:
-        // Retry on server conflict.
-        err = new Error("_putApp failed due to 409 conflict");
-        this._log.warn(err);
-        cb(err,null);
-        break;
-
-      case 400:
-      case 412:
-      case 413:
-        let msg = "_putApp returned: " + req.response.status;
-        this._log.warn(msg);
-        err = new Error(msg);
-        err.processed = true;
-        cb(err, null);
-        break;
-
-      default:
-        this._error(req);
-        err = new Error("Unexpected error with _putApp");
-        err.processed = false;
-        cb(err, null);
-        break;
-    }
-  },
-
-  /**
-   * Utility methods.
-   */
-  _error: function _error(req) {
-    this._log.error("Catch-all error for request: " +
-      req.uri.asciiSpec + " " + req.response.status + " with: " +
-      req.response.body);
-  },
-
-  _makeAppURI: function _makeAppURI(origin) {
-    let part = CommonUtils.encodeBase64URL(
-      CryptoUtils.UTF8AndSHA1(origin)
-    ).replace("=", "");
-    return this.uri + "/apps/" + part;
-  },
-
-  // Before making a request, check if we are allowed to.
-  _isRequestAllowed: function _isRequestAllowed() {
-    if (!this._backoff) {
-      return true;
-    }
-
-    let time = Date.now();
-    let backoff = parseInt(this._state.get("backoff", 0), 10);
-
-    if (time < backoff) {
-      this._log.warn(backoff - time + "ms left for backoff, aborting request");
-      return false;
-    }
-
-    this._backoff = false;
-    this._state.set("backoff", "0");
-    return true;
-  },
-
-  // Set values from X-Backoff and Retry-After headers, if present.
-  _setBackoff: function _setBackoff(req) {
-    let backoff = 0;
-    let statusCodesWithoutBackoff = [200, 201, 204, 304, 401];
-
-    let val;
-    if (req.response.headers["Retry-After"]) {
-      val = req.response.headers["Retry-After"];
-      backoff = parseInt(val, 10);
-      this._log.warn("Retry-Header header was seen: " + val);
-    } else if (req.response.headers["X-Backoff"]) {
-      val = req.response.headers["X-Backoff"];
-      backoff = parseInt(val, 10);
-      this._log.warn("X-Backoff header was seen: " + val);
-    } else if (statusCodesWithoutBackoff.indexOf(req.response.status) === -1) {
-      // Bad status code.
-      this._consecutiveFailures++;
-      if (this._consecutiveFailures === this._maxFailures) {
-        // Initialize the backoff.
-        backoff = this._state.get("backoff.initial", DEFAULT_INITIAL_BACKOFF);
-      } else if (this._consecutiveFailures > this._maxFailures) {
-        // Set the backoff to the smallest of either:
-        backoff = Math.min(
-          // double the current backoff
-          this._backoffTime * 2,
-          // or the max backoff.
-          this._state.get("backoff.max", DEFAULT_MAX_BACKOFF)
-        );
-      }
-    } else {
-      // Good Status Code.
-      this._consecutiveFailures = 0;
-    }
-    if (backoff) {
-      this._backoff = true;
-      let time = Date.now();
-      this._state.set("backoff", "" + (time + backoff));
-      this._backoffTime = backoff;
-      this._log.info("Client setting backoff to: " + backoff + "ms");
-    }
-  },
-};
deleted file mode 100644
--- a/services/aitc/modules/main.js
+++ /dev/null
@@ -1,174 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["Aitc"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Webapps.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-Cu.import("resource://services-aitc/manager.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/preferences.js");
-
-this.Aitc = function Aitc() {
-  this._log = Log4Moz.repository.getLogger("Service.AITC");
-  this._log.level = Log4Moz.Level[Preferences.get(
-    "services.aitc.service.log.level"
-  )];
-  this._log.info("Loading AitC");
-
-  this.DASHBOARD_ORIGIN = CommonUtils.makeURI(
-    Preferences.get("services.aitc.dashboard.url")
-  ).prePath;
-
-  let self = this;
-  this._manager = new AitcManager(function managerDone() {
-    CommonUtils.nextTick(self._init, self);
-  });
-}
-Aitc.prototype = {
-  // The goal of the init function is to be ready to activate the AITC
-  // client whenever the user is looking at the dashboard. It also calls
-  // the initialSchedule function on the manager.
-  _init: function _init() {
-    let self = this;
-
-    // Do an initial upload.
-    this._manager.initialSchedule(function queueDone(num) {
-      if (num == -1) {
-        self._log.debug("No initial upload was required");
-        return;
-      }
-      self._log.debug(num + " initial apps queued successfully");
-    });
-
-    // This is called iff the user is currently looking the dashboard.
-    function dashboardLoaded(browser) {
-      let win = browser.contentWindow;
-      self._log.info("Dashboard was accessed " + win);
-
-      // If page is ready to go, fire immediately.
-      if (win.document && win.document.readyState == "complete") {
-        self._manager.userActive(win);
-        return;
-      }
-
-      // Only fire event after the page fully loads.
-      browser.contentWindow.addEventListener(
-        "DOMContentLoaded",
-        function _contentLoaded(event) {
-          self._manager.userActive(win);
-        },
-        false
-      );
-    }
-
-    // This is called when the user's attention is elsewhere.
-    function dashboardUnloaded() {
-      self._log.info("Dashboard closed or in background");
-      self._manager.userIdle();
-    }
-
-    // Called when a URI is loaded in any tab. We have to listen for this
-    // because tabSelected is not called if I open a new tab which loads
-    // about:home and then navigate to the dashboard, or navigation via
-    // links on the currently open tab.
-    let listener = {
-      onLocationChange: function onLocationChange(browser, pr, req, loc, flag) {
-        let win = Services.wm.getMostRecentWindow("navigator:browser");
-        if (win.gBrowser.selectedBrowser == browser) {
-          if (loc.prePath == self.DASHBOARD_ORIGIN) {
-            dashboardLoaded(browser);
-          }
-        }
-      }
-    };
-    // Called when the current tab selection changes.
-    function tabSelected(event) {
-      let browser = event.target.linkedBrowser;
-      if (browser.currentURI.prePath == self.DASHBOARD_ORIGIN) {
-        dashboardLoaded(browser);
-      } else {
-        dashboardUnloaded();
-      }
-    }
-
-    // Add listeners for all windows opened in the future.
-    function winWatcher(subject, topic) {
-      if (topic != "domwindowopened") {
-        return;
-      }
-      subject.addEventListener("load", function winWatcherLoad() {
-        subject.removeEventListener("load", winWatcherLoad, false);
-        let doc = subject.document.documentElement;
-        if (doc.getAttribute("windowtype") == "navigator:browser") {
-          let browser = subject.gBrowser;
-          browser.addTabsProgressListener(listener);
-          browser.tabContainer.addEventListener("TabSelect", tabSelected);
-        }
-      }, false);
-    }
-    Services.ww.registerNotification(winWatcher);
-
-    // Add listeners for all current open windows.
-    let enumerator = Services.wm.getEnumerator("navigator:browser");
-    while (enumerator.hasMoreElements()) {
-      let browser = enumerator.getNext().gBrowser;
-      browser.addTabsProgressListener(listener);
-      browser.tabContainer.addEventListener("TabSelect", tabSelected);
-
-      // Also check the currently open URI.
-      if (browser.currentURI.prePath == this.DASHBOARD_ORIGIN) {
-        dashboardLoaded(browser);
-      }
-    }
-
-    // Add listeners for app installs/uninstall.
-    Services.obs.addObserver(this, "webapps-sync-install", false);
-    Services.obs.addObserver(this, "webapps-sync-uninstall", false);
-
-    // Add listener for idle service.
-    let idleSvc = Cc["@mozilla.org/widget/idleservice;1"].
-                  getService(Ci.nsIIdleService);
-    idleSvc.addIdleObserver(this,
-                            Preferences.get("services.aitc.main.idleTime"));
-  },
-
-  observe: function(subject, topic, data) {
-    let app;
-    switch (topic) {
-      case "webapps-sync-install":
-        app = JSON.parse(data);
-        this._log.info(app.origin + " was installed, initiating PUT");
-        this._manager.appEvent("install", app);
-        break;
-      case "webapps-sync-uninstall":
-        app = JSON.parse(data);
-        this._log.info(app.origin + " was uninstalled, initiating PUT");
-        this._manager.appEvent("uninstall", app);
-        break;
-      case "idle":
-        this._log.info("User went idle");
-        if (this._manager) {
-          this._manager.userIdle();
-        }
-        break;
-      case "back":
-        this._log.info("User is no longer idle");
-        let win = Services.wm.getMostRecentWindow("navigator:browser");
-        if (win && win.gBrowser.currentURI.prePath == this.DASHBOARD_ORIGIN &&
-            this._manager) {
-          this._manager.userActive();
-        }
-        break;
-    }
-  },
-
-};
deleted file mode 100644
--- a/services/aitc/modules/manager.js
+++ /dev/null
@@ -1,694 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["AitcManager"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-Cu.import("resource://gre/modules/Webapps.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-
-Cu.import("resource://services-aitc/client.js");
-Cu.import("resource://services-aitc/browserid.js");
-Cu.import("resource://services-aitc/storage.js");
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/preferences.js");
-Cu.import("resource://services-common/tokenserverclient.js");
-Cu.import("resource://services-common/utils.js");
-
-const PREFS = new Preferences("services.aitc.");
-const INITIAL_TOKEN_DURATION = 240000; // 4 minutes
-const DASHBOARD_URL = PREFS.get("dashboard.url");
-const MARKETPLACE_URL = PREFS.get("marketplace.url");
-
-/**
- * The constructor for the manager takes a callback, which will be invoked when
- * the manager is ready (construction is asynchronous). *DO NOT* call any
- * methods on this object until the callback has been invoked, doing so will
- * lead to undefined behaviour. The premadeClient and premadeToken are used
- * to bypass BrowserID for xpcshell tests, since the window object in not
- * available.
- */
-this.AitcManager = function AitcManager(cb, premadeClient, premadeToken) {
-  this._client = null;
-  this._getTimer = null;
-  this._putTimer = null;
-
-  this._lastTokenTime = 0;
-  this._tokenDuration = INITIAL_TOKEN_DURATION;
-  this._premadeToken = premadeToken || null;
-  this._invalidTokenFlag = false;
-
-  this._lastEmail = null;
-  this._dashboardWindow = null;
-
-  this._log = Log4Moz.repository.getLogger("Service.AITC.Manager");
-  this._log.level = Log4Moz.Level[Preferences.get("manager.log.level")];
-  this._log.info("Loading AitC manager module");
-
-  // Check if we have pending PUTs from last time.
-  let self = this;
-  this._pending = new AitcQueue("webapps-pending.json", function _queueDone() {
-    // Inform the AitC service that we're good to go!
-    self._log.info("AitC manager has finished loading");
-    try {
-      cb(true);
-    } catch (e) {
-      self._log.error(new Error("AitC manager callback threw " + e));
-    }
-
-    // Used for testing.
-    if (premadeClient) {
-      self._client = premadeClient;
-      cb(null, true);
-      return;
-    }
-
-    // Caller will invoke initialSchedule which will process any items in the
-    // queue, if present.
-  });
-}
-AitcManager.prototype = {
-  /**
-   * State of the user. ACTIVE implies user is looking at the dashboard,
-   * PASSIVE means either not at the dashboard or the idle timer started.
-   */
-  _ACTIVE: 1,
-  _PASSIVE: 2,
-
-  /**
-   * Smart setter that will only call _setPoll is the value changes.
-   */
-  _clientState: null,
-  get _state() {
-    return this._clientState;
-  },
-  set _state(value) {
-    if (this._clientState == value) {
-      return;
-    }
-    this._clientState = value;
-    this._setPoll();
-  },
-
-  /**
-   * Local app was just installed or uninstalled, ask client to PUT if user
-   * is logged in.
-   */
-  appEvent: function appEvent(type, app) {
-    // Add this to the equeue.
-    let self = this;
-    let obj = {type: type, app: app, retries: 0, lastTime: 0};
-    this._pending.enqueue(obj, function _enqueued(err, rec) {
-      if (err) {
-        self._log.error("Could not add " + type + " " + app + " to queue");
-        return;
-      }
-
-      // If we already have a client (i.e. user is logged in), attempt to PUT.
-      if (self._client) {
-        self._processQueue();
-        return;
-      }
-
-      // If not, try a silent client creation.
-      self._makeClient(function(err, client) {
-        if (!err && client) {
-          self._client = client;
-          self._processQueue();
-        }
-        // If user is not logged in, we'll just have to try later.
-      });
-    });
-  },
-
-  /**
-   * User is looking at dashboard. Start polling actively, but if user isn't
-   * logged in, prompt for them to login via a dialog.
-   */
-  userActive: function userActive(win) {
-    // Stash a reference to the dashboard window in case we need to prompt
-    this._dashboardWindow = win;
-
-    if (this._client) {
-      this._state = this._ACTIVE;
-      return;
-    }
-
-    // Make client will first try silent login, if it doesn't work, a popup
-    // will be shown in the context of the dashboard. We shouldn't be
-    // trying to make a client every time this function is called, there is
-    // room for optimization (Bug 750607).
-    let self = this;
-    this._makeClient(function(err, client) {
-      if (err) {
-        // Notify user of error (Bug 750610).
-        self._log.error("Client not created at Dashboard");
-        return;
-      }
-      self._client = client;
-      self._state = self._ACTIVE;
-    }, true, win);
-  },
-
-  /**
-   * User is idle, (either by idle observer, or by not being on the dashboard).
-   * When the user is no longer idle and the dashboard is the current active
-   * page, a call to userActive MUST be made.
-   */
-  userIdle: function userIdle() {
-    this._state = this._PASSIVE;
-    this._dashboardWindow = null;
-  },
-
-  /**
-   * Initial schedule for the manager. It is the responsibility of the
-   * caller who created this object to call this function if it wants to
-   * do an initial sync (i.e. upload local apps on a device that has never
-   * communicated with AITC before).
-   *
-   * The callback will be invoked with the number of local apps that were
-   * queued to be uploaded, or -1 if this client has already synced and a
-   * local upload is not required.
-   *
-   * Try to schedule PUTs but only if we can get a silent assertion, and if
-   * the queue in non-empty, or we've never done a GET (first run).
-   */
-  initialSchedule: function initialSchedule(cb) {
-    let self = this;
-
-    function startProcessQueue(num) {
-      self._makeClient(function(err, client) {
-        if (!err && client) {
-          self._client = client;
-          self._processQueue();
-          return;
-        }
-      });
-      cb(num);
-    }
-
-    // If we've already done a sync with AITC, it means we've already done
-    // an initial upload. Resume processing the queue, if there are items in it.
-    if (Preferences.get("services.aitc.client.lastModified", "0") != "0") {
-      if (this._pending.length) {
-        startProcessQueue(-1);
-      } else {
-        cb(-1);
-      }
-      return;
-    }
-
-    DOMApplicationRegistry.getAllWithoutManifests(function gotAllApps(apps) {
-      let done = 0;
-      let appids = Object.keys(apps);
-      let total = appids.length;
-      self._log.info("First run, queuing all local apps: " + total + " found");
-
-      function appQueued(err) {
-        if (err) {
-          self._log.error("Error queuing app " + apps[appids[done]].origin);
-        }
-
-        if (done == total) {
-          self._log.info("Finished queuing all initial local apps");
-          startProcessQueue(total);
-          return;
-        }
-
-        let app = apps[appids[done]];
-        let obj = {type: "install", app: app, retries: 0, lastTime: 0};
-
-        done += 1;
-        self._pending.enqueue(obj, appQueued);
-      }
-      appQueued();
-    });
-  },
-
-  /**
-   * Poll the AITC server for any changes and process them. It is safe to call
-   * this function multiple times. Last caller wins. The function will
-   * grab the current user state from _state and act accordingly.
-   *
-   * Invalid states will cause this function to throw.
-   */
-  _setPoll: function _setPoll() {
-    if (this._state == this._ACTIVE && !this._client) {
-      throw new Error("_setPoll(ACTIVE) called without client");
-    }
-    if (this._state != this._ACTIVE && this._state != this._PASSIVE) {
-      throw new Error("_state is invalid " + this._state);
-    }
-
-    if (!this._client) {
-      // User is not logged in, we cannot do anything.
-      self._log.warn("_setPoll called but user not logged in, ignoring");
-      return;
-    }
-
-    // Check if there are any PUTs pending first.
-    if (this._pending.length && !(this._putTimer)) {
-      // There are pending PUTs and no timer, so let's process them. GETs will
-      // resume after the PUTs finish (see processQueue)
-      this._processQueue();
-      return;
-    }
-
-    // Do one GET soon, but only if user is active.
-    let getFreq;
-    if (this._state == this._ACTIVE) {
-      CommonUtils.nextTick(this._checkServer, this);
-      getFreq = PREFS.get("manager.getActiveFreq");
-    } else {
-      getFreq = PREFS.get("manager.getPassiveFreq");
-    }
-
-    // Cancel existing timer, if any.
-    if (this._getTimer) {
-      this._getTimer.cancel();
-      this._getTimer = null;
-    }
-
-    // Start the timer for GETs.
-    let self = this;
-    this._log.info("Starting GET timer");
-    this._getTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    this._getTimer.initWithCallback({notify: this._checkServer.bind(this)},
-                                    getFreq, Ci.nsITimer.TYPE_REPEATING_SLACK);
-
-    this._log.info("GET timer set, next attempt in " + getFreq + "ms");
-  },
-
-  /**
-   * Checks if the current token we hold is valid. If not, we obtain a new one
-   * and execute the provided func. If a token could not be obtained, func will
-   * not be called and an error will be logged.
-   */
-  _validateToken: function _validateToken(func) {
-    let timeSinceLastToken = Date.now() - this._lastTokenTime;
-    if (!this._invalidTokenFlag && timeSinceLastToken < this._tokenDuration) {
-      this._log.info("Current token is valid");
-      func();
-      return;
-    }
-
-    this._log.info("Current token is invalid");
-    let win;
-    if (this._state == this.ACTIVE) {
-      win = this._dashboardWindow;
-    }
-
-    let self = this;
-    this._refreshToken(function(err, done) {
-      if (!done) {
-        self._log.warn("_checkServer could not refresh token, aborting");
-        return;
-      }
-      func(err);
-    }, win);
-  },
-
-  /**
-   * Do a GET check on the server to see if we have any new apps. Abort if
-   * there are pending PUTs. If we GET some apps, send to storage for
-   * further processing.
-   */
-  _checkServer: function _checkServer() {
-    if (!this._client) {
-      throw new Error("_checkServer called without a client");
-    }
-
-    if (this._pending.length) {
-      this._log.warn("_checkServer aborted because of pending PUTs");
-      return;
-    }
-
-    let self = this;
-    this._validateToken(function validation(err) {
-      if (err) {
-        self._log.error(err);
-      } else {
-        self._getApps();
-      }
-    });
-  },
-
-  _getApps: function _getApps() {
-    // Do a GET
-    this._log.info("Attempting to getApps");
-
-    let self = this;
-    this._client.getApps(function gotApps(err, apps) {
-      if (err) {
-        // Error was logged in client.
-        if (err.authfailure) {
-          self._invalidTokenFlag = true;
-          self._validateToken(function revalidated(err) {
-            if (!err) {
-              self._getApps();
-            }
-          });
-        } else {
-          return;
-        }
-      }
-      if (!apps) {
-        // No changes, got 304.
-        return;
-      }
-      if (!apps.length) {
-        // Empty array, nothing to process
-        self._log.info("No apps found on remote server");
-        return;
-      }
-
-      // Send list of remote apps to storage to apply locally
-      AitcStorage.processApps(apps, function processedApps() {
-        self._log.info("processApps completed successfully, changes applied");
-      });
-    });
-  },
-
-  /**
-   * Go through list of apps to PUT and attempt each one. If we fail, try
-   * again in PUT_FREQ. Will throw if called with an empty, _reschedule()
-   * makes sure that we don't.
-   */
-  _processQueue: function _processQueue() {
-    if (!this._client) {
-      throw new Error("_processQueue called without a client");
-    }
-    if (!this._pending.length) {
-      throw new Error("_processQueue called with an empty queue");
-    }
-
-    if (this._putInProgress) {
-      // The network request sent out as a result to the last call to
-      // _processQueue still isn't done. A timer is created they all
-      // finish to make sure this function is called again if neccessary.
-      return;
-    }
-
-    let self = this;
-    this._validateToken(function validation(err) {
-      if (err) {
-        self._log.error(err);
-      } else {
-        self._putApps();
-      }
-    });
-  },
-
-  _putApps: function _putApps() {
-    this._putInProgress = true;
-    let record = this._pending.peek();
-
-    this._log.info("Processing record type " + record.type);
-
-    let self = this;
-    function _clientCallback(err, done) {
-      // Send to end of queue if unsuccessful or err.removeFromQueue is false.
-      if (err && !err.removeFromQueue) {
-        self._log.info("PUT failed, re-adding to queue");
-        // Error was logged in client.
-        if (err.authfailure) {
-          self._invalidTokenFlag = true;
-          self._validateToken(function validation(err) {
-            if (err) {
-              self._log.error("Failed to obtain an updated token");
-            }
-            _reschedule();
-          });
-          return;
-        }
-
-        // Update retries and time
-        record.retries += 1;
-        record.lastTime = new Date().getTime();
-
-        // Add updated record to the end of the queue.
-        self._pending.enqueue(record, function(err, done) {
-          if (err) {
-            self._log.error("Enqueue failed " + err);
-            _reschedule();
-            return;
-          }
-          // If record was successfully added, remove old record.
-          self._pending.dequeue(function(err, done) {
-            if (err) {
-              self._log.error("Dequeue failed " + err);
-            }
-            _reschedule();
-            return;
-          });
-        });
-      }
-
-      // If succeeded or client told us to remove from queue
-      self._log.info("_putApp asked us to remove it from queue");
-      self._pending.dequeue(function(err, done) {
-        if (err) {
-          self._log.error("Dequeue failed " + e);
-        }
-        _reschedule();
-      });
-    }
-
-    function _reschedule() {
-      // Release PUT lock
-      self._putInProgress = false;
-
-      // We just finished PUTting an object, try the next one immediately,
-      // but only if haven't tried it already in the last putFreq (ms).
-      if (!self._pending.length) {
-        // Start GET timer now that we're done with PUTs.
-        self._setPoll();
-        return;
-      }
-
-      let obj = self._pending.peek();
-      let cTime = new Date().getTime();
-      let freq = PREFS.get("manager.putFreq");
-
-      // We tried this object recently, we'll come back to it later.
-      if (obj.lastTime && ((cTime - obj.lastTime) < freq)) {
-        self._log.info("Scheduling next processQueue in " + freq);
-        CommonUtils.namedTimer(self._processQueue, freq, self, "_putTimer");
-        return;
-      }
-
-      // Haven't tried this PUT yet, do it immediately.
-      self._log.info("Queue non-empty, processing next PUT");
-      self._processQueue();
-    }
-
-    switch (record.type) {
-      case "install":
-        this._client.remoteInstall(record.app, _clientCallback);
-        break;
-      case "uninstall":
-        record.app.hidden = true;
-        this._client.remoteUninstall(record.app, _clientCallback);
-        break;
-      default:
-        this._log.warn(
-          "Unrecognized type " + record.type + " in queue, removing"
-        );
-        let self = this;
-        this._pending.dequeue(function _dequeued(err) {
-          if (err) {
-            self._log.error("Dequeue of unrecognized app type failed");
-          }
-          _reschedule();
-        });
-    }
-  },
-
-  /* Obtain a (new) token from the Sagrada token server. If win is is specified,
-   * the user will be asked to login via UI, if required. The callback's
-   * signature is cb(err, done). If a token is obtained successfully, done will
-   * be true and err will be null.
-   */
-  _refreshToken: function _refreshToken(cb, win) {
-    if (!this._client) {
-      throw new Error("_refreshToken called without an active client");
-    }
-
-    this._log.info("Token refresh requested");
-
-    let self = this;
-    function refreshedAssertion(err, assertion) {
-      if (!err) {
-        self._getToken(assertion, function(err, token) {
-          if (err) {
-            cb(err, null);
-            return;
-          }
-          self._lastTokenTime = Date.now();
-          self._client.updateToken(token);
-          self._invalidTokenFlag = false;
-          cb(null, true);
-        });
-        return;
-      }
-
-      // Silent refresh was asked for.
-      if (!win) {
-        cb(err, null);
-        return;
-      }
-
-      // Prompt user to login.
-      self._makeClient(function(err, client) {
-        if (err) {
-          cb(err, null);
-          return;
-        }
-
-        // makeClient sets an updated token.
-        self._client = client;
-        self._invalidTokenFlag = false;
-        cb(null, true);
-      }, win);
-    }
-
-    let options = { audience: DASHBOARD_URL };
-    if (this._lastEmail) {
-      options.requiredEmail = this._lastEmail;
-    } else {
-      options.sameEmailAs = MARKETPLACE_URL;
-    }
-    if (this._premadeToken) {
-      this._client.updateToken(this._premadeToken);
-      this._tokenDuration = parseInt(this._premadeToken.duration, 10);
-      this._lastTokenTime = Date.now();
-      this._invalidTokenFlag = false;
-      cb(null, true);
-    } else {
-      BrowserID.getAssertion(refreshedAssertion, options);
-    }
-  },
-
-  /* Obtain a token from Sagrada token server, given a BrowserID assertion
-   * cb(err, token) will be invoked on success or failure.
-   */
-  _getToken: function _getToken(assertion, cb) {
-    let url = PREFS.get("tokenServer.url") + "/1.0/aitc/1.0";
-    let client = new TokenServerClient();
-
-    this._log.info("Obtaining token from " + url);
-
-    let self = this;
-    try {
-      client.getTokenFromBrowserIDAssertion(url, assertion, function(err, tok) {
-        self._gotToken(err, tok, cb);
-      });
-    } catch (e) {
-      cb(new Error(e), null);
-    }
-  },
-
-  // Token recieved from _getToken.
-  _gotToken: function _gotToken(err, tok, cb) {
-    if (!err) {
-      this._log.info("Got token from server: " + JSON.stringify(tok));
-      this._tokenDuration = parseInt(tok.duration, 10);
-      cb(null, tok);
-      return;
-    }
-
-    let msg = "Error in _getToken: " + err;
-    this._log.error(msg);
-    cb(msg, null);
-  },
-
-  // Extract the email address from a BrowserID assertion.
-  _extractEmail: function _extractEmail(assertion) {
-    // Please look the other way while I do this. Thanks.
-    let chain = assertion.split("~");
-    let len = chain.length;
-    if (len < 2) {
-      return null;
-    }
-
-    try {
-      // We need CommonUtils.decodeBase64URL.
-      let cert = JSON.parse(atob(
-        chain[0].split(".")[1].replace("-", "+", "g").replace("_", "/", "g")
-      ));
-      return cert.principal.email;
-    } catch (e) {
-      return null;
-    }
-  },
-
-  /* To start the AitcClient we need a token, for which we need a BrowserID
-   * assertion. If login is true, makeClient will ask the user to login in
-   * the context of win. cb is called with (err, client).
-   */
-  _makeClient: function makeClient(cb, login, win) {
-    if (!cb) {
-      throw new Error("makeClient called without callback");
-    }
-    if (login && !win) {
-      throw new Error("makeClient called with login as true but no win");
-    }
-
-    let self = this;
-    let ctxWin = win;
-    function processAssertion(val) {
-      // Store the email we got the token for so we can refresh.
-      self._lastEmail = self._extractEmail(val);
-      self._log.info("Got assertion from BrowserID, creating token");
-      self._getToken(val, function(err, token) {
-        if (err) {
-          cb(err, null);
-          return;
-        }
-
-        // Store when we got the token so we can refresh it as needed.
-        self._lastTokenTime = Date.now();
-
-        // We only create one client instance, store values in a pref tree
-        cb(null, new AitcClient(
-          token, new Preferences("services.aitc.client.")
-        ));
-      });
-    }
-    function gotSilentAssertion(err, val) {
-      self._log.info("gotSilentAssertion called");
-      if (err) {
-        // If we were asked to let the user login, do the popup method.
-        if (login) {
-          self._log.info("Could not obtain silent assertion, retrying login");
-          BrowserID.getAssertionWithLogin(function gotAssertion(err, val) {
-            if (err) {
-              self._log.error(err);
-              cb(err, false);
-              return;
-            }
-            processAssertion(val);
-          }, ctxWin);
-          return;
-        }
-        self._log.warn("Could not obtain assertion in _makeClient");
-        cb(err, false);
-      } else {
-        processAssertion(val);
-      }
-    }
-
-    // Check if we can get assertion silently first
-    self._log.info("Attempting to obtain assertion silently")
-    BrowserID.getAssertion(gotSilentAssertion, {
-      audience: DASHBOARD_URL,
-      sameEmailAs: MARKETPLACE_URL
-    });
-  },
-
-};
deleted file mode 100644
--- a/services/aitc/modules/storage.js
+++ /dev/null
@@ -1,450 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["AitcStorage", "AitcQueue"];
-
-const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
-
-Cu.import("resource://gre/modules/NetUtil.jsm");
-Cu.import("resource://gre/modules/Webapps.jsm");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/preferences.js");
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-common/utils.js");
-
-/**
- * Provides a file-backed queue. Currently used by manager.js as persistent
- * storage to manage pending installs and uninstalls.
- *
- * @param filename
- *        (String)    The file backing this queue will be named as this string.
- *
- * @param cb
- *        (Function)  This function will be called when the queue is ready to
- *                    use. *DO NOT* call any methods on this object until the
- *                    callback is invoked, if you do so, none of your operations
- *                    will be persisted on disk.
- *
- */
-this.AitcQueue = function AitcQueue(filename, cb) {
-  if (!cb) {
-    throw new Error("AitcQueue constructor called without callback");
-  }
-
-  this._log = Log4Moz.repository.getLogger("Service.AITC.Storage.Queue");
-  this._log.level = Log4Moz.Level[Preferences.get(
-    "services.aitc.storage.log.level"
-  )];
-
-  this._queue = [];
-  this._writeLock = false;
-  this._filePath = "webapps/" + filename;
-
-  this._log.info("AitcQueue instance loading");
-
-  CommonUtils.jsonLoad(this._filePath, this, function jsonLoaded(data) {
-    if (data && Array.isArray(data)) {
-      this._queue = data;
-    }
-    this._log.info("AitcQueue instance created");
-    cb(true);
-  });
-}
-AitcQueue.prototype = {
-  /**
-   * Add an object to the queue, and data is saved to disk.
-   */
-  enqueue: function enqueue(obj, cb) {
-    this._log.info("Adding to queue " + obj);
-
-    if (!cb) {
-      throw new Error("enqueue called without callback");
-    }
-
-    let self = this;
-    this._queue.push(obj);
-
-    try {
-      this._putFile(this._queue, function _enqueuePutFile(err) {
-        if (err) {
-          // Write unsuccessful, don't add to queue.
-          self._queue.pop();
-          cb(err, false);
-          return;
-        }
-        // Successful write.
-        cb(null, true);
-        return;
-      });
-    } catch (e) {
-      self._queue.pop();
-      cb(e, false);
-    }
-  },
-
-  /**
-   * Remove the object at the head of the queue, and data is saved to disk.
-   */
-  dequeue: function dequeue(cb) {
-    this._log.info("Removing head of queue");
-
-    if (!cb) {
-      throw new Error("dequeue called without callback");
-    }
-    if (!this._queue.length) {
-      throw new Error("Queue is empty");
-    }
-
-    let self = this;
-    let obj = this._queue.shift();
-
-    try {
-      this._putFile(this._queue, function _dequeuePutFile(err) {
-        if (!err) {
-          // Successful write.
-          cb(null, true);
-          return;
-        }
-        // Unsuccessful write, put back in queue.
-        self._queue.unshift(obj);
-        cb(err, false);
-      });
-    } catch (e) {
-      self._queue.unshift(obj);
-      cb(e, false);
-    }
-  },
-
-  /**
-   * Return the object at the front of the queue without removing it.
-   */
-  peek: function peek() {
-    this._log.info("Peek called when length " + this._queue.length);
-    if (!this._queue.length) {
-      throw new Error("Queue is empty");
-    }
-    return this._queue[0];
-  },
-
-  /**
-   * Find out the length of the queue.
-   */
-  get length() {
-    return this._queue.length;
-  },
-
-  /**
-   * Put an array into the cache file. Will throw an exception if there is
-   * an error while trying to write to the file.
-   */
-  _putFile: function _putFile(value, cb) {
-    if (this._writeLock) {
-      throw new Error("_putFile already in progress");
-    }
-
-    this._writeLock = true;
-    this._log.info("Writing queue to disk");
-    CommonUtils.jsonSave(this._filePath, this, value, function jsonSaved(err) {
-      if (err) {
-        let msg = new Error("_putFile failed with " + err);
-        this._writeLock = false;
-        cb(msg);
-        return;
-      }
-      this._log.info("_putFile succeeded");
-      this._writeLock = false;
-      cb(null);
-    });
-  },
-};
-
-/**
- * An interface to DOMApplicationRegistry, used by manager.js to process
- * remote changes received and apply them to the local registry.
- */
-function AitcStorageImpl() {
-  this._log = Log4Moz.repository.getLogger("Service.AITC.Storage");
-  this._log.level = Log4Moz.Level[Preferences.get(
-    "services.aitc.storage.log.level"
-  )];
-  this._log.info("Loading AitC storage module");
-}
-AitcStorageImpl.prototype = {
-  /**
-   * Determines what changes are to be made locally, given a list of
-   * remote apps.
-   *
-   * @param remoteApps
-   *        (Array)     An array of app records fetched from the AITC server.
-   *
-   * @param callback
-   *        (function)  A callback to be invoked when processing is finished.
-   */
-  processApps: function processApps(remoteApps, callback) {
-    let self = this;
-    this._log.info("Server check got " + remoteApps.length + " apps");
-
-    // Get the set of local apps, and then pass to _processApps.
-    // _processApps will check for the validity of remoteApps.
-    DOMApplicationRegistry.getAllWithoutManifests(
-      function _processAppsGotLocalApps(localApps) {
-        let changes = self._determineLocalChanges(localApps, remoteApps);
-        self._processChanges(changes, callback);
-      }
-    );
-  },
-
-  /**
-   * Determine the set of changes needed to reconcile local with remote data.
-   *
-   * The return value is a mapping describing the actions that need to be
-   * performed. It has the following keys:
-   *
-   *   deleteIDs
-   *     (Array) String app IDs of applications that need to be uninstalled.
-   *   installs
-   *     (Object) Maps app ID to the remote app record. The app ID may exist
-   *       locally. If the app did not exist previously, a new ID will be
-   *       generated and used here.
-   *
-   * @param localApps
-   *        (Object) Mapping of App ID to minimal application record (from
-   *        DOMApplicationRegistry.getAllWithoutManifests()).
-   * @param remoteApps
-   *        (Array) Application records from the server.
-   */
-  _determineLocalChanges: function _determineChanges(localApps, remoteApps) {
-    let changes = new Map();
-    changes.deleteIDs = [];
-    changes.installs  = {};
-
-    // If there are no remote apps, do nothing.
-    //
-    // Arguably, the correct thing to do is to delete all local apps. The
-    // server is the authoritative source, after all. But, we play it safe.
-    if (!Object.keys(remoteApps).length) {
-      this._log.warn("Empty set of remote apps. Not taking any action.");
-      return changes;
-    }
-
-    // This is all to avoid potential duplicates. Once JS Sets are iterable, we
-    // should switch everything to use them.
-    let deletes       = {};
-    let remoteOrigins = {};
-
-    let localOrigins = {};
-    for (let [id, app] in Iterator(localApps)) {
-      localOrigins[app.origin] = id;
-    }
-
-    for (let remoteApp of remoteApps) {
-      let origin = remoteApp.origin;
-      remoteOrigins[origin] = true;
-
-      // If the app is hidden on the remote server, that translates to a local
-      // delete/uninstall, but only if the app is present locally.
-      if (remoteApp.hidden) {
-        if (origin in localOrigins) {
-          deletes[localOrigins[origin]] = true;
-        }
-
-        continue;
-      }
-
-      // If the remote app isn't present locally, we install it under a
-      // newly-generated ID.
-      if (!localApps[origin]) {
-        changes.installs[DOMApplicationRegistry.makeAppId()] = remoteApp;
-        continue;
-      }
-
-      // If the remote app is newer, we force a re-install using the existing
-      // ID.
-      if (localApps[origin].installTime < remoteApp.installTime) {
-        changes.installs[localApps[origin]] = remoteApp;
-        continue;
-      }
-    }
-
-    // If we have local apps not on the server, we need to delete them, as the
-    // server is authoritative.
-    for (let [id, app] in Iterator(localApps)) {
-      if (!(app.origin in remoteOrigins)) {
-        deletes[id] = true;
-      }
-    }
-
-    changes.deleteIDs = Object.keys(deletes);
-
-    return changes;
-  },
-
-  /**
-   * Process changes so local client is in sync with server.
-   *
-   * This takes the output from _determineLocalChanges() and applies it.
-   *
-   * The supplied callback is invoked with no arguments when all operations
-   * have completed.
-   */
-  _processChanges: function _processChanges(changes, cb) {
-
-    if (!changes.deleteIDs.length && !Object.keys(changes.installs).length) {
-      this._log.info("No changes to be applied.");
-      cb();
-      return;
-    }
-
-    // First, we assemble all the changes in the right format.
-    let installs = [];
-    for (let [id, record] in Iterator(changes.installs)) {
-      installs.push({id: id, value: record});
-    }
-
-    let uninstalls = [];
-    for (let id of changes.deleteIDs) {
-      this._log.info("Uninstalling app: " + id);
-      uninstalls.push({id: id, hidden: true});
-    }
-
-    // Now we need to perform actions.
-    //
-    // We want to perform all the uninstalls followed by all the installs.
-    // However, this is somewhat complicated because uninstalls are
-    // asynchronous and there may not even be any uninstalls. So, we simply
-    // define a clojure to invoke installation and we call it whenever we're
-    // ready.
-    let doInstalls = function doInstalls() {
-      if (!installs.length) {
-        if (cb) {
-          try {
-            cb();
-          } catch (ex) {
-            this._log.warn("Exception when invoking callback: " +
-                           CommonUtils.exceptionStr(ex));
-          } finally {
-            cb = null;
-          }
-        }
-        return;
-      }
-
-      this._applyInstalls(installs, cb);
-
-      // Prevent double invoke, just in case.
-      installs = [];
-      cb       = null;
-    }.bind(this);
-
-    if (uninstalls.length) {
-      DOMApplicationRegistry.updateApps(uninstalls, function onComplete() {
-        doInstalls();
-        return;
-      });
-    } else {
-      doInstalls();
-    }
-  },
-
-  /**
-   * Apply a set of installs to the local registry. Fetch each app's manifest
-   * in parallel (don't retry on failure) and insert into registry.
-   */
-  _applyInstalls: function _applyInstalls(toInstall, callback) {
-    let done = 0;
-    let total = toInstall.length;
-    this._log.info("Applying " + total + " installs to registry");
-
-    /**
-     * The purpose of _checkIfDone is to invoke the callback after all the
-     * installs have been applied. They all fire in parallel, and each will
-     * check-in when it is done.
-     */
-    let self = this;
-    function _checkIfDone() {
-      done += 1;
-      self._log.debug(done + "/" + total + " apps processed");
-      if (done == total) {
-        callback();
-      }
-    }
-
-    function _makeManifestCallback(appObj) {
-      return function(err, manifest) {
-        if (err) {
-          self._log.warn("Could not fetch manifest for " + appObj.name);
-          _checkIfDone();
-          return;
-        }
-        appObj.value.manifest = manifest;
-        DOMApplicationRegistry.updateApps([appObj], _checkIfDone);
-      }
-    }
-
-    /**
-     * Now we get a manifest for each record, and add it to the local registry
-     * when we receive it. If a manifest GET times out, we will not add
-     * the app to the registry but count as "success" anyway. The app will
-     * be added on the next GET poll, hopefully the manifest will be
-     * available then.
-     */
-    for each (let app in toInstall) {
-      let url = app.value.manifestURL;
-      if (url[0] == "/") {
-        url = app.value.origin + app.value.manifestURL;
-      }
-      this._getManifest(url, _makeManifestCallback(app));
-    }
-  },
-
-  /**
-   * Fetch a manifest from given URL. No retries are made on failure. We'll
-   * timeout after 20 seconds.
-   */
-  _getManifest: function _getManifest(url, callback) {
-    let req = new RESTRequest(url);
-    req.timeout = 20;
-
-    let self = this;
-    req.get(function(error) {
-      if (error) {
-        callback(error, null);
-        return;
-      }
-      if (!req.response.success) {
-        callback(new Error("Non-200 while fetching manifest"), null);
-        return;
-      }
-
-      let err = null;
-      let manifest = null;
-      try {
-        manifest = JSON.parse(req.response.body);
-        if (!manifest.name) {
-          self._log.warn(
-            "_getManifest got invalid manifest: " + req.response.body
-          );
-          err = new Error("Invalid manifest fetched");
-        }
-      } catch (e) {
-        self._log.warn(
-          "_getManifest got invalid JSON response: " + req.response.body
-        );
-        err = new Error("Invalid manifest fetched");
-      }
-
-      callback(err, manifest);
-    });
-  },
-
-};
-
-XPCOMUtils.defineLazyGetter(this, "AitcStorage", function() {
-  return new AitcStorageImpl();
-});
deleted file mode 100644
--- a/services/aitc/services-aitc.js
+++ /dev/null
@@ -1,28 +0,0 @@
-pref("dom.mozApps.used", false); // Set to true by DOMApplicationRegistry
-
-pref("services.aitc.log.dump", false); // Root logger
-pref("services.aitc.log.level", "All");
-
-pref("services.aitc.browserid.url", "https://login.persona.org/sign_in");
-pref("services.aitc.browserid.log.level", "Debug");
-
-pref("services.aitc.client.log.level", "Debug");
-pref("services.aitc.client.timeout", 120); // 120 seconds
-
-pref("services.aitc.dashboard.url", "https://myapps.mozillalabs.com");
-
-pref("services.aitc.main.idleTime", 120000); // 2 minutes
-
-pref("services.aitc.manager.putFreq", 10000); // 10 seconds
-pref("services.aitc.manager.getActiveFreq", 120000); // 2 minutes
-pref("services.aitc.manager.getPassiveFreq", 7200000); // 2 hours
-pref("services.aitc.manager.log.level", "Debug");
-
-pref("services.aitc.marketplace.url", "https://marketplace.mozilla.org");
-
-pref("services.aitc.service.log.level", "Debug");
-
-// TODO: Temporary value. Change to the production server (bug 760903)
-pref("services.aitc.tokenServer.url", "https://stage-token.services.mozilla.com");
-
-pref("services.aitc.storage.log.level", "Debug");
deleted file mode 100644
--- a/services/aitc/tests/Makefile.in
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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/.
-
-DEPTH     = @DEPTH@
-topsrcdir = @top_srcdir@
-srcdir    = @srcdir@
-VPATH     = @srcdir@
-relativesrcdir = @relativesrcdir@
-
-include $(DEPTH)/config/autoconf.mk
-
-MODULE = test_services_aitc
-XPCSHELL_TESTS = unit
-
-
-MOCHITEST_BROWSER_FILES = \
-  mochitest/head.js \
-  mochitest/browser_id_simple.js \
-  mochitest/file_browser_id_mock.html \
-  $(NULL)
-
-include $(topsrcdir)/config/rules.mk
deleted file mode 100644
--- a/services/aitc/tests/mochitest/browser_id_simple.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-const AUD = "http://foo.net";
-
-function test() {
-  waitForExplicitFinish();
-  setEndpoint("browser_id_mock");
-
-  // Get an assertion for default email.
-  BrowserID.getAssertion(gotDefaultAssertion, {audience: AUD});
-}
-
-function gotDefaultAssertion(err, ast) {
-  is(err, null, "gotDefaultAssertion failed with " + err);
-  is(ast, "default@example.org_assertion_" + AUD,
-     "gotDefaultAssertion returned wrong assertion");
-
-  // Get an assertion for a specific email.
-  BrowserID.getAssertion(gotSpecificAssertion, {
-    requiredEmail: "specific@example.org",
-    audience: AUD
-  });
-}
-
-function gotSpecificAssertion(err, ast) {
-  is(err, null, "gotSpecificAssertion failed with " + err);
-  is(ast, "specific@example.org_assertion_" + AUD,
-     "gotSpecificAssertion returned wrong assertion");
-
-  // Get an assertion using sameEmailAs for another domain.
-  BrowserID.getAssertion(gotSameEmailAssertion, {
-    sameEmailAs: "http://zombo.com",
-    audience: AUD
-  });
-}
-
-function gotSameEmailAssertion(err, ast) {
-  is(err, null, "gotSameEmailAssertion failed with " + err);
-  is(ast, "assertion_for_sameEmailAs",
-     "gotSameEmailAssertion returned wrong assertion");
-
-  finish();
-}
deleted file mode 100644
--- a/services/aitc/tests/mochitest/file_browser_id_mock.html
+++ /dev/null
@@ -1,52 +0,0 @@
-<html>
-<head>
-  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-</head>
-<body>
-<p>Mock BrowserID endpoint for a logged-in user</p>
-</body>
-<script>
-
-/**
- * Object containing valid email/key paris for this user. An assertion is simply
- * the string "_assertion_$audience" appended to the email. The exception is
- * when the email address is "sameEmailAs@example.org" the assertion will
- * be "assertion_for_sameEmailAs".
- */
-var _emails = {
-  "default@example.org": "default@example.org_key",
-  "specific@example.org": "specific@example.org_key",
-  "sameEmailAs@example.org": "sameEmailAs@example.org_key"
-};
-var _sameEmailAs = "sameEmailAs@example.org";
-
-// Mock internal API
-window.BrowserID = {};
-window.BrowserID.User = {
-  getStoredEmailKeypairs: function() {
-    return _emails;
-  },
-  getAssertion: function(email, audience, success, error) {
-    if (email == _sameEmailAs) {
-      success("assertion_for_sameEmailAs");
-      return;
-    }
-    if (email in _emails) {
-      success(email + "_assertion_" + audience);
-      return;
-    }
-    error("invalid email specified");
-  }
-};
-window.BrowserID.Storage = {
-  site: {
-    get: function(domain, key) {
-      if (key == "email") {
-        return _sameEmailAs;
-      }
-      return "";
-    }
-  }
-};
-</script>
-</html>
\ No newline at end of file
deleted file mode 100644
--- a/services/aitc/tests/mochitest/head.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-let tmp = {};
-Components.utils.import("resource://gre/modules/Services.jsm");
-Components.utils.import("resource://services-aitc/browserid.js", tmp);
-
-const BrowserID = tmp.BrowserID;
-const testPath = "http://mochi.test:8888/browser/services/aitc/tests/";
-
-function loadURL(aURL, aCB) {
-  gBrowser.selectedBrowser.addEventListener("load", function () {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    is(gBrowser.currentURI.spec, aURL, "loaded expected URL");
-    aCB();
-  }, true);
-  gBrowser.loadURI(aURL);
-}
-
-function setEndpoint(name) {
-  let fullPath = testPath + "file_" + name + ".html";
-  Services.prefs.setCharPref("services.aitc.browserid.url", fullPath);
-}
\ No newline at end of file
deleted file mode 100644
--- a/services/aitc/tests/unit/test_aitc_client.js
+++ /dev/null
@@ -1,163 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Cu.import("resource://services-aitc/client.js");
-
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/preferences.js");
-
-Cu.import("resource://testing-common/services-common/aitcserver.js");
-
-const PREFS = new Preferences("services.aitc.client.")
-
-function run_test() {
-  initTestLogging("Trace");
-  run_next_test();
-}
-
-function get_aitc_server() {
-  _("Create new server.");
-
-  let server = new AITCServer10Server();
-  server.start(get_server_port());
-
-  return server;
-}
-
-function get_server_with_user(username) {
-  _("Create server user for User " + username);
-
-  let server = get_aitc_server();
-  server.createUser(username);
-
-  return server;
-}
-
-function get_mock_app(remote) {
-
-  let app = {
-    name: "Mocking Birds",
-    origin: "http://example.com",
-    installOrigin: "http://example.com",
-    installedAt: Date.now(),
-    modifiedAt: Date.now(),
-    receipts: []
-  };
-
-  app[remote ? 'manifestPath' : 'manifestURL'] = "/manifest.webapp";
-
-  return app;
-}
-
-function get_client_for_server(username, server) {
-  _("Create server user for User " + username);
-
-  let token = {
-    endpoint: server.url + username,
-    id: 'ID-HERE',
-    key: 'KEY-HERE'
-  };
-
-  let client = new AitcClient(token, PREFS);
-
-  return client;
-}
-
-// Clean up code - backoff is preserved between requests in a pref
-function advance(server) {
-  PREFS.set("backoff", "0");
-  if (server) {
-    server.stop(run_next_test);
-  } else {
-    run_next_test();
-  }
-}
-
-add_test(function test_getapps_empty() {
-  _("Ensure client request for empty user has appropriate content.");
-
-  const username = "123";
-
-  let server = get_server_with_user(username);
-  let client = get_client_for_server(username, server);
-
-  client.getApps(function(error, apps) {
-    _("Got response");
-    do_check_null(error);
-
-    do_check_true(Array.isArray(apps));
-    do_check_eq(apps.length, 0);
-
-    advance(server);
-  });
-});
-
-add_test(function test_install_app() {
-  _("Ensure client request for installing an app has appropriate content.");
-
-  const username = "123";
-  const app = get_mock_app();
-
-  let server = get_server_with_user(username);
-
-  let client = get_client_for_server(username, server);
-
-  // TODO _putApp instead of, as install requires app in registry
-  client._putApp(client._makeRemoteApp(app), function(error, status) {
-    _("Got response");
-    do_check_null(error);
-
-    do_check_true(status);
-
-    client.getApps(function(error, apps) {
-      _("Got response");
-      do_check_null(error);
-
-      do_check_true(Array.isArray(apps));
-      do_check_eq(apps.length, 1);
-
-      let first = apps[0];
-
-      do_check_eq(first.origin, app.origin);
-
-      advance(server);
-    });
-  });
-});
-
-add_test(function test_uninstall_app() {
-  _("Ensure client request for un-installing an app has appropriate content.");
-
-  const username = "123";
-  const app = get_mock_app();
-
-  let server = get_server_with_user(username);
-  let client = get_client_for_server(username, server);
-
-  server.users[username].addApp(get_mock_app(true));
-
-  client.remoteUninstall(app, function(error, status) {
-    _("Got response");
-    do_check_null(error);
-
-    do_check_true(status);
-
-    client.getApps(function(error, apps) {
-      _("Got response");
-      do_check_eq(error);
-
-      do_check_true(Array.isArray(apps));
-      do_check_eq(apps.length, 1);
-
-      let first = apps[0];
-
-      do_check_eq(first.origin, app.origin);
-      do_check_true(first.hidden);
-
-      advance(server);
-    });
-  });
-});
deleted file mode 100644
--- a/services/aitc/tests/unit/test_aitc_manager.js
+++ /dev/null
@@ -1,263 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Cu.import("resource://gre/modules/Webapps.jsm");
-
-Cu.import("resource://services-aitc/client.js");
-Cu.import("resource://services-aitc/manager.js");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://services-common/preferences.js");
-
-Cu.import("resource://testing-common/services-common/aitcserver.js");
-
-const PREFS = new Preferences("services.aitc.");
-
-let count = 0;
-
-function run_test() {
-  initTestLogging("Trace");
-  run_next_test();
-}
-
-function get_aitc_server() {
-  _("Create new server.");
-
-  let server = new AITCServer10Server();
-  server.start(get_server_port());
-
-  return server;
-}
-
-function get_server_with_user(username) {
-  _("Create server user for User " + username);
-
-  let server = get_aitc_server();
-  server.createUser(username);
-
-  return server;
-}
-
-function get_mock_app() {
-
-  let app = {
-    name: "Mocking Birds",
-    origin: "http://example" + ++count + ".com",
-    installOrigin: "http://example.com",
-    installedAt: Date.now(),
-    modifiedAt: Date.now(),
-    receipts: [],
-    manifestURL: "/manifest.webapp"
-  };
-
-  return app;
-}
-
-function get_mock_queue_element() {
-  return {
-    type: "install",
-    app: get_mock_app(),
-    retries: 0,
-    lastTime: 0
-  }
-}
-
-function get_client_for_server(username, server) {
-  _("Create server user for User " + username);
-
-  let token = {
-    endpoint: server.url + username,
-    id: "ID-HERE",
-    key: "KEY-HERE"
-  };
-
-  return new AitcClient(token, new Preferences("services.aitc.client."));
-}
-
-// Check that a is less than b.
-function do_check_lt(a, b) {
-  do_check_true(a < b);
-}
-
-add_test(function test_manager_localapps() {
-  // Install two fake apps into the DOM registry.
-  let fakeApp1 = get_mock_app();
-  fakeApp1.manifest = {
-    name: "Appasaurus 1",
-    description: "One of the best fake apps ever",
-    launch_path: "/",
-    fullscreen: true,
-    required_features: ["webgl"]
-  };
-
-  let fakeApp2 = get_mock_app();
-  fakeApp2.manifest = {
-    name: "Appasaurus 2",
-    description: "The other best fake app ever",
-    launch_path: "/",
-    fullscreen: true,
-    required_features: ["geolocation"]
-  };
-
-  DOMApplicationRegistry.confirmInstall({app: fakeApp1});
-  DOMApplicationRegistry.confirmInstall({app: fakeApp2});
-
-  // Create an instance of the manager and check if it put the app in the queue.
-  // We put doInitialUpload in nextTick, because maanger will not be defined
-  // in the callback. This pattern is used everywhere, AitcManager is created.
-  let manager = new AitcManager(function() {
-    CommonUtils.nextTick(doInitialUpload);
-  });
-
-  function doInitialUpload() {
-    manager.initialSchedule(function(num) {
-      // 2 apps should have been queued.
-      do_check_eq(num, 2);
-      do_check_eq(manager._pending.length, 2);
-
-      let entry = manager._pending.peek();
-      do_check_eq(entry.type, "install");
-      do_check_eq(entry.app.origin, fakeApp1.origin);
-
-      // Remove one app from queue.
-      manager._pending.dequeue(run_next_test);
-    });
-  }
-});
-
-add_test(function test_manager_alreadysynced() {
-  // The manager should ignore any local apps if we've already synced before.
-  Preferences.set("services.aitc.client.lastModified", "" + Date.now());
-
-  let manager = new AitcManager(function() {
-    CommonUtils.nextTick(doCheck);
-  });
-
-  function doCheck() {
-    manager.initialSchedule(function(num) {
-      do_check_eq(num, -1);
-      do_check_eq(manager._pending.length, 1);
-      // Clear queue for next test.
-      manager._pending.dequeue(run_next_test);
-    });
-  }
-});
-
-add_test(function test_401_responses() {
-  PREFS.set("client.backoff", "50");
-  PREFS.set("manager.putFreq", 50);
-  const app = get_mock_app();
-  const username = "123";
-  const premadeToken = {
-    id: "testtest",
-    key: "testtest",
-    endpoint: "http://localhost:8080/1.0/123",
-    uid: "uid",
-    duration: "5000"
-  };
-
-  let server = get_server_with_user(username);
-  let client = get_client_for_server(username, server);
-
-  server.mockStatus = {
-    code: 401,
-    method: "Unauthorized"
-  };
-
-  let mockRequestCount = 0;
-  let clientFirstToken = null;
-  server.onRequest = function mockstatus() {
-    mockRequestCount++;
-    switch (mockRequestCount) {
-      case 1:
-        clientFirstToken = client.token;
-        // Switch to using mock 201s.
-        this.mockStatus = {
-          code: 201,
-          method: "Created"
-        };
-        break;
-      case 2:
-        // Check that the client obtained a different token.
-        do_check_neq(client.token.id, clientFirstToken.id);
-        do_check_neq(client.token.key, clientFirstToken.key);
-        server.stop(run_next_test);
-        break;
-    }
-  }
-
-  let manager = new AitcManager(function() {
-    CommonUtils.nextTick(gotManager);
-  }, client, premadeToken);
-
-  function gotManager() {
-    // Assume first token is not outdated.
-    manager._lastTokenTime = Date.now();
-    manager.appEvent("install", get_mock_app());
-  }
-});
-
-add_test(function test_client_exponential_backoff() {
-  _("Test that the client is properly setting the backoff");
-
-  // Use prefs to speed up tests.
-  const putFreq = 50;
-  const initialBackoff = 50;
-  const username = "123";
-  PREFS.set("manager.putFreq", putFreq);
-  PREFS.set("client.backoff.initial", initialBackoff);
-  PREFS.set("client.backoff.max", 100000);
-
-  let mockRequestCount = 0;
-  let lastRequestTime = Date.now();
-  // Create server that returns failure codes.
-  let server = get_server_with_user(username);
-  server.mockStatus = {
-    code: 399,
-    method: "Self Destruct"
-  }
-
-  server.onRequest = function onRequest() {
-    mockRequestCount++;
-    let timeDiff, timeNow = Date.now();
-    if (mockRequestCount !== 1) {
-      timeDiff = timeNow - lastRequestTime;
-    }
-    lastRequestTime = timeNow;
-
-    // The time between the 3rd and 4th request should be atleast the
-    // initial backoff.
-    if (mockRequestCount === 4) {
-      do_check_lt(initialBackoff, timeDiff);
-    // The time beween the 4th and 5th request should be atleast double
-    // the intial backoff.
-    } else if (mockRequestCount === 5) {
-      do_check_lt(initialBackoff * 2, timeDiff);
-      server.stop(run_next_test);
-    }
-  }
-
-  // Create dummy client and manager.
-  let client = get_client_for_server(username, server);
-  let manager = new AitcManager(function() {
-    CommonUtils.nextTick(gotManager);
-  }, client);
-
-  function gotManager() {
-    manager._lastTokenTime = Date.now();
-    // Create a bunch of dummy apps for the queue to cycle through.
-    manager._pending._queue = [
-      get_mock_queue_element(),
-      get_mock_queue_element(),
-      get_mock_queue_element(),
-      get_mock_queue_element(),
-    ];
-    // Add the dummy apps to the queue, then start the polling cycle with an app
-    // event.
-    manager._pending._putFile(manager._pending._queue, function (err) {
-      manager.appEvent("install", get_mock_app());
-    });
-  }
-
-});
\ No newline at end of file
deleted file mode 100644
--- a/services/aitc/tests/unit/test_load_modules.js
+++ /dev/null
@@ -1,13 +0,0 @@
-const modules = [
-  "client.js",
-  "browserid.js",
-  "main.js",
-  "manager.js",
-  "storage.js"
-];
-
-function run_test() {
-  for each (let m in modules) {
-    Cu.import("resource://services-aitc/" + m, {});
-  }
-}
deleted file mode 100644
--- a/services/aitc/tests/unit/test_storage_queue.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-aitc/storage.js");
-Cu.import("resource://services-common/async.js");
-
-let queue = null;
-
-function run_test() {
-  initTestLogging();
-  queue = new AitcQueue("test", run_next_test);
-}
-
-add_test(function test_queue_create() {
-  do_check_eq(queue._queue.length, 0);
-  do_check_eq(queue._writeLock, false);
-  run_next_test();
-});
-
-add_test(function test_queue_enqueue() {
-  // Add to queue.
-  let testObj = {foo: "bar"};
-  queue.enqueue(testObj, function(err, done) {
-    do_check_eq(err, null);
-    do_check_true(done);
-
-    // Check if peek value is correct.
-    do_check_eq(queue.peek(), testObj);
-    // Peek should be idempotent.
-    do_check_eq(queue.peek(), testObj);
-
-    run_next_test();
-  });
-});
-
-add_test(function test_queue_dequeue() {
-  // Remove an item and see if queue is empty.
-  queue.dequeue(function(err, done) {
-    do_check_eq(err, null);
-    do_check_true(done);
-    do_check_eq(queue.length, 0);
-    try {
-      queue.peek();
-    } catch (e) {
-      do_check_eq(e.toString(), "Error: Queue is empty");
-      run_next_test();
-    }
-  });
-});
-
-add_test(function test_queue_multiaddremove() {
-  // Queues should handle objects, strings and numbers.
-  let items = [{test:"object"}, "teststring", 42];
-
-  // Two random numbers: how many items to queue and how many to remove.
-  // The next test relies on rem > 0.
-  let num = Math.floor(Math.random() * 100 + 20);
-  let rem = Math.floor(Math.random() * 15 + 5);
-  do_check_true(rem < num);
-  do_check_true(rem > 0);
-
-  // First insert all the items we will remove later.
-  for (let i = 0; i < rem; i++) {
-    let ins = items[Math.round(Math.random() * 2)];
-    let cb = Async.makeSpinningCallback();
-    queue.enqueue(ins, cb);
-    do_check_true(cb.wait());
-  }
-
-  do_check_eq(queue.length, rem);
-
-  // Now insert the items we won't remove.
-  let check = [];
-  for (let i = 0; i < (num - rem); i++) {
-    check.push(items[Math.round(Math.random() * 2)]);
-    let cb = Async.makeSpinningCallback();
-    queue.enqueue(check[check.length - 1], cb);
-    do_check_true(cb.wait());
-  }
-
-  do_check_eq(queue.length, num);
-
-  // Now dequeue rem items.
-  for (let i = 0; i < rem; i++) {
-    let cb = Async.makeSpinningCallback();
-    queue.dequeue(cb);
-    do_check_true(cb.wait());
-  }
-
-  do_check_eq(queue.length, num - rem);
-
-  // Check that the items left are the right ones.
-  do_check_eq(JSON.stringify(queue._queue), JSON.stringify(check));
-
-  // Another instance of the same queue should return correct data.
-  let queue2 = new AitcQueue("test", function(done) {
-    do_check_true(done);
-    do_check_eq(queue2.length, queue.length);
-    do_check_eq(JSON.stringify(queue._queue), JSON.stringify(queue2._queue));
-    run_next_test();
-  });
-});
-
-add_test(function test_queue_writelock() {
-  // Queue should not enqueue or dequeue if lock is enabled.
-  queue._writeLock = true;
-  let len = queue.length;
-
-  queue.enqueue("writeLock test", function(err, done) {
-    do_check_eq(err.toString(), "Error: _putFile already in progress");
-    do_check_eq(queue.length, len);
-
-    queue.dequeue(function(err, done) {
-      do_check_eq(err.toString(), "Error: _putFile already in progress");
-      do_check_eq(queue.length, len);
-      run_next_test();
-    });
-  });
-});
deleted file mode 100644
--- a/services/aitc/tests/unit/test_storage_registry.js
+++ /dev/null
@@ -1,151 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://gre/modules/Webapps.jsm");
-Cu.import("resource://services-common/async.js");
-Cu.import("resource://services-aitc/storage.js");
-
-const START_PORT = 8080;
-const SERVER = "http://localhost";
-
-let fakeApp1 = {
-  origin: SERVER + ":" + START_PORT,
-  receipts: [],
-  manifestURL: "/manifest.webapp",
-  installOrigin: "http://localhost",
-  installedAt: Date.now(),
-  modifiedAt: Date.now()
-};
-
-// Valid manifest for app1
-let manifest1 = {
-  name: "Appasaurus",
-  description: "Best fake app ever",
-  launch_path: "/",
-  fullscreen: true,
-  required_features: ["webgl"]
-};
-
-let fakeApp2 = {
-  origin: SERVER + ":" + (START_PORT + 1),
-  receipts: ["fake.jwt.token"],
-  manifestURL: "/manifest.webapp",
-  installOrigin: "http://localhost",
-  installedAt: Date.now(),
-  modifiedAt: Date.now()
-};
-
-// Invalid manifest for app2
-let manifest2_bad = {
-  not: "a manifest",
-  fullscreen: true
-};
-
-// Valid manifest for app2
-let manifest2_good = {
-  name: "Supercalifragilisticexpialidocious",
-  description: "Did we blow your mind yet?",
-  launch_path: "/"
-};
-
-let fakeApp3 = {
-  origin: SERVER + ":" + (START_PORT + 3), // 8082 is for the good manifest2
-  receipts: [],
-  manifestURL: "/manifest.webapp",
-  installOrigin: "http://localhost",
-  installedAt: Date.now(),
-  modifiedAt: Date.now()
-};
-
-let manifest3 = {
-  name: "Taumatawhakatangihangakoauauotamateapokaiwhenuakitanatahu",
-  description: "Find your way around this beautiful hill",
-  launch_path: "/"
-};
-
-function create_servers() {
-  // Setup servers to server manifests at each port
-  let manifests = [manifest1, manifest2_bad, manifest2_good, manifest3];
-  for (let i = 0; i < manifests.length; i++) {
-    let response = JSON.stringify(manifests[i]);
-    httpd_setup({"/manifest.webapp": function(req, res) {
-      res.setStatusLine(req.httpVersion, 200, "OK");
-      res.setHeader("Content-Type", "application/x-web-app-manifest+json");
-      res.bodyOutputStream.write(response, response.length);
-    }}, START_PORT + i);
-  }
-}
-
-function run_test() {
-  initTestLogging();
-  create_servers();
-  run_next_test();
-}
-
-add_test(function test_storage_install() {
-  let apps = [fakeApp1, fakeApp2];
-  AitcStorage.processApps(apps, function() {
-    // Verify that app1 got added to registry
-    let id = DOMApplicationRegistry._appId(fakeApp1.origin);
-    do_check_true(DOMApplicationRegistry.itemExists(id));
-
-    // app2 should be missing because of bad manifest
-    do_check_null(DOMApplicationRegistry._appId(fakeApp2.origin));
-
-    // Now associate fakeApp2 with a good manifest and process again
-    fakeApp2.origin = SERVER + ":8082";
-    AitcStorage.processApps([fakeApp1, fakeApp2], function() {
-      // Both apps must be installed
-      let id1 = DOMApplicationRegistry._appId(fakeApp1.origin);
-      let id2 = DOMApplicationRegistry._appId(fakeApp2.origin);
-      do_check_true(DOMApplicationRegistry.itemExists(id1));
-      do_check_true(DOMApplicationRegistry.itemExists(id2));
-      run_next_test();
-    });
-  });
-});
-
-add_test(function test_storage_uninstall() {
-  _("Ensure explicit uninstalls through hidden are honored.");
-  do_check_neq(DOMApplicationRegistry._appId(fakeApp1.origin), null);
-
-  // Set app1 as hidden.
-  fakeApp1.hidden = true;
-  AitcStorage.processApps([fakeApp1], function() {
-    // It should be missing.
-    do_check_null(DOMApplicationRegistry._appId(fakeApp1.origin));
-    run_next_test();
-  });
-});
-
-add_test(function test_storage_uninstall_missing() {
-  _("Ensure a local app with no remote record is uninstalled.");
-
-  // If the remote server has data, any local apps not on the remote server
-  // should be deleted.
-  let cb = Async.makeSpinningCallback();
-  AitcStorage.processApps([fakeApp2], cb);
-  cb.wait();
-  do_check_neq(DOMApplicationRegistry._appId(fakeApp2.origin), null);
-
-  AitcStorage.processApps([fakeApp3], function() {
-    let id3 = DOMApplicationRegistry._appId(fakeApp3.origin);
-    do_check_true(DOMApplicationRegistry.itemExists(id3));
-    do_check_null(DOMApplicationRegistry._appId(fakeApp2.origin));
-    run_next_test();
-  });
-});
-
-add_test(function test_uninstall_noop() {
-  _("Ensure that an empty set of remote records does nothing.");
-
-  let id = DOMApplicationRegistry._appId(fakeApp3.origin);
-  do_check_neq(id, null);
-  do_check_true(DOMApplicationRegistry.itemExists(id));
-
-  AitcStorage.processApps([], function onComplete() {
-    do_check_true(DOMApplicationRegistry.itemExists(id));
-
-    run_next_test();
-  });
-});
deleted file mode 100644
--- a/services/aitc/tests/unit/xpcshell.ini
+++ /dev/null
@@ -1,11 +0,0 @@
-[DEFAULT]
-head = ../../../common/tests/unit/head_global.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
-tail =
-
-[test_load_modules.js]
-[test_aitc_client.js]
-# Bug 752243: Profile cleanup frequently fails
-skip-if = os == "mac" || os == "linux"
-[test_aitc_manager.js]
-[test_storage_queue.js]
-[test_storage_registry.js]
\ No newline at end of file
--- a/services/common/Makefile.in
+++ b/services/common/Makefile.in
@@ -21,17 +21,16 @@ modules := \
 pp_modules := \
   async.js \
   bagheeraclient.js \
   observers.js \
   rest.js \
   $(NULL)
 
 testing_modules := \
-  aitcserver.js \
   bagheeraserver.js \
   logging.js \
   storageserver.js \
   utils.js \
   $(NULL)
 
 EXTRA_COMPONENTS := \
   servicesComponents.manifest \
@@ -60,15 +59,11 @@ include $(topsrcdir)/config/rules.mk
 # build system, this can go away.
 
 server_port := 8080
 
 storage-server:
 	$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
 	  $(MOZ_BUILD_ROOT) run_storage_server.js --port $(server_port)
 
-aitc-server:
-	$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
-		$(MOZ_BUILD_ROOT) run_aitc_server.js --port $(server_port)
-
 bagheera-server:
 	$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
 		$(MOZ_BUILD_ROOT) run_bagheera_server.js --port $(server_port)
--- a/services/common/bagheeraclient.js
+++ b/services/common/bagheeraclient.js
@@ -15,59 +15,75 @@
 
 this.EXPORTED_SYMBOLS = [
   "BagheeraClient",
   "BagheeraClientRequestResult",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
-Cu.import("resource://services-common/rest.js");
 #endif
 
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-common/utils.js");
 
 
 /**
  * Represents the result of a Bagheera request.
  */
 this.BagheeraClientRequestResult = function BagheeraClientRequestResult() {
   this.transportSuccess = false;
   this.serverSuccess = false;
   this.request = null;
+};
+
+Object.freeze(BagheeraClientRequestResult.prototype);
+
+
+/**
+ * Wrapper around RESTRequest so logging is sane.
+ */
+function BagheeraRequest(uri) {
+  RESTRequest.call(this, uri);
+
+  this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
+  this._log.level = Log4Moz.Level.Debug;
 }
 
-Object.freeze(BagheeraClientRequestResult.prototype);
+BagheeraRequest.prototype = Object.freeze({
+  __proto__: RESTRequest.prototype,
+});
+
 
 /**
  * Create a new Bagheera client instance.
  *
  * Each client is associated with a specific Bagheera HTTP URI endpoint.
  *
  * @param baseURI
  *        (string) The base URI of the Bagheera HTTP endpoint.
  */
 this.BagheeraClient = function BagheeraClient(baseURI) {
   if (!baseURI) {
     throw new Error("baseURI argument must be defined.");
   }
 
   this._log = Log4Moz.repository.getLogger("Services.BagheeraClient");
-  this._log.level = Log4Moz.Level["Debug"];
+  this._log.level = Log4Moz.Level.Debug;
 
   this.baseURI = baseURI;
 
   if (!baseURI.endsWith("/")) {
     this.baseURI += "/";
   }
-}
+};
 
-BagheeraClient.prototype = {
+BagheeraClient.prototype = Object.freeze({
   /**
    * Channel load flags for all requests.
    *
    * Caching is not applicable, so we bypass and disable it. We also
    * ignore any cookies that may be present for the domain because
    * Bagheera does not utilize cookies and the release of cookies may
    * inadvertantly constitute unncessary information disclosure.
    */
@@ -123,17 +139,17 @@ BagheeraClient.prototype = {
     }
 
     if (typeof(data) != "string") {
       throw new Error("Unknown type for payload: " + typeof(data));
     }
 
     this._log.info("Uploading data to " + uri);
 
-    let request = new RESTRequest(uri);
+    let request = new BagheeraRequest(uri);
     request.loadFlags = this._loadFlags;
     request.timeout = this.DEFAULT_TIMEOUT_MSEC;
 
     if (deleteOldID) {
       request.setHeader("X-Obsolete-Document", deleteOldID);
     }
 
     let deferred = Promise.defer();
@@ -162,17 +178,17 @@ BagheeraClient.prototype = {
    * @param id
    *        (string) ID of document to delete.
    *
    * @return Promise<BagheeraClientRequestResult>
    */
   deleteDocument: function deleteDocument(namespace, id) {
     let uri = this._submitURI(namespace, id);
 
-    let request = new RESTRequest(uri);
+    let request = new BagheeraRequest(uri);
     request.loadFlags = this._loadFlags;
     request.timeout = this.DEFAULT_TIMEOUT_MSEC;
 
     let result = new BagheeraClientRequestResult();
     result.namespace = namespace;
     result.id = id;
     let deferred = Promise.defer();
 
@@ -220,11 +236,10 @@ BagheeraClient.prototype = {
         result.serverSuccess = false;
 
         this._log.info("Received unexpected status code: " + response.status);
         this._log.debug("Response body: " + response.body);
     }
 
     deferred.resolve(result);
   },
-};
+});
 
-Object.freeze(BagheeraClient.prototype);
deleted file mode 100644
--- a/services/common/modules-testing/aitcserver.js
+++ /dev/null
@@ -1,555 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-"use strict";
-
-const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-
-this.EXPORTED_SYMBOLS = [
-  "AITCServer10User",
-  "AITCServer10Server",
-];
-
-Cu.import("resource://testing-common/httpd.js");
-Cu.import("resource://services-crypto/utils.js");
-Cu.import("resource://services-common/log4moz.js");
-Cu.import("resource://services-common/utils.js");
-
-/**
- * Represents an individual user on an AITC 1.0 server.
- *
- * This type provides convenience APIs for interacting with an individual
- * user's data.
- */
-this.AITCServer10User = function AITCServer10User() {
-  this._log = Log4Moz.repository.getLogger("Services.Common.AITCServer");
-  this.apps = {};
-}
-AITCServer10User.prototype = {
-  appRecordProperties: {
-    origin:        true,
-    manifestPath:  true,
-    installOrigin: true,
-    installedAt:   true,
-    modifiedAt:    true,
-    receipts:      true,
-    name:          true,
-    hidden:        true,
-  },
-
-  requiredAppProperties: [
-    "origin",
-    "manifestPath",
-    "installOrigin",
-    "installedAt",
-    "modifiedAt",
-    "name",
-    "receipts",
-  ],
-
-  /**
-   * Obtain the apps for this user.
-   *
-   * This is a generator of objects representing the apps. Returns the original
-   * apps object normally or an abbreviated version if `minimal` is truthy.
-   */
-  getApps: function getApps(minimal) {
-    let result;
-
-    for (let id in this.apps) {
-      let app = this.apps[id];
-
-      if (!minimal) {
-        yield app;
-        continue;
-      }
-
-      yield {origin: app.origin, modifiedAt: app.modifiedAt};
-    }
-  },
-
-  getAppByID: function getAppByID(id) {
-    return this.apps[id];
-  },
-
-  /**
-   * Adds an app to this user.
-   *
-   * The app record should be an object (likely from decoded JSON).
-   */
-  addApp: function addApp(app) {
-    for (let k in app) {
-      if (!(k in this.appRecordProperties)) {
-        throw new Error("Unexpected property in app record: " + k);
-      }
-    }
-
-    for each (let k in this.requiredAppProperties) {
-      if (!(k in app)) {
-        throw new Error("Required property not in app record: " + k);
-      }
-    }
-
-    this.apps[this.originToID(app.origin)] = app;
-  },
-
-  /**
-   * Returns whether a user has an app with the specified ID.
-   */
-  hasAppID: function hasAppID(id) {
-    return id in this.apps;
-  },
-
-  /**
-   * Delete an app having the specified ID.
-   */
-  deleteAppWithID: function deleteAppWithID(id) {
-    delete this.apps[id];
-  },
-
-  /**
-   * Convert an origin string to an ID.
-   */
-  originToID: function originToID(origin) {
-    let hash = CryptoUtils.UTF8AndSHA1(origin);
-    return CommonUtils.encodeBase64URL(hash, false);
-  },
-};
-
-/**
- * A fully-functional AITC 1.0 server implementation.
- *
- * Each server instance is capable of serving requests for multiple users.
- * By default, users do not exist and requests to URIs for a specific user
- * will result in 404s. To register a new user with an empty account, call
- * createUser(). If you wish for HTTP requests for non-existing users to
- * work, set autoCreateUsers to true and am empty user will be
- * provisioned at request time.
- */
-this.AITCServer10Server = function AITCServer10Server() {
-  this._log = Log4Moz.repository.getLogger("Services.Common.AITCServer");
-
-  this.server = new HttpServer();
-  this.port = null;
-  this.users = {};
-  this.autoCreateUsers = false;
-  this.mockStatus = {
-    code: null,
-    method: null
-  };
-  this.onRequest = null;
-
-  this._appsAppHandlers = {
-    GET:    this._appsAppGetHandler,
-    PUT:    this._appsAppPutHandler,
-    DELETE: this._appsAppDeleteHandler,
-  };
-}
-AITCServer10Server.prototype = {
-  ID_REGEX: /^[a-zA-Z0-9_-]{27}$/,
-  VERSION_PATH: "/1.0/",
-
-  /**
-   * Obtain the base URL the server can be accessed at as a string.
-   */
-  get url() {
-    // Is this available on the nsHttpServer instance?
-    return "http://localhost:" + this.port + this.VERSION_PATH;
-  },
-
-  /**
-   * Start the server on a specified port.
-   */
-  start: function start(port) {
-    if (!port) {
-      throw new Error("port argument must be specified.");
-    }
-
-    this.port = port;
-
-    this.server.registerPrefixHandler(this.VERSION_PATH,
-                                      this._generalHandler.bind(this));
-    this.server.start(port);
-  },
-
-  /**
-   * Stop the server.
-   *
-   * Calls the specified callback when the server is stopped.
-   */
-  stop: function stop(cb) {
-    let handler = {onStopped: cb};
-
-    this.server.stop(handler);
-  },
-
-  createUser: function createUser(username) {
-    if (username in this.users) {
-      throw new Error("User already exists: " + username);
-    }
-
-    this._log.info("Registering user: " + username);
-
-    this.users[username] = new AITCServer10User();
-    this.server.registerPrefixHandler(this.VERSION_PATH + username + "/",
-                                      this._userHandler.bind(this, username));
-
-    return this.users[username];
-  },
-
-  /**
-   * Returns information for an individual user.
-   *
-   * The returned object contains functions to access and manipulate an
-   * individual user.
-   */
-  getUser: function getUser(username) {
-    if (!(username in this.users)) {
-      throw new Error("user is not present in server: " + username);
-    }
-
-    return this.users[username];
-  },
-  
-  /**
-   * Returns a specific status code for testing.
-   */
-  _respondWithMockStatus: function _respondWithMockStatus(request, response) {
-    response.setStatusLine(request.httpVersion, this.mockStatus.code,
-      this.mockStatus.method);
-    this._onRequest();
-  },
-
-  _onRequest: function _onRequest() {
-    if (typeof this.onRequest === 'function') {
-      this.onRequest();
-    }
-  },
-
-  /**
-   * HTTP handler for requests to /1.0/ which don't have a specific user
-   * registered.
-   */
-  _generalHandler: function _generalHandler(request, response) {
-    if (this.mockStatus.code && this.mockStatus.method) {
-      this._respondWithMockStatus(request, response);
-      return;
-    }
-    this._onRequest();
-    let path = request.path;
-    this._log.info("Request: " + request.method + " " + path);
-
-    if (path.indexOf(this.VERSION_PATH) != 0) {
-      throw new Error("generalHandler invoked improperly.");
-    }
-
-    let rest = request.path.substr(this.VERSION_PATH.length);
-    if (!rest.length) {
-      throw HTTP_404;
-    }
-
-    if (!this.autoCreateUsers) {
-      throw HTTP_404;
-    }
-
-    let username;
-    let index = rest.indexOf("/");
-    if (index == -1) {
-      username = rest;
-    } else {
-      username = rest.substr(0, index);
-    }
-
-    this.createUser(username);
-    this._userHandler(username, request, response);
-  },
-
-  /**
-   * HTTP handler for requests for a specific user.
-   *
-   * This handles request routing to the appropriate handler.
-   */
-  _userHandler: function _userHandler(username, request, response) {
-    if (this.mockStatus.code && this.mockStatus.method) {
-      this._respondWithMockStatus(request, response);
-      return;
-    }
-    this._onRequest();
-    this._log.info("Request: " + request.method + " " + request.path);
-    let path = request.path;
-    let prefix = this.VERSION_PATH + username + "/";
-
-    if (path.indexOf(prefix) != 0) {
-      throw new Error("userHandler invoked improperly.");
-    }
-
-    let user = this.users[username];
-    if (!user) {
-      throw new Error("User handler should not have been invoked for an " +
-                      "unknown user!");
-    }
-
-    let requestTime = Date.now();
-    response.dispatchTime = requestTime;
-    response.setHeader("X-Timestamp", "" + requestTime);
-
-    let handler;
-    let remaining = path.substr(prefix.length);
-
-    if (remaining == "apps" || remaining == "apps/") {
-      this._log.info("Dispatching to apps index handler.");
-      handler = this._appsIndexHandler.bind(this, user, request, response);
-    } else if (!remaining.indexOf("apps/")) {
-      let id = remaining.substr("apps/".length);
-
-      this._log.info("Dispatching to app handler.");
-      handler = this._appsAppHandler.bind(this, user, id, request, response);
-    } else if (remaining == "devices" || !remaining.indexOf("devices/")) {
-      this._log.info("Dispatching to devices handler.");
-      handler = this._devicesHandler.bind(this, user,
-                                          remaining.substr("devices".length),
-                                          request, response);
-    } else {
-      throw HTTP_404;
-    }
-
-    try {
-      handler();
-    } catch (ex) {
-      if (ex instanceof HttpError) {
-        response.setStatusLine(request.httpVersion, ex.code, ex.description);
-        return;
-      }
-
-      this._log.warn("Exception when processing request: " +
-                     CommonUtils.exceptionStr(ex));
-      throw ex;
-    }
-  },
-
-  _appsIndexHandler: function _appsIndexHandler(user, request, response) {
-    if (request.method != "GET") {
-      response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
-      response.setHeader("Accept", "GET");
-
-      return;
-    }
-
-    let options = this._getQueryStringParams(request);
-    for (let key in options) {
-      let value = options[key];
-
-      switch (key) {
-        case "after":
-          let time = parseInt(value, 10);
-          if (isNaN(time)) {
-            throw HTTP_400;
-          }
-
-          options.after = time;
-          break;
-
-        case "full":
-          // Value is irrelevant.
-          break;
-
-        default:
-          this._log.info("Unknown query string parameter: " + key);
-          throw HTTP_400;
-      }
-    }
-
-    let apps = [];
-    let newest = 0;
-    for each (let app in user.getApps(!("full" in options))) {
-      if (app.modifiedAt > newest) {
-        newest = app.modifiedAt;
-      }
-
-      if ("after" in options && app.modifiedAt <= options.after) {
-        continue;
-      }
-
-      apps.push(app);
-    }
-
-    if (request.hasHeader("X-If-Modified-Since")) {
-      let modified = parseInt(request.getHeader("X-If-Modified-Since"), 10);
-      if (modified >= newest) {
-        response.setStatusLine(request.httpVersion, 304, "Not Modified");
-        return;
-      }
-    }
-
-    let body = JSON.stringify({apps: apps});
-    response.setStatusLine(request.httpVersion, 200, "OK");
-    response.setHeader("X-Last-Modified", "" + newest);
-    response.setHeader("Content-Type", "application/json");
-    response.bodyOutputStream.write(body, body.length);
-  },
-
-  _appsAppHandler: function _appAppHandler(user, id, request, response) {
-    if (!(request.method in this._appsAppHandlers)) {
-      response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
-      response.setHeader("Accept", Object.keys(this._appsAppHandlers).join(","));
-
-      return;
-    }
-
-    let handler = this._appsAppHandlers[request.method];
-    return handler.call(this, user, id, request, response);
-  },
-
-  _appsAppGetHandler: function _appsAppGetHandler(user, id, request, response) {
-    if (!user.hasAppID(id)) {
-      throw HTTP_404;
-    }
-
-    let app = user.getAppByID(id);
-
-    if (request.hasHeader("X-If-Modified-Since")) {
-      let modified = parseInt(request.getHeader("X-If-Modified-Since"), 10);
-
-      this._log.debug("Client time: " + modified + "; Server time: " +
-                      app.modifiedAt);
-
-      if (modified >= app.modifiedAt) {
-        response.setStatusLine(request.httpVersion, 304, "Not Modified");
-        return;
-      }
-    }
-
-    let body = JSON.stringify(app);
-    response.setStatusLine(request.httpVersion, 200, "OK");
-    response.setHeader("X-Last-Modified", "" + response.dispatchTime);
-    response.setHeader("Content-Type", "application/json");
-    response.bodyOutputStream.write(body, body.length);
-  },
-
-  _appsAppPutHandler: function _appsAppPutHandler(user, id, request, response) {
-    if (!request.hasHeader("Content-Type")) {
-      this._log.info("Request does not have Content-Type header.");
-      throw HTTP_400;
-    }
-
-    let ct = request.getHeader("Content-Type");
-    if (ct != "application/json" && ct.indexOf("application/json;") !== 0) {
-      this._log.info("Unknown media type: " + ct);
-      // TODO proper response headers.
-      throw HTTP_415;
-    }
-
-    let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
-    this._log.debug("Request body: " + requestBody);
-    if (requestBody.length > 8192) {
-      this._log.info("Request body too long: " + requestBody.length);
-      throw HTTP_413;
-    }
-
-    let hadApp = user.hasAppID(id);
-
-    let app;
-    try {
-      app = JSON.parse(requestBody);
-    } catch (e) {
-      this._log.info("JSON parse error.");
-      throw HTTP_400;
-    }
-
-    // URL and record mismatch.
-    if (user.originToID(app.origin) != id) {
-      this._log.warn("URL ID and origin mismatch. URL: " + id + "; Record: " +
-                     user.originToID(app.origin));
-      throw HTTP_403;
-    }
-
-    if (request.hasHeader("X-If-Unmodified-Since") && hadApp) {
-      let modified = parseInt(request.getHeader("X-If-Unmodified-Since"), 10);
-      let existing = user.getAppByID(id);
-
-      if (existing.modifiedAt > modified) {
-        this._log.info("Server modified after client.");
-        throw HTTP_412;
-      }
-    }
-
-    try {
-      app.modifiedAt = response.dispatchTime;
-
-      if (hadApp) {
-        app.installedAt = user.getAppByID(id).installedAt;
-      } else {
-        app.installedAt = response.dispatchTime;
-      }
-
-      user.addApp(app);
-    } catch (e) {
-      this._log.info("Error adding app: " + CommonUtils.exceptionStr(e));
-      throw HTTP_400;
-    }
-
-    let code = 201;
-    let status = "Created";
-
-    if (hadApp) {
-      code = 204;
-      status = "No Content";
-    }
-
-    response.setHeader("X-Last-Modified", "" + response.dispatchTime);
-    response.setStatusLine(request.httpVersion, code, status);
-  },
-
-  _appsAppDeleteHandler: function _appsAppDeleteHandler(user, id, request,
-                                                        response) {
-    if (!user.hasAppID(id)) {
-      throw HTTP_404;
-    }
-
-    let existing = user.getAppByID(id);
-    if (request.hasHeader("X-If-Unmodified-Since")) {
-      let modified = parseInt(request.getHeader("X-If-Unmodified-Since"), 10);
-
-      if (existing.modifiedAt > modified) {
-        throw HTTP_412;
-      }
-    }
-
-    user.deleteAppWithID(id);
-
-    response.setHeader("X-Last-Modified", "" + response.dispatchTime);
-    response.setStatusLine(request.httpVersion, 204, "No Content");
-  },
-
-  _devicesHandler: function _devicesHandler(user, path, request, response) {
-    // TODO need to support full API.
-    // For now, we just assume it is a request for /.
-    response.setHeader("Content-Type", "application/json");
-    let body = JSON.stringify({devices: []});
-
-    response.setStatusLine(request.httpVersion, 200, "OK");
-    response.bodyOutputStream.write(body, body.length);
-  },
-
-  // Surely this exists elsewhere in the Mozilla source tree...
-  _getQueryStringParams: function _getQueryStringParams(request) {
-    let params = {};
-    for each (let chunk in request.queryString.split("&")) {
-      if (!chunk) {
-        continue;
-      }
-
-      let parts = chunk.split("=");
-      // TODO URL decode key and value.
-      if (parts.length == 1) {
-        params[parts[0]] = "";
-      } else {
-        params[parts[0]] = parts[1];
-      }
-    }
-
-    return params;
-  },
-};
-
deleted file mode 100644
--- a/services/common/tests/run_aitc_server.js
+++ /dev/null
@@ -1,26 +0,0 @@
-/* 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/. */
-
-/**
- * This file runs a standalone AITC server.
- *
- * It is meant to be executed with an xpcshell.
- *
- * The Makefile in this directory contains a target to run it:
- *
- *   $ make aitc-server
- */
-
-Cu.import("resource://testing-common/services-common/aitcserver.js");
-
-initTestLogging();
-
-let server = new AITCServer10Server();
-server.autoCreateUsers = true;
-server.start(SERVER_PORT);
-
-_("AITC server started on port " + SERVER_PORT);
-
-// Launch the thread manager.
-_do_main();
--- a/services/common/tests/unit/head_global.js
+++ b/services/common/tests/unit/head_global.js
@@ -39,16 +39,16 @@ registrar.registerFactory(Components.ID(
                           "XULAppInfo", "@mozilla.org/xre/app-info;1",
                           XULAppInfoFactory);
 
 function addResourceAlias() {
   Cu.import("resource://gre/modules/Services.jsm");
   const handler = Services.io.getProtocolHandler("resource")
                   .QueryInterface(Ci.nsIResProtocolHandler);
 
-  let modules = ["aitc", "common", "crypto"];
+  let modules = ["common", "crypto"];
   for each (let module in modules) {
     let uri = Services.io.newURI("resource://gre/modules/services-" + module + "/",
                                  null, null);
     handler.setSubstitution("services-" + module, uri);
   }
 }
 addResourceAlias();
deleted file mode 100644
--- a/services/common/tests/unit/test_aitc_server.js
+++ /dev/null
@@ -1,190 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-Cu.import("resource://services-common/rest.js");
-Cu.import("resource://services-common/utils.js");
-
-Cu.import("resource://testing-common/services-common/aitcserver.js");
-
-function run_test() {
-  initTestLogging("Trace");
-  run_next_test();
-}
-
-function get_aitc_server() {
-  let server = new AITCServer10Server();
-  server.start(get_server_port());
-
-  return server;
-}
-
-function get_server_with_user(username) {
-  let server = get_aitc_server();
-  server.createUser(username);
-
-  return server;
-}
-
-add_test(function test_origin_conversion() {
-  let mapping = {
-    "www.mozilla.org": "xSMmiFEpg4b4TRtzJZd6Mvy4hGc",
-    "foo":             "C-7Hteo_D9vJXQ3UfzxbwnXaijM",
-  };
-
-  for (let k in mapping) {
-    do_check_eq(AITCServer10User.prototype.originToID(k), mapping[k]);
-  }
-
-  run_next_test();
-});
-
-add_test(function test_empty_user() {
-  _("Ensure user instances can be created.");
-
-  let user = new AITCServer10User();
-
-  let apps = user.getApps();
-  do_check_eq([app for (app in apps)].length, 0);
-  do_check_false(user.hasAppID("foobar"));
-
-  run_next_test();
-});
-
-add_test(function test_user_add_app() {
-  _("Ensure apps can be added to users.");
-
-  let user = new AITCServer10User();
-  let threw = false;
-  try {
-    user.addApp({});
-  } catch (ex) {
-    threw = true;
-  } finally {
-    do_check_true(threw);
-    threw = false;
-  }
-
-  run_next_test();
-});
-
-add_test(function test_server_run() {
-  _("Ensure server can be started properly.");
-
-  let server = new AITCServer10Server();
-  server.start(get_server_port());
-
-  server.stop(run_next_test);
-});
-
-add_test(function test_create_user() {
-  _("Ensure users can be created properly.");
-
-  let server = get_aitc_server();
-
-  let u1 = server.createUser("123");
-  do_check_true(u1 instanceof AITCServer10User);
-
-  let u2 = server.getUser("123");
-  do_check_eq(u1, u2);
-
-  server.stop(run_next_test);
-});
-
-add_test(function test_empty_server_404() {
-  _("Ensure empty server returns 404.");
-
-  let server = get_aitc_server();
-  let request = new RESTRequest(server.url + "123/");
-  request.get(function onComplete(error) {
-    do_check_eq(this.response.status, 404);
-
-    let request = new RESTRequest(server.url + "123/apps/");
-    request.get(function onComplete(error) {
-      do_check_eq(this.response.status, 404);
-
-      server.stop(run_next_test);
-    });
-  });
-});
-
-add_test(function test_empty_user_apps() {
-  _("Ensure apps request for empty user has appropriate content.");
-
-  const username = "123";
-
-  let server = get_server_with_user(username);
-  let request = new RESTRequest(server.url + username + "/apps/");
-  _("Performing request...");
-  request.get(function onComplete(error) {
-    _("Got response");
-    do_check_eq(error, null);
-
-    do_check_eq(200, this.response.status);
-    let headers = this.response.headers;
-    do_check_true("content-type" in headers);
-    do_check_eq(headers["content-type"], "application/json");
-    do_check_true("x-timestamp" in headers);
-
-    let body = this.response.body;
-    let parsed = JSON.parse(body);
-    do_check_attribute_count(parsed, 1);
-    do_check_true("apps" in parsed);
-    do_check_true(Array.isArray(parsed.apps));
-    do_check_eq(parsed.apps.length, 0);
-
-    server.stop(run_next_test);
-  });
-});
-
-add_test(function test_invalid_request_method() {
-  _("Ensure HTTP 405 works as expected.");
-
-  const username = "12345";
-
-  let server = get_server_with_user(username);
-  let request = new RESTRequest(server.url + username + "/apps/foobar");
-  request.dispatch("SILLY", null, function onComplete(error) {
-    do_check_eq(error, null);
-    do_check_eq(this.response.status, 405);
-
-    let headers = this.response.headers;
-    do_check_true("accept" in headers);
-
-    let allowed = new Set();
-
-    for (let method of headers["accept"].split(",")) {
-      allowed.add(method);
-    }
-
-    do_check_eq(allowed.size, 3);
-    for (let method of ["GET", "PUT", "DELETE"]) {
-      do_check_true(allowed.has(method));
-    }
-
-    server.stop(run_next_test);
-  });
-});
-add_test(function test_respond_with_mock_status() {
-  let username = "123"
-  let server = get_server_with_user(username);
-  server.mockStatus = {
-    code: 405,
-    method: "Method Not Allowed"
-  };
-  let request = new RESTRequest(server.url);
-  request.dispatch("GET", null, function onComplete(error){
-    do_check_eq(this.response.status, 405);
-    
-    server.mockStatus = {
-      code: 399,
-      method: "Self Destruct"
-    };
-    let request2 = new RESTRequest(server.url);
-    request2.dispatch("GET", null, function onComplete(error){
-      do_check_eq(this.response.status, 399);
-      server.stop(run_next_test);
-    });
-  });
-});
\ No newline at end of file
--- a/services/common/tests/unit/test_load_modules.js
+++ b/services/common/tests/unit/test_load_modules.js
@@ -9,17 +9,16 @@ const modules = [
   "rest.js",
   "storageservice.js",
   "stringbundle.js",
   "tokenserverclient.js",
   "utils.js",
 ];
 
 const test_modules = [
-  "aitcserver.js",
   "bagheeraserver.js",
   "logging.js",
   "storageserver.js",
 ];
 
 function run_test() {
   for each (let m in modules) {
     let resource = "resource://services-common/" + m;
--- a/services/common/tests/unit/test_utils_json.js
+++ b/services/common/tests/unit/test_utils_json.js
@@ -1,120 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/osfile.jsm")
 
 function run_test() {
   initTestLogging();
   run_next_test();
 }
 
-add_test(function test_roundtrip() {
-  _("Do a simple write of an array to json and read");
-  CommonUtils.jsonSave("foo", {}, ["v1", "v2"], ensureThrows(function(error) {
-    do_check_eq(error, null);
-
-    CommonUtils.jsonLoad("foo", {}, ensureThrows(function(val) {
-      let foo = val;
-      do_check_eq(typeof foo, "object");
-      do_check_eq(foo.length, 2);
-      do_check_eq(foo[0], "v1");
-      do_check_eq(foo[1], "v2");
-      run_next_test();
-    }));
-  }));
-});
-
-add_test(function test_string() {
-  _("Try saving simple strings");
-  CommonUtils.jsonSave("str", {}, "hi", ensureThrows(function(error) {
-    do_check_eq(error, null);
-
-    CommonUtils.jsonLoad("str", {}, ensureThrows(function(val) {
-      let str = val;
-      do_check_eq(typeof str, "string");
-      do_check_eq(str.length, 2);
-      do_check_eq(str[0], "h");
-      do_check_eq(str[1], "i");
-      run_next_test();
-    }));
-  }));
-});
-
-add_test(function test_number() {
-  _("Try saving a number");
-  CommonUtils.jsonSave("num", {}, 42, ensureThrows(function(error) {
-    do_check_eq(error, null);
-
-    CommonUtils.jsonLoad("num", {}, ensureThrows(function(val) {
-      let num = val;
-      do_check_eq(typeof num, "number");
-      do_check_eq(num, 42);
-      run_next_test();
-    }));
-  }));
-});
-
-add_test(function test_nonexistent_file() {
-  _("Try loading a non-existent file.");
-  CommonUtils.jsonLoad("non-existent", {}, ensureThrows(function(val) {
-    do_check_eq(val, undefined);
-    run_next_test();
-  }));
-});
-
-add_test(function test_save_logging() {
-  _("Verify that writes are logged.");
-  let trace;
-  CommonUtils.jsonSave("log", {_log: {trace: function(msg) { trace = msg; }}},
-                       "hi", ensureThrows(function () {
-    do_check_true(!!trace);
-    run_next_test();
-  }));
-});
-
-add_test(function test_load_logging() {
-  _("Verify that reads and read errors are logged.");
-
-  // Write a file with some invalid JSON
-  let filePath = "log.json";
-  let file = FileUtils.getFile("ProfD", filePath.split("/"), true);
-  let fos = Cc["@mozilla.org/network/file-output-stream;1"]
-              .createInstance(Ci.nsIFileOutputStream);
-  let flags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
-              | FileUtils.MODE_TRUNCATE;
-  fos.init(file, flags, FileUtils.PERMS_FILE, fos.DEFER_OPEN);
-  let stream = Cc["@mozilla.org/intl/converter-output-stream;1"]
-                 .createInstance(Ci.nsIConverterOutputStream);
-  stream.init(fos, "UTF-8", 4096, 0x0000);
-  stream.writeString("invalid json!");
-  stream.close();
-
-  let trace, debug;
-  let obj = {
-    _log: {
-      trace: function(msg) {
-        trace = msg;
-      },
-      debug: function(msg) {
-        debug = msg;
-      }
-    }
-  };
-  CommonUtils.jsonLoad("log", obj, ensureThrows(function(val) {
-    do_check_true(!val);
-    do_check_true(!!trace);
-    do_check_true(!!debug);
-    run_next_test();
-  }));
-});
-
 add_test(function test_writeJSON_readJSON() {
   _("Round-trip some JSON through the promise-based JSON writer.");
 
   let contents = {
     "a": 12345.67,
     "b": {
       "c": "héllö",
     },
--- a/services/common/tests/unit/xpcshell.ini
+++ b/services/common/tests/unit/xpcshell.ini
@@ -14,19 +14,16 @@ tail =
 [test_utils_ensureMillisecondsTimestamp.js]
 [test_utils_json.js]
 [test_utils_makeURI.js]
 [test_utils_namedTimer.js]
 [test_utils_stackTrace.js]
 [test_utils_utf8.js]
 [test_utils_uuid.js]
 
-[test_aitc_server.js]
-# Bug 752243: Profile cleanup frequently fails
-skip-if = os == "mac" || os == "linux"
 [test_async_chain.js]
 [test_async_querySpinningly.js]
 [test_bagheera_server.js]
 [test_bagheera_client.js]
 [test_log4moz.js]
 [test_observers.js]
 [test_preferences.js]
 [test_restrequest.js]
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -1,18 +1,16 @@
 /* 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/. */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 this.EXPORTED_SYMBOLS = ["CommonUtils"];
 
-Cu.import("resource://gre/modules/FileUtils.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm")
 Cu.import("resource://services-common/log4moz.js");
 
 this.CommonUtils = {
   exceptionStr: function exceptionStr(e) {
     let message = e.message ? e.message : e;
@@ -352,100 +350,16 @@ this.CommonUtils = {
    * @return a promise, as produced by OS.File.writeAtomic.
    */
   writeJSON: function(contents, path) {
     let encoder = new TextEncoder();
     let array = encoder.encode(JSON.stringify(contents));
     return OS.File.writeAtomic(path, array, {tmpPath: path + ".tmp"});
   },
 
-  /**
-   * Load a JSON file from disk in the profile directory.
-   *
-   * @param filePath
-   *        JSON file path load from profile. Loaded file will be
-   *        <profile>/<filePath>.json. i.e. Do not specify the ".json"
-   *        extension.
-   * @param that
-   *        Object to use for logging and "this" for callback.
-   * @param callback
-   *        Function to process json object as its first argument. If the file
-   *        could not be loaded, the first argument will be undefined.
-   */
-  jsonLoad: function jsonLoad(filePath, that, callback) {
-    let path = filePath + ".json";
-
-    if (that._log) {
-      that._log.trace("Loading json from disk: " + filePath);
-    }
-
-    let file = FileUtils.getFile("ProfD", path.split("/"), true);
-    if (!file.exists()) {
-      callback.call(that);
-      return;
-    }
-
-    let channel = NetUtil.newChannel(file);
-    channel.contentType = "application/json";
-
-    NetUtil.asyncFetch(channel, function (is, result) {
-      if (!Components.isSuccessCode(result)) {
-        callback.call(that);
-        return;
-      }
-      let string = NetUtil.readInputStreamToString(is, is.available());
-      is.close();
-      let json;
-      try {
-        json = JSON.parse(string);
-      } catch (ex) {
-        if (that._log) {
-          that._log.debug("Failed to load json: " +
-                          CommonUtils.exceptionStr(ex));
-        }
-      }
-      callback.call(that, json);
-    });
-  },
-
-  /**
-   * Save a json-able object to disk in the profile directory.
-   *
-   * @param filePath
-   *        JSON file path save to <filePath>.json
-   * @param that
-   *        Object to use for logging and "this" for callback
-   * @param obj
-   *        Function to provide json-able object to save. If this isn't a
-   *        function, it'll be used as the object to make a json string.
-   * @param callback
-   *        Function called when the write has been performed. Optional.
-   *        The first argument will be a Components.results error
-   *        constant on error or null if no error was encountered (and
-   *        the file saved successfully).
-   */
-  jsonSave: function jsonSave(filePath, that, obj, callback) {
-    let path = filePath + ".json";
-    if (that._log) {
-      that._log.trace("Saving json to disk: " + path);
-    }
-
-    let file = FileUtils.getFile("ProfD", path.split("/"), true);
-    let json = typeof obj == "function" ? obj.call(that) : obj;
-    let out = JSON.stringify(json);
-
-    let fos = FileUtils.openSafeFileOutputStream(file);
-    let is = this._utf8Converter.convertToInputStream(out);
-    NetUtil.asyncCopy(is, fos, function (result) {
-      if (typeof callback == "function") {
-        let error = (result == Cr.NS_OK) ? null : result;
-        callback.call(that, error);
-      }
-    });
-  },
 
   /**
    * Ensure that the specified value is defined in integer milliseconds since
    * UNIX epoch.
    *
    * This throws an error if the value is not an integer, is negative, or looks
    * like seconds, not milliseconds.
    *
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -12,16 +12,17 @@ Cu.import("resource://services-common/ut
 
 
 const ROOT_BRANCH = "datareporting.";
 const POLICY_BRANCH = ROOT_BRANCH + "policy.";
 const SESSIONS_BRANCH = ROOT_BRANCH + "sessions.";
 const HEALTHREPORT_BRANCH = ROOT_BRANCH + "healthreport.";
 const HEALTHREPORT_LOGGING_BRANCH = HEALTHREPORT_BRANCH + "logging.";
 const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
+const DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC = 60 * 1000;
 
 /**
  * The Firefox Health Report XPCOM service.
  *
  * External consumers will be interested in the "reporter" property of this
  * service. This property is a `HealthReporter` instance that powers the
  * service. The property may be null if the Health Report service is not
  * enabled.
@@ -39,17 +40,20 @@ const DEFAULT_LOAD_DELAY_MSEC = 10 * 100
  * }
  *
  * IMPLEMENTATION NOTES
  * ====================
  *
  * In order to not adversely impact application start time, the `HealthReporter`
  * instance is not initialized until a few seconds after "final-ui-startup."
  * The exact delay is configurable via preferences so it can be adjusted with
- * a hotfix extension if the default value is ever problematic.
+ * a hotfix extension if the default value is ever problematic. Because of the
+ * overhead with the initial creation of the database, the first run is delayed
+ * even more than subsequent runs. This does mean that the first moments of
+ * browser activity may be lost by FHR.
  *
  * Shutdown of the `HealthReporter` instance is handled completely within the
  * instance (it registers observers on initialization). See the notes on that
  * type for more.
  */
 this.DataReportingService = function () {
   this.wrappedJSObject = this;
 
@@ -123,36 +127,34 @@ DataReportingService.prototype = Object.
         this.policy = new DataReportingPolicy(policyPrefs, this._prefs, this);
 
         break;
 
       case "sessionstore-windows-restored":
         this._os.removeObserver(this, "sessionstore-windows-restored");
         this._os.addObserver(this, "quit-application", false);
 
-        // When the session recorder starts up above, first paint and session
-        // restore times likely aren't available. So, we wait until they are (here)
-        // and record them. In the case of session restore time, that appears
-        // to be set by an observer of this notification. So, we delay
-        // recording until the next tick of the event loop.
-        if (this.sessionRecorder) {
-          CommonUtils.nextTick(this.sessionRecorder.recordStartupFields,
-                               this.sessionRecorder);
-        }
-
         this.policy.startPolling();
 
         // Don't initialize Firefox Health Reporter collection and submission
         // service unless it is enabled.
         if (!this._prefs.get("service.enabled", true)) {
           return;
         }
 
-        let delayInterval = this._prefs.get("service.loadDelayMsec") ||
-                            DEFAULT_LOAD_DELAY_MSEC;
+        let haveFirstRun = this._prefs.get("service.firstRun", false);
+        let delayInterval;
+
+        if (haveFirstRun) {
+          delayInterval = this._prefs.get("service.loadDelayMsec") ||
+                          DEFAULT_LOAD_DELAY_MSEC;
+        } else {
+          delayInterval = this._prefs.get("service.loadDelayFirstRunMsec") ||
+                          DEFAULT_LOAD_DELAY_FIRST_RUN_MSEC;
+        }
 
         // Delay service loading a little more so things have an opportunity
         // to cool down first.
         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this.timer.initWithCallback({
           notify: function notify() {
             delete this.timer;
 
@@ -244,16 +246,22 @@ DataReportingService.prototype = Object.
         logger.addAppender(appender);
       }
     }
 
     // The reporter initializes in the background.
     this._healthReporter = new ns.HealthReporter(HEALTHREPORT_BRANCH,
                                                  this.policy,
                                                  this.sessionRecorder);
+
+    // Wait for initialization to finish so if a shutdown occurs before init
+    // has finished we don't adversely affect app startup on next run.
+    this._healthReporter.onInit().then(function onInit() {
+      this._prefs.set("service.firstRun", true);
+    }.bind(this));
   },
 });
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DataReportingService]);
 
 #define MERGED_COMPARTMENT
 
 #include ../common/observers.js
--- a/services/datareporting/Makefile.in
+++ b/services/datareporting/Makefile.in
@@ -22,8 +22,12 @@ EXTRA_COMPONENTS := \
   DataReporting.manifest \
   $(NULL)
 
 EXTRA_PP_COMPONENTS := \
   DataReportingService.js \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
+
+$(FINAL_TARGET)/components/DataReportingService.js: policy.jsm sessions.jsm ../common/observers.js
+
+
--- a/services/datareporting/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -301,17 +301,17 @@ DataReportingPolicy.prototype = Object.f
   SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
 
   /**
    * Time that must elapse with no user action for implicit acceptance.
    *
    * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
    * Privacy and/or Legal before modifying.
    */
-  IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 5 * 60 * 1000,
+  IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
 
   /**
    *  How often to poll to see if we need to do something.
    *
    * The interval needs to be short enough such that short-lived applications
    * have an opportunity to submit data. But, it also needs to be long enough
    * to not negatively impact performance.
    *
@@ -616,16 +616,18 @@ DataReportingPolicy.prototype = Object.f
 
   /**
    * Whether upload of Firefox Health Report data is enabled.
    */
   get healthReportUploadEnabled() {
     return !!this._healthReportPrefs.get("uploadEnabled", true);
   },
 
+  // External callers should update this via `recordHealthReportUploadEnabled`
+  // to ensure appropriate side-effects are performed.
   set healthReportUploadEnabled(value) {
     this._healthReportPrefs.set("uploadEnabled", !!value);
   },
 
   /**
    * Record user acceptance of data submission policy.
    *
    * Data submission will not be allowed to occur until this is called.
@@ -659,16 +661,49 @@ DataReportingPolicy.prototype = Object.f
   recordUserRejection: function recordUserRejection(reason="no-reason") {
     this._log.info("User rejected data submission policy: " + reason);
     this.dataSubmissionPolicyResponseDate = this.now();
     this.dataSubmissionPolicyResponseType = "rejected-" + reason;
     this.dataSubmissionPolicyAccepted = false;
   },
 
   /**
+   * Record the user's intent for whether FHR should upload data.
+   *
+   * This is the preferred way for the application to record a user's
+   * preference on whether Firefox Health Report should upload data to
+   * a server.
+   *
+   * If upload is disabled through this API, a request for remote data
+   * deletion is initiated automatically.
+   *
+   * If upload is being disabled and this operation is scheduled to
+   * occur immediately, a promise will be returned. This promise will be
+   * fulfilled when the deletion attempt finishes. If upload is being
+   * disabled and a promise is not returned, callers must poll
+   * `haveRemoteData` on the HealthReporter instance to see if remote
+   * data has been deleted.
+   *
+   * @param flag
+   *        (bool) Whether data submission is enabled or disabled.
+   * @param reason
+   *        (string) Why this value is being adjusted. For logging
+   *        purposes only.
+   */
+  recordHealthReportUploadEnabled: function (flag, reason="no-reason") {
+    this.healthReportUploadEnabled = flag;
+
+    if (flag) {
+      return null;
+    }
+
+    return this.deleteRemoteData(reason);
+  },
+
+  /**
    * Request that remote data be deleted.
    *
    * This will record an intent that previously uploaded data is to be deleted.
    * The policy will eventually issue a request to the listener for data
    * deletion. It will keep asking for deletion until the listener acknowledges
    * that data has been deleted.
    */
   deleteRemoteData: function deleteRemoteData(reason="no-reason") {
--- a/services/datareporting/sessions.jsm
+++ b/services/datareporting/sessions.jsm
@@ -17,17 +17,20 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 
 
 // We automatically prune sessions older than this.
 const MAX_SESSION_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days.
+const STARTUP_RETRY_INTERVAL_MS = 5000;
 
+// Wait up to 5 minutes for startup measurements before giving up.
+const MAX_STARTUP_TRIES = 300000 / STARTUP_RETRY_INTERVAL_MS;
 
 /**
  * Records information about browser sessions.
  *
  * This serves as an interface to both current session information as
  * well as a history of previous sessions.
  *
  * Typically only one instance of this will be installed in an
@@ -66,26 +69,31 @@ this.SessionRecorder = function (branch)
     throw new Error("branch argument must end with '.': " + branch);
   }
 
   this._log = Log4Moz.repository.getLogger("Services.DataReporting.SessionRecorder");
 
   this._prefs = new Preferences(branch);
   this._lastActivityWasInactive = false;
   this._activeTicks = 0;
+  this.fineTotalTime = 0;
   this._started = false;
+  this._timer = null;
+  this._startupFieldTries = 0;
 
   this._os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
 
 };
 
 SessionRecorder.prototype = Object.freeze({
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
 
+  STARTUP_RETRY_INTERVAL_MS: STARTUP_RETRY_INTERVAL_MS,
+
   get _currentIndex() {
     return this._prefs.get("currentIndex", 0);
   },
 
   set _currentIndex(value) {
     this._prefs.set("currentIndex", value);
   },
 
@@ -108,22 +116,30 @@ SessionRecorder.prototype = Object.freez
   get activeTicks() {
     return this._prefs.get("current.activeTicks", 0);
   },
 
   incrementActiveTicks: function () {
     this._prefs.set("current.activeTicks", ++this._activeTicks);
   },
 
+  /**
+   * Total time of this session in integer seconds.
+   *
+   * See also fineTotalTime for the time in milliseconds.
+   */
   get totalTime() {
     return this._prefs.get("current.totalTime", 0);
   },
 
   updateTotalTime: function () {
-    this._prefs.set("current.totalTime", Date.now() - this.startDate);
+    // We store millisecond precision internally to prevent drift from
+    // repeated rounding.
+    this.fineTotalTime = Date.now() - this.startDate;
+    this._prefs.set("current.totalTime", Math.floor(this.fineTotalTime / 1000));
   },
 
   get main() {
     return this._prefs.get("current.main", -1);
   },
 
   set _main(value) {
     if (!Number.isInteger(value)) {
@@ -199,23 +215,50 @@ SessionRecorder.prototype = Object.freez
 
   recordStartupFields: function () {
     let si = this._getStartupInfo();
 
     if (!si.process) {
       throw new Error("Startup info not available.");
     }
 
+    let missing = false;
+
     for (let field of ["main", "firstPaint", "sessionRestored"]) {
       if (!(field in si)) {
+        this._log.debug("Missing startup field: " + field);
+        missing = true;
         continue;
       }
 
       this["_" + field] = si[field].getTime() - si.process.getTime();
     }
+
+    if (!missing || this._startupFieldTries > MAX_STARTUP_TRIES) {
+      this._clearStartupTimer();
+      return;
+    }
+
+    // If we have missing fields, install a timer and keep waiting for
+    // data.
+    this._startupFieldTries++;
+
+    if (!this._timer) {
+      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._timer.initWithCallback({
+        notify: this.recordStartupFields.bind(this),
+      }, this.STARTUP_RETRY_INTERVAL_MS, this._timer.TYPE_REPEATING_SLACK);
+    }
+  },
+
+  _clearStartupTimer: function () {
+    if (this._timer) {
+      this._timer.cancel();
+      delete this._timer;
+    }
   },
 
   /**
    * Perform functionality on application startup.
    *
    * This is typically called in a "profile-do-change" handler.
    */
   onStartup: function () {
@@ -258,16 +301,17 @@ SessionRecorder.prototype = Object.freez
       this.incrementActiveTicks();
     }
   },
 
   onShutdown: function () {
     this._log.info("Recording clean session shutdown.");
     this._prefs.set("current.clean", true);
     this.updateTotalTime();
+    this._clearStartupTimer();
 
     this._os.removeObserver(this, "profile-before-change");
     this._os.removeObserver(this, "user-interaction-active");
     this._os.removeObserver(this, "user-interaction-inactive");
     this._os.removeObserver(this, "idle-daily");
   },
 
   _CURRENT_PREFS: [
--- a/services/datareporting/tests/xpcshell/test_policy.js
+++ b/services/datareporting/tests/xpcshell/test_policy.js
@@ -728,8 +728,35 @@ add_test(function test_polling_implicit_
     }
   });
 
   policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
   policy.nextDataSubmissionDate = new Date(Date.now());
   policy.startPolling();
 });
 
+add_test(function test_record_health_report_upload_enabled() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
+
+  // Preconditions.
+  do_check_false(policy.pendingDeleteRemoteData);
+  do_check_true(policy.healthReportUploadEnabled);
+  do_check_eq(listener.requestRemoteDeleteCount, 0);
+
+  // User intent to disable should immediately result in a pending
+  // delete request.
+  policy.recordHealthReportUploadEnabled(false, "testing 1 2 3");
+  do_check_false(policy.healthReportUploadEnabled);
+  do_check_true(policy.pendingDeleteRemoteData);
+  do_check_eq(listener.requestRemoteDeleteCount, 1);
+
+  // Fulfilling it should make it go away.
+  listener.lastRemoteDeleteRequest.onNoDataAvailable();
+  do_check_false(policy.pendingDeleteRemoteData);
+
+  // User intent to enable should get us back to default state.
+  policy.recordHealthReportUploadEnabled(true, "testing 1 2 3");
+  do_check_false(policy.pendingDeleteRemoteData);
+  do_check_true(policy.healthReportUploadEnabled);
+
+  run_next_test();
+});
+
--- a/services/datareporting/tests/xpcshell/test_session_recorder.js
+++ b/services/datareporting/tests/xpcshell/test_session_recorder.js
@@ -56,27 +56,111 @@ add_test(function test_basic() {
 add_task(function test_current_properties() {
   let now = new Date();
   let recorder = getRecorder("current_properties", now);
   yield sleep(25);
   recorder.onStartup();
 
   do_check_eq(recorder.startDate.getTime(), now.getTime());
   do_check_eq(recorder.activeTicks, 0);
-  do_check_true(recorder.totalTime > 0);
+  do_check_true(recorder.fineTotalTime > 0);
   do_check_eq(recorder.main, 500);
   do_check_eq(recorder.firstPaint, 1000);
   do_check_eq(recorder.sessionRestored, 1500);
 
   recorder.incrementActiveTicks();
   do_check_eq(recorder.activeTicks, 1);
 
+  recorder._startDate = new Date(Date.now() - 1000);
+  recorder.updateTotalTime();
+  do_check_eq(recorder.totalTime, 1);
+
   recorder.onShutdown();
 });
 
+// If startup info isn't present yet, we should install a timer and get
+// it eventually.
+add_task(function test_current_availability() {
+  let recorder = new SessionRecorder("testing.current_availability.");
+  let now = new Date();
+
+  Object.defineProperty(recorder, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: now,
+        main: new Date(now.getTime() + 500),
+        firstPaint: new Date(now.getTime() + 1000),
+      };
+    },
+    writable: true,
+  });
+
+  Object.defineProperty(recorder, "STARTUP_RETRY_INTERVAL_MS", {
+    value: 100,
+  });
+
+  let oldRecord = recorder.recordStartupFields;
+  let recordCount = 0;
+
+  Object.defineProperty(recorder, "recordStartupFields", {
+    value: function () {
+      recordCount++;
+      return oldRecord.call(recorder);
+    }
+  });
+
+  do_check_null(recorder._timer);
+  recorder.onStartup();
+  do_check_eq(recordCount, 1);
+  do_check_eq(recorder.sessionRestored, -1);
+  do_check_neq(recorder._timer, null);
+
+  yield sleep(125);
+  do_check_eq(recordCount, 2);
+  yield sleep(100);
+  do_check_eq(recordCount, 3);
+  do_check_eq(recorder.sessionRestored, -1);
+
+  monkeypatchStartupInfo(recorder, now);
+  yield sleep(100);
+  do_check_eq(recordCount, 4);
+  do_check_eq(recorder.sessionRestored, 1500);
+
+  // The timer should be removed and we should not fire again.
+  do_check_null(recorder._timer);
+  yield sleep(100);
+  do_check_eq(recordCount, 4);
+
+  recorder.onShutdown();
+});
+
+add_test(function test_timer_clear_on_shutdown() {
+  let recorder = new SessionRecorder("testing.timer_clear_on_shutdown.");
+  let now = new Date();
+
+  Object.defineProperty(recorder, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: now,
+        main: new Date(now.getTime() + 500),
+        firstPaint: new Date(now.getTime() + 1000),
+      };
+    },
+  });
+
+  do_check_null(recorder._timer);
+  recorder.onStartup();
+  do_check_neq(recorder._timer, null);
+
+  recorder.onShutdown();
+  do_check_null(recorder._timer);
+
+  run_next_test();
+});
+
 add_task(function test_previous_clean() {
   let now = new Date();
   let recorder = getRecorder("previous_clean", now);
   yield sleep(25);
   recorder.onStartup();
 
   recorder.incrementActiveTicks();
   recorder.incrementActiveTicks();
@@ -185,34 +269,34 @@ add_task(function test_record_activity()
   yield sleep(25);
   recorder.onStartup();
   let total = recorder.totalTime;
   yield sleep(25);
 
   for (let i = 0; i < 3; i++) {
     Services.obs.notifyObservers(null, "user-interaction-active", null);
     yield sleep(25);
-    do_check_true(recorder.totalTime > total);
-    total = recorder.totalTime;
+    do_check_true(recorder.fineTotalTime > total);
+    total = recorder.fineTotalTime;
   }
 
   do_check_eq(recorder.activeTicks, 3);
 
   // Now send inactive. We should increment total time but not active.
   Services.obs.notifyObservers(null, "user-interaction-inactive", null);
   do_check_eq(recorder.activeTicks, 3);
-  do_check_true(recorder.totalTime > total);
-  total = recorder.totalTime;
+  do_check_true(recorder.fineTotalTime > total);
+  total = recorder.fineTotalTime;
   yield sleep(25);
 
   // If we send active again, this should be counted as inactive.
   Services.obs.notifyObservers(null, "user-interaction-active", null);
   do_check_eq(recorder.activeTicks, 3);
-  do_check_true(recorder.totalTime > total);
-  total = recorder.totalTime;
+  do_check_true(recorder.fineTotalTime > total);
+  total = recorder.fineTotalTime;
   yield sleep(25);
 
   // If we send active again, this should be counted as active.
   Services.obs.notifyObservers(null, "user-interaction-active", null);
   do_check_eq(recorder.activeTicks, 4);
 
   Services.obs.notifyObservers(null, "user-interaction-active", null);
   do_check_eq(recorder.activeTicks, 5);
--- a/services/healthreport/HealthReport.jsm
+++ b/services/healthreport/HealthReport.jsm
@@ -5,16 +5,17 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "HealthReporter",
   "AddonsProvider",
   "AppInfoProvider",
   "CrashesProvider",
   "Metrics",
+  "PlacesProvider",
   "ProfileMetadataProvider",
   "SessionsProvider",
   "SysInfoProvider",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
--- a/services/healthreport/HealthReportComponents.manifest
+++ b/services/healthreport/HealthReportComponents.manifest
@@ -1,8 +1,9 @@
 # Register Firefox Health Report providers.
 category healthreport-js-provider AddonsProvider resource://gre/modules/HealthReport.jsm
 category healthreport-js-provider AppInfoProvider resource://gre/modules/HealthReport.jsm
 category healthreport-js-provider CrashesProvider resource://gre/modules/HealthReport.jsm
 category healthreport-js-provider SysInfoProvider resource://gre/modules/HealthReport.jsm
 category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/HealthReport.jsm
 category healthreport-js-provider SessionsProvider resource://gre/modules/HealthReport.jsm
+category healthreport-js-provider PlacesProvider resource://gre/modules/HealthReport.jsm
 
--- a/services/healthreport/Makefile.in
+++ b/services/healthreport/Makefile.in
@@ -16,24 +16,41 @@ modules := \
   $(NULL)
 
 testing_modules := \
   utils.jsm \
   $(NULL)
 
 TEST_DIRS += tests
 
+healthreport_depends = \
+  HealthReport.jsm \
+  ../common/async.js \
+  ../common/bagheeraclient.js \
+  ../metrics/Metrics.jsm \
+  ../metrics/collector.jsm \
+  ../metrics/dataprovider.jsm \
+  ../metrics/storage.jsm \
+  healthreporter.jsm \
+  profile.jsm \
+  providers.jsm \
+  $(NULL)
+
 MAIN_JS_MODULE := HealthReport.jsm
 MAIN_JS_MODULE_PATH = $(FINAL_TARGET)/modules
 PP_TARGETS += MAIN_JS_MODULE
 
 MODULES := $(modules)
 MODULES_PATH = $(FINAL_TARGET)/modules/services/healthreport
 PP_TARGETS += MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/healthreport
 
 EXTRA_COMPONENTS := \
   HealthReportComponents.manifest \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
+
+# Add extra prerequisites until bug 837792 is addressed.
+$(FINAL_TARGET)/modules/HealthReport.jsm: $(healthreport_depends)
+
--- a/services/healthreport/healthreport-prefs.js
+++ b/services/healthreport/healthreport-prefs.js
@@ -14,12 +14,13 @@ pref("datareporting.healthreport.lastDat
 pref("datareporting.healthreport.nextDataSubmissionTime", "0");
 pref("datareporting.healthreport.pendingDeleteRemoteData", false);
 
 // Health Report is enabled by default on all channels.
 pref("datareporting.healthreport.uploadEnabled", true);
 
 pref("datareporting.healthreport.service.enabled", true);
 pref("datareporting.healthreport.service.loadDelayMsec", 10000);
+pref("datareporting.healthreport.service.loadDelayFirstRunMsec", 60000);
 pref("datareporting.healthreport.service.providerCategories", "healthreport-js-provider");
 
 pref("datareporting.healthreport.about.glossaryUrl", "https://services.mozilla.com/healthreport/glossary.html");
 pref("datareporting.healthreport.about.reportUrl",   "https://services.mozilla.com/healthreport/placeholder.html");
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -19,27 +19,34 @@ Cu.import("resource://services-common/as
 
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 const DAYS_IN_PAYLOAD = 180;
 
 const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
 
+const TELEMETRY_INIT = "HEALTHREPORT_INIT_MS";
+const TELEMETRY_GENERATE_PAYLOAD = "HEALTHREPORT_GENERATE_JSON_PAYLOAD_MS";
+const TELEMETRY_PAYLOAD_SIZE = "HEALTHREPORT_PAYLOAD_UNCOMPRESSED_BYTES";
+const TELEMETRY_SAVE_LAST_PAYLOAD = "HEALTHREPORT_SAVE_LAST_PAYLOAD_MS";
+const TELEMETRY_UPLOAD = "HEALTHREPORT_UPLOAD_MS";
+const TELEMETRY_SHUTDOWN_DELAY = "HEALTHREPORT_SHUTDOWN_DELAY_MS";
 
 /**
  * Coordinates collection and submission of health report metrics.
  *
  * This is the main type for Firefox Health Report. It glues all the
  * lower-level components (such as collection and submission) together.
  *
  * An instance of this type is created as an XPCOM service. See
@@ -124,16 +131,21 @@ function HealthReporter(branch, policy, 
   this._initialized = false;
   this._initializeHadError = false;
   this._initializedDeferred = Promise.defer();
   this._shutdownRequested = false;
   this._shutdownInitiated = false;
   this._shutdownComplete = false;
   this._shutdownCompleteCallback = null;
 
+  this._constantOnlyProviders = {};
+  this._lastDailyDate = null;
+
+  TelemetryStopwatch.start(TELEMETRY_INIT, this);
+
   this._ensureDirectoryExists(this._stateDir)
       .then(this._onStateDirCreated.bind(this),
             this._onInitError.bind(this));
 
 }
 
 HealthReporter.prototype = Object.freeze({
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
@@ -241,16 +253,17 @@ HealthReporter.prototype = Object.freeze
 
   //----------------------------------------------------
   // SERVICE CONTROL FUNCTIONS
   //
   // You shouldn't need to call any of these externally.
   //----------------------------------------------------
 
   _onInitError: function (error) {
+    TelemetryStopwatch.cancel(TELEMETRY_INIT, this);
     this._log.error("Error during initialization: " +
                     CommonUtils.exceptionStr(error));
     this._initializeHadError = true;
     this._initiateShutdown();
     this._initializedDeferred.reject(error);
 
     // FUTURE consider poisoning prototype's functions so calls fail with a
     // useful error message.
@@ -296,16 +309,17 @@ HealthReporter.prototype = Object.freeze
     if (catString.length) {
       for (let category of catString.split(",")) {
         yield this.registerProvidersFromCategoryManager(category);
       }
     }
   },
 
   _onCollectorInitialized: function () {
+    TelemetryStopwatch.finish(TELEMETRY_INIT, this);
     this._log.debug("Collector initialized.");
     this._collectorInProgress = false;
 
     if (this._shutdownRequested) {
       this._initiateShutdown();
       return;
     }
 
@@ -435,19 +449,24 @@ HealthReporter.prototype = Object.freeze
     }
   },
 
   _waitForShutdown: function () {
     if (this._shutdownComplete) {
       return;
     }
 
-    this._shutdownCompleteCallback = Async.makeSpinningCallback();
-    this._shutdownCompleteCallback.wait();
-    this._shutdownCompleteCallback = null;
+    TelemetryStopwatch.start(TELEMETRY_SHUTDOWN_DELAY, this);
+    try {
+      this._shutdownCompleteCallback = Async.makeSpinningCallback();
+      this._shutdownCompleteCallback.wait();
+      this._shutdownCompleteCallback = null;
+    } finally {
+      TelemetryStopwatch.finish(TELEMETRY_SHUTDOWN_DELAY, this);
+    }
   },
 
   /**
    * Convenience method to shut down the instance.
    *
    * This should *not* be called outside of tests.
    */
   _shutdown: function () {
@@ -544,39 +563,126 @@ HealthReporter.prototype = Object.freeze
       let uri = cm.getCategoryEntry(category, entry);
       this._log.info("Attempting to load provider from category manager: " +
                      entry + " from " + uri);
 
       try {
         let ns = {};
         Cu.import(uri, ns);
 
-        let provider = new ns[entry]();
-        provider.initPreferences(this._branch + "provider.");
-        provider.healthReporter = this;
-        promises.push(this.registerProvider(provider));
+        let proto = ns[entry].prototype;
+        if (proto.constantOnly) {
+          this._log.info("Provider is constant-only. Deferring initialization: " +
+                         proto.name);
+          this._constantOnlyProviders[proto.name] = ns[entry];
+        } else {
+          let provider = this.initProviderFromType(ns[entry]);
+          promises.push(this.registerProvider(provider));
+        }
       } catch (ex) {
         this._log.warn("Error registering provider from category manager: " +
                        entry + "; " + CommonUtils.exceptionStr(ex));
         continue;
       }
     }
 
     return Task.spawn(function wait() {
       for (let promise of promises) {
         yield promise;
       }
     });
   },
 
+  initProviderFromType: function (providerType) {
+    let provider = new providerType();
+    provider.initPreferences(this._branch + "provider.");
+    provider.healthReporter = this;
+
+    return provider;
+  },
+
   /**
    * Collect all measurements for all registered providers.
    */
   collectMeasurements: function () {
-    return this._collector.collectConstantData();
+    return Task.spawn(function doCollection() {
+      for each (let providerType in this._constantOnlyProviders) {
+        try {
+          let provider = this.initProviderFromType(providerType);
+          yield this.registerProvider(provider);
+        } catch (ex) {
+          this._log.warn("Error registering constant-only provider: " +
+                         CommonUtils.exceptionStr(ex));
+        }
+      }
+
+      try {
+        yield this._collector.collectConstantData();
+      } finally {
+        for (let provider of this._collector.providers) {
+          if (!provider.constantOnly) {
+            continue;
+          }
+
+          this._log.info("Shutting down constant-only provider: " +
+                         provider.name);
+
+          try {
+            yield provider.shutdown();
+          } catch (ex) {
+            this._log.warn("Error when shutting down provider: " +
+                           CommonUtils.exceptionStr(ex));
+          } finally {
+            this._collector.unregisterProvider(provider.name);
+          }
+        }
+      }
+
+      // Daily data is collected if it hasn't yet been collected this
+      // application session or if it has been more than a day since the
+      // last collection. This means that providers could see many calls to
+      // collectDailyData per calendar day. However, this collection API
+      // makes no guarantees about limits. The alternative would involve
+      // recording state. The simpler implementation prevails for now.
+      if (!this._lastDailyDate ||
+          Date.now() - this._lastDailyDate > MILLISECONDS_PER_DAY) {
+
+        try {
+          this._lastDailyDate = new Date();
+          yield this._collector.collectDailyData();
+        } catch (ex) {
+          this._log.warn("Error collecting daily data from providers: " +
+                         CommonUtils.exceptionStr(ex));
+        }
+      }
+
+      throw new Task.Result();
+    }.bind(this));
+  },
+
+  /**
+   * Helper function to perform data collection and obtain the JSON payload.
+   *
+   * If you are looking for an up-to-date snapshot of FHR data that pulls in
+   * new data since the last upload, this is how you should obtain it.
+   *
+   * @param asObject
+   *        (bool) Whether to resolve an object or JSON-encoded string of that
+   *        object (the default).
+   *
+   * @return Promise<Object | string>
+   */
+  collectAndObtainJSONPayload: function (asObject=false) {
+    return Task.spawn(function collectAndObtain() {
+      yield this.collectMeasurements();
+
+      let payload = yield this.getJSONPayload(asObject);
+
+      throw new Task.Result(payload);
+    }.bind(this));
   },
 
   /**
    * Called to initiate a data upload.
    *
    * The passed argument is a `DataSubmissionRequest` from policy.jsm.
    */
   requestDataUpload: function (request) {
@@ -596,21 +702,47 @@ HealthReporter.prototype = Object.freeze
   requestDeleteRemoteData: function (reason) {
     if (!this.lastSubmitID) {
       return;
     }
 
     return this._policy.deleteRemoteData(reason);
   },
 
-  getJSONPayload: function () {
-    return Task.spawn(this._getJSONPayload.bind(this, this._now()));
+  /**
+   * Obtain the JSON payload for currently-collected data.
+   *
+   * The payload only contains data that has been recorded to FHR. Some
+   * providers may have newer data available. If you want to ensure you
+   * have all available data, call `collectAndObtainJSONPayload`
+   * instead.
+   *
+   * @param asObject
+   *        (bool) Whether to return an object or JSON encoding of that
+   *        object (the default).
+   */
+  getJSONPayload: function (asObject=false) {
+    TelemetryStopwatch.start(TELEMETRY_GENERATE_PAYLOAD, this);
+    let deferred = Promise.defer();
+
+    Task.spawn(this._getJSONPayload.bind(this, this._now(), asObject)).then(
+      function onResult(result) {
+        TelemetryStopwatch.finish(TELEMETRY_GENERATE_PAYLOAD, this);
+        deferred.resolve(result);
+      }.bind(this),
+      function onError(error) {
+        TelemetryStopwatch.cancel(TELEMETRY_GENERATE_PAYLOAD, this);
+        deferred.reject(error);
+      }.bind(this)
+    );
+
+    return deferred.promise;
   },
 
-  _getJSONPayload: function (now) {
+  _getJSONPayload: function (now, asObject=false) {
     let pingDateString = this._formatDate(now);
     this._log.info("Producing JSON payload for " + pingDateString);
 
     let o = {
       version: 1,
       thisPingDate: pingDateString,
       data: {last: {}, days: {}},
     };
@@ -699,17 +831,18 @@ HealthReporter.prototype = Object.freeze
       }
     }
 
     if (errors.length) {
       o.errors = errors;
     }
 
     this._storage.compact();
-    throw new Task.Result(JSON.stringify(o));
+
+    throw new Task.Result(asObject ? o : JSON.stringify(o));
   },
 
   _onBagheeraResult: function (request, isDelete, result) {
     this._log.debug("Received Bagheera result.");
 
     let promise = Promise.resolve(null);
 
     if (!result.transportSuccess) {
@@ -751,23 +884,50 @@ HealthReporter.prototype = Object.freeze
     let id = CommonUtils.generateUUID();
 
     this._log.info("Uploading data to server: " + this.serverURI + " " +
                    this.serverNamespace + ":" + id);
     let client = new BagheeraClient(this.serverURI);
 
     return Task.spawn(function doUpload() {
       let payload = yield this.getJSONPayload();
-      yield this._saveLastPayload(payload);
-      let result = yield client.uploadJSON(this.serverNamespace, id, payload,
-                                           this.lastSubmitID);
+
+      let histogram = Services.telemetry.getHistogramById(TELEMETRY_PAYLOAD_SIZE);
+      histogram.add(payload.length);
+
+      TelemetryStopwatch.start(TELEMETRY_SAVE_LAST_PAYLOAD, this);
+      try {
+        yield this._saveLastPayload(payload);
+        TelemetryStopwatch.finish(TELEMETRY_SAVE_LAST_PAYLOAD, this);
+      } catch (ex) {
+        TelemetryStopwatch.cancel(TELEMETRY_SAVE_LAST_PAYLOAD, this);
+        throw ex;
+      }
+
+      TelemetryStopwatch.start(TELEMETRY_UPLOAD, this);
+      let result;
+      try {
+        result = yield client.uploadJSON(this.serverNamespace, id, payload,
+                                         this.lastSubmitID);
+        TelemetryStopwatch.finish(TELEMETRY_UPLOAD, this);
+      } catch (ex) {
+        TelemetryStopwatch.cancel(TELEMETRY_UPLOAD, this);
+        throw ex;
+      }
+
       yield this._onBagheeraResult(request, false, result);
     }.bind(this));
   },
 
+  /**
+   * Request deletion of remote data.
+   *
+   * @param request
+   *        (DataSubmissionRequest) Tracks progress of this request.
+   */
   deleteRemoteData: function (request) {
     if (!this.lastSubmitID) {
       this._log.info("Received request to delete remote data but no data stored.");
       request.onNoDataAvailable();
       return;
     }
 
     this._log.warn("Deleting remote data.");
@@ -827,16 +987,20 @@ HealthReporter.prototype = Object.freeze
 
   /**
    * Obtain the last uploaded payload.
    *
    * The promise is resolved to a JSON-decoded object on success. The promise
    * is rejected if the last uploaded payload could not be found or there was
    * an error reading or parsing it.
    *
+   * This reads the last payload from disk. If you are looking for a
+   * current snapshot of the data, see `getJSONPayload` and
+   * `collectAndObtainJSONPayload`.
+   *
    * @return Promise<object>
    */
   getLastPayload: function () {
     let path = this._lastPayloadPath;
 
     return OS.File.read(path).then(
       function onData(buffer) {
         let decoder = new TextDecoder();
--- a/services/healthreport/profile.jsm
+++ b/services/healthreport/profile.jsm
@@ -209,16 +209,18 @@ function ProfileMetadataProvider() {
 }
 ProfileMetadataProvider.prototype = {
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.profile",
 
   measurementTypes: [ProfileMetadataMeasurement],
 
+  constantOnly: true,
+
   getProfileCreationDays: function () {
     let accessor = new ProfileCreationTimeAccessor(null, this._log);
 
     return accessor.created
                    .then(truncate);
   },
 
   collectConstantData: function () {
--- a/services/healthreport/providers.jsm
+++ b/services/healthreport/providers.jsm
@@ -16,16 +16,17 @@
 
 #ifndef MERGED_COMPARTMENT
 
 this.EXPORTED_SYMBOLS = [
   "AddonsProvider",
   "AppInfoProvider",
   "CrashDirectoryService",
   "CrashesProvider",
+  "PlacesProvider",
   "SessionsProvider",
   "SysInfoProvider",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Metrics.jsm");
 
@@ -38,16 +39,19 @@ Cu.import("resource://gre/modules/Task.j
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesDBUtils",
+                                  "resource://gre/modules/PlacesDBUtils.jsm");
+
 
 /**
  * Represents basic application state.
  *
  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
  * pieces thrown in.
  */
 function AppInfoMeasurement() {
@@ -116,16 +120,18 @@ this.AppInfoProvider = function AppInfoP
 }
 AppInfoProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.appInfo",
 
   measurementTypes: [AppInfoMeasurement, AppVersionMeasurement],
 
+  constantOnly: true,
+
   appInfoFields: {
     // From nsIXULAppInfo.
     vendor: "vendor",
     name: "name",
     id: "ID",
     version: "version",
     appBuildID: "appBuildID",
     platformVersion: "platformVersion",
@@ -291,16 +297,18 @@ this.SysInfoProvider = function SysInfoP
 
 SysInfoProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.sysinfo",
 
   measurementTypes: [SysInfoMeasurement],
 
+  constantOnly: true,
+
   sysInfoFields: {
     cpucount: "cpuCount",
     memsize: "memoryMB",
     manufacturer: "manufacturer",
     device: "device",
     hardware: "hardware",
     name: "name",
     version: "version",
@@ -366,17 +374,17 @@ SysInfoProvider.prototype = Object.freez
 function CurrentSessionMeasurement() {
   Metrics.Measurement.call(this);
 }
 
 CurrentSessionMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "current",
-  version: 2,
+  version: 3,
 
   configureStorage: function () {
     return Promise.resolve();
   },
 
   /**
    * All data is stored in prefs, so we have a custom implementation.
    */
@@ -415,17 +423,17 @@ CurrentSessionMeasurement.prototype = Ob
 function PreviousSessionsMeasurement() {
   Metrics.Measurement.call(this);
 }
 
 PreviousSessionsMeasurement.prototype = Object.freeze({
   __proto__: Metrics.Measurement.prototype,
 
   name: "previous",
-  version: 2,
+  version: 3,
 
   DAILY_DISCRETE_NUMERIC_FIELDS: [
     // Milliseconds of sessions that were properly shut down.
     "cleanActiveTicks",
     "cleanTotalTime",
 
     // Milliseconds of sessions that were not properly shut down.
     "abortedActiveTicks",
@@ -472,29 +480,31 @@ this.SessionsProvider = function () {
 
 SessionsProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.appSessions",
 
   measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
 
+  constantOnly: true,
+
   collectConstantData: function () {
-    let previous = this.getMeasurement("previous", 2);
+    let previous = this.getMeasurement("previous", 3);
 
     return this.storage.enqueueTransaction(this._recordAndPruneSessions.bind(this));
   },
 
   _recordAndPruneSessions: function () {
     this._log.info("Moving previous sessions from session recorder to storage.");
     let recorder = this.healthReporter.sessionRecorder;
     let sessions = recorder.getPreviousSessions();
     this._log.debug("Found " + Object.keys(sessions).length + " previous sessions.");
 
-    let daily = this.getMeasurement("previous", 2);
+    let daily = this.getMeasurement("previous", 3);
 
     for each (let session in sessions) {
       let type = session.clean ? "clean" : "aborted";
       let date = session.startDate;
       yield daily.addDailyDiscreteNumeric(type + "ActiveTicks", session.activeTicks, date);
       yield daily.addDailyDiscreteNumeric(type + "TotalTime", session.totalTime, date);
 
       for (let field of ["main", "firstPaint", "sessionRestored"]) {
@@ -741,16 +751,18 @@ this.CrashesProvider = function () {
 
 CrashesProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.crashes",
 
   measurementTypes: [DailyCrashesMeasurement],
 
+  constantOnly: true,
+
   collectConstantData: function () {
     return Task.spawn(this._populateCrashCounts.bind(this));
   },
 
   _populateCrashCounts: function () {
     let now = new Date();
     let service = new CrashDirectoryService();
 
@@ -875,8 +887,70 @@ CrashDirectoryService.prototype = Object
         throw new Task.Result(files);
       } finally {
         iterator.close();
       }
     });
   },
 });
 
+
+/**
+ * Holds basic statistics about the Places database.
+ */
+function PlacesMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+PlacesMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "places",
+  version: 1,
+
+  configureStorage: function () {
+    return Task.spawn(function registerFields() {
+      yield this.registerStorageField("pages", this.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield this.registerStorageField("bookmarks", this.storage.FIELD_DAILY_LAST_NUMERIC);
+    }.bind(this));
+  },
+});
+
+
+/**
+ * Collects information about Places.
+ */
+this.PlacesProvider = function () {
+  Metrics.Provider.call(this);
+};
+
+PlacesProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.places",
+
+  measurementTypes: [PlacesMeasurement],
+
+  collectDailyData: function () {
+    return this.storage.enqueueTransaction(this._collectData.bind(this));
+  },
+
+  _collectData: function () {
+    let now = new Date();
+    let data = yield this._getDailyValues();
+
+    let m = this.getMeasurement("places", 1);
+
+    yield m.setDailyLastNumeric("pages", data.PLACES_PAGES_COUNT);
+    yield m.setDailyLastNumeric("bookmarks", data.PLACES_BOOKMARKS_COUNT);
+  },
+
+  _getDailyValues: function () {
+    let deferred = Promise.defer();
+
+    PlacesDBUtils.telemetry(null, function onResult(data) {
+      deferred.resolve(data);
+    });
+
+    return deferred.promise;
+  },
+});
+
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -166,37 +166,103 @@ add_task(function test_register_provider
   let reporter = yield getReporter("category_manager");
   do_check_eq(reporter._collector._providers.size, 0);
   yield reporter.registerProvidersFromCategoryManager(category);
   do_check_eq(reporter._collector._providers.size, 1);
 
   reporter._shutdown();
 });
 
+// Constant only providers are only initialized at constant collect
+// time.
+add_task(function test_constant_only_providers() {
+  const category = "healthreporter-constant-only";
+
+  let cm = Cc["@mozilla.org/categorymanager;1"]
+             .getService(Ci.nsICategoryManager);
+  cm.addCategoryEntry(category, "DummyProvider",
+                      "resource://testing-common/services/metrics/mocks.jsm",
+                      false, true);
+  cm.addCategoryEntry(category, "DummyConstantProvider",
+                      "resource://testing-common/services/metrics/mocks.jsm",
+                      false, true);
+
+  let reporter = yield getReporter("constant_only_providers");
+  do_check_eq(reporter._collector._providers.size, 0);
+  yield reporter.registerProvidersFromCategoryManager(category);
+  do_check_eq(reporter._collector._providers.size, 1);
+  do_check_true(reporter._storage.hasProvider("DummyProvider"));
+  do_check_false(reporter._storage.hasProvider("DummyConstantProvider"));
+
+  yield reporter.collectMeasurements();
+
+  do_check_eq(reporter._collector._providers.size, 1);
+  do_check_true(reporter._storage.hasProvider("DummyConstantProvider"));
+
+  let mID = reporter._storage.measurementID("DummyConstantProvider", "DummyMeasurement", 1);
+  let values = yield reporter._storage.getMeasurementValues(mID);
+  do_check_true(values.singular.size > 0);
+
+  reporter._shutdown();
+});
+
+add_task(function test_collect_daily() {
+  let reporter = yield getReporter("collect_daily");
+
+  let now = new Date();
+  let provider = new DummyProvider();
+  yield reporter.registerProvider(provider);
+  yield reporter.collectMeasurements();
+
+  do_check_eq(provider.collectConstantCount, 1);
+  do_check_eq(provider.collectDailyCount, 1);
+
+  yield reporter.collectMeasurements();
+  do_check_eq(provider.collectConstantCount, 1);
+  do_check_eq(provider.collectDailyCount, 1);
+
+  yield reporter.collectMeasurements();
+  do_check_eq(provider.collectDailyCount, 1); // Too soon.
+
+  reporter._lastDailyDate = now.getTime() - MILLISECONDS_PER_DAY - 1;
+  yield reporter.collectMeasurements();
+  do_check_eq(provider.collectDailyCount, 2);
+
+  reporter._lastDailyDate = null;
+  yield reporter.collectMeasurements();
+  do_check_eq(provider.collectDailyCount, 3);
+
+  reporter._shutdown();
+});
+
 add_task(function test_json_payload_simple() {
   let reporter = yield getReporter("json_payload_simple");
 
   let now = new Date();
   let payload = yield reporter.getJSONPayload();
+  do_check_eq(typeof payload, "string");
   let original = JSON.parse(payload);
 
   do_check_eq(original.version, 1);
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
   do_check_eq(Object.keys(original.data.last).length, 0);
   do_check_eq(Object.keys(original.data.days).length, 0);
 
   reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
 
   original = JSON.parse(yield reporter.getJSONPayload());
   do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
 
   // This could fail if we cross UTC day boundaries at the exact instance the
   // test is executed. Let's tempt fate.
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
 
+  payload = yield reporter.getJSONPayload(true);
+  do_check_eq(typeof payload, "object");
+
   reporter._shutdown();
 });
 
 add_task(function test_json_payload_dummy_provider() {
   let reporter = yield getReporter("json_payload_dummy_provider");
 
   yield reporter.registerProvider(new DummyProvider());
   yield reporter.collectMeasurements();
@@ -207,16 +273,32 @@ add_task(function test_json_payload_dumm
   let name = "DummyProvider.DummyMeasurement";
   do_check_eq(Object.keys(o.data.last).length, 1);
   do_check_true(name in o.data.last);
   do_check_eq(o.data.last[name]._v, 1);
 
   reporter._shutdown();
 });
 
+add_task(function test_collect_and_obtain_json_payload() {
+  let reporter = yield getReporter("collect_and_obtain_json_payload");
+
+  yield reporter.registerProvider(new DummyProvider());
+  let payload = yield reporter.collectAndObtainJSONPayload();
+  do_check_eq(typeof payload, "string");
+
+  let o = JSON.parse(payload);
+  do_check_true("DummyProvider.DummyMeasurement" in o.data.last);
+
+  payload = yield reporter.collectAndObtainJSONPayload(true);
+  do_check_eq(typeof payload, "object");
+
+  reporter._shutdown();
+});
+
 add_task(function test_json_payload_multiple_days() {
   let reporter = yield getReporter("json_payload_multiple_days");
   let provider = new DummyProvider();
   yield reporter.registerProvider(provider);
 
   let now = new Date();
   let m = provider.getMeasurement("DummyMeasurement", 1);
   for (let i = 0; i < 200; i++) {
new file mode 100644
--- /dev/null
+++ b/services/healthreport/tests/xpcshell/test_provider_places.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_constructor() {
+  let provider = new PlacesProvider();
+
+  run_next_test();
+});
+
+add_task(function test_collect_smoketest() {
+  let storage = yield Metrics.Storage("collect_smoketest");
+  let provider = new PlacesProvider();
+
+  yield provider.init(storage);
+
+  let now = new Date();
+  yield provider.collectDailyData();
+
+  let m = provider.getMeasurement("places", 1);
+  let data = yield storage.getMeasurementValues(m.id);
+  do_check_eq(data.days.size, 1);
+  do_check_true(data.days.hasDay(now));
+
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let day = serializer.daily(data.days.getDay(now));
+
+  do_check_eq(day._v, 1);
+  do_check_eq(Object.keys(day).length, 3);
+  do_check_eq(day.pages, 0);
+  do_check_eq(day.bookmarks, 0);
+
+  yield storage.close();
+});
+
--- a/services/healthreport/tests/xpcshell/test_provider_sessions.js
+++ b/services/healthreport/tests/xpcshell/test_provider_sessions.js
@@ -78,17 +78,17 @@ function getProvider(name, now=new Date(
 
 add_task(function test_current_session() {
   let now = new Date();
   let [provider, storage, recorder] = yield getProvider("current_session", now);
 
   yield sleep(25);
   recorder.onActivity(true);
 
-  let current = provider.getMeasurement("current", 2);
+  let current = provider.getMeasurement("current", 3);
   let values = yield current.getValues();
   let fields = values.singular;
 
   for (let field of ["startDay", "activeTicks", "totalTime", "main", "firstPaint", "sessionRestored"]) {
     do_check_true(fields.has(field));
   }
 
   do_check_eq(fields.get("startDay")[1], Metrics.dateToDays(now));
@@ -119,17 +119,17 @@ add_task(function test_collect() {
 
   recorder = new SessionRecorder("testing.collect.sessions.");
   recorder.onStartup();
 
   yield provider.collectConstantData();
   let sessions = recorder.getPreviousSessions();
   do_check_eq(Object.keys(sessions).length, 0);
 
-  let daily = provider.getMeasurement("previous", 2);
+  let daily = provider.getMeasurement("previous", 3);
   let values = yield daily.getValues();
   do_check_true(values.days.hasDay(now));
   do_check_eq(values.days.size, 1);
 
   let day = values.days.getDay(now);
   do_check_eq(day.size, 5);
 
   for (let field of ["cleanActiveTicks", "cleanTotalTime", "main", "firstPaint", "sessionRestored"]) {
@@ -158,26 +158,30 @@ add_task(function test_collect() {
 
   yield provider.shutdown();
   yield storage.close();
 });
 
 add_task(function test_serialization() {
   let [provider, storage, recorder] = yield getProvider("serialization");
 
-  let current = provider.getMeasurement("current", 2);
+  yield sleep(1025);
+  recorder.onActivity(true);
+
+  let current = provider.getMeasurement("current", 3);
   let data = yield current.getValues();
   do_check_true("singular" in data);
 
   let serializer = current.serializer(current.SERIALIZE_JSON);
   let fields = serializer.singular(data.singular);
 
-  do_check_eq(fields._v, 2);
-  do_check_eq(fields.activeTicks, 0);
+  do_check_eq(fields._v, 3);
+  do_check_eq(fields.activeTicks, 1);
   do_check_eq(fields.startDay, Metrics.dateToDays(recorder.startDate));
   do_check_eq(fields.main, 500);
   do_check_eq(fields.firstPaint, 1000);
   do_check_eq(fields.sessionRestored, 1500);
   do_check_true(fields.totalTime > 0);
 
   yield provider.shutdown();
   yield storage.close();
 });
+
--- a/services/healthreport/tests/xpcshell/xpcshell.ini
+++ b/services/healthreport/tests/xpcshell/xpcshell.ini
@@ -3,11 +3,12 @@ head = head.js
 tail =
 
 [test_load_modules.js]
 [test_profile.js]
 [test_healthreporter.js]
 [test_provider_addons.js]
 [test_provider_appinfo.js]
 [test_provider_crashes.js]
+[test_provider_places.js]
 [test_provider_sysinfo.js]
 [test_provider_sessions.js]
 
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -1,28 +1,26 @@
 # 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/.
 
 add_makefiles "
   services/Makefile
-  services/aitc/Makefile
   services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
   services/healthreport/Makefile
   services/datareporting/Makefile
   services/metrics/Makefile
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
-    services/aitc/tests/Makefile
     services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/healthreport/tests/Makefile
     services/datareporting/tests/Makefile
     services/metrics/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/services/metrics/Makefile.in
+++ b/services/metrics/Makefile.in
@@ -30,8 +30,13 @@ TEST_DIRS += tests
 JS_MODULES := $(modules)
 JS_MODULES_PATH = $(FINAL_TARGET)/modules/services/metrics
 PP_TARGETS += JS_MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/metrics
 
 include $(topsrcdir)/config/rules.mk
+
+# Add extra prerequisites until bug 837792 is addressed.
+$(FINAL_TARGET)/modules/Metrics.jsm: Metrics.jsm collector.jsm dataprovider.jsm storage.jsm
+
+
--- a/services/metrics/collector.jsm
+++ b/services/metrics/collector.jsm
@@ -76,16 +76,26 @@ Collector.prototype = Object.freeze({
 
     if (this._providerInitQueue.length == 1) {
       this._popAndInitProvider();
     }
 
     return deferred.promise;
   },
 
+  /**
+   * Remove a named provider from the collector.
+   *
+   * It is the caller's responsibility to shut down the provider
+   * instance.
+   */
+  unregisterProvider: function (name) {
+    this._providers.delete(name);
+  },
+
   _popAndInitProvider: function () {
     if (!this._providerInitQueue.length || this._providerInitializing) {
       return;
     }
 
     let [provider, deferred] = this._providerInitQueue.shift();
     this._providerInitializing = true;
 
@@ -132,48 +142,79 @@ Collector.prototype = Object.freeze({
    * Collects all constant measurements from all providers.
    *
    * Returns a Promise that will be fulfilled once all data providers have
    * provided their constant data. A side-effect of this promise fulfillment
    * is that the collector is populated with the obtained collection results.
    * The resolved value to the promise is this `Collector` instance.
    */
   collectConstantData: function () {
-    let promises = [];
+    let entries = [];
 
     for (let [name, entry] of this._providers) {
       if (entry.constantsCollected) {
         this._log.trace("Provider has already provided constant data: " +
                         name);
         continue;
       }
 
+      entries.push(entry);
+    }
+
+    let onCollect = function (entry, result) {
+      entry.constantsCollected = true;
+    };
+
+    return this._callCollectOnProviders(entries, "collectConstantData",
+                                        onCollect);
+  },
+
+  /**
+   * Calls collectDailyData on all providers.
+   */
+  collectDailyData: function () {
+    return this._callCollectOnProviders(this._providers.values(),
+                                        "collectDailyData");
+  },
+
+  _callCollectOnProviders: function (entries, fnProperty, onCollect=null) {
+    let promises = [];
+
+    for (let entry of entries) {
+      let provider = entry.provider;
       let collectPromise;
       try {
-        collectPromise = entry.provider.collectConstantData();
+        collectPromise = provider[fnProperty].call(provider);
       } catch (ex) {
-        this._log.warn("Exception when calling " + name +
-                       ".collectConstantData: " +
-                       CommonUtils.exceptionStr(ex));
-        this.providerErrors.get(name).push(ex);
+        this._log.warn("Exception when calling " + provider.name + "." +
+                       fnProperty + ": " + CommonUtils.exceptionStr(ex));
+        this.providerErrors.get(provider.name).push(ex);
         continue;
       }
 
       if (!collectPromise) {
-        throw new Error("Provider does not return a promise from " +
-                        "collectConstantData():" + name);
+        this._log.warn("Provider does not return a promise from " +
+                       fnProperty + "(): " + provider.name);
+        continue;
       }
 
       let promise = collectPromise.then(function onCollected(result) {
-        entry.constantsCollected = true;
+        if (onCollect) {
+          try {
+            onCollect(entry, result);
+          } catch (ex) {
+            this._log.warn("onCollect callback threw: " +
+                           CommonUtils.exceptionStr(ex));
+          }
+        }
 
         return Promise.resolve(result);
       });
 
-      promises.push([name, promise]);
+      promises.push([provider.name, promise]);
     }
 
     return this._handleCollectionPromises(promises);
   },
 
   /**
    * Handles promises returned by the collect* functions.
    *
--- a/services/metrics/dataprovider.jsm
+++ b/services/metrics/dataprovider.jsm
@@ -387,16 +387,30 @@ this.Provider = function () {
   this._log = Log4Moz.repository.getLogger("Services.Metrics.Provider." + this.name);
 
   this.measurements = null;
   this.storage = null;
 }
 
 Provider.prototype = Object.freeze({
   /**
+   * Whether the provider provides only constant data.
+   *
+   * If this is true, the provider likely isn't instantiated until
+   * `collectConstantData` is called and the provider may be torn down after
+   * this function has finished.
+   *
+   * This is an optimization so provider instances aren't dead weight while the
+   * application is running.
+   *
+   * This must be set on the prototype for the optimization to be realized.
+   */
+  constantOnly: false,
+
+  /**
    * Obtain a `Measurement` from its name and version.
    *
    * If the measurement is not found, an Error is thrown.
    */
   getMeasurement: function (name, version) {
     if (!Number.isInteger(version)) {
       throw new Error("getMeasurement expects an integer version. Got: " + version);
     }
@@ -504,22 +518,40 @@ Provider.prototype = Object.freeze({
     return Promise.resolve();
   },
 
   /**
    * Collects data that doesn't change during the application's lifetime.
    *
    * Implementations should return a promise that resolves when all data has
    * been collected and storage operations have been finished.
+   *
+   * @return Promise<>
    */
   collectConstantData: function () {
     return Promise.resolve();
   },
 
   /**
+   * Collects data approximately every day.
+   *
+   * For long-running applications, this is called approximately every day.
+   * It may or may not be called every time the application is run. It also may
+   * be called more than once per day.
+   *
+   * Implementations should return a promise that resolves when all data has
+   * been collected and storage operations have completed.
+   *
+   * @return Promise<>
+   */
+  collectDailyData: function () {
+    return Promise.resolve();
+  },
+
+  /**
    * Queue a deferred storage operation.
    *
    * Deferred storage operations are the preferred method for providers to
    * interact with storage. When collected data is to be added to storage,
    * the provider creates a function that performs the necessary storage
    * interactions and then passes that function to this function. Pending
    * storage operations will be executed sequentially by a coordinator.
    *
--- a/services/metrics/modules-testing/mocks.jsm
+++ b/services/metrics/modules-testing/mocks.jsm
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "DummyMeasurement",
   "DummyProvider",
+  "DummyConstantProvider",
 ];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
@@ -48,16 +49,18 @@ this.DummyProvider = function DummyProvi
 
   Metrics.Provider.call(this);
 
   this.constantMeasurementName = "DummyMeasurement";
   this.collectConstantCount = 0;
   this.throwDuringCollectConstantData = null;
   this.throwDuringConstantPopulate = null;
 
+  this.collectDailyCount = 0;
+
   this.havePushedMeasurements = true;
 }
 
 DummyProvider.prototype = {
   __proto__: Metrics.Provider.prototype,
 
   collectConstantData: function () {
     this.collectConstantCount++;
@@ -80,10 +83,26 @@ DummyProvider.prototype = {
       m.addDailyDiscreteText("daily-discrete-text", "bar", now);
       m.setDailyLastNumeric("daily-last-numeric", 3, now);
       m.setDailyLastText("daily-last-text", "biz", now);
       m.setLastNumeric("last-numeric", 4, now);
       return m.setLastText("last-text", "bazfoo", now);
     }.bind(this));
   },
 
+  collectDailyData: function () {
+    this.collectDailyCount++;
+
+    return Promise.resolve();
+  },
 };
 
+
+this.DummyConstantProvider = function () {
+  DummyProvider.call(this, "DummyConstantProvider");
+}
+
+DummyConstantProvider.prototype = {
+  __proto__: DummyProvider.prototype,
+
+  constantOnly: true,
+};
+
--- a/services/metrics/tests/xpcshell/test_metrics_collector.js
+++ b/services/metrics/tests/xpcshell/test_metrics_collector.js
@@ -37,16 +37,19 @@ add_task(function test_register_provider
   } catch (ex) {
     do_check_true(ex.message.startsWith("Argument must be a Provider"));
     failed = true;
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
+  collector.unregisterProvider(dummy.name);
+  do_check_eq(collector._providers.size, 0);
+
   yield storage.close();
 });
 
 add_task(function test_collect_constant_data() {
   let storage = yield Metrics.Storage("collect_constant_data");
   let collector = new Metrics.Collector(storage);
   let provider = new DummyProvider();
   yield collector.registerProvider(provider);
@@ -120,8 +123,29 @@ add_task(function test_collect_multiple(
 
   do_check_eq(collector._providers.size, 10);
 
   yield collector.collectConstantData();
 
   yield storage.close();
 });
 
+add_task(function test_collect_daily() {
+  let storage = yield Metrics.Storage("collect_daily");
+  let collector = new Metrics.Collector(storage);
+
+  let provider1 = new DummyProvider("DP1");
+  let provider2 = new DummyProvider("DP2");
+
+  yield collector.registerProvider(provider1);
+  yield collector.registerProvider(provider2);
+
+  yield collector.collectDailyData();
+  do_check_eq(provider1.collectDailyCount, 1);
+  do_check_eq(provider2.collectDailyCount, 1);
+
+  yield collector.collectDailyData();
+  do_check_eq(provider1.collectDailyCount, 2);
+  do_check_eq(provider2.collectDailyCount, 2);
+
+  yield storage.close();
+});
+
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -319,22 +319,99 @@ this.Utils = {
    * Take a base64-encoded 128-bit AES key, returning it as five groups of five
    * uppercase alphanumeric characters, separated by hyphens.
    * A.K.A. base64-to-base32 encoding.
    */
   presentEncodedKeyAsSyncKey : function presentEncodedKeyAsSyncKey(encodedKey) {
     return Utils.encodeKeyBase32(atob(encodedKey));
   },
 
-  jsonLoad: function jsonLoad(path, that, callback) {
-    CommonUtils.jsonLoad("weave/" + path, that, callback);
+  /**
+   * Load a JSON file from disk in the profile directory.
+   *
+   * @param filePath
+   *        JSON file path load from profile. Loaded file will be
+   *        <profile>/<filePath>.json. i.e. Do not specify the ".json"
+   *        extension.
+   * @param that
+   *        Object to use for logging and "this" for callback.
+   * @param callback
+   *        Function to process json object as its first argument. If the file
+   *        could not be loaded, the first argument will be undefined.
+   */
+  jsonLoad: function jsonLoad(filePath, that, callback) {
+    let path = "weave/" + filePath + ".json";
+
+    if (that._log) {
+      that._log.trace("Loading json from disk: " + filePath);
+    }
+
+    let file = FileUtils.getFile("ProfD", path.split("/"), true);
+    if (!file.exists()) {
+      callback.call(that);
+      return;
+    }
+
+    let channel = NetUtil.newChannel(file);
+    channel.contentType = "application/json";
+
+    NetUtil.asyncFetch(channel, function (is, result) {
+      if (!Components.isSuccessCode(result)) {
+        callback.call(that);
+        return;
+      }
+      let string = NetUtil.readInputStreamToString(is, is.available());
+      is.close();
+      let json;
+      try {
+        json = JSON.parse(string);
+      } catch (ex) {
+        if (that._log) {
+          that._log.debug("Failed to load json: " +
+                          CommonUtils.exceptionStr(ex));
+        }
+      }
+      callback.call(that, json);
+    });
   },
 
-  jsonSave: function jsonSave(path, that, obj, callback) {
-    CommonUtils.jsonSave("weave/" + path, that, obj, callback);
+  /**
+   * Save a json-able object to disk in the profile directory.
+   *
+   * @param filePath
+   *        JSON file path save to <filePath>.json
+   * @param that
+   *        Object to use for logging and "this" for callback
+   * @param obj
+   *        Function to provide json-able object to save. If this isn't a
+   *        function, it'll be used as the object to make a json string.
+   * @param callback
+   *        Function called when the write has been performed. Optional.
+   *        The first argument will be a Components.results error
+   *        constant on error or null if no error was encountered (and
+   *        the file saved successfully).
+   */
+  jsonSave: function jsonSave(filePath, that, obj, callback) {
+    let path = "weave/" + filePath + ".json";
+    if (that._log) {
+      that._log.trace("Saving json to disk: " + path);
+    }
+
+    let file = FileUtils.getFile("ProfD", path.split("/"), true);
+    let json = typeof obj == "function" ? obj.call(that) : obj;
+    let out = JSON.stringify(json);
+
+    let fos = FileUtils.openSafeFileOutputStream(file);
+    let is = this._utf8Converter.convertToInputStream(out);
+    NetUtil.asyncCopy(is, fos, function (result) {
+      if (typeof callback == "function") {
+        let error = (result == Cr.NS_OK) ? null : result;
+        callback.call(that, error);
+      }
+    });
   },
 
   getIcon: function(iconUri, defaultIcon) {
     try {
       let iconURI = Utils.makeURI(iconUri);
       return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
     }
     catch(ex) {}
copy from services/common/tests/unit/test_utils_json.js
copy to services/sync/tests/unit/test_utils_json.js
--- a/services/common/tests/unit/test_utils_json.js
+++ b/services/sync/tests/unit/test_utils_json.js
@@ -1,89 +1,88 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
-Cu.import("resource://services-common/utils.js");
-Cu.import("resource://gre/modules/osfile.jsm")
+Cu.import("resource://services-sync/util.js");
 
 function run_test() {
   initTestLogging();
   run_next_test();
 }
 
 add_test(function test_roundtrip() {
   _("Do a simple write of an array to json and read");
-  CommonUtils.jsonSave("foo", {}, ["v1", "v2"], ensureThrows(function(error) {
+  Utils.jsonSave("foo", {}, ["v1", "v2"], ensureThrows(function(error) {
     do_check_eq(error, null);
 
-    CommonUtils.jsonLoad("foo", {}, ensureThrows(function(val) {
+    Utils.jsonLoad("foo", {}, ensureThrows(function(val) {
       let foo = val;
       do_check_eq(typeof foo, "object");
       do_check_eq(foo.length, 2);
       do_check_eq(foo[0], "v1");
       do_check_eq(foo[1], "v2");
       run_next_test();
     }));
   }));
 });
 
 add_test(function test_string() {
   _("Try saving simple strings");
-  CommonUtils.jsonSave("str", {}, "hi", ensureThrows(function(error) {
+  Utils.jsonSave("str", {}, "hi", ensureThrows(function(error) {
     do_check_eq(error, null);
 
-    CommonUtils.jsonLoad("str", {}, ensureThrows(function(val) {
+    Utils.jsonLoad("str", {}, ensureThrows(function(val) {
       let str = val;
       do_check_eq(typeof str, "string");
       do_check_eq(str.length, 2);
       do_check_eq(str[0], "h");
       do_check_eq(str[1], "i");
       run_next_test();
     }));
   }));
 });
 
 add_test(function test_number() {
   _("Try saving a number");
-  CommonUtils.jsonSave("num", {}, 42, ensureThrows(function(error) {
+  Utils.jsonSave("num", {}, 42, ensureThrows(function(error) {
     do_check_eq(error, null);
 
-    CommonUtils.jsonLoad("num", {}, ensureThrows(function(val) {
+    Utils.jsonLoad("num", {}, ensureThrows(function(val) {
       let num = val;
       do_check_eq(typeof num, "number");
       do_check_eq(num, 42);
       run_next_test();
     }));
   }));
 });
 
 add_test(function test_nonexistent_file() {
   _("Try loading a non-existent file.");
-  CommonUtils.jsonLoad("non-existent", {}, ensureThrows(function(val) {
+  Utils.jsonLoad("non-existent", {}, ensureThrows(function(val) {
     do_check_eq(val, undefined);
     run_next_test();
   }));
 });
 
 add_test(function test_save_logging() {
   _("Verify that writes are logged.");
   let trace;
-  CommonUtils.jsonSave("log", {_log: {trace: function(msg) { trace = msg; }}},
+  Utils.jsonSave("log", {_log: {trace: function(msg) { trace = msg; }}},
                        "hi", ensureThrows(function () {
     do_check_true(!!trace);
     run_next_test();
   }));
 });
 
 add_test(function test_load_logging() {
   _("Verify that reads and read errors are logged.");
 
   // Write a file with some invalid JSON
-  let filePath = "log.json";
+  let filePath = "weave/log.json";
   let file = FileUtils.getFile("ProfD", filePath.split("/"), true);
   let fos = Cc["@mozilla.org/network/file-output-stream;1"]
               .createInstance(Ci.nsIFileOutputStream);
   let flags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE
               | FileUtils.MODE_TRUNCATE;
   fos.init(file, flags, FileUtils.PERMS_FILE, fos.DEFER_OPEN);
   let stream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                  .createInstance(Ci.nsIConverterOutputStream);
@@ -97,45 +96,16 @@ add_test(function test_load_logging() {
       trace: function(msg) {
         trace = msg;
       },
       debug: function(msg) {
         debug = msg;
       }
     }
   };
-  CommonUtils.jsonLoad("log", obj, ensureThrows(function(val) {
+  Utils.jsonLoad("log", obj, ensureThrows(function(val) {
     do_check_true(!val);
     do_check_true(!!trace);
     do_check_true(!!debug);
     run_next_test();
   }));
 });
 
-add_test(function test_writeJSON_readJSON() {
-  _("Round-trip some JSON through the promise-based JSON writer.");
-
-  let contents = {
-    "a": 12345.67,
-    "b": {
-      "c": "héllö",
-    },
-    "d": undefined,
-    "e": null,
-  };
-
-  function checkJSON(json) {
-    do_check_eq(contents.a, json.a);
-    do_check_eq(contents.b.c, json.b.c);
-    do_check_eq(contents.d, json.d);
-    do_check_eq(contents.e, json.e);
-    run_next_test();
-  };
-
-  function doRead() {
-    CommonUtils.readJSON(path)
-               .then(checkJSON, do_throw);
-  }
-
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "bar.json");
-  CommonUtils.writeJSON(contents, path)
-             .then(doRead, do_throw);
-});
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -12,16 +12,17 @@ tail =
 # util contains a bunch of functionality used throughout.
 [test_utils_catch.js]
 [test_utils_deepEquals.js]
 [test_utils_deferGetSet.js]
 [test_utils_deriveKey.js]
 [test_utils_keyEncoding.js]
 [test_utils_getErrorString.js]
 [test_utils_getIcon.js]
+[test_utils_json.js]
 [test_utils_lazyStrings.js]
 [test_utils_lock.js]
 [test_utils_makeGUID.js]
 [test_utils_notify.js]
 [test_utils_passphrase.js]
 
 # We have a number of other libraries that are pretty much standalone.
 [test_addon_utils.js]
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -80,17 +80,16 @@ skip-if = os == "android"
 [include:extensions/cookie/test/unit/xpcshell.ini]
 [include:storage/test/unit/xpcshell.ini]
 [include:rdf/tests/unit/xpcshell.ini]
 [include:gfx/tests/unit/xpcshell.ini]
 [include:widget/tests/unit/xpcshell.ini]
 [include:content/base/test/unit/xpcshell.ini]
 [include:content/test/unit/xpcshell.ini]
 [include:toolkit/components/url-classifier/tests/unit/xpcshell.ini]
-[include:services/aitc/tests/unit/xpcshell.ini]
 [include:services/common/tests/unit/xpcshell.ini]
 [include:services/crypto/tests/unit/xpcshell.ini]
 [include:services/crypto/components/tests/unit/xpcshell.ini]
 [include:services/datareporting/tests/xpcshell/xpcshell.ini]
 [include:services/healthreport/tests/xpcshell/xpcshell.ini]
 [include:services/metrics/tests/xpcshell/xpcshell.ini]
 [include:services/sync/tests/unit/xpcshell.ini]
 # Bug 676978: tests hang on Android
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -812,48 +812,64 @@ this.PlacesDBUtils = {
     stmt.finalize();
 
     PlacesDBUtils._executeTasks(tasks);
   },
 
   /**
    * Collects telemetry data.
    *
+   * There are essentially two modes of collection and the mode is
+   * determined by the presence of aHealthReportCallback. If
+   * aHealthReportCallback is not defined (the default) then we are in
+   * "Telemetry" mode. Results will be reported to Telemetry. If we are
+   * in "Health Report" mode only the probes with a true healthreport
+   * flag will be collected and the results will be reported to the
+   * aHealthReportCallback.
+   *
    * @param [optional] aTasks
    *        Tasks object to execute.
+   * @param [optional] aHealthReportCallback
+   *        Function to receive data relevant for Firefox Health Report.
    */
-  telemetry: function PDBU_telemetry(aTasks)
+  telemetry: function PDBU_telemetry(aTasks, aHealthReportCallback=null)
   {
     let tasks = new Tasks(aTasks);
 
+    let isTelemetry = !aHealthReportCallback;
+
     // This will be populated with one integer property for each probe result,
     // using the histogram name as key.
     let probeValues = {};
 
     // The following array contains an ordered list of entries that are
     // processed to collect telemetry data.  Each entry has these properties:
     //
     //  histogram: Name of the telemetry histogram to update.
     //  query:     This is optional.  If present, contains a database command
     //             that will be executed asynchronously, and whose result will
     //             be added to the telemetry histogram.
     //  callback:  This is optional.  If present, contains a function that must
     //             return the value that will be added to the telemetry
     //             histogram. If a query is also present, its result is passed
     //             as the first argument of the function.  If the function
     //             raises an exception, no data is added to the histogram.
+    //  healthreport: Boolean indicating whether this probe is relevant
+    //                to Firefox Health Report.
     //
     // Since all queries are executed in order by the database backend, the
     // callbacks can also use the result of previous queries stored in the
     // probeValues object.
     let probes = [
       { histogram: "PLACES_PAGES_COUNT",
+        healthreport: true,
         query:     "SELECT count(*) FROM moz_places" },
 
       { histogram: "PLACES_BOOKMARKS_COUNT",
+        healthreport: true,
         query:     "SELECT count(*) FROM moz_bookmarks b "
                  + "JOIN moz_bookmarks t ON t.id = b.parent "
                  + "AND t.parent <> :tags_folder "
                  + "WHERE b.type = :type_bookmark " },
 
       { histogram: "PLACES_TAGS_COUNT",
         query:     "SELECT count(*) FROM moz_bookmarks "
                  + "WHERE parent = :tags_folder " },
@@ -939,51 +955,69 @@ this.PlacesDBUtils = {
 
     let params = {
       tags_folder: PlacesUtils.tagsFolderId,
       type_folder: PlacesUtils.bookmarks.TYPE_FOLDER,
       type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
       places_root: PlacesUtils.placesRootId
     };
 
-    function reportTelemetry(aProbe, aValue) {
+    let outstandingProbes = 0;
+
+    function reportResult(aProbe, aValue) {
+      outstandingProbes--;
+
       try {
         let value = aValue;
         if ("callback" in aProbe) {
           value = aProbe.callback(value);
         }
         probeValues[aProbe.histogram] = value;
         Services.telemetry.getHistogramById(aProbe.histogram)
                           .add(value);
       } catch (ex) {
         Components.utils.reportError(ex);
       }
+
+      if (!outstandingProbes && aHealthReportCallback) {
+        try {
+          aHealthReportCallback(probeValues);
+        } catch (ex) {
+          Components.utils.reportError(ex);
+        }
+      }
     }
 
     for (let i = 0; i < probes.length; i++) {
       let probe = probes[i];
- 
+
+      if (!isTelemetry && !probe.healthreport) {
+        continue;
+      }
+
+      outstandingProbes++;
+
       if (!("query" in probe)) {
-        reportTelemetry(probe);
+        reportResult(probe);
         continue;
       }
 
       let stmt = DBConn.createAsyncStatement(probe.query);
       for (param in params) {
         if (probe.query.indexOf(":" + param) > 0) {
           stmt.params[param] = params[param];
         }
       }
 
       try {
         stmt.executeAsync({
           handleError: PlacesDBUtils._handleError,
           handleResult: function (aResultSet) {
             let row = aResultSet.getNextRow();
-            reportTelemetry(probe, row.getResultByIndex(0));
+            reportResult(probe, row.getResultByIndex(0));
           },
           handleCompletion: function () {}
         });
       } finally{
         stmt.finalize();
       }
     }
 
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -123,10 +123,22 @@ add_task(function test_execute()
 
   for (let histogramId in histograms) {
     do_log_info("checking histogram " + histogramId);
     let validate = histograms[histogramId];
     let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
     validate(snapshot.sum);
     do_check_true(snapshot.counts.reduce(function(a, b) a + b) > 0);
   }
-  do_test_finished();
 });
+
+add_test(function test_healthreport_callback() {
+  PlacesDBUtils.telemetry(null, function onResult(data) {
+    do_check_neq(data, null);
+
+    do_check_eq(Object.keys(data).length, 2);
+    do_check_eq(data.PLACES_PAGES_COUNT, 1);
+    do_check_eq(data.PLACES_BOOKMARKS_COUNT, 1);
+
+    run_next_test();
+  });
+});
+
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2565,10 +2565,46 @@
     "n_buckets": 50,
     "description": "Time spent on an initially failed cert verification in libpix mode (ms)"
   },
   "SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_CLASSIC" : {
     "kind": "exponential",
     "high": "60000",
     "n_buckets": 50,
     "description": "Time spent on an initially failed cert  verification in classic mode (ms)"
+  },
+  "HEALTHREPORT_INIT_MS": {
+    "kind": "exponential",
+    "high": "20000",
+    "n_buckets": 15,
+    "description": "Time (ms) spent to initialize Firefox Health Report service."
+  },
+  "HEALTHREPORT_SHUTDOWN_DELAY_MS": {
+    "kind": "exponential",
+    "high": "20000",
+    "n_buckets": 15,
+    "description": "Time (ms) that Firefox Health Report delays application shutdown by."
+  },
+  "HEALTHREPORT_GENERATE_JSON_PAYLOAD_MS": {
+    "kind": "exponential",
+    "high": "30000",
+    "n_buckets": 20,
+    "description": "Time (ms) it takes to obtain and format a Health Report JSON payload."
+  },
+  "HEALTHREPORT_PAYLOAD_UNCOMPRESSED_BYTES": {
+    "kind": "linear",
+    "high": "2000000",
+    "n_buckets": 202,
+    "description": "Size (in bytes) of the raw Health Report payload."
+  },
+  "HEALTHREPORT_UPLOAD_MS": {
+    "kind": "exponential",
+    "high": "60000",
+    "n_buckets": 20,
+    "description": "Time (ms) it takes to upload the Health Report payload."
+  },
+  "HEALTHREPORT_SAVE_LAST_PAYLOAD_MS": {
+    "kind": "exponential",
+    "high": "10000",
+    "n_buckets": 10,
+    "description": "Time (ms) it takes to persist the last submitted Health Report payload to disk."
   }
 }
--- a/xulrunner/confvars.sh
+++ b/xulrunner/confvars.sh
@@ -7,13 +7,12 @@ MOZ_APP_NAME=xulrunner
 MOZ_APP_DISPLAYNAME=XULRunner
 MOZ_UPDATER=1
 MOZ_XULRUNNER=1
 MOZ_CHROME_FILE_FORMAT=omni
 MOZ_APP_VERSION=$MOZILLA_VERSION
 MOZ_PLACES=1
 MOZ_EXTENSIONS_DEFAULT=" gio"
 MOZ_URL_CLASSIFIER=1
-MOZ_SERVICES_AITC=1
 MOZ_SERVICES_COMMON=1
 MOZ_SERVICES_CRYPTO=1
 MOZ_SERVICES_METRICS=1
 MOZ_SERVICES_SYNC=1