Bug 986677 - Add time left and activity state to experiments in the Addon Manager UI. r=unfocused
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Wed, 23 Apr 2014 14:34:49 +0200
changeset 199288 18c0f7152372bd81141e07a4a25a4c7efcf0862c
parent 199287 bd993b75b61fddc41a75621ad46f7be36e4ba1a1
child 199289 07d2ac9298f788a5512a94d14892e96f98dd48ae
push id486
push userasasaki@mozilla.com
push dateMon, 14 Jul 2014 18:39:42 +0000
treeherdermozilla-release@d33428174ff1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersunfocused
bugs986677
milestone31.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 986677 - Add time left and activity state to experiments in the Addon Manager UI. r=unfocused
browser/experiments/Experiments.jsm
toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
toolkit/mozapps/extensions/content/extensions.css
toolkit/mozapps/extensions/content/extensions.js
toolkit/mozapps/extensions/content/extensions.xml
toolkit/mozapps/extensions/content/extensions.xul
toolkit/themes/linux/mozapps/extensions/extensions.css
toolkit/themes/osx/mozapps/extensions/extensions.css
toolkit/themes/windows/mozapps/extensions/extensions.css
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -588,16 +588,38 @@ Experiments.Experiments.prototype = {
 
       // Sort chronologically, descending.
       list.sort((a, b) => b.endDate - a.endDate);
       return list;
     }.bind(this));
   },
 
   /**
+   * Returns the ExperimentInfo for the active experiment, or null
+   * if there is none.
+   */
+  getActiveExperiment: function () {
+    let experiment = this._getActiveExperiment();
+    if (!experiment) {
+      return null;
+    }
+
+    let info = {
+      id: experiment.id,
+      name: experiment._name,
+      description: experiment._description,
+      active: experiment.enabled,
+      endDate: experiment.endDate.getTime(),
+      detailURL: experiment._homepageURL,
+    };
+
+    return info;
+  },
+
+  /**
    * Determine whether another date has the same UTC day as now().
    */
   _dateIsTodayUTC: function (d) {
     let now = this._policy.now();
 
     return stripDateToMidnight(now).getTime() == stripDateToMidnight(d).getTime();
   },
 
@@ -737,16 +759,17 @@ Experiments.Experiments.prototype = {
     this.disableExperiment(TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED);
   },
 
   onInstallStarted: function (install) {
     if (install.addon.type != "experiment") {
       return;
     }
 
+    this._log.trace("onInstallStarted() - " + install.addon.id);
     if (install.addon.appDisabled) {
       // This is a PreviousExperiment
       return;
     }
 
     // We want to be in control of all experiment add-ons: reject installs
     // for add-ons that we don't know about.
 
@@ -2056,16 +2079,17 @@ this.Experiments.PreviousExperimentProvi
 
 /**
  * An add-on that represents a previously-installed experiment.
  */
 function PreviousExperimentAddon(experiment) {
   this._id = experiment.id;
   this._name = experiment.name;
   this._endDate = experiment.endDate;
+  this._description = experiment.description;
 }
 
 PreviousExperimentAddon.prototype = Object.freeze({
   // BEGIN REQUIRED ADDON PROPERTIES
 
   get appDisabled() {
     return true;
   },
@@ -2129,17 +2153,19 @@ PreviousExperimentAddon.prototype = Obje
   get version() {
     return null;
   },
 
   // END REQUIRED PROPERTIES
 
   // BEGIN OPTIONAL PROPERTIES
 
-  // TODO description
+  get description() {
+    return this._description;
+  },
 
   get updateDate() {
     return new Date(this._endDate);
   },
 
   // END OPTIONAL PROPERTIES
 
   // BEGIN REQUIRED METHODS
@@ -2150,9 +2176,17 @@ PreviousExperimentAddon.prototype = Obje
 
   findUpdates: function (listener, reason, appVersion, platformVersion) {
     AddonManagerPrivate.callNoUpdateListeners(this, listener, reason,
                                               appVersion, platformVersion);
   },
 
   // END REQUIRED METHODS
 
+  /**
+   * The end-date of the experiment, required for the Addon Manager UI.
+   */
+
+   get endDate() {
+     return this._endDate;
+   },
+
 });
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
@@ -93,16 +93,42 @@ details.notification.enable=%1$S will be
 details.notification.disable=%1$S will be disabled after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.install) %1$S is the add-on name, %2$S is brand name
 details.notification.install=%1$S will be installed after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.uninstall) %1$S is the add-on name, %2$S is brand name
 details.notification.uninstall=%1$S will be uninstalled after you restart %2$S.
 #LOCALIZATION NOTE (details.notification.upgrade) %1$S is the add-on name, %2$S is brand name
 details.notification.upgrade=%1$S will be updated after you restart %2$S.
 
