bug 891225 implement new provider status buttons, r=markh
authorShane Caraveo <scaraveo@mozilla.com>
Wed, 04 Sep 2013 10:01:38 -0700
changeset 145493 58a67d0f50bcde0e65e1c5d7f5d07422233a41d2
parent 145492 00209f73f7b636ae7ad780b86c6eaf831cb0092f
child 145494 7fd6c2e59b65fcc6727f403f0941771c173d3fae
push id25214
push userkwierso@gmail.com
push dateThu, 05 Sep 2013 00:02:20 +0000
treeherdermozilla-central@99bd249e5a20 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs891225
milestone26.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 891225 implement new provider status buttons, r=markh
browser/base/content/browser-social.js
browser/base/content/test/social/Makefile.in
browser/base/content/test/social/browser_addons.js
browser/base/content/test/social/browser_blocklist.js
browser/base/content/test/social/browser_social_mozSocial_API.js
browser/base/content/test/social/browser_social_status.js
browser/base/content/test/social/head.js
browser/base/content/test/social/social_activate.html
browser/base/content/test/social/social_worker.js
browser/modules/Social.jsm
toolkit/components/social/SocialService.jsm
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -32,16 +32,20 @@ SocialUI = {
   init: function SocialUI_init() {
     Services.obs.addObserver(this, "social:ambient-notification-changed", false);
     Services.obs.addObserver(this, "social:profile-changed", false);
     Services.obs.addObserver(this, "social:page-mark-config", false);
     Services.obs.addObserver(this, "social:frameworker-error", false);
     Services.obs.addObserver(this, "social:provider-set", false);
     Services.obs.addObserver(this, "social:providers-changed", false);
     Services.obs.addObserver(this, "social:provider-reload", false);
+    Services.obs.addObserver(this, "social:provider-installed", false);
+    Services.obs.addObserver(this, "social:provider-uninstalled", false);
+    Services.obs.addObserver(this, "social:provider-enabled", false);
+    Services.obs.addObserver(this, "social:provider-disabled", false);
 
     Services.prefs.addObserver("social.sidebar.open", this, false);
     Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
 
     gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
 
     if (!Social.initialized) {
       Social.init();
@@ -57,30 +61,46 @@ SocialUI = {
   uninit: function SocialUI_uninit() {
     Services.obs.removeObserver(this, "social:ambient-notification-changed");
     Services.obs.removeObserver(this, "social:profile-changed");
     Services.obs.removeObserver(this, "social:page-mark-config");
     Services.obs.removeObserver(this, "social:frameworker-error");
     Services.obs.removeObserver(this, "social:provider-set");
     Services.obs.removeObserver(this, "social:providers-changed");
     Services.obs.removeObserver(this, "social:provider-reload");
+    Services.obs.removeObserver(this, "social:provider-installed");
+    Services.obs.removeObserver(this, "social:provider-uninstalled");
+    Services.obs.removeObserver(this, "social:provider-enabled");
+    Services.obs.removeObserver(this, "social:provider-disabled");
 
     Services.prefs.removeObserver("social.sidebar.open", this);
     Services.prefs.removeObserver("social.toast-notifications.enabled", this);
   },
 
   _matchesCurrentProvider: function (origin) {
     return Social.provider && Social.provider.origin == origin;
   },
 
   observe: function SocialUI_observe(subject, topic, data) {
     // Exceptions here sometimes don't get reported properly, report them
     // manually :(
     try {
       switch (topic) {
+        case "social:provider-installed":
+          SocialStatus.setPosition(data);
+          break;
+        case "social:provider-uninstalled":
+          SocialStatus.removePosition(data);
+          break;
+        case "social:provider-enabled":
+          SocialStatus.populateToolbarPalette();
+          break;
+        case "social:provider-disabled":
+          SocialStatus.removeProvider(data);
+          break;
         case "social:provider-reload":
           // if the reloaded provider is our current provider, fall through
           // to social:provider-set so the ui will be reset
           if (!Social.provider || Social.provider.origin != data)
             return;
           // be sure to unload the sidebar as it will not reload if the origin
           // has not changed, it will be loaded in provider-set below. Other
           // panels will be unloaded or handle reload.
@@ -93,28 +113,31 @@ SocialUI = {
           this._updateMenuItems();
 
           SocialFlyout.unload();
           SocialChatBar.update();
           SocialShare.update();
           SocialSidebar.update();
           SocialMark.update();
           SocialToolbar.update();
+          SocialStatus.populateToolbarPalette();
           SocialMenu.populate();
           break;
         case "social:providers-changed":
           // the list of providers changed - this may impact the "active" UI.
           this._updateActiveUI();
           // and the multi-provider menu
           SocialToolbar.populateProviderMenus();
           SocialShare.populateProviderMenu();
+          SocialStatus.populateToolbarPalette();
           break;
 
         // Provider-specific notifications
         case "social:ambient-notification-changed":
+          SocialStatus.updateNotification(data);
           if (this._matchesCurrentProvider(data)) {
             SocialToolbar.updateButton();
             SocialMenu.populate();
           }
           break;
         case "social:profile-changed":
           if (this._matchesCurrentProvider(data)) {
             SocialToolbar.updateProvider();
@@ -1051,16 +1074,25 @@ SocialToolbar = {
         (!Social.provider.haveLoggedInUser() && Social.provider.profile !== undefined)) {
       // Either no enabled provider, or there is a provider and it has
       // responded with a profile and the user isn't loggedin.  The icons
       // etc have already been removed by updateButtonHiddenState, so we want
       // to nuke any cached icons we have and get out of here!
       Services.prefs.clearUserPref(CACHE_PREF_NAME);
       return;
     }
+
+    // If the provider uses the new SocialStatus button, then they do not get
+    // to use the ambient icons in the old toolbar button.  Since the status
+    // button depends on multiple workers, if not enabled we will ignore this
+    // limitation.  That allows a provider to migrate to the new functionality
+    // once we enable multiple workers.
+    if (Social.provider.statusURL && Social.allowMultipleWorkers)
+      return;
+
     let icons = Social.provider.ambientNotificationIcons;
     let iconNames = Object.keys(icons);
 
     if (Social.provider.profile === undefined) {
       // provider has not told us about the login state yet - see if we have
       // a cached version for this provider.
       let cached;
       try {
@@ -1149,19 +1181,18 @@ SocialToolbar = {
         ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText",
                                                         [ariaLabel, badge]);
       toolbarButton.setAttribute("aria-label", ariaLabel);
     }
     let socialToolbarItem = document.getElementById("social-toolbar-item");
     socialToolbarItem.insertBefore(toolbarButtons, SocialMark.button);
 
     for (let frame of createdFrames) {
-      if (frame.socialErrorListener) {
+      if (frame.socialErrorListener)
         frame.socialErrorListener.remove();
-      }
       if (frame.docShell) {
         frame.docShell.isActive = false;
         Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this));
       }
     }
   },
 
   showAmbientPopup: function SocialToolbar_showAmbientPopup(aToolbarButton) {
@@ -1196,21 +1227,20 @@ SocialToolbar = {
       aToolbarButton.parentNode.removeAttribute("open");
       dynamicResizer.stop();
       notificationFrame.docShell.isActive = false;
       dispatchPanelEvent("socialFrameHide");
     });
 
     panel.addEventListener("popupshown", function onpopupshown() {
       panel.removeEventListener("popupshown", onpopupshown);
-      // This attribute is needed on both the button and the
-      // containing toolbaritem since the buttons on OS X have
-      // moz-appearance:none, while their container gets
-      // moz-appearance:toolbarbutton due to the way that toolbar buttons
-      // get combined on OS X.
+      // The "open" attribute is needed on both the button and the containing
+      // toolbaritem since the buttons on OS X have moz-appearance:none, while
+      // their container gets moz-appearance:toolbarbutton due to the way that
+      // toolbar buttons get combined on OS X.
       aToolbarButton.setAttribute("open", "true");
       aToolbarButton.parentNode.setAttribute("open", "true");
       notificationFrame.docShell.isActive = true;
       notificationFrame.docShell.isAppTab = true;
       if (notificationFrame.contentDocument.readyState == "complete" && wasAlive) {
         dynamicResizer.start(panel, notificationFrame);
         dispatchPanelEvent("socialFrameShow");
       } else {
@@ -1387,9 +1417,358 @@ SocialSidebar = {
       sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure");
     } else {
       let url = encodeURIComponent(Social.provider.sidebarURL);
       sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url, null, null);
     }
   }
 }
 
+// this helper class is used by removable/customizable buttons to handle
+// location persistence and insertion into palette and/or toolbars
+
+// When a provider is installed we show all their UI so the user will see the
+// functionality of what they installed. The user can later customize the UI,
+// moving buttons around or off the toolbar.
+//
+// To make this happen, on install we add a button id to the navbar currentset.
+// On enabling the provider (happens just after install) we insert the button
+// into the toolbar as well. The button is then persisted on restart (assuming
+// it was not removed).
+//
+// When a provider is disabled, we do not remove the buttons from currentset.
+// That way, if the provider is re-enabled during the same session, the buttons
+// will reappear where they were before. When a provider is uninstalled, we make
+// sure that the id is removed from currentset.
+//
+// On startup, we insert the buttons of any enabled provider into either the
+// apropriate toolbar or the palette.
+function ToolbarHelper(type, createButtonFn) {
+  this._createButton = createButtonFn;
+  this._type = type;
+}
+
+ToolbarHelper.prototype = {
+  idFromOrgin: function(origin) {
+    return this._type + "-" + origin;
+  },
+
+  // find a button either in the document or the palette
+  _getExistingButton: function(id) {
+    let button = document.getElementById(id);
+    if (button)
+      return button;
+    let palette = document.getElementById("navigator-toolbox").palette;
+    let paletteItem = palette.firstChild;
+    while (paletteItem) {
+      if (paletteItem.id == id)
+        return paletteItem;
+      paletteItem = paletteItem.nextSibling;
+    }
+    return null;
+  },
+
+  setPersistentPosition: function(id) {
+    // called when a provider is installed.  add provider buttons to nav-bar
+    let toolbar = document.getElementById("nav-bar");
+    // first startups will not have a currentset attribute, always rely on
+    // currentSet since it will be derived from the defaultset in that case.
+    let currentset = toolbar.currentSet;
+    if (currentset == "__empty")
+      currentset = []
+    else
+      currentset = currentset.split(",");
+    if (currentset.indexOf(id) >= 0)
+      return;
+    // we do not set toolbar.currentSet since that will try to add the button,
+    // and we have not added it yet (happens on provider being enabled)
+    currentset.push(id);
+    toolbar.setAttribute("currentset", currentset.join(","));
+    document.persist(toolbar.id, "currentset");
+  },
+
+  removeProviderButton: function(origin) {
+    // this will remove the button from the palette or the toolbar
+    let button = this._getExistingButton(this.idFromOrgin(origin));
+    if (button)
+      button.parentNode.removeChild(button);
+  },
+
+  removePersistence: function(id) {
+    let persisted = document.querySelectorAll("*[currentset]");
+    for (let pent of persisted) {
+      // the button will have been removed, but left in the currentset attribute
+      // in case the user re-enables (e.g. undo in addon manager). So we only
+      // check the attribute here.
+      let currentset = pent.getAttribute("currentset").split(",");
+
+      let pos = currentset.indexOf(id);
+      if (pos >= 0) {
+        currentset.splice(pos, 1);
+        pent.setAttribute("currentset", currentset.join(","));
+        document.persist(pent.id, "currentset");
+        return;
+      }
+    }
+  },
+
+  // if social is entirely disabled, we need to clear the palette, but leave
+  // the persisted id's in place
+  clearPalette: function() {
+    [this.removeProviderButton(p.origin) for (p of Social.providers)];
+  },
+
+  // should be called on startup of each window, otherwise the addon manager
+  // listener will handle new activations, or enable/disabling of a provider
+  // XXX we currently call more regularly, will fix during refactoring
+  populatePalette: function() {
+    if (!Social.enabled) {
+      this.clearPalette();
+      return;
+    }
+    let persisted = document.querySelectorAll("*[currentset]");
+    let persistedById = {};
+    for (let pent of persisted) {
+      let pset = pent.getAttribute("currentset").split(',');
+      for (let id of pset)
+        persistedById[id] = pent;
+    }
+
+    // create any buttons that do not exist yet if they have been persisted
+    // as a part of the UI (otherwise they belong in the palette).
+    for (let provider of Social.providers) {
+      let id = this.idFromOrgin(provider.origin);
+      if (this._getExistingButton(id))
+        return;
+      let button = this._createButton(provider);
+      if (button && persistedById.hasOwnProperty(id)) {
+        let parent = persistedById[id];
+        let pset = persistedById[id].getAttribute("currentset").split(',');
+        let pi = pset.indexOf(id) + 1;
+        let next = document.getElementById(pset[pi]);
+        parent.insertItem(id, next, null, false);
+      }
+    }
+  }
+}
+
+SocialStatus = {
+  populateToolbarPalette: function() {
+    if (!Social.allowMultipleWorkers)
+      return;
+    this._toolbarHelper.populatePalette();
+  },
+
+  setPosition: function(origin) {
+    if (!Social.allowMultipleWorkers)
+      return;
+    // this is called during install, before the provider is enabled so we have
+    // to use the manifest rather than the provider instance as we do elsewhere.
+    let manifest = Social.getManifestByOrigin(origin);
+    if (!manifest.statusURL)
+      return;
+    let tbh = this._toolbarHelper;
+    tbh.setPersistentPosition(tbh.idFromOrgin(origin));
+  },
+
+  removePosition: function(origin) {
+    if (!Social.allowMultipleWorkers)
+      return;
+    let tbh = this._toolbarHelper;
+    tbh.removePersistence(tbh.idFromOrgin(origin));
+  },
+
+  removeProvider: function(origin) {
+    if (!Social.allowMultipleWorkers)
+      return;
+    this._toolbarHelper.removeProviderButton(origin);
+  },
+
+  get _toolbarHelper() {
+    delete this._toolbarHelper;
+    this._toolbarHelper = new ToolbarHelper("social-status-button", this._createButton.bind(this));
+    return this._toolbarHelper;
+  },
+
+  get _dynamicResizer() {
+    delete this._dynamicResizer;
+    this._dynamicResizer = new DynamicResizeWatcher();
+    return this._dynamicResizer;
+  },
+
+  _createButton: function(provider) {
+    if (!provider.statusURL)
+      return null;
+    let palette = document.getElementById("navigator-toolbox").palette;
+    let button = document.createElement("toolbarbutton");
+    button.setAttribute("class", "toolbarbutton-1 social-status-button");
+    button.setAttribute("type", "badged");
+    button.setAttribute("removable", "true");
+    button.setAttribute("image", provider.iconURL);
+    button.setAttribute("label", provider.name);
+    button.setAttribute("tooltiptext", provider.name);
+    button.setAttribute("origin", provider.origin);
+    button.setAttribute("oncommand", "SocialStatus.showPopup(this);");
+    button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin));
+    palette.appendChild(button);
+    return button;
+  },
+
+  // status panels are one-per button per-process, we swap the docshells between
+  // windows when necessary
+  _attachNotificatonPanel: function(aButton, provider) {
+    let panel = document.getElementById("social-notification-panel");
+    panel.hidden = !SocialUI.enabled;
+    let notificationFrameId = "social-status-" + provider.origin;
+    let frame = document.getElementById(notificationFrameId);
+
+    if (!frame) {
+      frame = SharedFrame.createFrame(
+        notificationFrameId, /* frame name */
+        panel, /* parent */
+        {
+          "type": "content",
+          "mozbrowser": "true",
+          "class": "social-panel-frame",
+          "id": notificationFrameId,
+          "tooltip": "aHTMLTooltip",
+
+          // work around bug 793057 - by making the panel roughly the final size
+          // we are more likely to have the anchor in the correct position.
+          "style": "width: " + PANEL_MIN_WIDTH + "px;",
+
+          "origin": provider.origin,
+          "src": provider.statusURL
+        }
+      );
+
+      if (frame.socialErrorListener)
+        frame.socialErrorListener.remove();
+      if (frame.docShell) {
+        frame.docShell.isActive = false;
+        Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this));
+      }
+    } else {
+      frame.setAttribute("origin", provider.origin);
+      SharedFrame.updateURL(notificationFrameId, provider.statusURL);
+    }
+    aButton.setAttribute("notificationFrameId", notificationFrameId);
+  },
+
+  updateNotification: function(origin) {
+    if (!Social.allowMultipleWorkers)
+      return;
+    let provider = Social._getProviderFromOrigin(origin);
+    let button = document.getElementById(this._toolbarHelper.idFromOrgin(provider.origin));
+    if (button) {
+      // we only grab the first notification, ignore all others
+      let icons = provider.ambientNotificationIcons;
+      let iconNames = Object.keys(icons);
+      let notif = icons[iconNames[0]];
+      if (!notif) {
+        button.setAttribute("badge", "");
+        button.setAttribute("aria-label", "");
+        button.setAttribute("tooltiptext", "");
+        return;
+      }
+
+      button.style.listStyleImage = "url(" + notif.iconURL || provider.iconURL + ")";
+      button.setAttribute("tooltiptext", notif.label);
+
+      let badge = notif.counter || "";
+      button.setAttribute("badge", badge);
+      let ariaLabel = notif.label;
+      // if there is a badge value, we must use a localizable string to insert it.
+      if (badge)
+        ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText",
+                                                        [ariaLabel, badge]);
+      button.setAttribute("aria-label", ariaLabel);
+    }
+  },
+
+  showPopup: function(aToolbarButton) {
+    if (!Social.allowMultipleWorkers)
+      return;
+    // attach our notification panel if necessary
+    let origin = aToolbarButton.getAttribute("origin");
+    let provider = Social._getProviderFromOrigin(origin);
+    this._attachNotificatonPanel(aToolbarButton, provider);
+
+    let panel = document.getElementById("social-notification-panel");
+    let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId");
+    let notificationFrame = document.getElementById(notificationFrameId);
+
+    let wasAlive = SharedFrame.isGroupAlive(notificationFrameId);
+    SharedFrame.setOwner(notificationFrameId, notificationFrame);
+
+    // Clear dimensions on all browsers so the panel size will
+    // only use the selected browser.
+    let frameIter = panel.firstElementChild;
+    while (frameIter) {
+      frameIter.collapsed = (frameIter != notificationFrame);
+      frameIter = frameIter.nextElementSibling;
+    }
+
+    function dispatchPanelEvent(name) {
+      let evt = notificationFrame.contentDocument.createEvent("CustomEvent");
+      evt.initCustomEvent(name, true, true, {});
+      notificationFrame.contentDocument.documentElement.dispatchEvent(evt);
+    }
+
+    let dynamicResizer = this._dynamicResizer;
+    panel.addEventListener("popuphidden", function onpopuphiding() {
+      panel.removeEventListener("popuphidden", onpopuphiding);
+      aToolbarButton.removeAttribute("open");
+      dynamicResizer.stop();
+      notificationFrame.docShell.isActive = false;
+      dispatchPanelEvent("socialFrameHide");
+    });
+
+    panel.addEventListener("popupshown", function onpopupshown() {
+      panel.removeEventListener("popupshown", onpopupshown);
+      // This attribute is needed on both the button and the
+      // containing toolbaritem since the buttons on OS X have
+      // moz-appearance:none, while their container gets
+      // moz-appearance:toolbarbutton due to the way that toolbar buttons
+      // get combined on OS X.
+      aToolbarButton.setAttribute("open", "true");
+      notificationFrame.docShell.isActive = true;
+      notificationFrame.docShell.isAppTab = true;
+      if (notificationFrame.contentDocument.readyState == "complete" && wasAlive) {
+        dynamicResizer.start(panel, notificationFrame);
+        dispatchPanelEvent("socialFrameShow");
+      } else {
+        // first time load, wait for load and dispatch after load
+        notificationFrame.addEventListener("load", function panelBrowserOnload(e) {
+          notificationFrame.removeEventListener("load", panelBrowserOnload, true);
+          dynamicResizer.start(panel, notificationFrame);
+          dispatchPanelEvent("socialFrameShow");
+        }, true);
+      }
+    });
+
+    let navBar = document.getElementById("nav-bar");
+    let anchor = navBar.getAttribute("mode") == "text" ?
+                   document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-text") :
+                   document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container");
+    // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup
+    // handling from preventing it being opened in some cases.
+    setTimeout(function() {
+      panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
+    }, 0);
+  },
+
+  setPanelErrorMessage: function(aNotificationFrame) {
+    if (!aNotificationFrame)
+      return;
+
+    let src = aNotificationFrame.getAttribute("src");
+    aNotificationFrame.removeAttribute("src");
+    aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" +
+                                             encodeURIComponent(src),
+                                             null, null, null, null);
+    let panel = aNotificationFrame.parentNode;
+    sizeSocialPanelToContent(panel, aNotificationFrame);
+  },
+
+};
+
 })();
