Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 29 Aug 2014 15:10:18 -0400
changeset 224013 8fdcd7e6ecb252f7168d69909884f55d874fff4e
parent 224003 11e4f1678eaba483e926440ab2ef8fe0e0dde99e (current diff)
parent 224012 8f49394f80690a8b605e9363b19b12450e11da47 (diff)
child 224014 9b3fbc3706311126d788897df0445908d52365e1
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [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/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -342,16 +342,24 @@ function injectLoopAPI(targetWindow) {
         return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => {
           callback(null, response.body);
         }, (error) => {
           callback(Cu.cloneInto(error, targetWindow));
         });
       }
     },
 
+    logInToFxA: {
+      enumerable: true,
+      writable: true,
+      value: function() {
+        return MozLoopService.logInToFxA();
+      }
+    },
+
     /**
      * Copies passed string onto the system clipboard.
      *
      * @param {String} str The string to copy
      */
     copyString: {
       enumerable: true,
       writable: true,
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -775,16 +775,20 @@ this.MozLoopService = {
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
   logInToFxA: function() {
+    if (gFxAOAuthTokenData) {
+      return Promise.resolve(gFxAOAuthTokenData);
+    }
+
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
       gFxAOAuthTokenData = tokenData;
       return tokenData;
     },
     error => {
       gFxAOAuthTokenData = null;
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -189,16 +189,19 @@ loop.conversation = (function(OT, mozL10
       }.bind(this));
       this._conversation.once("decline", function() {
         this.navigate("call/decline", {trigger: true});
       }.bind(this));
       this._conversation.once("declineAndBlock", function() {
         this.navigate("call/declineAndBlock", {trigger: true});
       }.bind(this));
       this._conversation.once("call:incoming", this.startCall, this);
+      this._conversation.once("change:publishedStream", this._checkConnected, this);
+      this._conversation.once("change:subscribedStream", this._checkConnected, this);
+
       this._client.requestCallsInfo(loopVersion, function(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._notifier.errorL10n("cannot_start_call_session_not_ready");
           return;
         }
@@ -231,20 +234,33 @@ loop.conversation = (function(OT, mozL10
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
+     * Checks if the streams have been connected, and notifies the
+     * websocket that the media is now connected.
+     */
+    _checkConnected: function() {
+      // Check we've had both local and remote streams connected before
+      // sending the media up message.
+      if (this._conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
+      this._websocket.accept();
       this._conversation.incoming();
     },
 
     /**
      * Declines a call and handles closing of the window.
      */
     _declineCall: function() {
       this._websocket.decline();
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -189,16 +189,19 @@ loop.conversation = (function(OT, mozL10
       }.bind(this));
       this._conversation.once("decline", function() {
         this.navigate("call/decline", {trigger: true});
       }.bind(this));
       this._conversation.once("declineAndBlock", function() {
         this.navigate("call/declineAndBlock", {trigger: true});
       }.bind(this));
       this._conversation.once("call:incoming", this.startCall, this);
+      this._conversation.once("change:publishedStream", this._checkConnected, this);
+      this._conversation.once("change:subscribedStream", this._checkConnected, this);
+
       this._client.requestCallsInfo(loopVersion, function(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._notifier.errorL10n("cannot_start_call_session_not_ready");
           return;
         }
@@ -231,20 +234,33 @@ loop.conversation = (function(OT, mozL10
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
+     * Checks if the streams have been connected, and notifies the
+     * websocket that the media is now connected.
+     */
+    _checkConnected: function() {
+      // Check we've had both local and remote streams connected before
+      // sending the media up message.
+      if (this._conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
      * Accepts an incoming call.
      */
     accept: function() {
       navigator.mozLoop.stopAlerting();
+      this._websocket.accept();
       this._conversation.incoming();
     },
 
     /**
      * Declines a call and handles closing of the window.
      */
     _declineCall: function() {
       this._websocket.decline();
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -72,34 +72,32 @@ loop.panel = (function(_, mozL10n) {
         'dnd-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        React.DOM.div({className: "footer"}, 
-          React.DOM.div({className: "do-not-disturb"}, 
-            React.DOM.div({className: "dnd-status", onClick: this.showDropdownMenu}, 
-              React.DOM.span(null, availabilityText), 
-              React.DOM.i({className: availabilityStatus})
+        React.DOM.div({className: "do-not-disturb"}, 
+          React.DOM.p({className: "dnd-status", onClick: this.showDropdownMenu}, 
+            React.DOM.span(null, availabilityText), 
+            React.DOM.i({className: availabilityStatus})
+          ), 
+          React.DOM.ul({className: availabilityDropdown, 
+              onMouseLeave: this.hideDropdownMenu}, 
+            React.DOM.li({onClick: this.changeAvailability("available"), 
+                className: "dnd-menu-item dnd-make-available"}, 
+              React.DOM.i({className: "status status-available"}), 
+              React.DOM.span(null, __("display_name_available_status"))
             ), 
-            React.DOM.ul({className: availabilityDropdown, 
-                onMouseLeave: this.hideDropdownMenu}, 
-              React.DOM.li({onClick: this.changeAvailability("available"), 
-                  className: "dnd-menu-item dnd-make-available"}, 
-                React.DOM.i({className: "status status-available"}), 
-                React.DOM.span(null, __("display_name_available_status"))
-              ), 
-              React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), 
-                  className: "dnd-menu-item dnd-make-unavailable"}, 
-                React.DOM.i({className: "status status-dnd"}), 
-                React.DOM.span(null, __("display_name_dnd_status"))
-              )
+            React.DOM.li({onClick: this.changeAvailability("do-not-disturb"), 
+                className: "dnd-menu-item dnd-make-unavailable"}, 
+              React.DOM.i({className: "status status-dnd"}), 
+              React.DOM.span(null, __("display_name_dnd_status"))
             )
           )
         )
       );
     }
   });
 
   var ToSView = React.createClass({displayName: 'ToSView',
@@ -267,24 +265,33 @@ loop.panel = (function(_, mozL10n) {
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
     render: function() {
       return (
         React.DOM.div(null, 
           CallUrlResult({client: this.props.client, 
                          notifier: this.props.notifier, 
                          callUrl: this.props.callUrl}), 
           ToSView(null), 
-          AvailabilityDropdown(null)
+          React.DOM.div({className: "footer"}, 
+            AvailabilityDropdown(null), 
+            React.DOM.a({className: "signin-link", href: "#", onClick: this.handleSignUpLinkClick}, 
+              __("panel_footer_signin_or_signup_link")
+            )
+          )
         )
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
      * DOM document object.
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -72,36 +72,34 @@ loop.panel = (function(_, mozL10n) {
         'dnd-menu': true,
         'hide': !this.state.showMenu
       });
       var availabilityText = this.state.doNotDisturb ?
                               __("display_name_dnd_status") :
                               __("display_name_available_status");
 
       return (
-        <div className="footer">
-          <div className="do-not-disturb">
-            <div className="dnd-status" onClick={this.showDropdownMenu}>
-              <span>{availabilityText}</span>
-              <i className={availabilityStatus}></i>
-            </div>
-            <ul className={availabilityDropdown}
-                onMouseLeave={this.hideDropdownMenu}>
-              <li onClick={this.changeAvailability("available")}
-                  className="dnd-menu-item dnd-make-available">
-                <i className="status status-available"></i>
-                <span>{__("display_name_available_status")}</span>
-              </li>
-              <li onClick={this.changeAvailability("do-not-disturb")}
-                  className="dnd-menu-item dnd-make-unavailable">
-                <i className="status status-dnd"></i>
-                <span>{__("display_name_dnd_status")}</span>
-              </li>
-            </ul>
-          </div>
+        <div className="do-not-disturb">
+          <p className="dnd-status" onClick={this.showDropdownMenu}>
+            <span>{availabilityText}</span>
+            <i className={availabilityStatus}></i>
+          </p>
+          <ul className={availabilityDropdown}
+              onMouseLeave={this.hideDropdownMenu}>
+            <li onClick={this.changeAvailability("available")}
+                className="dnd-menu-item dnd-make-available">
+              <i className="status status-available"></i>
+              <span>{__("display_name_available_status")}</span>
+            </li>
+            <li onClick={this.changeAvailability("do-not-disturb")}
+                className="dnd-menu-item dnd-make-unavailable">
+              <i className="status status-dnd"></i>
+              <span>{__("display_name_dnd_status")}</span>
+            </li>
+          </ul>
         </div>
       );
     }
   });
 
   var ToSView = React.createClass({
     getInitialState: function() {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
@@ -267,24 +265,33 @@ loop.panel = (function(_, mozL10n) {
   var PanelView = React.createClass({
     propTypes: {
       notifier: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string
     },
 
+    handleSignUpLinkClick: function() {
+      navigator.mozLoop.logInToFxA();
+    },
+
     render: function() {
       return (
         <div>
           <CallUrlResult client={this.props.client}
                          notifier={this.props.notifier}
                          callUrl={this.props.callUrl} />
           <ToSView />
-          <AvailabilityDropdown />
+          <div className="footer">
+            <AvailabilityDropdown />
+            <a className="signin-link" href="#" onClick={this.handleSignUpLinkClick}>
+              {__("panel_footer_signin_or_signup_link")}
+            </a>
+          </div>
         </div>
       );
     }
   });
 
   var PanelRouter = loop.desktopRouter.DesktopRouter.extend({
     /**
      * DOM document object.
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -133,16 +133,27 @@
 .status-available {
   background: #6cb23e;
 }
 
 .status-dnd {
   border: 1px solid #888;
 }
 
+/* Sign in/up link */
+
+.signin-link {
+  display: none; /* XXX This should be removed as soon bugs 1047144 & 979845 land */
+  line-height: 100%;
+  font-size: .9em;
+  text-decoration: none;
+  color: #888;
+  margin-top: 16px;
+}
+
 /* Terms of Service */
 
 .terms-service {
   padding: 3px 10px 10px;
   background: #FFF;
   text-align: center;
   opacity: .5;
   transition: opacity .3s;
@@ -152,19 +163,22 @@
 
 .terms-service a {
   color: #0095dd;
 }
 
 /* Footer */
 
 .footer {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  justify-content: space-between;
+  align-content: stretch;
+  align-items: flex-start;
   font-size: 1em;
   border-top: 1px solid #D1D1D1;
   background: #EAEAEA;
   color: #7F7F7F;
-  display: flex;
-  align-items: center;
+  padding: 14px;
   margin-top: 14px;
-  flex-direction: row;
-  padding: 14px;
 }
 
--- a/browser/components/loop/content/shared/js/models.js
+++ b/browser/components/loop/content/shared/js/models.js
@@ -29,18 +29,22 @@ loop.shared.models = (function() {
       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")
-      callToken:    undefined      // Incoming call token.
+      callToken:    undefined,     // Incoming call token.
                                    // Used for blocking a call url
+      subscribedStream: false,     // Used to indicate that a stream has been
+                                   // subscribed to
+      publishedStream: false       // Used to indicate that a stream has been
+                                   // published
     },
 
     /**
      * SDK object.
      * @type {OT}
      */
     sdk: undefined,
 
@@ -215,16 +219,49 @@ loop.shared.models = (function() {
       }
       if (callType === "outgoing") {
         return this.get("selectedCallType") === "audio-video";
       }
       return undefined;
     },
 
     /**
+     * Publishes a local stream.
+     *
+     * @param {Publisher} publisher The publisher object to publish
+     *                              to the session.
+     */
+    publish: function(publisher) {
+      this.session.publish(publisher);
+      this.set("publishedStream", true);
+    },
+
+    /**
+     * Subscribes to a remote stream.
+     *
+     * @param {Stream} stream The remote stream to subscribe to.
+     * @param {DOMElement} element The element to display the stream in.
+     * @param {Object} config The display properties to set on the stream as
+     *                        documented in:
+     * https://tokbox.com/opentok/libraries/client/js/reference/Session.html#subscribe
+     */
+    subscribe: function(stream, element, config) {
+      this.session.subscribe(stream, element, config);
+      this.set("subscribedStream", true);
+    },
+
+    /**
+     * Returns true if a stream has been published and a stream has been
+     * subscribed to.
+     */
+    streamsConnected: function() {
+      return this.get("publishedStream") && this.get("subscribedStream");
+    },
+
+    /**
      * Handle a loop-server error, which has an optional `errno` property which
      * is server error identifier.
      *
      * Triggers the following events:
      *
      * - `session:expired` for expired call urls
      * - `session:error` for other generic errors
      *
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -273,23 +273,17 @@ loop.shared.views = (function(_, OT, l10
      *      element.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      *
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
-      event.streams.forEach(function(stream) {
-        if (stream.connection.connectionId !==
-            this.props.model.session.connection.connectionId) {
-          this.props.model.session.subscribe(stream, incoming,
-                                             this.publisherConfig);
-        }
-      }, this);
+      this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
     },
 
     /**
      * Publishes remote streams available once a session is connected.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param  {SessionConnectEvent} event
@@ -316,17 +310,17 @@ loop.shared.views = (function(_, OT, l10
 
       this.listenTo(this.publisher, "streamDestroyed", function() {
         this.setState({
           audio: {enabled: false},
           video: {enabled: false}
         });
       }.bind(this));
 
-      this.props.model.session.publish(this.publisher);
+      this.props.model.publish(this.publisher);
     },
 
     /**
      * Toggles streaming status for a given stream type.
      *
      * @param  {String}  type     Stream type ("audio" or "video").
      * @param  {Boolean} enabled  Enabled stream flag.
      */
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -273,23 +273,17 @@ loop.shared.views = (function(_, OT, l10
      *      element.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html
      *
      * @param  {StreamEvent} event
      */
     _streamCreated: function(event) {
       var incoming = this.getDOMNode().querySelector(".remote");
-      event.streams.forEach(function(stream) {
-        if (stream.connection.connectionId !==
-            this.props.model.session.connection.connectionId) {
-          this.props.model.session.subscribe(stream, incoming,
-                                             this.publisherConfig);
-        }
-      }, this);
+      this.props.model.subscribe(event.stream, incoming, this.publisherConfig);
     },
 
     /**
      * Publishes remote streams available once a session is connected.
      *
      * http://tokbox.com/opentok/libraries/client/js/reference/SessionConnectEvent.html
      *
      * @param  {SessionConnectEvent} event
@@ -316,17 +310,17 @@ loop.shared.views = (function(_, OT, l10
 
       this.listenTo(this.publisher, "streamDestroyed", function() {
         this.setState({
           audio: {enabled: false},
           video: {enabled: false}
         });
       }.bind(this));
 
-      this.props.model.session.publish(this.publisher);
+      this.props.model.publish(this.publisher);
     },
 
     /**
      * Toggles streaming status for a given stream type.
      *
      * @param  {String}  type     Stream type ("audio" or "video").
      * @param  {Boolean} enabled  Enabled stream flag.
      */
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -123,16 +123,37 @@ loop.CallConnectionWebSocket = (function
       this._send({
         messageType: "action",
         event: "terminate",
         reason: "reject"
       });
     },
 
     /**
+     * Notifies the server that the user has accepted the call.
+     */
+    accept: function() {
+      this._send({
+        messageType: "action",
+        event: "accept"
+      });
+    },
+
+    /**
+     * Notifies the server that the outgoing media is up, and the
+     * incoming media is being received.
+     */
+    mediaUp: function() {
+      this._send({
+        messageType: "action",
+        event: "media-up"
+      });
+    },
+
+    /**
      * 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));
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -406,16 +406,28 @@ loop.webapp = (function($, _, OT, webL10
         this._notifier.errorL10n("cannot_start_call_session_not_ready");
         return;
       }.bind(this));
 
       this._websocket.on("progress", this._handleWebSocketProgress, this);
     },
 
     /**
+     * Checks if the streams have been connected, and notifies the
+     * websocket that the media is now connected.
+     */
+    _checkConnected: function() {
+      // Check we've had both local and remote streams connected before
+      // sending the media up message.
+      if (this._conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
      * 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
@@ -490,16 +502,18 @@ loop.webapp = (function($, _, OT, webL10
       this._conversation.set("loopToken", loopToken);
 
       var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
         client: this._client
       });
       this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this._conversation.once("change:publishedStream", this._checkConnected, this);
+      this._conversation.once("change:subscribedStream", this._checkConnected, this);
       this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -406,16 +406,28 @@ loop.webapp = (function($, _, OT, webL10
         this._notifier.errorL10n("cannot_start_call_session_not_ready");
         return;
       }.bind(this));
 
       this._websocket.on("progress", this._handleWebSocketProgress, this);
     },
 
     /**
+     * Checks if the streams have been connected, and notifies the
+     * websocket that the media is now connected.
+     */
+    _checkConnected: function() {
+      // Check we've had both local and remote streams connected before
+      // sending the media up message.
+      if (this._conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
      * 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
@@ -490,16 +502,18 @@ loop.webapp = (function($, _, OT, webL10
       this._conversation.set("loopToken", loopToken);
 
       var startView = StartConversationView({
         model: this._conversation,
         notifier: this._notifier,
         client: this._client
       });
       this._conversation.once("call:outgoing:setup", this.setupOutgoingCall, this);
+      this._conversation.once("change:publishedStream", this._checkConnected, this);
+      this._conversation.once("change:subscribedStream", this._checkConnected, this);
       this.loadReactComponent(startView);
     },
 
     /**
      * Loads conversation establishment view.
      *
      */
     loadConversation: function(loopToken) {
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -32,16 +32,17 @@ describe("loop.conversation", function()
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
       },
       setLoopCharPref: sandbox.stub(),
       getLoopCharPref: sandbox.stub(),
+      getLoopBoolPref: sandbox.stub(),
       startAlerting: function() {},
       stopAlerting: function() {},
       ensureRegistered: function() {},
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
           platform: "test"
@@ -306,24 +307,45 @@ describe("loop.conversation", function()
                 done();
               });
             });
           });
         });
       });
 
       describe("#accept", function() {
+        beforeEach(function() {
+          conversation.setIncomingSessionData({
+            sessionId:      "sessionId",
+            sessionToken:   "sessionToken",
+            apiKey:         "apiKey",
+            callType:       "callType",
+            callId:         "Hello",
+            progressURL:    "http://progress.example.com",
+            websocketToken: 123
+          });
+          router._setupWebSocketAndCallView();
+
+          sandbox.stub(router._websocket, "accept");
+          sandbox.stub(navigator.mozLoop, "stopAlerting");
+        });
+
         it("should initiate the conversation", function() {
           router.accept();
 
           sinon.assert.calledOnce(conversation.incoming);
         });
 
+        it("should notify the websocket of the user acceptance", function() {
+          router.accept();
+
+          sinon.assert.calledOnce(router._websocket.accept);
+        });
+
         it("should stop alerting", function() {
-          sandbox.stub(navigator.mozLoop, "stopAlerting");
           router.accept();
 
           sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
         });
       });
 
       describe("#conversation", function() {
         beforeEach(function() {
@@ -526,16 +548,59 @@ describe("loop.conversation", function()
 
       it("should navigate to call/feedback when network disconnects",
         function() {
           conversation.trigger("session:network-disconnected");
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/feedback");
         });
+
+      describe("Published and Subscribed Streams", function() {
+        beforeEach(function() {
+          router._websocket = {
+            mediaUp: sinon.spy()
+          };
+          router.incoming("fakeVersion");
+        });
+
+        describe("publishStream", function() {
+          it("should not notify the websocket if only one stream is up",
+            function() {
+              conversation.set("publishedStream", true);
+
+              sinon.assert.notCalled(router._websocket.mediaUp);
+            });
+
+          it("should notify the websocket that media is up if both streams" +
+             "are connected", function() {
+              conversation.set("subscribedStream", true);
+              conversation.set("publishedStream", true);
+
+              sinon.assert.calledOnce(router._websocket.mediaUp);
+            });
+        });
+
+        describe("subscribedStream", function() {
+          it("should not notify the websocket if only one stream is up",
+            function() {
+              conversation.set("subscribedStream", true);
+
+              sinon.assert.notCalled(router._websocket.mediaUp);
+            });
+
+          it("should notify the websocket that media is up if both streams" +
+             "are connected", function() {
+              conversation.set("publishedStream", true);
+              conversation.set("subscribedStream", true);
+
+              sinon.assert.calledOnce(router._websocket.mediaUp);
+            });
+        });
+      });
     });
   });
 
   describe("IncomingCallView", function() {
     var view, model;
 
     beforeEach(function() {
       var Model = Backbone.Model.extend({});
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -212,22 +212,33 @@ describe("loop.panel", function() {
       };
 
       view = TestUtils.renderIntoDocument(loop.panel.PanelView({
         notifier: notifier,
         client: fakeClient
       }));
     });
 
+    describe("FxA sign in/up link", function() {
+      it("should trigger the FxA sign in/up process when clicking the link",
+        function() {
+          navigator.mozLoop.logInToFxA = sandbox.stub();
+
+          TestUtils.Simulate.click(
+            view.getDOMNode().querySelector(".signin-link"));
+
+          sinon.assert.calledOnce(navigator.mozLoop.logInToFxA);
+        });
+      });
+
     describe("#render", function() {
       it("should render a ToSView", function() {
         TestUtils.findRenderedComponentWithType(view, loop.panel.ToSView);
       });
     });
-
   });
 
   describe("loop.panel.CallUrlResult", function() {
     var fakeClient, callUrlData, view;
 
     beforeEach(function() {
       callUrlData = {
         callUrl: "http://call.invalid/fakeToken",
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -352,25 +352,24 @@ describe("loop.shared.views", function()
 
       describe("Model events", function() {
         it("should start streaming on session:connected", function() {
           model.trigger("session:connected");
 
           sinon.assert.calledOnce(fakeSDK.initPublisher);
         });
 
-        it("should publish remote streams on session:stream-created",
+        it("should publish remote stream on session:stream-created",
           function() {
             var s1 = {connection: {connectionId: 42}};
-            var s2 = {connection: {connectionId: 43}};
 
-            model.trigger("session:stream-created", {streams: [s1, s2]});
+            model.trigger("session:stream-created", {stream: s1});
 
             sinon.assert.calledOnce(fakeSession.subscribe);
-            sinon.assert.calledWith(fakeSession.subscribe, s2);
+            sinon.assert.calledWith(fakeSession.subscribe, s1);
           });
 
         it("should unpublish local stream on session:ended", function() {
           comp.startPublishing();
 
           model.trigger("session:ended");
 
           sinon.assert.calledOnce(fakeSession.unpublish);
--- a/browser/components/loop/test/shared/websocket_test.js
+++ b/browser/components/loop/test/shared/websocket_test.js
@@ -143,16 +143,44 @@ describe("loop.CallConnectionWebSocket",
         sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
           messageType: "action",
           event: "terminate",
           reason: "reject"
         }));
       });
     });
 
+    describe("#accept", function() {
+      it("should send an accept message to the server", function() {
+        callWebSocket.promiseConnect();
+
+        callWebSocket.accept();
+
+        sinon.assert.calledOnce(dummySocket.send);
+        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+          messageType: "action",
+          event: "accept"
+        }));
+      });
+    });
+
+    describe("#mediaUp", function() {
+      it("should send a media-up message to the server", function() {
+        callWebSocket.promiseConnect();
+
+        callWebSocket.mediaUp();
+
+        sinon.assert.calledOnce(dummySocket.send);
+        sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
+          messageType: "action",
+          event: "media-up"
+        }));
+      });
+    });
+
     describe("Events", function() {
       beforeEach(function() {
         sandbox.stub(callWebSocket, "trigger");
 
         callWebSocket.promiseConnect();
       });
 
       describe("Progress", function() {
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -410,16 +410,59 @@ describe("loop.webapp", function() {
       it("should navigate to call/{token} when network disconnects",
         function() {
           conversation.trigger("session:network-disconnected");
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWithMatch(router.navigate, "call/fakeToken");
         });
 
+      describe("Published and Subscribed Streams", function() {
+        beforeEach(function() {
+          router._websocket = {
+            mediaUp: sinon.spy()
+          };
+          router.initiate();
+        });
+
+        describe("publishStream", function() {
+          it("should not notify the websocket if only one stream is up",
+            function() {
+              conversation.set("publishedStream", true);
+
+              sinon.assert.notCalled(router._websocket.mediaUp);
+            });
+
+          it("should notify the websocket that media is up if both streams" +
+             "are connected", function() {
+              conversation.set("subscribedStream", true);
+              conversation.set("publishedStream", true);
+
+              sinon.assert.calledOnce(router._websocket.mediaUp);
+            });
+        });
+
+        describe("subscribedStream", function() {
+          it("should not notify the websocket if only one stream is up",
+            function() {
+              conversation.set("subscribedStream", true);
+
+              sinon.assert.notCalled(router._websocket.mediaUp);
+            });
+
+          it("should notify the websocket that media is up if both streams" +
+             "are connected", function() {
+              conversation.set("publishedStream", true);
+              conversation.set("subscribedStream", true);
+
+              sinon.assert.calledOnce(router._websocket.mediaUp);
+            });
+        });
+      });
+
       describe("#setupOutgoingCall", function() {
         beforeEach(function() {
           router.initiate();
         });
 
         describe("No loop token", function() {
           it("should navigate to home", function() {
             conversation.setupOutgoingCall();
--- a/browser/devtools/shared/DeveloperToolbar.jsm
+++ b/browser/devtools/shared/DeveloperToolbar.jsm
@@ -161,17 +161,19 @@ let CommandUtils = {
                 updateChecked(reply);
               }
             }
           };
 
           command.state.onChange(target, onChange);
           onChange("", { target: target });
           document.defaultView.addEventListener("unload", () => {
-            command.state.offChange(target, onChange);
+            if (command.state.offChange) {
+              command.state.offChange(target, onChange);
+            }
           }, false);
         }
 
         requisition.clear();
 
         return button;
       });
     });
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -63,8 +63,10 @@ feedback_window_will_close_in=This windo
 
 share_email_subject2=Invitation to chat
 ## LOCALIZATION NOTE (share_email_body2): In this item, don't translate the
 ## part between {{..}} and leave the \r\n\r\n part alone
 share_email_body2=Please click this link to call me:\r\n\r\n{{callUrl}}
 share_button=Email
 copy_url_button=Copy
 copied_url_button=Copied!
+
+panel_footer_signin_or_signup_link=Sign In or Sign Up
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1787,16 +1787,31 @@ toolbarbutton[sdk-button="true"][cui-are
 
 @media not all and (-moz-mac-lion-theme) {
   #urlbar:-moz-window-inactive,
   .searchbar-textbox:-moz-window-inactive {
     border-color: @toolbarbuttonInactiveBorderColor@;
   }
 }
 
+@media (-moz-mac-yosemite-theme) {
+  .searchbar-textbox,
+  #urlbar {
+    border-color: #fff;
+    border-radius: 3px;
+    box-shadow: 0 1px 0 0 #aeaeae, 1px 2px 0 0 #d8d8d8;
+    background-image: none;
+  }
+  .searchbar-textbox:-moz-window-inactive,
+  #urlbar:-moz-window-inactive {
+    box-shadow: none;
+    border-color: #dbdbdb;
+  }
+}
+
 #urlbar[focused="true"],
 .searchbar-textbox[focused="true"] {
   border-color: -moz-mac-focusring;
   box-shadow: @focusRingShadow@;
 }
 
 #urlbar-container {
   -moz-box-align: center;
--- a/config/android-common.mk
+++ b/config/android-common.mk
@@ -14,19 +14,19 @@ DEBUG_JARSIGNER=$(PYTHON) $(abspath $(to
   --jarsigner=$(JARSIGNER) \
   $(NULL)
 
 # For Android, this defaults to $(ANDROID_SDK)/android.jar
 ifndef JAVA_BOOTCLASSPATH
   JAVA_BOOTCLASSPATH = $(ANDROID_SDK)/android.jar
 endif
 
-# For Android, we default to 1.5
+# For Android, we default to 1.7
 ifndef JAVA_VERSION
-  JAVA_VERSION = 1.5
+  JAVA_VERSION = 1.7
 endif
 
 JAVAC_FLAGS = \
   -target $(JAVA_VERSION) \
   -source $(JAVA_VERSION) \
   $(if $(JAVA_CLASSPATH),-classpath $(JAVA_CLASSPATH),) \
   -bootclasspath $(JAVA_BOOTCLASSPATH) \
   -encoding UTF8 \
--- a/configure.in
+++ b/configure.in
@@ -5617,16 +5617,34 @@ if test -n "${JAVA_BIN_PATH}" -o \
     AC_MSG_ERROR([The program jar was not found.  Set \$JAVA_HOME to your Java SDK directory or use --with-java-bin-path={java-bin-dir}])
   fi
   if test -z "$JARSIGNER" -o "$JARSIGNER" = ":"; then
     AC_MSG_ERROR([The program jarsigner was not found.  Set \$JAVA_HOME to your Java SDK directory or use --with-java-bin-path={java-bin-dir}])
   fi
   if test -z "$KEYTOOL" -o "$KEYTOOL" = ":"; then
     AC_MSG_ERROR([The program keytool was not found.  Set \$JAVA_HOME to your Java SDK directory or use --with-java-bin-path={java-bin-dir}])
   fi
+
+  AC_MSG_CHECKING([for minimum required javac version = 1.7])
+
+  dnl Javac spits out something like `javac 1.7.0`. This line cuts off the 'javac'
+  _javac_version=$($JAVAC -version 2>&1 | cut -d ' ' -f 2)
+
+  dnl Here, we extract the major (1) and minor (7) version numbers from the
+  dnl acquired version string.
+  _javac_major_version=$(echo $_javac_version | cut -d '.' -f 1)
+  _javac_minor_version=$(echo $_javac_version | cut -d '.' -f 2)
+
+  AC_MSG_RESULT([$_javac_version])
+
+  dnl Fail if we have a version other than 1.7.X
+  if test "$_javac_major_version" -ne "1" -o \
+      \( "$_javac_minor_version" -ne "7" \); then
+      AC_MSG_ERROR([javac 1.7 is required.])
+  fi
 fi
 
 dnl ========================================================
 dnl = ANGLE OpenGL->D3D translator for WebGL
 dnl = * only applies to win32
 dnl ========================================================
 
 MOZ_ANGLE_RENDERER=
--- a/dom/system/OSFileConstants.cpp
+++ b/dom/system/OSFileConstants.cpp
@@ -133,16 +133,20 @@ struct Paths {
    * The user's Library directory.
    */
   nsString macUserLibDir;
   /**
    * The Application directory, that stores applications installed in the
    * system.
    */
   nsString macLocalApplicationsDir;
+  /**
+   * The user's trash directory.
+   */
+  nsString macTrashDir;
 #endif // defined(XP_MACOSX)
 
   Paths()
   {
     libDir.SetIsVoid(true);
     tmpDir.SetIsVoid(true);
     profileDir.SetIsVoid(true);
     localProfileDir.SetIsVoid(true);
@@ -153,16 +157,17 @@ struct Paths {
 #if defined(XP_WIN)
     winAppDataDir.SetIsVoid(true);
     winStartMenuProgsDir.SetIsVoid(true);
 #endif // defined(XP_WIN)
 
 #if defined(XP_MACOSX)
     macUserLibDir.SetIsVoid(true);
     macLocalApplicationsDir.SetIsVoid(true);
+    macTrashDir.SetIsVoid(true);
 #endif // defined(XP_MACOSX)
   }
 };
 
 /**
  * System directories.
  */
 Paths* gPaths = nullptr;
@@ -301,16 +306,17 @@ nsresult InitOSFileConstants()
 #if defined(XP_WIN)
   GetPathToSpecialDir(NS_WIN_APPDATA_DIR, paths->winAppDataDir);
   GetPathToSpecialDir(NS_WIN_PROGRAMS_DIR, paths->winStartMenuProgsDir);
 #endif // defined(XP_WIN)
 
 #if defined(XP_MACOSX)
   GetPathToSpecialDir(NS_MAC_USER_LIB_DIR, paths->macUserLibDir);
   GetPathToSpecialDir(NS_OSX_LOCAL_APPLICATIONS_DIR, paths->macLocalApplicationsDir);
+  GetPathToSpecialDir(NS_MAC_TRASH_DIR, paths->macTrashDir);
 #endif // defined(XP_MACOSX)
 
   gPaths = paths.forget();
 
   // Get the umask from the system-info service.
   // The property will always be present, but it will be zero on
   // non-Unix systems.
   nsCOMPtr<nsIPropertyBag2> infoService =
@@ -975,16 +981,20 @@ bool DefineOSFileConstants(JSContext *cx
 #if defined(XP_MACOSX)
   if (!SetStringProperty(cx, objPath, "macUserLibDir", gPaths->macUserLibDir)) {
     return false;
   }
 
   if (!SetStringProperty(cx, objPath, "macLocalApplicationsDir", gPaths->macLocalApplicationsDir)) {
     return false;
   }
+
+  if (!SetStringProperty(cx, objPath, "macTrashDir", gPaths->macTrashDir)) {
+    return false;
+  }
 #endif // defined(XP_MACOSX)
 
   // sqlite3 is linked from different places depending on the platform
   nsAutoString libsqlite3;
 #if defined(ANDROID)
   // On Android, we use the system's libsqlite3
   libsqlite3.AppendLiteral(DLL_PREFIX);
   libsqlite3.AppendLiteral("sqlite3");
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -489,16 +489,20 @@ Tester.prototype = {
           SocialFlyout.unload();
           SocialShare.uninit();
           TabView.uninit();
 
           // Destroying ContentSearch is asynchronous.
           promise = ContentSearch.destroy();
         }
 
+        // Simulate memory pressure so that we're forced to free more resources
+        // and thus get rid of more false leaks like already terminated workers.
+        Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
+
         // Schedule GC and CC runs before finishing in order to detect
         // DOM windows leaked by our tests or the tested code. Note that we
         // use a shrinking GC so that the JS engine will discard JIT code and
         // JIT caches more aggressively.
 
         let checkForLeakedGlobalWindows = aCallback => {
           Cu.schedulePreciseShrinkingGC(() => {
             let analyzer = new CCAnalyzer();
--- a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
+++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
@@ -68,15 +68,16 @@ add_task(function* test_desktop_paths() 
   compare_paths(OS.Constants.Path.desktopDir, "Desk");
   compare_paths(OS.Constants.Path.userApplicationDataDir, "UAppData");
 
   compare_paths(OS.Constants.Path.winAppDataDir, "AppData");
   compare_paths(OS.Constants.Path.winStartMenuProgsDir, "Progs");
 
   compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir");
   compare_paths(OS.Constants.Path.macLocalApplicationsDir, "LocApp");
+  compare_paths(OS.Constants.Path.macTrashDir, "Trsh");
 });
 
 // Open libxul
 add_task(function* test_libxul() {
   ctypes.open(OS.Constants.Path.libxul);
   do_print("Linked to libxul");
 });