Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Sat, 16 Aug 2014 17:18:48 -0400
changeset 199893 9a2152d29d8a12b1bebcdb179c907491cac8bcb2
parent 199878 675bab72b8a8960bc65f6ac42d67927c2d310e05 (current diff)
parent 199892 a4e46fd6959b36c5baae3f09f2a27dde52c3a388 (diff)
child 199904 c3eb1b5ad4e4b12e0982a07bdccb2de09799bc52
push id27324
push userryanvm@gmail.com
push dateSat, 16 Aug 2014 21:18:56 +0000
treeherdermozilla-central@9a2152d29d8a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone34.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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]