+#LOCALIZATION NOTE (details.experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (detail view).
+details.experiment.time.daysRemaining=#1 day remaining;#1 days remaining
+#LOCALIZATION NOTE (details.experiment.time.endsToday) The experiment will end in less than a day (detail view).
+details.experiment.time.endsToday=Less than a day remaining
+#LOCALIZATION NOTE (details.experiment.time.daysPassed) #1 is the number of days since the experiment ran (detail view).
+details.experiment.time.daysPassed=#1 day ago;#1 days ago
+#LOCALIZATION NOTE (details.experiment.time.endedToday) The experiment ended less than a day ago (detail view).
+details.experiment.time.endedToday=Less than a day ago
+#LOCALIZATION NOTE (details.experiment.state.active) This experiment is active (detail view).
+details.experiment.state.active=Active
+#LOCALIZATION NOTE (details.experiment.state.complete) This experiment is complete (it was previously active) (detail view).
+details.experiment.state.complete=Complete
+
+#LOCALIZATION NOTE (experiment.time.daysRemaining) #1 is the number of days from now that the experiment will remain active (list view item).
+experiment.time.daysRemaining=#1 day remaining;#1 days remaining
+#LOCALIZATION NOTE (experiment.time.endsToday) The experiment will end in less than a day (list view item).
+experiment.time.endsToday=Less than a day remaining
+#LOCALIZATION NOTE (experiment.time.daysPassed) #1 is the number of days since the experiment ran (list view item).
+experiment.time.daysPassed=#1 day ago;#1 days ago
+#LOCALIZATION NOTE (experiment.time.endedToday) The experiment ended less than a day ago (list view item).
+experiment.time.endedToday=Less than a day ago
+#LOCALIZATION NOTE (experiment.state.active) This experiment is active (list view item).
+experiment.state.active=Active
+#LOCALIZATION NOTE (experiment.state.complete) This experiment is complete (it was previously active) (list view item).
+experiment.state.complete=Complete
+
 installFromFile.dialogTitle=Select add-on to install
 installFromFile.filterName=Add-ons
 
 uninstallAddonTooltip=Uninstall this add-on
 uninstallAddonRestartRequiredTooltip=Uninstall this add-on (restart required)
 enableAddonTooltip=Enable this add-on
 enableAddonRestartRequiredTooltip=Enable this add-on (restart required)
 disableAddonTooltip=Disable this add-on
--- a/toolkit/mozapps/extensions/content/extensions.css
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -240,8 +240,13 @@ richlistitem:not([selected]) * {
 .view-pane[type="experiment"] .disabled-postfix,
 .view-pane[type="experiment"] .update-postfix,
 .view-pane[type="experiment"] .version,
 #detail-view[type="experiment"] .alert-container,
 #detail-view[type="experiment"] #detail-version,
 #detail-view[type="experiment"] #detail-creator {
   display: none;
 }
+
+.view-pane:not([type="experiment"]) .experiment-container,
+.view-pane:not([type="experiment"]) #detail-experiment-container {
+  display: none;
+}
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -15,16 +15,18 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function () {
   return Cu.import("resource:///modules/devtools/ToolboxProcess.jsm", {}).
          BrowserToolboxProcess;
 });
+XPCOMUtils.defineLazyModuleGetter(this, "Experiments",
+  "resource:///modules/experiments/Experiments.jsm");
 
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_MAXRESULTS = "extensions.getAddons.maxResults";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_ID_ENABLED = "extensions.%ID%.getAddons.cache.enabled";
 const PREF_UI_TYPE_HIDDEN = "extensions.ui.%TYPE%.hidden";
