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 181821 571814852fe80aca1e876c6dfb3ed7128328d8ee
parent 181704 032022e0dfcd98e3fd2ea6fef26c5914d08d9992
child 181822 88955975f26e9a839a0eba9e1d21909a1c9aca9d
push id26733
push userryanvm@gmail.com
push dateTue, 06 May 2014 20:50:37 +0000
treeherdermozilla-central@4e4e0f502969 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersirving
bugs992258
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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.");