Bug 1259245 - Land version 1.2.3 of the Loop system add-on in mozilla-central - code updates. rs=Standard8 for already reviewed code.
☠☠ backed out by d5f3da0cfe7c ☠ ☠
authorMark Banner <standard8@mozilla.com>
Wed, 23 Mar 2016 21:51:33 +0000
changeset 290033 7b0bb5c3ab01ab5003c64d2c715742db93eb9fd1
parent 290032 16aaf4cd418c2c26b27967f2940a31937590f646
child 290034 358a57aa077a81210fd12e1fb8fa4e6ec0039fdd
push id18344
push usermbanner@mozilla.com
push dateWed, 23 Mar 2016 21:52:05 +0000
treeherderfx-team@7b0bb5c3ab01 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1259245
milestone48.0a1
Bug 1259245 - Land version 1.2.3 of the Loop system add-on in mozilla-central - code updates. rs=Standard8 for already reviewed code.
browser/extensions/loop/bootstrap.js
browser/extensions/loop/chrome/content/modules/tabFrame.js
browser/extensions/loop/chrome/content/panels/css/desktop.css
browser/extensions/loop/chrome/content/panels/js/roomViews.js
browser/extensions/loop/chrome/content/preferences/prefs.js
browser/extensions/loop/chrome/content/shared/css/conversation.css
browser/extensions/loop/chrome/content/shared/js/textChatView.js
browser/extensions/loop/chrome/content/shared/js/views.js
browser/extensions/loop/chrome/content/shared/test/views_test.js
browser/extensions/loop/chrome/test/mochitest/browser.ini
browser/extensions/loop/chrome/test/mochitest/browser_sharingTitleListeners.js
browser/extensions/loop/install.rdf.in
browser/extensions/loop/jar.mn
--- a/browser/extensions/loop/bootstrap.js
+++ b/browser/extensions/loop/bootstrap.js
@@ -8,16 +8,19 @@
 const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
 
 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const kBrowserSharingNotificationId = "loop-sharing-notification";
 
 const CURSOR_MIN_DELTA = 3;
 const CURSOR_MIN_INTERVAL = 100;
 const CURSOR_CLICK_DELAY = 1000;
+// Due to bug 1051238 frame scripts are cached forever, so we can't update them
+// as a restartless add-on. The Math.random() is the work around for this.
+const FRAME_SCRIPT = "chrome://loop/content/modules/tabFrame.js?" + Math.random();
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
@@ -52,16 +55,17 @@ var WindowListener = {
    */
   setupBrowserUI: function(window) {
     let document = window.document;
     let gBrowser = window.gBrowser;
     let xhrClass = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"];
     let FileReader = window.FileReader;
     let menuItem = null;
     let isSlideshowOpen = false;
+    let titleChangedListener = null;
 
     // the "exported" symbols
     var LoopUI = {
       /**
        * @var {XULWidgetSingleWrapper} toolbarButton Getter for the Loop toolbarbutton
        *                                             instance for this window. This should
        *                                             not be used in the hidden window.
        */
@@ -110,16 +114,20 @@ var WindowListener = {
           }, result => {
             this._constants = result;
           });
         }
 
         return this._constants;
       },
 
