diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 61af5ef..071b7df 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -922,22 +922,23 @@ pref("browser.taskbar.lists.frequent.enabled", true);
 pref("browser.taskbar.lists.recent.enabled", false);
 pref("browser.taskbar.lists.maxListItemCount", 7);
 pref("browser.taskbar.lists.tasks.enabled", true);
 pref("browser.taskbar.lists.refreshInSeconds", 120);
 #endif
 
 #ifdef MOZ_SERVICES_SYNC
 // The sync engines to use.
-pref("services.sync.registerEngines", "Bookmarks,Form,History,Password,Prefs,Tab");
+pref("services.sync.registerEngines", "Bookmarks,Form,History,Password,Prefs,Tab,Addons");
 // Preferences to be synced by default
 pref("services.sync.prefs.sync.accessibility.blockautorefresh", true);
 pref("services.sync.prefs.sync.accessibility.browsewithcaret", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
+pref("services.sync.prefs.sync.addons.ignoreRepositoryChecking", false);
 pref("services.sync.prefs.sync.app.update.mode", true);
 pref("services.sync.prefs.sync.browser.download.manager.closeWhenDone", true);
 pref("services.sync.prefs.sync.browser.download.manager.retention", true);
 pref("services.sync.prefs.sync.browser.download.manager.scanWhenDone", true);
 pref("services.sync.prefs.sync.browser.download.manager.showWhenStarting", true);
 pref("services.sync.prefs.sync.browser.formfill.enable", true);
 pref("services.sync.prefs.sync.browser.link.open_newwindow", true);
 pref("services.sync.prefs.sync.browser.offline-apps.notify", true);
diff --git a/browser/base/content/syncSetup.js b/browser/base/content/syncSetup.js
index 0be9c24..05ca444 100644
--- a/browser/base/content/syncSetup.js
+++ b/browser/base/content/syncSetup.js
@@ -577,17 +577,18 @@ var gSyncSetup = {
       this.completePairing();
     }
 
     if (!this._resettingSync) {
       function isChecked(element) {
         return document.getElementById(element).hasAttribute("checked");
       }
 
-      let prefs = ["engine.bookmarks", "engine.passwords", "engine.history", "engine.tabs", "engine.prefs"];
+      let prefs = ["engine.bookmarks", "engine.passwords", "engine.history",
+                   "engine.tabs", "engine.prefs", "engine.addons"];
       for (let i = 0;i < prefs.length;i++) {
         Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i]));
       }
       this._handleNoScript(false);
       if (Weave.Svc.Prefs.get("firstSync", "") == "notReady")
         Weave.Svc.Prefs.reset("firstSync");
 
       Weave.Service.persistLogin();
diff --git a/browser/base/content/syncSetup.xul b/browser/base/content/syncSetup.xul
index 551f412..e978a48 100644
--- a/browser/base/content/syncSetup.xul
+++ b/browser/base/content/syncSetup.xul
@@ -423,16 +423,20 @@
             <checkbox label="&engine.history.label;"
                       accesskey="&engine.history.accesskey;"
                       id="engine.history"
                       checked="true"/>
             <checkbox label="&engine.tabs.label;"
                       accesskey="&engine.tabs.accesskey;"
                       id="engine.tabs"
                       checked="true"/>
+            <checkbox label="&engine.addons.label;"
+                      accesskey="&engine.addons.accesskey;"
+                      id="engine.addons"
+                      checked="true"/>
           </vbox>
         </row>
       </rows>
     </grid>
     </groupbox>
 
     <groupbox id="mergeOptions">
       <radiogroup id="mergeChoiceRadio" pack="start">
diff --git a/browser/components/preferences/sync.xul b/browser/components/preferences/sync.xul
index 7fbbf8b..3e4ee0b 100644
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -57,16 +57,17 @@
             onpaneload="gSyncPane.init()">
 
     <preferences>
       <preference id="engine.bookmarks" name="services.sync.engine.bookmarks" type="bool"/>
       <preference id="engine.history"   name="services.sync.engine.history"   type="bool"/>
       <preference id="engine.tabs"      name="services.sync.engine.tabs"      type="bool"/>
       <preference id="engine.prefs"     name="services.sync.engine.prefs"     type="bool"/>
       <preference id="engine.passwords" name="services.sync.engine.passwords" type="bool"/>
+      <preference id="engine.addons"    name="services.sync.engine.addons" type="bool"/>
     </preferences>
 
 
     <script type="application/javascript"
             src="chrome://browser/content/preferences/sync.js"/>
     <script type="application/javascript"
             src="chrome://browser/content/syncUtils.js"/>
 
@@ -147,16 +148,21 @@
                             accesskey="&engine.history.accesskey;"
                             preference="engine.history"/>
                 </richlistitem>
                 <richlistitem>
                   <checkbox label="&engine.tabs.label;"
                             accesskey="&engine.tabs.accesskey;"
                             preference="engine.tabs"/>
                 </richlistitem>
+                <richlistitem>
+                  <checkbox label="&engine.addons.label;"
+                            accesskey="&engine.addons.accesskey;"
+                            preference="engine.addons"/>
+                </richlistitem>
               </richlistbox>
             </vbox>
           </groupbox>
 
           <groupbox class="syncGroupBox">
             <grid>
               <columns>
                 <column/>
diff --git a/browser/locales/en-US/chrome/browser/preferences/sync.dtd b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
index ad348c8..e56afeb 100644
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -25,16 +25,18 @@
 <!ENTITY engine.tabs.label          "Tabs">
 <!ENTITY engine.tabs.accesskey      "T">
 <!ENTITY engine.history.label       "History">
 <!ENTITY engine.history.accesskey   "r">
 <!ENTITY engine.passwords.label     "Passwords">
 <!ENTITY engine.passwords.accesskey "P">
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
+<!ENTITY engine.addons.label        "Add-ons">
+<!ENTITY engine.addons.accesskey    "A">
 
 <!-- Device Settings -->
 <!ENTITY syncComputerName.label       "Computer Name:">
 <!ENTITY syncComputerName.accesskey   "c">
 <!ENTITY unlinkDevice.label           "Unlink This Device">
 
 <!-- Footer stuff -->
 <!ENTITY prefs.tosLink.label        "Terms of Service">
diff --git a/browser/locales/en-US/chrome/browser/syncSetup.dtd b/browser/locales/en-US/chrome/browser/syncSetup.dtd
index f346bd0..cca142a 100644
--- a/browser/locales/en-US/chrome/browser/syncSetup.dtd
+++ b/browser/locales/en-US/chrome/browser/syncSetup.dtd
@@ -83,16 +83,18 @@
 <!ENTITY engine.tabs.label          "Tabs">
 <!ENTITY engine.tabs.accesskey      "T">
 <!ENTITY engine.history.label       "History">
 <!ENTITY engine.history.accesskey   "r">
 <!ENTITY engine.passwords.label     "Passwords">
 <!ENTITY engine.passwords.accesskey "P">
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
+<!ENTITY engine.addons.label        "Add-ons">
+<!ENTITY engine.addons.accesskey    "A">
 
 <!ENTITY choice2.merge.main.label      "Merge this computer's data with my &syncBrand.shortName.label; data">
 <!ENTITY choice2.merge.recommended.label "Recommended:">
 <!ENTITY choice2.client.main.label     "Replace all data on this computer with my &syncBrand.shortName.label; data">
 <!ENTITY choice2.server.main.label     "Replace all other devices with this computer's data">
 
 <!-- Confirm Merge Options -->
 <!ENTITY setup.optionsConfirmPage.title "Confirm">
