Bug 1047284 - Update the Loop toolbar icon upon errors and "Do not disturb". f=Standard8 r=jaws
authorMatthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 01 Sep 2014 17:37:23 -0400
changeset 202790 a7819b6a9411e7e5a317d1df42058ba60a5fd03e
parent 202789 93ccc95acc15e22ed7d8812e176e60376e2c677b
child 202791 49cb37b48c053a911487accee55e1039de48684b
child 202844 96d70813ce16a9e05e4c0d3d5bb26a690ef21d5b
push id8478
push usermozilla@noorenberghe.ca
push dateMon, 01 Sep 2014 21:38:09 +0000
treeherderfx-team@a7819b6a9411 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1047284
milestone34.0a1
Bug 1047284 - Update the Loop toolbar icon upon errors and "Do not disturb". f=Standard8 r=jaws
browser/base/content/browser-loop.js
browser/base/content/browser.js
browser/components/loop/MozLoopService.jsm
browser/components/loop/test/mochitest/browser.ini
browser/components/loop/test/mochitest/browser_toolbarbutton.js
browser/themes/osx/browser.css
browser/themes/shared/menupanel.inc.css
browser/themes/shared/toolbarbuttons.inc.css
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -8,16 +8,21 @@ let LoopUI;
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
 
   LoopUI = {
+    get toolbarButton() {
+      delete this.toolbarButton;
+      return this.toolbarButton = CustomizableUI.getWidget("loop-call-button").forWindow(window);
+    },
+
     /**
      * Opens the panel for Loop and sizes it appropriately.
      *
      * @param {event} event The event opening the panel, used to anchor
      *                      the panel to the button which triggers it.
      */
     openCallPanel: function(event) {
       let callback = iframe => {
@@ -30,18 +35,45 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       PanelFrame.showPopup(window, event.target, "loop", null,
                            "about:looppanel", null, callback);
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
-    initialize: function() {
+    init: function() {
       if (!Services.prefs.getBoolPref("loop.enabled")) {
-        CustomizableUI.getWidget("loop-call-button").forWindow(window).node.hidden = true;
+        this.toolbarButton.node.hidden = true;
         return;
       }
 
+      // Add observer notifications before the service is initialized
+      Services.obs.addObserver(this, "loop-status-changed", false);
+
+
       MozLoopService.initialize();
+      this.updateToolbarState();
+    },
+
+    uninit: function() {
+      Services.obs.removeObserver(this, "loop-status-changed");
+    },
+
+    // Implements nsIObserver
+    observe: function(subject, topic, data) {
+      if (topic != "loop-status-changed") {
+        return;
+      }
+      this.updateToolbarState();
+    },
+
+    updateToolbarState: function() {
+      let state = "";
+      if (MozLoopService.errors.size) {
+        state = "error";
+      } else if (MozLoopService.doNotDisturb) {
+        state = "disabled";
+      }
+      this.toolbarButton.node.setAttribute("state", state);
     },
   };
 })();
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1208,17 +1208,17 @@ var gBrowserInit = {
     gSyncUI.init();
     gFxAccounts.init();
 #endif
 
 #ifdef MOZ_DATA_REPORTING
     gDataNotificationInfoBar.init();
 #endif
 
-    LoopUI.initialize();
+    LoopUI.init();
 
     gBrowserThumbnails.init();
 
     // Add Devtools menuitems and listeners
     gDevToolsBrowser.registerBrowserWindow(window);
 
     window.addEventListener("mousemove", MousePosTracker, false);
     window.addEventListener("dragover", MousePosTracker, false);
@@ -1379,16 +1379,17 @@ var gBrowserInit = {
       if (Win7Features)
         Win7Features.onCloseWindow();
 
       gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
       ctrlTab.uninit();
       TabView.uninit();
       SocialUI.uninit();
       gBrowserThumbnails.uninit();
+      LoopUI.uninit();
       FullZoom.destroy();
 
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -56,16 +56,17 @@ let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
 let gRegisteredLoopServer = false;
 let gLocalizedStrings =  null;
 let gInitializeTimer = null;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
 let gFxAOAuthTokenData = null;
+let gErrors = new Map();
 
 /**
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
@@ -130,16 +131,40 @@ let MozLoopServiceInternal = {
 
   /**
    * Sets MozLoopService "do not disturb" pref value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
+    this.notifyStatusChanged();
+  },
+
+  notifyStatusChanged: function() {
+    Services.obs.notifyObservers(null, "loop-status-changed", null);
+  },
+
+  /**
+   * @param {String} errorType a key to identify the type of error. Only one
+   *                           error of a type will be saved at a time.
+   * @param {Object} error     an object describing the error in the format from Hawk errors
+   */
+  setError: function(errorType, error) {
+    gErrors.set(errorType, error);
+    this.notifyStatusChanged();
+  },
+
+  clearError: function(errorType) {
+    gErrors.delete(errorType);
+    this.notifyStatusChanged();
+  },
+
+  get errors() {
+    return gErrors;
   },
 
   /**
    * Starts registration of Loop with the push server, and then will register
    * with the Loop server. It will return early if already registered.
    *
    * @param {Object} mockPushHandler Optional, test-only mock push handler. Used
    *                                 to allow mocking of the MozLoopPushHandler.
@@ -252,16 +277,17 @@ let MozLoopServiceInternal = {
     this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl})
       .then((response) => {
         // If this failed we got an invalid token. storeSessionToken rejects
         // the gRegisteredDeferred promise for us, so here we just need to
         // early return.
         if (!this.storeSessionToken(response.headers))
           return;
 
+        this.clearError("registration");
         gRegisteredDeferred.resolve();
         // No need to clear the promise here, everything was good, so we don't need
         // to re-register.
       }, (error) => {
         // There's other errors than invalid auth token, but we should only do the reset
         // as a last resort.
         if (error.code === 401 && error.errno === INVALID_AUTH_TOKEN) {
           if (this.urlExpiryTimeIsInFuture()) {
@@ -273,16 +299,17 @@ let MozLoopServiceInternal = {
           // Authorization failed, invalid token, we need to try again with a new token.
           Services.prefs.clearUserPref("loop.hawk-session-token");
           this.registerWithLoopServer(pushUrl, true);
           return;
         }
 
         // XXX Bubble the precise details up to the UI somehow (bug 1013248).
         Cu.reportError("Failed to register with the loop server. error: " + error);
+        this.setError("registration", error);
         gRegisteredDeferred.reject(error.errno);
         gRegisteredDeferred = null;
       }
     );
   },
 
   /**
    * Callback from MozLoopPushHandler - A push notification has been received from
@@ -690,16 +717,20 @@ this.MozLoopService = {
    * Sets MozLoopService "do not disturb" value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
+  get errors() {
+    return MozLoopServiceInternal.errors;
+  },
+
   /**
    * Returns the current locale
    *
    * @return {String} The code of the current locale.
    */
   get locale() {
     try {
       return Services.prefs.getComplexValue("general.useragent.locale",
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -8,9 +8,10 @@ support-files =
 [browser_fxa_login.js]
 skip-if = !debug
 [browser_loop_fxa_server.js]
 [browser_LoopContacts.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
+[browser_toolbarbutton.js]
 [browser_mozLoop_pluralStrings.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the toolbar button states.
+ */
+
+"use strict";
+
+const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).
+                               MozLoopServiceInternal;
+
+registerCleanupFunction(function*() {
+  MozLoopService.doNotDisturb = false;
+  yield MozLoopServiceInternal.clearError("testing");
+});
+
+add_task(function* test_doNotDisturb() {
+  yield MozLoopService.doNotDisturb = true;
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is in disabled state");
+  yield MozLoopService.doNotDisturb = false;
+  Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "disabled", "Check button is not in disabled state");
+});
+
+add_task(function* test_error() {
+  yield MozLoopServiceInternal.setError("testing", {});
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is in error state");
+  yield MozLoopServiceInternal.clearError("testing");
+  Assert.notStrictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "error", "Check button is not in error state");
+});
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1317,16 +1317,17 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     list-style-image: url("chrome://browser/skin/loop/toolbar@2x.png");
     -moz-image-region: rect(0, 36px, 36px, 0);
   }
 
   toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
     list-style-image: url("chrome://browser/skin/loop/toolbar-inverted@2x.png");
   }
 
+  #loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
   #loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 72px, 36px, 36px);
   }
 
   #loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 108px, 36px, 72px);
   }
 
@@ -1352,16 +1353,17 @@ toolbar .toolbarbutton-1 > .toolbarbutto
     -moz-image-region: rect(0, 64px, 64px, 0);
   }
 
   /* Make sure that the state icons are not shown in the customization palette. */
   toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 64px, 64px, 0) !important;
   }
 