@@ -203,16 +205,33 @@ function isDiscoverEnabled() {
   try {
     if (!Services.prefs.getBoolPref(PREF_XPI_ENABLED))
       return false;
   } catch (e) {}
 
   return true;
 }
 
+function getExperimentEndDate(aAddon) {
+  if (!("@mozilla.org/browser/experiments-service;1" in Cc)) {
+    return 0;
+  }
+
+  if (!aAddon.isActive) {
+    return aAddon.endDate;
+  }
+
+  let experiment = Experiments.instance().getActiveExperiment();
+  if (!experiment) {
+    return 0;
+  }
+
+  return experiment.endDate;
+}
+
 /**
  * Obtain the main DOMWindow for the current context.
  */
 function getMainWindow() {
   return window.QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIWebNavigation)
                .QueryInterface(Ci.nsIDocShellTreeItem)
                .rootTreeItem
@@ -1421,16 +1440,20 @@ function createItem(aObj, aIsInstall, aI
   item.mAddon = aObj;
 
   item.setAttribute("status", "installed");
 
   // set only attributes needed for sorting and XBL binding,
   // the binding handles the rest
   item.setAttribute("value", aObj.id);
 
+  if (aObj.type == "experiment") {
+    item.endDate = getExperimentEndDate(aObj);
+  }
+
   return item;
 }
 
 function sortElements(aElements, aSortBy, aAscending) {
   // aSortBy is an Array of attributes to sort by, in decending
   // order of priority.
 
   const DATE_FIELDS = ["updateDate"];
@@ -2869,16 +2892,44 @@ var gDetailView = {
       if (first && window.getComputedStyle(gridRow, null).getPropertyValue("display") != "none") {
         gridRow.setAttribute("first-row", true);
         first = false;
       } else {
         gridRow.removeAttribute("first-row");
       }
     }
 
+    if (this._addon.type == "experiment") {
+      let prefix = "details.experiment.";
+      let active = this._addon.isActive;
+
+      let stateKey = prefix + "state." + (active ? "active" : "complete");
+      let node = document.getElementById("detail-experiment-state");
+      node.value = gStrings.ext.GetStringFromName(stateKey);
+
+      let now = Date.now();
+      let end = getExperimentEndDate(this._addon);
+      let days = Math.abs(end - now) / (24 * 60 * 60 * 1000);
+
+      let timeKey = prefix + "time.";
+      let timeMessage;
+      if (days < 1) {
+        timeKey += (active ? "endsToday" : "endedToday");
+        timeMessage = gStrings.ext.GetStringFromName(timeKey);
+      } else {
+        timeKey += (active ? "daysRemaining" : "daysPassed");
+        days = Math.round(days);
+        let timeString = gStrings.ext.GetStringFromName(timeKey);
+        timeMessage = PluralForm.get(days, timeString)
+                                .replace("#1", days);
+      }
+
+      document.getElementById("detail-experiment-time").value = timeMessage;
+    }
+
     this.fillSettingsRows(aScrollToPreferences, (function updateView_fillSettingsRows() {
       this.updateState();
       gViewController.notifyViewChanged();
     }).bind(this));
   },
 
   show: function gDetailView_show(aAddonId, aRequest) {
     let index = aAddonId.indexOf("/preferences");
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -799,16 +799,25 @@
                 <xul:label anonid="version" class="version"/>
                 <xul:label class="disabled-postfix" value="&addon.disabled.postfix;"/>
                 <xul:label class="update-postfix" value="&addon.update.postfix;"/>
                 <xul:spacer flex="5000"/> <!-- Necessary to make the name crop -->
               </xul:hbox>
             <xul:label anonid="date-updated" class="date-updated"
                        unknown="&addon.unknownDate;"/>
           </xul:hbox>
+          <xul:hbox class="experiment-container">
+            <svg width="6" height="6" viewBox="0 0 6 6" version="1.1"
+                 xmlns="http://www.w3.org/2000/svg"
+                 class="experiment-bullet-container">
+              <circle cx="3" cy="3" r="3" class="experiment-bullet"/>
+            </svg>
+            <xul:label anonid="experiment-state" class="experiment-state"/>
+            <xul:label anonid="experiment-time" class="experiment-time"/>
+          </xul:hbox>
 
           <xul:hbox class="advancedinfo-container" flex="1">
             <xul:vbox class="description-outer-container" flex="1">
               <xul:hbox class="description-container">
                 <xul:label anonid="description" class="description" crop="end" flex="1"/>
                 <xul:button anonid="details-btn" class="details button-link"
                             label="&addon.details.label;"
                             tooltiptext="&addon.details.tooltip;"
@@ -968,16 +977,22 @@
       </field>
       <field name="_info">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "info");
       </field>
       <field name="_version">
         document.getAnonymousElementByAttribute(this, "anonid", "version");
       </field>