diff --git a/services/sync/modules/addonsreconciler.js b/services/sync/modules/addonsreconciler.js
new file mode 100644
index 0000000..ea001e9
--- /dev/null
+++ b/services/sync/modules/addonsreconciler.js
@@ -0,0 +1,564 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Firefox Sync.
+ *
+ * The Initial Developer of the Original Code is
+ * the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Gregory Szorc <gps@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * This file contains middleware to reconcile state of AddonManager for
+ * purposes of tracking events for Sync. The content in this file exists
+ * because AddonManager does not have a getChangesSinceX() API and adding
+ * that functionality properly was deemed too time-consuming at the time
+ * add-on sync was originally written. If/when AddonManager adds this API,
+ * this file can go away and the add-ons engine can be rewritten to use it.
+ *
+ * It was decided to have this tracking functionality exist in a separate
+ * standalone file so it could be more easily understood, tested, and
+ * hopefully ported.
+ */
+
+"use strict";
+
+const Cu = Components.utils;
+
+Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const EXPORTED_SYMBOLS = ["AddonsReconciler"];
+
+const DEFAULT_STATE_FILE = "addonsreconciler";
+
+const CHANGE_INSTALLED = 1;
+const CHANGE_UNINSTALLED = 2;
+const CHANGE_ENABLED = 3;
+const CHANGE_DISABLED = 4;
+
+/**
+ * Maintains state of add-ons.
+ *
+ * The AddonsReconciler is installed as an AddonManager listener. When it
+ * receives change notifications, it updates its internal state database.
+ *
+ * The internal state is persisted to a JSON file in the profile directory.
+ *
+ * An instance of this is bound to an AddonsEngine instance. In reality, it
+ * likely exists as a singleton. To AddonsEngine, it functions as a store and
+ * an entity which emits events for tracking.
+ *
+ * The usage pattern for instances of this class is:
+ *
+ *   let reconciler = new AddonsReconciler();
+ *   reconciler.loadState(null, function(error) { ... });
+ *
+ *   // At this point, your instance should be ready to use.
+ *
+ * When you are finished with the instance, please call:
+ *
+ *   reconciler.stopListening();
+ *   reconciler.saveStateFile(...);
+ *
+ *
+ * --------------
+ *
+ * There are 2 classes of listeners in the AddonManager: AddonListener and
+ * InstallListener. The class is a listener for both (member functions just
+ * get called directly).
+ *
+ * When an add-on is installed, listeners are called in the following order:
+ *
+ *  IL.onInstallStarted, AL.onInstalling, IL.onInstallEnded, AL.onInstalled
+ *
+ * For non-restartless add-ons, an application restart may occur between
+ * IL.onInstallEnded and AL.onInstalled. Unfortunately, Sync likely will
+ * not be lodaded when AL.onInstalled is fired shortly after application
+ * start, so it won't see this event. Therefore, for add-ons requiring a
+ * restart, Sync treats the IL.onInstallEnded event as good enough to
+ * denote an install. For restartless add-ons, Sync assumes AL.onInstalled
+ * will follow shortly after IL.onInstallEnded and thus is ignores
+ * IL.onInstallEnded.
+ *
+ * For uninstalls, we see AL.onUninstalling then AL.onUninstalled. Like
+ * installs, the events could be separated by an application restart and Sync
+ * may not see the onUninstalled event. Again, if we require a restart, we
+ * react to onUninstalling. If not, we assume we'll get onUninstalled.
+ *
+ * Enabling and disabling work by sending:
+ *
+ *   AL.onEnabling, AL.onEnabled
+ *   AL.onDisabling, AL.onDisabled
+ *
+ * Again, they may be separated by a restart, so we heed the requiresRestart
+ * flag.
+ *
+ * Actions can be undone. All undoable actions notify the same
+ * AL.onOperationCancelled event. We treat this event like any other.
+ *
+ * Restartless add-ons have interesting behavior during uninstall. These
+ * add-ons are first disabled then they are actually uninstalled. So, the
+ * tracker will see onDisabling and onDisabled. The onUninstalling and
+ * onUninstalled events only come after the Addon Manager is closed or another
+ * view is switched to.
+ *
+ * To further complicate things, the tracker will see some notifications before
+ * changes hit the database. In the case of non-restartless add-ons,
+ * IL.onInstallEnded is the observed event and this is called *before* the
+ * add-on has hit the database. In the case of restartless add-ons,
+ * AL.onInstalled is fired after the database is populated. What this means
+ * is the tracker knows a particular ID has changed, but when a sync comes
+ * along and the store calls AddonManager.getAddonBySyncGUID(), that may fail
+ * to retrieve anything because the add-on hasn't been inserted into the
+ * database yet! If we were to do nothing, the sync engine would treat this
+ * as a deleted record and the install event would never make it to a record.
+ *
+ * Fortunately, the tracker knows what the state should be based on the events
+ * it has seen. The tracker maintains state on syncGUIDs that should eventually
+ * be in a certain state. During a sync, the tracker is consulted. The tracker
+ * can effectively override the AddonManager, saying "Don't trust data for this
+ * add-on. But, if the AddonManager and I agree, you can trust the
+ * AddonManager."
+ *
+ */
+function AddonsReconciler() {
+  this._log = Log4Moz.repository.getLogger("Sync.AddonsReconciler");
+  let level = Svc.Prefs.get("log.logger.addonsreconciler", "Debug");
+  this._log.level = Log4Moz.Level[level];
+
+  AddonManager.addAddonListener(this);
+  AddonManager.addInstallListener(this);
+  this._listening = true;
+
+  let us = this;
+  Svc.Obs.add("xpcom-shutdown", function() {
+    us.stopListening();
+  });
+};
+AddonsReconciler.prototype = {
+  /** Flag indicating whether we are listening to AddonManager events. */
+  _listening: false,
+
+  /** log4moz logger instance */
+  _log: null,
+
+  /**
+   * This is the main data structure for an instance.
+   *
+   * Keys are add-on IDs. Values are objects which describe the state of the
+   * add-on.
+   */
+  _addons: {},
+
+  /**
+   * List of add-on changes over time.
+   *
+   * Each element is an array of [time, change, id].
+   */
+  _changes: [],
+
+  _listeners: [],
+
+  get addons() {
+    return this._addons;
+  },
+
+  /**
+   * Loads reconciler state from a file.
+   *
+   * The path is relative to the weave directory in the profile. If no
+   * path is given, the default one is used.
+   *
+   * @param path
+   *        Path to load. ".json" is appended automatically.
+   * @param callback
+   *        Callback to be executed upon file load. The callback receives a
+   *        truthy error argument signifying whether an error occurred and a
+   *        boolean indicating whether data was loaded.
+   */
+  loadState: function loadState(path, callback) {
+    let file = path || DEFAULT_STATE_FILE;
+    Utils.jsonLoad(file, this, function(json) {
+      this._addons = {};
+      this._changes = [];
+
+      if (json != undefined) {
+        this._addons = json.addons;
+        for each (let record in this._addons) {
+          record.modified = new Date(record.modified);
+        }
+
+        for each (let change in json.changes) {
+          this._changes.push([new Date(change[0]), change[1], change[2]]);
+        }
+
+        if (callback) {
+          callback(false, true);
+        }
+      } else {
+        if (callback) {
+          callback(false, false);
+        }
+      }
+    });
+  },
+
+  /**
+   * Saves the state to a file in the local profile.
+   *
+   * @param  path
+   *         String path in profile to save to. If not defined, the default
+   *         will be used.
+   * @param  callback
+   *         Function to be invoked on save completion. No parameters will be
+   *         passed to callback.
+   */
+  saveState: function saveState(path, callback) {
+    let state = {addons: {}, changes: []};
+
+    for (let [id, record] in Iterator(this._addons)) {
+      state[id] = {};
+      for (let [k, v] in Iterator(record)) {
+        if (k == "modified") {
+          state[id][k] = v.getTime();
+        }
+        else {
+          state[id][k] = v;
+        }
+      }
+    }
+
+    for each (let change in this._changes) {
+      state.changes.push([change[0].getTime(), change[1], change[2]]);
+    }
+
+    Utils.jsonSave(path || DEFAULT_STATE_FILE, this, state, callback);
+  },
+
+  /**
+   * Registers a change listener with this instance.
+   *
+   * Change listeners are called every time a change is recorded. The listener
+   * should be a function that takes 3 arguments, the Date at which the change
+   * happened, the type of change (a CHANGE_* constant), and the add-on state
+   * object reflecting the current state of the add-on at the time of the
+   * change.
+   */
+  addChangeListener: function addChangeListener(listener) {
+    if (!this._listeners.some(function(i) { return i == listener; })) {
+      this._log.debug("Adding change listener.");
+      this._listeners.push(listener);
+    }
+  },
+
+  /**
+   * Removes a previously-installed change listener from the instance.
+   */
+  removeChangeListener: function removeChangeListener(listener) {
+    this._listeners = this._listeners.filter(function(element) {
+      if (element == listener) {
+        this._log.debug("Removing change listener.");
+        return false;
+      } else {
+        this._log.debug("Retaining change listener.");
+        return true;
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Tells the instance to stop listening for AddonManager changes.
+   *
+   * The reconciler should always be listening. This should only be called when
+   * the instance is being destroyed.
+   */
+  stopListening: function stopListening() {
+    if (this._listening) {
+      AddonManager.removeInstallListener(this);
+      AddonManager.removeAddonListener(this);
+      this._listening = false;
+    }
+  },
+
+  /**
+   * Refreshes the global state of add-ons by querying the AddonsManager.
+   */
+  refreshGlobalState: function refreshGlobalState(callback) {
+    this._log.info("Refreshing global state from AddonManager.");
+    AddonManager.getAllAddons(function (addons) {
+      let ids = {};
+
+      for each (let addon in addons) {
+        ids[addon.id] = true;
+        this.rectifyStateFromAddon(addon);
+      }
+
+      // Look for locally-defined add-ons that don't exist any more and update
+      // their record
+      for (let [id, addon] in Iterator(this._addons)) {
+        if (id in ids) {
+          continue;
+        }
+
+        // If the id isn't in ids, it means that the add-on has been deleted.
+        if (addon.installed) {
+          addon.installed = false;
+          this.addChange(new Date(), CHANGE_UNINSTALLED, addon);
+        }
+      }
+
+      this.saveState(null, callback);
+    }.bind(this));
+  },
+
+  /**
+   * Rectifies the state of an add-on from an add-on instance.
+   *
+   * @param addon
+   *        Addon instance being updated.
+   */
+  rectifyStateFromAddon: function rectifyStateFromAddon(addon) {
+    this._log.debug("Rectifying state for addon: " + addon.id);
+
+    let id = addon.id;
+    let enabled = !addon.userDisabled;
+    let guid = addon.syncGUID;
+    let now = new Date();
+
+    if (!(id in this._addons)) {
+      let record = {
+        id: id,
+        guid: guid,
+        enabled: enabled,
+        installed: true,
+        modified: now,
+        type: addon.type,
+        scope: addon.scope,
+        foreignInstall: addon.foreignInstall
+      };
+      this._addons[id] = record;
+      this.addChange(now, CHANGE_INSTALLED, record);
+      return;
+    }
+
+    let record = this._addons[id];
+
+    if (!record.installed) {
+      record.installed = true;
+      record.modified = now;
+    }
+
+    if (record.enabled != enabled) {
+      record.enabled = enabled;
+      record.modified = now;
+      let change = enabled ? CHANGE_ENABLED : CHANGE_DISABLED;
+      this.addChange(new Date(), change, record);
+    }
+
+    if (record.guid != guid) {
+      record.guid = guid;
+      // We don't record a change because the Sync engine rectifies this on its
+      // own.
+    }
+  },
+
+  addChange: function addChange(date, change, addon) {
+    this._log.info("Change recorded for " + addon.id);
+    this._changes.push([date, change, addon.id]);
+
+    for each (let listener in this._listeners) {
+      try {
+        listener.changeListener.call(listener, date, change, addon);
+      } catch (ex) {
+        this._log.warn("Exception calling change listener: " +
+                       Utils.exceptionStr(ex));
+      }
+    }
+  },
+
+  /**
+   * Obtain the set of changes to add-ons since the date passed.
+   *
+   * This will return an array of arrays. Each entry in the array has the
+   * elements [time, change_type, id], where
+   *
+   *   date - Date instance representing when the change occurred
+   *   change_type - One of CHANGE_* constants.
+   */
+  getChangesSinceDate: function getChangesSinceDate(date) {
+    let length = this._changes.length;
+    for (let i = 0; i < length; i++) {
+      let entry = this._changes[i];
+
+      if (entry[0] < date) {
+        continue;
+      }
+
+      return this._changes.slice(i);
+    }
+
+    return [];
+  },
+
+  /**
+   * Prunes all recorded changes from before the specified Date.
+   *
+   * @param date
+   *        Entries older than this Date will be removed.
+   */
+  pruneChangesBeforeDate: function pruneChangesBeforeDate(date) {
+    while (this._changes.length > 0) {
+      let entry = this._changes[0];
+
+      if (entry[0] < date) {
+        delete this._changes[0];
+      } else {
+        return;
+      }
+    }
+  },
+
+  /**
+   * Obtains the set of all known Sync GUIDs for add-ons.
+   *
+   * @return Object with guids as keys and values of true.
+   */
+  getAllSyncGUIDs: function getAllSyncGUIDs() {
+    let result = {};
+    for (let id in this._addons) {
+      result[id] = true;
+    }
+
+    return result;
+  },
+
+  /**
+   * Obtain the add-on state record for an add-on by Sync GUID.
+   *
+   * If the add-on could not be found, returns null.
+   *
+   * @param  guid
+   *         Sync GUID of add-on to retrieve.
+   * @return Object on success on null on failure.
+   */
+  getAddonStateFromSyncGUID: function getAddonStateFromSyncGUID(guid) {
+    for each (let addon in this._addons) {
+      if (addon.guid == guid) {
+        return addon;
+      }
+    }
+
+    return null;
+  },
+
+  /**
+   * Handler that is invoked as part of the AddonManager listeners.
+   */
+  _handleListener: function _handlerListener(action, addon, requiresRestart) {
+    // Since this is called as an observer, we explicitly trap errors and
+    // log them to ourselves so we don't see errors reported elsewhere.
+    try {
+      let id = addon.id;
+      this._log.debug("Add-on change: " + action + " to " + id);
+
+      // We assume that every event for non-restartless add-ons is
+      // followed by another event and that this follow-up event is the most
+      // appropriate to react to. Currently we ignore onEnabling, onDisabling,
+      // and onUninstalling for non-restartless add-ons.
+      if (requiresRestart === false) {
+        this._log.debug("Ignoring notification because restartless");
+        return;
+      }
+
+      switch (action) {
+        case "onEnabling":
+        case "onEnabled":
+        case "onDisabling":
+        case "onDisabled":
+        case "onInstalled":
+        case "onOperationCancelled":
+          this.rectifyStateFromAddon(addon);
+          break;
+
+        case "onUninstalling":
+        case "onUninstalled":
+          let id = addon.id;
+          if (id in this._addons) {
+            let now = new Date();
+            let record = this._addons[id];
+            record.installed = false;
+            record.modified = now;
+            this.addChange(now, CHANGE_UNINSTALLED, record);
+          }
+      }
+
+      this.saveState(null, null);
+    }
+    catch (ex) {
+      this._log.warn("Exception: " + Utils.exceptionStr(ex));
+    }
+  },
+
+  // AddonListeners
+  onEnabling: function onEnabling(addon, requiresRestart) {
+    this._handleListener("onEnabling", addon, requiresRestart);
+  },
+  onEnabled: function onEnabled(addon) {
+    this._handleListener("onEnabled", addon);
+  },
+  onDisabling: function onDisabling(addon, requiresRestart) {
+    this._handleListener("onDisabling", addon, requiresRestart);
+  },
+  onDisabled: function onDisabled(addon) {
+    this._handleListener("onDisabled", addon);
+  },
+  onInstalling: function onInstalling(addon, requiresRestart) {
+    this._handleListener("onInstalling", addon, requiresRestart);
+  },
+  onInstalled: function onInstalled(addon) {
+    this._handleListener("onInstalled", addon);
+  },
+  onUninstalling: function onUninstalling(addon, requiresRestart) {
+    this._handleListener("onUninstalling", addon, requiresRestart);
+  },
+  onUninstalled: function onUninstalled(addon) {
+    this._handleListener("onUninstalled", addon);
+  },
+  onOperationCancelled: function onOperationCancelled(addon) {
+    this._handleListener("onOperationCancelled", addon);
+  },
+
+  // InstallListeners
+  onInstallEnded: function onInstallEnded(install, addon) {
+    this._handleListener("onInstallEnded", addon);
+  }
+};
diff --git a/services/sync/modules/constants.js b/services/sync/modules/constants.js
index 273abcb..0df6a98 100644
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -103,16 +103,17 @@ MOBILE_BATCH_SIZE:                     50,
 DEFAULT_GUID_FETCH_BATCH_SIZE:         50,
 DEFAULT_MOBILE_GUID_FETCH_BATCH_SIZE:  50,
 
 // Default batch size for applying incoming records.
 DEFAULT_STORE_BATCH_SIZE:              1,
 HISTORY_STORE_BATCH_SIZE:              50, // same as MOBILE_BATCH_SIZE
 FORMS_STORE_BATCH_SIZE:                50, // same as MOBILE_BATCH_SIZE
 PASSWORDS_STORE_BATCH_SIZE:            50, // same as MOBILE_BATCH_SIZE
+ADDONS_STORE_BATCH_SIZE:               1000000, // process all addons at once
 
 // score thresholds for early syncs
 SINGLE_USER_THRESHOLD:                 1000,
 MULTI_DEVICE_THRESHOLD:                300,
 
 // Other score increment constants
 SCORE_INCREMENT_SMALL:                 1,
 SCORE_INCREMENT_MEDIUM:                10,
diff --git a/services/sync/modules/engines/addons.js b/services/sync/modules/engines/addons.js
new file mode 100644
index 0000000..3c170fd
--- /dev/null
+++ b/services/sync/modules/engines/addons.js
@@ -0,0 +1,755 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Firefox Sync.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Gregory Szorc <gps@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * This file defines the add-on sync functionality.
+ *
+ * There are currently a number of known limitations:
+ *  - We only sync XPI extensions and themes available from addons.mozilla.org.
+ *    We hope to expand support for other add-ons eventually.
+ *  - We only attempt syncing of add-ons between applications of the same type.
+ *    This means add-ons will not synchronize between Firefox desktop and
+ *    Firefox mobile, for example. This is because of significant add-on
+ *    incompatibility between application types.
+ *
+ * Add-on records exist for each known {add-on, app-id} pair in the Sync client
+ * set. Each record has a randomly chosen GUID. The records then contain
+ * basic metadata about the add-on.
+ *
+ * We currently synchronize:
+ *
+ *  - Installations
+ *  - Uninstallations
+ *  - User enabling and disabling
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://services-sync/addonsreconciler.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/record.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/async.js");
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/AddonRepository.jsm");
+
+const EXPORTED_SYMBOLS = ["AddonsEngine"];
+
+const ADDON_REPOSITORY_WHITELIST_HOSTNAME = "addons.mozilla.org";
+
+// 7 days in milliseconds.
+const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
+
+/**
+ * AddonRecord represents the state of an add-on in an application.
+ *
+ * Each add-on has its own record for each application ID it is installed
+ * on.
+ *
+ * The ID of add-on records is a randomly-generated GUID. It is random instead
+ * of deterministic so the URIs of the records cannot be guessed and so
+ * compromised server credentials won't result in disclosure of the specific
+ * add-ons present in a Sync account.
+ *
+ * The record contains the following fields:
+ *
+ *  addonID
+ *    ID of the add-on. This correlates to the "id" property on an Addon type.
+ *
+ *  applicationID
+ *    The application ID this record is associated with. Clients currently
+ *    ignore records from other application IDs.
+ *
+ *  enabled
+ *    Boolean stating whether add-on is enabled or disabled by the user.
+ *
+ *  source
+ *    String indicating where an add-on is from. Currently, we only support
+ *    the value "amo" which indicates that the add-on came from the official
+ *    add-ons repository, addons.mozilla.org. In the future, we may support
+ *    installing add-ons from other sources. This provides a future-compatible
+ *    mechanism for clients to only apply records they know how to handle.
+ */
+function AddonRecord(collection, id) {
+  CryptoWrapper.call(this, collection, id);
+}
+AddonRecord.prototype = {
+  __proto__: CryptoWrapper.prototype,
+  _logName: "Record.Addon"
+};
+
+Utils.deferGetSet(AddonRecord, "cleartext", ["addonID",
+                                             "applicationID",
+                                             "enabled",
+                                             "source"]);
+
+/**
+ * The AddonsEngine handles synchronization of add-ons between clients.
+ *
+ * The engine handles incoming add-ons in one large batch, as it needs
+ * to assess the overall state at one time.
+ *
+ * The engine fires the following notifications (all prefixed with
+ * "weave:engine:addons:"):
+ *
+ *   restart-required  Fired at the tail end of performing a sync when an
+ *                     an application restart is required to finish add-on
+ *                     processing. The observer receives an array of add-on IDs
+ *                     that require restart. Observers should likely wait until
+ *                     after the sync is done (signified by reception of the
+ *                     "weave:service:sync:finish" event) to actually restart
+ *                     or give the user an opportunity to restart.
+ */
+function AddonsEngine() {
+  SyncEngine.call(this, "Addons");
+
+  this._reconciler = new AddonsReconciler();
+}
+AddonsEngine.prototype = {
+  __proto__:              SyncEngine.prototype,
+  _storeObj:              AddonsStore,
+  _trackerObj:            AddonsTracker,
+  _recordObj:             AddonRecord,
+  version:                1,
+
+  _reconciler:            null,
+  _reconcilerStateLoaded: false,
+
+  /**
+   * Override parent method to find add-ons by ID, not Sync GUID.
+   */
+  _findDupe: function _findDupe(item) {
+    let id = item.addonID;
+
+    let addons = this._reconciler.addons;
+    if (!(id in addons)) {
+      return null;
+    }
+
+    let addon = addons[id];
+    if (addon.guid != item.id) {
+      return addon.guid;
+    }
+
+    return null;
+  },
+
+  /**
+   * We override getChangedIDs to pull in tracker changes plus changes from the
+   * reconciler log.
+   */
+  getChangedIDs: function getChangedIDs() {
+    let changes = {};
+    for (let [id, modified] in Iterator(this._tracker.changedIDs)) {
+      changes[id] = modified;
+    }
+
+    let lastSyncDate = new Date(this.lastSync * 1000);
+    let reconcilerChanges = this._reconciler.getChangesSinceDate(lastSyncDate);
+    let addons = this._reconciler.addons;
+    for each (let change in reconcilerChanges) {
+      let changeTime = change[0];
+      let id = change[2];
+
+      if (!(id in addons)) {
+        continue;
+      }
+
+      // Keep newest modified time.
+      if (id in changes && changeTime < changes[id]) {
+          continue;
+      }
+
+      this._log.debug("Adding changed add-on from changes log: " + id);
+      let addon = addons[id];
+      changes[addon.guid] = changeTime.getTime() / 1000;
+    }
+
+    return changes;
+  },
+
+  /**
+   * Override start of sync function to refresh add-on global state and pull
+   * in any missing changes.
+   */
+  _syncStartup: function _syncStartup() {
+    // We refresh state before calling parent because syncStartup in the parent
+    // looks for changed IDs, which is dependent on add-on state being up to
+    // date.
+    this._refreshReconcilerState();
+
+    SyncEngine.prototype._syncStartup.call(this);
+  },
+
+  /**
+   * Override end of sync to perform a little housekeeping on the reconciler.
+   */
+  _syncCleanup: function _syncCleanup() {
+    let ms = 1000 * this.lastSync - PRUNE_ADDON_CHANGES_THRESHOLD;
+    this._reconciler.pruneChangesBeforeDate(new Date(ms));
+
+    SyncEngine.prototype._syncCleanup.call(this);
+  },
+
+  /**
+   * Helper function to ensure reconciler is up to date.
+   */
+  _refreshReconcilerState: function _refreshReconcilerState() {
+    this._log.debug("Refreshing reconciler state");
+    if (!this._reconcilerStateLoaded) {
+      let cb = Async.makeSpinningCallback();
+      this._reconciler.loadState(null, cb);
+      cb.wait();
+      this._reconcilerStateLoaded = true;
+    }
+
+    let cb = Async.makeSpinningCallback();
+    this._reconciler.refreshGlobalState(cb);
+    cb.wait();
+  }
+};
+
+/**
+ * This is the primary interface between Sync and the Addons Manager.
+ */
+function AddonsStore(name) {
+  Store.call(this, name);
+}
+AddonsStore.prototype = {
+  __proto__: Store.prototype,
+
+  // Define the add-on types (.type) that we support.
+  _syncableTypes: ["extension", "theme"],
+
+  get reconciler() {
+    return this.engine._reconciler;
+  },
+
+  get engine() {
+    return Engines.get("addons");
+  },
+
+  /**
+   * Override applyIncoming to filter out records we can't handle.
+   */
+  applyIncoming: function applyIncoming(record) {
+    // Ignore records not belonging to our application ID because that is the
+    // current policy.
+    if (record.applicationID != Services.appinfo.ID) {
+      this._log.info("Ignoring incoming record from other App ID: " +
+                      record.id);
+      return;
+    }
+
+    // Ignore records that aren't from the official add-on repository, as that
+    // is our current policy.
+    if (record.source != "amo") {
+      this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
+                     " for " + record.id);
+      return;
+    }
+
+    Store.prototype.applyIncoming.call(this, record);
+  },
+
+
+  /**
+   * Provides core Store API to create/install an add-on from a record.
+   */
+  create: function create(record) {
+    let cb = Async.makeSpinningCallback();
+    this.installAddonsFromIDs([record.addonID], cb);
+
+    // This will throw if there was an error. This will get caught by the sync
+    // engine and the record will try to be applied later.
+    cb.wait();
+
+    this._log.info("Add-on installed: " + record.addonID);
+    let addon = this.getAddonByID(record.addonID);
+    if (addon) {
+      this._log.info("Setting add-on Sync GUID to remote: " + record.id);
+      addon.syncGUID = record.id;
+    }
+  },
+
+  /**
+   * Provides core Store API to remove/uninstall an add-on from a record.
+   */
+  remove: function remove(record) {
+    let addon = this.getAddonByID(record.addonID);
+    if (!addon) {
+      return;
+    }
+
+    this._log.debug("Uninstalling add-on: " + addon.id);
+    let cb = Async.makeSpinningCallback();
+    this.uninstallAddon(addon, cb);
+    cb.wait();
+  },
+
+  update: function update(record) {
+    let addon = this.getAddonByID(record.addonID);
+    if (!addon) {
+      // TODO log error?
+      return;
+    }
+
+    if (record.enabled == addon.userDisabled) {
+      this._log.info("Updating userEnabled flag: " + addon.id);
+
+      addon.userDisabled = !record.enabled;
+    }
+  },
+
+  itemExists: function itemExists(guid) {
+    let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
+
+    if (!addon) {
+      return false;
+    }
+
+    return !!addon.installed;
+  },
+
+  /**
+   * Create an add-on record from its GUID.
+   *
+   * @param guid
+   *        Add-on GUID (from extensions DB)
+   * @param collection
+   *        Collection to add record to.
+   *
+   * @return AddonRecord instance
+   */
+  createRecord: function createRecord(guid, collection) {
+    let record = new AddonRecord(collection, guid);
+    record.applicationID = Services.appinfo.ID;
+
+    let addon = this.reconciler.getAddonStateFromSyncGUID(guid);
+
+    // If we don't know about this GUID or if it has been uninstalled, we mark
+    // the record as deleted.
+    if (!addon || !addon.installed) {
+      record.deleted = true;
+      return record;
+    }
+
+    record.modified = addon.modified.getTime() / 1000;
+
+    record.addonID = addon.id;
+    record.enabled = addon.enabled;
+
+    // This needs to be dynamic when add-ons don't come from AddonRepository.
+    record.source = "amo";
+
+    return record;
+  },
+
+  /**
+   * Changes the id of an add-on.
+   *
+   * This implements a core API of the store.
+   */
+  changeItemID: function changeItemID(oldID, newID) {
+    let addon = this.getAddonByGUID(oldID);
+    if (!addon) {
+      this._log.debug("Cannot change item ID because old add-on not present: " +
+                      oldID);
+      return;
+    }
+
+    addon.syncGUID = newID;
+
+    let state = this.reconciler.addons[addon.id];
+    if (state) {
+      state.guid = newID;
+    }
+  },
+
+  /**
+   * Obtain the set of all syncable add-on Sync GUIDs.
+   *
+   * This implements a core Store API.
+   */
+  getAllIDs: function getAllIDs() {
+    let ids = {};
+
+    let addons = this.reconciler.addons;
+    for each (let addon in addons) {
+      if (this.isAddonSyncable(addon)) {
+        ids[addon.guid] = true;
+      }
+    }
+
+    return ids;
+  },
+
+  /**
+   * Wipe engine data.
+   *
+   * This uninstalls all syncable addons from the application. In case of
+   * error, it logs the error and keeps trying with other add-ons.
+   */
+  wipe: function wipe() {
+    this._log.info("Processing wipe.");
+
+    // TODO should this wipe *all* add-ons or just the syncable ones?
+    this.engine._refreshReconcilerState();
+
+    for (let id in this.getAllIDs()) {
+      let addon = this.getAddonByID(id);
+      if (!addon) {
+        this._log.debug("Ignoring add-on because it couldn't be obtained: " +
+                        id);
+        continue;
+      }
+
+      this._log.info("Uninstalling add-on as part of wipe: " + addon.id);
+      Utils.catch(addon.uninstall)();
+    }
+  },
+
+  /***************************************************************************
+   * Functions below are unique to this store and not part of the Store API  *
+   ***************************************************************************/
+
+  /**
+   * Obtain an add-on from its database ID
+   *
+   * @param id
+   *        Add-on ID
+   * @return Addon or undefined if not found
+   */
+  getAddonByID: function getAddonByID(id) {
+    let cb = Async.makeSyncCallback();
+    AddonManager.getAddonByID(id, cb);
+    return Async.waitForSyncCallback(cb);
+  },
+
+  /**
+   * Obtain an add-on from its database/Sync GUID
+   *
+   * @param  guid
+   *         Add-on Sync GUID
+   * @return DBAddonInternal or null
+   */
+  getAddonByGUID: function getAddonByGUID(guid) {
+    let cb = Async.makeSyncCallback();
+    AddonManager.getAddonBySyncGUID(guid, cb);
+    return Async.waitForSyncCallback(cb);
+  },
+
+  /**
+   * Determines whether an add-on is suitable for Sync.
+   *
+   * @param  addon
+   *         Addon instance
+   * @return Boolean indicating whether it is appropriate for Sync
+   */
+  isAddonSyncable: function isAddonSyncable(addon) {
+    // Currently, we limit syncable add-ons to those that:
+    //   1) In a well-defined set of types
+    //   2) Installed in current profile
+    //   3) Not installed by a foreign entity (i.e. installed by the app)
+    //      since they act like global extensions.
+    //   4) Are installed from AMO
+    let syncable = addon &&
+                   this._syncableTypes.indexOf(addon.type) != -1 &&
+                   addon.scope | AddonManager.SCOPE_PROFILE &&
+                   !addon.foreignInstall;
+
+    // We provide a back door to skip the repository checking of an add-on.
+    // This is utilized by the tests to make testing easier.
+    if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
+      return syncable;
+    }
+
+    let cb = Async.makeSyncCallback();
+    AddonRepository.getCachedAddonByID(addon.id, cb);
+    let result = Async.waitForSyncCallback(cb);
+
+    return result && result.sourceURI &&
+           result.sourceURI.host == ADDON_REPOSITORY_WHITELIST_HOSTNAME;
+  },
+
+  /**
+   * Obtain an AddonInstall object from an AddonSearchResult instance.
+   *
+   * The callback will be invoked with the result of the operation. The
+   * callback receives 2 arguments, error and result. Error will be falsey
+   * on success or some kind of error value otherwise. The result argument
+   * will be an AddonInstall on success or null on failure. It is possible
+   * for the error to be falsey but result to be null. This could happen if
+   * an install was not found.
+   *
+   * @param addon
+   *        AddonSearchResult to obtain install from.
+   * @param cb
+   *        Function to be called with result of operation.
+   */
+  getInstallFromSearchResult: function getInstallFromSearchResult(addon, cb) {
+    if (addon.install) {
+      cb(null, addon.install);
+      return;
+    }
+
+    this._log.debug("Manually obtaining install for " + addon.id);
+
+    // TODO do we need extra verification on sourceURI source?
+    AddonManager.getInstallForURL(
+      addon.sourceURI.spec,
+      function handleInstall(install) {
+        cb(null, install);
+      },
+      "application/x-xpinstall",
+      undefined,
+      addon.name,
+      addon.iconURL,
+      addon.version
+    );
+  },
+
+  /**
+   * Installs an add-on from an AddonSearchResult instance.
+   *
+   * When complete it calls a callback with 2 arguments, error and result.
+   *
+   * If error is falesy, result is an object. If error is truthy, result is
+   * null.
+   *
+   * The result object has the following keys:
+   *   id               ID of add-on that was installed.
+   *   requiresRestart  Boolean indicating whether install requires restart.
+   *
+   * @param addon
+   *        AddonSearchResult to install add-on from.
+   * @param cb
+   *        Function to be invoked with result of operation.
+   */
+  installAddonFromSearchResult:
+    function installAddonFromSearchResult(addon, cb) {
+    this._log.info("Trying to install add-on from search result: " + addon.id);
+
+    this.getInstallFromSearchResult(addon, function(error, install) {
+      if (error) {
+        cb(error, null);
+        return;
+      }
+
+      if (!install) {
+        cb("AddonInstall not available: " + addon.id, null);
+        return;
+      }
+
+      try {
+        this._log.info("Installing " + addon.id);
+
+        let restart = addon.operationRequiringRestart &
+          AddonManager.OP_NEEDS_RESTART_INSTALL;
+
+        let listener = {
+          onInstallEnded: function(install, addon) {
+            install.removeListener(listener);
+
+            cb(null, {id: addon.id, requiresRestart: restart});
+          },
+          onInstallFailed: function(install) {
+            install.removeListener(listener);
+
+            cb("Install failed: " + install.error, null);
+          },
+          onDownloadFailed: function(install) {
+            install.removeListener(listener);
+
+            cb("Download failed: " + install.error, null);
+          }
+        };
+        install.addListener(listener);
+        install.install();
+      }
+      catch (ex) {
+        this._log.error("Error installing add-on: " + Utils.exceptionstr(ex));
+        cb(ex, null);
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Uninstalls the Addon instance and invoke a callback when it is done.
+   *
+   * @param addon
+   *        Addon instance ot uninstall.
+   * @param callback
+   *        Function to be invoked when uninstall has finished. It receives a
+   *        truthy value signifying error and the add-on which was uninstalled.
+   */
+  uninstallAddon: function uninstallAddon(addon, callback) {
+    let listener = {
+      onUninstalled: function(uninstalled) {
+        if (addon.id == uninstalled.id) {
+          AddonManager.removeAddonListener(listener);
+          callback(false, addon);
+        }
+      }
+    };
+    AddonManager.addAddonListener(listener);
+    addon.uninstall();
+  },
+
+  /**
+   * Installs multiple add-ons specified by their IDs.
+   *
+   * The callback will be called when activity on all add-ons is complete. The
+   * callback receives 2 arguments, error and result.
+   *
+   * If error is truthy, it contains a string describing the overall error.
+   *
+   * result is always an object with details on the overall execution state. It
+   * contains the following keys.
+   *
+   *   installed  Array of add-on IDs that were installed
+   *   errors     Array of errors encountered. Only has elements if error is
+   *              truthy.
+   *
+   * @param ids
+   *        Array of add-on string IDs to install.
+   * @param cb
+   *        Function to be called when all actions are complete.
+   */
+  installAddonsFromIDs: function installAddonsFromIDs(ids, cb) {
+    AddonRepository.getAddonsByIDs(ids, {
+      searchSucceeded: function searchSucceeded(addons, addonsLength, total) {
+        this._log.info("Found " + addonsLength + "/" + ids.length +
+                       " add-ons during repository search.");
+
+        let ourResult = {
+          installed: [],
+          errors:    [],
+        };
+
+        if (!addonsLength) {
+          cb(null, ourResult);
+          return;
+        }
+
+        let finishedCount = 0;
+        let installCallback = function installCallback(error, result) {
+          finishedCount++;
+
+          if (error) {
+            ourResult.errors.push(error);
+          } else {
+            ourResult.installed.push(result.id);
+          }
+
+          if (finishedCount >= addonsLength) {
+            if (ourResult.errors.length > 0) {
+              cb("1 or more add-ons failed to install", ourResult);
+            } else {
+              cb(null, ourResult);
+            }
+          }
+        }.bind(this);
+
+        for (let i = 0; i < addonsLength; i++) {
+          this.installAddonFromSearchResult(addons[i], installCallback);
+        }
+
+      }.bind(this),
+
+      searchFailed: function searchFailed() {
+        cb("AddonRepository search failed", null);
+      }.bind(this)
+    });
+  }
+};
+
+function AddonsTracker(name) {
+  Tracker.call(this, name);
+
+  Svc.Obs.add("weave:engine:start-tracking", this);
+  Svc.Obs.add("weave:engine:stop-tracking", this);
+}
+AddonsTracker.prototype = {
+  __proto__: Tracker.prototype,
+
+  get reconciler() {
+    return Engines.get("addons")._reconciler;
+  },
+
+  get store() {
+    return Engines.get("addons")._store;
+  },
+
+  /**
+   * This callback is executed whenever the AddonsReconciler sends out a change
+   * notification. See AddonsReconciler.addChangeListener().
+   */
+  changeListener: function changeHandler(date, change, addon) {
+    this._log.debug("changeListener invoked: " + change + " " + addon.id);
+    // Ignore changes that occur during sync.
+    if (this.ignoreAll) {
+      return;
+    }
+
+    if (!this.store.isAddonSyncable(addon)) {
+      this._log.debug("Ignoring change because add-on isn't syncable: " +
+                      addon.id);
+      return;
+    }
+
+    this.addChangedID(addon.guid, date.getTime() / 1000);
+    this.score += SCORE_INCREMENT_XLARGE;
+  },
+
+  observe: function(subject, topic, data) {
+    switch (topic) {
+      case "weave:engine:start-tracking":
+        this.reconciler.addChangeListener(this);
+        break;
+
+      case "weave:engine:stop-tracking":
+        this.reconciler.removeChangeListener(this);
+        break;
+    }
+  }
+};
diff --git a/services/sync/modules/main.js b/services/sync/modules/main.js
index 23bbe33..f35addd 100644
--- a/services/sync/modules/main.js
+++ b/services/sync/modules/main.js
@@ -37,16 +37,17 @@
 
 const EXPORTED_SYMBOLS = ['Weave'];
 
 let Weave = {};
 Components.utils.import("resource://services-sync/constants.js", Weave);
 let lazies = {
   "record.js":            ["CollectionKeys", "BulkKeyBundle", "SyncKeyBundle"],
   "engines.js":           ['Engines', 'Engine', 'SyncEngine', 'Store'],
+  "engines/addons.js":    ["AddonsEngine"],
   "engines/bookmarks.js": ['BookmarksEngine', 'BookmarksSharingManager'],
   "engines/clients.js":   ["Clients"],
   "engines/forms.js":     ["FormEngine"],
   "engines/history.js":   ["HistoryEngine"],
   "engines/prefs.js":     ["PrefsEngine"],
   "engines/passwords.js": ["PasswordEngine"],
   "engines/tabs.js":      ["TabEngine"],
   "identity.js":          ["Identity", "ID"],
diff --git a/services/sync/modules/util.js b/services/sync/modules/util.js
index 6b92bee..bd81249 100644
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -189,16 +189,20 @@ let Utils = {
 
   /**
    * Encode byte string as base64url (RFC 4648).
    */
   encodeBase64url: function encodeBase64url(bytes) {
     return btoa(bytes).replace('+', '-', 'g').replace('/', '_', 'g');
   },
 
+  decodeBase64url: function decodeBase64url(data) {
+    return atob(data.replace('-', '+', 'g').replace('_', '/', 'g'));
+  },
+
   /**
    * GUIDs are 9 random bytes encoded with base64url (RFC 4648).
    * That makes them 12 characters long with 72 bits of entropy.
    */
   makeGUID: function makeGUID() {
     return Utils.encodeBase64url(Utils.generateRandomBytes(9));
   },
 
diff --git a/services/sync/services-sync.js b/services/sync/services-sync.js
index 56e715f..400ce62 100644
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -12,16 +12,17 @@ pref("services.sync.sendVersionInfo", true);
 pref("services.sync.scheduler.singleDeviceInterval", 86400); // 1 day
 pref("services.sync.scheduler.idleInterval",         3600);  // 1 hour
 pref("services.sync.scheduler.activeInterval",       600);   // 10 minutes
 pref("services.sync.scheduler.immediateInterval",    90);    // 1.5 minutes
 pref("services.sync.scheduler.idleTime",             300);   // 5 minutes
 
 pref("services.sync.errorhandler.networkFailureReportTimeout", 604800); // 1 week
 
+pref("services.sync.engine.addons", true);
 pref("services.sync.engine.bookmarks", true);
 pref("services.sync.engine.history", true);
 pref("services.sync.engine.passwords", true);
 pref("services.sync.engine.prefs", true);
 pref("services.sync.engine.tabs", true);
 pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$");
 
 pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
@@ -44,9 +45,10 @@ pref("services.sync.log.logger.network.resources", "Debug");
 pref("services.sync.log.logger.service.jpakeclient", "Debug");
 pref("services.sync.log.logger.engine.bookmarks", "Debug");
 pref("services.sync.log.logger.engine.clients", "Debug");
 pref("services.sync.log.logger.engine.forms", "Debug");
 pref("services.sync.log.logger.engine.history", "Debug");
 pref("services.sync.log.logger.engine.passwords", "Debug");
 pref("services.sync.log.logger.engine.prefs", "Debug");
 pref("services.sync.log.logger.engine.tabs", "Debug");
+pref("services.sync.log.logger.engine.addons", "Debug");
 pref("services.sync.log.cryptoDebug", false);
diff --git a/services/sync/tests/tps/all_tests.json b/services/sync/tests/tps/all_tests.json
index 43f4bed..35bcd34 100644
--- a/services/sync/tests/tps/all_tests.json
+++ b/services/sync/tests/tps/all_tests.json
@@ -16,13 +16,16 @@
     "test_bug575423.js",
     "test_bug546807.js",
     "test_history_collision.js",
     "test_privbrw_formdata.js",
     "test_privbrw_passwords.js",
     "test_privbrw_tabs.js",
     "test_bookmarks_in_same_named_folder.js",
     "test_client_wipe.js",
-    "test_special_tabs.js"
+    "test_special_tabs.js",
+    "test_addon_sanity.js",
+    "test_addon_restartless_xpi.js",
+    "test_addon_nonrestartless_xpi.js"
   ]
 }
 
 
diff --git a/services/sync/tests/tps/restartless-xpi.xml b/services/sync/tests/tps/restartless-xpi.xml
new file mode 100644
index 0000000..a6be2ec
--- /dev/null
+++ b/services/sync/tests/tps/restartless-xpi.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5617">
+  <name>Restartless Test XPI</name>
+  <type id="1">Extension</type>
+  <guid>restartless-xpi@tests.mozilla.org</guid>
+  <slug>restartless-xpi</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="485">http://127.0.0.1:4567/restartless.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
diff --git a/services/sync/tests/tps/restartless.xml b/services/sync/tests/tps/restartless.xml
deleted file mode 100644
index a6be2ec..0000000
--- a/services/sync/tests/tps/restartless.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<searchresults total_results="1">
-  <addon id="5617">
-  <name>Restartless Test XPI</name>
-  <type id="1">Extension</type>
-  <guid>restartless-xpi@tests.mozilla.org</guid>
-  <slug>restartless-xpi</slug>
-  <version>1.0</version>
-
-  <compatible_applications><application>
-      <name>Firefox</name>
-      <application_id>1</application_id>
-      <min_version>3.6</min_version>
-      <max_version>*</max_version>
-      <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
-    </application></compatible_applications>
-  <all_compatible_os><os>ALL</os></all_compatible_os>
-
-  <install os="ALL" size="485">http://127.0.0.1:4567/restartless.xpi</install>
-    <created epoch="1252903662">
-      2009-09-14T04:47:42Z
-    </created>
-    <last_updated epoch="1315255329">
-      2011-09-05T20:42:09Z
-    </last_updated>
-    </addon>
-</searchresults>
diff --git a/services/sync/tests/tps/test_addon_nonrestartless_xpi.js b/services/sync/tests/tps/test_addon_nonrestartless_xpi.js
new file mode 100644
index 0000000..d5d8dff
--- /dev/null
+++ b/services/sync/tests/tps/test_addon_nonrestartless_xpi.js
@@ -0,0 +1,91 @@
+
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that install of extensions that require restart
+// syncs between profiles.
+
+let phases = {
+  "phase01": "profile1",
+  "phase02": "profile1",
+  "phase03": "profile2",
+  "phase04": "profile2",
+  "phase05": "profile1",
+  "phase06": "profile1",
+  "phase07": "profile2",
+  "phase08": "profile2",
+  "phase09": "profile1",
+  "phase10": "profile1",
+  "phase11": "profile2",
+  "phase12": "profile2",
+  "phase13": "profile1",
+  "phase14": "profile1",
+  "phase15": "profile2",
+  "phase16": "profile2"
+};
+
+const id = "unsigned-xpi@tests.mozilla.org";
+
+Phase("phase01", [
+  [Sync, SYNC_WIPE_SERVER],
+  [Addons.verifyNot, [id]],
+  [Addons.install, ["unsigned-xpi.xml"]],
+  [Addons.verify, [id], STATE_DISABLED],
+]);
+Phase("phase02", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync],
+]);
+Phase("phase03", [
+  [Addons.verifyNot, [id]],
+  [Sync],
+]);
+Phase("phase04", [
+  [Addons.verify, [id], STATE_ENABLED],
+]);
+
+// Now we disable the add-on
+Phase("phase05", [
+  [Addons.setState, [id], STATE_DISABLED]
+]);
+Phase("phase06", [
+  [Addons.verify, [id], STATE_DISABLED],
+  [Sync]
+]);
+Phase("phase07", [
+  [Sync]
+]);
+Phase("phase08", [
+  [Addons.verify, [id], STATE_DISABLED]
+]);
+
+// Now we re-enable it again.
+Phase("phase09", [
+  [Addons.setState, [id], STATE_ENABLED]
+]);
+Phase("phase10", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync]
+]);
+Phase("phase11", [
+  [Sync]
+]);
+Phase("phase12", [
+  [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// And we uninstall it
+Phase("phase13", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Addons.uninstall, [id]]
+]);
+Phase("phase14", [
+  [Addons.verifyNot, [id]],
+  [Sync]
+]);
+Phase("phase15", [
+  [Sync]
+]);
+Phase("phase16", [
+  [Addons.verifyNot, [id]]
+]);
diff --git a/services/sync/tests/tps/test_addon_restartless_xpi.js b/services/sync/tests/tps/test_addon_restartless_xpi.js
new file mode 100644
index 0000000..3e9b6a8
--- /dev/null
+++ b/services/sync/tests/tps/test_addon_restartless_xpi.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test verifies that install of restartless extensions syncs to
+// other profiles.
+let phases = {
+  "phase01": "profile1",
+  "phase02": "profile2",
+  "phase03": "profile1",
+  "phase04": "profile2",
+  "phase05": "profile1",
+  "phase06": "profile2",
+  "phase07": "profile1",
+  "phase08": "profile2"
+};
+
+const id = "restartless-xpi@tests.mozilla.org";
+
+// Verify install is synced
+Phase("phase01", [
+  [Addons.verifyNot, [id]],
+  [Addons.install, ["restartless-xpi.xml"]],
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync, SYNC_WIPE_SERVER]
+]);
+Phase("phase02", [
+  [Addons.verifyNot, [id]],
+  [Sync],
+  [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// Now disable and see that is is synced.
+Phase("phase03", [
+  [Addons.setState, [id], STATE_DISABLED],
+  [Addons.verify, [id], STATE_DISABLED],
+  [Sync]
+]);
+Phase("phase04", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync],
+  [Addons.verify, [id], STATE_DISABLED]
+]);
+
+// Enable and see it is synced.
+Phase("phase05", [
+  [Addons.setState, [id], STATE_ENABLED],
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync]
+]);
+Phase("phase06", [
+  [Sync],
+  [Addons.verify, [id], STATE_ENABLED]
+]);
+
+// Uninstall and see it is synced.
+Phase("phase07", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Addons.uninstall, [id]],
+  [Addons.verifyNot, [id]],
+  [Sync]
+]);
+Phase("phase08", [
+  [Addons.verify, [id], STATE_ENABLED],
+  [Sync],
+  [Addons.verifyNot, [id]]
+]);
diff --git a/services/sync/tests/tps/unsigned-1.0.xml b/services/sync/tests/tps/unsigned-1.0.xml
deleted file mode 100644
index e311f47..0000000
--- a/services/sync/tests/tps/unsigned-1.0.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<searchresults total_results="1">
-  <addon id="5612">
-  <name>Unsigned Test XPI</name>
-  <type id="1">Extension</type>
-  <guid>unsigned-xpi@tests.mozilla.org</guid>
-  <slug>unsigned-xpi</slug>
-  <version>1.0</version>
-
-  <compatible_applications><application>
-      <name>Firefox</name>
-      <application_id>1</application_id>
-      <min_version>3.6</min_version>
-      <max_version>*</max_version>
-      <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
-    </application></compatible_applications>
-  <all_compatible_os><os>ALL</os></all_compatible_os>
-
-  <install os="ALL" size="452">http://127.0.0.1:4567/unsigned-1.0.xpi</install>
-    <created epoch="1252903662">
-      2009-09-14T04:47:42Z
-    </created>
-    <last_updated epoch="1315255329">
-      2011-09-05T20:42:09Z
-    </last_updated>
-    </addon>
-</searchresults>
\ No newline at end of file
diff --git a/services/sync/tests/tps/unsigned-1.0.xpi b/services/sync/tests/tps/unsigned-1.0.xpi
deleted file mode 100644
index 51b0047..0000000
Binary files a/services/sync/tests/tps/unsigned-1.0.xpi and /dev/null differ
diff --git a/services/sync/tests/tps/unsigned-xpi.xml b/services/sync/tests/tps/unsigned-xpi.xml
new file mode 100644
index 0000000..614927e
--- /dev/null
+++ b/services/sync/tests/tps/unsigned-xpi.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5612">
+  <name>Unsigned Test XPI</name>
+  <type id="1">Extension</type>
+  <guid>unsigned-xpi@tests.mozilla.org</guid>
+  <slug>unsigned-xpi</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="452">http://127.0.0.1:4567/unsigned.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
diff --git a/services/sync/tests/tps/unsigned.xpi b/services/sync/tests/tps/unsigned.xpi
new file mode 100644
index 0000000..51b0047
Binary files /dev/null and b/services/sync/tests/tps/unsigned.xpi differ
diff --git a/services/sync/tests/unit/head_appinfo.js b/services/sync/tests/unit/head_appinfo.js
index bfdee48..ed06d34 100644
--- a/services/sync/tests/unit/head_appinfo.js
+++ b/services/sync/tests/unit/head_appinfo.js
@@ -1,12 +1,12 @@
-var gProfD;
+let gSyncProfile;
 
 do_load_httpd_js();
