Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Sat, 16 Aug 2014 17:18:48 -0400
changeset 199903 9a2152d29d8a12b1bebcdb179c907491cac8bcb2
parent 199888 675bab72b8a8960bc65f6ac42d67927c2d310e05 (current diff)
parent 199902 a4e46fd6959b36c5baae3f09f2a27dde52c3a388 (diff)
child 199904 c3eb1b5ad4e4b12e0982a07bdccb2de09799bc52
push id9784
push userryanvm@gmail.com
push dateSat, 16 Aug 2014 21:45:40 +0000
treeherderb2g-inbound@94ba78a42305 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.0a1
Merge fx-team to m-c. a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1577,22 +1577,32 @@ pref("loop.seenToS", "unseen");
 pref("loop.legal.ToS_url", "https://accounts.firefox.com/legal/terms");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
+pref("loop.debug.websocket", false);
 
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
+// activation from inside of share panel is possible if activationPanelEnabled
+// is true. Pref'd off for release while usage testing is done through beta.
+#ifdef RELEASE_BUILD
+pref("social.share.activationPanelEnabled", false);
+#else
+pref("social.share.activationPanelEnabled", true);
+#endif
+pref("social.shareDirectory", "https://activations.cdn.mozilla.net/en-US/sharePanel.html");
+
 pref("dom.identity.enabled", false);
 
 // Block insecure active content on https pages
 pref("security.mixed_content.block_active_content", true);
 
 // 1 = allow MITM for certificate pinning checks.
 pref("security.cert_pinning.enforcement_level", 1);
 
--- a/browser/base/content/aboutSocialError.xhtml
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -38,36 +38,37 @@
     Cu.import("resource:///modules/Social.jsm");
 
     let config = {
       tryAgainCallback: reloadProvider
     }
 
     function parseQueryString() {
       let url = document.documentURI;
-      let queryString = url.replace(/^about:socialerror\??/, "");
+      var searchParams = new URLSearchParams(url);
 
-      let modeMatch = queryString.match(/mode=([^&]+)/);
-      let mode = modeMatch && modeMatch[1] ? modeMatch[1] : "";
-      let originMatch = queryString.match(/origin=([^&]+)/);
-      config.origin = originMatch && originMatch[1] ? decodeURIComponent(originMatch[1]) : "";
+      let mode = searchParams.get("mode");
+      config.directory = searchParams.get("directory");
+      config.origin = searchParams.get("origin");
+      let encodedURL = searchParams.get("url");
+      let url = decodeURIComponent(encodedURL);
+      if (config.directory) {
+        let URI = Services.io.newURI(url, null, null);
+        config.origin = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI).origin;
+      }
 
       switch (mode) {
         case "compactInfo":
           document.getElementById("btnTryAgain").style.display = 'none';
           document.getElementById("btnCloseSidebar").style.display = 'none';
           break;
         case "tryAgainOnly":
           document.getElementById("btnCloseSidebar").style.display = 'none';
           //intentional fall-through
         case "tryAgain":
-          let urlMatch = queryString.match(/url=([^&]+)/);
-          let encodedURL = urlMatch && urlMatch[1] ? urlMatch[1] : "";
-          let url = decodeURIComponent(encodedURL);
-
           config.tryAgainCallback = loadQueryURL;
           config.queryURL = url;
           break;
         case "workerFailure":
           config.tryAgainCallback = reloadProvider;
           break;
         default:
           break;
@@ -75,17 +76,17 @@
     }
 
     function setUpStrings() {
       let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
       let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
 
       let productName = brandBundle.GetStringFromName("brandShortName");
       let provider = Social._getProviderFromOrigin(config.origin);
-      let providerName = provider && provider.name;
+      let providerName = provider ? provider.name : config.origin;
 
       // Sets up the error message
       let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);
       document.getElementById("main-error-msg").textContent = msg;
 
       // Sets up the buttons' labels and accesskeys
       let btnTryAgain = document.getElementById("btnTryAgain");
       btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label");
--- a/browser/base/content/browser-data-submission-info-bar.js
+++ b/browser/base/content/browser-data-submission-info-bar.js
@@ -57,68 +57,59 @@ let gDataNotificationInfoBar = {
 
     this._actionTaken = false;
 
     let buttons = [{
       label: gNavigatorBundle.getString("dataReportingNotification.button.label"),
       accessKey: gNavigatorBundle.getString("dataReportingNotification.button.accessKey"),
       popup: null,
       callback: function () {
-        // Clicking the button to go to the preferences tab constitutes
-        // acceptance of the data upload policy for Firefox Health Report.
-        // This will ensure the checkbox is checked. The user has the option of
-        // unchecking it.
-        request.onUserAccept("info-bar-button-pressed");
         this._actionTaken = true;
         window.openAdvancedPreferences("dataChoicesTab");
       }.bind(this),
     }];
 
     this._log.info("Creating data reporting policy notification.");
     let notification = this._notificationBox.appendNotification(
       message,
       this._DATA_REPORTING_NOTIFICATION,
       null,
       this._notificationBox.PRIORITY_INFO_HIGH,
       buttons,
       function onEvent(event) {
         if (event == "removed") {
-          if (!this._actionTaken) {
-            request.onUserAccept("info-bar-dismissed");
-          }
-
           Services.obs.notifyObservers(null, "datareporting:notify-data-policy:close", null);
         }
       }.bind(this)
     );
-
-    // Tell the notification request we have displayed the notification.
+    // It is important to defer calling onUserNotifyComplete() until we're
+    // actually sure the notification was displayed. If we ever called
+    // onUserNotifyComplete() without showing anything to the user, that
+    // would be very good for user choice. It may also have legal impact.
     request.onUserNotifyComplete();
   },
 
   _clearPolicyNotification: function () {
     let notification = this._getDataReportingNotification();
     if (notification) {
       this._log.debug("Closing notification.");
       notification.close();
     }
   },
 