+      get mm() {
+        return window.getGroupMessageManager("browsers");
+      },
+
       /**
        * @return {Promise}
        */
       promiseDocumentVisible(aDocument) {
         if (!aDocument.hidden) {
           return Promise.resolve(aDocument);
         }
 
@@ -308,16 +316,20 @@ var WindowListener = {
         this.addMenuItem();
 
         // Don't do the rest if this is for the hidden window - we don't
         // have a toolbar there.
         if (window == Services.appShell.hiddenDOMWindow) {
           return;
         }
 
+        // Load the frame script into any tab, plus any that get created in the
+        // future.
+        this.mm.loadFrameScript(FRAME_SCRIPT, true);
+
         // Cleanup when the window unloads.
         window.addEventListener("unload", () => {
           Services.obs.removeObserver(this, "loop-status-changed");
         });
 
         Services.obs.addObserver(this, "loop-status-changed", false);
 
         this.updateToolbarState();
@@ -517,19 +529,23 @@ var WindowListener = {
        * Push message parameters:
        * - {Integer} windowId  The new windowId for the browser.
        */
       startBrowserSharing: function() {
         if (!this._listeningToTabSelect) {
           gBrowser.tabContainer.addEventListener("TabSelect", this);
           this._listeningToTabSelect = true;
 
+          titleChangedListener = this.handleDOMTitleChanged.bind(this);
+
           // Watch for title changes as opposed to location changes as more
           // metadata about the page is available when this event fires.
-          gBrowser.addEventListener("DOMTitleChanged", this);
+          this.mm.addMessageListener("loop@mozilla.org:DOMTitleChanged",
+            titleChangedListener);
+
           this._browserSharePaused = false;
 
           // Add this event to the parent gBrowser to avoid adding and removing
           // it for each individual tab's browsers.
           gBrowser.addEventListener("mousemove", this);
           gBrowser.addEventListener("click", this);
         }
 
@@ -545,17 +561,22 @@ var WindowListener = {
        */
       stopBrowserSharing: function() {
         if (!this._listeningToTabSelect) {
           return;
         }
 
         this._hideBrowserSharingInfoBar();
         gBrowser.tabContainer.removeEventListener("TabSelect", this);
-        gBrowser.removeEventListener("DOMTitleChanged", this);
+
+        if (titleChangedListener) {
+          this.mm.removeMessageListener("loop@mozilla.org:DOMTitleChanged",
+            titleChangedListener);
+          titleChangedListener = null;
+        }
 
         // Remove shared pointers related events
         gBrowser.removeEventListener("mousemove", this);
         gBrowser.removeEventListener("click", this);
         this.removeRemoteCursor();
 
         this._listeningToTabSelect = false;
         this._browserSharePaused = false;
@@ -784,24 +805,36 @@ var WindowListener = {
        */
       _notifyBrowserSwitch: function() {
          // Get the first window Id for the listener.
         this.LoopAPI.broadcastPushMessage("BrowserSwitch",
           gBrowser.selectedBrowser.outerWindowID);
       },
 
       /**
+       * Handles events from the frame script.
+       *
+       * @param {Object} message The message received from the frame script.
+       */
+      handleDOMTitleChanged: function(message) {
+        if (!this._listeningToTabSelect || this._browserSharePaused) {
+          return;
+        }
+
+        if (gBrowser.selectedBrowser == message.target) {
+          // Get the new title of the shared tab
+          this._notifyBrowserSwitch();
+        }
+      },
+
+      /**
        * Handles events from gBrowser.
        */
       handleEvent: function(event) {
         switch (event.type) {
-          case "DOMTitleChanged":
-            // Get the new title of the shared tab
-            this._notifyBrowserSwitch();
-            break;
           case "TabSelect":
             let wasVisible = false;
             // Hide the infobar from the previous tab.
             if (event.detail.previousTab) {
               wasVisible = this._hideBrowserSharingInfoBar(
                             event.detail.previousTab.linkedBrowser);
               // And remove the cursor.
               this.removeRemoteCursor();
@@ -934,16 +967,20 @@ var WindowListener = {
    * document.getElementById() etc. will work here.
    *
    * @param {Object} window The window to remove the integration from.
    */
   tearDownBrowserUI: function(window) {
     if (window.LoopUI) {
       window.LoopUI.removeMenuItem();
 
+      // This stops the frame script being loaded to new tabs, but doesn't
+      // remove it from existing tabs (there's no way to do that).
+      window.LoopUI.mm.removeDelayedFrameScript(FRAME_SCRIPT);
+
       // XXX Bug 1229352 - Add in tear-down of the panel.
     }
   },
 
   // nsIWindowMediatorListener functions.
   onOpenWindow: function(xulWindow) {
     // A new window has opened.
     let domWindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/loop/chrome/content/modules/tabFrame.js
@@ -0,0 +1,22 @@
+/* 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/. */
+
+"use strict";
+
+/* global sendAsyncMessage */
+
+/**
+ * This script runs in the content process and is attached to browsers when
+ * they are created.
+ */
+
+// Listen for when the title is changed and send a message back to the chrome
+// process.
+addEventListener("DOMTitleChanged", ({ target }) => {
+  sendAsyncMessage("loop@mozilla.org:DOMTitleChanged", {
+    details: "titleChanged"
+  }, {
+     target: target
+  });
+});
--- a/browser/extensions/loop/chrome/content/panels/css/desktop.css
+++ b/browser/extensions/loop/chrome/content/panels/css/desktop.css
@@ -25,17 +25,16 @@
 .room-invitation-content {
   display: flex;
   flex-flow: column nowrap;
   margin: 12px 0;
   font-size: 1.4rem;
 }
 
 .room-invitation-content > * {
-  width: 100%;
   margin: 0 15px;
 }
 
 .room-context-header {
   font-weight: bold;
   font-size: 1.6rem;
   margin-bottom: 10px;
   text-align: center;
@@ -225,20 +224,16 @@ html[dir="rtl"] .share-panel-container >
   right: initial;
   transform: translate(-100%);
 }
 
 .share-panel-container > .room-invitation-overlay > .room-invitation-content {
   margin: 0 0 12px;
 }
 
-.share-panel-container > .room-invitation-overlay > .room-invitation-content > * {
-  width: initial;
-}
-
 .share-panel-open > .room-invitation-overlay,
 html[dir="rtl"] .share-panel-open > .room-invitation-overlay {
   transform: translateX(0);
 }
 
 .share-panel-open > .share-panel-overlay {
   display: block;
 }
--- a/browser/extensions/loop/chrome/content/panels/js/roomViews.js
+++ b/browser/extensions/loop/chrome/content/panels/js/roomViews.js
@@ -351,16 +351,17 @@ loop.roomViews = function (mozL10n) {
                   localVideoMuted: this.state.videoMuted,
                   matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
                   remotePosterUrl: this.props.remotePosterUrl,
                   remoteSrcMediaElement: this.state.remoteSrcMediaElement,
                   renderRemoteVideo: this.shouldRenderRemoteVideo(),
                   screenShareMediaElement: this.state.screenShareMediaElement,
                   screenSharePosterUrl: null,
                   showInitialContext: false,
+                  showMediaWait: false,
                   showTile: false },
                 React.createElement(sharedViews.ConversationToolbar, {
                   audio: { enabled: !this.state.audioMuted, visible: true },
                   dispatcher: this.props.dispatcher,
                   hangup: this.leaveRoom,
                   showHangup: this.props.chatWindowDetached,
                   video: { enabled: !this.state.videoMuted, visible: true } }),
                 React.createElement(sharedDesktopViews.SharePanelView, {
--- a/browser/extensions/loop/chrome/content/preferences/prefs.js
+++ b/browser/extensions/loop/chrome/content/preferences/prefs.js
@@ -1,12 +1,17 @@
 pref("loop.enabled", true);
 pref("loop.remote.autostart", true);
+#ifdef LOOP_DEV_XPI
+pref("loop.server", "https://loop-dev.stage.mozaws.net/v0");
+pref("loop.linkClicker.url", "https://loop-webapp-dev.stage.mozaws.net/");
+#else
 pref("loop.server", "https://loop.services.mozilla.com/v0");
 pref("loop.linkClicker.url", "https://hello.firefox.com/");
+#endif
 pref("loop.gettingStarted.latestFTUVersion", 1);
 pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start/");
 pref("loop.gettingStarted.resumeOnFirstJoin", false);
 pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");
 pref("loop.do_not_disturb", false);
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
--- a/browser/extensions/loop/chrome/content/shared/css/conversation.css
+++ b/browser/extensions/loop/chrome/content/shared/css/conversation.css
@@ -505,50 +505,84 @@ html, .fx-embedded, #main,
    fix its height. */
 .media-wrapper > .text-chat-view {
   flex: 0 0 auto;
   height: 100%;
   /* Text chat is a fixed 272px width for normal displays. */
   width: 272px;
 }
 
-.media-wrapper.showing-local-streams > .text-chat-view {
+.media-wrapper > .text-chat-view > .text-chat-entries > .text-chat-scroller > .welcome-message {
+  font-size: 1.2rem;
+  margin: 0 0 15px;
+  color: #5e5f64;
+  line-height: 20px;
+}
+
+.media-wrapper.showing-local-streams > .text-chat-view,
+.media-wrapper.showing-media-wait > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
      chat view a bit shorter to give room. */
   height: calc(100% - 204px);
 }
 
+.media-wrapper.showing-media-wait > .text-chat-view {
+  order: 2;
+}
+
+.media-wrapper.showing-media-wait > .local {
+  /* Hides the local stream video box while we're asking the user for permissions */
+  display: none;
+}
+
 .media-wrapper.showing-local-streams.receiving-screen-share {
   position: relative;
 }
 
 .media-wrapper.showing-local-streams.receiving-screen-share > .text-chat-view {
   /* When we're displaying the local streams, then we need to make the text
-     chat view a bit shorter to give room. 2 streams x 204px each*/
-  height: calc(100% - 408px);
+     chat view a bit shorter to give room. 1 streams x 204px */
+  height: calc(100% - 204px);
 }
 
 .media-wrapper.receiving-screen-share > .screen {
   order: 1;
 }
 
-.media-wrapper.receiving-screen-share > .text-chat-view {
-  order: 2;
+.media-wrapper.receiving-screen-share > .text-chat-view,
+.media-wrapper.showing-local-streams > .text-chat-view  {
+  order: 4;
 }
 
 .media-wrapper.receiving-screen-share > .remote {
   flex: 0 1 auto;
-  order: 3;
+  order: 2;
   /* to keep the 4:3 ratio set both height and width */
   height: 204px;
   width: 272px;
 }
 
 .media-wrapper.receiving-screen-share > .local {
-  order: 4;
+  order: 3;
+}
+
+.media-wrapper.receiving-screen-share.showing-remote-streams > .local {
+  position: absolute;
+  z-index: 2;
+  padding: 8px;
+  right: 0;
+  left: auto;
+  top: 124px;
+  /* to keep the 4:3 ratio 80x60px + 16px padding + 4px border */
+  width: calc(80px + 16px + 4px);
+  height: calc(60px + 16px + 4px);
+}
+
+.media-wrapper.receiving-screen-share.showing-remote-streams > .local > .remote-video-box {
+  border: solid 2px #fff;
 }
 
 @media screen and (max-width:640px) {
   .media-layout > .media-wrapper {
     flex-direction: row;
     margin: 0;
     width: 100%;
   }
@@ -625,34 +659,36 @@ html, .fx-embedded, #main,
     max-width: 50%;
   }
 
   .media-wrapper.receiving-screen-share > .remote .remote-video {
       /* Reset the object-fit for this. */
     object-fit: contain;
   }
 
-  .media-wrapper.receiving-screen-share > .local {
+  .media-wrapper.receiving-screen-share.showing-remote-streams > .local {
     /* Screen shares have remote & local video side-by-side on narrow screens */
     order: 3;
     flex: 1 1 auto;
     height: 20%;
     /* Ensure no previously specified widths take effect, and we take up no more
        than half the width. */
     width: auto;
     max-width: 50%;
     /* This cancels out the absolute positioning when it's just remote video. */
     position: relative;
+    top: auto;
     bottom: auto;
     right: auto;
     margin: 0;
+    padding: 0;
   }
 
-  .media-wrapper.receiving-screen-share > .text-chat-view {
-    order: 4;
+  .media-wrapper.receiving-screen-share.showing-remote-streams > .local > .remote-video-box {
+    border: 0;
   }
 }
 
 /* e.g. very narrow widths similar to conversation window.
    Note: on some displays (e.g. windows / medium size) the width
    may be very slightly over the expected width, so we add on 2px
    just in case. */
 @media screen and (max-width:352px) {
--- a/browser/extensions/loop/chrome/content/shared/js/textChatView.js
+++ b/browser/extensions/loop/chrome/content/shared/js/textChatView.js
@@ -211,16 +211,21 @@ loop.shared.views.chat = function (mozL1
       });
 
       return React.createElement(
         "div",
         { className: entriesClasses },
         React.createElement(
           "div",
           { className: "text-chat-scroller" },
+          loop.shared.utils.isDesktop() ? null : React.createElement(
+            "p",
+            { className: "welcome-message" },
+            mozL10n.get("rooms_welcome_text_chat_label", { clientShortname: mozL10n.get("clientShortname2") })
+          ),
           this.props.messageList.map(function (entry, i) {
             if (entry.type === CHAT_MESSAGE_TYPES.SPECIAL) {
               if (!this.props.showInitialContext) {
                 return null;
               }
               switch (entry.contentType) {
                 case CHAT_CONTENT_TYPES.ROOM_NAME:
                   return React.createElement(TextChatRoomName, {
--- a/browser/extensions/loop/chrome/content/shared/js/views.js
+++ b/browser/extensions/loop/chrome/content/shared/js/views.js
@@ -880,16 +880,17 @@ loop.shared.views = function (_, mozL10n
       matchMedia: React.PropTypes.func.isRequired,
       remotePosterUrl: React.PropTypes.string,
       remoteSrcMediaElement: React.PropTypes.object,
       renderRemoteVideo: React.PropTypes.bool.isRequired,
       screenShareMediaElement: React.PropTypes.object,
       screenSharePosterUrl: React.PropTypes.string,
       screenSharingPaused: React.PropTypes.bool,
       showInitialContext: React.PropTypes.bool.isRequired,
+      showMediaWait: React.PropTypes.bool.isRequired,
       showTile: React.PropTypes.bool.isRequired
     },
 
     isLocalMediaAbsolutelyPositioned: function (matchMedia) {
       if (!matchMedia) {
         matchMedia = this.props.matchMedia;
       }
       return matchMedia && (
@@ -937,32 +938,57 @@ loop.shared.views = function (_, mozL10n
           displayAvatar: this.props.localVideoMuted,
           isLoading: this.props.isLocalLoading,
           mediaType: "local",
           posterUrl: this.props.localPosterUrl,
           srcMediaElement: this.props.localSrcMediaElement })
       );
     },
 
+    renderMediaWait: function () {
+      var msg = mozL10n.get("call_progress_getting_media_description", { clientShortname: mozL10n.get("clientShortname2") });
+      var utils = loop.shared.utils;
+      var isChrome = utils.isChrome(navigator.userAgent);
+      var isFirefox = utils.isFirefox(navigator.userAgent);
+      var isOpera = utils.isOpera(navigator.userAgent);
+      var promptMediaMessageClasses = classNames({
+        "prompt-media-message": true,
+        "chrome": isChrome,
+        "firefox": isFirefox,
+        "opera": isOpera,
+        "other": !isChrome && !isFirefox && !isOpera
+      });
+      return React.createElement(
+        "div",
+        { className: "prompt-media-message-wrapper" },
+        React.createElement(
+          "p",
+          { className: promptMediaMessageClasses },
+          msg
+        )
+      );
+    },
+
     render: function () {
       var remoteStreamClasses = classNames({
         "remote": true,
         "focus-stream": !this.props.displayScreenShare
       });
 
       var screenShareStreamClasses = classNames({
         "screen": true,
         "focus-stream": this.props.displayScreenShare,
         "screen-sharing-paused": this.props.screenSharingPaused
       });
 
       var mediaWrapperClasses = classNames({
         "media-wrapper": true,
         "receiving-screen-share": this.props.displayScreenShare,
         "showing-local-streams": this.props.localSrcMediaElement || this.props.localPosterUrl,
+        "showing-media-wait": this.props.showMediaWait,
         "showing-remote-streams": this.props.remoteSrcMediaElement || this.props.remotePosterUrl || this.props.isRemoteLoading
       });
 
       return React.createElement(
         "div",
         { className: "media-layout" },
         React.createElement(
           "div",
@@ -998,17 +1024,18 @@ loop.shared.views = function (_, mozL10n
               shareCursor: true,
               srcMediaElement: this.props.screenShareMediaElement }),
             this.props.displayScreenShare ? this.props.children : null
           ),
           React.createElement(loop.shared.views.chat.TextChatView, {
             dispatcher: this.props.dispatcher,
             showInitialContext: this.props.showInitialContext,
             showTile: this.props.showTile }),
-          this.state.localMediaAboslutelyPositioned ? null : this.renderLocalVideo()
+          this.state.localMediaAboslutelyPositioned ? null : this.renderLocalVideo(),
+          this.props.showMediaWait ? this.renderMediaWait() : null
         )
       );
     }
   });
 
   var RemoteCursorView = React.createClass({
     displayName: "RemoteCursorView",
 
--- a/browser/extensions/loop/chrome/content/shared/test/views_test.js
+++ b/browser/extensions/loop/chrome/content/shared/test/views_test.js
@@ -826,16 +826,17 @@ describe("loop.shared.views", function()
         displayScreenShare: false,
         isLocalLoading: false,
         isRemoteLoading: false,
         isScreenShareLoading: false,
         localVideoMuted: false,
         matchMedia: window.matchMedia,
         renderRemoteVideo: false,
         showInitialContext: false,
+        showMediaWait: false,
         showTile: false
       };
 
       return TestUtils.renderIntoDocument(
         React.createElement(sharedViews.MediaLayoutView,
           _.extend(defaultProps, extraProps)));
     }
 
@@ -963,16 +964,34 @@ describe("loop.shared.views", function()
       view = mountTestComponent({
         remoteSrcMediaElement: {},
         remotePosterUrl: "fake/url"
       });
 
       expect(view.getDOMNode().querySelector(".media-wrapper")
         .classList.contains("showing-remote-streams")).eql(true);
     });
+
+    it("should mark the wrapper as showing media wait tile when asking for user media", function() {
+      view = mountTestComponent({
+        showMediaWait: true
+      });
+
+      expect(view.getDOMNode().querySelector(".media-wrapper")
+        .classList.contains("showing-media-wait")).eql(true);
+    });
+
+    it("should display a media wait tile when asking for user media", function() {
+      view = mountTestComponent({
+        showMediaWait: true
+      });
+
+      expect(view.getDOMNode().querySelector(".prompt-media-message-wrapper"))
+        .not.eql(null);
+    });
   });
 
   describe("RemoteCursorView", function() {
     var view;
     var fakeVideoElementSize;
     var remoteCursorStore;
 
     function mountTestComponent(props) {
--- a/browser/extensions/loop/chrome/test/mochitest/browser.ini
+++ b/browser/extensions/loop/chrome/test/mochitest/browser.ini
@@ -11,9 +11,10 @@ support-files =
 [browser_menuitem.js]
 [browser_mozLoop_appVersionInfo.js]
 [browser_mozLoop_chat.js]
 [browser_mozLoop_context.js]
 [browser_mozLoop_socialShare.js]
 [browser_mozLoop_sharingListeners.js]
 skip-if = e10s
 [browser_mozLoop_telemetry.js]
+[browser_sharingTitleListeners.js]
 [browser_toolbarbutton.js]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/loop/chrome/test/mochitest/browser_sharingTitleListeners.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This file contains tests for the browser sharing document title listeners.
+ */
+"use strict";
+
+var [, gHandlers] = LoopAPI.inspect();
+
+function promiseBrowserSwitch() {
+  return new Promise(resolve => {
+    LoopAPI.stub([{
+      sendAsyncMessage: function(messageName, data) {
+        if (data[0] == "BrowserSwitch") {
+          LoopAPI.restore();
+          resolve();
+        }
+      }
+    }]);
+  });
+}
+
+add_task(function* setup() {
+  Services.prefs.setBoolPref("loop.remote.autostart", true);
+
+  gHandlers.AddBrowserSharingListener({ data: [42] }, () => {});
+
+  let newTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", true);
+
+  registerCleanupFunction(function* () {
+    // Remove the listener.
+    gHandlers.RemoveBrowserSharingListener({ data: [42] }, function() {});
+
+    yield BrowserTestUtils.removeTab(newTab);
+
+    Services.prefs.clearUserPref("loop.remote.autostart");
+  });
+});
+
+add_task(function* test_notifyOnTitleChanged() {
+  // Hook up the async listener and wait for the BrowserSwitch to happen.
+  let browserSwitchPromise = promiseBrowserSwitch();
+
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:mozilla");
+
+  // Now check we get the notification of the browser switch.
+  yield browserSwitchPromise;
+
+  Assert.ok(true, "We got notification of the browser switch");
+});
--- a/browser/extensions/loop/install.rdf.in
+++ b/browser/extensions/loop/install.rdf.in
@@ -4,18 +4,19 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 #filter substitution
 
 <RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
   <Description about="urn:mozilla:install-manifest">
     <em:id>loop@mozilla.org</em:id>
     <em:bootstrap>true</em:bootstrap>
-    <em:version>1.2.2</em:version>
+    <em:version>1.2.3</em:version>
     <em:type>2</em:type>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
 
     <!-- Target Application this extension can install into,
          with minimum and maximum supported versions. -->
     <em:targetApplication>
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>46.0a1</em:minVersion>
         <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
--- a/browser/extensions/loop/jar.mn
+++ b/browser/extensions/loop/jar.mn
@@ -1,17 +1,17 @@
 #filter substitution
 # 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/.
 
 [features/loop@mozilla.org] chrome.jar:
 % content loop %content/ contentaccessible=yes
 % content loop-locale-fallback %content/locale-fallback/en-US/
-% skin loop classic/1.0 %skin/linux/ os=Linux
+% skin loop classic/1.0 %skin/linux/
 % skin loop classic/1.0 %skin/osx/ os=Darwin
 % skin loop classic/1.0 %skin/windows/ os=WINNT
 % skin loop-shared classic/1.0 %skin/shared/
 % override chrome://loop/skin/menuPanel.png       chrome://loop/skin/menuPanel-yosemite.png       os=Darwin osversion>=10.10
 % override chrome://loop/skin/menuPanel@2x.png    chrome://loop/skin/menuPanel-yosemite@2x.png    os=Darwin osversion>=10.10
 % override chrome://loop/skin/toolbar.png         chrome://loop/skin/toolbar-yosemite.png         os=Darwin osversion>=10.10
 % override chrome://loop/skin/toolbar@2x.png      chrome://loop/skin/toolbar-yosemite@2x.png      os=Darwin osversion>=10.10
 # Windows 10+ uses the default toolbar.png