-gProfD = do_get_profile();
+gSyncProfile = do_get_profile();
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 // Make sure to provide the right OS so crypto loads the right binaries
 let OS = "XPCShell";
 if ("@mozilla.org/windows-registry-key;1" in Cc)
   OS = "WINNT";
 else if ("nsILocalFileMac" in Ci)
@@ -21,17 +21,18 @@ let XULAppInfo = {
   version: "1",
   appBuildID: "20100621",
   platformVersion: "",
   platformBuildID: "20100621",
   inSafeMode: false,
   logConsoleErrors: true,
   OS: OS,
   XPCOMABI: "noarch-spidermonkey",
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime])
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
+  invalidateCachesOnRestart: function invalidateCachesOnRestart() { }
 };
 
 let XULAppInfoFactory = {
   createInstance: function (outer, iid) {
     if (outer != null)
       throw Cr.NS_ERROR_NO_AGGREGATION;
     return XULAppInfo.QueryInterface(iid);
   }
diff --git a/services/sync/tests/unit/head_helpers.js b/services/sync/tests/unit/head_helpers.js
index cad7448..66580cc 100644
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,8 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 var btoa;
 
 let provider = {
   getFile: function(prop, persistent) {
     persistent.value = true;
@@ -65,16 +71,110 @@ function initTestLogging(level) {
   appender.level = Log4Moz.Level.Trace;
   // Overwrite any other appenders (e.g. from previous incarnations)
   log.ownAppenders = [appender];
   log.updateAppenders();
 
   return logStats;
 }
 
+// This is needed for loadAddonTestFunctions().
+let gGlobalScope = this;
+
+/**
+ * Loads the AddonManager test functions by importing its test file.
+ *
+ * This should be called in the global scope of any test file needing to
+ * interface with the AddonManager. It should only be called once, or the
+ * universe will end.
+ */
+function loadAddonTestFunctions() {
+  const path = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
+  let file = do_get_file(path);
+  let uri = Services.io.newFileURI(file);
+  Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+}
+
+function getAddonInstall(name) {
+  let f = do_get_file("../../../../toolkit/mozapps/extensions/test/xpcshell/addons/"
+                      + name + ".xpi");
+  let cb = Async.makeSyncCallback();
+  AddonManager.getInstallForFile(f, cb);
+
+  return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Obtains an addon from the add-on manager by id.
+ *
+ * This is merely a synchronous wrapper.
+ *
+ * @param  id
+ *         ID of add-on to fetch
+ * @return addon object on success or undefined or null on failure
+ */
+function getAddonFromAddonManagerByID(id) {
+   let cb = Async.makeSyncCallback();
+   AddonManager.getAddonByID(id, cb);
+   return Async.waitForSyncCallback(cb);
+}
+
+/**
+ * Installs an add-on synchronously from an addonInstall
+ *
+ * @param  install addonInstall instance to install
+ */
+function installAddonFromInstall(install) {
+  let cb = Async.makeSyncCallback();
+  let listener = {onInstalled: cb};
+  AddonManager.addAddonListener(listener);
+  install.install();
+  Async.waitForSyncCallback(cb);
+  AddonManager.removeAddonListener(listener);
+
+  do_check_neq(null, install.addon);
+  do_check_neq(null, install.addon.syncGUID);
+
+  return install.addon;
+}
+
+/**
+ * Convenience function to install an add-on from the extensions unit tests.
+ *
+ * @param  name
+ *         String name of add-on to install. e.g. test_install1
+ * @return addon object that was installed
+ */
+function installAddon(name) {
+  let install = getAddonInstall(name);
+  do_check_neq(null, install);
+  return installAddonFromInstall(install);
+}
+
+/**
+ * Convenience function to uninstall an add-on synchronously.
+ *
+ * @param addon
+ *        Addon instance to uninstall
+ */
+function uninstallAddon(addon) {
+  let cb = Async.makeSyncCallback();
+  let listener = {onUninstalled: function(uninstalled) {
+    if (uninstalled.id == addon.id) {
+      AddonManager.removeAddonListener(listener);
+      cb(uninstalled);
+    }
+  }};
+
+  AddonManager.addAddonListener(listener);
+  addon.uninstall();
+  Async.waitForSyncCallback(cb);
+}
+
 function FakeFilesystemService(contents) {
   this.fakeContents = contents;
   let self = this;
 
   Utils.jsonSave = function jsonSave(filePath, that, obj, callback) {
     let json = typeof obj == "function" ? obj.call(that) : obj;
     self.fakeContents["weave/" + filePath + ".json"] = JSON.stringify(json);
     callback.call(that);
diff --git a/services/sync/tests/unit/head_http_server.js b/services/sync/tests/unit/head_http_server.js
index da3a190..02676bd 100644
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,15 +1,17 @@
 const Cm = Components.manager;
 
 // Shared logging for all HTTP server functions.
 Cu.import("resource://services-sync/log4moz.js");
 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
 const SYNC_API_VERSION = "1.1";
 
+Cu.import("resource://services-sync/engines.js");
+
 // Use the same method that record.js does, which mirrors the server.
 // The server returns timestamps with 1/100 sec granularity. Note that this is
 // subject to change: see Bug 650435.
 function new_timestamp() {
   return Math.round(Date.now() / 10) / 100;
 }
 
 function return_timestamp(request, response, timestamp) {
diff --git a/services/sync/tests/unit/install1-search.xml b/services/sync/tests/unit/install1-search.xml
new file mode 100644
index 0000000..99e6080
--- /dev/null
+++ b/services/sync/tests/unit/install1-search.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5617">
+  <name>Restartless Test Extension</name>
+  <type id="1">Extension</type>
+  <guid>addon1@tests.mozilla.org</guid>
+  <slug>addon1</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="485">http://127.0.0.1:8888/install1.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
diff --git a/services/sync/tests/unit/missing-xpi-search.xml b/services/sync/tests/unit/missing-xpi-search.xml
new file mode 100644
index 0000000..9b547cd
--- /dev/null
+++ b/services/sync/tests/unit/missing-xpi-search.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5617">
+  <name>Restartless Test Extension</name>
+  <type id="1">Extension</type>
+  <guid>missing-xpi@tests.mozilla.org</guid>
+  <slug>missing-xpi</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>{3e3ba16c-1675-4e88-b9c8-afef81b3d2ef}</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="485">http://127.0.0.1:8888/THIS_DOES_NOT_EXIST.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
diff --git a/services/sync/tests/unit/test_addons_engine.js b/services/sync/tests/unit/test_addons_engine.js
new file mode 100644
index 0000000..a6cc363
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -0,0 +1,117 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-sync/engines/addons.js");
+
+loadAddonTestFunctions();
+startupManager();
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let reconciler = engine._reconciler;
+let tracker = engine._tracker;
+
+function advance_test() {
+  reconciler._addons = {};
+  reconciler._changes = [];
+
+  let cb = Async.makeSpinningCallback();
+  reconciler.saveState(null, cb);
+  cb.wait();
+
+  run_next_test();
+}
+
+// This is a basic sanity test for the unit test itself. If this breaks, the
+// add-ons API likely changed upstream.
+add_test(function test_addon_install() {
+  _("Ensure basic add-on APIs work as expected.");
+
+  let install = getAddonInstall("test_install1");
+  do_check_neq(install, null);
+  do_check_eq(install.type, "extension");
+  do_check_eq(install.name, "Test 1");
+
+  advance_test();
+});
+
+add_test(function test_find_dupe() {
+  _("Ensure the _findDupe() implementation is sane.");
+
+  // This gets invoked at the top of sync, which is bypassed by this
+  // test, so we do it manually.
+  engine._refreshReconcilerState();
+
+  let addon = installAddon("test_install1");
+
+  let record = {
+    id:            Utils.makeGUID(),
+    addonID:       addon.id,
+    enabled:       true,
+    applicationID: Services.appinfo.ID,
+    source:        "amo"
+  };
+
+  let dupe = engine._findDupe(record);
+  do_check_eq(addon.syncGUID, dupe);
+
+  record.id = addon.syncGUID;
+  dupe = engine._findDupe(record);
+  do_check_eq(null, dupe);
+
+  uninstallAddon(addon);
+  advance_test();
+});
+
+add_test(function test_get_changed_ids() {
+  _("Ensure getChangedIDs() has the appropriate behavior.");
+
+  _("Ensure getChangedIDs() returns an empty object by default.");
+  let changes = engine.getChangedIDs();
+  do_check_eq("object", typeof(changes));
+  do_check_eq(0, Object.keys(changes).length);
+
+  _("Ensure tracker changes are populated.");
+  let now = new Date();
+  let changeTime = now.getTime() / 1000;
+  let guid1 = Utils.makeGUID();
+  tracker.addChangedID(guid1, changeTime);
+
+  changes = engine.getChangedIDs();
+  do_check_eq("object", typeof(changes));
+  do_check_eq(1, Object.keys(changes).length);
+  do_check_true(guid1 in changes);
+  do_check_eq(changeTime, changes[guid1]);
+
+  tracker.clearChangedIDs();
+
+  _("Ensure reconciler changes are populated.");
+  let addon = installAddon("test_install1");
+  tracker.clearChangedIDs(); // Just in case.
+  changes = engine.getChangedIDs();
+  do_check_eq("object", typeof(changes));
+  do_check_eq(1, Object.keys(changes).length);
+  do_check_true(addon.syncGUID in changes);
+  do_check_true(changes[addon.syncGUID] > changeTime);
+
+  let oldTime = changes[addon.syncGUID];
+  let guid2 = addon.syncGUID;
+  uninstallAddon(addon);
+  changes = engine.getChangedIDs();
+  do_check_eq(1, Object.keys(changes).length);
+  do_check_true(guid2 in changes);
+  do_check_true(changes[guid2] > oldTime);
+
+  advance_test();
+});
+
+function run_test() {
+  initTestLogging("Trace");
+  Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+  advance_test();
+}
diff --git a/services/sync/tests/unit/test_addons_store.js b/services/sync/tests/unit/test_addons_store.js
new file mode 100644
index 0000000..78229f1
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -0,0 +1,293 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/engines/addons.js");
+Cu.import("resource://services-sync/ext/Preferences.js");
+
+const HTTP_PORT = 8888;
+
+let prefs = new Preferences();
+
+Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
+prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
+loadAddonTestFunctions();
+startupManager();
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let tracker = engine._tracker;
+let store = engine._store;
+let reconciler = engine._reconciler;
+
+/**
+ * Create a AddonsRec for this application with the fields specified.
+ *
+ * @param  id       Sync GUID of record
+ * @param  addonId  ID of add-on
+ * @param  enabled  Boolean whether record is enabled
+ * @param  deleted  Boolean whether record was deleted
+ */
+function createRecordForThisApp(id, addonId, enabled, deleted) {
+  return {
+    id:            id,
+    addonID:       addonId,
+    enabled:       enabled,
+    deleted:       !!deleted,
+    applicationID: Services.appinfo.ID,
+    source:        "amo"
+  };
+}
+
+function createAndStartHTTPServer(port) {
+  try {
+    let server = new nsHttpServer();
+
+    let install1_xpi = "../../../../toolkit/mozapps/extensions/test/xpcshell/" +
+                       "addons/test_install1.xpi";
+
+    server.registerFile("/search/guid:addon1%40tests.mozilla.org",
+                        do_get_file("install1-search.xml"));
+    server.registerFile("/install1.xpi", do_get_file(install1_xpi));
+
+    server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
+                        do_get_file("missing-xpi-search.xml"));
+
+    server.start(port);
+
+    return server;
+  } catch (ex) {
+    _("Got exception starting HTTP server on port " + port);
+    _("Error: " + Utils.exceptionStr(ex));
+    do_throw(ex);
+  }
+}
+
+function run_test() {
+  initTestLogging("Trace");
+  Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+
+  run_next_test();
+}
+
+add_test(function test_get_all_ids() {
+  _("Ensures that getAllIDs() returns an appropriate set.");
+
+  engine._refreshReconcilerState();
+
+  let addon1 = installAddon("test_install1");
+  let addon2 = installAddon("test_install2_1");
+
+  let ids = store.getAllIDs();
+  do_check_eq("object", typeof(ids));
+  do_check_eq(2, Object.keys(ids).length);
+  do_check_true(addon1.syncGUID in ids);
+  do_check_true(addon2.syncGUID in ids);
+
+  uninstallAddon(addon1);
+  uninstallAddon(addon2);
+
+  run_next_test();
+});
+
+add_test(function test_change_item_id() {
+  _("Ensures that changeItemID() works properly.");
+
+  let addon = installAddon("test_install1");
+
+  let oldID = addon.syncGUID;
+  let newID = Utils.makeGUID();
+
+  store.changeItemID(oldID, newID);
+
+  let newAddon = getAddonFromAddonManagerByID(addon.id);
+  do_check_neq(null, newAddon);
+  do_check_eq(newID, newAddon.syncGUID);
+
+  uninstallAddon(newAddon);
+
+  run_next_test();
+});
+
+add_test(function test_create() {
+  _("Ensure creating/installing an add-on from a record works.");
+
+  let server = createAndStartHTTPServer(HTTP_PORT);
+
+  let addon = installAddon("test_install1");
+  let id = addon.id;
+  uninstallAddon(addon);
+
+  let guid = Utils.makeGUID();
+  let record = createRecordForThisApp(guid, id, true, false);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  let newAddon = getAddonFromAddonManagerByID(id);
+  do_check_neq(null, newAddon);
+  do_check_eq(guid, newAddon.syncGUID);
+  do_check_false(newAddon.userDisabled);
+
+  uninstallAddon(newAddon);
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_create_missing_search() {
+  _("Ensures that failed add-on searches are handled gracefully.");
+
+  let server = createAndStartHTTPServer(HTTP_PORT);
+
+  // The handler for this ID is not installed, so a search should 404.
+  const id = "missing@tests.mozilla.org";
+  let guid = Utils.makeGUID();
+  let record = createRecordForThisApp(guid, id, true, false);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(1, failed.length);
+  do_check_eq(guid, failed[0]);
+
+  let addon = getAddonFromAddonManagerByID(id);
+  do_check_eq(null, addon);
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_create_bad_install() {
+  _("Ensures that add-ons without a valid install are handled gracefully.");
+
+  let server = createAndStartHTTPServer(HTTP_PORT);
+
+  // The handler returns a search result but the XPI will 404.
+  const id = "missing-xpi@tests.mozilla.org";
+  let guid = Utils.makeGUID();
+  let record = createRecordForThisApp(guid, id, true, false);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(1, failed.length);
+  do_check_eq(guid, failed[0]);
+
+  let addon = getAddonFromAddonManagerByID(id);
+  do_check_eq(null, addon);
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_remove() {
+  _("Ensure removing add-ons from deleted records works.");
+
+  let addon = installAddon("test_install1");
+  let record = createRecordForThisApp(addon.syncGUID, addon.id, true, true);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  let newAddon = getAddonFromAddonManagerByID(addon.id);
+  do_check_eq(null, newAddon);
+
+  run_next_test();
+});
+
+add_test(function test_apply_enabled() {
+  _("Ensures that changes to the userEnabled flag apply.");
+
+  let addon = installAddon("test_install1");
+  do_check_false(addon.userDisabled);
+
+  let records = [];
+  records.push(createRecordForThisApp(addon.syncGUID, addon.id, false, false));
+  _("Ensure application of a disable record works as expected.");
+  let failed = store.applyIncomingBatch(records);
+  do_check_eq(0, failed.length);
+  addon = getAddonFromAddonManagerByID(addon.id);
+  do_check_true(addon.userDisabled);
+
+  records = [];
+
+  _("Ensure enable record works as expected.");
+  records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, false));
+  failed = store.applyIncomingBatch(records);
+  do_check_eq(0, failed.length);
+  addon = getAddonFromAddonManagerByID(addon.id);
+  do_check_false(addon.userDisabled);
+
+  uninstallAddon(addon);
+
+  run_next_test();
+});
+
+add_test(function test_ignore_different_appid() {
+  _("Ensure that incoming records with a different application ID are ignored.");
+
+  // We test by creating a record that should result in an update.
+  let addon = installAddon("test_install1");
+  do_check_false(addon.userDisabled);
+
+  let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
+  record.applicationID = "FAKE_ID";
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  let newAddon = getAddonFromAddonManagerByID(addon.id);
+  do_check_false(addon.userDisabled);
+
+  uninstallAddon(addon);
+
+  run_next_test();
+});
+
+add_test(function test_ignore_unknown_source() {
+  _("Ensure incoming records with unknown source are ignored.");
+
+  let addon = installAddon("test_install1");
+
+  let record = createRecordForThisApp(addon.syncGUID, addon.id, false, false);
+  record.source = "DUMMY_SOURCE";
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  let newAddon = getAddonFromAddonManagerByID(addon.id);
+  do_check_false(addon.userDisabled);
+
+  uninstallAddon(addon);
+
+  run_next_test();
+});
+
+add_test(function test_apply_uninstall() {
+  _("Ensures that uninstalling an add-on from a record works.");
+
+  let addon = installAddon("test_install1");
+
+  let records = [];
+  records.push(createRecordForThisApp(addon.syncGUID, addon.id, true, true));
+  let failed = store.applyIncomingBatch(records);
+  do_check_eq(0, failed.length);
+
+  addon = getAddonFromAddonManagerByID(addon.id);
+  do_check_eq(null, addon);
+
+  run_next_test();
+});
+
+/*
+add_test(function test_wipe() {
+  _("Ensures that wiping causes add-ons to be uninstalled.");
+
+  let addon1 = installAddon("test_install1");
+  let addon2 = installAddon("test_install2_1");
+
+  store.wipe();
+
+  let addon = getAddonFromAddonManagerByID(addon1.id);
+  do_check_eq(null, addon);
+  addon = getAddonFromAddonManagerByID(addon2.id);
+  do_check_eq(null, addon);
+
+  run_next_test();
+});
+*/
diff --git a/services/sync/tests/unit/test_addons_tracker.js b/services/sync/tests/unit/test_addons_tracker.js
new file mode 100644
index 0000000..76634e5
--- /dev/null
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Cu.import("resource://services-sync/engines/addons.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+loadAddonTestFunctions();
+startupManager();
+Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
+
+Engines.register(AddonsEngine);
+let engine = Engines.get("addons");
+let reconciler = engine._reconciler;
+let store = engine._store;
+let tracker = engine._tracker;
+
+const addon1ID = "addon1@tests.mozilla.org";
+
+function cleanup_and_advance() {
+  Svc.Obs.notify("weave:engine:stop-tracking");
+  tracker.observe(null, "weave:engine:stop-tracking");
+
+  tracker.resetScore();
+  tracker.clearChangedIDs();
+
+  reconciler._addons = {};
+  reconciler._changes = [];
+  let cb = Async.makeSpinningCallback();
+  reconciler.saveState(null, cb);
+  cb.wait();
+
+  run_next_test();
+}
+
+function run_test() {
+  initTestLogging("Trace");
+  Log4Moz.repository.getLogger("Sync.Engine.Addons").level = Log4Moz.Level.Trace;
+
+  cleanup_and_advance();
+}
+
+add_test(function test_empty() {
+  _("Verify the tracker is empty to start with.");
+
+  do_check_eq(0, Object.keys(tracker.changedIDs).length);
+  do_check_eq(0, tracker.score);
+
+  cleanup_and_advance();
+});
+
+add_test(function test_not_tracking() {
+  _("Ensures the tracker doesn't do anything when it isn't tracking.");
+
+  let addon = installAddon("test_install1");
+  uninstallAddon(addon);
+
+  do_check_eq(0, Object.keys(tracker.changedIDs).length);
+  do_check_eq(0, tracker.score);
+
+  cleanup_and_advance();
+});
+
+add_test(function test_track_install() {
+  _("Ensure that installing an add-on notifies tracker.");
+
+  Svc.Obs.notify("weave:engine:start-tracking");
+
+  do_check_eq(0, tracker.score);
+  let addon = installAddon("test_install1");
+  let changed = tracker.changedIDs;
+
+  do_check_eq(1, Object.keys(changed).length);
+  do_check_true(addon.syncGUID in changed);
+  do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+  uninstallAddon(addon);
+  cleanup_and_advance();
+});
+
+add_test(function test_track_uninstall() {
+  _("Ensure that uninstalling an add-on notifies tracker.");
+
+  let addon = installAddon("test_install1");
+  let guid = addon.syncGUID;
+  do_check_eq(0, tracker.score);
+
+  Svc.Obs.notify("weave:engine:start-tracking");
+
+  uninstallAddon(addon);
+  let changed = tracker.changedIDs;
+  do_check_eq(1, Object.keys(changed).length);
+  do_check_true(guid in changed);
+  do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+  cleanup_and_advance();
+});
+
+// The following don't work for an unknown reason. The listeners for disabling
+// never get invoked. Weird
+// TODO fix this
+/*
+add_test(function test_track_user_disable() {
+  _("Ensure that tracker sees disabling of add-on");
+
+  let addon = installAddon("test_install1");
+  do_check_false(addon.userDisabled);
+
+  Svc.Obs.notify("weave:engine:start-tracking");
+  do_check_eq(0, tracker.score);
+
+  let cb = Async.makeSyncCallback();
+
+  let listener = {
+    onDisabled: function(disabled) {
+      _("onDisabled");
+      if (disabled.id == addon.id) {
+        AddonManager.removeAddonListener(listener);
+        cb();
+      }
+    },
+    onDisabling: function(disabling) {
+      _("onDisabling add-on");
+    }
+  };
+  AddonManager.addAddonListener(listener);
+
+  _("Disabling add-on");
+  addon.userDisabled = true;
+  _("Disabling started...");
+  Async.waitForSyncCallback(cb);
+
+  let changed = tracker.changedIDs;
+  do_check_eq(1, Object.keys(changed).length);
+  do_check_true(addon.syncGUID in changed);
+  do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+  uninstallAddon(addon);
+  cleanup_and_advance();
+});
+
+add_test(function test_track_enable() {
+  _("Ensure that enabling a disabled add-on notifies tracker.");
+
+  let addon = installAddon("test_install1");
+  addon.userDisabled = true;
+  store._sleep(0);
+
+  do_check_eq(0, tracker.score);
+
+  Svc.Obs.notify("weave:engine:start-tracking");
+  addon.userDisabled = false;
+  store._sleep(0);
+
+  let changed = tracker.changedIDs;
+  do_check_eq(1, Object.keys(changed).length);
+  do_check_true(addon.syncGUID in changed);
+  do_check_eq(SCORE_INCREMENT_XLARGE, tracker.score);
+
+  uninstallAddon(addon);
+  cleanup_and_advance();
+});
+*/
+
diff --git a/services/sync/tests/unit/test_places_guid_downgrade.js b/services/sync/tests/unit/test_places_guid_downgrade.js
index 38e4c7e..c080219 100644
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -10,22 +10,22 @@ const storageSvc = Cc["@mozilla.org/storage/service;1"]
 
 const fxuri = Utils.makeURI("http://getfirefox.com/");
 const tburi = Utils.makeURI("http://getthunderbird.com/");
 
 function setPlacesDatabase(aFileName) {
   removePlacesDatabase();
   _("Copying over places.sqlite.");
   let file = do_get_file(aFileName);
-  file.copyTo(gProfD, kDBName);
+  file.copyTo(gSyncProfile, kDBName);
 }
 
 function removePlacesDatabase() {
   _("Removing places.sqlite.");
-  let file = gProfD.clone();
+  let file = gSyncProfile.clone();
   file.append(kDBName);
   try {
     file.remove(false);
   } catch (ex) {
     // Windows is awesome. NOT.
   }
 }
 
@@ -33,17 +33,17 @@ Svc.Obs.add("places-shutdown", function () {
   do_timeout(0, removePlacesDatabase);
 });
 
 
 // Verify initial database state. Function borrowed from places tests.
 function test_initial_state() {
   // Mostly sanity checks our starting DB to make sure it's setup as we expect
   // it to be.
-  let dbFile = gProfD.clone();
+  let dbFile = gSyncProfile.clone();
   dbFile.append(kDBName);
   let db = storageSvc.openUnsharedDatabase(dbFile);
 
   let stmt = db.createStatement("PRAGMA journal_mode");
   do_check_true(stmt.executeStep());
   // WAL journal mode should have been unset this database when it was migrated
   // down to v10.
   do_check_neq(stmt.getString(0).toLowerCase(), "wal");
diff --git a/services/sync/tests/unit/xpcshell.ini b/services/sync/tests/unit/xpcshell.ini
index f8cc583..d8fce75 100644
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,14 +1,17 @@
 [DEFAULT]
 head = head_appinfo.js head_helpers.js head_http_server.js
 tail = 
 
 [test_Observers.js]
 [test_Preferences.js]
+[test_addons_engine.js]
+[test_addons_store.js]
+[test_addons_tracker.js]
 [test_async_chain.js]
 [test_async_querySpinningly.js]
 [test_auth_manager.js]
 [test_bookmark_batch_fail.js]
 [test_bookmark_engine.js]
 [test_bookmark_legacy_microsummaries_support.js]
 [test_bookmark_livemarks.js]
 [test_bookmark_order.js]
diff --git a/services/sync/tps/extensions/tps/modules/addons.jsm b/services/sync/tps/extensions/tps/modules/addons.jsm
index 9324b7a..4139127 100644
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -41,18 +41,16 @@ const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://gre/modules/AddonManager.jsm");
 CU.import("resource://gre/modules/AddonRepository.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://services-sync/async.js");
 CU.import("resource://services-sync/util.js");
 CU.import("resource://tps/logger.jsm");
-var XPIProvider = CU.import("resource://gre/modules/XPIProvider.jsm")
-                  .XPIProvider;
 
 const ADDONSGETURL = 'http://127.0.0.1:4567/';
 const STATE_ENABLED = 1;
 const STATE_DISABLED = 2;
 
 function GetFileAsText(file)
 {
   let channel = Services.io.newChannel(file, null, null);
@@ -83,60 +81,48 @@ function Addon(TPS, id) {
 
 Addon.prototype = {
   _addons_requiring_restart: [],
   _addons_pending_install: [],
 
   Delete: function() {
     // find our addon locally
     let cb = Async.makeSyncCallback();
-    XPIProvider.getAddonsByTypes(null, cb);
-    let results =  Async.waitForSyncCallback(cb);
-    var addon;
-    var id = this.id;
-    results.forEach(function(result) {
-      if (result.id == id) {
-        addon = result;
-      }
-    });
+    AddonManager.getAddonByID(this.id, cb);
+    let addon = Async.waitForSyncCallback(cb);
     Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
     addon.uninstall();
   },
 
   Find: function(state) {
-    let cb = Async.makeSyncCallback();
     let addon_found = false;
-    var that = this;
+    let that = this;
 
-    var log_addon = function(addon) {
+    let log_addon = function(addon) {
       that.addon = addon;
       Logger.logInfo('addon ' + addon.id + ' found, isActive: ' + addon.isActive);
       if (state == STATE_ENABLED || state == STATE_DISABLED) {
           Logger.AssertEqual(addon.isActive,
             state == STATE_ENABLED ? true : false,
             "addon " + that.id + " has an incorrect enabled state");
       }
     };
 
-    // first look in the list of all addons
-    XPIProvider.getAddonsByTypes(null, cb);
-    let addonlist = Async.waitForSyncCallback(cb);
-    addonlist.forEach(function(addon) {
-      if (addon.id == that.id) {
+    let cb = Async.makeSyncCallback();
+    AddonManager.getAddonByID(this.id, cb);
+    let addon = Async.waitForSyncCallback(cb);
+    if (addon) {
         addon_found = true;
         log_addon.call(that, addon);
-      }
-    });
-
-    if (!addon_found) {
+    } else {
       // then look in the list of recent installs
-      cb = Async.makeSyncCallback();
-      XPIProvider.getInstallsByTypes(null, cb);
+      let cb = Async.makeSyncCallback();
+      AddonManager.getInstallsByTypes(null, cb);
       addonlist = Async.waitForSyncCallback(cb);
-      for (var i in addonlist) {
+      for (let i in addonlist) {
         if (addonlist[i].addon && addonlist[i].addon.id == that.id &&
             addonlist[i].state == AddonManager.STATE_INSTALLED) {
           addon_found = true;
           log_addon.call(that, addonlist[i].addon);
         }
       }
     }
 
@@ -173,17 +159,18 @@ Addon.prototype = {
     Logger.AssertTrue(install_addons,
                       "no addons found for id " + this.id);
     Logger.AssertEqual(install_addons.length,
                        1,
                        "multiple addons found for id " + this.id);
 
     let addon = install_addons[0];
     Logger.logInfo(JSON.stringify(addon), null, ' ');
-    if (XPIProvider.installRequiresRestart(addon)) {
+
+    if (addon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_INSTALL) {
       this._addons_requiring_restart.push(addon.id);
     }
 
     // Start installing the addon asynchronously; finish up in
     // onInstallEnded(), onInstallFailed(), or onDownloadFailed().
     this._addons_pending_install.push(addon.id);
     this.TPS.StartAsyncOperation();
 
diff --git a/testing/tps/tps/mozhttpd.py b/testing/tps/tps/mozhttpd.py
index daccf8f..6171d28 100644
--- a/testing/tps/tps/mozhttpd.py
+++ b/testing/tps/tps/mozhttpd.py
@@ -51,17 +51,43 @@ DOCROOT = '.'
 
 class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
     allow_reuse_address = True
     
 class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
     def translate_path(self, path):
         # It appears that the default path is '/' and os.path.join makes the '/' 
         o = urlparse(path)
-        return "%s%s" % ('' if sys.platform == 'win32' else '/', '/'.join([i.strip('/') for i in (DOCROOT, o.path)]))
+
+        sep = '/'
+        if sys.platform == 'win32':
+            sep = ''
+
+        ret = '%s%s' % ( sep, DOCROOT.strip('/') )
+
+        # Stub out addons.mozilla.org search API, which is used when installing
+        # add-ons. The version is hard-coded because we want tests to fail when
+        # the API updates so we can update our stubbed files with the changes.
+        if o.path.find('/en-US/firefox/api/1.5/search/guid:') == 0:
+            ids = urllib.unquote(o.path[len('/en-US/firefox/api/1.5/search/guid:'):])
+
+            if ids.count(',') > 0:
+                raise Exception('Only searching for single ids is currently supported.')
+
+            base = ids
+            at_loc = ids.find('@')
+            if at_loc > 0:
+                base = ids[0:at_loc]
+
+            ret += '/%s.xml' % base
+
+        else:
+            ret += '/%s' % o.path.strip('/')
+
+        return ret
 
     # I found on my local network that calls to this were timing out
     # I believe all of these calls are from log_message
     def address_string(self):
         return "a.b.c.d"
 
     # This produces a LOT of noise
     def log_message(self, format, *args):
diff --git a/testing/tps/tps/testrunner.py b/testing/tps/tps/testrunner.py
index 4ecf528..0cecc4b 100644
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -84,25 +84,30 @@ class TPSTestRunner(object):
 
   default_env = { 'MOZ_CRASHREPORTER_DISABLE': '1',
                   'GNOME_DISABLE_CRASH_DIALOG': '1',
                   'XRE_NO_WINDOWS_CRASH_DIALOG': '1',
                   'MOZ_NO_REMOTE': '1',
                   'XPCOM_DEBUG_BREAK': 'warn',
                 }
   default_preferences = { 'app.update.enabled' : False,
+                          'extensions.getAddons.get.url':
+                          'http://127.0.0.1:4567/en-US/firefox/api/%API_VERSION%/search/guid:%IDS%',
+                          'extensions.logging.enabled': True,
                           'extensions.update.enabled'    : False,
                           'extensions.update.notifyUser' : False,
                           'browser.shell.checkDefaultBrowser' : False,
                           'browser.tabs.warnOnClose' : False,
                           'browser.warnOnQuit': False,
                           'browser.sessionstore.resume_from_crash': False,
+                          'services.sync.addon.ignoreRepositoryChecking': True,
                           'services.sync.firstSync': 'notReady',
                           'services.sync.lastversion': '1.0',
                           'services.sync.log.rootLogger': 'Trace',
+                          'services.sync.log.logger.engine.addons': 'Trace',
                           'services.sync.log.logger.service.main': 'Trace',
                           'services.sync.log.logger.engine.bookmarks': 'Trace',
                           'services.sync.log.appender.console': 'Trace',
                           'services.sync.log.appender.debugLog.enabled': True,
                           'browser.dom.window.dump.enabled': True,
                           # Allow installing extensions dropped into the profile folder
                           'extensions.autoDisableScopes': 10,
                           # Don't open a dialog to show available add-on updates
diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
index 1c53330..8915728 100644
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -1399,16 +1399,38 @@ var AddonManager = {
    * @return An array of add-on IDs
    */
   getStartupChanges: function AM_getStartupChanges(aType) {
     if (!(aType in AddonManagerInternal.startupChanges))
       return [];
     return AddonManagerInternal.startupChanges[aType].slice(0);
   },
 
+  /**
+   * Returns an object describing all startup changes.
+   *
+   * Object keys are the AddonManager.STARTUP_CHANGE_* constants. Values
+   * are arrays of add-on IDs.
+   *
+   * The object is copied from the source of truth, so it is safe to
+   * mutate.
+   */
+  getAllStartupChanges: function AM_getAllStartupChanges() {
+    let result = {};
+    for (let [k, v] in Iterator(AddonManagerInternal.startupChanges)) {
+      if (!v || !v.length) {
+        continue;
+      }
+
+      result[k] = v.slice(0);
+    }
+
+    return result;
+  },
+
   getAddonByID: function AM_getAddonByID(aId, aCallback) {
     AddonManagerInternal.getAddonByID(aId, aCallback);
   },
 
   getAddonBySyncGUID: function AM_getAddonBySyncGUID(aId, aCallback) {
     AddonManagerInternal.getAddonBySyncGUID(aId, aCallback);
   },
 
diff --git a/toolkit/mozapps/extensions/XPIProvider.jsm b/toolkit/mozapps/extensions/XPIProvider.jsm
index e3c266c..ffcaa9f 100644
--- a/toolkit/mozapps/extensions/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/XPIProvider.jsm
@@ -818,16 +818,26 @@ function loadManifestFromRDF(aUri, aStream) {
   }
   else {
     addon.userDisabled = false;
     addon.softDisabled = addon.blocklistState == Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
   }
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
+  // Generate random GUID used for Sync.
+  let rng = Cc["@mozilla.org/security/random-generator;1"].
+            createInstance(Ci.nsIRandomGenerator);
+  let bytes = rng.generateRandomBytes(9);
+  let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
+                    .join("");
+  // Base64 encode
+  addon.syncGUID = btoa(byte_string).replace('+', '-', 'g')
+                                    .replace('/', '_', 'g');
+
   return addon;
 }
 
 /**
  * Loads an AddonInternal object from an add-on extracted in a directory.
  *
  * @param  aDir
  *         The nsIFile directory holding the add-on
@@ -5282,28 +5292,16 @@ var XPIDatabase = {
    * Synchronously adds an AddonInternal's metadata to the database.
    *
    * @param  aAddon
    *         AddonInternal to add
    * @param  aDescriptor
    *         The file descriptor of the add-on
    */
   addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
-    // Create a GUID if one does not exist
-    if (!aAddon.syncGUID) {
-      let rng = Cc["@mozilla.org/security/random-generator;1"].
-                createInstance(Ci.nsIRandomGenerator);
-      let bytes = rng.generateRandomBytes(9);
-      let byte_string = [String.fromCharCode(byte) for each (byte in bytes)]
-                        .join("");
-      // Base64 encode
-      aAddon.syncGUID = btoa(byte_string).replace('+', '-', 'g')
-                                         .replace('/', '_', 'g');
-    }
-
     // If there is no DB yet then forcibly create one
     if (!this.connection)
       this.openConnection(false, true);
 
     this.beginTransaction();
 
     var self = this;
     function insertLocale(aLocale) {
@@ -7546,17 +7544,20 @@ function AddonWrapper(aAddon) {
 
     return val;
   });
 
   this.__defineSetter__("syncGUID", function(val) {
     if (aAddon.syncGUID == val)
       return val;
 
+    if (aAddon instanceof DBAddonInternal) {
     XPIDatabase.setAddonSyncGUID(aAddon, val);
+    }
+
     aAddon.syncGUID = val;
 
     return val;
   });
 
   this.__defineGetter__("install", function() {
     if (!("_install" in aAddon) || !aAddon._install)
       return null;
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
index 5daddb4..bd12e8f 100644
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -67,17 +67,17 @@ function run_test_1() {
 
     do_check_neq(install, null);
     do_check_eq(install.linkedInstalls, null);
     do_check_eq(install.type, "extension");
     do_check_eq(install.version, "1.0");
     do_check_eq(install.name, "Test 1");
     do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
     do_check_true(install.addon.hasResource("install.rdf"));
-    do_check_eq(install.addon.syncGUID, null);
+    do_check_neq(install.addon.syncGUID, null);
     do_check_eq(install.addon.install, install);
     do_check_eq(install.addon.size, ADDON1_SIZE);
     do_check_true(hasFlag(install.addon.operationsRequiringRestart,
                           AddonManager.OP_NEEDS_RESTART_INSTALL));
     let file = do_get_addon("test_install1");
     let uri = Services.io.newFileURI(file).spec;
     do_check_eq(install.addon.getResourceURI("install.rdf").spec, "jar:" + uri + "!/install.rdf");
     do_check_eq(install.addon.iconURL, "jar:" + uri + "!/icon.png");