+  #loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
   #loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 128px, 64px, 64px);
   }
 
   #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
     -moz-image-region: rect(0, 192px, 64px, 128px);
   }
 
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -156,16 +156,17 @@ toolbarpaletteitem[place="palette"] > #l
   -moz-image-region: rect(0, 32px, 32px, 0);
 }
 
 /* Make sure that the state icons are not shown in the customization palette. */
 toolbarpaletteitem[place="palette"] > #loop-call-button > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 32px, 32px, 0) !important;
 }
 
+#loop-call-button[cui-areatype="menu-panel"][state="disabled"] > .toolbarbutton-badge-container,
 #loop-call-button[cui-areatype="menu-panel"][disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 64px, 32px, 32px);
 }
 
 #loop-call-button[cui-areatype="menu-panel"]:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 96px, 32px, 64px);
 }
 
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -172,16 +172,17 @@ toolbar[brighttext] #sync-button[status=
   list-style-image: url(chrome://browser/skin/loop/toolbar.png);
   -moz-image-region: rect(0, 18px, 18px, 0);
 }
 
 toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
   list-style-image: url(chrome://browser/skin/loop/toolbar-inverted.png);
 }
 
+#loop-call-button[state="disabled"] > .toolbarbutton-badge-container,
 #loop-call-button[disabled="true"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 36px, 18px, 18px);
 }
 
 #loop-call-button:not([disabled="true"])[state="error"] > .toolbarbutton-badge-container {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }