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 180173 18c0f7152372bd81141e07a4a25a4c7efcf0862c
parent 180172 bd993b75b61fddc41a75621ad46f7be36e4ba1a1
child 180174 07d2ac9298f788a5512a94d14892e96f98dd48ae
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersunfocused
bugs986677
milestone31.0a1
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);
+}