--- a/browser/base/content/test/social/Makefile.in
+++ b/browser/base/content/test/social/Makefile.in
@@ -26,16 +26,17 @@ MOCHITEST_BROWSER_FILES = \
                  browser_social_mozSocial_API.js \
                  browser_social_isVisible.js \
                  browser_social_chatwindow.js \
                  browser_social_chatwindow_resize.js \
                  browser_social_chatwindowfocus.js \
                  browser_social_multiprovider.js \
                  browser_social_multiworker.js \
                  browser_social_errorPage.js \
+                 browser_social_status.js \
                  browser_social_window.js \
                  social_activate.html \
                  social_activate_iframe.html \
                  browser_share.js \
                  social_panel.html \
                  social_mark_image.png \
                  social_sidebar.html \
                  social_chat.html \
--- a/browser/base/content/test/social/browser_addons.js
+++ b/browser/base/content/test/social/browser_addons.js
@@ -45,19 +45,20 @@ function test() {
     finish();
   });
 }
 
 function installListener(next, aManifest) {
   let expectEvent = "onInstalling";
   let prefname = getManifestPrefname(aManifest);
   // wait for the actual removal to call next
-  SocialService.registerProviderListener(function providerListener(topic, data) {
-    if (topic == "provider-removed") {
+  SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
+    if (topic == "provider-disabled") {
       SocialService.unregisterProviderListener(providerListener);
+      is(origin, aManifest.origin, "provider disabled");
       executeSoon(next);
     }
   });
 
   return {
     onInstalling: function(addon) {
       is(expectEvent, "onInstalling", "install started");
       is(addon.manifest.origin, aManifest.origin, "provider about to be installed");
@@ -290,24 +291,25 @@ var tests = {
       let installFrom = doc.nodePrincipal.origin;
       Services.prefs.setCharPref("social.whitelist", installFrom);
       Social.installProvider(doc, manifest2, function(addonManifest) {
         SocialService.addBuiltinProvider(addonManifest.origin, function(provider) {
           is(provider.manifest.version, 1, "manifest version is 1");
           Social.enabled = true;
 
           // watch for the provider-update and test the new version
-          SocialService.registerProviderListener(function providerListener(topic, data) {
+          SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
             if (topic != "provider-update")
               return;
+            is(origin, addonManifest.origin, "provider updated")
             SocialService.unregisterProviderListener(providerListener);
             Services.prefs.clearUserPref("social.whitelist");
-            let provider = Social._getProviderFromOrigin(addonManifest.origin);
+            let provider = Social._getProviderFromOrigin(origin);
             is(provider.manifest.version, 2, "manifest version is 2");
-            Social.uninstallProvider(addonManifest.origin, function() {
+            Social.uninstallProvider(origin, function() {
               gBrowser.removeTab(tab);
               next();
             });
           });
 
           let port = provider.getWorkerPort();
           port.onmessage = function (e) {
             let topic = e.data.topic;
--- a/browser/base/content/test/social/browser_blocklist.js
+++ b/browser/base/content/test/social/browser_blocklist.js
@@ -150,24 +150,25 @@ var tests = {
       onWindowTitleChange: function(aXULWindow, aNewTitle) { }
     };
 
     Services.wm.addListener(listener);
 
     setManifestPref("social.manifest.blocked", manifest_bad);
     try {
       SocialService.addProvider(manifest_bad, function(provider) {
-        // the act of blocking should cause a 'provider-removed' notification
+        // the act of blocking should cause a 'provider-disabled' notification
         // from SocialService.
-        SocialService.registerProviderListener(function providerListener(topic) {
-          if (topic != "provider-removed")
+        SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
+          if (topic != "provider-disabled")
             return;
           SocialService.unregisterProviderListener(providerListener);
+          is(origin, provider.origin, "provider disabled");
           SocialService.getProvider(provider.origin, function(p) {
-            ok(p==null, "blocklisted provider removed");
+            ok(p == null, "blocklisted provider disabled");
             Services.prefs.clearUserPref("social.manifest.blocked");
             resetBlocklist(finish);
           });
         });
         // no callback - the act of updating should cause the listener above
         // to fire.
         setAndUpdateBlocklist(blocklistURL);
       });
--- a/browser/base/content/test/social/browser_social_mozSocial_API.js
+++ b/browser/base/content/test/social/browser_social_mozSocial_API.js
@@ -14,16 +14,23 @@ function test() {
   };
   runSocialTestWithProvider(manifest, function (finishcb) {
     runSocialTests(tests, undefined, undefined, finishcb);
   });
 }
 
 var tests = {
   testStatusIcons: function(next) {
+    let icon = {
+      name: "testIcon",
+      iconURL: "chrome://browser/skin/Info.png",
+      contentPanel: "https://example.com/browser/browser/base/content/test/social/social_panel.html",
+      counter: 1
+    };
+
     let iconsReady = false;
     let gotSidebarMessage = false;
 
     function checkNext() {
       if (iconsReady && gotSidebarMessage)
         triggerIconPanel();
     }
 
@@ -66,16 +73,16 @@ var tests = {
             next();
           }
           break;
         case "got-sidebar-message":
           // The sidebar message will always come first, since it loads by default
           ok(true, "got sidebar message");
           gotSidebarMessage = true;
           // load a status panel
-          port.postMessage({topic: "test-ambient-notification"});
+          port.postMessage({topic: "test-ambient-notification", data: icon});
           checkNext();
           break;
       }
     }
     port.postMessage({topic: "test-init"});
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/browser_social_status.js
@@ -0,0 +1,220 @@
+/* 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/. */
+
+let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
+
+let manifest = { // builtin provider
+  name: "provider example.com",
+  origin: "https://example.com",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
+  iconURL: "https://example.com/browser/browser/base/content/test/moz.png"
+};
+let manifest2 = { // used for testing install
+  name: "provider test1",
+  origin: "https://test1.example.com",
+  workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
+  statusURL: "https://test1.example.com/browser/browser/base/content/test/social/social_panel.html",
+  iconURL: "https://test1.example.com/browser/browser/base/content/test/moz.png",
+  version: 1
+};
+let manifest3 = { // used for testing install
+  name: "provider test2",
+  origin: "https://test2.example.com",
+  sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html",
+  iconURL: "https://test2.example.com/browser/browser/base/content/test/moz.png",
+  version: 1
+};
+
+
+function openWindowAndWaitForInit(callback) {
+  let topic = "browser-delayed-startup-finished";
+  let w = OpenBrowserWindow();
+  Services.obs.addObserver(function providerSet(subject, topic, data) {
+    Services.obs.removeObserver(providerSet, topic);
+    executeSoon(() => callback(w));
+  }, topic, false);
+}
+
+function test() {
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
+  let toolbar = document.getElementById("nav-bar");
+  let currentsetAtStart = toolbar.currentSet;
+  info("tb0 "+currentsetAtStart);
+  runSocialTestWithProvider(manifest, function () {
+    runSocialTests(tests, undefined, undefined, function () {
+      Services.prefs.clearUserPref("social.remote-install.enabled");
+      // just in case the tests failed, clear these here as well
+      Services.prefs.clearUserPref("social.allowMultipleWorkers");
+      Services.prefs.clearUserPref("social.whitelist");
+      
+      // This post-test test ensures that a new window maintains the same
+      // toolbar button set as when we started. That means our insert/removal of
+      // persistent id's is working correctly
+      is(currentsetAtStart, toolbar.currentSet, "toolbar currentset unchanged");
+      openWindowAndWaitForInit(function(w1) {
+        checkSocialUI(w1);
+        // Sometimes the new window adds other buttons to currentSet that are
+        // outside the scope of what we're checking. So we verify that all
+        // buttons from startup are in currentSet for a new window, and that the
+        // provider buttons are properly removed. (e.g on try, window-controls
+        // was not present in currentsetAtStart, but present on the second
+        // window)
+        let tb1 = w1.document.getElementById("nav-bar");
+        info("tb0 "+toolbar.currentSet);
+        info("tb1 "+tb1.currentSet);
+        let startupSet = Set(toolbar.currentSet.split(','));
+        let newSet = Set(tb1.currentSet.split(','));
+        let intersect = Set([x for (x of startupSet) if (newSet.has(x))]);
+        info("intersect "+intersect);
+        let difference = Set([x for (x of newSet) if (!startupSet.has(x))]);
+        info("difference "+difference);
+        is(startupSet.size, intersect.size, "new window toolbar same as old");
+        // verify that our provider buttons are not in difference
+        let id = SocialStatus._toolbarHelper.idFromOrgin(manifest2.origin);
+        ok(!difference.has(id), "status button not persisted at end");
+        w1.close();
+        finish();
+      });
+    });
+  });
+}
+
+var tests = {
+  testNoButtonOnInstall: function(next) {
+    // we expect the addon install dialog to appear, we need to accept the
+    // install from the dialog.
+    info("Waiting for install dialog");
+    let panel = document.getElementById("servicesInstall-notification");
+    PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
+      PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    })
+
+    let id = "social-status-button-" + manifest3.origin;
+    let toolbar = document.getElementById("nav-bar");
+    let currentset = toolbar.getAttribute("currentset").split(',');
+    ok(currentset.indexOf(id) < 0, "button is not part of currentset at start");
+
+    let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    addTab(activationURL, function(tab) {
+      let doc = tab.linkedBrowser.contentDocument;
+      Social.installProvider(doc, manifest3, function(addonManifest) {
+        // enable the provider so we know the button would have appeared
+        SocialService.addBuiltinProvider(manifest3.origin, function(provider) {
+          ok(provider, "provider is installed");
+          currentset = toolbar.getAttribute("currentset").split(',');
+          ok(currentset.indexOf(id) < 0, "button was not added to currentset");
+          Social.uninstallProvider(manifest3.origin, function() {
+            gBrowser.removeTab(tab);
+            next();
+          });
+        });
+      });
+    });
+  },
+  testButtonOnInstall: function(next) {
+    // we expect the addon install dialog to appear, we need to accept the
+    // install from the dialog.
+    info("Waiting for install dialog");
+    let panel = document.getElementById("servicesInstall-notification");
+    PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() {
+      PopupNotifications.panel.removeEventListener("popupshown", onpopupshown);
+      info("servicesInstall-notification panel opened");
+      panel.button.click();
+    })
+
+    let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html"
+    addTab(activationURL, function(tab) {
+      let doc = tab.linkedBrowser.contentDocument;
+      Social.installProvider(doc, manifest2, function(addonManifest) {
+          // at this point, we should have a button id in the currentset for our provider
+          let id = "social-status-button-" + manifest2.origin;
+          let toolbar = document.getElementById("nav-bar");
+
+          waitForCondition(function() {
+                             let currentset = toolbar.getAttribute("currentset").split(',');
+                             return currentset.indexOf(id) >= 0;
+                           },
+                           function() {
+                             // no longer need the tab
+                             gBrowser.removeTab(tab);
+                             next();
+                           }, "status button added to currentset");
+      });
+    });
+  },
+  testButtonOnEnable: function(next) {
+    // enable the provider now
+    SocialService.addBuiltinProvider(manifest2.origin, function(provider) {
+      ok(provider, "provider is installed");
+      let id = "social-status-button-" + manifest2.origin;
+      waitForCondition(function() { return document.getElementById(id) },
+                       next, "button exists after enabling social");
+    });
+  },
+  testStatusPanel: function(next) {
+    let icon = {
+      name: "testIcon",
+      iconURL: "chrome://browser/skin/Info.png",
+      counter: 1
+    };
+    // click on panel to open and wait for visibility
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    let id = "social-status-button-" + provider.origin;
+    let btn = document.getElementById(id)
+    ok(btn, "got a status button");
+    let port = provider.getWorkerPort();
+
+    port.onmessage = function (e) {
+      let topic = e.data.topic;
+      switch (topic) {
+        case "test-init-done":
+          ok(true, "test-init-done received");
+          ok(provider.profile.userName, "profile was set by test worker");
+          btn.click();
+          break;
+        case "got-social-panel-visibility":
+          ok(true, "got the panel message " + e.data.result);
+          if (e.data.result == "shown") {
+            let panel = document.getElementById("social-notification-panel");
+            panel.hidePopup();
+          } else {
+            port.postMessage({topic: "test-ambient-notification", data: icon});
+            port.close();
+            waitForCondition(function() { return btn.getAttribute("badge"); },
+                       function() {
+                         is(btn.style.listStyleImage, "url(\"" + icon.iconURL + "\")", "notification icon updated");
+                         next();
+                       }, "button updated by notification");
+          }
+          break;
+      }
+    };
+    port.postMessage({topic: "test-init"});
+  },
+  testButtonOnDisable: function(next) {
+    // enable the provider now
+    let provider = Social._getProviderFromOrigin(manifest2.origin);
+    ok(provider, "provider is installed");
+    SocialService.removeProvider(manifest2.origin, function() {
+      let id = "social-status-button-" + manifest2.origin;
+      waitForCondition(function() { return !document.getElementById(id) },
+                       next, "button does not exist after disabling the provider");
+    });
+  },
+  testButtonOnUninstall: function(next) {
+    Social.uninstallProvider(manifest2.origin, function() {
+      // test that the button is no longer persisted
+      let id = "social-status-button-" + manifest2.origin;
+      let toolbar = document.getElementById("nav-bar");
+      let currentset = toolbar.getAttribute("currentset").split(',');
+      is(currentset.indexOf(id), -1, "button no longer in currentset");
+      next();
+    });
+  }
+}
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -172,17 +172,17 @@ function runSocialTests(tests, cbPreTest
     // We run on a timeout as the frameworker also makes use of timeouts, so
     // this helps keep the debug messages sane.
     executeSoon(function() {
       function cleanupAndRunNextTest() {
         info("sub-test " + name + " complete");
         cbPostTest(runNextTest);
       }
       cbPreTest(function() {
-        is(providersAtStart, Social.providers.length, "pre-test: no new providers left enabled");
+        info("pre-test: starting with " + Social.providers.length + " providers");
         info("sub-test " + name + " starting");
         try {
           func.call(tests, cleanupAndRunNextTest);
         } catch (ex) {
           ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
           cleanupAndRunNextTest();
         }
       })
--- a/browser/base/content/test/social/social_activate.html
+++ b/browser/base/content/test/social/social_activate.html
@@ -9,16 +9,17 @@ var data = {
   "name": "Demo Social Service",
   "iconURL": "chrome://branding/content/icon16.png",
   "icon32URL": "chrome://branding/content/favicon32.png",
   "icon64URL": "chrome://branding/content/icon64.png",
 
   // at least one of these must be defined
   "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar.html",
   "workerURL": "/browser/browser/base/content/test/social/social_worker.js",
+  "statusURL": "/browser/browser/base/content/test/social/social_panel.html",
 
   // should be available for display purposes
   "description": "A short paragraph about this provider",
   "author": "Shane Caraveo, Mozilla",
 
   // optional
   "version": 1
 }
--- a/browser/base/content/test/social/social_worker.js
+++ b/browser/base/content/test/social/social_worker.js
@@ -114,23 +114,17 @@ onconnect = function(e) {
               markedTooltip: "Unmark this page",
               unmarkedLabel: "Mark",
               markedLabel: "Unmark",
             }
           }
         });
         break;
       case "test-ambient-notification":
-        let icon = {
-          name: "testIcon",
-          iconURL: "chrome://browser/skin/Info.png",
-          contentPanel: "https://example.com/browser/browser/base/content/test/social/social_panel.html",
-          counter: 1
-        };
-        apiPort.postMessage({topic: "social.ambient-notification", data: icon});
+        apiPort.postMessage({topic: "social.ambient-notification", data: event.data.data});
         break;
       case "test-isVisible":
         sidebarPort.postMessage({topic: "test-isVisible"});
         break;
       case "test-isVisible-response":
         testPort.postMessage({topic: "got-isVisible-response", result: event.data.result});
         break;
       case "share-data-message":
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -164,34 +164,38 @@ this.Social = {
       // Retrieve the current set of providers, and set the current provider.
       SocialService.getOrderedProviderList(function (providers) {
         Social._updateProviderCache(providers);
         Social._updateWorkerState(true);
       });
     }
 
     // Register an observer for changes to the provider list
-    SocialService.registerProviderListener(function providerListener(topic, data) {
+    SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
       // An engine change caused by adding/removing a provider should notify.
       // any providers we receive are enabled in the AddonsManager
-      if (topic == "provider-added" || topic == "provider-removed") {
-        Social._updateProviderCache(data);
+      if (topic == "provider-installed" || topic == "provider-uninstalled") {
+        // installed/uninstalled do not send the providers param
+        Services.obs.notifyObservers(null, "social:" + topic, origin);
+        return;
+      }
+      if (topic == "provider-enabled" || topic == "provider-disabled") {
+        Social._updateProviderCache(providers);
         Social._updateWorkerState(true);
         Services.obs.notifyObservers(null, "social:providers-changed", null);
+        Services.obs.notifyObservers(null, "social:" + topic, origin);
         return;
       }
       if (topic == "provider-update") {
         // a provider has self-updated its manifest, we need to update our cache
         // and reload the provider.
-        let provider = data;
-        SocialService.getOrderedProviderList(function(providers) {
-          Social._updateProviderCache(providers);
-          provider.reload();
-          Services.obs.notifyObservers(null, "social:providers-changed", null);
-        });
+        Social._updateProviderCache(providers);
+        let provider = Social._getProviderFromOrigin(origin);
+        provider.reload();
+        Services.obs.notifyObservers(null, "social:providers-changed", null);
       }
     });
   },
 
   _updateWorkerState: function(enable) {
     // ensure that our providers are all disabled, and enabled if we allow
     // multiple workers
     if (enable && !Social.allowMultipleWorkers)
@@ -254,16 +258,20 @@ this.Social = {
     for (let p of this.providers) {
       if (p.origin == origin) {
         return p;
       }
     }
     return null;
   },
 
+  getManifestByOrigin: function(origin) {
+    return SocialService.getManifestByOrigin(origin);
+  },
+
   installProvider: function(doc, data, installCallback) {
     SocialService.installProvider(doc, data, installCallback);
   },
 
   uninstallProvider: function(origin, aCallback) {
     SocialService.uninstallProvider(origin, aCallback);
   },
 
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -57,24 +57,16 @@ let SocialServiceInternal = {
         if (manifest && typeof(manifest) == "object" && manifest.origin)
           yield manifest;
       } catch (err) {
         Cu.reportError("SocialService: failed to load manifest: " + pref +
                        ", exception: " + err);
       }
     }
   },
-  getManifestByOrigin: function(origin) {
-    for (let manifest of SocialServiceInternal.manifests) {
-      if (origin == manifest.origin) {
-        return manifest;
-      }
-    }
-    return null;
-  },
   getManifestPrefname: function(origin) {
     // Retrieve the prefname for a given origin/manifest.
     // If no existing pref, return a generated prefname.
     let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
     let prefs = MANIFEST_PREFS.getChildList("", []);
     for (let pref of prefs) {
       try {
         var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data);
@@ -386,17 +378,17 @@ this.SocialService = {
   // provider exists, or the activated provider on success.
   addBuiltinProvider: function addBuiltinProvider(origin, onDone) {
     if (SocialServiceInternal.providers[origin]) {
       schedule(function() {
         onDone(SocialServiceInternal.providers[origin]);
       });
       return;
     }
-    let manifest = SocialServiceInternal.getManifestByOrigin(origin);
+    let manifest = SocialService.getManifestByOrigin(origin);
     if (manifest) {
       let addon = new AddonWrapper(manifest);
       AddonManagerPrivate.callAddonListeners("onEnabling", addon, false);
       addon.pendingOperations |= AddonManager.PENDING_ENABLE;
       this.addProvider(manifest, onDone);
       addon.pendingOperations -= AddonManager.PENDING_ENABLE;
       AddonManagerPrivate.callAddonListeners("onEnabled", addon);
       return;
@@ -411,30 +403,30 @@ this.SocialService = {
     if (SocialServiceInternal.providers[manifest.origin])
       throw new Error("SocialService.addProvider: provider with this origin already exists");
 
     let provider = new SocialProvider(manifest);
     SocialServiceInternal.providers[provider.origin] = provider;
     ActiveProviders.add(provider.origin);
 
     this.getOrderedProviderList(function (providers) {
-      this._notifyProviderListeners("provider-added", providers);
+      this._notifyProviderListeners("provider-enabled", provider.origin, providers);
       if (onDone)
         onDone(provider);
     }.bind(this));
   },
 
   // Removes a provider with the given origin, and notifies when the removal is
   // complete.
   removeProvider: function removeProvider(origin, onDone) {
     if (!(origin in SocialServiceInternal.providers))
       throw new Error("SocialService.removeProvider: no provider with origin " + origin + " exists!");
 
     let provider = SocialServiceInternal.providers[origin];
-    let manifest = SocialServiceInternal.getManifestByOrigin(origin);
+    let manifest = SocialService.getManifestByOrigin(origin);
     let addon = manifest && new AddonWrapper(manifest);
     if (addon) {
       AddonManagerPrivate.callAddonListeners("onDisabling", addon, false);
       addon.pendingOperations |= AddonManager.PENDING_DISABLE;
     }
     provider.enabled = false;
 
     ActiveProviders.delete(provider.origin);
@@ -445,17 +437,17 @@ this.SocialService = {
       // we have to do this now so the addon manager ui will update an uninstall
       // correctly.
       addon.pendingOperations -= AddonManager.PENDING_DISABLE;
       AddonManagerPrivate.callAddonListeners("onDisabled", addon);
       AddonManagerPrivate.notifyAddonChanged(addon.id, ADDON_TYPE_SERVICE, false);
     }
 
     this.getOrderedProviderList(function (providers) {
-      this._notifyProviderListeners("provider-removed", providers);
+      this._notifyProviderListeners("provider-disabled", origin, providers);
       if (onDone)
         onDone();
     }.bind(this));
   },
 
   // Returns a single provider object with the specified origin.  The provider
   // must be "installed" (ie, in ActiveProviders)
   getProvider: function getProvider(origin, onDone) {
@@ -466,16 +458,25 @@ this.SocialService = {
 
   // Returns an unordered array of installed providers
   getProviderList: function(onDone) {
     schedule(function () {
       onDone(SocialServiceInternal.providerArray);
     });
   },
 
+  getManifestByOrigin: function(origin) {
+    for (let manifest of SocialServiceInternal.manifests) {
+      if (origin == manifest.origin) {
+        return manifest;
+      }
+    }
+    return null;
+  },
+
   // Returns an array of installed providers, sorted by frecency
   getOrderedProviderList: function(onDone) {
     SocialServiceInternal.orderedProviders(onDone);
   },
 
   getOriginActivationType: function (origin) {
     return getOriginActivationType(origin);
   },
@@ -483,28 +484,28 @@ this.SocialService = {
   _providerListeners: new Map(),
   registerProviderListener: function registerProviderListener(listener) {
     this._providerListeners.set(listener, 1);
   },
   unregisterProviderListener: function unregisterProviderListener(listener) {
     this._providerListeners.delete(listener);
   },
 
-  _notifyProviderListeners: function (topic, data) {
+  _notifyProviderListeners: function (topic, origin, providers) {
     for (let [listener, ] of this._providerListeners) {
       try {
-        listener(topic, data);
+        listener(topic, origin, providers);
       } catch (ex) {
         Components.utils.reportError("SocialService: provider listener threw an exception: " + ex);
       }
     }
   },
 
   _manifestFromData: function(type, data, principal) {
-    let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL'];
+    let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL'];
 
     if (type == 'directory') {
       // directory provided manifests must have origin in manifest, use that
       if (!data['origin']) {
         Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin.");
         return null;
       }
       let URI = Services.io.newURI(data.origin, null, null);
@@ -512,18 +513,19 @@ this.SocialService = {
     }
     // force/fixup origin
     data.origin = principal.origin;
 
     // workerURL, sidebarURL is required and must be same-origin
     // iconURL and name are required
     // iconURL may be a different origin (CDN or data url support) if this is
     // a whitelisted or directory listed provider
-    if (!data['workerURL'] && !data['sidebarURL'] && !data['shareURL']) {
-      Cu.reportError("SocialService.manifestFromData manifest missing required workerURL or sidebarURL.");
+    let providerHasFeatures = [url for (url of sameOriginRequired) if (data[url])].length > 0;
+    if (!providerHasFeatures) {
+      Cu.reportError("SocialService.manifestFromData manifest missing required urls.");
       return null;
     }
     if (!data['name'] || !data['iconURL']) {
       Cu.reportError("SocialService.manifestFromData manifest missing name or iconURL.");
       return null;
     }
     for (let url of sameOriginRequired) {
       if (data[url]) {
@@ -591,17 +593,20 @@ this.SocialService = {
                       installOrigin + "] is blocklisted");
 
     AddonManager.getAddonByID(id, function(aAddon) {
       if (aAddon && aAddon.userDisabled) {
         aAddon.cancelUninstall();
         aAddon.userDisabled = false;
       }
       schedule(function () {
-        this._installProvider(aDOMDocument, data, installCallback);
+        this._installProvider(aDOMDocument, data, aManifest => {
+          this._notifyProviderListeners("provider-installed", aManifest.origin);
+          installCallback(aManifest);
+        });
       }.bind(this));
     }.bind(this));
   },
 
   _installProvider: function(aDOMDocument, data, installCallback) {
     let sourceURI = aDOMDocument.location.href;
     let installOrigin = aDOMDocument.nodePrincipal.origin;
 
@@ -656,17 +661,17 @@ this.SocialService = {
     }
   },
 
   /**
    * updateProvider is used from the worker to self-update.  Since we do not
    * have knowledge of the currently selected provider here, we will notify
    * the front end to deal with any reload.
    */
-  updateProvider: function(aUpdateOrigin, aManifest, aCallback) {
+  updateProvider: function(aUpdateOrigin, aManifest) {
     let originUri = Services.io.newURI(aUpdateOrigin, null, null);
     let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);
     let installType = this.getOriginActivationType(aUpdateOrigin);
     // if we get data, we MUST have a valid manifest generated from the data
     let manifest = this._manifestFromData(installType, aManifest, principal);
     if (!manifest)
       throw new Error("SocialService.installProvider: service configuration is invalid from " + aUpdateOrigin);
 
@@ -677,23 +682,25 @@ this.SocialService = {
     Services.prefs.setComplexValue(getPrefnameFromOrigin(manifest.origin), Ci.nsISupportsString, string);
 
     // overwrite the existing provider then notify the front end so it can
     // handle any reload that might be necessary.
     if (ActiveProviders.has(manifest.origin)) {
       let provider = new SocialProvider(manifest);
       SocialServiceInternal.providers[provider.origin] = provider;
       // update the cache and ui, reload provider if necessary
-      this._notifyProviderListeners("provider-update", provider);
+      this.getOrderedProviderList(providers => {
+        this._notifyProviderListeners("provider-update", provider.origin, providers);
+      });
     }
 
   },
 
   uninstallProvider: function(origin, aCallback) {
-    let manifest = SocialServiceInternal.getManifestByOrigin(origin);
+    let manifest = SocialService.getManifestByOrigin(origin);
     let addon = new AddonWrapper(manifest);
     addon.uninstall(aCallback);
   }
 };
 
 /**
  * The SocialProvider object represents a social provider, and allows
  * access to its FrameWorker (if it has one).
@@ -715,16 +722,17 @@ function SocialProvider(input) {
 
   this.name = input.name;
   this.iconURL = input.iconURL;
   this.icon32URL = input.icon32URL;
   this.icon64URL = input.icon64URL;
   this.workerURL = input.workerURL;
   this.sidebarURL = input.sidebarURL;
   this.shareURL = input.shareURL;
+  this.statusURL = input.statusURL;
   this.origin = input.origin;
   let originUri = Services.io.newURI(input.origin, null, null);
   this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri);
   this.ambientNotificationIcons = {};
   this.errorState = null;
   this.frecency = 0;
 
   let activationType = getOriginActivationType(input.origin);
@@ -761,17 +769,17 @@ SocialProvider.prototype = {
     if (enable) {
       this._activate();
     } else {
       this._terminate();
     }
   },
 
   get manifest() {
-    return SocialServiceInternal.getManifestByOrigin(this.origin);
+    return SocialService.getManifestByOrigin(this.origin);
   },
 
   // Reference to a workerAPI object for this provider. Null if the provider has
   // no FrameWorker, or is disabled.
   workerAPI: null,
 
   // Contains information related to the user's profile. Populated by the
   // workerAPI via updateUserProfile.
@@ -1007,17 +1015,17 @@ function getAddonIDFromOrigin(origin) {
 
 function getPrefnameFromOrigin(origin) {
   return "social.manifest." + SocialServiceInternal.getManifestPrefname(origin);
 }
 
 function AddonInstaller(sourceURI, aManifest, installCallback) {
   aManifest.updateDate = Date.now();
   // get the existing manifest for installDate
-  let manifest = SocialServiceInternal.getManifestByOrigin(aManifest.origin);
+  let manifest = SocialService.getManifestByOrigin(aManifest.origin);
   let isNewInstall = !manifest;
   if (manifest && manifest.installDate)
     aManifest.installDate = manifest.installDate;
   else
     aManifest.installDate = aManifest.updateDate;
 
   this.sourceURI = sourceURI;
   this.install = function() {
@@ -1083,16 +1091,17 @@ var SocialAddonProvider = {
   },
 
   removeAddon: function(aAddon, aCallback) {
     AddonManagerPrivate.callAddonListeners("onUninstalling", aAddon, false);
     aAddon.pendingOperations |= AddonManager.PENDING_UNINSTALL;
     Services.prefs.clearUserPref(getPrefnameFromOrigin(aAddon.manifest.origin));
     aAddon.pendingOperations -= AddonManager.PENDING_UNINSTALL;
     AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
+    SocialService._notifyProviderListeners("provider-uninstalled", aAddon.manifest.origin);
     if (aCallback)
       schedule(aCallback);
   }
 }
 
 
 function AddonWrapper(aManifest) {
   this.manifest = aManifest;