Bug 992258 - Trigger a refresh of the experiments view in the addon manager when experiments change. r=irving
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Tue, 06 May 2014 12:18:10 +0200
changeset 181685 571814852fe80aca1e876c6dfb3ed7128328d8ee
parent 181684 032022e0dfcd98e3fd2ea6fef26c5914d08d9992
child 181733 88955975f26e9a839a0eba9e1d21909a1c9aca9d
push id6652
push usergeorg.fritzsche@googlemail.com
push dateTue, 06 May 2014 10:19:49 +0000
treeherderfx-team@571814852fe8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersirving
bugs992258
milestone32.0a1
Bug 992258 - Trigger a refresh of the experiments view in the addon manager when experiments change. r=irving
browser/experiments/Experiments.jsm
browser/experiments/test/xpcshell/test_previous_provider.js
toolkit/mozapps/extensions/test/browser/browser_experiments.js
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -44,17 +44,17 @@ XPCOMUtils.defineLazyGetter(this, "CertU
     return mod;
   });
 
 XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
                                    "@mozilla.org/xre/app-info;1",
                                    "nsICrashReporter");
 
 const FILE_CACHE                = "experiments.json";
-const OBSERVER_TOPIC            = "experiments-changed";
+const EXPERIMENTS_CHANGED_TOPIC = "experiments-changed";
 const MANIFEST_VERSION          = 1;
 const CACHE_VERSION             = 1;
 
 const KEEP_HISTORY_N_DAYS       = 180;
 const MIN_EXPERIMENT_ACTIVE_SECONDS = 60;
 
 const PREF_BRANCH               = "experiments.";
 const PREF_ENABLED              = "enabled"; // experiments.enabled
@@ -109,16 +109,17 @@ const gPrefs = new Preferences(PREF_BRAN
 const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY);
 let gExperimentsEnabled = false;
 let gAddonProvider = null;
 let gExperiments = null;
 let gLogAppenderDump = null;
 let gPolicyCounter = 0;
 let gExperimentsCounter = 0;
 let gExperimentEntryCounter = 0;
+let gPreviousProviderCounter = 0;
 
 // Tracks active AddonInstall we know about so we can deny external
 // installs.
 let gActiveInstallURLs = new Set();
 
 // Tracks add-on IDs that are being uninstalled by us. This allows us
 // to differentiate between expected uninstalled and user-driven uninstalls.
 let gActiveUninstallAddonIDs = new Set();
@@ -459,35 +460,35 @@ Experiments.Experiments.prototype = {
       } catch (e if e instanceof AlreadyShutdownError) {
         // We error out of tasks after shutdown via that exception.
       }
     }
 
     this._log.info("Completed uninitialization.");
   }),
 
-  _registerWithAddonManager: function () {
+  _registerWithAddonManager: function (previousExperimentsProvider) {
     this._log.trace("Registering instance with Addon Manager.");
 
     AddonManager.addAddonListener(this);
     AddonManager.addInstallListener(this);
 
     if (!gAddonProvider) {
       // The properties of this AddonType should be kept in sync with the
       // experiment AddonType registered in XPIProvider.
       this._log.trace("Registering previous experiment add-on provider.");
-      gAddonProvider = new Experiments.PreviousExperimentProvider(this, [
+      gAddonProvider = previousExperimentsProvider || new Experiments.PreviousExperimentProvider(this);
+      AddonManagerPrivate.registerProvider(gAddonProvider, [
           new AddonManagerPrivate.AddonType("experiment",
                                             URI_EXTENSION_STRINGS,
                                             STRING_TYPE_NAME,
                                             AddonManager.VIEW_TYPE_LIST,
                                             11000,
                                             AddonManager.TYPE_UI_HIDE_EMPTY),
-        ]);
-      AddonManagerPrivate.registerProvider(gAddonProvider);
+      ]);
     }
 
   },
 
   _unregisterWithAddonManager: function () {
     this._log.trace("Unregistering instance with Addon Manager.");
 
     if (gAddonProvider) {
@@ -495,16 +496,25 @@ Experiments.Experiments.prototype = {
       AddonManagerPrivate.unregisterProvider(gAddonProvider);
       gAddonProvider = null;
     }
 
     AddonManager.removeInstallListener(this);
     AddonManager.removeAddonListener(this);
   },
 
+  /*
+   * Change the PreviousExperimentsProvider that this instance uses.
+   * For testing only.
+   */
+  _setPreviousExperimentsProvider: function (provider) {
+    this._unregisterWithAddonManager();
+    this._registerWithAddonManager(provider);
+  },
+
   /**
    * Throws an exception if we've already shut down.
    */
   _checkForShutdown: function() {
     if (this._shutdown) {
       throw new AlreadyShutdownError("uninit() already called");
     }
   },
@@ -1146,17 +1156,17 @@ Experiments.Experiments.prototype = {
           yield experiment.reconcileAddonState();
         }
       }
     }
 
     gPrefs.set(PREF_ACTIVE_EXPERIMENT, activeExperiment != null);
 
     if (activeChanged) {
-      Services.obs.notifyObservers(null, OBSERVER_TOPIC, null);
+      Services.obs.notifyObservers(null, EXPERIMENTS_CHANGED_TOPIC, null);
     }
 
     if ("@mozilla.org/toolkit/crash-reporter;1" in Cc && activeExperiment) {
       gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
     }
   },
 
   /*
@@ -1965,17 +1975,17 @@ ExperimentsProvider.prototype = Object.f
 
   name: "org.mozilla.experiments",
 
   measurementTypes: [
     ExperimentsLastActiveMeasurement1,
   ],
 
   _OBSERVERS: [
-    OBSERVER_TOPIC,
+    EXPERIMENTS_CHANGED_TOPIC,
   ],
 
   postInit: function () {
     for (let o of this._OBSERVERS) {
       Services.obs.addObserver(this, o, false);
     }
 
     return Promise.resolve();
@@ -1986,17 +1996,17 @@ ExperimentsProvider.prototype = Object.f
       Services.obs.removeObserver(this, o);
     }
 
     return Promise.resolve();
   },
 
   observe: function (subject, topic, data) {
     switch (topic) {
-      case OBSERVER_TOPIC:
+      case EXPERIMENTS_CHANGED_TOPIC:
         this.recordLastActiveExperiment();
         break;
     }
   },
 
   collectDailyData: function () {
     return this.recordLastActiveExperiment();
   },
@@ -2035,55 +2045,97 @@ ExperimentsProvider.prototype = Object.f
  *
  * This provider exposes read-only add-ons corresponding to previously-active
  * experiments. The existence of this provider (and the add-ons it knows about)
  * facilitates the display of old experiments in the Add-ons Manager UI with
  * very little custom code in that component.
  */
 this.Experiments.PreviousExperimentProvider = function (experiments) {
   this._experiments = experiments;
+  this._experimentList = [];
+  this._log = Log.repository.getLoggerWithMessagePrefix(
+    "Browser.Experiments.Experiments",
+    "PreviousExperimentProvider #" + gPreviousProviderCounter++ + "::");
 }
 
 this.Experiments.PreviousExperimentProvider.prototype = Object.freeze({
-  startup: function () {},
-  shutdown: function () {},
+  startup: function () {
+    this._log.trace("startup()");
+    Services.obs.addObserver(this, EXPERIMENTS_CHANGED_TOPIC, false);
+  },
+
+  shutdown: function () {
+    this._log.trace("shutdown()");
+    Services.obs.removeObserver(this, EXPERIMENTS_CHANGED_TOPIC);
+  },
+
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case EXPERIMENTS_CHANGED_TOPIC:
+        this._updateExperimentList();
+        break;
+    }
+  },
 
   getAddonByID: function (id, cb) {
-    this._getPreviousExperiments().then((experiments) => {
-      for (let experiment of experiments) {
-        if (experiment.id == id) {
-          cb(new PreviousExperimentAddon(experiment));
-          return;
-        }
+    for (let experiment of this._experimentList) {
+      if (experiment.id == id) {
+        cb(new PreviousExperimentAddon(experiment));
+        return;
       }
+    }
 
-      cb(null);
-    },
-    (error) => {
-      cb(null);
-    });
+    cb(null);
   },
 
   getAddonsByTypes: function (types, cb) {
     if (types && types.length > 0 && types.indexOf("experiment") == -1) {
       cb([]);
       return;
     }
 
-    this._getPreviousExperiments().then((experiments) => {
-      cb([new PreviousExperimentAddon(e) for (e of experiments)]);
-    },
-    (error) => {
-      cb([]);
-    });
+    cb([new PreviousExperimentAddon(e) for (e of this._experimentList)]);
   },
 
-  _getPreviousExperiments: function () {
+  _updateExperimentList: function () {
     return this._experiments.getExperiments().then((experiments) => {
-      return Promise.resolve([e for (e of experiments) if (!e.active)]);
+      let list = [e for (e of experiments) if (!e.active)];
+
+      let newMap = new Map([[e.id, e] for (e of list)]);
+      let oldMap = new Map([[e.id, e] for (e of this._experimentList)]);
+
+      let added = [e.id for (e of list) if (!oldMap.has(e.id))];
+      let removed = [e.id for (e of this._experimentList) if (!newMap.has(e.id))];
+
+      for (let id of added) {
+        this._log.trace("updateExperimentList() - adding " + id);
+        let wrapper = new PreviousExperimentAddon(newMap.get(id));
+        AddonManagerPrivate.callInstallListeners("onExternalInstall", null, wrapper, null, false);
+        AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
+      }
+
+      for (let id of removed) {
+        this._log.trace("updateExperimentList() - removing " + id);
+        let wrapper = new PreviousExperimentAddon(oldMap.get(id));
+        AddonManagerPrivate.callAddonListeners("onUninstalling", plugin, false);
+      }
+
+      this._experimentList = list;
+
+      for (let id of added) {
+        let wrapper = new PreviousExperimentAddon(newMap.get(id));
+        AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
+      }
+
+      for (let id of removed) {
+        let wrapper = new PreviousExperimentAddon(oldMap.get(id));
+        AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+      }
+
+      return this._experimentList;
     });
   },
 });
 
 /**
  * An add-on that represents a previously-installed experiment.
  */
 function PreviousExperimentAddon(experiment) {
--- a/browser/experiments/test/xpcshell/test_previous_provider.js
+++ b/browser/experiments/test/xpcshell/test_previous_provider.js
@@ -39,16 +39,18 @@ add_task(function test_setup() {
   Services.prefs.setCharPref("experiments.logging.level", "Trace");
   disableCertificateChecks();
 });
 
 add_task(function* test_provider_basic() {
   let e = Experiments.instance();
 
   let provider = new Experiments.PreviousExperimentProvider(e);
+  e._setPreviousExperimentsProvider(provider);
+
   let deferred = Promise.defer();
   provider.getAddonsByTypes(["experiment"], (addons) => {
     deferred.resolve(addons);
   });
   let addons = yield deferred.promise;
   Assert.ok(Array.isArray(addons), "getAddonsByTypes returns an Array.");
   Assert.equal(addons.length, 0, "No previous add-ons returned.");
 
@@ -119,16 +121,17 @@ add_task(function* test_provider_basic()
   Assert.equal(addons[0].id, EXPERIMENT1_ID, "ID matches expected.");
   Assert.ok(addons[0].appDisabled, "It is a previous experiment add-on.");
 });
 
 add_task(function* test_active_and_previous() {
   // Building on the previous test, activate experiment 2.
   let e = Experiments.instance();
   let provider = new Experiments.PreviousExperimentProvider(e);
+  e._setPreviousExperimentsProvider(provider);
 
   gManifestObject = {
     version: 1,
     experiments: [
       {
         id: EXPERIMENT2_ID,
         xpiURL: gDataRoot + EXPERIMENT2_XPI_NAME,
         xpiHash: EXPERIMENT2_XPI_SHA1,
--- a/toolkit/mozapps/extensions/test/browser/browser_experiments.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
@@ -279,22 +279,18 @@ add_task(function testDeactivateExperime
     deferred.resolve(addons);
   });
   let addons = yield deferred.promise;
   Assert.equal(addons.length, 1, "1 experiment add-on known.");
   Assert.ok(addons[0].appDisabled, "It is a previous experiment.");
   Assert.equal(addons[0].id, "experiment-1", "Add-on ID matches expected.");
 
   // Verify the UI looks sane.
-  // TODO remove the pane cycle once the UI refreshes automatically.
-  yield gCategoryUtilities.openType("extension");
 
   Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
-  yield gCategoryUtilities.openType("experiment");
-
   let item = get_addon_element(gManagerWindow, "experiment-1");
   Assert.ok(item, "Got add-on element.");
   Assert.ok(!item.active, "Element should not be active.");
 
   // User control buttons should not be present because previous experiments
   // should have no permissions.
   let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
   is_element_hidden(el, "Remove button is not visible.");