-  onNotifyDataPolicy: function (request) {
-    try {
-      this._displayDataPolicyInfoBar(request);
-    } catch (ex) {
-      request.onUserNotifyFailed(ex);
-    }
-  },
-
   observe: function(subject, topic, data) {
     switch (topic) {
       case "datareporting:notify-data-policy:request":
-        this.onNotifyDataPolicy(subject.wrappedJSObject.object);
+        let request = subject.wrappedJSObject.object;
+        try {
+          this._displayDataPolicyInfoBar(request);
+        } catch (ex) {
+          request.onUserNotifyFailed(ex);
+          return;
+        }
         break;
 
       case "datareporting:notify-data-policy:close":
         // If this observer fires, it means something else took care of
         // responding. Therefore, we don't need to do anything. So, we
         // act like we took action and clear state.
         this._actionTaken = true;
         this._clearPolicyNotification();
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -114,17 +114,17 @@
 #ifdef E10S_TESTING_ONLY
     <command id="Tools:RemoteWindow"
       oncommand="OpenBrowserWindow({remote: true});"/>
     <command id="Tools:NonRemoteWindow"
       oncommand="OpenBrowserWindow({remote: false});"/>
 #endif
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
-    <command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/>
+    <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
     <command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
     <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/>
     <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
     <command id="Chat:Focus" oncommand="Cu.import('resource:///modules/Chat.jsm', {}).Chat.focus(window);"/>
   </commandset>
 
   <commandset id="placesCommands">
     <command id="Browser:ShowAllBookmarks"
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -178,31 +178,33 @@ SocialUI = {
     SocialShare.update();
   },
 
   // This handles "ActivateSocialFeature" events fired against content documents
   // in this window.  If this activation happens from within Firefox, such as
   // about:home or the share panel, we bypass the enable prompt. Any website
   // activation, such as from the activations directory or a providers website
   // will still get the prompt.
-  _activationEventHandler: function SocialUI_activationHandler(e, aBypassUserEnable=false) {
+  _activationEventHandler: function SocialUI_activationHandler(e, options={}) {
     let targetDoc;
     let node;
     if (e.target instanceof HTMLDocument) {
       // version 0 support
       targetDoc = e.target;
       node = targetDoc.documentElement
     } else {
       targetDoc = e.target.ownerDocument;
       node = e.target;
     }
     if (!(targetDoc instanceof HTMLDocument))
       return;
 
-    if (!aBypassUserEnable && targetDoc.defaultView != content)
+    // The share panel iframe will not match "content" so it passes a bypass
+    // flag
+    if (!options.bypassContentCheck && targetDoc.defaultView != content)
       return;
 
     // If we are in PB mode, we silently do nothing (bug 829404 exists to
     // do something sensible here...)
     if (PrivateBrowsingUtils.isWindowPrivate(window))
       return;
 
     // If the last event was received < 1s ago, ignore this one
@@ -228,21 +230,35 @@ SocialUI = {
         return;
       }
     }
     Social.installProvider(targetDoc, data, function(manifest) {
       Social.activateFromOrigin(manifest.origin, function(provider) {
         if (provider.sidebarURL) {
           SocialSidebar.show(provider.origin);
         }
+        if (provider.shareURL) {
+          // make this new provider the selected provider. If the panel hasn't
+          // been opened, we need to make the frame first.
+          SocialShare._createFrame();
+          SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
+          SocialShare.iframe.setAttribute('origin', provider.origin);
+          // get the right button selected
+          SocialShare.populateProviderMenu();
+          if (SocialShare.panel.state == "open") {
+            SocialShare.sharePage(provider.origin);
+          }
+        }
         if (provider.postActivationURL) {
-          openUILinkIn(provider.postActivationURL, "tab");
+          // if activated from an open share panel, we load the landing page in
+          // a background tab
+          gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
         }
       });
-    }, aBypassUserEnable);
+    }, options);
   },
 
   showLearnMore: function() {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
     openUILinkIn(url, "tab");
   },
 
   closeSocialPanelForLinkTraversal: function (target, linkNode) {
@@ -285,20 +301,20 @@ SocialUI = {
     if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window))
       return false;
     return Social.providers.length > 0;
   },
 
   // called on tab/urlbar/location changes and after customization. Update
   // anything that is tab specific.
   updateState: function() {
+    SocialShare.update();
     if (!SocialUI.enabled)
       return;
     SocialMarks.update();
-    SocialShare.update();
   }
 }
 
 SocialFlyout = {
   get panel() {
     return document.getElementById("social-flyout-panel");
   },
 
@@ -429,16 +445,22 @@ SocialFlyout = {
           Cu.reportError(e);
         }
       }
     });
   }
 }
 
 SocialShare = {
+  get _dynamicResizer() {
+    delete this._dynamicResizer;
+    this._dynamicResizer = new DynamicResizeWatcher();
+    return this._dynamicResizer;
+  },
+
   // Share panel may be attached to the overflow or menu button depending on
   // customization, we need to manage open state of the anchor.
   get anchor() {
     let widget = CustomizableUI.getWidget("social-share-button");
     return widget.forWindow(window).anchor;
   },
   get panel() {
     return document.getElementById("social-share-panel");
@@ -447,93 +469,108 @@ SocialShare = {
   get iframe() {
     // first element is our menu vbox.
     if (this.panel.childElementCount == 1)
       return null;
     else
       return this.panel.lastChild;
   },
 
+  get activationPanelEnabled () {
+    // ability to pref off for release
+    return Services.prefs.getBoolPref("social.share.activationPanelEnabled");
+  },
+
+  _activationHandler: function(event) {
+    if (!SocialShare.activationPanelEnabled)
+      return;
+    SocialUI._activationEventHandler(event, { bypassContentCheck: true, bypassInstallPanel: true });
+  },
+
   uninit: function () {
     if (this.iframe) {
+      this.iframe.removeEventListener("ActivateSocialFeature", this._activationHandler, true, true);
       this.iframe.remove();
     }
   },
 
   _createFrame: function() {
     let panel = this.panel;
-    if (!SocialUI.enabled || this.iframe)
+    if (this.iframe)
       return;
     this.panel.hidden = false;
     // create and initialize the panel for this window
     let iframe = document.createElement("browser");
     iframe.setAttribute("type", "content");
     iframe.setAttribute("class", "social-share-frame");
     iframe.setAttribute("context", "contentAreaContextMenu");
     iframe.setAttribute("tooltip", "aHTMLTooltip");
     iframe.setAttribute("disableglobalhistory", "true");
     iframe.setAttribute("flex", "1");
     panel.appendChild(iframe);
+    this.iframe.addEventListener("ActivateSocialFeature", this._activationHandler, true, true);
     this.populateProviderMenu();
   },
 
   getSelectedProvider: function() {
     let provider;
     let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
     if (lastProviderOrigin) {
       provider = Social._getProviderFromOrigin(lastProviderOrigin);
     }
+    // if we are able to activate a provider we don't need to do anything fancy
+    // here, the user will land on the activation panel if no previously
+    // selected provider is available.
+    if (this.activationPanelEnabled)
+      return provider;
+
     // if they have a provider selected in the sidebar use that for the initial
     // default in share
     if (!provider)
       provider = SocialSidebar.provider;
-    // if our provider has no shareURL, select the first one that does
+    // if our provider has no shareURL, select the first one that does. If we
+    // have no selected provider and activation is available, default to that
+    // panel.
     if (!provider || !provider.shareURL) {
       let providers = [p for (p of Social.providers) if (p.shareURL)];
       provider = providers.length > 0  && providers[0];
     }
     return provider;
   },
 
   populateProviderMenu: function() {
     if (!this.iframe)
       return;
     let providers = [p for (p of Social.providers) if (p.shareURL)];
     let hbox = document.getElementById("social-share-provider-buttons");
-    // selectable providers are inserted before the provider-menu seperator,
-    // remove any menuitems in that area
-    while (hbox.firstChild) {
+    // remove everything before the add-share-provider button (which should also
+    // be lastChild if any share providers were added)
+    let addButton = document.getElementById("add-share-provider");
+    while (hbox.firstChild != addButton) {
       hbox.removeChild(hbox.firstChild);
     }
-    // reset our share toolbar
-    // only show a selection if there is more than one
-    if (!SocialUI.enabled || providers.length < 2) {
-      this.panel.firstChild.hidden = true;
-      return;
-    }
     let selectedProvider = this.getSelectedProvider();
     for (let provider of providers) {
       let button = document.createElement("toolbarbutton");
       button.setAttribute("class", "toolbarbutton share-provider-button");
       button.setAttribute("type", "radio");
       button.setAttribute("group", "share-providers");
       button.setAttribute("image", provider.iconURL);
       button.setAttribute("tooltiptext", provider.name);
       button.setAttribute("origin", provider.origin);
-      button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;");
+      button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
       if (provider == selectedProvider) {
         this.defaultButton = button;
       }
-      hbox.appendChild(button);
+      hbox.insertBefore(button, addButton);
     }
     if (!this.defaultButton) {
-      this.defaultButton = hbox.firstChild
+      this.defaultButton = this.activationPanelEnabled ? addButton : hbox.firstChild;
     }
     this.defaultButton.setAttribute("checked", "true");
-    this.panel.firstChild.hidden = false;
   },
 
   get shareButton() {
     // web-panels (bookmark/sidebar) don't include customizableui, so
     // nsContextMenu fails when accessing shareButton, breaking
     // browser_bug409481.js.
     if (!window.CustomizableUI)
       return null;
@@ -555,33 +592,36 @@ SocialShare = {
 
   update: function() {
     let widget = CustomizableUI.getWidget("social-share-button");
     if (!widget)
       return;
     let shareButton = widget.forWindow(window).node;
     // hidden state is based on available share providers and location of
     // button. It's always visible and disabled in the customization palette.
-    shareButton.hidden = !SocialUI.enabled || (widget.areaType &&
-                         [p for (p of Social.providers) if (p.shareURL)].length == 0);
+    shareButton.hidden = !this.activationPanelEnabled && (!SocialUI.enabled || (widget.areaType &&
+                         [p for (p of Social.providers) if (p.shareURL)].length == 0));
     let disabled = !widget.areaType || shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
 
     // 1. update the relevent command's disabled state so the keyboard
     // shortcut only works when available.
     // 2. If the button has been relocated to a place that is not visible by
     // default (e.g. menu panel) then the disabled attribute will not update
     // correctly based on the command, so we update the attribute directly as.
     let cmd = document.getElementById("Social:SharePage");
     if (disabled) {
       cmd.setAttribute("disabled", "true");
       shareButton.setAttribute("disabled", "true");
     } else {
       cmd.removeAttribute("disabled");
       shareButton.removeAttribute("disabled");
     }
+
+    // enable or disable the activation panel
+    document.getElementById("add-share-provider").hidden = !this.activationPanelEnabled;
   },
 
   _onclick: function() {
     Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
   },
   
   onShowing: function() {
     this.anchor.setAttribute("open", "true");
@@ -603,36 +643,34 @@ SocialShare = {
     }
   },
 
   setErrorMessage: function() {
     let iframe = this.iframe;
     if (!iframe)
       return;
 
-    iframe.removeAttribute("src");
-    iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
-                                 encodeURIComponent(iframe.getAttribute("origin")),
-                                 null, null, null, null);
+    let url;
+    let origin = iframe.getAttribute("origin");
+    if (!origin && this.activationPanelEnabled) {
+      // directory site is down
+      url = "about:socialerror?mode=tryAgainOnly&directory=1&url=" + encodeURIComponent(iframe.getAttribute("src"));
+    } else {
+      url = "about:socialerror?mode=compactInfo&origin=" + encodeURIComponent(origin);
+    }
+    iframe.webNavigation.loadURI(url, null, null, null, null);
     sizeSocialPanelToContent(this.panel, iframe);
   },
 
   sharePage: function(providerOrigin, graphData, target) {
     // if providerOrigin is undefined, we use the last-used provider, or the
     // current/default provider.  The provider selection in the share panel
     // will call sharePage with an origin for us to switch to.
     this._createFrame();
     let iframe = this.iframe;
-    let provider;
-    if (providerOrigin)
-      provider = Social._getProviderFromOrigin(providerOrigin);
-    else
-      provider = this.getSelectedProvider();
-    if (!provider || !provider.shareURL)
-      return;
 
     // graphData is an optional param that either defines the full set of data
     // to be shared, or partial data about the current page. It is set by a call
     // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
     // define at least url. If it is undefined, we're sharing the current url in
     // the browser tab.
     let pageData = graphData ? graphData : this.currentShare;
     let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
@@ -654,42 +692,47 @@ SocialShare = {
       }
     }
     // if this is a share of a selected item, get any microdata
     if (!pageData.microdata && target) {
       pageData.microdata = OpenGraphBuilder.getMicrodata(gBrowser, target);
     }
     this.currentShare = pageData;
 
+    let provider;
+    if (providerOrigin)
+      provider = Social._getProviderFromOrigin(providerOrigin);
+    else
+      provider = this.getSelectedProvider();
+    if (!provider || !provider.shareURL) {
+      this.showDirectory();
+      return;
+    }
+    // check the menu button
+    let hbox = document.getElementById("social-share-provider-buttons");
+    let btn = hbox.querySelector("[origin='" + provider.origin + "']");
+    btn.checked = true;
+
     let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
 
     let size = provider.getPageSize("share");
     if (size) {
-      if (this._dynamicResizer) {
-        this._dynamicResizer.stop();
-        this._dynamicResizer = null;
-      }
-      let {width, height} = size;
-      width += this.panel.boxObject.width - iframe.boxObject.width;
-      height += this.panel.boxObject.height - iframe.boxObject.height;
-      this.panel.sizeTo(width, height);
-    } else {
-      this._dynamicResizer = new DynamicResizeWatcher();
+      this._dynamicResizer.stop();
     }
 
     // if we've already loaded this provider/page share endpoint, we don't want
     // to add another load event listener.
     let reload = true;
     let endpointMatch = shareEndpoint == iframe.getAttribute("src");
     let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete";
     if (endpointMatch && docLoaded) {
       reload = shareEndpoint != iframe.contentDocument.location.spec;
     }
     if (!reload) {
-      if (this._dynamicResizer)
+      if (!size)
         this._dynamicResizer.start(this.panel, iframe);
       iframe.docShell.isActive = true;
       iframe.docShell.isAppTab = true;
       let evt = iframe.contentDocument.createEvent("CustomEvent");
       evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
       iframe.contentDocument.documentElement.dispatchEvent(evt);
     } else {
       // first time load, wait for load and dispatch after load
@@ -697,17 +740,23 @@ SocialShare = {
         iframe.removeEventListener("load", panelBrowserOnload, true);
         iframe.docShell.isActive = true;
         iframe.docShell.isAppTab = true;
         // to support standard share endpoints mimick window.open by setting
         // window.opener, some share endpoints rely on w.opener to know they
         // should close the window when done.
         iframe.contentWindow.opener = iframe.contentWindow;
         setTimeout(function() {
-          if (SocialShare._dynamicResizer) { // may go null if hidden quickly
+          if (size) {
+            let panel = SocialShare.panel;
+            let {width, height} = size;
+            width += panel.boxObject.width - iframe.boxObject.width;
+            height += panel.boxObject.height - iframe.boxObject.height;
+            panel.sizeTo(width, height);
+          } else {
             SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
           }
         }, 0);
         let evt = iframe.contentDocument.createEvent("CustomEvent");
         evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
         iframe.contentDocument.documentElement.dispatchEvent(evt);
       }, true);
     }
@@ -718,20 +767,42 @@ SocialShare = {
       if (purge > 0)
         iframe.sessionHistory.PurgeHistory(purge);
     }
 
     // always ensure that origin belongs to the endpoint
     let uri = Services.io.newURI(shareEndpoint, null, null);
     iframe.setAttribute("origin", provider.origin);
     iframe.setAttribute("src", shareEndpoint);
+    this._openPanel();
+  },
 
+  showDirectory: function() {
+    let url = Services.prefs.getCharPref("social.shareDirectory");
+    this._createFrame();
+    let iframe = this.iframe;
+    iframe.removeAttribute("origin");
+    iframe.setAttribute("src", url);
+    iframe.addEventListener("load", function panelBrowserOnload(e) {
+      iframe.removeEventListener("load", panelBrowserOnload, true);
+      SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
+
+      iframe.addEventListener("unload", function panelBrowserOnload(e) {
+        iframe.removeEventListener("unload", panelBrowserOnload, true);
+        SocialShare._dynamicResizer.stop();
+      }, true);
+
+    }, true);
+    this._openPanel();
+  },
+
+  _openPanel: function() {
     let anchor = document.getAnonymousElementByAttribute(this.anchor, "class", "toolbarbutton-icon");
     this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
-    Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
+    Social.setErrorListener(this.iframe, this.setErrorMessage.bind(this));
     Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
   }
 };
 
 SocialSidebar = {
   _openStartTime: 0,
 
   // Whether the sidebar can be shown for this window.
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -241,17 +241,21 @@
     <panel id="social-share-panel"
            class="social-panel"
            type="arrow"
            orient="horizontal"
            onpopupshowing="SocialShare.onShowing()"
            onpopuphidden="SocialShare.onHidden()"
            hidden="true">
       <vbox class="social-share-toolbar">
-        <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1"/>
+        <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1">
+          <toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio"
+                         group="share-providers" tooltiptext="&findShareServices.label;"
+                         oncommand="SocialShare.showDirectory()"/>
+        </arrowscrollbox>
       </vbox>
     </panel>
 
     <panel id="social-notification-panel"
            class="social-panel"
            type="arrow"
            hidden="true"
            noautofocus="true"/>
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -329,17 +329,17 @@ nsContextMenu.prototype = {
     linkmenus = document.getElementsByClassName("context-marklink");
     [m.hidden = !enableLinkMarkItems for (m of linkmenus)];
 
     // SocialShare
     let shareButton = SocialShare.shareButton;
     let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
     let pageShare = shareEnabled && !(this.isContentSelected ||
                             this.onTextInput || this.onLink || this.onImage ||
-                            this.onVideo || this.onAudio);
+                            this.onVideo || this.onAudio || this.onCanvas);
     this.showItem("context-sharepage", pageShare);
     this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
     this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
     this.showItem("context-shareimage", shareEnabled && this.onImage);
     this.showItem("context-sharevideo", shareEnabled && this.onVideo);
     this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL);
   },
 
--- a/browser/base/content/test/general/browser_datareporting_notification.js
+++ b/browser/base/content/test/general/browser_datareporting_notification.js
@@ -1,36 +1,55 @@
 /* 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 originalPolicy = null;
+
+/**
+ * Display a datareporting notification to the user.
+ *
+ * @param  {String} name
+ */
 function sendNotifyRequest(name) {
   let ns = {};
-  Components.utils.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
-  Components.utils.import("resource://gre/modules/Preferences.jsm", ns);
+  Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
+  Cu.import("resource://gre/modules/Preferences.jsm", ns);
 
-  let service = Components.classes["@mozilla.org/datareporting/service;1"]
-                                  .getService(Components.interfaces.nsISupports)
-                                  .wrappedJSObject;
+  let service = Cc["@mozilla.org/datareporting/service;1"]
+                  .getService(Ci.nsISupports)
+                  .wrappedJSObject;
   ok(service.healthReporter, "Health Reporter instance is available.");
 
+  Cu.import("resource://gre/modules/Promise.jsm", ns);
+  let deferred = ns.Promise.defer();
+
+  if (!originalPolicy) {
+    originalPolicy = service.policy;
+  }
+
   let policyPrefs = new ns.Preferences("testing." + name + ".");
   ok(service._prefs, "Health Reporter prefs are available.");
   let hrPrefs = service._prefs;
 
   let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
+  policy.dataSubmissionPolicyBypassNotification = false;
+  service.policy = policy;
   policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
 
-  is(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED, "Policy is in unnotified state.");
+  service.healthReporter.onInit().then(function onSuccess () {
+    is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
+    ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
+    policy._userNotifyPromise.then(
+      deferred.resolve.bind(deferred),
+      deferred.reject.bind(deferred)
+    );
+  }.bind(this), deferred.reject.bind(deferred));
 
-  service.healthReporter.onInit().then(function onInit() {
-    is(policy.ensureNotifyResponse(new Date()), false, "User has not responded to policy.");
-  });
-
-  return policy;
+  return [policy, deferred.promise];
 }
 
 /**
  * Wait for a <notification> to be closed then call the specified callback.
  */
 function waitForNotificationClose(notification, cb) {
   let parent = notification.parentNode;
 
@@ -50,60 +69,73 @@ function waitForNotificationClose(notifi
   });
 
   observer.observe(parent, {childList: true});
 }
 
 let dumpAppender, rootLogger;
 
 function test() {
+  registerCleanupFunction(cleanup);
   waitForExplicitFinish();
 
   let ns = {};
   Components.utils.import("resource://gre/modules/Log.jsm", ns);
   rootLogger = ns.Log.repository.rootLogger;
   dumpAppender = new ns.Log.DumpAppender();
   dumpAppender.level = ns.Log.Level.All;
   rootLogger.addAppender(dumpAppender);
 
-  let notification = document.getElementById("global-notificationbox");
-  let policy;
+  closeAllNotifications().then(function onSuccess () {
+    let notification = document.getElementById("global-notificationbox");
+
+    notification.addEventListener("AlertActive", function active() {
+      notification.removeEventListener("AlertActive", active, true);
+      is(notification.allNotifications.length, 1, "Notification Displayed.");
 
-  notification.addEventListener("AlertActive", function active() {
-    notification.removeEventListener("AlertActive", active, true);
-
-    executeSoon(function afterNotification() {
-      is(policy.notifyState, policy.STATE_NOTIFY_WAIT, "Policy is waiting for user response.");
-      ok(!policy.dataSubmissionPolicyAccepted, "Data submission policy not yet accepted.");
+      executeSoon(function afterNotification() {
+        waitForNotificationClose(notification.currentNotification, function onClose() {
+          is(notification.allNotifications.length, 0, "No notifications remain.");
+          is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
+          ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
+          test_multiple_windows();
+        });
+        notification.currentNotification.close();
+      });
+    }, true);
 
-      waitForNotificationClose(notification.currentNotification, function onClose() {
-        is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE, "Closing info bar completes user notification.");
-        ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
-        is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-dismissed",
-           "Reason for acceptance was info bar dismissal.");
-        is(notification.allNotifications.length, 0, "No notifications remain.");
-        test_multiple_windows();
-      });
-      notification.currentNotification.close();
+    let [policy, promise] = sendNotifyRequest("single_window_notified");
+
+    is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
+    is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
+    is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
+
+    promise.then(function () {
+      is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
+      is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
+      is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
+    }.bind(this), function (err) {
+      throw err;
     });
-  }, true);
 
-  policy = sendNotifyRequest("single_window_notified");
+  }.bind(this), function onError (err) {
+    throw err;
+  });
 }
 
 function test_multiple_windows() {
   // Ensure we see the notification on all windows and that action on one window
   // results in dismiss on every window.
   let window2 = OpenBrowserWindow();
   whenDelayedStartupFinished(window2, function onWindow() {
     let notification1 = document.getElementById("global-notificationbox");
     let notification2 = window2.document.getElementById("global-notificationbox");
     ok(notification2, "2nd window has a global notification box.");
 
-    let policy;
+    let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
     let displayCount = 0;
     let prefWindowClosed = false;
     let mutationObserversRemoved = false;
 
     function onAlertDisplayed() {
       displayCount++;
 
       if (displayCount != 2) {
@@ -124,36 +156,32 @@ function test_multiple_windows() {
           dump("Not finishing test yet because mutation observers haven't been removed yet.\n");
           return;
         }
 
         window2.close();
 
         dump("Finishing multiple window test.\n");
         rootLogger.removeAppender(dumpAppender);
-        delete dumpAppender;
-        delete rootLogger;
+        dumpAppender = null;
+        rootLogger = null;
         finish();
       }
       let closeCount = 0;
 
       function onAlertClose() {
         closeCount++;
 
         if (closeCount != 2) {
           return;
         }
 
         ok(true, "Closing info bar on one window closed them on all.");
+        is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
 
-        is(policy.notifyState, policy.STATE_NOTIFY_COMPLETE,
-           "Closing info bar with multiple windows completes notification.");
-        ok(policy.dataSubmissionPolicyAccepted, "Data submission policy accepted.");
-        is(policy.dataSubmissionPolicyResponseType, "accepted-info-bar-button-pressed",
-           "Policy records reason for acceptance was button press.");
         is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
         is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
 
         mutationObserversRemoved = true;
         maybeFinish();
       }
 
       waitForNotificationClose(notification1.currentNotification, onAlertClose);
@@ -187,12 +215,25 @@ function test_multiple_windows() {
       executeSoon(onAlertDisplayed);
     }, true);
 
     notification2.addEventListener("AlertActive", function active2() {
       notification2.removeEventListener("AlertActive", active2, true);
       executeSoon(onAlertDisplayed);
     }, true);
 
-    policy = sendNotifyRequest("multiple_window_behavior");
+    promise.then(null, function onError(err) {
+      throw err;
+    });
   });
 }
 
+function cleanup () {
+  // In case some test fails.
+  if (originalPolicy) {
+    let service = Cc["@mozilla.org/datareporting/service;1"]
+                    .getService(Ci.nsISupports)
+                    .wrappedJSObject;
+    service.policy = originalPolicy;
+  }
+
+  return closeAllNotifications();
+}
--- a/browser/base/content/test/general/browser_keywordSearch.js
+++ b/browser/base/content/test/general/browser_keywordSearch.js
@@ -46,19 +46,21 @@ function test() {
         return;
 
       info("received document start");
 
       ok(req instanceof Ci.nsIChannel, "req is a channel");
       is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
       info("Actual URI: " + req.URI.spec);
 
+      req.cancel(Components.results.NS_ERROR_FAILURE);
+
       executeSoon(nextTest);
     }
-  }
+  };
   gBrowser.addProgressListener(listener);
 
   registerCleanupFunction(function () {
     Services.ww.unregisterNotification(windowObserver);
 
     gBrowser.removeProgressListener(listener);
     gBrowser.removeTab(tab);
   });
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -2,16 +2,36 @@ Components.utils.import("resource://gre/
 
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 
+function closeAllNotifications () {
+  let notificationBox = document.getElementById("global-notificationbox");
+
+  if (!notificationBox || !notificationBox.currentNotification) {
+    return Promise.resolve();
+  }
+
+  let deferred = Promise.defer();
+  for (let notification of notificationBox.allNotifications) {
+    waitForNotificationClose(notification, function () {
+      if (notificationBox.allNotifications.length === 0) {
+        deferred.resolve();
+      }
+    });
+    notification.close();
+  }
+
+  return deferred.promise;
+}
+
 function whenDelayedStartupFinished(aWindow, aCallback) {
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
--- a/browser/base/content/test/general/test_contextmenu.html
+++ b/browser/base/content/test/general/test_contextmenu.html
@@ -84,121 +84,133 @@ function runTest(testNum) {
 
   var tests = [
     function () {
         // Invoke context menu for next test.
         openContextMenuFor(text);
     },
 
     function () {
+        info("context menu for text");
         // Context menu for plain text
         plainTextItems = ["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "---",                  null,
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems);
         checkContextMenu(plainTextItems);
         closeContextMenu();
         openContextMenuFor(link); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for text link");
         // Context menu for text link
         if (perWindowPrivateBrowsing) {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "context-openlinkprivate", true,
                             "---",                   null,
                             "context-bookmarklink",  true,
+                            "context-sharelink",     true,
                             "context-savelink",      true,
                             "context-copylink",      true,
                             "context-searchselect",  true
                            ].concat(inspectItems));
         } else {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "---",                   null,
                             "context-bookmarklink",  true,
+                            "context-sharelink",     true,
                             "context-savelink",      true,
                             "context-copylink",      true,
                             "context-searchselect",  true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         openContextMenuFor(mailto); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for mailto link");
         // Context menu for text mailto-link
         checkContextMenu(["context-copyemail", true,
                           "context-searchselect", true
                         ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(img); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for image");
         // Context menu for an image
         checkContextMenu(["context-viewimage",            true,
                           "context-copyimage-contents",   true,
                           "context-copyimage",            true,
                           "---",                          null,
                           "context-saveimage",            true,
+                          "context-shareimage",           true,
                           "context-sendimage",            true,
                           "context-setDesktopBackground", true,
                           "context-viewimageinfo",        true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(canvas); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for canvas");
         // Context menu for a canvas
         checkContextMenu(["context-viewimage",    true,
                           "context-saveimage",    true,
                           "context-selectall",    true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(video_ok); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for video_ok");
         // Context menu for a video (with a VALID media source)
         checkContextMenu(["context-media-play",         true,
                           "context-media-mute",         true,
                           "context-media-playbackrate", null,
                               ["context-media-playbackrate-050x", true,
                                "context-media-playbackrate-100x", true,
                                "context-media-playbackrate-150x", true,
                                "context-media-playbackrate-200x", true], null,
                           "context-media-hidecontrols", true,
                           "context-video-showstats",    true,
                           "context-video-fullscreen",   true,
                           "---",                        null,
                           "context-viewvideo",          true,
                           "context-copyvideourl",       true,
                           "---",                        null,
                           "context-savevideo",          true,
+                          "context-sharevideo",         true,
                           "context-video-saveimage",    true,
                           "context-sendvideo",          true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(audio_in_video); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for audio_in_video");
         // Context menu for a video (with an audio-only file)
           checkContextMenu(["context-media-play",         true,
                             "context-media-mute",         true,
                             "context-media-playbackrate", null,
                                 ["context-media-playbackrate-050x", true,
                                  "context-media-playbackrate-100x", true,
                                  "context-media-playbackrate-150x", true,
                                  "context-media-playbackrate-200x", true], null,
@@ -209,71 +221,77 @@ function runTest(testNum) {
                             "context-saveaudio",          true,
                             "context-sendaudio",          true
                            ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(video_bad); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for video_bad");
         // Context menu for a video (with an INVALID media source)
         checkContextMenu(["context-media-play",         false,
                           "context-media-mute",         false,
                           "context-media-playbackrate", null,
                               ["context-media-playbackrate-050x", false,
                                "context-media-playbackrate-100x", false,
                                "context-media-playbackrate-150x", false,
                                "context-media-playbackrate-200x", false], null,
                           "context-media-hidecontrols", false,
                           "context-video-showstats",    false,
                           "context-video-fullscreen",   false,
                           "---",                        null,
                           "context-viewvideo",          true,
                           "context-copyvideourl",       true,
                           "---",                        null,
                           "context-savevideo",          true,
+                          "context-sharevideo",         true,
                           "context-video-saveimage",    false,
                           "context-sendvideo",          true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(video_bad2); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for video_bad2");
         // Context menu for a video (with an INVALID media source)
         checkContextMenu(["context-media-play",         false,
                           "context-media-mute",         false,
                           "context-media-playbackrate", null,
                               ["context-media-playbackrate-050x", false,
                                "context-media-playbackrate-100x", false,
                                "context-media-playbackrate-150x", false,
                                "context-media-playbackrate-200x", false], null,
                           "context-media-hidecontrols", false,
                           "context-video-showstats",    false,
                           "context-video-fullscreen",   false,
                           "---",                        null,
                           "context-viewvideo",          false,
                           "context-copyvideourl",       false,
                           "---",                        null,
                           "context-savevideo",          false,
+                          "context-sharevideo",         false,
                           "context-video-saveimage",    false,
                           "context-sendvideo",          false
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(iframe); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for iframe");
         // Context menu for an iframe
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "frame",                null,
                               ["context-showonlythisframe", true,
                                "context-openframeintab",    true,
                                "context-openframe",         true,
@@ -291,32 +309,34 @@ function runTest(testNum) {
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(video_in_iframe); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for video_in_iframe");
         // Context menu for a video in an iframe
         checkContextMenu(["context-media-play",         true,
                           "context-media-mute",         true,
                           "context-media-playbackrate", null,
                               ["context-media-playbackrate-050x", true,
                                "context-media-playbackrate-100x", true,
                                "context-media-playbackrate-150x", true,
                                "context-media-playbackrate-200x", true], null,
                           "context-media-hidecontrols", true,
                           "context-video-showstats",    true,
                           "context-video-fullscreen",   true,
                           "---",                        null,
                           "context-viewvideo",          true,
                           "context-copyvideourl",       true,
                           "---",                        null,
                           "context-savevideo",          true,
+                          "context-sharevideo",         true,
                           "context-video-saveimage",    true,
                           "context-sendvideo",          true,
                           "frame",                null,
                               ["context-showonlythisframe", true,
                                "context-openframeintab",    true,
                                "context-openframe",         true,
                                "---",                       null,
                                "context-reloadframe",       true,
@@ -327,22 +347,24 @@ function runTest(testNum) {
                                "context-printframe",        true,
                                "---",                       null,
                                "context-viewframeinfo",     true], null].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(image_in_iframe); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for image_in_iframe");
         // Context menu for an image in an iframe
         checkContextMenu(["context-viewimage",            true,
                           "context-copyimage-contents",   true,
                           "context-copyimage",            true,
                           "---",                          null,
                           "context-saveimage",            true,
+                          "context-shareimage",           true,
                           "context-sendimage",            true,
                           "context-setDesktopBackground", true,
                           "context-viewimageinfo",        true,
                           "frame",                null,
                               ["context-showonlythisframe", true,
                                "context-openframeintab",    true,
                                "context-openframe",         true,
                                "---",                       null,
@@ -354,16 +376,17 @@ function runTest(testNum) {
                                "context-printframe",        true,
                                "---",                       null,
                                "context-viewframeinfo",     true], null].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(textarea); // Invoke context menu for next test
     },
 
     function () {
+        info("context menu for textarea");
         // Context menu for textarea before spell check initialization finishes
         checkContextMenu(["context-undo",                false,
                           "---",                         null,
                           "context-cut",                 false,
                           "context-copy",                false,
                           "context-paste",               null,
                           "context-delete",              false,
                           "---",                         null,
@@ -371,16 +394,17 @@ function runTest(testNum) {
                           "---",                         null,
                           "spell-add-dictionaries-main", true,
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(textarea, false, true); // Invoke context menu for next test, but wait for the spellcheck.
     },
 
     function () {
+        info("context menu for textarea, wait for spell check");
         // Context menu for textarea after spell check initialization finishes
         checkContextMenu(["*chubbiness",         true, // spelling suggestion
                           "spell-add-to-dictionary", true,
                           "---",                 null,
                           "context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
@@ -396,23 +420,25 @@ function runTest(testNum) {
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
         contextMenu.ownerDocument.getElementById("spell-add-to-dictionary").doCommand(); // Add to dictionary
         closeContextMenu();
         openContextMenuFor(text); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for text");
         // Re-check context menu for plain text to make sure it hasn't changed
         checkContextMenu(plainTextItems);
         closeContextMenu();
         openContextMenuFor(textarea, false, true); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for textarea after word added");
         // Context menu for textarea after a word has been added
         // to the dictionary
         checkContextMenu(["spell-undo-add-to-dictionary", true,
                           "---",                 null,
                           "context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
@@ -428,16 +454,17 @@ function runTest(testNum) {
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
         contextMenu.ownerDocument.getElementById("spell-undo-add-to-dictionary").doCommand(); // Undo add to dictionary
         closeContextMenu();
         openContextMenuFor(contenteditable, false, true);
     },
 
     function () {
+        info("context menu for contenteditable");
         // Context menu for contenteditable
         checkContextMenu(["spell-no-suggestions", false,
                           "spell-add-to-dictionary", true,
                           "---",                 null,
                           "context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
@@ -453,22 +480,24 @@ function runTest(testNum) {
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
 
         closeContextMenu();
         openContextMenuFor(link); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for link");
         executeCopyCommand("cmd_copyLink", "http://mozilla.com/");
         closeContextMenu();
         openContextMenuFor(pagemenu); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for pagemenu");
         // Context menu for element with assigned content context menu
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
                           "+Plain item",          {type: "", icon: "", checked: false, disabled: false},
@@ -486,16 +515,17 @@ function runTest(testNum) {
                           "---",                  null,
                           "generated-submenu-1",  true,
                               ["+Radio1",             {type: "checkbox", icon: "", checked: false, disabled: false},
                                "+Radio2",             {type: "checkbox", icon: "", checked: true, disabled: false},
                                "+Radio3",             {type: "checkbox", icon: "", checked: false, disabled: false},
                                "---",                 null,
                                "+Checkbox",           {type: "checkbox", icon: "", checked: false, disabled: false}], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "---",                  null,
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems));
@@ -511,25 +541,27 @@ function runTest(testNum) {
         }
         subwindow.addEventListener("mozfullscreenchange", openDomFullScreen, false);
         SpecialPowers.setBoolPref("full-screen-api.approval-required", false);
         SpecialPowers.setBoolPref("full-screen-api.allow-trusted-requests-only", false);
         full_screen_element.mozRequestFullScreen();
     },
 
     function () {
+        info("context menu for fullscreen mode");
         // Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img)
         checkContextMenu(["context-navigation", null,
                               ["context-back",            false,
                                "context-forward",         false,
                                "context-reload",          true,
                                "context-bookmarkpage",    true], null,
                           "---",                          null,
                           "context-leave-dom-fullscreen", true,
                           "---",                          null,
+                          "context-sharepage",            true,
                           "context-savepage",             true,
                           "---",                          null,
                           "context-viewbgimage",          false,
                           "context-selectall",            true,
                           "---",                          null,
                           "context-viewsource",           true,
                           "context-viewinfo",             true
                          ].concat(inspectItems));
@@ -541,137 +573,152 @@ function runTest(testNum) {
             SpecialPowers.clearUserPref("full-screen-api.allow-trusted-requests-only");
             openContextMenuFor(pagemenu, true); // Invoke context menu for next test.
         }
         subwindow.addEventListener("mozfullscreenchange", openPagemenu, false);
         subwindow.document.mozCancelFullScreen();
     },
 
     function () {
+        info("context menu for element with assigned content context menu");
         // Context menu for element with assigned content context menu
         // The shift key should bypass content context menu processing
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "---",                  null,
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems));
         closeContextMenu();
         selectText(selecttext); // Select text prior to opening context menu.
         openContextMenuFor(selecttext); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for text selection");
         // Context menu for selected text
         if (SpecialPowers.Services.appinfo.OS == "Darwin") {
           // This test is only enabled on Mac due to bug 736399.
           checkContextMenu(["context-copy",                        true,
                             "context-selectall",                   true,
                             "---",                                 null,
                             "context-searchselect",                true,
+                            "context-shareselect",                 true,
                             "context-viewpartialsource-selection", true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         selectText(selecttextlink); // Select text prior to opening context menu.
         openContextMenuFor(selecttextlink); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for text selection with url pattern");
         // Context menu for selected text which matches valid URL pattern
         if (SpecialPowers.Services.appinfo.OS == "Darwin") {
           // This test is only enabled on Mac due to bug 736399.
           if (perWindowPrivateBrowsing) {
             checkContextMenu(["context-openlinkincurrent",           true,
                               "context-openlinkintab",               true,
                               "context-openlink",                    true,
                               "context-openlinkprivate",             true,
                               "---",                                 null,
                               "context-bookmarklink",                true,
+                              "context-sharelink",                   true,
                               "context-savelink",                    true,
                               "context-copy",                        true,
                               "context-selectall",                   true,
                               "---",                                 null,
                               "context-searchselect",                true,
+                              "context-shareselect",                 true,
                               "context-viewpartialsource-selection", true
                              ].concat(inspectItems));
           } else {
             checkContextMenu(["context-openlinkincurrent",           true,
                               "context-openlinkintab",               true,
                               "context-openlink",                    true,
                               "---",                                 null,
                               "context-bookmarklink",                true,
+                              "context-sharelink",                   true,
                               "context-savelink",                    true,
                               "context-copy",                        true,
                               "context-selectall",                   true,
                               "---",                                 null,
                               "context-searchselect",                true,
+                              "context-shareselect",                 true,
                               "context-viewpartialsource-selection", true
                              ].concat(inspectItems));
           }
         }
         closeContextMenu();
         // clear the selection because following tests don't expect any selection
         subwindow.getSelection().removeAllRanges();
 
         openContextMenuFor(imagelink)
     },
 
     function () {
+        info("context menu for imagelink");
         // Context menu for image link
         if (perWindowPrivateBrowsing) {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "context-openlinkprivate", true,
                             "---",                   null,
                             "context-bookmarklink",  true,
+                            "context-sharelink",     true,
                             "context-savelink",      true,
                             "context-copylink",      true,
                             "---",                   null,
                             "context-viewimage",            true,
                             "context-copyimage-contents",   true,
                             "context-copyimage",            true,
                             "---",                          null,
                             "context-saveimage",            true,
+                            "context-shareimage",           true,
                             "context-sendimage",            true,
                             "context-setDesktopBackground", true,
                             "context-viewimageinfo",        true
                            ].concat(inspectItems));
         } else {
           checkContextMenu(["context-openlinkintab", true,
                             "context-openlink",      true,
                             "---",                   null,
                             "context-bookmarklink",  true,
+                            "context-sharelink",     true,
                             "context-savelink",      true,
                             "context-copylink",      true,
                             "---",                   null,
                             "context-viewimage",            true,
                             "context-copyimage-contents",   true,
                             "context-copyimage",            true,
                             "---",                          null,
                             "context-saveimage",            true,
+                            "context-shareimage",           true,
                             "context-sendimage",            true,
                             "context-setDesktopBackground", true,
                             "context-viewimageinfo",        true
                            ].concat(inspectItems));
         }
         closeContextMenu();
         selectInputText(select_inputtext); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for select_inputtext");
         // Context menu for selected text in input
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
                           "context-cut",         true,
                           "context-copy",        true,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      true,
                           "---",                 null,
@@ -681,16 +728,17 @@ function runTest(testNum) {
                           "spell-check-enabled", true
                          ].concat(inspectItems));
         closeContextMenu();
         selectInputText(select_inputtext_password); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext_password); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for selected text in input[type='password']");
         // Context menu for selected text in input[type="password"]
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
                           "context-cut",         true,
                           "context-copy",        true,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      true,
                           "---",                 null,
@@ -704,64 +752,70 @@ function runTest(testNum) {
                                "spell-add-dictionaries",       true], null
                          ].concat(inspectItems));
         closeContextMenu();
         subwindow.getSelection().removeAllRanges();
         openContextMenuFor(plugin);
     },
 
     function () {
+        info("context menu for click-to-play blocked plugin");
         // Context menu for click-to-play blocked plugin
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
                           "context-ctp-play",     true,
                           "context-ctp-hide",     true,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "---",                  null,
                           "context-viewsource",   true,
                           "context-viewinfo",     true
                          ].concat(inspectItems));
         closeContextMenu();
         SpecialPowers.clearUserPref("plugins.click_to_play");
         getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
         openContextMenuFor(longdesc);
     },
 
     function () {
+        info("context menu for image with longdesc");
         // Context menu for an image with longdesc
         checkContextMenu(["context-viewimage",            true,
                           "context-copyimage-contents",   true,
                           "context-copyimage",            true,
                           "---",                          null,
                           "context-saveimage",            true,
+                          "context-shareimage",           true,
                           "context-sendimage",            true,
                           "context-setDesktopBackground", true,
                           "context-viewimageinfo",        true,
                           "context-viewimagedesc",        true
                          ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(srcdoc);
     },
 
     function () {
+        info("context menu for iframe with srcdoc attribute set");
         // Context menu for an iframe with srcdoc attribute set
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    true,
                           "frame",                null,
                               ["context-reloadframe",       true,
                                "---",                       null,
                                "context-saveframe",         true,
@@ -774,16 +828,17 @@ function runTest(testNum) {
                           "context-viewsource",   true,
                           "context-viewinfo",     true
         ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(inputspellfalse, false, true); // Invoke context menu for next test.
     },
 
     function () {
+        info("context menu for text input field with spellcheck=false");
         // Context menu for text input field with spellcheck=false
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
                           "context-cut",         false,
                           "context-copy",        false,
                           "context-paste",       null, // ignore clipboard state
                           "context-delete",      false,
                           "---",                 null,
--- a/browser/base/content/test/general/test_contextmenu_input.html
+++ b/browser/base/content/test/general/test_contextmenu_input.html
@@ -193,16 +193,17 @@ function runTest(testNum) {
     case 14: // type='color'
     case 15: // type='range'
         checkContextMenu(["context-navigation", null,
                               ["context-back",         false,
                                "context-forward",      false,
                                "context-reload",       true,
                                "context-bookmarkpage", true], null,
                           "---",                  null,
+                          "context-sharepage",    true,
                           "context-savepage",     true,
                           "---",                  null,
                           "context-viewbgimage",  false,
                           "context-selectall",    null,
                           "---",                  null,
                           "context-viewsource",   true,
                           "context-viewinfo",     true,
                           "---",                 null,
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -6,16 +6,17 @@ support-files =
   head.js
   opengraph/og_invalid_url.html
   opengraph/opengraph.html
   opengraph/shortlink_linkrel.html
   opengraph/shorturl_link.html
   opengraph/shorturl_linkrel.html
   microdata.html
   share.html
+  share_activate.html
   social_activate.html
   social_activate_iframe.html
   social_chat.html
   social_crash_content_helper.js
   social_flyout.html
   social_mark.html
   social_panel.html
   social_postActivation.html
--- a/browser/base/content/test/social/browser_aboutHome_activation.js
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -14,17 +14,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 let snippet =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/favicon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
 '     </script>' +
@@ -36,17 +36,17 @@ let snippet =
 let snippet2 =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/favicon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '         "oneclick": true' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
--- a/browser/base/content/test/social/browser_share.js
+++ b/browser/base/content/test/social/browser_share.js
@@ -5,20 +5,56 @@ let baseURL = "https://example.com/brows
 
 let manifest = { // normal provider
   name: "provider 1",
   origin: "https://example.com",
   workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
   shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
 };
+let activationPage = "https://example.com/browser/browser/base/content/test/social/share_activate.html";
+
+function waitForProviderEnabled(cb) {
+  Services.obs.addObserver(function providerSet(subject, topic, data) {
+    Services.obs.removeObserver(providerSet, "social:provider-enabled");
+    info("social:provider-enabled observer was notified");
+    cb();
+  }, "social:provider-enabled", false);
+}
+
+function sendActivationEvent(callback) {
+  // hack Social.lastEventReceived so we don't hit the "too many events" check.
+  Social.lastEventReceived = 0;
+  let doc = SocialShare.iframe.contentDocument;
+  // if our test has a frame, use it
+  let button = doc.getElementById("activation");
+  ok(!!button, "got the activation button");
+  EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
+  if (callback)
+    executeSoon(callback);
+}
+
+function waitForEvent(iframe, eventName, callback) {
+  iframe.addEventListener(eventName, function load() {
+    info("page load is "+iframe.contentDocument.location.href);
+    if (iframe.contentDocument.location.href != "data:text/plain;charset=utf8,") {
+      iframe.removeEventListener(eventName, load, true);
+      executeSoon(callback);
+    }
+  }, true);
+}
 
 function test() {
   waitForExplicitFinish();
-
+  Services.prefs.setCharPref("social.shareDirectory", activationPage);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref("social.directories");
+    Services.prefs.clearUserPref("social.shareDirectory");
+    Services.prefs.clearUserPref("social.share.activationPanelEnabled");
+  });
   runSocialTests(tests);
 }
 
 let corpus = [
   {
     url: baseURL+"opengraph/opengraph.html",
     options: {
       // og:title
@@ -70,21 +106,20 @@ let corpus = [
       shortUrl: "http://imshort/p/abcde"
     }
   }
 ];
 
 function loadURLInTab(url, callback) {
   info("Loading tab with "+url);
   let tab = gBrowser.selectedTab = gBrowser.addTab(url);
-  tab.linkedBrowser.addEventListener("load", function listener() {
+  waitForEvent(tab.linkedBrowser, "load", () => {
     is(tab.linkedBrowser.currentURI.spec, url, "tab loaded")
-    tab.linkedBrowser.removeEventListener("load", listener, true);
-    executeSoon(function() { callback(tab) });
-  }, true);
+    callback(tab)
+  });
 }
 
 function hasoptions(testOptions, options) {
   let msg;
   for (let option in testOptions) {
     let data = testOptions[option];
     info("data: "+JSON.stringify(data));
     let message_data = options[option];
@@ -105,17 +140,16 @@ var tests = {
     // starting on about:blank page, share should be visible but disabled when
     // adding provider
     is(gBrowser.contentDocument.location.href, "about:blank");
     SocialService.addProvider(manifest, function(provider) {
       is(SocialUI.enabled, true, "SocialUI is enabled");
       checkSocialUI();
       // share should not be enabled since we only have about:blank page
       let shareButton = SocialShare.shareButton;
-      is(shareButton.disabled, true, "share button is disabled");
       // verify the attribute for proper css
       is(shareButton.getAttribute("disabled"), "true", "share button attribute is disabled");
       // button should be visible
       is(shareButton.hidden, false, "share button is visible");
       SocialService.disableProvider(manifest.origin, next);
     });
   },
   testShareEnabledOnActivation: function(next) {
@@ -123,17 +157,16 @@ var tests = {
     // activating provider
     let testData = corpus[0];
     loadURLInTab(testData.url, function(tab) {
       SocialService.addProvider(manifest, function(provider) {
         is(SocialUI.enabled, true, "SocialUI is enabled");
         checkSocialUI();
         // share should not be enabled since we only have about:blank page
         let shareButton = SocialShare.shareButton;
-        is(shareButton.disabled, false, "share button is enabled");
         // verify the attribute for proper css
         ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
         // button should be visible
         is(shareButton.hidden, false, "share button is visible");
         gBrowser.removeTab(tab);
         next();
       });
     });
@@ -144,17 +177,17 @@ var tests = {
     ok(port, "provider has a port");
     let testTab;
     let testIndex = 0;
     let testData = corpus[testIndex++];
 
     function runOneTest() {
       loadURLInTab(testData.url, function(tab) {
         testTab = tab;
-        SocialShare.sharePage();
+        SocialShare.sharePage(manifest.origin);
       });
     }
 
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "got-share-data-message":
           gBrowser.removeTab(testTab);
@@ -236,10 +269,51 @@ var tests = {
       let url = "https://example.com/browser/browser/base/content/test/social/microdata.html"
       addTab(url, function(tab) {
         testTab = tab;
         let doc = tab.linkedBrowser.contentDocument;
         target = doc.getElementById("simple-hcard");
         SocialShare.sharePage(manifest.origin, null, target);
       });
     });
+  },
+  testSharePanelActivation: function(next) {
+    let testTab;
+    // cleared in the cleanup function
+    Services.prefs.setCharPref("social.directories", "https://example.com");
+    Services.prefs.setBoolPref("social.share.activationPanelEnabled", true);
+    // make the iframe so we can wait on the load
+    SocialShare._createFrame();
+    let iframe = SocialShare.iframe;
+
+    waitForEvent(iframe, "load", () => {
+      waitForCondition(() => {
+          // sometimes the iframe is ready before the panel is open, we need to
+          // wait for both conditions
+          return SocialShare.panel.state == "open";
+        }, () => {
+        is(iframe.contentDocument.location.href, activationPage, "activation page loaded");
+        waitForProviderEnabled(() => {
+          let provider = Social._getProviderFromOrigin(manifest.origin);
+          let port = provider.getWorkerPort();
+          ok(!!port, "got port");
+          port.onmessage = function (e) {
+            let topic = e.data.topic;
+            info("got topic "+topic+"\n");
+            switch (topic) {
+              case "got-share-data-message":
+                ok(true, "share completed");
+                gBrowser.removeTab(testTab);
+                SocialService.uninstallProvider(manifest.origin, next);
+                break;
+            }
+          }
+          port.postMessage({topic: "test-init"});
+        });
+        sendActivationEvent();
+      }, "share panel did not open and load share page");
+    });
+    loadURLInTab(activationPage, function(tab) {
+      testTab = tab;
+      SocialShare.sharePage();
+    });
   }
 }
--- a/browser/base/content/test/social/browser_social_chatwindow.js
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -29,24 +29,24 @@ let manifests = [
 ];
 
 let chatId = 0;
 function openChat(provider, callback) {
   let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
   let port = provider.getWorkerPort();
   port.onmessage = function(e) {
     if (e.data.topic == "got-chatbox-message") {
-      port.close();
       callback();
     }
   }
   let url = chatUrl + "?" + (chatId++);
   port.postMessage({topic: "test-init"});
   port.postMessage({topic: "test-worker-chat", data: url});
   gURLsNotRemembered.push(url);
+  return port;
 }
 
 function windowHasChats(win) {
   return !!getChatBar().firstElementChild;
 }
 
 function test() {
   requestLongerTimeout(2); // only debug builds seem to need more time...
@@ -167,16 +167,19 @@ var tests = {
 
   // Check what happens when you close the only visible chat.
   testCloseOnlyVisible: function(next) {
     let chatbar = getChatBar();
     let chatWidth = undefined;
     let num = 0;
     is(chatbar.childNodes.length, 0, "chatbar starting empty");
     is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
+    let port = SocialSidebar.provider.getWorkerPort();
+    ok(port, "provider has a port");
+    port.postMessage({topic: "test-init"});
 
     makeChat("normal", "first chat", function() {
       // got the first one.
       checkPopup();
       ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible");
       // we kinda cheat here and get the width of the first chat, assuming
       // that all future chats will have the same width when open.
       chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat);
@@ -190,16 +193,17 @@ var tests = {
           let second = chatbar.childNodes[1];
           is(chatbar.selectedChat, first, "first chat is selected");
           ok(second.collapsed, "second chat is currently collapsed");
           // closing the first chat will leave enough room for the second
           // chat to appear, and thus become selected.
           chatbar.selectedChat.close();
           is(chatbar.selectedChat, second, "second chat is selected");
           closeAllChats();
+          port.close();
           next();
         });
       });
     });
   },
 
   testShowWhenCollapsed: function(next) {
     let port = SocialSidebar.provider.getWorkerPort();
@@ -238,34 +242,34 @@ var tests = {
           });
       }
     }
     port.postMessage({topic: "test-init", data: { id: 1 }});
   },
 
   testMultipleProviderChat: function(next) {
     // test incomming chats from all providers
-    openChat(Social.providers[0], function() {
-      openChat(Social.providers[1], function() {
-        openChat(Social.providers[2], function() {
+    let port0 = openChat(Social.providers[0], function() {
+      let port1 = openChat(Social.providers[1], function() {
+        let port2 = openChat(Social.providers[2], function() {
           let chats = document.getElementById("pinnedchats");
           waitForCondition(function() chats.children.length == Social.providers.length,
             function() {
               ok(true, "one chat window per provider opened");
               // test logout of a single provider
-              let provider = Social.providers[2];
-              let port = provider.getWorkerPort();
-              port.postMessage({topic: "test-logout"});
+              port2.postMessage({topic: "test-logout"});
               waitForCondition(function() chats.children.length == Social.providers.length - 1,
                 function() {
                   closeAllChats();
                   waitForCondition(function() chats.children.length == 0,
                                    function() {
                                     ok(!chats.selectedChat, "multiprovider chats are all closed");
-                                    port.close();
+                                    port0.close();
+                                    port1.close();
+                                    port2.close();
                                     next();
                                    },
                                    "chat windows didn't close");
                 },
                 "chat window didn't close");
             }, "chat windows did not open");
         });
       });
--- a/browser/base/content/test/social/browser_social_errorPage.js
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -31,20 +31,26 @@ function goOnline(callback) {
     BrowserOffline.toggleOfflineStatus();
   if (callback)
     callback();
 }
 
 function openPanel(url, panelCallback, loadCallback) {
   // open a flyout
   SocialFlyout.open(url, 0, panelCallback);
-  SocialFlyout.panel.firstChild.addEventListener("load", function panelLoad() {
-    SocialFlyout.panel.firstChild.removeEventListener("load", panelLoad, true);
-    loadCallback();
-  }, true);
+  // wait for both open and loaded before callback. Since the test doesn't close
+  // the panel between opens, we cannot rely on events here. We need to ensure
+  // popupshown happens before we finish out the tests.
+  waitForCondition(function() {
+                    return SocialFlyout.panel.state == "open" &&
+                           SocialFlyout.iframe.contentDocument.readyState == "complete";
+                   },
+                   loadCallback,
+                   "flyout is open and loaded");
+
 }
 
 function openChat(url, panelCallback, loadCallback) {
   // open a chat window
   let chatbar = getChatBar();
   openChatWindow(null, SocialSidebar.provider, url, panelCallback);
   chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() {
     chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true);
@@ -178,16 +184,20 @@ var tests = {
       }
     );
   },
 
   testChatWindowAfterTearOff: function(next) {
     // Ensure that the error listener survives the chat window being detached.
     let url = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
     let panelCallbackCount = 0;
+    // chatwindow tests throw errors, which muddy test output, if the worker
+    // doesn't get test-init
+    let port = SocialSidebar.provider.getWorkerPort();
+    port.postMessage({topic: "test-init"});
     // open a chat while we are still online.
     openChat(
       url,
       null,
       function() { // the "load" callback.
         executeSoon(function() {
           let chat = getChatBar().selectedChat;
           is(chat.contentDocument.location.href, url, "correct url loaded");
@@ -195,16 +205,17 @@ var tests = {
           chat.swapWindows().then(
             chat => {
               // now go offline and reload the chat - about:socialerror should be loaded.
               goOffline();
               chat.contentDocument.location.reload();
               waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
                                function() {
                                 chat.close();
+                                port.close();
                                 next();
                                 },
                                "error page didn't appear");
             }
           );
         });
       }
     );
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/share_activate.html
@@ -0,0 +1,36 @@
+<html>
+<!-- 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/. -->
+<head>
+  <title>Activation test</title>
+</head>
+<script>
+
+var data = {
+  // currently required
+  "name": "Demo Social Service",
+  // browser_share.js serves this page from "https://example.com"
+  "origin": "https://example.com",
+  "iconURL": "chrome://branding/content/icon16.png",
+  "icon32URL": "chrome://branding/content/favicon32.png",
+  "icon64URL": "chrome://branding/content/icon64.png",
+  "workerURL": "/browser/browser/base/content/test/social/social_worker.js",
+  "shareURL": "/browser/browser/base/content/test/social/share.html"
+}
+
+function activate(node) {
+  node.setAttribute("data-service", JSON.stringify(data));
+  var event = new CustomEvent("ActivateSocialFeature");
+  node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this, true)">Activate the share provider</button>
+
+</body>
+</html>
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -304,18 +304,16 @@ function openLinkIn(url, where, params) 
       loadInBackground = false;
     }
   }
 
   // Raise the target window before loading the URI, since loading it may
   // result in a new frontmost window (e.g. "javascript:window.open('');").
   w.focus();
 
-  let newTab;
-
   switch (where) {
   case "current":
     let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
 
     if (aAllowThirdPartyFixup) {
       flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
       flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
     }
@@ -329,39 +327,32 @@ function openLinkIn(url, where, params) 
 
     w.gBrowser.loadURIWithFlags(url, flags, aReferrerURI, null, aPostData);
     break;
   case "tabshifted":
     loadInBackground = !loadInBackground;
     // fall through
   case "tab":
     let browser = w.gBrowser;
-    newTab = browser.loadOneTab(url, {
-                                referrerURI: aReferrerURI,
-                                charset: aCharset,
-                                postData: aPostData,
-                                inBackground: loadInBackground,
-                                allowThirdPartyFixup: aAllowThirdPartyFixup,
-                                relatedToCurrent: aRelatedToCurrent,
-                                skipAnimation: aSkipTabAnimation,
-                                allowMixedContent: aAllowMixedContent });
+    browser.loadOneTab(url, {
+                       referrerURI: aReferrerURI,
+                       charset: aCharset,
+                       postData: aPostData,
+                       inBackground: loadInBackground,
+                       allowThirdPartyFixup: aAllowThirdPartyFixup,
+                       relatedToCurrent: aRelatedToCurrent,
+                       skipAnimation: aSkipTabAnimation,
+                       allowMixedContent: aAllowMixedContent });
     break;
   }
 
   w.gBrowser.selectedBrowser.focus();
 
-  if (!loadInBackground && w.isBlankPageURL(url)) {
-    if (newTab) {
-      // Remote tab content does not focus synchronously, so we set the flag
-      // on this tab to skip focusing the content if we want to focus the URL
-      // bar instead.
-      newTab._urlbarFocused = true;
-    }
+  if (!loadInBackground && w.isBlankPageURL(url))
     w.focusAndSelectUrlBar();
-  }
 }
 
 // Used as an onclick handler for UI elements with link-like behavior.
 // e.g. onclick="checkForMiddleClick(this, event);"
 function checkForMiddleClick(node, event) {
   // We should be using the disabled property here instead of the attribute,
   // but some elements that this function is used with don't support it (e.g.
   // menuitem).
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -161,16 +161,17 @@ let CustomizableUIInternal = {
       "save-page-button",
       "print-button",
       "history-panelmenu",
       "fullscreen-button",
       "find-button",
       "preferences-button",
       "add-ons-button",
       "developer-button",
+      "social-share-button",
     ];
 
     if (gPalette.has("switch-to-metro-button")) {
       panelPlacements.push("switch-to-metro-button");
     }
 
 #ifdef E10S_TESTING_ONLY
     if (gPalette.has("e10s-button")) {
@@ -202,17 +203,16 @@ let CustomizableUIInternal = {
       overflowable: true,
       defaultPlacements: [
         "urlbar-container",
         "search-container",
         "bookmarks-menu-button",
         "downloads-button",
         "home-button",
         "loop-call-button",
-        "social-share-button",
       ],
       defaultCollapsed: false,
     }, true);
 #ifndef XP_MACOSX
     this.registerArea(CustomizableUI.AREA_MENUBAR, {
       legacy: true,
       type: CustomizableUI.TYPE_TOOLBAR,
       defaultPlacements: [
--- a/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
+++ b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js
@@ -22,25 +22,25 @@ add_task(function testWrapUnwrap() {
   ok(!wrapper, "There should be a wrapper");
   let item = document.getElementById(kTestWidget1);
   ok(!item, "There should no longer be an item");
 });
 
 // Creating and destroying a widget should correctly deal with panel placeholders
 add_task(function testPanelPlaceholders() {
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
-  is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 1 : 2, "The number of placeholders should be correct.");
+  is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 3 : 1, "The number of placeholders should be correct.");
   CustomizableUI.createWidget({id: kTestWidget2, label: 'Pretty label', tooltiptext: 'Pretty tooltip', defaultArea: CustomizableUI.AREA_PANEL});
   let elem = document.getElementById(kTestWidget2);
   let wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(elem, "There should be an item");
   ok(wrapper, "There should be a wrapper");
   is(wrapper.firstChild.id, kTestWidget2, "Wrapper should have test widget");
   is(wrapper.parentNode, panel, "Wrapper should be in panel");
-  is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 3 : 1, "The number of placeholders should be correct.");
+  is(panel.querySelectorAll(".panel-customization-placeholder").length, isInWin8() ? 2 : 3, "The number of placeholders should be correct.");
   CustomizableUI.destroyWidget(kTestWidget2);
   wrapper = document.getElementById("wrapper-" + kTestWidget2);
   ok(!wrapper, "There should be a wrapper");
   let item = document.getElementById(kTestWidget2);
   ok(!item, "There should no longer be an item");
   yield endCustomizing();
 });
 
--- a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
+++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
@@ -17,17 +17,18 @@ add_task(function() {
                              "save-page-button",
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, printButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -43,17 +44,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
 });
 
 
 // Dragging the zoom controls to be before the new-window button should not move any widgets.
@@ -67,17 +69,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the zoom controls to be before the history-panelmenu should move the zoom-controls in to the row higher than the history-panelmenu.
 add_task(function() {
@@ -90,17 +93,18 @@ add_task(function() {
                              "save-page-button",
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, historyPanelMenu);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -117,17 +121,18 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "zoom-controls",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, preferencesButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -144,17 +149,18 @@ add_task(function() {
                                "zoom-controls",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
-                               "developer-button"];
+                               "developer-button",
+                               "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterInsert);
   simulateItemDrag(openFileButton, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -183,17 +189,18 @@ add_task(function() {
                                "zoom-controls",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
-                               "developer-button"];
+                               "developer-button",
+                               "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterInsert);
   simulateItemDrag(openFileButton, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -219,17 +226,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the new-window-button should
 // move the zoom-controls before the edit-controls.
@@ -243,17 +251,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -270,17 +279,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, privateBrowsingButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -297,17 +307,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -323,17 +334,18 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, panel);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -348,17 +360,18 @@ add_task(function() {
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   let paletteChildElementCount = palette.childElementCount;
   simulateItemDrag(editControls, palette);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   is(paletteChildElementCount + 1, palette.childElementCount,
      "The palette should have a new child, congratulations!");
   is(editControls.parentNode.id, "wrapper-edit-controls",
      "The edit-controls should be properly wrapped.");
@@ -372,33 +385,35 @@ add_task(function() {
 });
 
 // Dragging the edit-controls to each of the panel placeholders
 // should append the edit-controls to the bottom of the panel.
 add_task(function() {
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
-  let numPlaceholders = isInWin8() ? 1 : 2;
+  let numPlaceholders = isInWin8() ? 3 : 1;
+  is(numPlaceholders, panel.getElementsByClassName("panel-customization-placeholder").length, "correct number of placeholders");
   for (let i = 0; i < numPlaceholders; i++) {
     // NB: We can't just iterate over all of the placeholders
     // because each drag-drop action recreates them.
     let placeholder = panel.getElementsByClassName("panel-customization-placeholder")[i];
     let placementsAfterMove = ["zoom-controls",
                                "new-window-button",
                                "privatebrowsing-button",
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "edit-controls",
-                               "developer-button"];
+                               "developer-button",
+                               "social-share-button"];
     addSwitchToMetroButtonInWindows8(placementsAfterMove);
     simulateItemDrag(editControls, placeholder);
     assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
     let zoomControls = document.getElementById("zoom-controls");
     simulateItemDrag(editControls, zoomControls);
     ok(CustomizableUI.inDefaultState, "Should still be in default state.");
   }
 });
@@ -418,40 +433,46 @@ add_task(function() {
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging a small button onto the last big button should work.
 add_task(function() {
   yield startCustomizing();
   let editControls = document.getElementById("edit-controls");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
+  let numPlaceholders = isInWin8() ? 3 : 1;
+  is(panel.getElementsByClassName("panel-customization-placeholder").length, numPlaceholders, "correct number of placeholders");
   let target = panel.getElementsByClassName("panel-customization-placeholder")[0];
   let placementsAfterMove = ["zoom-controls",
                              "new-window-button",
                              "privatebrowsing-button",
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
-                             "developer-button"];
+                             "developer-button",
+                             "social-share-button"];
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
+  if (isInWin8()) {
+    placementsAfterMove.splice(10, 1);
+    placementsAfterMove.push("edit-controls");
+  }
   simulateItemDrag(editControls, target);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let itemToDrag = "sync-button";
   let button = document.getElementById(itemToDrag);
-  placementsAfterMove.splice(11, 0, itemToDrag);
   if (isInWin8()) {
-    placementsAfterMove[10] = placementsAfterMove[11];
-    placementsAfterMove[11] = placementsAfterMove[12];
-    placementsAfterMove[12] = placementsAfterMove[13];
-    placementsAfterMove[13] = "edit-controls";
+    placementsAfterMove.push(itemToDrag);
+  } else {
+    placementsAfterMove.splice(10, 1, itemToDrag);
+    placementsAfterMove.push("edit-controls");
   }
   simulateItemDrag(button, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
 
   // Put stuff back:
   let palette = document.getElementById("customization-palette");
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(button, palette);
--- a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
+++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
@@ -6,117 +6,113 @@
 
 requestLongerTimeout(2);
 
 // One orphaned item should have two placeholders next to it.
 add_task(function() {
   yield startCustomizing();
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
-  let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
+  CustomizableUI.removeWidgetFromArea("social-share-button");
   if (isInWin8()) {
     CustomizableUI.removeWidgetFromArea("switch-to-metro-button");
-    placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
-    ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
-  } else {
-    ok(CustomizableUI.inDefaultState, "Should be in default state.");
   }
+  let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
+  ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering");
 
+  CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
   if (isInWin8()) {
     CustomizableUI.addWidgetToArea("switch-to-metro-button", CustomizableUI.AREA_PANEL);
   }
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 1).
 add_task(function() {
   yield startCustomizing();
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
+  CustomizableUI.removeWidgetFromArea("social-share-button");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   let placementsAfterAppend = placements;
 
   if (!isInWin8()) {
     placementsAfterAppend = placements.concat(["open-file-button"]);
     simulateItemDrag(btn, panel);
   }
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
-  is(CustomizableUI.inDefaultState, isInWin8(), "Should only be in default state if on Win8");
+  ok(!CustomizableUI.inDefaultState, "Should not be in default state");
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering");
 
   let palette = document.getElementById("customization-palette");
   simulateItemDrag(btn, palette);
 
   if (!isInWin8()) {
     btn = document.getElementById("open-file-button");
     simulateItemDrag(btn, palette);
   }
+  CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state again."); 
 });
 
 // Two orphaned items should have one placeholder next to them (case 2).
 add_task(function() {
   yield startCustomizing();
-  let btn = document.getElementById("add-ons-button");
-  let btn2 = document.getElementById("developer-button");
-  let btn3 = document.getElementById("switch-to-metro-button");
+  let buttonsToMove = ["add-ons-button", "developer-button", "social-share-button"];
+  if (isInWin8()) {
+    buttonsToMove.push("switch-to-metro-button");
+  }
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
-  let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id);
-  simulateItemDrag(btn, palette);
-  simulateItemDrag(btn2, palette);
-
-  if (isInWin8()) {
-    placementsAfterAppend = placementsAfterAppend.filter(p => p != btn3.id);
-    simulateItemDrag(btn3, palette);
+  let placementsAfterAppend = placements.filter(p => buttonsToMove.indexOf(p) < 0);
+  for (let i in buttonsToMove) {
+    CustomizableUI.removeWidgetFromArea(buttonsToMove[i]);
   }
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering");
 
-  simulateItemDrag(btn, panel);
-  simulateItemDrag(btn2, panel);
-
-  if (isInWin8()) {
-    simulateItemDrag(btn3, panel);
+  for (let i in buttonsToMove) {
+    CustomizableUI.addWidgetToArea(buttonsToMove[i], CustomizableUI.AREA_PANEL);
   }
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // A wide widget at the bottom of the panel should have three placeholders after it.
 add_task(function() {
   yield startCustomizing();
   let btn = document.getElementById("edit-controls");
   let developerButton = document.getElementById("developer-button");
   let metroBtn = document.getElementById("switch-to-metro-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
+  CustomizableUI.removeWidgetFromArea("social-share-button");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   placements.pop();
   simulateItemDrag(developerButton, palette);
   if (isInWin8()) {
     // Remove switch-to-metro-button
     placements.pop();
     simulateItemDrag(metroBtn, palette);
@@ -128,28 +124,29 @@ add_task(function() {
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders after re-entering");
 
   simulateItemDrag(developerButton, panel);
+  CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_PANEL);
   if (isInWin8()) {
     simulateItemDrag(metroBtn, panel);
   }
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(btn, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
-// The default placements should have two placeholders at the bottom (or 1 in win8).
+// The default placements should have one placeholder at the bottom (or 3 in metro-enabled win8).
 add_task(function() {
   yield startCustomizing();
-  let numPlaceholders = isInWin8() ? 1 : 2;
+  let numPlaceholders = isInWin8() ? 3 : 1;
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering");
 
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -31,13 +31,14 @@
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/router.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
+    <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/desktopRouter.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -184,48 +184,80 @@ loop.conversation = (function(OT, mozL10
       this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
       });
       this._conversation.once("call:incoming", this.startCall, this);
       this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
         if (err) {
           console.error("Failed to get the sessionData", err);
           // XXX Not the ideal response, but bug 1047410 will be replacing
-          //this by better "call failed" UI.
+          // this by better "call failed" UI.
           this._notifier.errorL10n("cannot_start_call_session_not_ready");
           return;
         }
+
         // XXX For incoming calls we might have more than one call queued.
         // For now, we'll just assume the first call is the right information.
         // We'll probably really want to be getting this data from the
         // background worker on the desktop client.
         // Bug 1032700 should fix this.
         this._conversation.setIncomingSessionData(sessionData[0]);
+
+        this._setupWebSocketAndCallView();
+      });
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     */
+    _setupWebSocketAndCallView: function() {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this._conversation.get("progressURL"),
+        websocketToken: this._conversation.get("websocketToken"),
+        callId: this._conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
           video: {enabled: this._conversation.hasVideoStream("incoming")}
         }));
-      });
+      }.bind(this), function() {
+        this._handleSessionError();
+        return;
+      }.bind(this));
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
       this._conversation.incoming();
     },
 
     /**
+     * Declines a call and handles closing of the window.
+     */
+    _declineCall: function() {
+      this._websocket.decline();
+      // XXX Don't close the window straight away, but let any sends happen
+      // first. Ideally we'd wait to close the window until after we have a
+      // response from the server, to know that everything has completed
+      // successfully. However, that's quite difficult to ensure at the
+      // moment so we'll add it later.
+      setTimeout(window.close, 0);
+    },
+
+    /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
-      // XXX For now, we just close the window
-      window.close();
+      this._declineCall();
     },
 
     /**
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
@@ -233,43 +265,52 @@ loop.conversation = (function(OT, mozL10
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref("loopToken");
       this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
         // (bug 1048909).
         console.log(error);
       });
-      window.close();
+      this._declineCall();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
      */
     conversation: function() {
       if (!this._conversation.isSessionReady()) {
         console.error("Error: navigated to conversation route without " +
           "the start route to initialise the call first");
-        this._notifier.errorL10n("cannot_start_call_session_not_ready");
+        this._handleSessionError();
         return;
       }
 
       var callType = this._conversation.get("selectedCallType");
       var videoStream = callType === "audio" ? false : true;
 
       /*jshint newcap:false*/
       this.loadReactComponent(sharedViews.ConversationView({
         sdk: OT,
         model: this._conversation,
         video: {enabled: videoStream}
       }));
     },
 
     /**
+     * Handles a error starting the session
+     */
+    _handleSessionError: function() {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      this._notifier.errorL10n("cannot_start_call_session_not_ready");
+    },
+
+    /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
       document.title = mozL10n.get("call_has_ended");
 
       var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
         "feedback.baseUrl");
 
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -184,48 +184,80 @@ loop.conversation = (function(OT, mozL10
       this._conversation.once("declineAndBlock", () => {
         this.navigate("call/declineAndBlock", {trigger: true});
       });
       this._conversation.once("call:incoming", this.startCall, this);
       this._client.requestCallsInfo(loopVersion, (err, sessionData) => {
         if (err) {
           console.error("Failed to get the sessionData", err);
           // XXX Not the ideal response, but bug 1047410 will be replacing
-          //this by better "call failed" UI.
+          // this by better "call failed" UI.
           this._notifier.errorL10n("cannot_start_call_session_not_ready");
           return;
         }
+
         // XXX For incoming calls we might have more than one call queued.
         // For now, we'll just assume the first call is the right information.
         // We'll probably really want to be getting this data from the
         // background worker on the desktop client.
         // Bug 1032700 should fix this.
         this._conversation.setIncomingSessionData(sessionData[0]);
+
+        this._setupWebSocketAndCallView();
+      });
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     */
+    _setupWebSocketAndCallView: function() {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this._conversation.get("progressURL"),
+        websocketToken: this._conversation.get("websocketToken"),
+        callId: this._conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
           video: {enabled: this._conversation.hasVideoStream("incoming")}
         }));
-      });
+      }.bind(this), function() {
+        this._handleSessionError();
+        return;
+      }.bind(this));
     },
 
     /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
       this._conversation.incoming();
     },
 
     /**
+     * Declines a call and handles closing of the window.
+     */
+    _declineCall: function() {
+      this._websocket.decline();
+      // XXX Don't close the window straight away, but let any sends happen
+      // first. Ideally we'd wait to close the window until after we have a
+      // response from the server, to know that everything has completed
+      // successfully. However, that's quite difficult to ensure at the
+      // moment so we'll add it later.
+      setTimeout(window.close, 0);
+    },
+
+    /**
      * Declines an incoming call.
      */
     decline: function() {
       navigator.mozLoop.stopAlerting();
-      // XXX For now, we just close the window
-      window.close();
+      this._declineCall();
     },
 
     /**
      * Decline and block an incoming call
      * @note:
      * - loopToken is the callUrl identifier. It gets set in the panel
      *   after a callUrl is received
      */
@@ -233,43 +265,52 @@ loop.conversation = (function(OT, mozL10
       navigator.mozLoop.stopAlerting();
       var token = navigator.mozLoop.getLoopCharPref("loopToken");
       this._client.deleteCallUrl(token, function(error) {
         // XXX The conversation window will be closed when this cb is triggered
         // figure out if there is a better way to report the error to the user
         // (bug 1048909).
         console.log(error);
       });
-      window.close();
+      this._declineCall();
     },
 
     /**
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
      */
     conversation: function() {
       if (!this._conversation.isSessionReady()) {
         console.error("Error: navigated to conversation route without " +
           "the start route to initialise the call first");
-        this._notifier.errorL10n("cannot_start_call_session_not_ready");
+        this._handleSessionError();
         return;
       }
 
       var callType = this._conversation.get("selectedCallType");
       var videoStream = callType === "audio" ? false : true;
 
       /*jshint newcap:false*/
       this.loadReactComponent(sharedViews.ConversationView({
         sdk: OT,
         model: this._conversation,
         video: {enabled: videoStream}
       }));
     },
 
     /**
+     * Handles a error starting the session
+     */
+    _handleSessionError: function() {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      this._notifier.errorL10n("cannot_start_call_session_not_ready");
+    },
+
+    /**
      * Call has ended, display a feedback form.
      */
     feedback: function() {
       document.title = mozL10n.get("call_has_ended");
 
       var feebackAPIBaseUrl = navigator.mozLoop.getLoopCharPref(
         "feedback.baseUrl");
 
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -20,16 +20,21 @@ loop.shared.models = (function() {
       loopToken:    undefined,     // Loop conversation token
       loopVersion:  undefined,     // Loop version for /calls/ information. This
                                    // is the version received from the push
                                    // notification and is used by the server to
                                    // determine the pending calls
       sessionId:    undefined,     // OT session id
       sessionToken: undefined,     // OT session token
       apiKey:       undefined,     // OT api key
+      callId:       undefined,     // The callId on the server
+      progressURL:  undefined,     // The websocket url to use for progress
+      websocketToken: undefined,   // The token to use for websocket auth, this is
+                                   // stored as a hex string which is what the server
+                                   // requires.
       callType:     undefined,     // The type of incoming call selected by
                                    // other peer ("audio" or "audio-video")
       selectedCallType: undefined  // The selected type for the call that was
                                    // initiated ("audio" or "audio-video")
     },
 
     /**
      * SDK object.
@@ -135,34 +140,40 @@ loop.shared.models = (function() {
      * Sets session information.
      * Session data received by creating an outgoing call.
      *
      * @param {Object} sessionData Conversation session information.
      */
     setOutgoingSessionData: function(sessionData) {
       // Explicit property assignment to prevent later "surprises"
       this.set({
-        sessionId:    sessionData.sessionId,
-        sessionToken: sessionData.sessionToken,
-        apiKey:       sessionData.apiKey
+        sessionId:      sessionData.sessionId,
+        sessionToken:   sessionData.sessionToken,
+        apiKey:         sessionData.apiKey,
+        callId:         sessionData.callId,
+        progressURL:    sessionData.progressURL,
+        websocketToken: sessionData.websocketToken.toString(16)
       });
     },
 
     /**
      * Sets session information about the incoming call.
      *
      * @param {Object} sessionData Conversation session information.
      */
     setIncomingSessionData: function(sessionData) {
       // Explicit property assignment to prevent later "surprises"
       this.set({
-        sessionId:    sessionData.sessionId,
-        sessionToken: sessionData.sessionToken,
-        apiKey:       sessionData.apiKey,
-        callType:     sessionData.callType || "audio-video"
+        sessionId:      sessionData.sessionId,
+        sessionToken:   sessionData.sessionToken,
+        apiKey:         sessionData.apiKey,
+        callId:         sessionData.callId,
+        progressURL:    sessionData.progressURL,
+        websocketToken: sessionData.websocketToken.toString(16),
+        callType:       sessionData.callType || "audio-video"
       });
     },
 
     /**
      * Starts a SDK session and subscribe to call events.
      */
     startSession: function() {
       if (!this.isSessionReady()) {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -0,0 +1,237 @@
+/* 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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.CallConnectionWebSocket = (function() {
+  "use strict";
+
+  // Response timeout is 5 seconds as per API.
+  var kResponseTimeout = 5000;
+
+  /**
+   * Handles a websocket specifically for a call connection.
+   *
+   * There should be one of these created for each call connection.
+   *
+   * options items:
+   * - url             The url of the websocket to connect to.
+   * - callId          The call id for the call
+   * - websocketToken  The authentication token for the websocket
+   *
+   * @param {Object} options The options for this websocket.
+   */
+  function CallConnectionWebSocket(options) {
+    this.options = options || {};
+
+    if (!this.options.url) {
+      throw new Error("No url in options");
+    }
+    if (!this.options.callId) {
+      throw new Error("No callId in options");
+    }
+    if (!this.options.websocketToken) {
+      throw new Error("No websocketToken in options");
+    }
+
+    // Save the debug pref now, to avoid getting it each time.
+    if (navigator.mozLoop) {
+      this._debugWebSocket =
+        navigator.mozLoop.getLoopBoolPref("debug.websocket");
+    }
+
+    _.extend(this, Backbone.Events);
+  };
+
+  CallConnectionWebSocket.prototype = {
+    /**
+     * Start the connection to the websocket.
+     *
+     * @return {Promise} A promise that resolves when the websocket
+     *                   server connection is open and "hello"s have been
+     *                   exchanged. It is rejected if there is a failure in
+     *                   connection or the initial exchange of "hello"s.
+     */
+    promiseConnect: function() {
+      var promise = new Promise(
+        function(resolve, reject) {
+          this.socket = new WebSocket(this.options.url);
+          this.socket.onopen = this._onopen.bind(this);
+          this.socket.onmessage = this._onmessage.bind(this);
+          this.socket.onerror = this._onerror.bind(this);
+          this.socket.onclose = this._onclose.bind(this);
+
+          var timeout = setTimeout(function() {
+            if (this.connectDetails && this.connectDetails.reject) {
+              this.connectDetails.reject("timeout");
+              this._clearConnectionFlags();
+            }
+          }.bind(this), kResponseTimeout);
+          this.connectDetails = {
+            resolve: resolve,
+            reject: reject,
+            timeout: timeout
+          };
+        }.bind(this));
+
+      return promise;
+    },
+
+    _clearConnectionFlags: function() {
+      clearTimeout(this.connectDetails.timeout);
+      delete this.connectDetails;
+    },
+
+    /**
+     * Internal function called to resolve the connection promise.
+     *
+     * It will log an error if no promise is found.
+     */
+    _completeConnection: function() {
+      if (this.connectDetails && this.connectDetails.resolve) {
+        this.connectDetails.resolve();
+        this._clearConnectionFlags();
+        return;
+      }
+
+      console.error("Failed to complete connection promise - no promise available");
+    },
+
+    /**
+     * Checks if the websocket is connecting, and rejects the connection
+     * promise if appropriate.
+     *
+     * @param {Object} event The event to reject the promise with if
+     *                       appropriate.
+     */
+    _checkConnectionFailed: function(event) {
+      if (this.connectDetails && this.connectDetails.reject) {
+        this.connectDetails.reject(event);
+        this._clearConnectionFlags();
+        return true;
+      }
+
+      return false;
+    },
+
+    /**
+     * Notifies the server that the user has declined the call.
+     */
+    decline: function() {
+      this._send({
+        messageType: "action",
+        event: "terminate",
+        reason: "reject"
+      });
+    },
+
+    /**
+     * Sends data on the websocket.
+     *
+     * @param {Object} data The data to send.
+     */
+    _send: function(data) {
+      this._log("WS Sending", data);
+
+      this.socket.send(JSON.stringify(data));
+    },
+
+    /**
+     * Used to determine if the server state is in a completed state, i.e.
+     * the server has determined the connection is terminated or connected.
+     *
+     * @return True if the last received state is terminated or connected.
+     */
+    get _stateIsCompleted() {
+      return this._lastServerState === "terminated" ||
+             this._lastServerState === "connected";
+    },
+
+    /**
+     * Called when the socket is open. Automatically sends a "hello"
+     * message to the server.
+     */
+    _onopen: function() {
+      // Auto-register with the server.
+      this._send({
+        messageType: "hello",
+        callId: this.options.callId,
+        auth: this.options.websocketToken
+      });
+    },
+
+    /**
+     * Called when a message is received from the server.
+     *
+     * @param {Object} event The websocket onmessage event.
+     */
+    _onmessage: function(event) {
+      var msg;
+      try {
+        msg = JSON.parse(event.data);
+      } catch (x) {
+        console.error("Error parsing received message:", x);
+        return;
+      }
+
+      this._log("WS Receiving", event.data);
+
+      this._lastServerState = msg.state;
+
+      switch(msg.messageType) {
+        case "hello":
+          this._completeConnection();
+          break;
+        case "progress":
+          this.trigger("progress", msg);
+          break;
+      }
+    },
+
+    /**
+     * Called when there is an error on the websocket.
+     *
+     * @param {Object} event A simple error event.
+     */
+    _onerror: function(event) {
+      this._log("WS Error", event);
+
+      if (!this._stateIsCompleted &&
+          !this._checkConnectionFailed(event)) {
+        this.trigger("error", event);
+      }
+    },
+
+    /**
+     * Called when the websocket is closed.
+     *
+     * @param {CloseEvent} event The details of the websocket closing.
+     */
+    _onclose: function(event) {
+      this._log("WS Close", event);
+
+      // If the websocket goes away when we're not in a completed state
+      // then its an error. So we either pass it back via the connection
+      // promise, or trigger the closed event.
+      if (!this._stateIsCompleted &&
+          !this._checkConnectionFailed(event)) {
+        this.trigger("closed", event);
+      }
+    },
+
+    /**
+     * Logs debug to the console.
+     *
+     * Parameters: same as console.log
+     */
+    _log: function() {
+      if (this._debugWebSocket) {
+        console.log.apply(console, arguments);
+      }
+    }
+  };
+
+  return CallConnectionWebSocket;
+})();
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -41,16 +41,17 @@ browser.jar:
   content/browser/loop/shared/img/dropdown-inverse@2x.png    (content/shared/img/dropdown-inverse@2x.png)
 
   # Shared scripts
   content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
   content/browser/loop/shared/js/models.js            (content/shared/js/models.js)
   content/browser/loop/shared/js/router.js            (content/shared/js/router.js)
   content/browser/loop/shared/js/views.js             (content/shared/js/views.js)
   content/browser/loop/shared/js/utils.js             (content/shared/js/utils.js)
+  content/browser/loop/shared/js/websocket.js         (content/shared/js/websocket.js)
 
   # Shared libs
   content/browser/loop/shared/libs/react-0.11.1.js    (content/shared/libs/react-0.11.1.js)
   content/browser/loop/shared/libs/lodash-2.4.1.js    (content/shared/libs/lodash-2.4.1.js)
   content/browser/loop/shared/libs/jquery-2.1.0.js    (content/shared/libs/jquery-2.1.0.js)
   content/browser/loop/shared/libs/backbone-1.1.2.js  (content/shared/libs/backbone-1.1.2.js)
 
   # Shared sounds
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -32,16 +32,17 @@
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
 
     <!-- app scripts -->
     <script type="text/javascript" src="config.js"></script>
     <script type="text/javascript" src="shared/js/utils.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="shared/js/router.js"></script>
+    <script type="text/javascript" src="shared/js/websocket.js"></script>
     <script type="text/javascript" src="js/standaloneClient.js"></script>
     <script type="text/javascript" src="js/webapp.js"></script>
 
     <script>
       // Wait for all the localization notes to load
       window.addEventListener('localized', function() {
         loop.webapp.init();
       }, false);
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -369,23 +369,78 @@ loop.webapp = (function($, _, OT, webL10
      * Actually starts the call.
      */
     startCall: function() {
       var loopToken = this._conversation.get("loopToken");
       if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
+        this._setupWebSocketAndCallView(loopToken);
+      }
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     *
+     * @param {string} loopToken The session token to use.
+     */
+    _setupWebSocketAndCallView: function(loopToken) {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this._conversation.get("progressURL"),
+        websocketToken: this._conversation.get("websocketToken"),
+        callId: this._conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function() {
         this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
+      }.bind(this), function() {
+        // XXX Not the ideal response, but bug 1047410 will be replacing
+        // this by better "call failed" UI.
+        this._notifier.errorL10n("cannot_start_call_session_not_ready");
+        return;
+      }.bind(this));
+
+      this._websocket.on("progress", this._handleWebSocketProgress, this);
+    },
+
+    /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     */
+    _handleWebSocketProgress: function(progressData) {
+      if (progressData.state === "terminated") {
+        // XXX Before adding more states here, the basic protocol messages to the
+        // server need implementing on both the standalone and desktop side.
+        // These are covered by bug 1045643, but also check the dependencies on
+        // bug 1034041.
+        //
+        // Failure to do this will break desktop - standalone call setup. We're
+        // ok to handle reject, as that is a specific message from the destkop via
+        // the server.
+        switch (progressData.reason) {
+          case "reject":
+            this._handleCallRejected();
+        }
       }
     },
 
     /**
+     * Handles call rejection.
+     * XXX This should really display the call failed view - bug 1046959
+     * will implement this.
+     */
+    _handleCallRejected: function() {
+      this.endCall();
+      this._notifier.errorL10n("call_timeout_notification_text");
+    },
+
+    /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
     endCall: function() {
       var route = "home";
       if (this._conversation.get("loopToken")) {
         route = "call/" + this._conversation.get("loopToken");
       }
       this.navigate(route, {trigger: true});
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -369,23 +369,78 @@ loop.webapp = (function($, _, OT, webL10
      * Actually starts the call.
      */
     startCall: function() {
       var loopToken = this._conversation.get("loopToken");
       if (!loopToken) {
         this._notifier.errorL10n("missing_conversation_info");
         this.navigate("home", {trigger: true});
       } else {
+        this._setupWebSocketAndCallView(loopToken);
+      }
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     *
+     * @param {string} loopToken The session token to use.
+     */
+    _setupWebSocketAndCallView: function(loopToken) {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this._conversation.get("progressURL"),
+        websocketToken: this._conversation.get("websocketToken"),
+        callId: this._conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function() {
         this.navigate("call/ongoing/" + loopToken, {
           trigger: true
         });
+      }.bind(this), function() {
+        // XXX Not the ideal response, but bug 1047410 will be replacing
+        // this by better "call failed" UI.
+        this._notifier.errorL10n("cannot_start_call_session_not_ready");
+        return;
+      }.bind(this));
+
+      this._websocket.on("progress", this._handleWebSocketProgress, this);
+    },
+
+    /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     */
+    _handleWebSocketProgress: function(progressData) {
+      if (progressData.state === "terminated") {
+        // XXX Before adding more states here, the basic protocol messages to the
+        // server need implementing on both the standalone and desktop side.
+        // These are covered by bug 1045643, but also check the dependencies on
+        // bug 1034041.
+        //
+        // Failure to do this will break desktop - standalone call setup. We're
+        // ok to handle reject, as that is a specific message from the destkop via
+        // the server.
+        switch (progressData.reason) {
+          case "reject":
+            this._handleCallRejected();
+        }
       }
     },
 
     /**
+     * Handles call rejection.
+     * XXX This should really display the call failed view - bug 1046959
+     * will implement this.
+     */
+    _handleCallRejected: function() {
+      this.endCall();
+      this._notifier.errorL10n("call_timeout_notification_text");
+    },
+
+    /**
      * @override {loop.shared.router.BaseConversationRouter.endCall}
      */
     endCall: function() {
       var route = "home";
       if (this._conversation.get("loopToken")) {
         route = "call/" + this._conversation.get("loopToken");
       }
       this.navigate(route, {trigger: true});
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -10,16 +10,17 @@ describe("loop.conversation", function()
   "use strict";
 
   var ConversationRouter = loop.conversation.ConversationRouter,
       sandbox,
       notifier;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    sandbox.useFakeTimers();
     notifier = {
       notify: sandbox.spy(),
       warn: sandbox.spy(),
       warnL10n: sandbox.spy(),
       error: sandbox.spy(),
       errorL10n: sandbox.spy()
     };
 
@@ -114,17 +115,16 @@ describe("loop.conversation", function()
 
     beforeEach(function() {
       client = new loop.Client();
       conversation = new loop.shared.models.ConversationModel({}, {
         sdk: {},
         pendingCallTimeout: 1000,
       });
       sandbox.stub(client, "requestCallsInfo");
-      sandbox.stub(conversation, "setIncomingSessionData");
       sandbox.stub(conversation, "setOutgoingSessionData");
     });
 
     describe("Routes", function() {
       var router;
 
       beforeEach(function() {
         router = new ConversationRouter({
@@ -182,63 +182,135 @@ describe("loop.conversation", function()
             client.requestCallsInfo.callsArgWith(1, "failed");
 
             router.incoming(42);
 
             sinon.assert.calledOnce(notifier.errorL10n);
           });
 
         describe("requestCallsInfo successful", function() {
-          var fakeSessionData;
+          var fakeSessionData, resolvePromise, rejectPromise;
 
           beforeEach(function() {
             fakeSessionData  = {
-              sessionId:    "sessionId",
-              sessionToken: "sessionToken",
-              apiKey:       "apiKey",
-              callType:     "callType"
+              sessionId:      "sessionId",
+              sessionToken:   "sessionToken",
+              apiKey:         "apiKey",
+              callType:       "callType",
+              callId:         "Hello",
+              progressURL:    "http://progress.example.com",
+              websocketToken: 123
             };
 
+            sandbox.stub(router, "_setupWebSocketAndCallView");
+            sandbox.stub(conversation, "setIncomingSessionData");
+
             client.requestCallsInfo.callsArgWith(1, null, [fakeSessionData]);
           });
 
           it("should store the session data", function() {
-            router.incoming(42);
+            router.incoming("fakeVersion");
 
             sinon.assert.calledOnce(conversation.setIncomingSessionData);
             sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
                                            fakeSessionData);
           });
 
-          it("should call the view with video.enabled=false", function() {
-            sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
+          it("should call #_setupWebSocketAndCallView", function() {
+
             router.incoming("fakeVersion");
 
-            sinon.assert.calledOnce(conversation.get);
-            sinon.assert.calledOnce(loop.conversation.IncomingCallView);
-            sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
-                                           {model: conversation,
-                                           video: {enabled: false}});
+            sinon.assert.calledOnce(router._setupWebSocketAndCallView);
+            sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
+          });
+        });
+
+        describe("#_setupWebSocketAndCallView", function() {
+          beforeEach(function() {
+            conversation.setIncomingSessionData({
+              sessionId:      "sessionId",
+              sessionToken:   "sessionToken",
+              apiKey:         "apiKey",
+              callType:       "callType",
+              callId:         "Hello",
+              progressURL:    "http://progress.example.com",
+              websocketToken: 123
+            });
           });
 
-          it("should display the incoming call view", function() {
-            sandbox.stub(conversation, "get").withArgs("callType")
-                                                      .returns("audio-video");
-            router.incoming("fakeVersion");
+          describe("Websocket connection successful", function() {
+            var promise;
+
+            beforeEach(function() {
+              sandbox.stub(loop, "CallConnectionWebSocket").returns({
+                promiseConnect: function() {
+                  promise = new Promise(function(resolve, reject) {
+                    resolve();
+                  });
+                  return promise;
+                }
+              });
+            });
+
+            it("should create a CallConnectionWebSocket", function(done) {
+              router._setupWebSocketAndCallView();
+
+              promise.then(function () {
+                sinon.assert.calledOnce(loop.CallConnectionWebSocket);
+                sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
+                  callId: "Hello",
+                  url: "http://progress.example.com",
+                  // The websocket token is converted to a hex string.
+                  websocketToken: "7b"
+                });
+                done();
+              });
+            });
+
+            it("should create the view with video.enabled=false", function(done) {
+              sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
+
+              router._setupWebSocketAndCallView();
 
-            sinon.assert.calledOnce(loop.conversation.IncomingCallView);
-            sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
-                                           {model: conversation,
-                                           video: {enabled: true}});
-            sinon.assert.calledOnce(router.loadReactComponent);
-            sinon.assert.calledWith(router.loadReactComponent,
-              sinon.match(function(value) {
-                return TestUtils.isDescriptorOfType(value,
-                  loop.conversation.IncomingCallView);
-              }));
+              promise.then(function () {
+                sinon.assert.called(conversation.get);
+                sinon.assert.calledOnce(loop.conversation.IncomingCallView);
+                sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
+                                               {model: conversation,
+                                               video: {enabled: false}});
+                done();
+              });
+            });
+          });
+
+          describe("Websocket connection failed", function() {
+            var promise;
+
+            beforeEach(function() {
+              sandbox.stub(loop, "CallConnectionWebSocket").returns({
+                promiseConnect: function() {
+                  promise = new Promise(function(resolve, reject) {
+                    reject();
+                  });
+                  return promise;
+                }
+              });
+            });
+
+            it("should display an error", function(done) {
+              router._setupWebSocketAndCallView();
+
+              promise.then(function() {
+              }, function () {
+                sinon.assert.calledOnce(router._notifier.errorL10n);
+                sinon.assert.calledWithExactly(router._notifier.errorL10n,
+                  "cannot_start_call_session_not_ready");
+                done();
+              });
+            });
           });
         });
       });
 
       describe("#accept", function() {
         it("should initiate the conversation", function() {
           router.accept();
 
@@ -286,20 +358,24 @@ describe("loop.conversation", function()
             sinon.assert.calledWithExactly(router._notifier.errorL10n,
               "cannot_start_call_session_not_ready");
         });
       });
 
       describe("#decline", function() {
         beforeEach(function() {
           sandbox.stub(window, "close");
+          router._websocket = {
+            decline: sandbox.spy()
+          };
         });
 
         it("should close the window", function() {
           router.decline();
+          sandbox.clock.tick(1);
 
           sinon.assert.calledOnce(window.close);
         });
 
         it("should stop alerting", function() {
           sandbox.stub(navigator.mozLoop, "stopAlerting");
           router.decline();
 
@@ -340,16 +416,23 @@ describe("loop.conversation", function()
         it("should update the conversation window title", function() {
           router.feedback();
 
           expect(document.title).eql("Call ended");
         });
       });
 
       describe("#blocked", function() {
+        beforeEach(function() {
+          router._websocket = {
+            decline: sandbox.spy()
+          };
+          sandbox.stub(window, "close");
+        });
+
         it("should call mozLoop.stopAlerting", function() {
           sandbox.stub(navigator.mozLoop, "stopAlerting");
           router.declineAndBlock();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
 
         it("should call delete call", function() {
@@ -370,19 +453,20 @@ describe("loop.conversation", function()
           });
           router.declineAndBlock();
 
           sinon.assert.calledOnce(log);
           sinon.assert.calledWithExactly(log, fakeError);
         });
 
         it("should close the window", function() {
-          sandbox.stub(window, "close");
           router.declineAndBlock();
 
+          sandbox.clock.tick(1);
+
           sinon.assert.calledOnce(window.close);
         });
       });
     });
 
     describe("Events", function() {
       var router, fakeSessionData;
 
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -32,16 +32,17 @@
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/feedbackApiClient.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/router.js"></script>
   <script src="../../content/shared/js/views.js"></script>
+  <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/js/client.js"></script>
   <script src="../../content/js/desktopRouter.js"></script>
   <script src="../../content/js/conversation.js"></script>
   <script src="../../content/js/panel.js"></script>
 
   <!-- Test scripts -->
   <script src="client_test.js"></script>
   <script src="conversation_test.js"></script>
--- a/browser/components/loop/test/mochitest/browser.ini
+++ b/browser/components/loop/test/mochitest/browser.ini
@@ -1,8 +1,10 @@
 [DEFAULT]
 support-files =
     head.js
+    loop_fxa.sjs
 
+[browser_loop_fxa_server.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_prefs.js]
 [browser_mozLoop_doNotDisturb.js]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/browser_loop_fxa_server.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test the server mocking FxA integration endpoints on the Loop server.
+ */
+
+"use strict";
+
+const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?";
+
+registerCleanupFunction(function* () {
+  yield promiseDeletedOAuthParams(BASE_URL);
+});
+
+add_task(function* required_setup_params() {
+  let params = {
+    client_id: "my_client_id",
+    content_uri: "https://example.com/content/",
+    oauth_uri: "https://example.com/oauth/",
+    profile_uri: "https://example.com/profile/",
+    state: "my_state",
+  };
+  let request = yield promiseOAuthParamsSetup(BASE_URL, params);
+  is(request.status, 200, "Check /setup_params status");
+  request = yield promiseParams();
+  is(request.status, 200, "Check /fxa-oauth/params status");
+  for (let param of Object.keys(params)) {
+    is(request.response[param], params[param], "Check /fxa-oauth/params " + param);
+  }
+});
+
+add_task(function* optional_setup_params() {
+  let params = {
+    action: "signin",
+    client_id: "my_client_id",
+    content_uri: "https://example.com/content/",
+    oauth_uri: "https://example.com/oauth/",
+    profile_uri: "https://example.com/profile/",
+    scope: "profile",
+    state: "my_state",
+  };
+  let request = yield promiseOAuthParamsSetup(BASE_URL, params);
+  is(request.status, 200, "Check /setup_params status");
+  request = yield promiseParams();
+  is(request.status, 200, "Check /fxa-oauth/params status");
+  for (let param of Object.keys(params)) {
+    is(request.response[param], params[param], "Check /fxa-oauth/params " + param);
+  }
+});
+
+add_task(function* delete_setup_params() {
+  yield promiseDeletedOAuthParams(BASE_URL);
+  let request = yield promiseParams();
+  is(Object.keys(request.response).length, 0, "Params should have been deleted");
+});
+
+// Begin /fxa-oauth/token tests
+
+add_task(function* token_request() {
+  let params = {
+    client_id: "my_client_id",
+    content_uri: "https://example.com/content/",
+    oauth_uri: "https://example.com/oauth/",
+    profile_uri: "https://example.com/profile/",
+    state: "my_state",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+  let request = yield promiseToken("my_code", params.state);
+  ise(request.status, 200, "Check token response status");
+  ise(request.response.access_token, "my_code_access_token", "Check access_token");
+  ise(request.response.scopes, "", "Check scopes");
+  ise(request.response.token_type, "bearer", "Check token_type");
+});
+
+add_task(function* token_request_invalid_state() {
+  let params = {
+    client_id: "my_client_id",
+    content_uri: "https://example.com/content/",
+    oauth_uri: "https://example.com/oauth/",
+    profile_uri: "https://example.com/profile/",
+    state: "my_invalid_state",
+  };
+  yield promiseOAuthParamsSetup(BASE_URL, params);
+  let request = yield promiseToken("my_code", "my_state");
+  ise(request.status, 400, "Check token response status");
+  ise(request.response, null, "Check token response body");
+});
+
+
+// Helper methods
+
+function promiseParams() {
+  let deferred = Promise.defer();
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+              createInstance(Ci.nsIXMLHttpRequest);
+  xhr.open("POST", BASE_URL + "/fxa-oauth/params", true);
+  xhr.responseType = "json";
+  xhr.addEventListener("load", () => {
+    info("/fxa-oauth/params response:\n" + JSON.stringify(xhr.response, null, 4));
+    deferred.resolve(xhr);
+  });
+  xhr.addEventListener("error", deferred.reject);
+  xhr.send();
+
+  return deferred.promise;
+}
+
+function promiseToken(code, state) {
+  let deferred = Promise.defer();
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+              createInstance(Ci.nsIXMLHttpRequest);
+  xhr.open("POST", BASE_URL + "/fxa-oauth/token", true);
+  xhr.responseType = "json";
+  xhr.addEventListener("load", () => {
+    info("/fxa-oauth/token response:\n" + JSON.stringify(xhr.response, null, 4));
+    deferred.resolve(xhr);
+  });
+  xhr.addEventListener("error", deferred.reject);
+  let payload = {
+    code: code,
+    state: state,
+  };
+  xhr.send(JSON.stringify(payload, null, 4));
+
+  return deferred.promise;
+}
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -64,8 +64,33 @@ function loadLoopPanel() {
 
   // Turn off animations to make tests quicker.
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.setAttribute("animate", "false");
 
   // Now get the actual API.
   yield promiseGetMozLoopAPI();
 }
+
+function promiseOAuthParamsSetup(baseURL, params) {
+  let deferred = Promise.defer();
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+              createInstance(Ci.nsIXMLHttpRequest);
+  xhr.open("POST", baseURL + "/setup_params", true);
+  xhr.setRequestHeader("X-Params", JSON.stringify(params));
+  xhr.addEventListener("load", () => deferred.resolve(xhr));
+  xhr.addEventListener("error", error => deferred.reject(error));
+  xhr.send();
+
+  return deferred.promise;
+}
+
+function promiseDeletedOAuthParams(baseURL) {
+  let deferred = Promise.defer();
+  let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+              createInstance(Ci.nsIXMLHttpRequest);
+  xhr.open("DELETE", baseURL + "/setup_params", true);
+  xhr.addEventListener("load", () => deferred.resolve(xhr));
+  xhr.addEventListener("error", error => deferred.reject(error));
+  xhr.send();
+
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/loop_fxa.sjs
@@ -0,0 +1,118 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This is a mock server that implements the FxA endpoints on the Loop server.
+ */
+
+"use strict";
+
+const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"];
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * Entry point for HTTP requests.
+ */
+function handleRequest(request, response) {
+  switch (request.queryString) {
+    case "/setup_params":
+      setup_params(request, response);
+      return;
+    case "/fxa-oauth/params":
+      params(request, response);
+      return;
+    case "/fxa-oauth/token":
+      token(request, response);
+      return;
+  }
+  response.setStatusLine(request.httpVersion, 404, "Not Found");
+}
+
+/**
+ * POST /setup_params
+ * DELETE /setup_params
+ *
+ * Test-only endpoint to setup the /fxa-oauth/params response.
+ *
+ * For a POST the X-Params header should contain a JSON object with keys to set for /fxa-oauth/params.
+ * A DELETE request will delete the stored parameters and should be run in a cleanup function to
+ * avoid interfering with subsequen tests.
+ */
+function setup_params(request, response) {
+  response.setHeader("Content-Type", "text/plain", false);
+  if (request.method == "DELETE") {
+    setSharedState("/fxa-oauth/params", "");
+    response.write("Params deleted");
+    return;
+  }
+  let params = JSON.parse(request.getHeader("X-Params"));
+  if (!params) {
+    response.setStatusLine(request.httpVersion, 400, "Bad Request");
+    return;
+  }
+  setSharedState("/fxa-oauth/params", JSON.stringify(params));
+  response.write("Params updated");
+}
+
+/**
+ * POST /fxa-oauth/params endpoint
+ *
+ * Fetch OAuth parameters used to start the OAuth flow in the browser.
+ * Parameters: None
+ * Response: JSON containing an object of oauth parameters.
+ */
+function params(request, response) {
+  if (request.method != "POST") {
+    response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
+    response.setHeader("Allow", "POST", false);
+
+    // Add a button to make a POST request to make this endpoint easier to debug in the browser.
+    response.write("<form method=POST><button type=submit>POST</button></form>");
+    return;
+  }
+
+  let origin = request.scheme + "://" + request.host + ":" + request.port;
+
+  let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
+
+  // Warn if required parameters are missing.
+  for (let paramName of REQUIRED_PARAMS) {
+    if (!(paramName in params)) {
+      dump("Warning: " + paramName + " is a required parameter\n");
+    }
+  }
+
+  // Save the result so we have the effective `state` value.
+  setSharedState("/fxa-oauth/params", JSON.stringify(params));
+  response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+  response.write(JSON.stringify(params, null, 2));
+}
+
+/**
+ * POST /fxa-oauth/token
+ *
+ * Validate the state parameter with the server session state and if it matches, exchange the code
+ * for an OAuth Token.
+ * Parameters: code & state as JSON in the POST body.
+ * Response: JSON containing an object of OAuth token information.
+ */
+function token(request, response) {
+  let params = JSON.parse(getSharedState("/fxa-oauth/params") || "{}");
+  let body = NetUtil.readInputStreamToString(request.bodyInputStream,
+                                             request.bodyInputStream.available());
+  let payload = JSON.parse(body);
+  if (!params.state || params.state !== payload.state) {
+    response.setStatusLine(request.httpVersion, 400, "State mismatch");
+    response.write("State mismatch");
+    return;
+  }
+
+  let tokenData = {
+    access_token: payload.code + "_access_token",
+    scopes: "",
+    token_type: "bearer",
+  };
+  response.setHeader("Content-Type", "application/json; charset=utf-8", false);
+  response.write(JSON.stringify(tokenData, null, 2));
+}
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -31,22 +31,24 @@
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
 
   <!-- App scripts -->
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/router.js"></script>
+  <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../content/shared/js/feedbackApiClient.js"></script>
 
   <!-- Test scripts -->
   <script src="models_test.js"></script>
   <script src="views_test.js"></script>
   <script src="router_test.js"></script>
+  <script src="websocket_test.js"></script>
   <script src="feedbackApiClient_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
     });
   </script>
 </body>
 </html>
--- a/browser/components/loop/test/shared/models_test.js
+++ b/browser/components/loop/test/shared/models_test.js
@@ -17,20 +17,21 @@ describe("loop.shared.models", function(
     sandbox.useFakeTimers();
     fakeXHR = sandbox.useFakeXMLHttpRequest();
     requests = [];
     // https://github.com/cjohansen/Sinon.JS/issues/393
     fakeXHR.xhr.onCreate = function(xhr) {
       requests.push(xhr);
     };
     fakeSessionData = {
-      sessionId:    "sessionId",
-      sessionToken: "sessionToken",
-      apiKey:       "apiKey",
-      callType:     "callType"
+      sessionId:      "sessionId",
+      sessionToken:   "sessionToken",
+      apiKey:         "apiKey",
+      callType:       "callType",
+      websocketToken: 123
     };
     fakeSession = _.extend({
       connect: function () {},
       endSession: sandbox.stub(),
       set: sandbox.stub(),
       disconnect: sandbox.spy(),
       unpublish: sandbox.spy()
     }, Backbone.Events);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -0,0 +1,224 @@
+/* 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/. */
+
+/*global loop, sinon, it, beforeEach, afterEach, describe */
+
+var expect = chai.expect;
+
+describe("loop.CallConnectionWebSocket", function() {
+  "use strict";
+
+  var sandbox,
+      dummySocket;
+
+  beforeEach(function() {
+    sandbox = sinon.sandbox.create();
+    sandbox.useFakeTimers();
+
+    dummySocket = {
+      send: sinon.spy()
+    };
+    sandbox.stub(window, 'WebSocket').returns(dummySocket);
+  });
+
+  afterEach(function() {
+    sandbox.restore();
+  });
+
+  describe("#constructor", function() {
+    it("should require a url option", function() {
+      expect(function() {
+        return new loop.CallConnectionWebSocket();
+      }).to.Throw(/No url/);
+    });
+
+    it("should require a callId setting", function() {
+      expect(function() {
+        return new loop.CallConnectionWebSocket({url: "wss://fake/"});
+      }).to.Throw(/No callId/);
+    });
+
+    it("should require a websocketToken setting", function() {
+      expect(function() {
+        return new loop.CallConnectionWebSocket({
+          url: "http://fake/",
+          callId: "hello"
+        });
+      }).to.Throw(/No websocketToken/);
+    });
+  });
+
+  describe("constructed", function() {
+    var callWebSocket, fakeUrl, fakeCallId, fakeWebSocketToken;
+
+    beforeEach(function() {
+      fakeUrl = "wss://fake/";
+      fakeCallId = "callId";
+      fakeWebSocketToken = "7b";
+
+      callWebSocket = new loop.CallConnectionWebSocket({
+        url: fakeUrl,
+        callId: fakeCallId,
+        websocketToken: fakeWebSocketToken
+      });
+    });
+
+    describe("#promiseConnect", function() {
+      it("should create a new websocket connection", function() {
+        callWebSocket.promiseConnect();
+
+        sinon.assert.calledOnce(window.WebSocket);
+        sinon.assert.calledWithExactly(window.WebSocket, fakeUrl);
+      });
+
+      it("should reject the promise if connection is not completed in " +
+         "5 seconds", function(done) {
+        var promise = callWebSocket.promiseConnect();
+
+        sandbox.clock.tick(5101);
+
+        promise.then(function() {}, function(error) {
+          expect(error).to.be.equal("timeout");
+          done();
+        });
+      });
+
+      it("should reject the promise if the connection errors", function(done) {
+        var promise = callWebSocket.promiseConnect();
+
+        dummySocket.onerror("error");
+
+        promise.then(function() {}, function(error) {
+          expect(error).to.be.equal("error");
+          done();
+        });
+      });
+
+      it("should reject the promise if the connection closes", function(done) {
+        var promise = callWebSocket.promiseConnect();
+
+        dummySocket.onclose("close");
+
+        promise.then(function() {}, function(error) {
+          expect(error).to.be.equal("close");
+          done();
+        });
+      });
+
+      it("should send hello when the socket is opened", function() {
+        callWebSocket.promiseConnect();
+
+        dummySocket.onopen();
+
+        sinon.assert.calledOnce(dummySocket.send);
+        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+          messageType: "hello",
+          callId: fakeCallId,
+          auth: fakeWebSocketToken
+        }));
+      });
+
+      it("should resolve the promise when the 'hello' is received",
+        function(done) {
+          var promise = callWebSocket.promiseConnect();
+
+          dummySocket.onmessage({
+            data: '{"messageType":"hello", "state":"init"}'
+          });
+
+          promise.then(function() {
+            done();
+          });
+        });
+    });
+
+    describe("#decline", function() {
+      it("should send a terminate message to the server", function() {
+        callWebSocket.promiseConnect();
+
+        callWebSocket.decline();
+
+        sinon.assert.calledOnce(dummySocket.send);
+        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+          messageType: "action",
+          event: "terminate",
+          reason: "reject"
+        }));
+      });
+    });
+
+    describe("Events", function() {
+      beforeEach(function() {
+        sandbox.stub(callWebSocket, "trigger");
+
+        callWebSocket.promiseConnect();
+      });
+
+      describe("Progress", function() {
+        it("should trigger a progress event on the callWebSocket", function() {
+          var eventData = {
+            messageType: "progress",
+            state: "terminate",
+            reason: "reject"
+          };
+
+          dummySocket.onmessage({
+            data: JSON.stringify(eventData)
+          });
+
+          sinon.assert.calledOnce(callWebSocket.trigger);
+          sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
+        });
+      });
+
+      describe("Error", function() {
+        // Handled in constructed -> #promiseConnect:
+        //   should reject the promise if the connection errors
+
+        it("should trigger an error if state is not completed", function() {
+          callWebSocket._clearConnectionFlags();
+
+          dummySocket.onerror("Error");
+
+          sinon.assert.calledOnce(callWebSocket.trigger);
+          sinon.assert.calledWithExactly(callWebSocket.trigger,
+                                         "error", "Error");
+        });
+
+        it("should not trigger an error if state is completed", function() {
+          callWebSocket._clearConnectionFlags();
+          callWebSocket._lastServerState = "connected";
+
+          dummySocket.onerror("Error");
+
+          sinon.assert.notCalled(callWebSocket.trigger);
+        });
+      });
+
+      describe("Close", function() {
+        // Handled in constructed -> #promiseConnect:
+        //   should reject the promise if the connection closes
+
+        it("should trigger a close event if state is not completed", function() {
+          callWebSocket._clearConnectionFlags();
+
+          dummySocket.onclose("Error");
+
+          sinon.assert.calledOnce(callWebSocket.trigger);
+          sinon.assert.calledWithExactly(callWebSocket.trigger,
+                                         "closed", "Error");
+        });
+
+        it("should not trigger an error if state is completed", function() {
+          callWebSocket._clearConnectionFlags();
+          callWebSocket._lastServerState = "terminated";
+
+          dummySocket.onclose("Error");
+
+          sinon.assert.notCalled(callWebSocket.trigger);
+        });
+      });
+    });
+  });
+});
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -30,16 +30,17 @@
     chai.Assertion.includeStack = true;
     mocha.setup('bdd');
   </script>
   <!-- App scripts -->
   <script src="../../content/shared/js/utils.js"></script>
   <script src="../../content/shared/js/models.js"></script>
   <script src="../../content/shared/js/views.js"></script>
   <script src="../../content/shared/js/router.js"></script>
+  <script src="../../content/shared/js/websocket.js"></script>
   <script src="../../standalone/content/js/standaloneClient.js"></script>
   <script src="../../standalone/content/js/webapp.js"></script>
  <!-- Test scripts -->
   <script src="standalone_client_test.js"></script>
   <script src="webapp_test.js"></script>
   <script>
     mocha.run(function () {
       $("#mocha").append("<p id='complete'>Complete.</p>");
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -90,40 +90,175 @@ describe("loop.webapp", function() {
         conversation: conversation,
         notifier: notifier
       });
       sandbox.stub(router, "loadView");
       sandbox.stub(router, "navigate");
     });
 
     describe("#startCall", function() {
+      beforeEach(function() {
+        sandbox.stub(router, "_setupWebSocketAndCallView");
+      });
+
       it("should navigate back home if session token is missing", function() {
         router.startCall();
 
         sinon.assert.calledOnce(router.navigate);
         sinon.assert.calledWithMatch(router.navigate, "home");
       });
 
       it("should notify the user if session token is missing", function() {
         router.startCall();
 
         sinon.assert.calledOnce(notifier.errorL10n);
         sinon.assert.calledWithExactly(notifier.errorL10n,
                                        "missing_conversation_info");
       });
 
-      it("should navigate to call/ongoing/:token if session token is available",
-        function() {
-          conversation.set("loopToken", "fake");
+      it("should setup the websocket if session token is available", function() {
+        conversation.set("loopToken", "fake");
+
+        router.startCall();
+
+        sinon.assert.calledOnce(router._setupWebSocketAndCallView);
+        sinon.assert.calledWithExactly(router._setupWebSocketAndCallView, "fake");
+      });
+    });
+
+    describe("#_setupWebSocketAndCallView", function() {
+      beforeEach(function() {
+        conversation.setOutgoingSessionData({
+          sessionId:      "sessionId",
+          sessionToken:   "sessionToken",
+          apiKey:         "apiKey",
+          callId:         "Hello",
+          progressURL:    "http://progress.example.com",
+          websocketToken: 123
+        });
+      });
+
+      describe("Websocket connection successful", function() {
+        var promise;
+
+        beforeEach(function() {
+          sandbox.stub(loop, "CallConnectionWebSocket").returns({
+            promiseConnect: function() {
+              promise = new Promise(function(resolve, reject) {
+                resolve();
+              });
+              return promise;
+            },
+
+            on: sandbox.spy()
+          });
+        });
+
+        it("should create a CallConnectionWebSocket", function(done) {
+          router._setupWebSocketAndCallView("fake");
+
+          promise.then(function () {
+            sinon.assert.calledOnce(loop.CallConnectionWebSocket);
+            sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
+              callId: "Hello",
+              url: "http://progress.example.com",
+              // The websocket token is converted to a hex string.
+              websocketToken: "7b"
+            });
+            done();
+          });
+        });
+
+        it("should navigate to call/ongoing/:token", function(done) {
+          router._setupWebSocketAndCallView("fake");
+
+          promise.then(function () {
+            sinon.assert.calledOnce(router.navigate);
+            sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
+            done();
+          });
+        });
+      });
+
+      describe("Websocket connection failed", function() {
+        var promise;
 
-          router.startCall();
+        beforeEach(function() {
+          sandbox.stub(loop, "CallConnectionWebSocket").returns({
+            promiseConnect: function() {
+              promise = new Promise(function(resolve, reject) {
+                reject();
+              });
+              return promise;
+            },
+
+            on: sandbox.spy()
+          });
+        });
+
+        it("should display an error", function() {
+          router._setupWebSocketAndCallView();
+
+          promise.then(function() {
+          }, function () {
+            sinon.assert.calledOnce(router._notifier.errorL10n);
+            sinon.assert.calledWithExactly(router._notifier.errorL10n,
+              "cannot_start_call_session_not_ready");
+            done();
+          });
+        });
+      });
+
+      describe("Websocket Events", function() {
+        beforeEach(function() {
+          conversation.setOutgoingSessionData({
+            sessionId:      "sessionId",
+            sessionToken:   "sessionToken",
+            apiKey:         "apiKey",
+            callId:         "Hello",
+            progressURL:    "http://progress.example.com",
+            websocketToken: 123
+          });
 
-          sinon.assert.calledOnce(router.navigate);
-          sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
+          sandbox.stub(loop.CallConnectionWebSocket.prototype,
+                       "promiseConnect").returns({
+            then: sandbox.spy()
+          });
+
+          router._setupWebSocketAndCallView();
         });
+
+        describe("Progress", function() {
+          describe("state: terminate, reason: reject", function() {
+            beforeEach(function() {
+              sandbox.stub(router, "endCall");
+            });
+
+            it("should end the call", function() {
+              router._websocket.trigger("progress", {
+                state: "terminated",
+                reason: "reject"
+              });
+
+              sinon.assert.calledOnce(router.endCall);
+            });
+
+            it("should display an error message", function() {
+              router._websocket.trigger("progress", {
+                state: "terminated",
+                reason: "reject"
+              });
+
+              sinon.assert.calledOnce(router._notifier.errorL10n);
+              sinon.assert.calledWithExactly(router._notifier.errorL10n,
+                "call_timeout_notification_text");
+            });
+          });
+        });
+      });
     });
 
     describe("#endCall", function() {
       it("should navigate to home if session token is unset", function() {
         router.endCall();
 
         sinon.assert.calledOnce(router.navigate);
         sinon.assert.calledWithMatch(router.navigate, "home");
@@ -236,30 +371,31 @@ describe("loop.webapp", function() {
       });
     });
 
     describe("Events", function() {
       var fakeSessionData;
 
       beforeEach(function() {
         fakeSessionData = {
-          sessionId:    "sessionId",
-          sessionToken: "sessionToken",
-          apiKey:       "apiKey"
+          sessionId:      "sessionId",
+          sessionToken:   "sessionToken",
+          apiKey:         "apiKey",
+          websocketToken: 123
         };
         conversation.set("loopToken", "fakeToken");
+        sandbox.stub(router, "startCall");
       });
 
-      it("should navigate to call/ongoing/:token once call session is ready",
+      it("should attempt to start the call once call session is ready",
         function() {
           router.setupOutgoingCall();
           conversation.outgoing(fakeSessionData);
 
-          sinon.assert.calledOnce(router.navigate);
-          sinon.assert.calledWith(router.navigate, "call/ongoing/fakeToken");
+          sinon.assert.calledOnce(router.startCall);
         });
 
       it("should navigate to call/{token} when conversation ended", function() {
         conversation.trigger("session:ended");
 
         sinon.assert.calledOnce(router.navigate);
         sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
       });
--- a/browser/components/preferences/in-content/tests/browser_healthreport.js
+++ b/browser/components/preferences/in-content/tests/browser_healthreport.js
@@ -24,17 +24,16 @@ function runPaneTest(fn) {
 function test() {
   waitForExplicitFinish();
   resetPreferences();
   registerCleanupFunction(resetPreferences);
   runPaneTest(testBasic);
 }
 
 function testBasic(win, doc, policy) {
-  is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
   is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
 
   let checkbox = doc.getElementById("submitHealthReportBox");
   ok(checkbox);
   is(checkbox.checked, true, "Health Report checkbox is checked on app first run.");
 
   checkbox.checked = false;
   checkbox.doCommand();
--- a/browser/components/preferences/tests/browser_healthreport.js
+++ b/browser/components/preferences/tests/browser_healthreport.js
@@ -8,29 +8,50 @@ function runPaneTest(fn) {
     Services.obs.removeObserver(observer, "advanced-pane-loaded");
 
     let policy = Components.classes["@mozilla.org/datareporting/service;1"]
                                    .getService(Components.interfaces.nsISupports)
                                    .wrappedJSObject
                                    .policy;
     ok(policy, "Policy object defined");
 
+    resetPreferences();
+
     fn(win, policy);
   }
 
   Services.obs.addObserver(observer, "advanced-pane-loaded", false);
   openDialog("chrome://browser/content/preferences/preferences.xul", "Preferences",
              "chrome,titlebar,toolbar,centerscreen,dialog=no", "paneAdvanced");
 }
 
+let logDetails = {
+  dumpAppender: null,
+  rootLogger: null,
+};
+
 function test() {
   waitForExplicitFinish();
   resetPreferences();
   registerCleanupFunction(resetPreferences);
 
+  let ld = logDetails;
+  registerCleanupFunction(() => {
+    ld.rootLogger.removeAppender(ld.dumpAppender);
+    delete ld.dumpAppender;
+    delete ld.rootLogger;
+  });
+
+  let ns = {};
+  Cu.import("resource://gre/modules/Log.jsm", ns);
+  ld.rootLogger = ns.Log.repository.rootLogger;
+  ld.dumpAppender = new ns.Log.DumpAppender();
+  ld.dumpAppender.level = ns.Log.Level.All;
+  ld.rootLogger.addAppender(ld.dumpAppender);
+
   Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
   runPaneTest(testUploadDisabled);
 }
 
 function testUploadDisabled(win, policy) {
   ok(policy.healthReportUploadLocked, "Upload enabled flag is locked.");
   let checkbox = win.document.getElementById("submitHealthReportBox");
   is(checkbox.getAttribute("disabled"), "true", "Checkbox is disabled if upload setting is locked.");
@@ -38,17 +59,18 @@ function testUploadDisabled(win, policy)
 
   win.close();
   runPaneTest(testBasic);
 }
 
 function testBasic(win, policy) {
   let doc = win.document;
 
-  is(policy.dataSubmissionPolicyAccepted, false, "Data submission policy not accepted.");
+  resetPreferences();
+
   is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
 
   let checkbox = doc.getElementById("submitHealthReportBox");
   ok(checkbox);
   is(checkbox.checked, true, "Health Report checkbox is checked on app first run.");
 
   checkbox.checked = false;
   checkbox.doCommand();
@@ -58,11 +80,15 @@ function testBasic(win, policy) {
   checkbox.doCommand();
   is(policy.healthReportUploadEnabled, true, "Checking checkbox allows FHR upload.");
 
   win.close();
   finish();
 }
 
 function resetPreferences() {
-  Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
+  let service = Cc["@mozilla.org/datareporting/service;1"]
+                  .getService(Ci.nsISupports)
+                  .wrappedJSObject;
+  service.policy._prefs.resetBranch("datareporting.policy.");
+  service.policy.dataSubmissionPolicyBypassNotification = true;
 }
 
--- a/browser/components/search/test/browser_healthreport.js
+++ b/browser/components/search/test/browser_healthreport.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 function test() {
   requestLongerTimeout(2);
   waitForExplicitFinish();
+  resetPreferences();
 
   try {
     let cm = Components.classes["@mozilla.org/categorymanager;1"]
                        .getService(Components.interfaces.nsICategoryManager);
     cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
   } catch (ex) {
     // Health Report disabled, or no SearchesProvider.
     // We need a test or else we'll be marked as failure.
@@ -96,8 +97,15 @@ function test() {
   Services.obs.addObserver(observer, "browser-search-engine-modified", false);
   Services.search.addEngine("http://mochi.test:8888/browser/browser/components/search/test/testEngine.xml",
                             Ci.nsISearchEngine.DATA_XML,
                             "data:image/x-icon,%00",
                             false);
 
 }
 
+function resetPreferences() {
+  let service = Components.classes["@mozilla.org/datareporting/service;1"]
+                                  .getService(Components.interfaces.nsISupports)
+                                  .wrappedJSObject;
+  service.policy._prefs.resetBranch("datareporting.policy.");
+  service.policy.dataSubmissionPolicyBypassNotification = true;
+}
\ No newline at end of file
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -1229,21 +1229,17 @@ SourceScripts.prototype = {
   },
 
   /**
    * Handler for the debugger client's 'blackboxchange' notification.
    */
   _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
     const item = DebuggerView.Sources.getItemByValue(url);
     if (item) {
-      if (isBlackBoxed) {
-        item.prebuiltNode.classList.add("black-boxed");
-      } else {
-        item.prebuiltNode.classList.remove("black-boxed");
-      }
+      item.prebuiltNode.classList.toggle("black-boxed", isBlackBoxed);
     }
     DebuggerView.Sources.updateToolbarButtonsState();
     DebuggerView.maybeShowBlackBoxMessage();
   },
 
   /**
    * Set the black boxed status of the given source.
    *
@@ -1550,51 +1546,54 @@ Tracer.prototype = {
     }
 
     DebuggerView.Tracer.commit();
   },
 
   /**
    * Callback for handling a new call frame.
    */
-  _onCall: function({ name, location, parameterNames, depth, arguments: args }) {
+  _onCall: function({ name, location, blackBoxed, parameterNames, depth, arguments: args }) {
     const item = {
       name: name,
       location: location,
-      id: this._idCounter++
+      id: this._idCounter++,
+      blackBoxed
     };
 
     this._stack.push(item);
     DebuggerView.Tracer.addTrace({
       type: "call",
       name: name,
       location: location,
       depth: depth,
       parameterNames: parameterNames,
       arguments: args,
-      frameId: item.id
+      frameId: item.id,
+      blackBoxed
     });
   },
 
   /**
    * Callback for handling an exited frame.
    */
   _onReturn: function(aPacket) {
     if (!this._stack.length) {
       return;
     }
 
-    const { name, id, location } = this._stack.pop();
+    const { name, id, location, blackBoxed } = this._stack.pop();
     DebuggerView.Tracer.addTrace({
       type: aPacket.why,
       name: name,
       location: location,
       depth: aPacket.depth,
       frameId: id,
-      returnVal: aPacket.return || aPacket.throw || aPacket.yield
+      returnVal: aPacket.return || aPacket.throw || aPacket.yield,
+      blackBoxed
     });
   },
 
   /**
    * Create an object which has the same interface as a normal object client,
    * but since we already have all the information for an object that we will
    * ever get (the server doesn't create actors when tracing, just firehoses
    * data and forgets about it) just return the data immdiately.
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -1432,20 +1432,21 @@ TracerView.prototype = Heritage.extend(W
 
   /**
    * Customization function for creating an item's UI.
    *
    * @return nsIDOMNode
    *         The network request view.
    */
   _createView: function(aTrace) {
-    let { type, name, location, depth, frameId } = aTrace;
+    let { type, name, location, blackBoxed, depth, frameId } = aTrace;
     let { parameterNames, returnVal, arguments: args } = aTrace;
     let fragment = document.createDocumentFragment();
 
+    this._templateItem.classList.toggle("black-boxed", blackBoxed);
     this._templateItem.setAttribute("tooltiptext", SourceUtils.trimUrl(location.url));
     this._templateItem.style.MozPaddingStart = depth + "em";
 
     const TYPES = ["call", "yield", "return", "throw"];
     for (let t of TYPES) {
       this._templateTypeIcon.classList.toggle("trace-" + t, t == type);
     }
     this._templateTypeIcon.setAttribute("value", {
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -257,16 +257,17 @@ skip-if = os == "linux" || e10s # Bug 88
 [browser_dbg_tabactor-02.js]
 [browser_dbg_terminate-on-tab-close.js]
 [browser_dbg_tracing-01.js]
 [browser_dbg_tracing-02.js]
 [browser_dbg_tracing-03.js]
 [browser_dbg_tracing-04.js]
 [browser_dbg_tracing-05.js]
 [browser_dbg_tracing-06.js]
+[browser_dbg_tracing-07.js]
 [browser_dbg_variables-view-01.js]
 [browser_dbg_variables-view-02.js]
 [browser_dbg_variables-view-03.js]
 [browser_dbg_variables-view-04.js]
 [browser_dbg_variables-view-05.js]
 [browser_dbg_variables-view-accessibility.js]
 [browser_dbg_variables-view-data.js]
 [browser_dbg_variables-view-edit-cancel.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_tracing-07.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Execute code both before and after blackboxing and test that we get
+ * appropriately styled traces.
+ */
+
+const TAB_URL = EXAMPLE_URL + "doc_tracing-01.html";
+
+let gTab, gDebuggee, gPanel;
+
+function test() {
+  Task.async(function*() {
+    yield pushPref();
+
+    [gTab, gDebuggee, gPanel] = yield initDebugger(TAB_URL);
+
+    yield startTracing(gPanel);
+    yield clickButton();
+    yield waitForClientEvents(gPanel, "traces");
+
+    /**
+     * Test that there are some traces which are not blackboxed.
+     */
+    const firstBbButton = getBlackBoxButton(gPanel);
+    ok(!firstBbButton.checked, "Should not be black boxed by default");
+
+    const blackBoxedTraces =
+      gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed");
+    ok(blackBoxedTraces.length === 0, "There should no blackboxed traces.");
+
+    const notBlackBoxedTraces =
+      gPanel.panelWin.document.querySelectorAll(".trace-item:not(.black-boxed)");
+    ok(notBlackBoxedTraces.length > 0,
+      "There should be some traces which are not blackboxed.");
+
+    yield toggleBlackBoxing(gPanel);
+    yield clickButton();
+    yield waitForClientEvents(gPanel, "traces");
+
+    /**
+     * Test that there are some traces which are blackboxed.
+     */
+    const secondBbButton = getBlackBoxButton(gPanel);
+    ok(secondBbButton.checked, "The checkbox should no longer be checked.");
+    const traces =
+      gPanel.panelWin.document.querySelectorAll(".trace-item.black-boxed");
+    ok(traces.length > 0, "There should be some blackboxed traces.");
+
+    yield stopTracing(gPanel);
+    yield popPref();
+    yield closeDebuggerAndFinish(gPanel);
+
+    finish();
+  })().catch(e => {
+    ok(false, "Got an error: " + e.message + "\n" + e.stack);
+    finish();
+  });
+}
+
+function clickButton() {
+  EventUtils.sendMouseEvent({ type: "click" },
+                            gDebuggee.document.querySelector("button"),
+                            gDebuggee);
+}
+
+function pushPref() {
+  let deferred = promise.defer();
+  SpecialPowers.pushPrefEnv({'set': [["devtools.debugger.tracer", true]]},
+    deferred.resolve);
+  return deferred.promise;
+}
+
+function popPref() {
+  let deferred = promise.defer();
+  SpecialPowers.popPrefEnv(deferred.resolve);
+  return deferred.promise;
+}
+
+registerCleanupFunction(function() {
+  gTab = null;
+  gDebuggee = null;
+  gPanel = null;
+});
+
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -135,27 +135,28 @@ These should match what Safari and other
 <!ENTITY closeWindow.accesskey "d">
 
 <!ENTITY bookmarksMenu.label "Bookmarks">
 <!ENTITY bookmarksMenu.accesskey "B">
 <!ENTITY bookmarkThisPageCmd.label "Bookmark This Page">
 <!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
 <!ENTITY bookmarkThisPageCmd.commandkey "d">
 <!ENTITY markPageCmd.commandkey "l">
+<!ENTITY findShareServices.label "Find more Share services…">
 <!ENTITY sharePageCmd.label "Share This Page">
 <!ENTITY sharePageCmd.commandkey "S">
 <!ENTITY sharePageCmd.accesskey "s">
 <!ENTITY shareLinkCmd.label "Share This Link">
-<!ENTITY shareLinkCmd.accesskey "s">
+<!ENTITY shareLinkCmd.accesskey "h">
 <!ENTITY shareImageCmd.label "Share This Image">
-<!ENTITY shareImageCmd.accesskey "s">
+<!ENTITY shareImageCmd.accesskey "r">
 <!ENTITY shareSelectCmd.label "Share Selection">
-<!ENTITY shareSelectCmd.accesskey "s">
+<!ENTITY shareSelectCmd.accesskey "r">
 <!ENTITY shareVideoCmd.label "Share This Video">
-<!ENTITY shareVideoCmd.accesskey "s">
+<!ENTITY shareVideoCmd.accesskey "r">
 <!ENTITY feedsMenu.label "Subscribe">
 <!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
 <!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">
 <!ENTITY addCurPagesCmd.label "Bookmark All Tabs…">
 <!ENTITY showAllBookmarks2.label "Show All Bookmarks">
 <!ENTITY unsortedBookmarksCmd.label "Unsorted Bookmarks">
 <!ENTITY bookmarksToolbarChevron.tooltip "Show more bookmarks">
 
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -358,27 +358,28 @@ SocialErrorListener.prototype = {
       }
     }
 
     // Calling cancel() will raise some OnStateChange notifications by itself,
     // so avoid doing that more than once
     if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
       let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
-      provider.errorState = "content-error";
+      if (provider && !provider.errorState)
+        provider.errorState = "content-error";
       this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
                               .chromeEventHandler);
     }
   },
 
   onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
       aRequest.cancel(Components.results.NS_BINDING_ABORTED);
       let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
-      if (!provider.errorState)
+      if (provider && !provider.errorState)
         provider.errorState = "content-error";
       schedule(function() {
         this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
                               .chromeEventHandler);
       }.bind(this));
     }
   },
 
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1308,16 +1308,21 @@ toolbar .toolbarbutton-1 > .toolbarbutto
 
   #PanelUI-fxa-status > .toolbarbutton-icon,
   #PanelUI-quit > .toolbarbutton-icon,
   #PanelUI-customize > .toolbarbutton-icon,
   #PanelUI-help > .toolbarbutton-icon {
     width: 16px;
   }
 
+  #add-share-provider {
+    list-style-image: url(chrome://browser/skin/menuPanel-small@2x.png);
+    -moz-image-region: rect(0px, 192px, 32px, 160px);
+  }
+
   #loop-call-button > .toolbarbutton-badge-container {
     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");
   }
--- a/browser/themes/shared/devtools/debugger.inc.css
+++ b/browser/themes/shared/devtools/debugger.inc.css
@@ -244,16 +244,20 @@
 }
 
 /* Tracer dark theme */
 
 .theme-dark .trace-item {
   color: #f5f7fa; /* Light foreground text */
 }
 
+.theme-dark .trace-item.black-boxed {
+  color: rgba(128,128,128,0.4);
+}
+
 .theme-dark .trace-item.selected-matching {
   background-color: rgba(29,79,115,.4); /* Select highlight blue at 40% alpha */
 }
 
 .theme-dark .selected > .trace-item {
   background-color: rgba(29,79,115,.6); /* Select highlight blue at 60% alpha */
 }
 
@@ -279,16 +283,20 @@
 }
 
 /* Tracer light theme */
 
 .theme-light .trace-item {
   color: #292e33; /* Dark foreground text */
 }
 
+.theme-light .trace-item.black-boxed {
+  color: rgba(128,128,128,0.4);
+}
+
 .theme-light .trace-item.selected-matching {
   background-color: rgba(76,158,217,.4); /* Select highlight blue at 40% alpha */
 }
 
 .theme-light .selected > .trace-item {
   background-color: rgba(76,158,217,.6); /* Select highlight blue at 60% alpha */
 }
 
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -213,8 +213,13 @@ toolbarpaletteitem[place="palette"] > #e
 toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-out-button {
   -moz-image-region: rect(0px, 80px, 16px, 64px);
 }
 
 #zoom-controls@inAnyPanel@ > #zoom-in-button,
 toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
   -moz-image-region: rect(0px, 96px, 16px, 80px);
 }
+
+#add-share-provider {
+  list-style-image: url(chrome://browser/skin/menuPanel-small.png);
+  -moz-image-region: rect(0px, 96px, 16px, 80px);
+}
\ No newline at end of file
--- a/services/datareporting/DataReportingService.js
+++ b/services/datareporting/DataReportingService.js
@@ -170,16 +170,17 @@ DataReportingService.prototype = Object.
             }
 
             // Side effect: instantiates the reporter instance if not already
             // accessed.
             //
             // The instance installs its own shutdown observers. So, we just
             // fire and forget: it will clean itself up.
             let reporter = this.healthReporter;
+            this.policy.ensureUserNotified();
           }.bind(this),
         }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
         break;
 
       case "quit-application":
         this._os.removeObserver(this, "quit-application");
         this._quitting = true;
--- a/services/datareporting/datareporting-prefs.js
+++ b/services/datareporting/datareporting-prefs.js
@@ -1,16 +1,12 @@
 /* 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/. */
 
 pref("datareporting.policy.dataSubmissionEnabled", true);
-pref("datareporting.policy.dataSubmissionPolicyAccepted", false);
-pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", false);
+pref("datareporting.policy.firstRunTime", "0");
 pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "0");
-pref("datareporting.policy.dataSubmissionPolicyResponseType", "");
-pref("datareporting.policy.dataSubmissionPolicyResponseTime", "0");
-pref("datareporting.policy.firstRunTime", "0");
-
+pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 0);
+pref("datareporting.policy.dataSubmissionPolicyBypassNotification", false);
 pref("datareporting.policy.currentPolicyVersion", 2);
 pref("datareporting.policy.minimumPolicyVersion", 1);
 pref("datareporting.policy.minimumPolicyVersion.channel-beta", 2);
-
--- a/services/datareporting/modules-testing/mocks.jsm
+++ b/services/datareporting/modules-testing/mocks.jsm
@@ -21,27 +21,32 @@ this.MockPolicyListener = function MockP
   this.requestRemoteDeleteCount = 0;
   this.lastRemoteDeleteRequest = null;
 
   this.notifyUserCount = 0;
   this.lastNotifyRequest = null;
 }
 
 MockPolicyListener.prototype = {
-  onRequestDataUpload: function onRequestDataUpload(request) {
+  onRequestDataUpload: function (request) {
     this._log.info("onRequestDataUpload invoked.");
     this.requestDataUploadCount++;
     this.lastDataRequest = request;
   },
 
-  onRequestRemoteDelete: function onRequestRemoteDelete(request) {
+  onRequestRemoteDelete: function (request) {
     this._log.info("onRequestRemoteDelete invoked.");
     this.requestRemoteDeleteCount++;
     this.lastRemoteDeleteRequest = request;
   },
 
-  onNotifyDataPolicy: function onNotifyDataPolicy(request) {
-    this._log.info("onNotifyUser invoked.");
+  onNotifyDataPolicy: function (request, rejectMessage=null) {
+    this._log.info("onNotifyDataPolicy invoked.");
     this.notifyUserCount++;
     this.lastNotifyRequest = request;
+    if (rejectMessage) {
+      request.onUserNotifyFailed(rejectMessage);
+    } else {
+      request.onUserNotifyComplete();
+    }
   },
 };
 
--- a/services/datareporting/policy.jsm
+++ b/services/datareporting/policy.jsm
@@ -1,132 +1,88 @@
 /* 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/. */
 
 /**
- * This file is in transition. It was originally conceived to fulfill the
- * needs of only Firefox Health Report. It is slowly being morphed into
- * fulfilling the needs of all data reporting facilities in Gecko applications.
- * As a result, some things feel a bit weird.
- *
- * DataReportingPolicy is both a driver for data reporting notification
- * (a true policy) and the driver for FHR data submission. The latter should
- * eventually be split into its own type and module.
+ * This file is in transition. Most of its content needs to be moved under
+ * /services/healthreport.
  */
 
 #ifndef MERGED_COMPARTMENT
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
   "DataSubmissionRequest", // For test use only.
   "DataReportingPolicy",
+  "DATAREPORTING_POLICY_VERSION",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 #endif
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/UpdateChannel.jsm");
 
+// The current policy version number. If the version number stored in the prefs
+// is smaller than this, data upload will be disabled until the user is re-notified
+// about the policy changes.
+const DATAREPORTING_POLICY_VERSION = 1;
+
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 // Used as a sanity lower bound for dates stored in prefs. This module was
 // implemented in 2012, so any earlier dates indicate an incorrect clock.
 const OLDEST_ALLOWED_YEAR = 2012;
 
 /**
  * Represents a request to display data policy.
  *
- * Instances of this are created when the policy is requesting the user's
- * approval to agree to the data submission policy.
- *
  * Receivers of these instances are expected to call one or more of the on*
  * functions when events occur.
  *
  * When one of these requests is received, the first thing a callee should do
  * is present notification to the user of the data policy. When the notice
  * is displayed to the user, the callee should call `onUserNotifyComplete`.
- * This begins a countdown timer that upon completion will signal implicit
- * acceptance of the policy. If for whatever reason the callee could not
- * display a notice, it should call `onUserNotifyFailed`.
  *
- * Once the user is notified of the policy, the callee has the option of
- * signaling explicit user acceptance or rejection of the policy. They do this
- * by calling `onUserAccept` or `onUserReject`, respectively. These functions
- * are essentially proxies to
- * DataReportingPolicy.{recordUserAcceptance,recordUserRejection}.
- *
- * If the user never explicitly accepts or rejects the policy, it will be
- * implicitly accepted after a specified duration of time. The notice is
- * expected to remain displayed even after implicit acceptance (in case the
- * user is away from the device). So, no event signaling implicit acceptance
- * is exposed.
- *
- * Receivers of instances of this type should treat it as a black box with
- * the exception of the on* functions.
+ * If for whatever reason the callee could not display a notice,
+ * it should call `onUserNotifyFailed`.
  *
  * @param policy
  *        (DataReportingPolicy) The policy instance this request came from.
  * @param deferred
  *        (deferred) The promise that will be fulfilled when display occurs.
  */
 function NotifyPolicyRequest(policy, deferred) {
   this.policy = policy;
   this.deferred = deferred;
 }
-NotifyPolicyRequest.prototype = {
+NotifyPolicyRequest.prototype = Object.freeze({
   /**
    * Called when the user is notified of the policy.
-   *
-   * This starts a countdown timer that will eventually signify implicit
-   * acceptance of the data policy.
    */
-  onUserNotifyComplete: function onUserNotified() {
-    this.deferred.resolve();
-    return this.deferred.promise;
-  },
+  onUserNotifyComplete: function () {
+    return this.deferred.resolve();
+   },
 
   /**
    * Called when there was an error notifying the user about the policy.
    *
    * @param error
    *        (Error) Explains what went wrong.
    */
-  onUserNotifyFailed: function onUserNotifyFailed(error) {
-    this.deferred.reject(error);
-  },
-
-  /**
-   * Called when the user agreed to the data policy.
-   *
-   * @param reason
-   *        (string) How the user agreed to the policy.
-   */
-  onUserAccept: function onUserAccept(reason) {
-    this.policy.recordUserAcceptance(reason);
+  onUserNotifyFailed: function (error) {
+    return this.deferred.reject(error);
   },
-
-  /**
-   * Called when the user rejected the data policy.
-   *
-   * @param reason
-   *        (string) How the user rejected the policy.
-   */
-  onUserReject: function onUserReject(reason) {
-    this.policy.recordUserRejection(reason);
-  },
-};
-
-Object.freeze(NotifyPolicyRequest.prototype);
+});
 
 /**
  * Represents a request to submit data.
  *
  * Instances of this are created when the policy requests data upload or
  * deletion.
  *
  * Receivers are expected to call one of the provided on* functions to signal
@@ -233,19 +189,17 @@ this.DataSubmissionRequest.prototype = O
  *  1. Do not submit data more than once every 24 hours.
  *  2. Try to submit as close to 24 hours apart as possible.
  *  3. Do not submit too soon after application startup so as to not negatively
  *     impact performance at startup.
  *  4. Before first ever data submission, the user should be notified about
  *     data collection practices.
  *  5. User should have opportunity to react to this notification before
  *     data submission.
- *  6. Display of notification without any explicit user action constitutes
- *     implicit consent after a certain duration of time.
- *  7. If data submission fails, try at most 2 additional times before giving
+ *  6. If data submission fails, try at most 2 additional times before giving
  *     up on that day's submission.
  *
  * The listener passed into the instance must have the following properties
  * (which are callbacks that will be invoked at certain key events):
  *
  *   * onRequestDataUpload(request) - Called when the policy is requesting
  *     data to be submitted. The function is passed a `DataSubmissionRequest`.
  *     The listener should call one of the special resolving functions on that
@@ -284,29 +238,21 @@ this.DataReportingPolicy = function (pre
       throw new Error("Passed listener does not contain required handler: " +
                       handler);
     }
   }
 
   this._prefs = prefs;
   this._healthReportPrefs = healthReportPrefs;
   this._listener = listener;
+  this._userNotifyPromise = null;
 
-  // If the policy version has changed, reset all preferences, so that
-  // the notification reappears.
-  let acceptedVersion = this._prefs.get("dataSubmissionPolicyAcceptedVersion");
-  if (typeof(acceptedVersion) == "number" &&
-      acceptedVersion < this.minimumPolicyVersion) {
-    this._log.info("policy version has changed - resetting all prefs");
-    // We don't want to delay the notification in this case.
-    let firstRunToRestore = this.firstRunDate;
-    this._prefs.resetBranch();
-    this.firstRunDate = firstRunToRestore.getTime() ?
-                        firstRunToRestore : this.now();
-  } else if (!this.firstRunDate.getTime()) {
+  this._migratePrefs();
+
+  if (!this.firstRunDate.getTime()) {
     // If we've never run before, record the current time.
     this.firstRunDate = this.now();
   }
 
   // Install an observer so that we can act on changes from external
   // code (such as Android UI).
   // Use a function because this is the only place where the Preferences
   // abstraction is way less usable than nsIPrefBranch.
@@ -324,41 +270,23 @@ this.DataReportingPolicy = function (pre
 
   healthReportPrefs.observe("uploadEnabled", this.uploadEnabledObserver);
 
   // Ensure we are scheduled to submit.
   if (!this.nextDataSubmissionDate.getTime()) {
     this.nextDataSubmissionDate = this._futureDate(MILLISECONDS_PER_DAY);
   }
 
-  // Date at which we performed user notification of acceptance.
-  // This is an instance variable because implicit acceptance should only
-  // carry forward through a single application instance.
-  this._dataSubmissionPolicyNotifiedDate = null;
-
   // Record when we last requested for submitted data to be sent. This is
   // to avoid having multiple outstanding requests.
   this._inProgressSubmissionRequest = null;
 };
 
 this.DataReportingPolicy.prototype = Object.freeze({
   /**
-   * How long after first run we should notify about data submission.
-   */
-  SUBMISSION_NOTIFY_INTERVAL_MSEC: 12 * 60 * 60 * 1000,
-
-  /**
-   * Time that must elapse with no user action for implicit acceptance.
-   *
-   * THERE ARE POTENTIAL LEGAL IMPLICATIONS OF CHANGING THIS VALUE. Check with
-   * Privacy and/or Legal before modifying.
-   */
-  IMPLICIT_ACCEPTANCE_INTERVAL_MSEC: 8 * 60 * 60 * 1000,
-
-  /**
    *  How often to poll to see if we need to do something.
    *
    * The interval needs to be short enough such that short-lived applications
    * have an opportunity to submit data. But, it also needs to be long enough
    * to not negatively impact performance.
    *
    * The random bit is to ensure that other systems scheduling around the same
    * interval don't all get scheduled together.
@@ -388,23 +316,16 @@ this.DataReportingPolicy.prototype = Obj
    * we run out of values in this array, we give up on that day's submission
    * and schedule for a day out.
    */
   FAILURE_BACKOFF_INTERVALS: [
     15 * 60 * 1000,
     60 * 60 * 1000,
   ],
 
-  /**
-   * State of user notification of data submission.
-   */
-  STATE_NOTIFY_UNNOTIFIED: "not-notified",
-  STATE_NOTIFY_WAIT: "waiting",
-  STATE_NOTIFY_COMPLETE: "ok",
-
   REQUIRED_LISTENERS: [
     "onRequestDataUpload",
     "onRequestRemoteDelete",
     "onNotifyDataPolicy",
   ],
 
   /**
    * The first time the health report policy came into existence.
@@ -417,84 +338,34 @@ this.DataReportingPolicy.prototype = Obj
   },
 
   set firstRunDate(value) {
     this._log.debug("Setting first-run date: " + value);
     CommonUtils.setDatePref(this._prefs, "firstRunTime", value,
                             OLDEST_ALLOWED_YEAR);
   },
 
-  /**
-   * Short circuit policy checking and always assume acceptance.
-   *
-   * This shuld never be set by the user. Instead, it is a per-application or
-   * per-deployment default pref.
-   */
-  get dataSubmissionPolicyBypassAcceptance() {
-    return this._prefs.get("dataSubmissionPolicyBypassAcceptance", false);
-  },
-
-  /**
-   * When the user was notified that data submission could occur.
-   *
-   * This is used for logging purposes. this._dataSubmissionPolicyNotifiedDate
-   * is what's used internally.
-   */
   get dataSubmissionPolicyNotifiedDate() {
     return CommonUtils.getDatePref(this._prefs,
                                    "dataSubmissionPolicyNotifiedTime", 0,
                                    this._log, OLDEST_ALLOWED_YEAR);
   },
 
   set dataSubmissionPolicyNotifiedDate(value) {
     this._log.debug("Setting user notified date: " + value);
     CommonUtils.setDatePref(this._prefs, "dataSubmissionPolicyNotifiedTime",
                             value, OLDEST_ALLOWED_YEAR);
   },
 
-  /**
-   * When the user accepted or rejected the data submission policy.
-   *
-   * If there was implicit acceptance, this will be set to the time of that.
-   */
-  get dataSubmissionPolicyResponseDate() {
-    return CommonUtils.getDatePref(this._prefs,
-                                   "dataSubmissionPolicyResponseTime",
-                                   0, this._log, OLDEST_ALLOWED_YEAR);
-  },
-
-  set dataSubmissionPolicyResponseDate(value) {
-    this._log.debug("Setting user notified reaction date: " + value);
-    CommonUtils.setDatePref(this._prefs,
-                            "dataSubmissionPolicyResponseTime",
-                            value, OLDEST_ALLOWED_YEAR);
+  get dataSubmissionPolicyBypassNotification() {
+    return this._prefs.get("dataSubmissionPolicyBypassNotification", false);
   },
 
-  /**
-   * Records the result of user notification of data submission policy.
-   *
-   * This is used for logging and diagnostics purposes. It can answer the
-   * question "how was data submission agreed to on this profile?"
-   *
-   * Not all values are defined by this type and can come from other systems.
-   *
-   * The value must be a string and should be something machine readable. e.g.
-   * "accept-user-clicked-ok-button-in-info-bar"
-   */
-  get dataSubmissionPolicyResponseType() {
-    return this._prefs.get("dataSubmissionPolicyResponseType",
-                           "none-recorded");
-  },
-
-  set dataSubmissionPolicyResponseType(value) {
-    if (typeof(value) != "string") {
-      throw new Error("Value must be a string. Got " + typeof(value));
-    }
-
-    this._prefs.set("dataSubmissionPolicyResponseType", value);
+  set dataSubmissionPolicyBypassNotification(value) {
+    return this._prefs.set("dataSubmissionPolicyBypassNotification", !!value);
   },
 
   /**
    * Whether submission of data is allowed.
    *
    * This is the master switch for remote server communication. If it is
    * false, we never request upload or deletion.
    */
@@ -502,70 +373,47 @@ this.DataReportingPolicy.prototype = Obj
     // Default is true because we are opt-out.
     return this._prefs.get("dataSubmissionEnabled", true);
   },
 
   set dataSubmissionEnabled(value) {
     this._prefs.set("dataSubmissionEnabled", !!value);
   },
 
+  get currentPolicyVersion() {
+    return this._prefs.get("currentPolicyVersion", DATAREPORTING_POLICY_VERSION);
+  },
+
   /**
    * The minimum policy version which for dataSubmissionPolicyAccepted to
    * to be valid.
    */
   get minimumPolicyVersion() {
     // First check if the current channel has an ove
     let channel = UpdateChannel.get(false);
     let channelPref = this._prefs.get("minimumPolicyVersion.channel-" + channel);
     return channelPref !== undefined ?
            channelPref : this._prefs.get("minimumPolicyVersion", 1);
   },
 
-  /**
-   * Whether the user has accepted that data submission can occur.
-   *
-   * This overrides dataSubmissionEnabled.
-   */
-  get dataSubmissionPolicyAccepted() {
-    // Be conservative and default to false.
-    return this._prefs.get("dataSubmissionPolicyAccepted", false);
+  get dataSubmissionPolicyAcceptedVersion() {
+    return this._prefs.get("dataSubmissionPolicyAcceptedVersion", 0);
   },
 
-  set dataSubmissionPolicyAccepted(value) {
-    this._prefs.set("dataSubmissionPolicyAccepted", !!value);
-    if (!!value) {
-      let currentPolicyVersion = this._prefs.get("currentPolicyVersion", 1);
-      this._prefs.set("dataSubmissionPolicyAcceptedVersion", currentPolicyVersion);
-    } else {
-      this._prefs.reset("dataSubmissionPolicyAcceptedVersion");
-    }
+  set dataSubmissionPolicyAcceptedVersion(value) {
+    this._prefs.set("dataSubmissionPolicyAcceptedVersion", value);
   },
 
   /**
-   * The state of user notification of the data policy.
-   *
-   * This must be DataReportingPolicy.STATE_NOTIFY_COMPLETE before data
-   * submission can occur.
-   *
-   * @return DataReportingPolicy.STATE_NOTIFY_* constant.
+   * Checks to see if the user has been notified about data submission
+   * @return {bool}
    */
-  get notifyState() {
-    if (this.dataSubmissionPolicyResponseDate.getTime()) {
-      return this.STATE_NOTIFY_COMPLETE;
-    }
-
-    // We get the local state - not the state from prefs - because we don't want
-    // a value from a previous application run to interfere. This prevents
-    // a scenario where notification occurs just before application shutdown and
-    // notification is displayed for shorter than the policy requires.
-    if (!this._dataSubmissionPolicyNotifiedDate) {
-      return this.STATE_NOTIFY_UNNOTIFIED;
-    }
-
-    return this.STATE_NOTIFY_WAIT;
+  get userNotifiedOfCurrentPolicy() {
+    return  this.dataSubmissionPolicyNotifiedDate.getTime() > 0 &&
+            this.dataSubmissionPolicyAcceptedVersion >= this.currentPolicyVersion;
   },
 
   /**
    * When this policy last requested data submission.
    *
    * This is used mainly for forensics purposes and should have no bearing
    * on scheduling or run-time behavior.
    */
@@ -689,53 +537,16 @@ this.DataReportingPolicy.prototype = Obj
   /**
    * Whether the FHR upload enabled setting is locked and can't be changed.
    */
   get healthReportUploadLocked() {
     return this._healthReportPrefs.locked("uploadEnabled");
   },
 
   /**
-   * Record user acceptance of data submission policy.
-   *
-   * Data submission will not be allowed to occur until this is called.
-   *
-   * This is typically called through the `onUserAccept` property attached to
-   * the promise passed to `onUserNotify` in the policy listener. But, it can
-   * be called through other interfaces at any time and the call will have
-   * an impact on future data submissions.
-   *
-   * @param reason
-   *        (string) How the user accepted the data submission policy.
-   */
-  recordUserAcceptance: function recordUserAcceptance(reason="no-reason") {
-    this._log.info("User accepted data submission policy: " + reason);
-    this.dataSubmissionPolicyResponseDate = this.now();
-    this.dataSubmissionPolicyResponseType = "accepted-" + reason;
-    this.dataSubmissionPolicyAccepted = true;
-  },
-
-  /**
-   * Record user rejection of submission policy.
-   *
-   * Data submission will not be allowed to occur if this is called.
-   *
-   * This is typically called through the `onUserReject` property attached to
-   * the promise passed to `onUserNotify` in the policy listener. But, it can
-   * be called through other interfaces at any time and the call will have an
-   * impact on future data submissions.
-   */
-  recordUserRejection: function recordUserRejection(reason="no-reason") {
-    this._log.info("User rejected data submission policy: " + reason);
-    this.dataSubmissionPolicyResponseDate = this.now();
-    this.dataSubmissionPolicyResponseType = "rejected-" + reason;
-    this.dataSubmissionPolicyAccepted = false;
-  },
-
-  /**
    * Record the user's intent for whether FHR should upload data.
    *
    * This is the preferred way for XUL applications to record a user's
    * preference on whether Firefox Health Report should upload data to
    * a server.
    *
    * If upload is disabled through this API, a request for remote data
    * deletion is initiated automatically.
@@ -877,116 +688,90 @@ this.DataReportingPolicy.prototype = Obj
       return this._dispatchSubmissionRequest("onRequestRemoteDelete", true);
     }
 
     if (!this.healthReportUploadEnabled) {
       this._log.debug("Data upload is disabled. Doing nothing.");
       return;
     }
 
-    // If the user hasn't responded to the data policy, don't do anything.
-    if (!this.ensureNotifyResponse(now)) {
+    if (!this.ensureUserNotified()) {
+      this._log.warn("The user has not been notified about the data submission " +
+                     "policy. Not attempting upload.");
       return;
     }
 
-    // User has opted out of data submission.
-    if (!this.dataSubmissionPolicyAccepted && !this.dataSubmissionPolicyBypassAcceptance) {
-      this._log.debug("Data submission has been disabled per user request.");
-      return;
-    }
-
-    // User has responded to data policy and data submission is enabled. Now
-    // comes the scheduling part.
+    // Data submission is allowed to occur. Now comes the scheduling part.
 
     if (nowT < nextSubmissionDate.getTime()) {
       this._log.debug("Next data submission is scheduled in the future: " +
                      nextSubmissionDate);
       return;
     }
 
     return this._dispatchSubmissionRequest("onRequestDataUpload", false);
   },
 
   /**
-   * Ensure user has responded to data submission policy.
+   * Ensure that the data policy notification has been displayed.
    *
    * This must be called before data submission. If the policy has not been
-   * responded to, data submission must not occur.
+   * displayed, data submission must not occur.
    *
-   * @return bool Whether user has responded to data policy.
+   * @return bool Whether the notification has been displayed.
    */
-  ensureNotifyResponse: function ensureNotifyResponse(now) {
-    if (this.dataSubmissionPolicyBypassAcceptance) {
+  ensureUserNotified: function () {
+    if (this.userNotifiedOfCurrentPolicy || this.dataSubmissionPolicyBypassNotification) {
       return true;
     }
 
-    let notifyState = this.notifyState;
-
-    if (notifyState == this.STATE_NOTIFY_UNNOTIFIED) {
-      let notifyAt = new Date(this.firstRunDate.getTime() +
-                              this.SUBMISSION_NOTIFY_INTERVAL_MSEC);
-
-      if (now.getTime() < notifyAt.getTime()) {
-        this._log.debug("Don't have to notify about data submission yet.");
-        return false;
-      }
-
-      let onComplete = function onComplete() {
-        this._log.info("Data submission notification presented.");
-        let now = this.now();
-
-        this._dataSubmissionPolicyNotifiedDate = now;
-        this.dataSubmissionPolicyNotifiedDate = now;
-      }.bind(this);
-
-      let deferred = Promise.defer();
-
-      deferred.promise.then(onComplete, (error) => {
-        this._log.warn("Data policy notification presentation failed: " +
-                       CommonUtils.exceptionStr(error));
-      });
-
-      this._log.info("Requesting display of data policy.");
-      let request = new NotifyPolicyRequest(this, deferred);
-
-      try {
-        this._listener.onNotifyDataPolicy(request);
-      } catch (ex) {
-        this._log.warn("Exception when calling onNotifyDataPolicy: " +
-                       CommonUtils.exceptionStr(ex));
-      }
+    // The user has not been notified yet, but is in the process of being notified.
+    if (this._userNotifyPromise) {
       return false;
     }
 
-    // We're waiting for user action or implicit acceptance after display.
-    if (notifyState == this.STATE_NOTIFY_WAIT) {
-      // Check for implicit acceptance.
-      let implicitAcceptance =
-        this._dataSubmissionPolicyNotifiedDate.getTime() +
-        this.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC;
+    let deferred = Promise.defer();
+    deferred.promise.then((function onSuccess() {
+      this._recordDataPolicyNotification(this.now(), this.currentPolicyVersion);
+      this._userNotifyPromise = null;
+    }).bind(this), ((error) => {
+      this._log.warn("Data policy notification presentation failed: " +
+                     CommonUtils.exceptionStr(error));
+      this._userNotifyPromise = null;
+    }).bind(this));
 
-      this._log.debug("Now: " + now.getTime());
-      this._log.debug("Will accept: " + implicitAcceptance);
-      if (now.getTime() < implicitAcceptance) {
-        this._log.debug("Still waiting for reaction or implicit acceptance. " +
-                        "Now: " + now.getTime() + " < " +
-                        "Accept: " + implicitAcceptance);
-        return false;
-      }
-
-      this.recordUserAcceptance("implicit-time-elapsed");
-      return true;
+    this._log.info("Requesting display of data policy.");
+    let request = new NotifyPolicyRequest(this, deferred);
+    try {
+      this._listener.onNotifyDataPolicy(request);
+    } catch (ex) {
+      this._log.warn("Exception when calling onNotifyDataPolicy: " +
+                     CommonUtils.exceptionStr(ex));
     }
 
-    // If this happens, we have a coding error in this file.
-    if (notifyState != this.STATE_NOTIFY_COMPLETE) {
-      throw new Error("Unknown notification state: " + notifyState);
-    }
+    this._userNotifyPromise = deferred.promise;
+
+    return false;
+  },
 
-    return true;
+  _recordDataPolicyNotification: function (date, version) {
+    this._log.debug("Recording data policy notification to version " + version +
+                  " on date " + date);
+    this.dataSubmissionPolicyNotifiedDate = date;
+    this.dataSubmissionPolicyAcceptedVersion = version;
+  },
+
+  _migratePrefs: function () {
+    // Current prefs are mostly the same than the old ones, except for some deprecated ones.
+    this._prefs.reset([
+      "dataSubmissionPolicyAccepted",
+      "dataSubmissionPolicyBypassAcceptance",
+      "dataSubmissionPolicyResponseType",
+      "dataSubmissionPolicyResponseTime"
+    ]);
   },
 
   _processInProgressSubmission: function _processInProgressSubmission() {
     if (!this._inProgressSubmissionRequest) {
       return false;
     }
 
     let now = this.now().getTime();
--- a/services/datareporting/tests/xpcshell/test_policy.js
+++ b/services/datareporting/tests/xpcshell/test_policy.js
@@ -4,16 +4,17 @@
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
 Cu.import("resource://gre/modules/UpdateChannel.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 function getPolicy(name,
                    aCurrentPolicyVersion = 1,
                    aMinimumPolicyVersion = 1,
                    aBranchMinimumVersionOverride) {
   let branch = "testing.datareporting." + name;
 
   // The version prefs should not be removed on reset, so set them in the
@@ -32,16 +33,32 @@ function getPolicy(name,
   let healthReportPrefs = new Preferences(branch + ".healthreport.");
 
   let listener = new MockPolicyListener();
   let policy = new DataReportingPolicy(policyPrefs, healthReportPrefs, listener);
 
   return [policy, policyPrefs, healthReportPrefs, listener];
 }
 
+/**
+ * Ensure that the notification has been displayed to the user therefore having
+ * policy.ensureUserNotified() === true, which will allow for a successful
+ * data upload and afterwards does a call to policy.checkStateAndTrigger()
+ * @param  {Policy} policy
+ * @return {Promise}
+ */
+function ensureUserNotifiedAndTrigger(policy) {
+  return Task.spawn(function* ensureUserNotifiedAndTrigger () {
+    policy.ensureUserNotified();
+    yield policy._listener.lastNotifyRequest.deferred.promise;
+    do_check_true(policy.userNotifiedOfCurrentPolicy);
+    policy.checkStateAndTrigger();
+  });
+}
+
 function defineNow(policy, now) {
   print("Adjusting fake system clock to " + now);
   Object.defineProperty(policy, "now", {
     value: function customNow() {
       return now;
     },
     writable: true,
   });
@@ -61,54 +78,49 @@ add_test(function test_constructor() {
   };
 
   let policy = new DataReportingPolicy(policyPrefs, hrPrefs, listener);
   do_check_true(Date.now() - policy.firstRunDate.getTime() < 1000);
 
   let tomorrow = Date.now() + 24 * 60 * 60 * 1000;
   do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000);
 
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
+  do_check_eq(policy.dataSubmissionPolicyAcceptedVersion, 0);
+  do_check_false(policy.userNotifiedOfCurrentPolicy);
 
   run_next_test();
 });
 
 add_test(function test_prefs() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("prefs");
 
   let now = new Date();
   let nowT = now.getTime();
 
   policy.firstRunDate = now;
   do_check_eq(policyPrefs.get("firstRunTime"), nowT);
   do_check_eq(policy.firstRunDate.getTime(), nowT);
 
-  policy.dataSubmissionPolicyNotifiedDate= now;
+  policy.dataSubmissionPolicyNotifiedDate = now;
   do_check_eq(policyPrefs.get("dataSubmissionPolicyNotifiedTime"), nowT);
+  do_check_neq(policy.dataSubmissionPolicyNotifiedDate, null);
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), nowT);
 
-  policy.dataSubmissionPolicyResponseDate = now;
-  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseTime"), nowT);
-  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), nowT);
-
-  policy.dataSubmissionPolicyResponseType = "type-1";
-  do_check_eq(policyPrefs.get("dataSubmissionPolicyResponseType"), "type-1");
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "type-1");
-
   policy.dataSubmissionEnabled = false;
   do_check_false(policyPrefs.get("dataSubmissionEnabled", true));
   do_check_false(policy.dataSubmissionEnabled);
 
-  policy.dataSubmissionPolicyAccepted = false;
-  do_check_false(policyPrefs.get("dataSubmissionPolicyAccepted", true));
-  do_check_false(policy.dataSubmissionPolicyAccepted);
+  let new_version = DATAREPORTING_POLICY_VERSION + 1;
+  policy.dataSubmissionPolicyAcceptedVersion = new_version;
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), new_version);
 
-  do_check_false(policy.dataSubmissionPolicyBypassAcceptance);
-  policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
-  do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
+  do_check_false(policy.dataSubmissionPolicyBypassNotification);
+  policy.dataSubmissionPolicyBypassNotification = true;
+  do_check_true(policy.dataSubmissionPolicyBypassNotification);
+  do_check_true(policyPrefs.get("dataSubmissionPolicyBypassNotification"));
 
   policy.lastDataSubmissionRequestedDate = now;
   do_check_eq(hrPrefs.get("lastDataSubmissionRequestedTime"), nowT);
   do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), nowT);
 
   policy.lastDataSubmissionSuccessfulDate = now;
   do_check_eq(hrPrefs.get("lastDataSubmissionSuccessfulTime"), nowT);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), nowT);
@@ -137,261 +149,172 @@ add_test(function test_prefs() {
   hrPrefs.lock("uploadEnabled");
   do_check_true(policy.healthReportUploadLocked);
   hrPrefs.unlock("uploadEnabled");
   do_check_false(policy.healthReportUploadLocked);
 
   run_next_test();
 });
 
-add_test(function test_notify_state_prefs() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notify_state_prefs");
-
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
+add_task(function test_migratePrefs () {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("migratePrefs");
+  let outdated_prefs = {
+    dataSubmissionPolicyAccepted: true,
+    dataSubmissionPolicyBypassAcceptance: true,
+    dataSubmissionPolicyResponseType: "something",
+    dataSubmissionPolicyResponseTime: Date.now() + "",
+  };
 
-  policy._dataSubmissionPolicyNotifiedDate = new Date();
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
-
-  policy.dataSubmissionPolicyResponseDate = new Date();
-  policy._dataSubmissionPolicyNotifiedDate = null;
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
-
-  run_next_test();
+  // Test removal of old prefs.
+  for (let name in outdated_prefs) {
+    policyPrefs.set(name, outdated_prefs[name]);
+  }
+  policy._migratePrefs();
+  for (let name in outdated_prefs) {
+    do_check_false(policyPrefs.has(name));
+  }
 });
 
-add_task(function test_initial_submission_notification() {
+add_task(function test_userNotifiedOfCurrentPolicy () {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("initial_submission_notification");
 
-  do_check_eq(listener.notifyUserCount, 0);
-
-  // Fresh instances should not do anything initially.
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.notifyUserCount, 0);
-
-  // We still shouldn't notify up to the millisecond before the barrier.
-  defineNow(policy, new Date(policy.firstRunDate.getTime() +
-                             policy.SUBMISSION_NOTIFY_INTERVAL_MSEC - 1));
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.notifyUserCount, 0);
-  do_check_null(policy._dataSubmissionPolicyNotifiedDate);
+  do_check_false(policy.userNotifiedOfCurrentPolicy,
+                 "The initial state should be unnotified.");
   do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
 
-  // We have crossed the threshold. We should see notification.
-  defineNow(policy, new Date(policy.firstRunDate.getTime() +
-                             policy.SUBMISSION_NOTIFY_INTERVAL_MSEC));
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.notifyUserCount, 1);
-  yield listener.lastNotifyRequest.onUserNotifyComplete();
-  do_check_true(policy._dataSubmissionPolicyNotifiedDate instanceof Date);
-  do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
-  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(),
-              policy._dataSubmissionPolicyNotifiedDate.getTime());
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
+  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION;
+  do_check_false(policy.userNotifiedOfCurrentPolicy,
+                 "The default state of the date should have a time of 0 and it should therefore fail");
+  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0,
+              "Updating the accepted version should not set a notified date.");
+
+  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
+  do_check_true(policy.userNotifiedOfCurrentPolicy,
+                "Using the proper API causes user notification to report as true.");
+
+  // It is assumed that later versions of the policy will incorporate previous
+  // ones, therefore this should also return true.
+  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
+  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION + 1;
+  do_check_true(policy.userNotifiedOfCurrentPolicy, 'A future version of the policy should pass.');
+
+  policy._recordDataPolicyNotification(new Date(), DATAREPORTING_POLICY_VERSION);
+  policy.dataSubmissionPolicyAcceptedVersion = DATAREPORTING_POLICY_VERSION - 1;
+  do_check_false(policy.userNotifiedOfCurrentPolicy, 'A previous version of the policy should fail.');
 });
 
-add_test(function test_bypass_acceptance() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("bypass_acceptance");
-
-  policyPrefs.set("dataSubmissionPolicyBypassAcceptance", true);
-  do_check_false(policy.dataSubmissionPolicyAccepted);
-  do_check_true(policy.dataSubmissionPolicyBypassAcceptance);
-  defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime()));
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.requestDataUploadCount, 1);
-
-  run_next_test();
-});
+add_task(function* test_notification_displayed () {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accept_displayed");
 
-add_task(function test_notification_implicit_acceptance() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_implicit_acceptance");
+  do_check_eq(listener.requestDataUploadCount, 0);
+  do_check_eq(listener.notifyUserCount, 0);
+  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
 
-  let now = new Date(policy.nextDataSubmissionDate.getTime() -
-                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
-  defineNow(policy, now);
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.notifyUserCount, 1);
-  yield listener.lastNotifyRequest.onUserNotifyComplete();
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
-
-  do_check_true(5000 < policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC);
-  defineNow(policy, new Date(now.getTime() + 5000));
+  // Uploads will trigger user notifications as needed.
   policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 1);
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
-  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), 0);
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "none-recorded");
+  do_check_eq(listener.requestDataUploadCount, 0);
 
-  defineNow(policy, new Date(now.getTime() + policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + 1));
-  policy.checkStateAndTrigger();
-  do_check_eq(listener.notifyUserCount, 1);
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
-  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), policy.now().getTime());
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed");
-});
+  yield ensureUserNotifiedAndTrigger(policy);
 
-add_task(function test_notification_rejected() {
-  // User notification failed. We should not record it as being presented.
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_failed");
-
-  let now = new Date(policy.nextDataSubmissionDate.getTime() -
-                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
-  defineNow(policy, now);
-  policy.checkStateAndTrigger();
   do_check_eq(listener.notifyUserCount, 1);
-  yield listener.lastNotifyRequest.onUserNotifyFailed(new Error("testing failed."));
-  do_check_null(policy._dataSubmissionPolicyNotifiedDate);
-  do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED);
+  do_check_true(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0);
+  do_check_true(policy.userNotifiedOfCurrentPolicy);
 });
 
-add_task(function test_notification_accepted() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_accepted");
-
-  let now = new Date(policy.nextDataSubmissionDate.getTime() -
-                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
-  defineNow(policy, now);
-  policy.checkStateAndTrigger();
-  yield listener.lastNotifyRequest.onUserNotifyComplete();
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
-  do_check_false(policy.dataSubmissionPolicyAccepted);
-  listener.lastNotifyRequest.onUserNotifyComplete();
-  listener.lastNotifyRequest.onUserAccept("foo-bar");
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-foo-bar");
-  do_check_true(policy.dataSubmissionPolicyAccepted);
-  do_check_eq(policy.dataSubmissionPolicyResponseDate.getTime(), now.getTime());
-});
-
-add_task(function test_notification_rejected() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("notification_rejected");
-
-  let now = new Date(policy.nextDataSubmissionDate.getTime() -
-                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC + 1);
-  defineNow(policy, now);
-  policy.checkStateAndTrigger();
-  yield listener.lastNotifyRequest.onUserNotifyComplete();
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_WAIT);
-  do_check_false(policy.dataSubmissionPolicyAccepted);
-  listener.lastNotifyRequest.onUserReject();
-  do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE);
-  do_check_eq(policy.dataSubmissionPolicyResponseType, "rejected-no-reason");
-  do_check_false(policy.dataSubmissionPolicyAccepted);
-
-  // No requests for submission should occur if user has rejected.
-  defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000));
+add_task(function* test_submission_kill_switch() {
+  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
+  policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
-});
-
-add_test(function test_submission_kill_switch() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_kill_switch");
-
-  policy.firstRunDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000);
-  policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.recordUserAcceptance("accept-old-ack");
-  do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
 
   defineNow(policy,
     new Date(Date.now() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC + 100));
   policy.dataSubmissionEnabled = false;
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
-
-  run_next_test();
 });
 
-add_test(function test_upload_kill_switch() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
+add_task(function* test_upload_kill_switch() {
+   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("upload_kill_switch");
 
-  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
-  policy.recordUserAcceptance();
+  yield ensureUserNotifiedAndTrigger(policy);
   defineNow(policy, policy.nextDataSubmissionDate);
 
   // So that we don't trigger deletions, which cause uploads to be delayed.
   hrPrefs.ignore("uploadEnabled", policy.uploadEnabledObserver);
 
   policy.healthReportUploadEnabled = false;
-  policy.checkStateAndTrigger();
+  yield policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
   policy.healthReportUploadEnabled = true;
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
-
-  run_next_test();
 });
 
-add_test(function test_data_submission_no_data() {
+add_task(function* test_data_submission_no_data() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_no_data");
 
-  policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.dataSubmissionPolicyAccepted = true;
   let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
   defineNow(policy, now);
   do_check_eq(listener.requestDataUploadCount, 0);
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   listener.lastDataRequest.onNoDataAvailable();
 
   // The next trigger should try again.
   defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
+ });
 
-  run_next_test();
-});
-
-add_task(function test_data_submission_submit_failure_hard() {
+add_task(function* test_data_submission_submit_failure_hard() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_submit_failure_hard");
 
-  policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime() + 1);
   defineNow(policy, now);
 
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   yield listener.lastDataRequest.onSubmissionFailureHard();
   do_check_eq(listener.lastDataRequest.state,
               listener.lastDataRequest.SUBMISSION_FAILURE_HARD);
 
   let expected = new Date(now.getTime() + 24 * 60 * 60 * 1000);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), expected.getTime());
 
   defineNow(policy, new Date(now.getTime() + 10));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 });
 
-add_task(function test_data_submission_submit_try_again() {
+add_task(function* test_data_submission_submit_try_again() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("data_submission_failure_soft");
 
-  policy.recordUserAcceptance();
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   yield listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               nextDataSubmissionDate.getTime() + 15 * 60 * 1000);
 });
 
-add_task(function test_submission_daily_scheduling() {
+add_task(function* test_submission_daily_scheduling() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_daily_scheduling");
 
-  policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmissionDate = policy.nextDataSubmissionDate;
 
   // Skip ahead to next submission date. We should get a submission request.
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime());
 
   let finishedDate = new Date(now.getTime() + 250);
   defineNow(policy, new Date(finishedDate.getTime() + 50));
   yield listener.lastDataRequest.onSubmissionSuccess(finishedDate);
   do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime());
 
@@ -409,51 +332,46 @@ add_task(function test_submission_daily_
   defineNow(policy, nextScheduled);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
   yield listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200));
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
     new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime());
 });
 
-add_test(function test_submission_far_future_scheduling() {
+add_task(function* test_submission_far_future_scheduling() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_far_future_scheduling");
 
   let now = new Date(Date.now() - 24 * 60 * 60 * 1000);
   defineNow(policy, now);
-  policy.recordUserAcceptance();
-  now = new Date();
-  defineNow(policy, now);
+  yield ensureUserNotifiedAndTrigger(policy);
 
   let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1);
   policy.nextDataSubmissionDate = nextDate;
   policy.checkStateAndTrigger();
+  do_check_true(policy.dataSubmissionPolicyAcceptedVersion >= DATAREPORTING_POLICY_VERSION);
   do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime());
 
   policy.nextDataSubmissionDate = new Date(nextDate.getTime() + 1);
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               policy._futureDate(24 * 60 * 60 * 1000).getTime());
-
-  run_next_test();
 });
 
-add_task(function test_submission_backoff() {
+add_task(function* test_submission_backoff() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_backoff");
 
   do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2);
 
-  policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.dataSubmissionPolicyAccepted = true;
 
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(policy.currentDaySubmissionFailureCount, 0);
 
   now = new Date(now.getTime() + 5000);
   defineNow(policy, now);
 
   // On first soft failure we should back off by scheduled interval.
   yield listener.lastDataRequest.onSubmissionFailureSoft();
@@ -494,40 +412,36 @@ add_task(function test_submission_backof
   // On 3rd failure we should back off by a whole day.
   yield listener.lastDataRequest.onSubmissionFailureSoft();
   do_check_eq(policy.currentDaySubmissionFailureCount, 0);
   do_check_eq(policy.nextDataSubmissionDate.getTime(),
               new Date(now.getTime() + 24 * 60 * 60 * 1000).getTime());
 });
 
 // Ensure that only one submission request can be active at a time.
-add_test(function test_submission_expiring() {
+add_task(function* test_submission_expiring() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("submission_expiring");
 
-  policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
-  policy.dataSubmissionPolicyAccepted = true;
   let nextDataSubmission = policy.nextDataSubmissionDate;
   let now = new Date(policy.nextDataSubmissionDate.getTime());
   defineNow(policy, now);
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   defineNow(policy, new Date(now.getTime() + 500));
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
 
   defineNow(policy, new Date(policy.now().getTime() +
                              policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC));
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 2);
-
-  run_next_test();
 });
 
-add_task(function test_delete_remote_data() {
+add_task(function* test_delete_remote_data() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data");
 
   do_check_false(policy.pendingDeleteRemoteData);
   let nextSubmissionDate = policy.nextDataSubmissionDate;
 
   let now = new Date();
   defineNow(policy, now);
 
@@ -541,43 +455,37 @@ add_task(function test_delete_remote_dat
   do_check_true(listener.lastRemoteDeleteRequest.isDelete);
   defineNow(policy, policy._futureDate(1000));
 
   yield listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now());
   do_check_false(policy.pendingDeleteRemoteData);
 });
 
 // Ensure that deletion requests take priority over regular data submission.
-add_test(function test_delete_remote_data_priority() {
+add_task(function* test_delete_remote_data_priority() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_priority");
 
   let now = new Date();
-  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
-  policy.recordUserAcceptance();
   defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000));
 
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   policy._inProgressSubmissionRequest = null;
 
   policy.deleteRemoteData();
   policy.checkStateAndTrigger();
 
   do_check_eq(listener.requestRemoteDeleteCount, 1);
   do_check_eq(listener.requestDataUploadCount, 1);
-
-  run_next_test();
 });
 
 add_test(function test_delete_remote_data_backoff() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_backoff");
 
   let now = new Date();
-  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
-  policy.recordUserAcceptance();
   defineNow(policy, now);
   policy.nextDataSubmissionDate = now;
   policy.deleteRemoteData();
 
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestRemoteDeleteCount, 1);
   defineNow(policy, policy._futureDate(1000));
   policy.checkStateAndTrigger();
@@ -595,25 +503,22 @@ add_test(function test_delete_remote_dat
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestRemoteDeleteCount, 2);
 
   run_next_test();
 });
 
 // If we request delete while an upload is in progress, delete should be
 // scheduled immediately after upload.
-add_task(function test_delete_remote_data_in_progress_upload() {
+add_task(function* test_delete_remote_data_in_progress_upload() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("delete_remote_data_in_progress_upload");
 
-  let now = new Date();
-  defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
-  policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
 
-  policy.checkStateAndTrigger();
+  yield ensureUserNotifiedAndTrigger(policy);
   do_check_eq(listener.requestDataUploadCount, 1);
   defineNow(policy, policy._futureDate(50 * 1000));
 
   // If we request a delete during a pending request, nothing should be done.
   policy.deleteRemoteData();
   policy.checkStateAndTrigger();
   do_check_eq(listener.requestDataUploadCount, 1);
   do_check_eq(listener.requestRemoteDeleteCount, 0);
@@ -649,17 +554,16 @@ add_test(function test_polling() {
 
       print("Polled at " + now + " after " + after + "ms, intended " + intended);
       do_check_true(after >= acceptable);
       DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
 
       if (count >= 2) {
         policy.stopPolling();
 
-        do_check_eq(listener.notifyUserCount, 0);
         do_check_eq(listener.requestDataUploadCount, 0);
 
         run_next_test();
       }
 
       // "Specified timer period will be at least the time between when
       // processing for last firing the callback completes and when the next
       // firing occurs."
@@ -667,89 +571,17 @@ add_test(function test_polling() {
       // That means we should set 'then' at the *end* of our handler, not
       // earlier.
       then = Date.now();
     }
   });
   policy.startPolling();
 });
 
-// Ensure that implicit acceptance of policy is resolved through polling.
-//
-// This is probably covered by other tests. But, it's best to have explicit
-// coverage from a higher-level.
-add_test(function test_polling_implicit_acceptance() {
-  let [policy, policyPrefs, hrPrefs, listener] = getPolicy("polling_implicit_acceptance");
-
-  // Redefine intervals with shorter, test-friendly values.
-  Object.defineProperty(policy, "POLL_INTERVAL_MSEC", {
-    value: 250,
-  });
-
-  Object.defineProperty(policy, "IMPLICIT_ACCEPTANCE_INTERVAL_MSEC", {
-    value: 700,
-  });
-
-  let count = 0;
-
-  // Track JS elapsed time, so we can decide if we've waited for enough ticks.
-  let start;
-  Object.defineProperty(policy, "checkStateAndTrigger", {
-    value: function CheckStateAndTriggerProxy() {
-      count++;
-      let now = Date.now();
-      let delta = now - start;
-      print("checkStateAndTrigger count: " + count + ", now " + now +
-            ", delta " + delta);
-
-      // Account for some slack.
-      DataReportingPolicy.prototype.checkStateAndTrigger.call(policy);
-
-      // What should happen on different invocations:
-      //
-      //   1) We are inside the prompt interval so user gets prompted.
-      //   2) still ~300ms away from implicit acceptance
-      //   3) still ~50ms away from implicit acceptance
-      //   4) Implicit acceptance recorded. Data submission requested.
-      //   5) Request still pending. No new submission requested.
-      //
-      // Note that, due to the inaccuracy of timers, 4 might not happen until 5
-      // firings have occurred. Yay. So we watch times, not just counts.
-
-      do_check_eq(listener.notifyUserCount, 1);
-
-      if (count == 1) {
-        listener.lastNotifyRequest.onUserNotifyComplete();
-      }
-
-      if (delta <= (policy.IMPLICIT_ACCEPTANCE_INTERVAL_MSEC + policy.POLL_INTERVAL_MSEC)) {
-        do_check_false(policy.dataSubmissionPolicyAccepted);
-        do_check_eq(listener.requestDataUploadCount, 0);
-      } else if (count > 3) {
-        do_check_true(policy.dataSubmissionPolicyAccepted);
-        do_check_eq(policy.dataSubmissionPolicyResponseType,
-                    "accepted-implicit-time-elapsed");
-        do_check_eq(listener.requestDataUploadCount, 1);
-      }
-
-      if ((count > 4) && policy.dataSubmissionPolicyAccepted) {
-        do_check_eq(listener.requestDataUploadCount, 1);
-        policy.stopPolling();
-        run_next_test();
-      }
-    }
-  });
-
-  policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000);
-  policy.nextDataSubmissionDate = new Date(Date.now());
-  start = Date.now();
-  policy.startPolling();
-});
-
-add_task(function test_record_health_report_upload_enabled() {
+add_task(function* test_record_health_report_upload_enabled() {
   let [policy, policyPrefs, hrPrefs, listener] = getPolicy("record_health_report_upload_enabled");
 
   // Preconditions.
   do_check_false(policy.pendingDeleteRemoteData);
   do_check_true(policy.healthReportUploadEnabled);
   do_check_eq(listener.requestRemoteDeleteCount, 0);
 
   // User intent to disable should immediately result in a pending
@@ -786,78 +618,72 @@ add_test(function test_pref_change_initi
       do_check_false(policy.pendingDeleteRemoteData);     // Just called.
 
       run_next_test();
     },
   });
 
   hrPrefs.set("uploadEnabled", false);
 });
- 
+
 add_task(function* test_policy_version() {
   let policy, policyPrefs, hrPrefs, listener, now, firstRunTime;
-  function createPolicy(shouldBeAccepted = false,
+  function createPolicy(shouldBeNotified = false,
                         currentPolicyVersion = 1, minimumPolicyVersion = 1,
                         branchMinimumVersionOverride) {
     [policy, policyPrefs, hrPrefs, listener] =
       getPolicy("policy_version_test", currentPolicyVersion,
                 minimumPolicyVersion, branchMinimumVersionOverride);
     let firstRun = now === undefined;
     if (firstRun) {
       firstRunTime = policy.firstRunDate.getTime();
       do_check_true(firstRunTime > 0);
-      now = new Date(policy.firstRunDate.getTime() +
-                     policy.SUBMISSION_NOTIFY_INTERVAL_MSEC);
+      now = new Date(policy.firstRunDate.getTime());
     }
     else {
       // The first-run time should not be reset even after policy-version
       // upgrades.
       do_check_eq(policy.firstRunDate.getTime(), firstRunTime);
     }
     defineNow(policy, now);
-    do_check_eq(policy.dataSubmissionPolicyAccepted, shouldBeAccepted);
+    do_check_eq(policy.userNotifiedOfCurrentPolicy, shouldBeNotified);
   }
 
-  function* triggerPolicyCheckAndEnsureNotified(notified = true, accept = true) {
+  function* triggerPolicyCheckAndEnsureNotified(notified = true) {
     policy.checkStateAndTrigger();
     do_check_eq(listener.notifyUserCount, Number(notified));
     if (notified) {
-      yield listener.lastNotifyRequest.onUserNotifyComplete();
-      if (accept) {
-        listener.lastNotifyRequest.onUserAccept("because,");
-        do_check_true(policy.dataSubmissionPolicyAccepted);
-        do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
-                    policyPrefs.get("currentPolicyVersion"));
-      }
-      else {
-        do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
-      }
+      policy.ensureUserNotified();
+      yield listener.lastNotifyRequest.deferred.promise;
+      do_check_true(policy.userNotifiedOfCurrentPolicy);
+      do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
+                  policyPrefs.get("currentPolicyVersion"));
     }
   }
 
   createPolicy();
   yield triggerPolicyCheckAndEnsureNotified();
 
   // We shouldn't be notified again if the current version is still valid;
   createPolicy(true);
   yield triggerPolicyCheckAndEnsureNotified(false);
 
   // Just increasing the current version isn't enough. The minimum
   // version must be changed.
   let currentPolicyVersion = policyPrefs.get("currentPolicyVersion");
   let minimumPolicyVersion = policyPrefs.get("minimumPolicyVersion");
-  createPolicy(true, ++currentPolicyVersion, minimumPolicyVersion);
-  yield triggerPolicyCheckAndEnsureNotified(false);
-  do_check_true(policy.dataSubmissionPolicyAccepted);
-  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"),
-              minimumPolicyVersion);
+  createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion);
+  yield triggerPolicyCheckAndEnsureNotified(true);
+  do_check_eq(policyPrefs.get("dataSubmissionPolicyAcceptedVersion"), currentPolicyVersion);
 
   // Increase the minimum policy version and check if we're notified.
-  createPolicy(false, currentPolicyVersion, ++minimumPolicyVersion);
-  do_check_false(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
-  yield triggerPolicyCheckAndEnsureNotified();
+
+  createPolicy(true, currentPolicyVersion, ++minimumPolicyVersion);
+  do_check_true(policyPrefs.has("dataSubmissionPolicyAcceptedVersion"));
+  yield triggerPolicyCheckAndEnsureNotified(false);
+
 
   // Test increasing the minimum version just on the current channel.
   createPolicy(true, currentPolicyVersion, minimumPolicyVersion);
   yield triggerPolicyCheckAndEnsureNotified(false);
   createPolicy(false, ++currentPolicyVersion, minimumPolicyVersion, minimumPolicyVersion + 1);
   yield triggerPolicyCheckAndEnsureNotified(true);
 });
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -1255,18 +1255,18 @@ this.HealthReporter.prototype = Object.f
 
     this._prefs.set("documentServerNamespace", value);
   },
 
   /**
    * Whether this instance will upload data to a server.
    */
   get willUploadData() {
-    return this._policy.dataSubmissionPolicyAccepted &&
-           this._policy.healthReportUploadEnabled;
+    return  this._policy.userNotifiedOfCurrentPolicy &&
+            this._policy.healthReportUploadEnabled;
   },
 
   /**
    * Whether remote data is currently stored.
    *
    * @return bool
    */
   haveRemoteData: function () {
@@ -1316,32 +1316,32 @@ this.HealthReporter.prototype = Object.f
 
   /**
    * Override default handler to incur an upload describing the error.
    */
   _onInitError: function (error) {
     // Need to capture this before we call the parent else it's always
     // set.
     let inShutdown = this._shutdownRequested;
+    let result;
 
-    let result;
     try {
       result = AbstractHealthReporter.prototype._onInitError.call(this, error);
     } catch (ex) {
       this._log.error("Error when calling _onInitError: " +
                       CommonUtils.exceptionStr(ex));
     }
 
     // This bypasses a lot of the checks in policy, such as respect for
     // backoff. We should arguably not do this. However, reporting
     // startup errors is important. And, they should not occur with much
     // frequency in the wild. So, it shouldn't be too big of a deal.
     if (!inShutdown &&
-        this._policy.ensureNotifyResponse(new Date()) &&
-        this._policy.healthReportUploadEnabled) {
+        this._policy.healthReportUploadEnabled &&
+        this._policy.ensureUserNotified()) {
       // We don't care about what happens to this request. It's best
       // effort.
       let request = {
         onNoDataAvailable: function () {},
         onSubmissionSuccess: function () {},
         onSubmissionFailureSoft: function () {},
         onSubmissionFailureHard: function () {},
         onUploadInProgress: function () {},
--- a/services/healthreport/modules-testing/utils.jsm
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -20,16 +20,17 @@ Cu.import("resource://gre/modules/Promis
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services-common/utils.js");
 Cu.import("resource://gre/modules/services/datareporting/policy.jsm");
 Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+Cu.import("resource://testing-common/services/datareporting/mocks.jsm");
 
 
 let APP_INFO = {
   vendor: "Mozilla",
   name: "xpcshell",
   ID: "xpcshell@tests.mozilla.org",
   version: "1",
   appBuildID: "20121107",
@@ -185,26 +186,24 @@ this.getHealthReporter = function (name,
 
   let prefs = new Preferences(branch + "healthreport.");
   prefs.set("documentServerURI", uri);
   prefs.set("dbName", name);
 
   let reporter;
 
   let policyPrefs = new Preferences(branch + "policy.");
-  let policy = new DataReportingPolicy(policyPrefs, prefs, {
-    onRequestDataUpload: function (request) {
-      reporter.requestDataUpload(request);
-    },
-
-    onNotifyDataPolicy: function (request) { },
-
-    onRequestRemoteDelete: function (request) {
-      reporter.deleteRemoteData(request);
-    },
-  });
-
+  let listener = new MockPolicyListener();
+  listener.onRequestDataUpload = function (request) {
+    reporter.requestDataUpload(request);
+    MockPolicyListener.prototype.onRequestDataUpload.call(this, request);
+  }
+  listener.onRequestRemoteDelete = function (request) {
+    reporter.deleteRemoteData(request);
+    MockPolicyListener.prototype.onRequestRemoteDelete.call(this, request);
+  }
+  let policy = new DataReportingPolicy(policyPrefs, prefs, listener);
   let type = inspected ? InspectedHealthReporter : HealthReporter;
   reporter = new type(branch + "healthreport.", policy, null,
                       "state-" + name + ".json");
 
   return reporter;
 };
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -87,16 +87,31 @@ function getHealthReportProviderValues(r
     let serializer = m.serializer(m.SERIALIZE_JSON)
     let json = serializer.daily(data.days.getDay(day));
     do_check_eq(json._v, 2);
 
     throw new Task.Result(json);
   });
 }
 
+/*
+ * Ensure that the notification has been displayed to the user therefore having
+ * reporter._policy.userNotifiedOfCurrentPolicy === true, which will allow for a
+ * successful data upload.
+ * @param  {HealthReporter} reporter
+ * @return {Promise}
+ */
+function ensureUserNotified (reporter) {
+  return Task.spawn(function* ensureUserNotified () {
+    reporter._policy.ensureUserNotified();
+    yield reporter._policy._listener.lastNotifyRequest.deferred.promise;
+    do_check_true(reporter._policy.userNotifiedOfCurrentPolicy);
+  });
+}
+
 function run_test() {
   run_next_test();
 }
 
 // run_test() needs to finish synchronously, so we do async init here.
 add_task(function test_init() {
   yield makeFakeAppDir();
 });
@@ -668,19 +683,18 @@ add_task(function test_data_submission_s
 
 add_task(function test_recurring_daily_pings() {
   let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
   try {
     reporter._providerManager.registerProvider(new DummyProvider());
 
     let policy = reporter._policy;
 
-    defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
-    policy.recordUserAcceptance();
     defineNow(policy, policy.nextDataSubmissionDate);
+    yield ensureUserNotified(reporter);
     let promise = policy.checkStateAndTrigger();
     do_check_neq(promise, null);
     yield promise;
 
     let lastID = reporter.lastSubmitID;
     do_check_neq(lastID, null);
     do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
 
@@ -707,18 +721,18 @@ add_task(function test_recurring_daily_p
 });
 
 add_task(function test_request_remote_data_deletion() {
   let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion");
 
   try {
     let policy = reporter._policy;
     defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
-    policy.recordUserAcceptance();
     defineNow(policy, policy.nextDataSubmissionDate);
+    yield ensureUserNotified(reporter);
     yield policy.checkStateAndTrigger();
     let id = reporter.lastSubmitID;
     do_check_neq(id, null);
     do_check_true(server.hasDocument(reporter.serverNamespace, id));
 
     let clientID = reporter._state.clientID;
     do_check_neq(clientID, null);
 
@@ -795,26 +809,22 @@ add_task(function test_multiple_simultan
 });
 
 add_task(function test_policy_accept_reject() {
   let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
 
   try {
     let policy = reporter._policy;
 
-    do_check_false(policy.dataSubmissionPolicyAccepted);
+    do_check_eq(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0);
+    do_check_true(policy.dataSubmissionPolicyAcceptedVersion < DATAREPORTING_POLICY_VERSION);
     do_check_false(reporter.willUploadData);
 
-    policy.recordUserAcceptance();
-    do_check_true(policy.dataSubmissionPolicyAccepted);
+    yield ensureUserNotified(reporter);
     do_check_true(reporter.willUploadData);
-
-    policy.recordUserRejection();
-    do_check_false(policy.dataSubmissionPolicyAccepted);
-    do_check_false(reporter.willUploadData);
   } finally {
     yield reporter._shutdown();
     yield shutdownServer(server);
   }
 });
 
 add_task(function test_error_message_scrubbing() {
   let reporter = yield getReporter("error_message_scrubbing");
@@ -935,19 +945,19 @@ add_task(function test_upload_on_init_fa
       do_check_true(result.transportSuccess);
       do_check_true(result.serverSuccess);
 
       oldOnResult.call(reporter, request, isDelete, new Date(), result);
       deferred.resolve();
     },
   });
 
-  reporter._policy.recordUserAcceptance();
   let error = false;
   try {
+    yield ensureUserNotified(reporter);
     yield reporter.init();
   } catch (ex) {
     error = true;
   } finally {
     do_check_true(error);
   }
 
   // At this point the emergency upload should have been initiated. We
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -123,18 +123,20 @@ user_pref("security.turn_off_all_securit
 // to run our XBL tests in automation, in which case we really want to be testing
 // the configuration that we ship to users without special whitelisting. So we
 // use an additional pref here to allow automation to use the "normal" behavior.
 user_pref("dom.use_xbl_scopes_for_remote_xul", true);
 
 // Get network events.
 user_pref("network.activity.blipIntervalMilliseconds", 250);
 
-// Don't allow the Data Reporting service to prompt for policy acceptance.
-user_pref("datareporting.policy.dataSubmissionPolicyBypassAcceptance", true);
+// We do not wish to display datareporting policy notifications as it might
+// cause other tests to fail. Tests that wish to test the notification functionality
+// should explicitly disable this pref.
+user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
 
 // Point Firefox Health Report at a local server. We don't care if it actually
 // works. It just can't hit the default production endpoint.
 user_pref("datareporting.healthreport.documentServerURI", "http://%(server)s/healthreport/");
 user_pref("datareporting.healthreport.about.reportUrl", "http://%(server)s/abouthealthreport/");
 
 // Make sure CSS error reporting is enabled for tests
 user_pref("layout.css.report_errors", true);
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -580,89 +580,65 @@ this.SocialService = {
                     learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api",
                   };
     let anchor = "servicesInstall-notification-icon";
     let notificationid = "servicesInstall";
     chromeWin.PopupNotifications.show(browser, notificationid, message, anchor,
                                       action, [], options);
   },
 
-  installProvider: function(aDOMDocument, data, installCallback, aBypassUserEnable=false) {
+  installProvider: function(aDOMDocument, data, installCallback, options={}) {
     let manifest;
     let installOrigin = aDOMDocument.nodePrincipal.origin;
 
-    if (data) {
-      let installType = getOriginActivationType(installOrigin);
-      // if we get data, we MUST have a valid manifest generated from the data
-      manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
-      if (!manifest)
-        throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
+    let installType = getOriginActivationType(installOrigin);
+    // if we get data, we MUST have a valid manifest generated from the data
+    manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
+    if (!manifest)
+      throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
 
-      let addon = new AddonWrapper(manifest);
-      if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
-        throw new Error("installProvider: provider with origin [" +
-                        installOrigin + "] is blocklisted");
-      // manifestFromData call above will enforce correct origin. To support
-      // activation from about: uris, we need to be sure to use the updated
-      // origin on the manifest.
-      installOrigin = manifest.origin;
-    }
+    let addon = new AddonWrapper(manifest);
+    if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+      throw new Error("installProvider: provider with origin [" +
+                      installOrigin + "] is blocklisted");
+    // manifestFromData call above will enforce correct origin. To support
+    // activation from about: uris, we need to be sure to use the updated
+    // origin on the manifest.
+    installOrigin = manifest.origin;
 
     let id = getAddonIDFromOrigin(installOrigin);
     AddonManager.getAddonByID(id, function(aAddon) {
       if (aAddon && aAddon.userDisabled) {
         aAddon.cancelUninstall();
         aAddon.userDisabled = false;
       }
       schedule(function () {
-        this._installProvider(aDOMDocument, manifest, aBypassUserEnable, aManifest => {
+        this._installProvider(aDOMDocument, manifest, options, aManifest => {
           this._notifyProviderListeners("provider-installed", aManifest.origin);
           installCallback(aManifest);
         });
       }.bind(this));
     }.bind(this));
   },
 
-  _installProvider: function(aDOMDocument, manifest, aBypassUserEnable, installCallback) {
-    let sourceURI = aDOMDocument.location.href;
-    let installOrigin = aDOMDocument.nodePrincipal.origin;
+  _installProvider: function(aDOMDocument, manifest, options, installCallback) {
+    if (!manifest)
+      throw new Error("Cannot install provider without manifest data");
 
-    let installType = getOriginActivationType(installOrigin);
-    let installer;
-    switch(installType) {
-      case "foreign":
-        if (!Services.prefs.getBoolPref("social.remote-install.enabled"))
-          throw new Error("Remote install of services is disabled");
-        if (!manifest)
-          throw new Error("Cannot install provider without manifest data");
+    let installType = getOriginActivationType(aDOMDocument.nodePrincipal.origin);
+    if (installType == "foreign" && !Services.prefs.getBoolPref("social.remote-install.enabled"))
+      throw new Error("Remote install of services is disabled");
 
-        installer = new AddonInstaller(sourceURI, manifest, installCallback);
-        this._showInstallNotification(aDOMDocument, installer);
-        break;
-      case "internal":
-        // double check here since "builtin" falls through this as well.
-        aBypassUserEnable = installType == "internal" && manifest.oneclick;
-      case "directory":
-        // a manifest is requried, and will have been vetted by reviewers. We
-        // also handle in-product installations without the verification step.
-        if (aBypassUserEnable) {
-          installer = new AddonInstaller(sourceURI, manifest, installCallback);
-          installer.install();
-          return;
-        }
-        // a manifest is required, we'll catch a missing manifest below.
-        if (!manifest)
-          throw new Error("Cannot install provider without manifest data");
-        installer = new AddonInstaller(sourceURI, manifest, installCallback);
-        this._showInstallNotification(aDOMDocument, installer);
-        break;
-      default:
-        throw new Error("SocialService.installProvider: Invalid install type "+installType+"\n");
-        break;
-    }
+    let installer = new AddonInstaller(aDOMDocument.location.href, manifest, installCallback);
+    let bypassPanel = options.bypassInstallPanel ||
+                      (installType == "internal" && manifest.oneclick);
+    if (bypassPanel)
+      installer.install();
+    else
+      this._showInstallNotification(aDOMDocument, installer);
   },
 
   createWrapper: function(manifest) {
     return new AddonWrapper(manifest);
   },
 
   /**
    * updateProvider is used from the worker to self-update.  Since we do not
--- a/toolkit/devtools/server/actors/tracer.js
+++ b/toolkit/devtools/server/actors/tracer.js
@@ -279,16 +279,22 @@ TracerActor.prototype = {
       // statement in the function).
       packet.location = {
         url: aFrame.script.url,
         line: aFrame.script.getOffsetLine(aFrame.offset),
         column: getOffsetColumn(aFrame.offset, aFrame.script)
       };
     }
 
+    if (this._parent.threadActor && aFrame.script) {
+      packet.blackBoxed = this._parent.threadActor.sources.isBlackBoxed(aFrame.script.url);
+    } else {
+      packet.blackBoxed = false;
+    }
+
     if (this._requestsForTraceType.callsite
         && aFrame.older
         && aFrame.older.script) {
       let older = aFrame.older;
       packet.callsite = {
         url: older.script.url,
         line: older.script.getOffsetLine(older.offset),
         column: getOffsetColumn(older.offset, older.script)
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -170,19 +170,19 @@ function attachTestTab(aClient, aTitle, 
   });
 }
 
 // Attach to |aClient|'s tab whose title is |aTitle|, and then attach to
 // that tab's thread. Pass |aCallback| the thread attach response packet, a
 // TabClient referring to the tab, and a ThreadClient referring to the
 // thread.
 function attachTestThread(aClient, aTitle, aCallback) {
-  attachTestTab(aClient, aTitle, function (aResponse, aTabClient) {
+  attachTestTab(aClient, aTitle, function (aTabResponse, aTabClient) {
     function onAttach(aResponse, aThreadClient) {
-      aCallback(aResponse, aTabClient, aThreadClient);
+      aCallback(aResponse, aTabClient, aThreadClient, aTabResponse);
     }
     aTabClient.attachThread({
       useSourceMaps: true,
       autoBlackBox: true
     }, onAttach);
   });
 }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_trace_actor-10.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Create 2 sources, A and B, B is black boxed. When calling functions A->B->A,
+ * verify that only traces from source B are black boxed.
+ */
+
+var gDebuggee;
+var gClient;
+var gTraceClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestTracerServer();
+  gDebuggee = addTestGlobal("test-tracer-actor");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestThread(gClient, "test-tracer-actor",
+      function(aResponse, aTabClient, aThreadClient, aTabResponse) {
+      gThreadClient = aThreadClient;
+      gThreadClient.resume(function (aResponse) {
+        gClient.attachTracer(aTabResponse.traceActor,
+          function(aResponse, aTraceClient) {
+          gTraceClient = aTraceClient;
+          testTraces();
+        });
+      });
+    });
+  });
+  do_test_pending();
+}
+
+const BLACK_BOXED_URL = "http://example.com/blackboxme.js";
+const SOURCE_URL = "http://example.com/source.js";
+
+const testTraces = Task.async(function* () {
+  // Read traces
+  const tracesStopped = promise.defer();
+  gClient.addListener("traces", (aEvent, { traces }) => {
+    for (let t of traces) {
+      check_trace(t);
+    }
+    tracesStopped.resolve();
+  });
+
+  yield startTrace();
+
+  evalSetup();
+
+  // Blackbox source
+  const sourcesResponse = yield getSources(gThreadClient);
+  let sourceClient = gThreadClient.source(
+    sourcesResponse.sources.filter(s => s.url == BLACK_BOXED_URL)[0]);
+  do_check_true(!sourceClient.isBlackBoxed,
+    "By default the source is not black boxed.");
+  yield blackBox(sourceClient);
+  do_check_true(sourceClient.isBlackBoxed);
+
+  evalTestCode();
+
+  yield tracesStopped.promise;
+  yield stopTrace();
+
+  finishClient(gClient);
+});
+
+function startTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.startTrace(["depth", "name", "location"], null,
+    function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function evalSetup()
+{
+  Components.utils.evalInSandbox(
+    "" + function fnBlackBoxed(k) {
+      fnInner();
+    },
+    gDebuggee,
+    "1.8",
+    BLACK_BOXED_URL,
+    1
+  );
+
+  Components.utils.evalInSandbox(
+    "" + function fnOuter() {
+      fnBlackBoxed();
+    } + "\n" +
+    "" + function fnInner() {
+      [1].forEach(function noop() {});
+    },
+    gDebuggee,
+    "1.8",
+    SOURCE_URL,
+    1
+  );
+}
+
+function evalTestCode()
+{
+  Components.utils.evalInSandbox(
+    "fnOuter();",
+    gDebuggee,
+    "1.8",
+    SOURCE_URL,
+    1
+  );
+}
+
+function stopTrace()
+{
+  let deferred = promise.defer();
+  gTraceClient.stopTrace(null, function() { deferred.resolve(); });
+  return deferred.promise;
+}
+
+function check_trace({ type, sequence, depth, name, location, blackBoxed })
+{
+  switch(sequence) {
+  // First two packets come from evalInSandbox in evalSetup
+  // The third packet comes from evalInSandbox in evalTestCode
+  case 0:
+  case 2:
+  case 4:
+    do_check_eq(name, "(global)");
+    do_check_eq(type, "enteredFrame");
+    break;
+
+  case 5:
+    do_check_eq(blackBoxed, false);
+    do_check_eq(name, "fnOuter");
+    break;
+
+  case 6:
+    do_check_eq(blackBoxed, true);
+    do_check_eq(name, "fnBlackBoxed");
+    break;
+
+  case 7:
+    do_check_eq(blackBoxed, false);
+    do_check_eq(name, "fnInner");
+    break;
+
+  case 8:
+    do_check_eq(blackBoxed, false);
+    do_check_eq(name, "noop");
+    break;
+
+  case 1: // evalInSandbox
+  case 3: // evalInSandbox
+  case 9: // noop
+  case 10: // fnInner
+  case 11: // fnBlackBoxed
+  case 12: // fnOuter
+  case 13: // evalInSandbox
+    do_check_eq(type, "exitedFrame");
+    break;
+
+  default:
+    // Should have covered all sequences.
+    do_check_true(false);
+  }
+}
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -58,18 +58,18 @@ function createRootActor(aConnection)
   return root;
 }
 
 function TestTabActor(aConnection, aGlobal)
 {
   this.conn = aConnection;
   this._global = aGlobal;
   this._global.wrappedJSObject = aGlobal;
-  this._threadActor = new ThreadActor(this, this._global);
-  this.conn.addActor(this._threadActor);
+  this.threadActor = new ThreadActor(this, this._global);
+  this.conn.addActor(this.threadActor);
   this._attached = false;
   this._extraActors = {};
   this.makeDebugger = makeDebugger.bind(null, {
     findDebuggees: () => [this._global],
     shouldAddNewGlobalAsDebuggee: g => g.hostAnnotations &&
                                        g.hostAnnotations.type == "document" &&
                                        g.hostAnnotations.element === this._global
 
@@ -102,17 +102,17 @@ TestTabActor.prototype = {
     this._appendExtraActors(response);
 
     return response;
   },
 
   onAttach: function(aRequest) {
     this._attached = true;
 
-    let response = { type: "tabAttached", threadActor: this._threadActor.actorID };
+    let response = { type: "tabAttached", threadActor: this.threadActor.actorID };
     this._appendExtraActors(response);
 
     return response;
   },
 
   onDetach: function(aRequest) {
     if (!this._attached) {
       return { "error":"wrongState" };
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -191,16 +191,17 @@ reason = bug 820380
 [test_trace_actor-02.js]
 [test_trace_actor-03.js]
 [test_trace_actor-04.js]
 [test_trace_actor-05.js]
 [test_trace_actor-06.js]
 [test_trace_actor-07.js]
 [test_trace_actor-08.js]
 [test_trace_actor-09.js]
+[test_trace_actor-10.js]
 [test_ignore_caught_exceptions.js]
 [test_requestTypes.js]
 reason = bug 937197
 [test_layout-reflows-observer.js]
 [test_protocolSpec.js]
 [test_registerClient.js]
 [test_client_request.js]
 [test_monitor_actor.js]