+      <field name="_experimentState">
+        document.getAnonymousElementByAttribute(this, "anonid", "experiment-state");
+      </field>
+      <field name="_experimentTime">
+        document.getAnonymousElementByAttribute(this, "anonid", "experiment-time");
+      </field>
       <field name="_icon">
         document.getAnonymousElementByAttribute(this, "anonid", "icon");
       </field>
       <field name="_dateUpdated">
         document.getAnonymousElementByAttribute(this, "anonid",
                                                 "date-updated");
       </field>
       <field name="_description">
@@ -1342,16 +1357,43 @@
                              this.mAddon.install.state != AddonManager.STATE_INSTALLED);
           this._showStatus(showProgress ? "progress" : "none");
 
           let debuggable = this.mAddon.isDebuggable &&
                            Services.prefs.getBoolPref('devtools.chrome.enabled') &&
                            Services.prefs.getBoolPref('devtools.debugger.remote-enabled');
 
           this._debugBtn.disabled = this._debugBtn.hidden = !debuggable
+
+          if (this.mAddon.type == "experiment") {
+            let prefix = "experiment.";
+            let active = this.mAddon.isActive;
+
+            let stateKey = prefix + "state." + (active ? "active" : "complete");
+            this._experimentState.value = gStrings.ext.GetStringFromName(stateKey);
+
+            let now = Date.now();
+            let end = this.endDate;
+            let days = Math.abs(end - now) / (24 * 60 * 60 * 1000);
+
+            let timeKey = prefix + "time.";
+            let timeMessage;
+            if (days < 1) {
+              timeKey += (active ? "endsToday" : "endedToday");
+              timeMessage = gStrings.ext.GetStringFromName(timeKey);
+            } else {
+              timeKey += (active ? "daysRemaining" : "daysPassed");
+              days = Math.round(days);
+              let timeString = gStrings.ext.GetStringFromName(timeKey);
+              timeMessage = PluralForm.get(days, timeString)
+                                      .replace("#1", days);
+            }
+
+            this._experimentTime.value = timeMessage;
+          }
         ]]></body>
       </method>
 
       <method name="_updateUpgradeInfo">
         <body><![CDATA[
           // Only update the version string if we're displaying the upgrade info
           if (this.hasAttribute("upgrade") && shouldShowVersionNumber(this.mAddon))
             this._version.value = this.mManualUpdate.version;
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -527,16 +527,25 @@
                       <label id="detail-name" flex="1"/>
                       <label id="detail-version"/>
                       <label class="disabled-postfix" value="&addon.disabled.postfix;"/>
                       <label class="update-postfix" value="&addon.update.postfix;"/>
                       <spacer flex="5000"/> <!-- Necessary to allow the name to wrap -->
                     </hbox>
                     <label id="detail-creator" class="creator"/>
                   </vbox>
+                  <hbox id="detail-experiment-container">
+                    <svg width="8" height="8" viewBox="0 0 8 8" version="1.1"
+                         xmlns="http://www.w3.org/2000/svg"
+                         id="detail-experiment-bullet-container">
+                      <circle cx="4" cy="4" r="4" id="detail-experiment-bullet"/>
+                    </svg>
+                    <label id="detail-experiment-state"/>
+                    <label id="detail-experiment-time"/>
+                  </hbox>
                   <hbox id="detail-desc-container" align="start">
                     <vbox pack="center"> <!-- Necessary to work around bug 394738 -->
                       <image id="detail-screenshot" hidden="true"/>
                     </vbox>
                     <vbox flex="1">
                       <description id="detail-desc"/>
                       <description id="detail-fulldesc"/>
                     </vbox>
--- a/toolkit/themes/linux/mozapps/extensions/extensions.css
+++ b/toolkit/themes/linux/mozapps/extensions/extensions.css
@@ -918,8 +918,35 @@ setting[type="radio"] > radiogroup {
 
 .button-link:active {
   color: -moz-activehyperlinktext;
 }
 
 .header-button .toolbarbutton-text {
   display: none;
 }
+
+/*** telemetry experiments ***/
+
+#detail-experiment-container {
+  font-size: 80%;
+  margin-bottom: 1em;
+}
+
+#detail-experiment-bullet-container,
+#detail-experiment-state,
+#detail-experiment-time,
+.experiment-bullet-container,
+.experiment-state,
+.experiment-time {
+  vertical-align: middle;
+  display: inline-block;
+}
+
+.addon .experiment-bullet,
+#detail-experiment-bullet {
+  fill: rgb(158, 158, 158);
+}
+
+.addon[active="true"] .experiment-bullet,
+#detail-view[active="true"] #detail-experiment-bullet {
+  fill: rgb(106, 201, 20);
+}
--- a/toolkit/themes/osx/mozapps/extensions/extensions.css
+++ b/toolkit/themes/osx/mozapps/extensions/extensions.css
@@ -1,17 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import url("chrome://global/skin/inContentUI.css");
 
 %include ../../global/shared.inc
 
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 @namespace html url("http://www.w3.org/1999/xhtml");
 
 
 /*** global warnings ***/
 
 .global-warning-container {
   overflow-x: hidden;
 }
@@ -1158,8 +1157,35 @@ button.button-link:not([disabled="true"]
 }
 
 .header-button:not([disabled="true"]):active:hover,
 .header-button[open="true"] {
   border-color: rgba(45,54,71,0.7);
   box-shadow: inset 0 0 4px rgb(45,54,71), 0 1px rgba(255,255,255,0.25);
   background-image: linear-gradient(rgba(45,54,71,0.6), rgba(45,54,71,0));
 }
+
+/*** telemetry experiments ***/
+
+#detail-experiment-container {
+  font-size: 80%;
+  margin-bottom: 1em;
+}
+
+#detail-experiment-bullet-container,
+#detail-experiment-state,
+#detail-experiment-time,
+.experiment-bullet-container,
+.experiment-state,
+.experiment-time {
+  vertical-align: middle;
+  display: inline-block;
+}
+
+.addon .experiment-bullet,
+#detail-experiment-bullet {
+  fill: rgb(158, 158, 158);
+}
+
+.addon[active="true"] .experiment-bullet,
+#detail-view[active="true"] #detail-experiment-bullet {
+  fill: rgb(106, 201, 20);
+}
--- a/toolkit/themes/windows/mozapps/extensions/extensions.css
+++ b/toolkit/themes/windows/mozapps/extensions/extensions.css
@@ -1,16 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import url("chrome://global/skin/inContentUI.css");
 
-@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
-
 
 .nav-button {
   list-style-image: url(chrome://mozapps/skin/extensions/navigation.png);
 }
 
 #forward-btn {
   -moz-border-start: none;
 }
@@ -1192,8 +1190,35 @@ button.button-link:not([disabled="true"]
   background-color: rgba(61, 76, 92, 0.2);
   border-color: rgba(39, 53, 68, 0.5);
   box-shadow: 0 0 3px 1px rgba(39, 53, 68, 0.2) inset;
 }
 
 .header-button > .toolbarbutton-text {
   display: none;
 }
+
+/*** telemetry experiments ***/
+
+#detail-experiment-container {
+  font-size: 80%;
+  margin-bottom: 1em;
+}
+
+#detail-experiment-bullet-container,
+#detail-experiment-state,
+#detail-experiment-time,
+.experiment-bullet-container,
+.experiment-state,
+.experiment-time {
+  vertical-align: middle;
+  display: inline-block;
+}
+
+.addon .experiment-bullet,
+#detail-experiment-bullet {
+  fill: rgb(158, 158, 158);
+}
+
+.addon[active="true"] .experiment-bullet,
+#detail-view[active="true"] #detail-experiment-bullet {
+  fill: rgb(106, 201, 20);
+}