Merge fx-team to m-c a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 05 Jan 2015 17:08:49 -0800
changeset 247915 2a193b7f395c8e6f3c21e83777ce2f540e4c04fe
parent 247905 fd223e4af53dd3fa313a7bc04a869a95374f689a (current diff)
parent 247914 724554c093a8e1c621ad3920bdc00c9a3494066e (diff)
child 247935 6056958e94946d24a5024ac53d88bdec93fbd70d
child 247990 337faa3baf86d32dece801d3feb57c4649c9cb00
child 248054 77c0488fa25d712dfc3f0d1fb9161e07cf2a2693
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
2a193b7f395c / 37.0a1 / 20150106030201 / files
nightly linux64
2a193b7f395c / 37.0a1 / 20150106030201 / files
nightly mac
2a193b7f395c / 37.0a1 / 20150106030201 / files
nightly win32
2a193b7f395c / 37.0a1 / 20150106030201 / files
nightly win64
2a193b7f395c / 37.0a1 / 20150106030201 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c a=merge
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -11,531 +11,21 @@ var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
+  var IncomingConversationView = loop.conversationViews.IncomingConversationView;
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
-
-  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
-  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
-
-  var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
-    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
-
-    propTypes: {
-      model: React.PropTypes.object.isRequired,
-      video: React.PropTypes.bool.isRequired
-    },
-
-    getDefaultProps: function() {
-      return {
-        showMenu: false,
-        video: true
-      };
-    },
-
-    clickHandler: function(e) {
-      var target = e.target;
-      if (!target.classList.contains('btn-chevron')) {
-        this._hideDeclineMenu();
-      }
-    },
-
-    _handleAccept: function(callType) {
-      return function() {
-        this.props.model.set("selectedCallType", callType);
-        this.props.model.trigger("accept");
-      }.bind(this);
-    },
-
-    _handleDecline: function() {
-      this.props.model.trigger("decline");
-    },
-
-    _handleDeclineBlock: function(e) {
-      this.props.model.trigger("declineAndBlock");
-      /* Prevent event propagation
-       * stop the click from reaching parent element */
-      return false;
-    },
-
-    /*
-     * Generate props for <AcceptCallButton> component based on
-     * incoming call type. An incoming video call will render a video
-     * answer button primarily, an audio call will flip them.
-     **/
-    _answerModeProps: function() {
-      var videoButton = {
-        handler: this._handleAccept("audio-video"),
-        className: "fx-embedded-btn-icon-video",
-        tooltip: "incoming_call_accept_audio_video_tooltip"
-      };
-      var audioButton = {
-        handler: this._handleAccept("audio"),
-        className: "fx-embedded-btn-audio-small",
-        tooltip: "incoming_call_accept_audio_only_tooltip"
-      };
-      var props = {};
-      props.primary = videoButton;
-      props.secondary = audioButton;
-
-      // When video is not enabled on this call, we swap the buttons around.
-      if (!this.props.video) {
-        audioButton.className = "fx-embedded-btn-icon-audio";
-        videoButton.className = "fx-embedded-btn-video-small";
-        props.primary = audioButton;
-        props.secondary = videoButton;
-      }
-
-      return props;
-    },
-
-    render: function() {
-      /* jshint ignore:start */
-      var dropdownMenuClassesDecline = React.addons.classSet({
-        "native-dropdown-menu": true,
-        "conversation-window-dropdown": true,
-        "visually-hidden": !this.state.showMenu
-      });
-
-      return (
-        React.DOM.div({className: "call-window"}, 
-          CallIdentifierView({video: this.props.video, 
-            peerIdentifier: this.props.model.getCallIdentifier(), 
-            urlCreationDate: this.props.model.get("urlCreationDate"), 
-            showIcons: true}), 
-
-          React.DOM.div({className: "btn-group call-action-group"}, 
-
-            React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
-
-            React.DOM.div({className: "btn-chevron-menu-group"}, 
-              React.DOM.div({className: "btn-group-chevron"}, 
-                React.DOM.div({className: "btn-group"}, 
-
-                  React.DOM.button({className: "btn btn-decline", 
-                          onClick: this._handleDecline}, 
-                    mozL10n.get("incoming_call_cancel_button")
-                  ), 
-                  React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
-                ), 
-
-                React.DOM.ul({className: dropdownMenuClassesDecline}, 
-                  React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, 
-                    mozL10n.get("incoming_call_cancel_and_block_button")
-                  )
-                )
-
-              )
-            ), 
-
-            React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
-
-            AcceptCallButton({mode: this._answerModeProps()}), 
-
-            React.DOM.div({className: "fx-embedded-call-button-spacer"})
-
-          )
-        )
-      );
-      /* jshint ignore:end */
-    }
-  });
-
-  /**
-   * Incoming call view accept button, renders different primary actions
-   * (answer with video / with audio only) based on the props received
-   **/
-  var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
-
-    propTypes: {
-      mode: React.PropTypes.object.isRequired,
-    },
-
-    render: function() {
-      var mode = this.props.mode;
-      return (
-        /* jshint ignore:start */
-        React.DOM.div({className: "btn-chevron-menu-group"}, 
-          React.DOM.div({className: "btn-group"}, 
-            React.DOM.button({className: "btn btn-accept", 
-                    onClick: mode.primary.handler, 
-                    title: mozL10n.get(mode.primary.tooltip)}, 
-              React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
-                mozL10n.get("incoming_call_accept_button")
-              ), 
-              React.DOM.span({className: mode.primary.className})
-            ), 
-            React.DOM.div({className: mode.secondary.className, 
-                 onClick: mode.secondary.handler, 
-                 title: mozL10n.get(mode.secondary.tooltip)}
-            )
-          )
-        )
-        /* jshint ignore:end */
-      );
-    }
-  });
-
-  /**
-   * Something went wrong view. Displayed when there's a big problem.
-   *
-   * XXX Based on CallFailedView, but built specially until we flux-ify the
-   * incoming call views (bug 1088672).
-   */
-  var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
-    mixins: [sharedMixins.AudioMixin],
-
-    propTypes: {
-      cancelCall: React.PropTypes.func.isRequired
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-    },
-
-    render: function() {
-      document.title = mozL10n.get("generic_failure_title");
-
-      return (
-        React.DOM.div({className: "call-window"}, 
-          React.DOM.h2(null, mozL10n.get("generic_failure_title")), 
-
-          React.DOM.div({className: "btn-group call-action-group"}, 
-            React.DOM.button({className: "btn btn-cancel", 
-                    onClick: this.props.cancelCall}, 
-              mozL10n.get("cancel_button")
-            )
-          )
-        )
-      );
-    }
-  });
-
-  /**
-   * This view manages the incoming conversation views - from
-   * call initiation through to the actual conversation and call end.
-   *
-   * At the moment, it does more than that, these parts need refactoring out.
-   */
-  var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
-    mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
-
-    propTypes: {
-      client: React.PropTypes.instanceOf(loop.Client).isRequired,
-      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
-                         .isRequired,
-      sdk: React.PropTypes.object.isRequired,
-      conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
-    },
-
-    getInitialState: function() {
-      return {
-        callFailed: false, // XXX this should be removed when bug 1047410 lands.
-        callStatus: "start"
-      };
-    },
-
-    componentDidMount: function() {
-      this.props.conversation.on("accept", this.accept, this);
-      this.props.conversation.on("decline", this.decline, this);
-      this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
-      this.props.conversation.on("call:accepted", this.accepted, this);
-      this.props.conversation.on("change:publishedStream", this._checkConnected, this);
-      this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
-      this.props.conversation.on("session:ended", this.endCall, this);
-      this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
-      this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
-      this.props.conversation.on("session:connection-error", this._notifyError, this);
-
-      this.setupIncomingCall();
-    },
-
-    componentDidUnmount: function() {
-      this.props.conversation.off(null, null, this);
-    },
-
-    render: function() {
-      switch (this.state.callStatus) {
-        case "start": {
-          document.title = mozL10n.get("incoming_call_title2");
-
-          // XXX Don't render anything initially, though this should probably
-          // be some sort of pending view, whilst we connect the websocket.
-          return null;
-        }
-        case "incoming": {
-          document.title = mozL10n.get("incoming_call_title2");
-
-          return (
-            IncomingCallView({
-              model: this.props.conversation, 
-              video: this.props.conversation.hasVideoStream("incoming")}
-            )
-          );
-        }
-        case "connected": {
-          document.title = this.props.conversation.getCallIdentifier();
-
-          var callType = this.props.conversation.get("selectedCallType");
-
-          return (
-            sharedViews.ConversationView({
-              initiate: true, 
-              sdk: this.props.sdk, 
-              model: this.props.conversation, 
-              video: {enabled: callType !== "audio"}}
-            )
-          );
-        }
-        case "end": {
-          // XXX To be handled with the "failed" view state when bug 1047410 lands
-          if (this.state.callFailed) {
-            return GenericFailureView({
-              cancelCall: this.closeWindow.bind(this)}
-            );
-          }
-
-          document.title = mozL10n.get("conversation_has_ended");
-
-          this.play("terminated");
-
-          return (
-            sharedViews.FeedbackView({
-              feedbackStore: this.props.feedbackStore, 
-              onAfterFeedbackReceived: this.closeWindow.bind(this)}
-            )
-          );
-        }
-        case "close": {
-          this.closeWindow();
-          return (React.DOM.div(null));
-        }
-      }
-    },
-
-    /**
-     * Notify the user that the connection was not possible
-     * @param {{code: number, message: string}} error
-     */
-    _notifyError: function(error) {
-      // XXX Not the ideal response, but bug 1047410 will be replacing
-      // this by better "call failed" UI.
-      console.error(error);
-      this.setState({callFailed: true, callStatus: "end"});
-    },
-
-    /**
-     * Peer hung up. Notifies the user and ends the call.
-     *
-     * Event properties:
-     * - {String} connectionId: OT session id
-     */
-    _onPeerHungup: function() {
-      this.setState({callFailed: false, callStatus: "end"});
-    },
-
-    /**
-     * Network disconnected. Notifies the user and ends the call.
-     */
-    _onNetworkDisconnected: function() {
-      // XXX Not the ideal response, but bug 1047410 will be replacing
-      // this by better "call failed" UI.
-      this.setState({callFailed: true, callStatus: "end"});
-    },
-
-    /**
-     * Incoming call route.
-     */
-    setupIncomingCall: function() {
-      navigator.mozLoop.startAlerting();
-
-      // XXX This is a hack until we rework for the flux model in bug 1088672.
-      var callData = this.props.conversationAppStore.getStoreState().windowData;
-
-      this.props.conversation.setIncomingSessionData(callData);
-      this._setupWebSocket();
-    },
-
-    /**
-     * Starts the actual conversation
-     */
-    accepted: function() {
-      this.setState({callStatus: "connected"});
-    },
-
-    /**
-     * Moves the call to the end state
-     */
-    endCall: function() {
-      navigator.mozLoop.calls.clearCallInProgress(
-        this.props.conversation.get("windowId"));
-      this.setState({callStatus: "end"});
-    },
-
-    /**
-     * Used to set up the web socket connection and navigate to the
-     * call view if appropriate.
-     */
-    _setupWebSocket: function() {
-      this._websocket = new loop.CallConnectionWebSocket({
-        url: this.props.conversation.get("progressURL"),
-        websocketToken: this.props.conversation.get("websocketToken"),
-        callId: this.props.conversation.get("callId"),
-      });
-      this._websocket.promiseConnect().then(function(progressStatus) {
-        this.setState({
-          callStatus: progressStatus === "terminated" ? "close" : "incoming"
-        });
-      }.bind(this), function() {
-        this._handleSessionError();
-        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.props.conversation.streamsConnected()) {
-        this._websocket.mediaUp();
-      }
-    },
-
-    /**
-     * Used to receive websocket progress and to determine how to handle
-     * it if appropraite.
-     * If we add more cases here, then we should refactor this function.
-     *
-     * @param {Object} progressData The progress data from the websocket.
-     * @param {String} previousState The previous state from the websocket.
-     */
-    _handleWebSocketProgress: function(progressData, previousState) {
-      // We only care about the terminated state at the moment.
-      if (progressData.state !== "terminated")
-        return;
-
-      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
-      // it here for now due to server-side issues that are being fixed in bug 1088351.
-      // This is before the abort call to ensure that it happens before the window is
-      // closed.
-      navigator.mozLoop.stopAlerting();
-
-      // If we hit any of the termination reasons, and the user hasn't accepted
-      // then it seems reasonable to close the window/abort the incoming call.
-      //
-      // If the user has accepted the call, and something's happened, display
-      // the call failed view.
-      //
-      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
-      if (previousState === "init" || previousState === "alerting") {
-        this._abortIncomingCall();
-      } else {
-        this.setState({callFailed: true, callStatus: "end"});
-      }
-
-    },
-
-    /**
-     * Silently aborts an incoming call - stops the alerting, and
-     * closes the websocket.
-     */
-    _abortIncomingCall: function() {
-      this._websocket.close();
-      // Having a timeout here lets the logging for the websocket complete and be
-      // displayed on the console if both are on.
-      setTimeout(this.closeWindow, 0);
-    },
-
-    /**
-     * Accepts an incoming call.
-     */
-    accept: function() {
-      navigator.mozLoop.stopAlerting();
-      this._websocket.accept();
-      this.props.conversation.accepted();
-    },
-
-    /**
-     * Declines a call and handles closing of the window.
-     */
-    _declineCall: function() {
-      this._websocket.decline();
-      navigator.mozLoop.calls.clearCallInProgress(
-        this.props.conversation.get("windowId"));
-      this._websocket.close();
-      // Having a timeout here lets the logging for the websocket complete and be
-      // displayed on the console if both are on.
-      setTimeout(this.closeWindow, 0);
-    },
-
-    /**
-     * Declines an incoming call.
-     */
-    decline: function() {
-      navigator.mozLoop.stopAlerting();
-      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
-     */
-    declineAndBlock: function() {
-      navigator.mozLoop.stopAlerting();
-      var token = this.props.conversation.get("callToken");
-      var callerId = this.props.conversation.get("callerId");
-
-      // If this is a direct call, we'll need to block the caller directly.
-      if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
-        navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
-          // 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 1103150).
-          console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
-        });
-      } else {
-        this.props.client.deleteCallUrl(token,
-          this.props.conversation.get("sessionType"),
-          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);
-          });
-      }
-
-      this._declineCall();
-    },
-
-    /**
-     * 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.
-      console.error("Failed initiating the call session.");
-    },
-  });
+  var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({displayName: 'AppControllerView',
     mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
 
@@ -706,16 +196,13 @@ loop.conversation = (function(mozL10n) {
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
-    IncomingConversationView: IncomingConversationView,
-    IncomingCallView: IncomingCallView,
-    GenericFailureView: GenericFailureView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -11,531 +11,21 @@ var loop = loop || {};
 loop.conversation = (function(mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
   var sharedActions = loop.shared.actions;
 
+  var IncomingConversationView = loop.conversationViews.IncomingConversationView;
   var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
   var CallIdentifierView = loop.conversationViews.CallIdentifierView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
-
-  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
-  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
-
-  var IncomingCallView = React.createClass({
-    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
-
-    propTypes: {
-      model: React.PropTypes.object.isRequired,
-      video: React.PropTypes.bool.isRequired
-    },
-
-    getDefaultProps: function() {
-      return {
-        showMenu: false,
-        video: true
-      };
-    },
-
-    clickHandler: function(e) {
-      var target = e.target;
-      if (!target.classList.contains('btn-chevron')) {
-        this._hideDeclineMenu();
-      }
-    },
-
-    _handleAccept: function(callType) {
-      return function() {
-        this.props.model.set("selectedCallType", callType);
-        this.props.model.trigger("accept");
-      }.bind(this);
-    },
-
-    _handleDecline: function() {
-      this.props.model.trigger("decline");
-    },
-
-    _handleDeclineBlock: function(e) {
-      this.props.model.trigger("declineAndBlock");
-      /* Prevent event propagation
-       * stop the click from reaching parent element */
-      return false;
-    },
-
-    /*
-     * Generate props for <AcceptCallButton> component based on
-     * incoming call type. An incoming video call will render a video
-     * answer button primarily, an audio call will flip them.
-     **/
-    _answerModeProps: function() {
-      var videoButton = {
-        handler: this._handleAccept("audio-video"),
-        className: "fx-embedded-btn-icon-video",
-        tooltip: "incoming_call_accept_audio_video_tooltip"
-      };
-      var audioButton = {
-        handler: this._handleAccept("audio"),
-        className: "fx-embedded-btn-audio-small",
-        tooltip: "incoming_call_accept_audio_only_tooltip"
-      };
-      var props = {};
-      props.primary = videoButton;
-      props.secondary = audioButton;
-
-      // When video is not enabled on this call, we swap the buttons around.
-      if (!this.props.video) {
-        audioButton.className = "fx-embedded-btn-icon-audio";
-        videoButton.className = "fx-embedded-btn-video-small";
-        props.primary = audioButton;
-        props.secondary = videoButton;
-      }
-
-      return props;
-    },
-
-    render: function() {
-      /* jshint ignore:start */
-      var dropdownMenuClassesDecline = React.addons.classSet({
-        "native-dropdown-menu": true,
-        "conversation-window-dropdown": true,
-        "visually-hidden": !this.state.showMenu
-      });
-
-      return (
-        <div className="call-window">
-          <CallIdentifierView video={this.props.video}
-            peerIdentifier={this.props.model.getCallIdentifier()}
-            urlCreationDate={this.props.model.get("urlCreationDate")}
-            showIcons={true} />
-
-          <div className="btn-group call-action-group">
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-            <div className="btn-chevron-menu-group">
-              <div className="btn-group-chevron">
-                <div className="btn-group">
-
-                  <button className="btn btn-decline"
-                          onClick={this._handleDecline}>
-                    {mozL10n.get("incoming_call_cancel_button")}
-                  </button>
-                  <div className="btn-chevron" onClick={this.toggleDropdownMenu} />
-                </div>
-
-                <ul className={dropdownMenuClassesDecline}>
-                  <li className="btn-block" onClick={this._handleDeclineBlock}>
-                    {mozL10n.get("incoming_call_cancel_and_block_button")}
-                  </li>
-                </ul>
-
-              </div>
-            </div>
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-            <AcceptCallButton mode={this._answerModeProps()} />
-
-            <div className="fx-embedded-call-button-spacer"></div>
-
-          </div>
-        </div>
-      );
-      /* jshint ignore:end */
-    }
-  });
-
-  /**
-   * Incoming call view accept button, renders different primary actions
-   * (answer with video / with audio only) based on the props received
-   **/
-  var AcceptCallButton = React.createClass({
-
-    propTypes: {
-      mode: React.PropTypes.object.isRequired,
-    },
-
-    render: function() {
-      var mode = this.props.mode;
-      return (
-        /* jshint ignore:start */
-        <div className="btn-chevron-menu-group">
-          <div className="btn-group">
-            <button className="btn btn-accept"
-                    onClick={mode.primary.handler}
-                    title={mozL10n.get(mode.primary.tooltip)}>
-              <span className="fx-embedded-answer-btn-text">
-                {mozL10n.get("incoming_call_accept_button")}
-              </span>
-              <span className={mode.primary.className}></span>
-            </button>
-            <div className={mode.secondary.className}
-                 onClick={mode.secondary.handler}
-                 title={mozL10n.get(mode.secondary.tooltip)}>
-            </div>
-          </div>
-        </div>
-        /* jshint ignore:end */
-      );
-    }
-  });
-
-  /**
-   * Something went wrong view. Displayed when there's a big problem.
-   *
-   * XXX Based on CallFailedView, but built specially until we flux-ify the
-   * incoming call views (bug 1088672).
-   */
-  var GenericFailureView = React.createClass({
-    mixins: [sharedMixins.AudioMixin],
-
-    propTypes: {
-      cancelCall: React.PropTypes.func.isRequired
-    },
-
-    componentDidMount: function() {
-      this.play("failure");
-    },
-
-    render: function() {
-      document.title = mozL10n.get("generic_failure_title");
-
-      return (
-        <div className="call-window">
-          <h2>{mozL10n.get("generic_failure_title")}</h2>
-
-          <div className="btn-group call-action-group">
-            <button className="btn btn-cancel"
-                    onClick={this.props.cancelCall}>
-              {mozL10n.get("cancel_button")}
-            </button>
-          </div>
-        </div>
-      );
-    }
-  });
-
-  /**
-   * This view manages the incoming conversation views - from
-   * call initiation through to the actual conversation and call end.
-   *
-   * At the moment, it does more than that, these parts need refactoring out.
-   */
-  var IncomingConversationView = React.createClass({
-    mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
-
-    propTypes: {
-      client: React.PropTypes.instanceOf(loop.Client).isRequired,
-      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
-                         .isRequired,
-      sdk: React.PropTypes.object.isRequired,
-      conversationAppStore: React.PropTypes.instanceOf(
-        loop.store.ConversationAppStore).isRequired,
-      feedbackStore:
-        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
-    },
-
-    getInitialState: function() {
-      return {
-        callFailed: false, // XXX this should be removed when bug 1047410 lands.
-        callStatus: "start"
-      };
-    },
-
-    componentDidMount: function() {
-      this.props.conversation.on("accept", this.accept, this);
-      this.props.conversation.on("decline", this.decline, this);
-      this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
-      this.props.conversation.on("call:accepted", this.accepted, this);
-      this.props.conversation.on("change:publishedStream", this._checkConnected, this);
-      this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
-      this.props.conversation.on("session:ended", this.endCall, this);
-      this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
-      this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
-      this.props.conversation.on("session:connection-error", this._notifyError, this);
-
-      this.setupIncomingCall();
-    },
-
-    componentDidUnmount: function() {
-      this.props.conversation.off(null, null, this);
-    },
-
-    render: function() {
-      switch (this.state.callStatus) {
-        case "start": {
-          document.title = mozL10n.get("incoming_call_title2");
-
-          // XXX Don't render anything initially, though this should probably
-          // be some sort of pending view, whilst we connect the websocket.
-          return null;
-        }
-        case "incoming": {
-          document.title = mozL10n.get("incoming_call_title2");
-
-          return (
-            <IncomingCallView
-              model={this.props.conversation}
-              video={this.props.conversation.hasVideoStream("incoming")}
-            />
-          );
-        }
-        case "connected": {
-          document.title = this.props.conversation.getCallIdentifier();
-
-          var callType = this.props.conversation.get("selectedCallType");
-
-          return (
-            <sharedViews.ConversationView
-              initiate={true}
-              sdk={this.props.sdk}
-              model={this.props.conversation}
-              video={{enabled: callType !== "audio"}}
-            />
-          );
-        }
-        case "end": {
-          // XXX To be handled with the "failed" view state when bug 1047410 lands
-          if (this.state.callFailed) {
-            return <GenericFailureView
-              cancelCall={this.closeWindow.bind(this)}
-            />;
-          }
-
-          document.title = mozL10n.get("conversation_has_ended");
-
-          this.play("terminated");
-
-          return (
-            <sharedViews.FeedbackView
-              feedbackStore={this.props.feedbackStore}
-              onAfterFeedbackReceived={this.closeWindow.bind(this)}
-            />
-          );
-        }
-        case "close": {
-          this.closeWindow();
-          return (<div/>);
-        }
-      }
-    },
-
-    /**
-     * Notify the user that the connection was not possible
-     * @param {{code: number, message: string}} error
-     */
-    _notifyError: function(error) {
-      // XXX Not the ideal response, but bug 1047410 will be replacing
-      // this by better "call failed" UI.
-      console.error(error);
-      this.setState({callFailed: true, callStatus: "end"});
-    },
-
-    /**
-     * Peer hung up. Notifies the user and ends the call.
-     *
-     * Event properties:
-     * - {String} connectionId: OT session id
-     */
-    _onPeerHungup: function() {
-      this.setState({callFailed: false, callStatus: "end"});
-    },
-
-    /**
-     * Network disconnected. Notifies the user and ends the call.
-     */
-    _onNetworkDisconnected: function() {
-      // XXX Not the ideal response, but bug 1047410 will be replacing
-      // this by better "call failed" UI.
-      this.setState({callFailed: true, callStatus: "end"});
-    },
-
-    /**
-     * Incoming call route.
-     */
-    setupIncomingCall: function() {
-      navigator.mozLoop.startAlerting();
-
-      // XXX This is a hack until we rework for the flux model in bug 1088672.
-      var callData = this.props.conversationAppStore.getStoreState().windowData;
-
-      this.props.conversation.setIncomingSessionData(callData);
-      this._setupWebSocket();
-    },
-
-    /**
-     * Starts the actual conversation
-     */
-    accepted: function() {
-      this.setState({callStatus: "connected"});
-    },
-
-    /**
-     * Moves the call to the end state
-     */
-    endCall: function() {
-      navigator.mozLoop.calls.clearCallInProgress(
-        this.props.conversation.get("windowId"));
-      this.setState({callStatus: "end"});
-    },
-
-    /**
-     * Used to set up the web socket connection and navigate to the
-     * call view if appropriate.
-     */
-    _setupWebSocket: function() {
-      this._websocket = new loop.CallConnectionWebSocket({
-        url: this.props.conversation.get("progressURL"),
-        websocketToken: this.props.conversation.get("websocketToken"),
-        callId: this.props.conversation.get("callId"),
-      });
-      this._websocket.promiseConnect().then(function(progressStatus) {
-        this.setState({
-          callStatus: progressStatus === "terminated" ? "close" : "incoming"
-        });
-      }.bind(this), function() {
-        this._handleSessionError();
-        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.props.conversation.streamsConnected()) {
-        this._websocket.mediaUp();
-      }
-    },
-
-    /**
-     * Used to receive websocket progress and to determine how to handle
-     * it if appropraite.
-     * If we add more cases here, then we should refactor this function.
-     *
-     * @param {Object} progressData The progress data from the websocket.
-     * @param {String} previousState The previous state from the websocket.
-     */
-    _handleWebSocketProgress: function(progressData, previousState) {
-      // We only care about the terminated state at the moment.
-      if (progressData.state !== "terminated")
-        return;
-
-      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
-      // it here for now due to server-side issues that are being fixed in bug 1088351.
-      // This is before the abort call to ensure that it happens before the window is
-      // closed.
-      navigator.mozLoop.stopAlerting();
-
-      // If we hit any of the termination reasons, and the user hasn't accepted
-      // then it seems reasonable to close the window/abort the incoming call.
-      //
-      // If the user has accepted the call, and something's happened, display
-      // the call failed view.
-      //
-      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
-      if (previousState === "init" || previousState === "alerting") {
-        this._abortIncomingCall();
-      } else {
-        this.setState({callFailed: true, callStatus: "end"});
-      }
-
-    },
-
-    /**
-     * Silently aborts an incoming call - stops the alerting, and
-     * closes the websocket.
-     */
-    _abortIncomingCall: function() {
-      this._websocket.close();
-      // Having a timeout here lets the logging for the websocket complete and be
-      // displayed on the console if both are on.
-      setTimeout(this.closeWindow, 0);
-    },
-
-    /**
-     * Accepts an incoming call.
-     */
-    accept: function() {
-      navigator.mozLoop.stopAlerting();
-      this._websocket.accept();
-      this.props.conversation.accepted();
-    },
-
-    /**
-     * Declines a call and handles closing of the window.
-     */
-    _declineCall: function() {
-      this._websocket.decline();
-      navigator.mozLoop.calls.clearCallInProgress(
-        this.props.conversation.get("windowId"));
-      this._websocket.close();
-      // Having a timeout here lets the logging for the websocket complete and be
-      // displayed on the console if both are on.
-      setTimeout(this.closeWindow, 0);
-    },
-
-    /**
-     * Declines an incoming call.
-     */
-    decline: function() {
-      navigator.mozLoop.stopAlerting();
-      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
-     */
-    declineAndBlock: function() {
-      navigator.mozLoop.stopAlerting();
-      var token = this.props.conversation.get("callToken");
-      var callerId = this.props.conversation.get("callerId");
-
-      // If this is a direct call, we'll need to block the caller directly.
-      if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
-        navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
-          // 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 1103150).
-          console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
-        });
-      } else {
-        this.props.client.deleteCallUrl(token,
-          this.props.conversation.get("sessionType"),
-          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);
-          });
-      }
-
-      this._declineCall();
-    },
-
-    /**
-     * 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.
-      console.error("Failed initiating the call session.");
-    },
-  });
+  var GenericFailureView = loop.conversationViews.GenericFailureView;
 
   /**
    * Master controller view for handling if incoming or outgoing calls are
    * in progress, and hence, which view to display.
    */
   var AppControllerView = React.createClass({
     mixins: [Backbone.Events, sharedMixins.WindowCloseMixin],
 
@@ -706,16 +196,13 @@ loop.conversation = (function(mozL10n) {
 
     dispatcher.dispatch(new sharedActions.GetWindowData({
       windowId: windowId
     }));
   }
 
   return {
     AppControllerView: AppControllerView,
-    IncomingConversationView: IncomingConversationView,
-    IncomingCallView: IncomingCallView,
-    GenericFailureView: GenericFailureView,
     init: init
   };
 })(document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.conversation.init);
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -10,16 +10,17 @@ var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
+  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
@@ -124,16 +125,528 @@ loop.conversationViews = (function(mozL1
             peerIdentifier: contactName, 
             showIcons: false}), 
           React.DOM.div(null, this.props.children)
         )
       );
     }
   });
 
+  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
+  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
+
+  var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
+    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
+
+    propTypes: {
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
+    },
+
+    getDefaultProps: function() {
+      return {
+        showMenu: false,
+        video: true
+      };
+    },
+
+    clickHandler: function(e) {
+      var target = e.target;
+      if (!target.classList.contains('btn-chevron')) {
+        this._hideDeclineMenu();
+      }
+    },
+
+    _handleAccept: function(callType) {
+      return function() {
+        this.props.model.set("selectedCallType", callType);
+        this.props.model.trigger("accept");
+      }.bind(this);
+    },
+
+    _handleDecline: function() {
+      this.props.model.trigger("decline");
+    },
+
+    _handleDeclineBlock: function(e) {
+      this.props.model.trigger("declineAndBlock");
+      /* Prevent event propagation
+       * stop the click from reaching parent element */
+      return false;
+    },
+
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
+    render: function() {
+      /* jshint ignore:start */
+      var dropdownMenuClassesDecline = React.addons.classSet({
+        "native-dropdown-menu": true,
+        "conversation-window-dropdown": true,
+        "visually-hidden": !this.state.showMenu
+      });
+
+      return (
+        React.DOM.div({className: "call-window"}, 
+          CallIdentifierView({video: this.props.video, 
+            peerIdentifier: this.props.model.getCallIdentifier(), 
+            urlCreationDate: this.props.model.get("urlCreationDate"), 
+            showIcons: true}), 
+
+          React.DOM.div({className: "btn-group call-action-group"}, 
+
+            React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
+
+            React.DOM.div({className: "btn-chevron-menu-group"}, 
+              React.DOM.div({className: "btn-group-chevron"}, 
+                React.DOM.div({className: "btn-group"}, 
+
+                  React.DOM.button({className: "btn btn-decline", 
+                          onClick: this._handleDecline}, 
+                    mozL10n.get("incoming_call_cancel_button")
+                  ), 
+                  React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
+                ), 
+
+                React.DOM.ul({className: dropdownMenuClassesDecline}, 
+                  React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, 
+                    mozL10n.get("incoming_call_cancel_and_block_button")
+                  )
+                )
+
+              )
+            ), 
+
+            React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
+
+            AcceptCallButton({mode: this._answerModeProps()}), 
+
+            React.DOM.div({className: "fx-embedded-call-button-spacer"})
+
+          )
+        )
+      );
+      /* jshint ignore:end */
+    }
+  });
+
+  /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        React.DOM.div({className: "btn-chevron-menu-group"}, 
+          React.DOM.div({className: "btn-group"}, 
+            React.DOM.button({className: "btn btn-accept", 
+                    onClick: mode.primary.handler, 
+                    title: mozL10n.get(mode.primary.tooltip)}, 
+              React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
+                mozL10n.get("incoming_call_accept_button")
+              ), 
+              React.DOM.span({className: mode.primary.className})
+            ), 
+            React.DOM.div({className: mode.secondary.className, 
+                 onClick: mode.secondary.handler, 
+                 title: mozL10n.get(mode.secondary.tooltip)}
+            )
+          )
+        )
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
+   * Something went wrong view. Displayed when there's a big problem.
+   *
+   * XXX Based on CallFailedView, but built specially until we flux-ify the
+   * incoming call views (bug 1088672).
+   */
+  var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
+    mixins: [sharedMixins.AudioMixin],
+
+    propTypes: {
+      cancelCall: React.PropTypes.func.isRequired
+    },
+
+    componentDidMount: function() {
+      this.play("failure");
+    },
+
+    render: function() {
+      document.title = mozL10n.get("generic_failure_title");
+
+      return (
+        React.DOM.div({className: "call-window"}, 
+          React.DOM.h2(null, mozL10n.get("generic_failure_title")), 
+
+          React.DOM.div({className: "btn-group call-action-group"}, 
+            React.DOM.button({className: "btn btn-cancel", 
+                    onClick: this.props.cancelCall}, 
+              mozL10n.get("cancel_button")
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * This view manages the incoming conversation views - from
+   * call initiation through to the actual conversation and call end.
+   *
+   * At the moment, it does more than that, these parts need refactoring out.
+   */
+  var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
+    mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
+
+    propTypes: {
+      client: React.PropTypes.instanceOf(loop.Client).isRequired,
+      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                         .isRequired,
+      sdk: React.PropTypes.object.isRequired,
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+    },
+
+    getInitialState: function() {
+      return {
+        callFailed: false, // XXX this should be removed when bug 1047410 lands.
+        callStatus: "start"
+      };
+    },
+
+    componentDidMount: function() {
+      this.props.conversation.on("accept", this.accept, this);
+      this.props.conversation.on("decline", this.decline, this);
+      this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
+      this.props.conversation.on("call:accepted", this.accepted, this);
+      this.props.conversation.on("change:publishedStream", this._checkConnected, this);
+      this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
+      this.props.conversation.on("session:ended", this.endCall, this);
+      this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
+      this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
+      this.props.conversation.on("session:connection-error", this._notifyError, this);
+
+      this.setupIncomingCall();
+    },
+
+    componentDidUnmount: function() {
+      this.props.conversation.off(null, null, this);
+    },
+
+    render: function() {
+      switch (this.state.callStatus) {
+        case "start": {
+          document.title = mozL10n.get("incoming_call_title2");
+
+          // XXX Don't render anything initially, though this should probably
+          // be some sort of pending view, whilst we connect the websocket.
+          return null;
+        }
+        case "incoming": {
+          document.title = mozL10n.get("incoming_call_title2");
+
+          return (
+            IncomingCallView({
+              model: this.props.conversation, 
+              video: this.props.conversation.hasVideoStream("incoming")}
+            )
+          );
+        }
+        case "connected": {
+          document.title = this.props.conversation.getCallIdentifier();
+
+          var callType = this.props.conversation.get("selectedCallType");
+
+          return (
+            sharedViews.ConversationView({
+              initiate: true, 
+              sdk: this.props.sdk, 
+              model: this.props.conversation, 
+              video: {enabled: callType !== "audio"}}
+            )
+          );
+        }
+        case "end": {
+          // XXX To be handled with the "failed" view state when bug 1047410 lands
+          if (this.state.callFailed) {
+            return GenericFailureView({
+              cancelCall: this.closeWindow.bind(this)}
+            );
+          }
+
+          document.title = mozL10n.get("conversation_has_ended");
+
+          this.play("terminated");
+
+          return (
+            sharedViews.FeedbackView({
+              feedbackStore: this.props.feedbackStore, 
+              onAfterFeedbackReceived: this.closeWindow.bind(this)}
+            )
+          );
+        }
+        case "close": {
+          this.closeWindow();
+          return (React.DOM.div(null));
+        }
+      }
+    },
+
+    /**
+     * Notify the user that the connection was not possible
+     * @param {{code: number, message: string}} error
+     */
+    _notifyError: function(error) {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      console.error(error);
+      this.setState({callFailed: true, callStatus: "end"});
+    },
+
+    /**
+     * Peer hung up. Notifies the user and ends the call.
+     *
+     * Event properties:
+     * - {String} connectionId: OT session id
+     */
+    _onPeerHungup: function() {
+      this.setState({callFailed: false, callStatus: "end"});
+    },
+
+    /**
+     * Network disconnected. Notifies the user and ends the call.
+     */
+    _onNetworkDisconnected: function() {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      this.setState({callFailed: true, callStatus: "end"});
+    },
+
+    /**
+     * Incoming call route.
+     */
+    setupIncomingCall: function() {
+      navigator.mozLoop.startAlerting();
+
+      // XXX This is a hack until we rework for the flux model in bug 1088672.
+      var callData = this.props.conversationAppStore.getStoreState().windowData;
+
+      this.props.conversation.setIncomingSessionData(callData);
+      this._setupWebSocket();
+    },
+
+    /**
+     * Starts the actual conversation
+     */
+    accepted: function() {
+      this.setState({callStatus: "connected"});
+    },
+
+    /**
+     * Moves the call to the end state
+     */
+    endCall: function() {
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
+      this.setState({callStatus: "end"});
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     */
+    _setupWebSocket: function() {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this.props.conversation.get("progressURL"),
+        websocketToken: this.props.conversation.get("websocketToken"),
+        callId: this.props.conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function(progressStatus) {
+        this.setState({
+          callStatus: progressStatus === "terminated" ? "close" : "incoming"
+        });
+      }.bind(this), function() {
+        this._handleSessionError();
+        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.props.conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     * If we add more cases here, then we should refactor this function.
+     *
+     * @param {Object} progressData The progress data from the websocket.
+     * @param {String} previousState The previous state from the websocket.
+     */
+    _handleWebSocketProgress: function(progressData, previousState) {
+      // We only care about the terminated state at the moment.
+      if (progressData.state !== "terminated")
+        return;
+
+      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
+      // it here for now due to server-side issues that are being fixed in bug 1088351.
+      // This is before the abort call to ensure that it happens before the window is
+      // closed.
+      navigator.mozLoop.stopAlerting();
+
+      // If we hit any of the termination reasons, and the user hasn't accepted
+      // then it seems reasonable to close the window/abort the incoming call.
+      //
+      // If the user has accepted the call, and something's happened, display
+      // the call failed view.
+      //
+      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
+      if (previousState === "init" || previousState === "alerting") {
+        this._abortIncomingCall();
+      } else {
+        this.setState({callFailed: true, callStatus: "end"});
+      }
+
+    },
+
+    /**
+     * Silently aborts an incoming call - stops the alerting, and
+     * closes the websocket.
+     */
+    _abortIncomingCall: function() {
+      this._websocket.close();
+      // Having a timeout here lets the logging for the websocket complete and be
+      // displayed on the console if both are on.
+      setTimeout(this.closeWindow, 0);
+    },
+
+    /**
+     * Accepts an incoming call.
+     */
+    accept: function() {
+      navigator.mozLoop.stopAlerting();
+      this._websocket.accept();
+      this.props.conversation.accepted();
+    },
+
+    /**
+     * Declines a call and handles closing of the window.
+     */
+    _declineCall: function() {
+      this._websocket.decline();
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
+      this._websocket.close();
+      // Having a timeout here lets the logging for the websocket complete and be
+      // displayed on the console if both are on.
+      setTimeout(this.closeWindow, 0);
+    },
+
+    /**
+     * Declines an incoming call.
+     */
+    decline: function() {
+      navigator.mozLoop.stopAlerting();
+      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
+     */
+    declineAndBlock: function() {
+      navigator.mozLoop.stopAlerting();
+      var token = this.props.conversation.get("callToken");
+      var callerId = this.props.conversation.get("callerId");
+
+      // If this is a direct call, we'll need to block the caller directly.
+      if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
+        navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
+          // 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 1103150).
+          console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
+        });
+      } else {
+        this.props.client.deleteCallUrl(token,
+          this.props.conversation.get("sessionType"),
+          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);
+          });
+      }
+
+      this._declineCall();
+    },
+
+    /**
+     * 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.
+      console.error("Failed initiating the call session.");
+    },
+  });
+
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
     mixins: [sharedMixins.AudioMixin],
 
     propTypes: {
@@ -530,13 +1043,16 @@ loop.conversationViews = (function(mozL1
     },
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
     CallFailedView: CallFailedView,
+    GenericFailureView: GenericFailureView,
+    IncomingCallView: IncomingCallView,
+    IncomingConversationView: IncomingConversationView,
     OngoingConversationView: OngoingConversationView,
     OutgoingConversationView: OutgoingConversationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -10,16 +10,17 @@ var loop = loop || {};
 loop.conversationViews = (function(mozL10n) {
 
   var CALL_STATES = loop.store.CALL_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
   var sharedActions = loop.shared.actions;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
+  var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
@@ -124,16 +125,528 @@ loop.conversationViews = (function(mozL1
             peerIdentifier={contactName}
             showIcons={false} />
           <div>{this.props.children}</div>
         </div>
       );
     }
   });
 
+  // Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
+  var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
+
+  var IncomingCallView = React.createClass({
+    mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
+
+    propTypes: {
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
+    },
+
+    getDefaultProps: function() {
+      return {
+        showMenu: false,
+        video: true
+      };
+    },
+
+    clickHandler: function(e) {
+      var target = e.target;
+      if (!target.classList.contains('btn-chevron')) {
+        this._hideDeclineMenu();
+      }
+    },
+
+    _handleAccept: function(callType) {
+      return function() {
+        this.props.model.set("selectedCallType", callType);
+        this.props.model.trigger("accept");
+      }.bind(this);
+    },
+
+    _handleDecline: function() {
+      this.props.model.trigger("decline");
+    },
+
+    _handleDeclineBlock: function(e) {
+      this.props.model.trigger("declineAndBlock");
+      /* Prevent event propagation
+       * stop the click from reaching parent element */
+      return false;
+    },
+
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
+    render: function() {
+      /* jshint ignore:start */
+      var dropdownMenuClassesDecline = React.addons.classSet({
+        "native-dropdown-menu": true,
+        "conversation-window-dropdown": true,
+        "visually-hidden": !this.state.showMenu
+      });
+
+      return (
+        <div className="call-window">
+          <CallIdentifierView video={this.props.video}
+            peerIdentifier={this.props.model.getCallIdentifier()}
+            urlCreationDate={this.props.model.get("urlCreationDate")}
+            showIcons={true} />
+
+          <div className="btn-group call-action-group">
+
+            <div className="fx-embedded-call-button-spacer"></div>
+
+            <div className="btn-chevron-menu-group">
+              <div className="btn-group-chevron">
+                <div className="btn-group">
+
+                  <button className="btn btn-decline"
+                          onClick={this._handleDecline}>
+                    {mozL10n.get("incoming_call_cancel_button")}
+                  </button>
+                  <div className="btn-chevron" onClick={this.toggleDropdownMenu} />
+                </div>
+
+                <ul className={dropdownMenuClassesDecline}>
+                  <li className="btn-block" onClick={this._handleDeclineBlock}>
+                    {mozL10n.get("incoming_call_cancel_and_block_button")}
+                  </li>
+                </ul>
+
+              </div>
+            </div>
+
+            <div className="fx-embedded-call-button-spacer"></div>
+
+            <AcceptCallButton mode={this._answerModeProps()} />
+
+            <div className="fx-embedded-call-button-spacer"></div>
+
+          </div>
+        </div>
+      );
+      /* jshint ignore:end */
+    }
+  });
+
+  /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        <div className="btn-chevron-menu-group">
+          <div className="btn-group">
+            <button className="btn btn-accept"
+                    onClick={mode.primary.handler}
+                    title={mozL10n.get(mode.primary.tooltip)}>
+              <span className="fx-embedded-answer-btn-text">
+                {mozL10n.get("incoming_call_accept_button")}
+              </span>
+              <span className={mode.primary.className}></span>
+            </button>
+            <div className={mode.secondary.className}
+                 onClick={mode.secondary.handler}
+                 title={mozL10n.get(mode.secondary.tooltip)}>
+            </div>
+          </div>
+        </div>
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
+   * Something went wrong view. Displayed when there's a big problem.
+   *
+   * XXX Based on CallFailedView, but built specially until we flux-ify the
+   * incoming call views (bug 1088672).
+   */
+  var GenericFailureView = React.createClass({
+    mixins: [sharedMixins.AudioMixin],
+
+    propTypes: {
+      cancelCall: React.PropTypes.func.isRequired
+    },
+
+    componentDidMount: function() {
+      this.play("failure");
+    },
+
+    render: function() {
+      document.title = mozL10n.get("generic_failure_title");
+
+      return (
+        <div className="call-window">
+          <h2>{mozL10n.get("generic_failure_title")}</h2>
+
+          <div className="btn-group call-action-group">
+            <button className="btn btn-cancel"
+                    onClick={this.props.cancelCall}>
+              {mozL10n.get("cancel_button")}
+            </button>
+          </div>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * This view manages the incoming conversation views - from
+   * call initiation through to the actual conversation and call end.
+   *
+   * At the moment, it does more than that, these parts need refactoring out.
+   */
+  var IncomingConversationView = React.createClass({
+    mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
+
+    propTypes: {
+      client: React.PropTypes.instanceOf(loop.Client).isRequired,
+      conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
+                         .isRequired,
+      sdk: React.PropTypes.object.isRequired,
+      conversationAppStore: React.PropTypes.instanceOf(
+        loop.store.ConversationAppStore).isRequired,
+      feedbackStore:
+        React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
+    },
+
+    getInitialState: function() {
+      return {
+        callFailed: false, // XXX this should be removed when bug 1047410 lands.
+        callStatus: "start"
+      };
+    },
+
+    componentDidMount: function() {
+      this.props.conversation.on("accept", this.accept, this);
+      this.props.conversation.on("decline", this.decline, this);
+      this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
+      this.props.conversation.on("call:accepted", this.accepted, this);
+      this.props.conversation.on("change:publishedStream", this._checkConnected, this);
+      this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
+      this.props.conversation.on("session:ended", this.endCall, this);
+      this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
+      this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
+      this.props.conversation.on("session:connection-error", this._notifyError, this);
+
+      this.setupIncomingCall();
+    },
+
+    componentDidUnmount: function() {
+      this.props.conversation.off(null, null, this);
+    },
+
+    render: function() {
+      switch (this.state.callStatus) {
+        case "start": {
+          document.title = mozL10n.get("incoming_call_title2");
+
+          // XXX Don't render anything initially, though this should probably
+          // be some sort of pending view, whilst we connect the websocket.
+          return null;
+        }
+        case "incoming": {
+          document.title = mozL10n.get("incoming_call_title2");
+
+          return (
+            <IncomingCallView
+              model={this.props.conversation}
+              video={this.props.conversation.hasVideoStream("incoming")}
+            />
+          );
+        }
+        case "connected": {
+          document.title = this.props.conversation.getCallIdentifier();
+
+          var callType = this.props.conversation.get("selectedCallType");
+
+          return (
+            <sharedViews.ConversationView
+              initiate={true}
+              sdk={this.props.sdk}
+              model={this.props.conversation}
+              video={{enabled: callType !== "audio"}}
+            />
+          );
+        }
+        case "end": {
+          // XXX To be handled with the "failed" view state when bug 1047410 lands
+          if (this.state.callFailed) {
+            return <GenericFailureView
+              cancelCall={this.closeWindow.bind(this)}
+            />;
+          }
+
+          document.title = mozL10n.get("conversation_has_ended");
+
+          this.play("terminated");
+
+          return (
+            <sharedViews.FeedbackView
+              feedbackStore={this.props.feedbackStore}
+              onAfterFeedbackReceived={this.closeWindow.bind(this)}
+            />
+          );
+        }
+        case "close": {
+          this.closeWindow();
+          return (<div/>);
+        }
+      }
+    },
+
+    /**
+     * Notify the user that the connection was not possible
+     * @param {{code: number, message: string}} error
+     */
+    _notifyError: function(error) {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      console.error(error);
+      this.setState({callFailed: true, callStatus: "end"});
+    },
+
+    /**
+     * Peer hung up. Notifies the user and ends the call.
+     *
+     * Event properties:
+     * - {String} connectionId: OT session id
+     */
+    _onPeerHungup: function() {
+      this.setState({callFailed: false, callStatus: "end"});
+    },
+
+    /**
+     * Network disconnected. Notifies the user and ends the call.
+     */
+    _onNetworkDisconnected: function() {
+      // XXX Not the ideal response, but bug 1047410 will be replacing
+      // this by better "call failed" UI.
+      this.setState({callFailed: true, callStatus: "end"});
+    },
+
+    /**
+     * Incoming call route.
+     */
+    setupIncomingCall: function() {
+      navigator.mozLoop.startAlerting();
+
+      // XXX This is a hack until we rework for the flux model in bug 1088672.
+      var callData = this.props.conversationAppStore.getStoreState().windowData;
+
+      this.props.conversation.setIncomingSessionData(callData);
+      this._setupWebSocket();
+    },
+
+    /**
+     * Starts the actual conversation
+     */
+    accepted: function() {
+      this.setState({callStatus: "connected"});
+    },
+
+    /**
+     * Moves the call to the end state
+     */
+    endCall: function() {
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
+      this.setState({callStatus: "end"});
+    },
+
+    /**
+     * Used to set up the web socket connection and navigate to the
+     * call view if appropriate.
+     */
+    _setupWebSocket: function() {
+      this._websocket = new loop.CallConnectionWebSocket({
+        url: this.props.conversation.get("progressURL"),
+        websocketToken: this.props.conversation.get("websocketToken"),
+        callId: this.props.conversation.get("callId"),
+      });
+      this._websocket.promiseConnect().then(function(progressStatus) {
+        this.setState({
+          callStatus: progressStatus === "terminated" ? "close" : "incoming"
+        });
+      }.bind(this), function() {
+        this._handleSessionError();
+        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.props.conversation.streamsConnected()) {
+        this._websocket.mediaUp();
+      }
+    },
+
+    /**
+     * Used to receive websocket progress and to determine how to handle
+     * it if appropraite.
+     * If we add more cases here, then we should refactor this function.
+     *
+     * @param {Object} progressData The progress data from the websocket.
+     * @param {String} previousState The previous state from the websocket.
+     */
+    _handleWebSocketProgress: function(progressData, previousState) {
+      // We only care about the terminated state at the moment.
+      if (progressData.state !== "terminated")
+        return;
+
+      // XXX This would be nicer in the _abortIncomingCall function, but we need to stop
+      // it here for now due to server-side issues that are being fixed in bug 1088351.
+      // This is before the abort call to ensure that it happens before the window is
+      // closed.
+      navigator.mozLoop.stopAlerting();
+
+      // If we hit any of the termination reasons, and the user hasn't accepted
+      // then it seems reasonable to close the window/abort the incoming call.
+      //
+      // If the user has accepted the call, and something's happened, display
+      // the call failed view.
+      //
+      // https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
+      if (previousState === "init" || previousState === "alerting") {
+        this._abortIncomingCall();
+      } else {
+        this.setState({callFailed: true, callStatus: "end"});
+      }
+
+    },
+
+    /**
+     * Silently aborts an incoming call - stops the alerting, and
+     * closes the websocket.
+     */
+    _abortIncomingCall: function() {
+      this._websocket.close();
+      // Having a timeout here lets the logging for the websocket complete and be
+      // displayed on the console if both are on.
+      setTimeout(this.closeWindow, 0);
+    },
+
+    /**
+     * Accepts an incoming call.
+     */
+    accept: function() {
+      navigator.mozLoop.stopAlerting();
+      this._websocket.accept();
+      this.props.conversation.accepted();
+    },
+
+    /**
+     * Declines a call and handles closing of the window.
+     */
+    _declineCall: function() {
+      this._websocket.decline();
+      navigator.mozLoop.calls.clearCallInProgress(
+        this.props.conversation.get("windowId"));
+      this._websocket.close();
+      // Having a timeout here lets the logging for the websocket complete and be
+      // displayed on the console if both are on.
+      setTimeout(this.closeWindow, 0);
+    },
+
+    /**
+     * Declines an incoming call.
+     */
+    decline: function() {
+      navigator.mozLoop.stopAlerting();
+      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
+     */
+    declineAndBlock: function() {
+      navigator.mozLoop.stopAlerting();
+      var token = this.props.conversation.get("callToken");
+      var callerId = this.props.conversation.get("callerId");
+
+      // If this is a direct call, we'll need to block the caller directly.
+      if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
+        navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
+          // 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 1103150).
+          console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
+        });
+      } else {
+        this.props.client.deleteCallUrl(token,
+          this.props.conversation.get("sessionType"),
+          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);
+          });
+      }
+
+      this._declineCall();
+    },
+
+    /**
+     * 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.
+      console.error("Failed initiating the call session.");
+    },
+  });
+
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({
     mixins: [sharedMixins.AudioMixin],
 
     propTypes: {
@@ -530,13 +1043,16 @@ loop.conversationViews = (function(mozL1
     },
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
     CallFailedView: CallFailedView,
+    GenericFailureView: GenericFailureView,
+    IncomingCallView: IncomingCallView,
+    IncomingConversationView: IncomingConversationView,
     OngoingConversationView: OngoingConversationView,
     OutgoingConversationView: OutgoingConversationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -290,17 +290,17 @@ loop.roomViews = (function(mozL10n) {
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
-          return loop.conversation.GenericFailureView({
+          return loop.conversationViews.GenericFailureView({
             cancelCall: this.closeWindow}
           );
         }
         case ROOM_STATES.ENDED: {
           if (this.state.used)
             return sharedViews.FeedbackView({
               feedbackStore: this.props.feedbackStore, 
               onAfterFeedbackReceived: this.closeWindow}
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -290,17 +290,17 @@ loop.roomViews = (function(mozL10n) {
         "room-preview": this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS
       });
 
       switch(this.state.roomState) {
         case ROOM_STATES.FAILED:
         case ROOM_STATES.FULL: {
           // Note: While rooms are set to hold a maximum of 2 participants, the
           //       FULL case should never happen on desktop.
-          return <loop.conversation.GenericFailureView
+          return <loop.conversationViews.GenericFailureView
             cancelCall={this.closeWindow}
           />;
         }
         case ROOM_STATES.ENDED: {
           if (this.state.used)
             return <sharedViews.FeedbackView
               feedbackStore={this.props.feedbackStore}
               onAfterFeedbackReceived={this.closeWindow}
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -2,23 +2,38 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.conversationViews", function () {
   "use strict";
 
   var sharedUtils = loop.shared.utils;
+  var sharedView = loop.shared.views;
   var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR;
   var fakeMozLoop, fakeWindow;
 
   var CALL_STATES = loop.store.CALL_STATES;
 
+  // XXX refactor to Just Work with "sandbox.stubComponent" or else
+  // just pass in the sandbox and put somewhere generally usable
+
+  function stubComponent(obj, component, mockTagName){
+    var reactClass = React.createClass({
+      render: function() {
+        var mockTagName = mockTagName || "div";
+        return React.DOM[mockTagName](null, this.props.children);
+      }
+    });
+    return sandbox.stub(obj, component, reactClass);
+  }
+
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    sandbox.useFakeTimers();
 
     oldTitle = document.title;
     sandbox.stub(document.mozL10n, "get", function(x) {
       return x;
     });
 
     dispatcher = new loop.Dispatcher();
     sandbox.stub(dispatcher, "dispatch");
@@ -40,32 +55,45 @@ describe("loop.conversationViews", funct
           return "audio/ogg";
       },
       responseType: null,
       response: new ArrayBuffer(10),
       onload: null
     };
 
     fakeMozLoop = navigator.mozLoop = {
-      getLoopPref: sinon.stub().returns("http://fakeurl"),
+      // Dummy function, stubbed below.
+      getLoopPref: function() {},
+      calls: {
+        clearCallInProgress: sinon.stub()
+      },
       composeEmail: sinon.spy(),
       get appVersionInfo() {
         return {
           version: "42",
           channel: "test",
           platform: "test"
         };
       },
       getAudioBlob: sinon.spy(function(name, callback) {
         callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
       }),
+      startAlerting: sinon.stub(),
+      stopAlerting: sinon.stub(),
       userProfile: {
         email: "bob@invalid.tld"
       }
     };
+    sinon.stub(fakeMozLoop, "getLoopPref", function(pref) {
+        if (pref === "fake") {
+          return"http://fakeurl";
+        }
+
+        return false;
+    });
 
     fakeWindow = {
       navigator: { mozLoop: fakeMozLoop },
       close: sandbox.stub(),
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
   });
@@ -573,9 +601,726 @@ describe("loop.conversationViews", funct
           loop.conversationViews.PendingConversationView);
 
         store.setStoreState({callState: CALL_STATES.TERMINATED});
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
   });
+
+  describe("IncomingConversationView", function() {
+    var conversationAppStore, conversation, client, icView, oldTitle,
+        feedbackStore;
+
+    function mountTestComponent() {
+      return TestUtils.renderIntoDocument(
+        loop.conversationViews.IncomingConversationView({
+          client: client,
+          conversation: conversation,
+          sdk: {},
+          conversationAppStore: conversationAppStore,
+          feedbackStore: feedbackStore
+        }));
+    }
+
+    beforeEach(function() {
+      oldTitle = document.title;
+      client = new loop.Client();
+      conversation = new loop.shared.models.ConversationModel({}, {
+        sdk: {}
+      });
+      conversation.set({windowId: 42});
+      var dispatcher = new loop.Dispatcher();
+      conversationAppStore = new loop.store.ConversationAppStore({
+        dispatcher: dispatcher,
+        mozLoop: navigator.mozLoop
+      });
+      feedbackStore = new loop.store.FeedbackStore(dispatcher, {
+        feedbackClient: {}
+      });
+      sandbox.stub(conversation, "setOutgoingSessionData");
+    });
+
+    afterEach(function() {
+      icView = undefined;
+      document.title = oldTitle;
+    });
+
+    describe("start", function() {
+      it("should set the title to incoming_call_title2", function() {
+        conversationAppStore.setStoreState({
+          windowData: {
+            progressURL:    "fake",
+            websocketToken: "fake",
+            callId: 42
+          }
+        });
+
+        icView = mountTestComponent();
+
+        expect(document.title).eql("incoming_call_title2");
+      });
+    });
+
+    describe("componentDidMount", function() {
+      var fakeSessionData, promise, resolveWebSocketConnect;
+      var rejectWebSocketConnect;
+
+      beforeEach(function() {
+        fakeSessionData  = {
+          sessionId:      "sessionId",
+          sessionToken:   "sessionToken",
+          apiKey:         "apiKey",
+          callType:       "callType",
+          callId:         "Hello",
+          progressURL:    "http://progress.example.com",
+          websocketToken: "7b"
+        };
+
+        conversationAppStore.setStoreState({
+          windowData: fakeSessionData
+        });
+
+        stubComponent(loop.conversationViews, "IncomingCallView");
+        stubComponent(sharedView, "ConversationView");
+      });
+
+      it("should start alerting", function() {
+        icView = mountTestComponent();
+
+        sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
+      });
+
+      describe("Session Data setup", function() {
+        beforeEach(function() {
+          sandbox.stub(loop, "CallConnectionWebSocket").returns({
+            promiseConnect: function () {
+              promise = new Promise(function(resolve, reject) {
+                resolveWebSocketConnect = resolve;
+                rejectWebSocketConnect = reject;
+              });
+              return promise;
+            },
+            on: sinon.stub()
+          });
+        });
+
+        it("should store the session data", function() {
+          sandbox.stub(conversation, "setIncomingSessionData");
+
+          icView = mountTestComponent();
+
+          sinon.assert.calledOnce(conversation.setIncomingSessionData);
+          sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
+                                         fakeSessionData);
+        });
+
+        it("should setup the websocket connection", function() {
+          icView = mountTestComponent();
+
+          sinon.assert.calledOnce(loop.CallConnectionWebSocket);
+          sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
+            callId: "Hello",
+            url: "http://progress.example.com",
+            websocketToken: "7b"
+          });
+        });
+      });
+
+      describe("WebSocket Handling", function() {
+        beforeEach(function() {
+          promise = new Promise(function(resolve, reject) {
+            resolveWebSocketConnect = resolve;
+            rejectWebSocketConnect = reject;
+          });
+
+          sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
+        });
+
+        it("should set the state to incoming on success", function(done) {
+          icView = mountTestComponent();
+          resolveWebSocketConnect("incoming");
+
+          promise.then(function () {
+            expect(icView.state.callStatus).eql("incoming");
+            done();
+          });
+        });
+
+        it("should set the state to close on success if the progress " +
+          "state is terminated", function(done) {
+            icView = mountTestComponent();
+            resolveWebSocketConnect("terminated");
+
+            promise.then(function () {
+              expect(icView.state.callStatus).eql("close");
+              done();
+            });
+          });
+
+        // XXX implement me as part of bug 1047410
+        // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
+        it.skip("should should switch view state to failed", function(done) {
+          icView = mountTestComponent();
+          rejectWebSocketConnect();
+
+          promise.then(function() {}, function() {
+            done();
+          });
+        });
+      });
+
+      describe("WebSocket Events", function() {
+        describe("Call cancelled or timed out before acceptance", function() {
+          beforeEach(function() {
+            // Mounting the test component automatically calls the required
+            // setup functions
+            icView = mountTestComponent();
+            promise = new Promise(function(resolve, reject) {
+              resolve();
+            });
+
+            sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
+            sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
+          });
+
+          describe("progress - terminated (previousState = alerting)", function() {
+            it("should stop alerting", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "timeout"
+                }, "alerting");
+
+                sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+                done();
+              });
+            });
+
+            it("should close the websocket", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "closed"
+                }, "alerting");
+
+                sinon.assert.calledOnce(icView._websocket.close);
+                done();
+              });
+            });
+
+            it("should close the window", function(done) {
+              promise.then(function() {
+                icView._websocket.trigger("progress", {
+                  state: "terminated",
+                  reason: "answered-elsewhere"
+                }, "alerting");
+
+                sandbox.clock.tick(1);
+
+                sinon.assert.calledOnce(fakeWindow.close);
+                done();
+              });
+            });
+          });
+
+
+          describe("progress - terminated (previousState not init" +
+                   " nor alerting)",
+            function() {
+              it("should set the state to end", function(done) {
+                promise.then(function() {
+                  icView._websocket.trigger("progress", {
+                    state: "terminated",
+                    reason: "media-fail"
+                  }, "connecting");
+
+                  expect(icView.state.callStatus).eql("end");
+                  done();
+                });
+              });
+
+              it("should stop alerting", function(done) {
+                promise.then(function() {
+                  icView._websocket.trigger("progress", {
+                    state: "terminated",
+                    reason: "media-fail"
+                  }, "connecting");
+
+                  sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+                  done();
+                });
+              });
+            });
+        });
+      });
+
+      describe("#accept", function() {
+        beforeEach(function() {
+          icView = mountTestComponent();
+          conversation.setIncomingSessionData({
+            sessionId:      "sessionId",
+            sessionToken:   "sessionToken",
+            apiKey:         "apiKey",
+            callType:       "callType",
+            callId:         "Hello",
+            progressURL:    "http://progress.example.com",
+            websocketToken: 123
+          });
+
+          sandbox.stub(icView._websocket, "accept");
+          sandbox.stub(icView.props.conversation, "accepted");
+        });
+
+        it("should initiate the conversation", function() {
+          icView.accept();
+
+          sinon.assert.calledOnce(icView.props.conversation.accepted);
+        });
+
+        it("should notify the websocket of the user acceptance", function() {
+          icView.accept();
+
+          sinon.assert.calledOnce(icView._websocket.accept);
+        });
+
+        it("should stop alerting", function() {
+          icView.accept();
+
+          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+        });
+      });
+
+      describe("#decline", function() {
+        beforeEach(function() {
+          icView = mountTestComponent();
+
+          icView._websocket = {
+            decline: sinon.stub(),
+            close: sinon.stub()
+          };
+          conversation.set({
+            windowId: "8699"
+          });
+          conversation.setIncomingSessionData({
+            websocketToken: 123
+          });
+        });
+
+        it("should close the window", function() {
+          icView.decline();
+
+          sandbox.clock.tick(1);
+
+          sinon.assert.calledOnce(fakeWindow.close);
+        });
+
+        it("should stop alerting", function() {
+          icView.decline();
+
+          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+        });
+
+        it("should release callData", function() {
+          icView.decline();
+
+          sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
+          sinon.assert.calledWithExactly(
+            navigator.mozLoop.calls.clearCallInProgress, "8699");
+        });
+      });
+
+      describe("#blocked", function() {
+        var mozLoop, deleteCallUrlStub;
+
+        beforeEach(function() {
+          icView = mountTestComponent();
+
+          icView._websocket = {
+            decline: sinon.spy(),
+            close: sinon.stub()
+          };
+
+          mozLoop = {
+            LOOP_SESSION_TYPE: {
+              GUEST: 1,
+              FXA: 2
+            }
+          };
+
+          deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
+                                           "deleteCallUrl");
+        });
+
+        it("should call mozLoop.stopAlerting", function() {
+          icView.declineAndBlock();
+
+          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
+        });
+
+        it("should call delete call", function() {
+          sandbox.stub(conversation, "get").withArgs("callToken")
+                                           .returns("fakeToken")
+                                           .withArgs("sessionType")
+                                           .returns(mozLoop.LOOP_SESSION_TYPE.FXA);
+
+          icView.declineAndBlock();
+
+          sinon.assert.calledOnce(deleteCallUrlStub);
+          sinon.assert.calledWithExactly(deleteCallUrlStub,
+            "fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
+        });
+
+        it("should get callToken from conversation model", function() {
+          sandbox.stub(conversation, "get");
+          icView.declineAndBlock();
+
+          sinon.assert.called(conversation.get);
+          sinon.assert.calledWithExactly(conversation.get, "callToken");
+          sinon.assert.calledWithExactly(conversation.get, "windowId");
+        });
+
+        it("should trigger error handling in case of error", function() {
+          // XXX just logging to console for now
+          var log = sandbox.stub(console, "log");
+          var fakeError = {
+            error: true
+          };
+          deleteCallUrlStub.callsArgWith(2, fakeError);
+          icView.declineAndBlock();
+
+          sinon.assert.calledOnce(log);
+          sinon.assert.calledWithExactly(log, fakeError);
+        });
+
+        it("should close the window", function() {
+          icView.declineAndBlock();
+
+          sandbox.clock.tick(1);
+
+          sinon.assert.calledOnce(fakeWindow.close);
+        });
+      });
+    });
+
+    describe("Events", function() {
+      var fakeSessionData;
+
+      beforeEach(function() {
+
+        fakeSessionData = {
+          sessionId:    "sessionId",
+          sessionToken: "sessionToken",
+          apiKey:       "apiKey"
+        };
+
+        conversationAppStore.setStoreState({
+          windowData: fakeSessionData
+        });
+
+        sandbox.stub(conversation, "setIncomingSessionData");
+        sandbox.stub(loop, "CallConnectionWebSocket").returns({
+          promiseConnect: function() {
+            return new Promise(function() {});
+          },
+          on: sandbox.spy()
+        });
+
+        icView = mountTestComponent();
+
+        conversation.set("loopToken", "fakeToken");
+        stubComponent(sharedView, "ConversationView");
+      });
+
+      describe("call:accepted", function() {
+        it("should display the ConversationView",
+          function() {
+            conversation.accepted();
+
+            TestUtils.findRenderedComponentWithType(icView,
+              sharedView.ConversationView);
+          });
+
+        it("should set the title to the call identifier", function() {
+          sandbox.stub(conversation, "getCallIdentifier").returns("fakeId");
+
+          conversation.accepted();
+
+          expect(document.title).eql("fakeId");
+        });
+      });
+
+      describe("session:ended", function() {
+        it("should display the feedback view when the call session ends",
+          function() {
+            conversation.trigger("session:ended");
+
+            TestUtils.findRenderedComponentWithType(icView,
+              sharedView.FeedbackView);
+          });
+      });
+
+      describe("session:peer-hungup", function() {
+        it("should display the feedback view when the peer hangs up",
+          function() {
+            conversation.trigger("session:peer-hungup");
+
+              TestUtils.findRenderedComponentWithType(icView,
+                sharedView.FeedbackView);
+          });
+      });
+
+      describe("session:network-disconnected", function() {
+        it("should navigate to call failed when network disconnects",
+          function() {
+            conversation.trigger("session:network-disconnected");
+
+            TestUtils.findRenderedComponentWithType(icView,
+              loop.conversationViews.GenericFailureView);
+          });
+
+        it("should update the conversation window toolbar title",
+          function() {
+            conversation.trigger("session:network-disconnected");
+
+            expect(document.title).eql("generic_failure_title");
+          });
+      });
+
+      describe("Published and Subscribed Streams", function() {
+        beforeEach(function() {
+          icView._websocket = {
+            mediaUp: sinon.spy()
+          };
+        });
+
+        describe("publishStream", function() {
+          it("should not notify the websocket if only one stream is up",
+            function() {
+              conversation.set("publishedStream", true);
+
+              sinon.assert.notCalled(icView._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(icView._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(icView._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(icView._websocket.mediaUp);
+            });
+        });
+      });
+    });
+  });
+
+  describe("IncomingCallView", function() {
+    var view, model, fakeAudio;
+
+    beforeEach(function() {
+      var Model = Backbone.Model.extend({
+        getCallIdentifier: function() {return "fakeId";}
+      });
+      model = new Model();
+      sandbox.spy(model, "trigger");
+      sandbox.stub(model, "set");
+
+      fakeAudio = {
+        play: sinon.spy(),
+        pause: sinon.spy(),
+        removeAttribute: sinon.spy()
+      };
+      sandbox.stub(window, "Audio").returns(fakeAudio);
+
+      view = TestUtils.renderIntoDocument(
+        loop.conversationViews.IncomingCallView({
+          model: model,
+          video: true
+        }));
+    });
+
+    describe("default answer mode", function() {
+      it("should display video as primary answer mode", function() {
+        view = TestUtils.renderIntoDocument(
+          loop.conversationViews.IncomingCallView({
+            model: model,
+            video: true
+          }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-video');
+
+        expect(primaryBtn).not.to.eql(null);
+      });
+
+      it("should display audio as primary answer mode", function() {
+        view = TestUtils.renderIntoDocument(
+          loop.conversationViews.IncomingCallView({
+            model: model,
+            video: false
+          }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-audio');
+
+        expect(primaryBtn).not.to.eql(null);
+      });
+
+      it("should accept call with video", function() {
+        view = TestUtils.renderIntoDocument(
+          loop.conversationViews.IncomingCallView({
+            model: model,
+            video: true
+          }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-video');
+
+        React.addons.TestUtils.Simulate.click(primaryBtn);
+
+        sinon.assert.calledOnce(model.set);
+        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWithExactly(model.trigger, "accept");
+      });
+
+      it("should accept call with audio", function() {
+        view = TestUtils.renderIntoDocument(
+          loop.conversationViews.IncomingCallView({
+            model: model,
+            video: false
+          }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-audio');
+
+        React.addons.TestUtils.Simulate.click(primaryBtn);
+
+        sinon.assert.calledOnce(model.set);
+        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWithExactly(model.trigger, "accept");
+      });
+
+      it("should accept call with video when clicking on secondary btn",
+         function() {
+           view = TestUtils.renderIntoDocument(
+             loop.conversationViews.IncomingCallView({
+               model: model,
+               video: false
+             }));
+           var secondaryBtn = view.getDOMNode()
+           .querySelector('.fx-embedded-btn-video-small');
+
+           React.addons.TestUtils.Simulate.click(secondaryBtn);
+
+           sinon.assert.calledOnce(model.set);
+           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
+           sinon.assert.calledOnce(model.trigger);
+           sinon.assert.calledWithExactly(model.trigger, "accept");
+         });
+
+      it("should accept call with audio when clicking on secondary btn",
+         function() {
+           view = TestUtils.renderIntoDocument(
+             loop.conversationViews.IncomingCallView({
+               model: model,
+               video: true
+             }));
+           var secondaryBtn = view.getDOMNode()
+           .querySelector('.fx-embedded-btn-audio-small');
+
+           React.addons.TestUtils.Simulate.click(secondaryBtn);
+
+           sinon.assert.calledOnce(model.set);
+           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
+           sinon.assert.calledOnce(model.trigger);
+           sinon.assert.calledWithExactly(model.trigger, "accept");
+         });
+    });
+
+    describe("click event on .btn-accept", function() {
+      it("should trigger an 'accept' conversation model event", function () {
+        var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
+        model.trigger.withArgs("accept");
+        TestUtils.Simulate.click(buttonAccept);
+
+        /* Setting a model property triggers 2 events */
+        sinon.assert.calledOnce(model.trigger.withArgs("accept"));
+      });
+
+      it("should set selectedCallType to audio-video", function () {
+        var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
+
+        TestUtils.Simulate.click(buttonAccept);
+
+        sinon.assert.calledOnce(model.set);
+        sinon.assert.calledWithExactly(model.set, "selectedCallType",
+          "audio-video");
+      });
+    });
+
+    describe("click event on .btn-decline", function() {
+      it("should trigger an 'decline' conversation model event", function() {
+        var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
+
+        TestUtils.Simulate.click(buttonDecline);
+
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWith(model.trigger, "decline");
+        });
+    });
+
+    describe("click event on .btn-block", function() {
+      it("should trigger a 'block' conversation model event", function() {
+        var buttonBlock = view.getDOMNode().querySelector(".btn-block");
+
+        TestUtils.Simulate.click(buttonBlock);
+
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWith(model.trigger, "declineAndBlock");
+      });
+    });
+  });
+
+  describe("GenericFailureView", function() {
+    var view, fakeAudio;
+
+    beforeEach(function() {
+      fakeAudio = {
+        play: sinon.spy(),
+        pause: sinon.spy(),
+        removeAttribute: sinon.spy()
+      };
+      navigator.mozLoop.doNotDisturb = false;
+      sandbox.stub(window, "Audio").returns(fakeAudio);
+
+      view = TestUtils.renderIntoDocument(
+        loop.conversationViews.GenericFailureView({
+          cancelCall: function() {}
+        })
+      );
+    });
+
+    it("should play a failure sound, once", function() {
+      sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
+      sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
+                                     "failure", sinon.match.func);
+      sinon.assert.calledOnce(fakeAudio.play);
+      expect(fakeAudio.loop).to.equal(false);
+    });
+
+  });
 });
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -5,36 +5,21 @@
 /* global loop, sinon, React, TestUtils */
 
 var expect = chai.expect;
 
 describe("loop.conversation", function() {
   "use strict";
 
   var sharedModels = loop.shared.models,
-      sharedView = loop.shared.views,
       fakeWindow,
       sandbox;
 
-  // XXX refactor to Just Work with "sandbox.stubComponent" or else
-  // just pass in the sandbox and put somewhere generally usable
-
-  function stubComponent(obj, component, mockTagName){
-    var reactClass = React.createClass({
-      render: function() {
-        var mockTagName = mockTagName || "div";
-        return React.DOM[mockTagName](null, this.props.children);
-      }
-    });
-    return sandbox.stub(obj, component, reactClass);
-  }
-
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
-    sandbox.useFakeTimers();
 
     navigator.mozLoop = {
       doNotDisturb: true,
       getStrings: function() {
         return JSON.stringify({textContent: "fakeText"});
       },
       get locale() {
         return "en-US";
@@ -42,19 +27,16 @@ describe("loop.conversation", function()
       setLoopPref: sinon.stub(),
       getLoopPref: function(prefName) {
         if (prefName == "debug.sdk") {
           return false;
         }
 
         return "http://fake";
       },
-      calls: {
-        clearCallInProgress: sinon.stub()
-      },
       LOOP_SESSION_TYPE: {
         GUEST: 1,
         FXA: 2
       },
       startAlerting: sinon.stub(),
       stopAlerting: sinon.stub(),
       ensureRegistered: sinon.stub(),
       get appVersionInfo() {
@@ -215,17 +197,17 @@ describe("loop.conversation", function()
         },
         on: sandbox.spy()
       });
       conversationAppStore.setStoreState({windowType: "incoming"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversation.IncomingConversationView);
+        loop.conversationViews.IncomingConversationView);
     });
 
     it("should display the RoomView for rooms", function() {
       conversationAppStore.setStoreState({windowType: "room"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
@@ -233,722 +215,12 @@ describe("loop.conversation", function()
     });
 
     it("should display the GenericFailureView for failures", function() {
       conversationAppStore.setStoreState({windowType: "failed"});
 
       ccView = mountTestComponent();
 
       TestUtils.findRenderedComponentWithType(ccView,
-        loop.conversation.GenericFailureView);
+        loop.conversationViews.GenericFailureView);
     });
   });
-
-  describe("IncomingConversationView", function() {
-    var conversationAppStore, conversation, client, icView, oldTitle,
-        feedbackStore;
-
-    function mountTestComponent() {
-      return TestUtils.renderIntoDocument(
-        loop.conversation.IncomingConversationView({
-          client: client,
-          conversation: conversation,
-          sdk: {},
-          conversationAppStore: conversationAppStore,
-          feedbackStore: feedbackStore
-        }));
-    }
-
-    beforeEach(function() {
-      oldTitle = document.title;
-      client = new loop.Client();
-      conversation = new loop.shared.models.ConversationModel({}, {
-        sdk: {}
-      });
-      conversation.set({windowId: 42});
-      var dispatcher = new loop.Dispatcher();
-      conversationAppStore = new loop.store.ConversationAppStore({
-        dispatcher: dispatcher,
-        mozLoop: navigator.mozLoop
-      });
-      feedbackStore = new loop.store.FeedbackStore(dispatcher, {
-        feedbackClient: {}
-      });
-      sandbox.stub(conversation, "setOutgoingSessionData");
-    });
-
-    afterEach(function() {
-      icView = undefined;
-      document.title = oldTitle;
-    });
-
-    describe("start", function() {
-      it("should set the title to incoming_call_title2", function() {
-        conversationAppStore.setStoreState({
-          windowData: {
-            progressURL:    "fake",
-            websocketToken: "fake",
-            callId: 42
-          }
-        });
-
-        icView = mountTestComponent();
-
-        expect(document.title).eql("incoming_call_title2");
-      });
-    });
-
-    describe("componentDidMount", function() {
-      var fakeSessionData, promise, resolveWebSocketConnect;
-      var rejectWebSocketConnect;
-
-      beforeEach(function() {
-        fakeSessionData  = {
-          sessionId:      "sessionId",
-          sessionToken:   "sessionToken",
-          apiKey:         "apiKey",
-          callType:       "callType",
-          callId:         "Hello",
-          progressURL:    "http://progress.example.com",
-          websocketToken: "7b"
-        };
-
-        conversationAppStore.setStoreState({
-          windowData: fakeSessionData
-        });
-
-        stubComponent(loop.conversation, "IncomingCallView");
-        stubComponent(sharedView, "ConversationView");
-      });
-
-      it("should start alerting", function() {
-        icView = mountTestComponent();
-
-        sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
-      });
-
-      describe("Session Data setup", function() {
-        beforeEach(function() {
-          sandbox.stub(loop, "CallConnectionWebSocket").returns({
-            promiseConnect: function () {
-              promise = new Promise(function(resolve, reject) {
-                resolveWebSocketConnect = resolve;
-                rejectWebSocketConnect = reject;
-              });
-              return promise;
-            },
-            on: sinon.stub()
-          });
-        });
-
-        it("should store the session data", function() {
-          sandbox.stub(conversation, "setIncomingSessionData");
-
-          icView = mountTestComponent();
-
-          sinon.assert.calledOnce(conversation.setIncomingSessionData);
-          sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
-                                         fakeSessionData);
-        });
-
-        it("should setup the websocket connection", function() {
-          icView = mountTestComponent();
-
-          sinon.assert.calledOnce(loop.CallConnectionWebSocket);
-          sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
-            callId: "Hello",
-            url: "http://progress.example.com",
-            websocketToken: "7b"
-          });
-        });
-      });
-
-      describe("WebSocket Handling", function() {
-        beforeEach(function() {
-          promise = new Promise(function(resolve, reject) {
-            resolveWebSocketConnect = resolve;
-            rejectWebSocketConnect = reject;
-          });
-
-          sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
-        });
-
-        it("should set the state to incoming on success", function(done) {
-          icView = mountTestComponent();
-          resolveWebSocketConnect("incoming");
-
-          promise.then(function () {
-            expect(icView.state.callStatus).eql("incoming");
-            done();
-          });
-        });
-
-        it("should set the state to close on success if the progress " +
-          "state is terminated", function(done) {
-            icView = mountTestComponent();
-            resolveWebSocketConnect("terminated");
-
-            promise.then(function () {
-              expect(icView.state.callStatus).eql("close");
-              done();
-            });
-          });
-
-        // XXX implement me as part of bug 1047410
-        // see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
-        it.skip("should should switch view state to failed", function(done) {
-          icView = mountTestComponent();
-          rejectWebSocketConnect();
-
-          promise.then(function() {}, function() {
-            done();
-          });
-        });
-      });
-
-      describe("WebSocket Events", function() {
-        describe("Call cancelled or timed out before acceptance", function() {
-          beforeEach(function() {
-            // Mounting the test component automatically calls the required
-            // setup functions
-            icView = mountTestComponent();
-            promise = new Promise(function(resolve, reject) {
-              resolve();
-            });
-
-            sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
-            sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
-          });
-
-          describe("progress - terminated (previousState = alerting)", function() {
-            it("should stop alerting", function(done) {
-              promise.then(function() {
-                icView._websocket.trigger("progress", {
-                  state: "terminated",
-                  reason: "timeout"
-                }, "alerting");
-
-                sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-                done();
-              });
-            });
-
-            it("should close the websocket", function(done) {
-              promise.then(function() {
-                icView._websocket.trigger("progress", {
-                  state: "terminated",
-                  reason: "closed"
-                }, "alerting");
-
-                sinon.assert.calledOnce(icView._websocket.close);
-                done();
-              });
-            });
-
-            it("should close the window", function(done) {
-              promise.then(function() {
-                icView._websocket.trigger("progress", {
-                  state: "terminated",
-                  reason: "answered-elsewhere"
-                }, "alerting");
-
-                sandbox.clock.tick(1);
-
-                sinon.assert.calledOnce(fakeWindow.close);
-                done();
-              });
-            });
-          });
-
-
-          describe("progress - terminated (previousState not init" +
-                   " nor alerting)",
-            function() {
-              it("should set the state to end", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "media-fail"
-                  }, "connecting");
-
-                  expect(icView.state.callStatus).eql("end");
-                  done();
-                });
-              });
-
-              it("should stop alerting", function(done) {
-                promise.then(function() {
-                  icView._websocket.trigger("progress", {
-                    state: "terminated",
-                    reason: "media-fail"
-                  }, "connecting");
-
-                  sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-                  done();
-                });
-              });
-            });
-        });
-      });
-
-      describe("#accept", function() {
-        beforeEach(function() {
-          icView = mountTestComponent();
-          conversation.setIncomingSessionData({
-            sessionId:      "sessionId",
-            sessionToken:   "sessionToken",
-            apiKey:         "apiKey",
-            callType:       "callType",
-            callId:         "Hello",
-            progressURL:    "http://progress.example.com",
-            websocketToken: 123
-          });
-
-          sandbox.stub(icView._websocket, "accept");
-          sandbox.stub(icView.props.conversation, "accepted");
-        });
-
-        it("should initiate the conversation", function() {
-          icView.accept();
-
-          sinon.assert.calledOnce(icView.props.conversation.accepted);
-        });
-
-        it("should notify the websocket of the user acceptance", function() {
-          icView.accept();
-
-          sinon.assert.calledOnce(icView._websocket.accept);
-        });
-
-        it("should stop alerting", function() {
-          icView.accept();
-
-          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-        });
-      });
-
-      describe("#decline", function() {
-        beforeEach(function() {
-          icView = mountTestComponent();
-
-          icView._websocket = {
-            decline: sinon.stub(),
-            close: sinon.stub()
-          };
-          conversation.set({
-            windowId: "8699"
-          });
-          conversation.setIncomingSessionData({
-            websocketToken: 123
-          });
-        });
-
-        it("should close the window", function() {
-          icView.decline();
-
-          sandbox.clock.tick(1);
-
-          sinon.assert.calledOnce(fakeWindow.close);
-        });
-
-        it("should stop alerting", function() {
-          icView.decline();
-
-          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-        });
-
-        it("should release callData", function() {
-          icView.decline();
-
-          sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
-          sinon.assert.calledWithExactly(
-            navigator.mozLoop.calls.clearCallInProgress, "8699");
-        });
-      });
-
-      describe("#blocked", function() {
-        var mozLoop, deleteCallUrlStub;
-
-        beforeEach(function() {
-          icView = mountTestComponent();
-
-          icView._websocket = {
-            decline: sinon.spy(),
-            close: sinon.stub()
-          };
-
-          mozLoop = {
-            LOOP_SESSION_TYPE: {
-              GUEST: 1,
-              FXA: 2
-            }
-          };
-
-          deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
-                                           "deleteCallUrl");
-        });
-
-        it("should call mozLoop.stopAlerting", function() {
-          icView.declineAndBlock();
-
-          sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
-        });
-
-        it("should call delete call", function() {
-          sandbox.stub(conversation, "get").withArgs("callToken")
-                                           .returns("fakeToken")
-                                           .withArgs("sessionType")
-                                           .returns(mozLoop.LOOP_SESSION_TYPE.FXA);
-
-          icView.declineAndBlock();
-
-          sinon.assert.calledOnce(deleteCallUrlStub);
-          sinon.assert.calledWithExactly(deleteCallUrlStub,
-            "fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
-        });
-
-        it("should get callToken from conversation model", function() {
-          sandbox.stub(conversation, "get");
-          icView.declineAndBlock();
-
-          sinon.assert.called(conversation.get);
-          sinon.assert.calledWithExactly(conversation.get, "callToken");
-          sinon.assert.calledWithExactly(conversation.get, "windowId");
-        });
-
-        it("should trigger error handling in case of error", function() {
-          // XXX just logging to console for now
-          var log = sandbox.stub(console, "log");
-          var fakeError = {
-            error: true
-          };
-          deleteCallUrlStub.callsArgWith(2, fakeError);
-          icView.declineAndBlock();
-
-          sinon.assert.calledOnce(log);
-          sinon.assert.calledWithExactly(log, fakeError);
-        });
-
-        it("should close the window", function() {
-          icView.declineAndBlock();
-
-          sandbox.clock.tick(1);
-
-          sinon.assert.calledOnce(fakeWindow.close);
-        });
-      });
-    });
-
-    describe("Events", function() {
-      var fakeSessionData;
-
-      beforeEach(function() {
-
-        fakeSessionData = {
-          sessionId:    "sessionId",
-          sessionToken: "sessionToken",
-          apiKey:       "apiKey"
-        };
-
-        conversationAppStore.setStoreState({
-          windowData: fakeSessionData
-        });
-
-        sandbox.stub(conversation, "setIncomingSessionData");
-        sandbox.stub(loop, "CallConnectionWebSocket").returns({
-          promiseConnect: function() {
-            return new Promise(function() {});
-          },
-          on: sandbox.spy()
-        });
-
-        icView = mountTestComponent();
-
-        conversation.set("loopToken", "fakeToken");
-        stubComponent(sharedView, "ConversationView");
-      });
-
-      describe("call:accepted", function() {
-        it("should display the ConversationView",
-          function() {
-            conversation.accepted();
-
-            TestUtils.findRenderedComponentWithType(icView,
-              sharedView.ConversationView);
-          });
-
-        it("should set the title to the call identifier", function() {
-          sandbox.stub(conversation, "getCallIdentifier").returns("fakeId");
-
-          conversation.accepted();
-
-          expect(document.title).eql("fakeId");
-        });
-      });
-
-      describe("session:ended", function() {
-        it("should display the feedback view when the call session ends",
-          function() {
-            conversation.trigger("session:ended");
-
-            TestUtils.findRenderedComponentWithType(icView,
-              sharedView.FeedbackView);
-          });
-      });
-
-      describe("session:peer-hungup", function() {
-        it("should display the feedback view when the peer hangs up",
-          function() {
-            conversation.trigger("session:peer-hungup");
-
-              TestUtils.findRenderedComponentWithType(icView,
-                sharedView.FeedbackView);
-          });
-      });
-
-      describe("session:network-disconnected", function() {
-        it("should navigate to call failed when network disconnects",
-          function() {
-            conversation.trigger("session:network-disconnected");
-
-            TestUtils.findRenderedComponentWithType(icView,
-              loop.conversation.GenericFailureView);
-          });
-
-        it("should update the conversation window toolbar title",
-          function() {
-            conversation.trigger("session:network-disconnected");
-
-            expect(document.title).eql("generic_failure_title");
-          });
-      });
-
-      describe("Published and Subscribed Streams", function() {
-        beforeEach(function() {
-          icView._websocket = {
-            mediaUp: sinon.spy()
-          };
-        });
-
-        describe("publishStream", function() {
-          it("should not notify the websocket if only one stream is up",
-            function() {
-              conversation.set("publishedStream", true);
-
-              sinon.assert.notCalled(icView._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(icView._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(icView._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(icView._websocket.mediaUp);
-            });
-        });
-      });
-    });
-  });
-
-  describe("IncomingCallView", function() {
-    var view, model, fakeAudio;
-
-    beforeEach(function() {
-      var Model = Backbone.Model.extend({
-        getCallIdentifier: function() {return "fakeId";}
-      });
-      model = new Model();
-      sandbox.spy(model, "trigger");
-      sandbox.stub(model, "set");
-
-      fakeAudio = {
-        play: sinon.spy(),
-        pause: sinon.spy(),
-        removeAttribute: sinon.spy()
-      };
-      sandbox.stub(window, "Audio").returns(fakeAudio);
-
-      view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-        model: model,
-        video: true
-      }));
-    });
-
-    describe("default answer mode", function() {
-      it("should display video as primary answer mode", function() {
-        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-          model: model,
-          video: true
-        }));
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector('.fx-embedded-btn-icon-video');
-
-        expect(primaryBtn).not.to.eql(null);
-      });
-
-      it("should display audio as primary answer mode", function() {
-        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-          model: model,
-          video: false
-        }));
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector('.fx-embedded-btn-icon-audio');
-
-        expect(primaryBtn).not.to.eql(null);
-      });
-
-      it("should accept call with video", function() {
-        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-          model: model,
-          video: true
-        }));
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector('.fx-embedded-btn-icon-video');
-
-        React.addons.TestUtils.Simulate.click(primaryBtn);
-
-        sinon.assert.calledOnce(model.set);
-        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
-        sinon.assert.calledOnce(model.trigger);
-        sinon.assert.calledWithExactly(model.trigger, "accept");
-      });
-
-      it("should accept call with audio", function() {
-        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-          model: model,
-          video: false
-        }));
-        var primaryBtn = view.getDOMNode()
-                                  .querySelector('.fx-embedded-btn-icon-audio');
-
-        React.addons.TestUtils.Simulate.click(primaryBtn);
-
-        sinon.assert.calledOnce(model.set);
-        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
-        sinon.assert.calledOnce(model.trigger);
-        sinon.assert.calledWithExactly(model.trigger, "accept");
-      });
-
-      it("should accept call with video when clicking on secondary btn",
-         function() {
-           view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-             model: model,
-             video: false
-           }));
-           var secondaryBtn = view.getDOMNode()
-           .querySelector('.fx-embedded-btn-video-small');
-
-           React.addons.TestUtils.Simulate.click(secondaryBtn);
-
-           sinon.assert.calledOnce(model.set);
-           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
-           sinon.assert.calledOnce(model.trigger);
-           sinon.assert.calledWithExactly(model.trigger, "accept");
-         });
-
-      it("should accept call with audio when clicking on secondary btn",
-         function() {
-           view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-             model: model,
-             video: true
-           }));
-           var secondaryBtn = view.getDOMNode()
-           .querySelector('.fx-embedded-btn-audio-small');
-
-           React.addons.TestUtils.Simulate.click(secondaryBtn);
-
-           sinon.assert.calledOnce(model.set);
-           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
-           sinon.assert.calledOnce(model.trigger);
-           sinon.assert.calledWithExactly(model.trigger, "accept");
-         });
-    });
-
-    describe("click event on .btn-accept", function() {
-      it("should trigger an 'accept' conversation model event", function () {
-        var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
-        model.trigger.withArgs("accept");
-        TestUtils.Simulate.click(buttonAccept);
-
-        /* Setting a model property triggers 2 events */
-        sinon.assert.calledOnce(model.trigger.withArgs("accept"));
-      });
-
-      it("should set selectedCallType to audio-video", function () {
-        var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
-
-        TestUtils.Simulate.click(buttonAccept);
-
-        sinon.assert.calledOnce(model.set);
-        sinon.assert.calledWithExactly(model.set, "selectedCallType",
-          "audio-video");
-      });
-    });
-
-    describe("click event on .btn-decline", function() {
-      it("should trigger an 'decline' conversation model event", function() {
-        var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
-
-        TestUtils.Simulate.click(buttonDecline);
-
-        sinon.assert.calledOnce(model.trigger);
-        sinon.assert.calledWith(model.trigger, "decline");
-        });
-    });
-
-    describe("click event on .btn-block", function() {
-      it("should trigger a 'block' conversation model event", function() {
-        var buttonBlock = view.getDOMNode().querySelector(".btn-block");
-
-        TestUtils.Simulate.click(buttonBlock);
-
-        sinon.assert.calledOnce(model.trigger);
-        sinon.assert.calledWith(model.trigger, "declineAndBlock");
-      });
-    });
-  });
-
-  describe("GenericFailureView", function() {
-    var view, fakeAudio;
-
-    beforeEach(function() {
-      fakeAudio = {
-        play: sinon.spy(),
-        pause: sinon.spy(),
-        removeAttribute: sinon.spy()
-      };
-      navigator.mozLoop.doNotDisturb = false;
-      sandbox.stub(window, "Audio").returns(fakeAudio);
-
-      view = TestUtils.renderIntoDocument(
-        loop.conversation.GenericFailureView({
-          cancelCall: function() {}
-        })
-      );
-    });
-
-    it("should play a failure sound, once", function() {
-      sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
-      sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
-                                     "failure", sinon.match.func);
-      sinon.assert.calledOnce(fakeAudio.play);
-      expect(fakeAudio.loop).to.equal(false);
-    });
-
-  });
 });
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -316,27 +316,27 @@ describe("loop.roomViews", function () {
 
       it("should render the GenericFailureView if the roomState is `FAILED`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.FAILED});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
-            loop.conversation.GenericFailureView);
+            loop.conversationViews.GenericFailureView);
         });
 
       it("should render the GenericFailureView if the roomState is `FULL`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
 
           view = mountTestComponent();
 
           TestUtils.findRenderedComponentWithType(view,
-            loop.conversation.GenericFailureView);
+            loop.conversationViews.GenericFailureView);
         });
 
       it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
         function() {
           activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
 
           view = mountTestComponent();
 
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -13,17 +13,17 @@
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener('DOMContentLoaded', loop.panel.init);
   document.removeEventListener('DOMContentLoaded', loop.conversation.init);
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
-  var IncomingCallView = loop.conversation.IncomingCallView;
+  var IncomingCallView = loop.conversationViews.IncomingCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -13,17 +13,17 @@
   // Stop the default init functions running to avoid conflicts.
   document.removeEventListener('DOMContentLoaded', loop.panel.init);
   document.removeEventListener('DOMContentLoaded', loop.conversation.init);
 
   // 1. Desktop components
   // 1.1 Panel
   var PanelView = loop.panel.PanelView;
   // 1.2. Conversation Window
-  var IncomingCallView = loop.conversation.IncomingCallView;
+  var IncomingCallView = loop.conversationViews.IncomingCallView;
   var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
   var CallFailedView = loop.conversationViews.CallFailedView;
   var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
 
   // 2. Standalone webapp
   var HomeView = loop.webapp.HomeView;
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
--- a/browser/devtools/fontinspector/font-inspector.js
+++ b/browser/devtools/fontinspector/font-inspector.js
@@ -80,17 +80,17 @@ FontInspector.prototype = {
    */
   undim: function FI_undim() {
     this.chromeDoc.body.classList.remove("dim");
   },
 
  /**
   * Retrieve all the font info for the selected node and display it.
   */
-  update: Task.async(function*() {
+  update: Task.async(function*(showAllFonts) {
     let node = this.inspector.selection.nodeFront;
 
     if (!node ||
         !this.isActive() ||
         !this.inspector.selection.isConnected() ||
         !this.inspector.selection.isElementNode() ||
         this.chromeDoc.body.classList.contains("dim")) {
       return;
@@ -99,20 +99,26 @@ FontInspector.prototype = {
     this.chromeDoc.querySelector("#all-fonts").innerHTML = "";
 
     let fillStyle = (Services.prefs.getCharPref("devtools.theme") == "light") ?
         "black" : "white";
     let options = {
       includePreviews: true,
       previewFillStyle: fillStyle
     }
-
-    let fonts = yield this.pageStyle.getUsedFontFaces(node, options)
+    let fonts = [];
+    if (showAllFonts){
+      fonts = yield this.pageStyle.getAllUsedFontFaces(options)
                       .then(null, console.error);
-    if (!fonts) {
+    }
+    else{
+      fonts = yield this.pageStyle.getUsedFontFaces(node, options)
+                      .then(null, console.error);
+    }
+    if (!fonts || !fonts.length) {
       return;
     }
 
     for (let font of fonts) {
       font.previewUrl = yield font.preview.data.string();
     }
 
     // in case we've been destroyed in the meantime
@@ -164,31 +170,20 @@ FontInspector.prototype = {
     }
     let preview = s.querySelector(".font-preview");
     preview.src = font.previewUrl;
 
     this.chromeDoc.querySelector("#all-fonts").appendChild(s);
   },
 
   /**
-   * Select the <body> to show all the fonts included in the document.
+   * Show all fonts for the document (including iframes)
    */
   showAll: function FI_showAll() {
-    if (!this.isActive() ||
-        !this.inspector.selection.isConnected() ||
-        !this.inspector.selection.isElementNode()) {
-      return;
-    }
-
-    // Select the body node to show all fonts
-    let walker = this.inspector.walker;
-
-    walker.getRootNode().then(root => walker.querySelector(root, "body")).then(body => {
-      this.inspector.selection.setNodeFront(body, "fontinspector");
-    });
+    this.update(true);
   },
 }
 
 window.setPanel = function(panel) {
   window.fontInspector = new FontInspector(panel, window);
 }
 
 window.onunload = function() {
--- a/browser/devtools/fontinspector/test/browser.ini
+++ b/browser/devtools/fontinspector/test/browser.ini
@@ -1,9 +1,10 @@
 [DEFAULT]
 subsuite = devtools
 support-files =
   browser_fontinspector.html
+  test_iframe.html
   ostrich-black.ttf
   ostrich-regular.ttf
   head.js
 
 [browser_fontinspector.js]
--- a/browser/devtools/fontinspector/test/browser_fontinspector.html
+++ b/browser/devtools/fontinspector/test/browser_fontinspector.html
@@ -40,12 +40,13 @@
     font-family: bar;
     font-weight: 800;
   }
 </style>
 
 <body>
   BODY
   <div>DIV</div>
+  <iframe src="test_iframe.html"></iframe>
   <div class="normal-text">NORMAL DIV</div>
   <div class="bold-text">BOLD DIV</div>
   <div class="black-text">800 DIV</div>
 </body>
--- a/browser/devtools/fontinspector/test/browser_fontinspector.js
+++ b/browser/devtools/fontinspector/test/browser_fontinspector.js
@@ -102,12 +102,13 @@ function* testDivFonts(inspector) {
 
 function* testShowAllFonts(inspector) {
   info("testing showing all fonts");
 
   let updated = inspector.once("fontinspector-updated");
   viewDoc.querySelector("#showall").click();
   yield updated;
 
-  is(inspector.selection.nodeFront.nodeName, "BODY", "Show all fonts selected the body node");
+  // shouldn't change the node selection
+  is(inspector.selection.nodeFront.nodeName, "DIV", "Show all fonts selected");
   let sections = viewDoc.querySelectorAll("#all-fonts > section");
-  is(sections.length, 5, "And font-inspector still shows 5 fonts for body");
+  is(sections.length, 6, "Font inspector shows 6 fonts (1 from iframe)");
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/fontinspector/test/test_iframe.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+
+<style>
+  div{
+    font-family: "Times New Roman";
+  }
+</style>
+
+<body>
+  <div>Hello world</div>
+</body>
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,4 +1,4 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.0.978
+Current extension version is: 1.0.1040
 
--- a/browser/extensions/pdfjs/content/PdfJs.jsm
+++ b/browser/extensions/pdfjs/content/PdfJs.jsm
@@ -1,56 +1,64 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+/* jshint esnext:true */
+/* globals Components, Services, XPCOMUtils, PdfjsChromeUtils, PdfRedirector,
+           PdfjsContentUtils, DEFAULT_PREFERENCES, PdfStreamConverter */
 
-var EXPORTED_SYMBOLS = ["PdfJs"];
+'use strict';
+
+var EXPORTED_SYMBOLS = ['PdfJs'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cm = Components.manager;
 const Cu = Components.utils;
 
 const PREF_PREFIX = 'pdfjs';
 const PREF_DISABLED = PREF_PREFIX + '.disabled';
 const PREF_MIGRATION_VERSION = PREF_PREFIX + '.migrationVersion';
 const PREF_PREVIOUS_ACTION = PREF_PREFIX + '.previousHandler.preferredAction';
-const PREF_PREVIOUS_ASK = PREF_PREFIX + '.previousHandler.alwaysAskBeforeHandling';
+const PREF_PREVIOUS_ASK = PREF_PREFIX +
+                          '.previousHandler.alwaysAskBeforeHandling';
 const PREF_DISABLED_PLUGIN_TYPES = 'plugin.disable_full_page_plugin_for_types';
 const TOPIC_PDFJS_HANDLER_CHANGED = 'pdfjs:handlerChanged';
-const TOPIC_PLUGINS_LIST_UPDATED = "plugins-list-updated";
-const TOPIC_PLUGIN_INFO_UPDATED = "plugin-info-updated";
+const TOPIC_PLUGINS_LIST_UPDATED = 'plugins-list-updated';
+const TOPIC_PLUGIN_INFO_UPDATED = 'plugin-info-updated';
 const PDF_CONTENT_TYPE = 'application/pdf';
 
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import('resource://gre/modules/Services.jsm');
 
 let Svc = {};
 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
                                    '@mozilla.org/mime;1',
                                    'nsIMIMEService');
 XPCOMUtils.defineLazyServiceGetter(Svc, 'pluginHost',
                                    '@mozilla.org/plugin/host;1',
                                    'nsIPluginHost');
-XPCOMUtils.defineLazyModuleGetter(this, "PdfjsChromeUtils",
-                                  "resource://pdf.js/PdfjsChromeUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PdfjsContentUtils",
-                                  "resource://pdf.js/PdfjsContentUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsChromeUtils',
+                                  'resource://pdf.js/PdfjsChromeUtils.jsm');
+XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils',
+                                  'resource://pdf.js/PdfjsContentUtils.jsm');
 
 function getBoolPref(aPref, aDefaultValue) {
   try {
     return Services.prefs.getBoolPref(aPref);
   } catch (ex) {
     return aDefaultValue;
   }
 }
@@ -59,17 +67,17 @@ function getIntPref(aPref, aDefaultValue
   try {
     return Services.prefs.getIntPref(aPref);
   } catch (ex) {
     return aDefaultValue;
   }
 }
 
 function isDefaultHandler() {
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
    return PdfjsContentUtils.isDefaultHandlerApp();
  }
  return PdfjsChromeUtils.isDefaultHandlerApp();
 }
 
 function initializeDefaultPreferences() {
 
 var DEFAULT_PREFERENCES = {
@@ -129,18 +137,20 @@ Factory.prototype = {
 };
 
 let PdfJs = {
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
   _registered: false,
   _initialized: false,
 
   init: function init(remote) {
-    if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
-      throw new Error("PdfJs.init should only get called in the parent process.");
+    if (Services.appinfo.processType !==
+        Services.appinfo.PROCESS_TYPE_DEFAULT) {
+      throw new Error('PdfJs.init should only get called ' +
+                      'in the parent process.');
     }
     PdfjsChromeUtils.init();
     if (!remote) {
       PdfjsContentUtils.init();
     }
     this.initPrefs();
     this.updateRegistration();
   },
@@ -234,28 +244,29 @@ let PdfJs = {
     }
 
     if (types.indexOf(PDF_CONTENT_TYPE) === -1) {
       types.push(PDF_CONTENT_TYPE);
     }
     prefs.setCharPref(PREF_DISABLED_PLUGIN_TYPES, types.join(','));
 
     // Update the category manager in case the plugins are already loaded.
-    let categoryManager = Cc["@mozilla.org/categorymanager;1"];
+    let categoryManager = Cc['@mozilla.org/categorymanager;1'];
     categoryManager.getService(Ci.nsICategoryManager).
-                    deleteCategoryEntry("Gecko-Content-Viewers",
+                    deleteCategoryEntry('Gecko-Content-Viewers',
                                         PDF_CONTENT_TYPE,
                                         false);
   },
 
   // nsIObserver
   observe: function observe(aSubject, aTopic, aData) {
     this.updateRegistration();
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
-      let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm";
+    if (Services.appinfo.processType ===
+        Services.appinfo.PROCESS_TYPE_DEFAULT) {
+      let jsm = 'resource://pdf.js/PdfjsChromeUtils.jsm';
       let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils;
       PdfjsChromeUtils.notifyChildOfSettingsChange();
     }
   },
   
   /**
    * pdf.js is only enabled if it is both selected as the pdf viewer and if the 
    * global switch enabling it is true.
@@ -277,19 +288,19 @@ let PdfJs = {
       let disabledPluginTypes =
         Services.prefs.getCharPref(PREF_DISABLED_PLUGIN_TYPES).split(',');
       if (disabledPluginTypes.indexOf(PDF_CONTENT_TYPE) >= 0) {
         return true;
       }
     }
 
     // Check if there is an enabled pdf plugin.
-    // Note: this check is performed last because getPluginTags() triggers costly
-    // plugin list initialization (bug 881575)
-    let tags = Cc["@mozilla.org/plugin/host;1"].
+    // Note: this check is performed last because getPluginTags() triggers
+    // costly plugin list initialization (bug 881575)
+    let tags = Cc['@mozilla.org/plugin/host;1'].
                   getService(Ci.nsIPluginHost).
                   getPluginTags();
     let enabledPluginFound = tags.some(function(tag) {
       if (tag.disabled) {
         return false;
       }
       let mimeTypes = tag.getMimeTypes();
       return mimeTypes.some(function(mimeType) {
@@ -297,37 +308,37 @@ let PdfJs = {
       });
     });
 
     // Use pdf.js if pdf plugin is not present or disabled
     return !enabledPluginFound;
   },
 
   _ensureRegistered: function _ensureRegistered() {
-    if (this._registered)
+    if (this._registered) {
       return;
-
+    }
     this._pdfStreamConverterFactory = new Factory();
     Cu.import('resource://pdf.js/PdfStreamConverter.jsm');
     this._pdfStreamConverterFactory.register(PdfStreamConverter);
 
     this._pdfRedirectorFactory = new Factory();
     Cu.import('resource://pdf.js/PdfRedirector.jsm');
     this._pdfRedirectorFactory.register(PdfRedirector);
 
     Svc.pluginHost.registerPlayPreviewMimeType(PDF_CONTENT_TYPE, true,
       'data:application/x-moz-playpreview-pdfjs;,');
 
     this._registered = true;
   },
 
   _ensureUnregistered: function _ensureUnregistered() {
-    if (!this._registered)
+    if (!this._registered) {
       return;
-
+    }
     this._pdfStreamConverterFactory.unregister();
     Cu.unload('resource://pdf.js/PdfStreamConverter.jsm');
     delete this._pdfStreamConverterFactory;
 
     this._pdfRedirectorFactory.unregister();
     Cu.unload('resource://pdf.js/PdfRedirector.jsm');
     delete this._pdfRedirectorFactory;
 
--- a/browser/extensions/pdfjs/content/PdfJsTelemetry.jsm
+++ b/browser/extensions/pdfjs/content/PdfJsTelemetry.jsm
@@ -9,63 +9,64 @@
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* jshint esnext:true */
+/* jshint esnext:true, maxlen: 100 */
+/* globals Components, Services */
 
 'use strict';
 
 this.EXPORTED_SYMBOLS = ['PdfJsTelemetry'];
 
 const Cu = Components.utils;
 Cu.import('resource://gre/modules/Services.jsm');
 
 this.PdfJsTelemetry = {
   onViewerIsUsed: function () {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_USED");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_USED');
     histogram.add(true);
   },
   onFallback: function () {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FALLBACK_SHOWN");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FALLBACK_SHOWN');
     histogram.add(true);
   },
   onDocumentSize: function (size) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_SIZE_KB");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_SIZE_KB');
     histogram.add(size / 1024);
   },
   onDocumentVersion: function (versionId) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_VERSION");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_VERSION');
     histogram.add(versionId);
   },
   onDocumentGenerator: function (generatorId) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_GENERATOR");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_GENERATOR');
     histogram.add(generatorId);
   },
   onEmbed: function (isObject) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_EMBED");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_EMBED');
     histogram.add(isObject);
   },
   onFontType: function (fontTypeId) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FONT_TYPES");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FONT_TYPES');
     histogram.add(fontTypeId);
   },
   onForm: function (isAcroform) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FORM");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FORM');
     histogram.add(isAcroform);
   },
   onPrint: function () {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_PRINT");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_PRINT');
     histogram.add(true);
   },
   onStreamType: function (streamTypeId) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_STREAM_TYPES");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_STREAM_TYPES');
     histogram.add(streamTypeId);
   },
   onTimeToView: function (ms) {
-    let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_TIME_TO_VIEW_MS");
+    let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_TIME_TO_VIEW_MS');
     histogram.add(ms);
   }
 };
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -11,17 +11,17 @@
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 /* jshint esnext:true */
 /* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
-           dump, NetworkManager, PdfJsTelemetry */
+           dump, NetworkManager, PdfJsTelemetry, PdfjsContentUtils */
 
 'use strict';
 
 var EXPORTED_SYMBOLS = ['PdfStreamConverter'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
@@ -138,30 +138,33 @@ function getLocalizedStrings(path) {
   while (enumerator.hasMoreElements()) {
     var string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
     var key = string.key, property = 'textContent';
     var i = key.lastIndexOf('.');
     if (i >= 0) {
       property = key.substring(i + 1);
       key = key.substring(0, i);
     }
-    if (!(key in map))
+    if (!(key in map)) {
       map[key] = {};
+    }
     map[key][property] = string.value;
   }
   return map;
 }
 function getLocalizedString(strings, id, property) {
   property = property || 'textContent';
-  if (id in strings)
+  if (id in strings) {
     return strings[id][property];
+  }
   return id;
 }
 
 function makeContentReadable(obj, window) {
+  /* jshint -W027 */
   return Cu.cloneInto(obj, window);
 }
 
 // PDF data storage
 function PdfDataListener(length) {
   this.length = length; // less than 0, if length is unknown
   this.buffer = null;
   this.loaded = 0;
@@ -237,17 +240,17 @@ ChromeActions.prototype = {
   },
   download: function(data, sendResponse) {
     var self = this;
     var originalUrl = data.originalUrl;
     // The data may not be downloaded so we need just retry getting the pdf with
     // the original url.
     var originalUri = NetUtil.newURI(data.originalUrl);
     var filename = data.filename;
-    if (typeof filename !== 'string' || 
+    if (typeof filename !== 'string' ||
         (!/\.pdf$/i.test(filename) && !data.isAttachment)) {
       filename = 'document.pdf';
     }
     var blobUri = data.blobUrl ? NetUtil.newURI(data.blobUrl) : originalUri;
     var extHelperAppSvc =
           Cc['@mozilla.org/uriloader/external-helper-app-service;1'].
              getService(Ci.nsIExternalHelperAppService);
     var frontWindow = Cc['@mozilla.org/embedcomp/window-watcher;1'].
@@ -256,18 +259,19 @@ ChromeActions.prototype = {
     var docIsPrivate = this.isInPrivateBrowsing();
     var netChannel = NetUtil.newChannel(blobUri);
     if ('nsIPrivateBrowsingChannel' in Ci &&
         netChannel instanceof Ci.nsIPrivateBrowsingChannel) {
       netChannel.setPrivate(docIsPrivate);
     }
     NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
       if (!Components.isSuccessCode(aResult)) {
-        if (sendResponse)
+        if (sendResponse) {
           sendResponse(true);
+        }
         return;
       }
       // Create a nsIInputStreamChannel so we can set the url on the channel
       // so the filename will be correct.
       var channel = Cc['@mozilla.org/network/input-stream-channel;1'].
                        createInstance(Ci.nsIInputStreamChannel);
       channel.QueryInterface(Ci.nsIChannel);
       try {
@@ -291,21 +295,23 @@ ChromeActions.prototype = {
         onStartRequest: function(aRequest, aContext) {
           this.extListener = extHelperAppSvc.doContent(
             (data.isAttachment ? 'application/octet-stream' :
                                  'application/pdf'),
             aRequest, frontWindow, false);
           this.extListener.onStartRequest(aRequest, aContext);
         },
         onStopRequest: function(aRequest, aContext, aStatusCode) {
-          if (this.extListener)
+          if (this.extListener) {
             this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
+          }
           // Notify the content code we're done downloading.
-          if (sendResponse)
+          if (sendResponse) {
             sendResponse(false);
+          }
         },
         onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
                                   aCount) {
           this.extListener.onDataAvailable(aRequest, aContext, aInputStream,
                                            aOffset, aCount);
         }
       };
 
@@ -313,19 +319,19 @@ ChromeActions.prototype = {
     });
   },
   getLocale: function() {
     return getStringPref('general.useragent.locale', 'en-US');
   },
   getStrings: function(data) {
     try {
       // Lazy initialization of localizedStrings
-      if (!('localizedStrings' in this))
+      if (!('localizedStrings' in this)) {
         this.localizedStrings = getLocalizedStrings('viewer.properties');
-
+      }
       var result = this.localizedStrings[data];
       return JSON.stringify(result || null);
     } catch (e) {
       log('Unable to retrive localized strings: ' + e);
       return 'null';
     }
   },
   supportsIntegratedFind: function() {
@@ -368,31 +374,31 @@ ChromeActions.prototype = {
       case 'documentStats':
         // documentStats can be called several times for one documents.
         // if stream/font types are reported, trying not to submit the same
         // enumeration value multiple times.
         var documentStats = probeInfo.stats;
         if (!documentStats || typeof documentStats !== 'object') {
           break;
         }
-        var streamTypes = documentStats.streamTypes;
+        var i, streamTypes = documentStats.streamTypes;
         if (Array.isArray(streamTypes)) {
           var STREAM_TYPE_ID_LIMIT = 20;
-          for (var i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
+          for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
             if (streamTypes[i] &&
                 !this.telemetryState.streamTypesUsed[i]) {
               PdfJsTelemetry.onStreamType(i);
               this.telemetryState.streamTypesUsed[i] = true;
             }
           }
         }
         var fontTypes = documentStats.fontTypes;
         if (Array.isArray(fontTypes)) {
           var FONT_TYPE_ID_LIMIT = 20;
-          for (var i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
+          for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
             if (fontTypes[i] &&
                 !this.telemetryState.fontTypesUsed[i]) {
               PdfJsTelemetry.onFontType(i);
               this.telemetryState.fontTypesUsed[i] = true;
             }
           }
         }
         break;
@@ -415,18 +421,19 @@ ChromeActions.prototype = {
       message = getLocalizedString(strings, 'unsupported_feature');
     }
     PdfJsTelemetry.onFallback();
     PdfjsContentUtils.displayWarning(domWindow, message, sendResponse,
       getLocalizedString(strings, 'open_with_different_viewer'),
       getLocalizedString(strings, 'open_with_different_viewer', 'accessKey'));
   },
   updateFindControlState: function(data) {
-    if (!this.supportsIntegratedFind())
+    if (!this.supportsIntegratedFind()) {
       return;
+    }
     // Verify what we're sending to the findbar.
     var result = data.result;
     var findPrevious = data.findPrevious;
     var findPreviousType = typeof findPrevious;
     if ((typeof result !== 'number' || result < 0 || result > 3) ||
         (findPreviousType !== 'undefined' && findPreviousType !== 'boolean')) {
       return;
     }
@@ -701,29 +708,30 @@ RequestListener.prototype.receive = func
   var action = event.detail.action;
   var data = event.detail.data;
   var sync = event.detail.sync;
   var actions = this.actions;
   if (!(action in actions)) {
     log('Unknown action: ' + action);
     return;
   }
+  var response;
   if (sync) {
-    var response = actions[action].call(this.actions, data);
+    response = actions[action].call(this.actions, data);
     event.detail.response = response;
   } else {
-    var response;
     if (!event.detail.responseExpected) {
       doc.documentElement.removeChild(message);
       response = null;
     } else {
       response = function sendResponse(response) {
         try {
           var listener = doc.createEvent('CustomEvent');
-          let detail = makeContentReadable({response: response}, doc.defaultView);
+          let detail = makeContentReadable({response: response},
+                                           doc.defaultView);
           listener.initCustomEvent('pdf.js.response', true, false, detail);
           return message.dispatchEvent(listener);
         } catch (e) {
           // doc is no longer accessible because the requestor is already
           // gone. unloaded content cannot receive the response anyway.
           return false;
         }
       };
@@ -982,17 +990,18 @@ PdfStreamConverter.prototype = {
 
   // nsIRequestObserver::onStopRequest
   onStopRequest: function(aRequest, aContext, aStatusCode) {
     if (!this.dataListener) {
       // Do nothing
       return;
     }
 
-    if (Components.isSuccessCode(aStatusCode))
+    if (Components.isSuccessCode(aStatusCode)) {
       this.dataListener.finish();
-    else
+    } else {
       this.dataListener.error(aStatusCode);
+    }
     delete this.dataListener;
     delete this.binaryStream;
   }
 };
 
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -9,17 +9,18 @@
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
- /*globals DEFAULT_PREFERENCES */
+/* jshint esnext:true */
+/* globals Components, Services, XPCOMUtils, DEFAULT_PREFERENCES */
 
 'use strict';
 
 var EXPORTED_SYMBOLS = ['PdfjsChromeUtils'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
@@ -62,110 +63,113 @@ let PdfjsChromeUtils = {
 
   /*
    * Public API
    */
 
   init: function () {
     if (!this._ppmm) {
       // global parent process message manager (PPMM)
-      this._ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
-      this._ppmm.addMessageListener("PDFJS:Parent:clearUserPref", this);
-      this._ppmm.addMessageListener("PDFJS:Parent:setIntPref", this);
-      this._ppmm.addMessageListener("PDFJS:Parent:setBoolPref", this);
-      this._ppmm.addMessageListener("PDFJS:Parent:setCharPref", this);
-      this._ppmm.addMessageListener("PDFJS:Parent:setStringPref", this);
-      this._ppmm.addMessageListener("PDFJS:Parent:isDefaultHandlerApp", this);
+      this._ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
+        getService(Ci.nsIMessageBroadcaster);
+      this._ppmm.addMessageListener('PDFJS:Parent:clearUserPref', this);
+      this._ppmm.addMessageListener('PDFJS:Parent:setIntPref', this);
+      this._ppmm.addMessageListener('PDFJS:Parent:setBoolPref', this);
+      this._ppmm.addMessageListener('PDFJS:Parent:setCharPref', this);
+      this._ppmm.addMessageListener('PDFJS:Parent:setStringPref', this);
+      this._ppmm.addMessageListener('PDFJS:Parent:isDefaultHandlerApp', this);
 
       // global dom message manager (MMg)
-      this._mmg = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
-      this._mmg.addMessageListener("PDFJS:Parent:getChromeWindow", this);
-      this._mmg.addMessageListener("PDFJS:Parent:getFindBar", this);
-      this._mmg.addMessageListener("PDFJS:Parent:displayWarning", this);
+      this._mmg = Cc['@mozilla.org/globalmessagemanager;1'].
+        getService(Ci.nsIMessageListenerManager);
+      this._mmg.addMessageListener('PDFJS:Parent:getChromeWindow', this);
+      this._mmg.addMessageListener('PDFJS:Parent:getFindBar', this);
+      this._mmg.addMessageListener('PDFJS:Parent:displayWarning', this);
 
       // observer to handle shutdown
-      Services.obs.addObserver(this, "quit-application", false);
+      Services.obs.addObserver(this, 'quit-application', false);
     }
   },
 
   uninit: function () {
     if (this._ppmm) {
-      this._ppmm.removeMessageListener("PDFJS:Parent:clearUserPref", this);
-      this._ppmm.removeMessageListener("PDFJS:Parent:setIntPref", this);
-      this._ppmm.removeMessageListener("PDFJS:Parent:setBoolPref", this);
-      this._ppmm.removeMessageListener("PDFJS:Parent:setCharPref", this);
-      this._ppmm.removeMessageListener("PDFJS:Parent:setStringPref", this);
-      this._ppmm.removeMessageListener("PDFJS:Parent:isDefaultHandlerApp", this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:clearUserPref', this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:setIntPref', this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:setBoolPref', this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:setCharPref', this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:setStringPref', this);
+      this._ppmm.removeMessageListener('PDFJS:Parent:isDefaultHandlerApp',
+                                       this);
 
-      this._mmg.removeMessageListener("PDFJS:Parent:getChromeWindow", this);
-      this._mmg.removeMessageListener("PDFJS:Parent:getFindBar", this);
-      this._mmg.removeMessageListener("PDFJS:Parent:displayWarning", this);
+      this._mmg.removeMessageListener('PDFJS:Parent:getChromeWindow', this);
+      this._mmg.removeMessageListener('PDFJS:Parent:getFindBar', this);
+      this._mmg.removeMessageListener('PDFJS:Parent:displayWarning', this);
 
-      Services.obs.removeObserver(this, "quit-application", false);
+      Services.obs.removeObserver(this, 'quit-application', false);
 
       this._mmg = null;
       this._ppmm = null;
     }
   },
 
   /*
    * Called by the main module when preference changes are picked up
    * in the parent process. Observers don't propagate so we need to
    * instruct the child to refresh its configuration and (possibly)
    * the module's registration.
    */
   notifyChildOfSettingsChange: function () {
-    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT &&
-        this._ppmm) {
+    if (Services.appinfo.processType ===
+        Services.appinfo.PROCESS_TYPE_DEFAULT && this._ppmm) {
       // XXX kinda bad, we want to get the parent process mm associated
       // with the content process. _ppmm is currently the global process
       // manager, which means this is going to fire to every child process
       // we have open. Unfortunately I can't find a way to get at that
       // process specific mm from js.
-      this._ppmm.broadcastAsyncMessage("PDFJS:Child:refreshSettings", {});
+      this._ppmm.broadcastAsyncMessage('PDFJS:Child:refreshSettings', {});
     }
   },
 
   /*
    * Events
    */
 
   observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "quit-application") {
+    if (aTopic === 'quit-application') {
       this.uninit();
     }
   },
 
   receiveMessage: function (aMsg) {
     switch (aMsg.name) {
-      case "PDFJS:Parent:clearUserPref":
+      case 'PDFJS:Parent:clearUserPref':
         this._clearUserPref(aMsg.data.name);
         break;
-      case "PDFJS:Parent:setIntPref":
+      case 'PDFJS:Parent:setIntPref':
         this._setIntPref(aMsg.data.name, aMsg.data.value);
         break;
-      case "PDFJS:Parent:setBoolPref":
+      case 'PDFJS:Parent:setBoolPref':
         this._setBoolPref(aMsg.data.name, aMsg.data.value);
         break;
-      case "PDFJS:Parent:setCharPref":
+      case 'PDFJS:Parent:setCharPref':
         this._setCharPref(aMsg.data.name, aMsg.data.value);
         break;
-      case "PDFJS:Parent:setStringPref":
+      case 'PDFJS:Parent:setStringPref':
         this._setStringPref(aMsg.data.name, aMsg.data.value);
         break;
-      case "PDFJS:Parent:isDefaultHandlerApp":
+      case 'PDFJS:Parent:isDefaultHandlerApp':
         return this.isDefaultHandlerApp();
-      case "PDFJS:Parent:displayWarning":
+      case 'PDFJS:Parent:displayWarning':
         this._displayWarning(aMsg);
         break;
 
       // CPOW getters
-      case "PDFJS:Parent:getChromeWindow":
+      case 'PDFJS:Parent:getChromeWindow':
         return this._getChromeWindow(aMsg);
-      case "PDFJS:Parent:getFindBar":
+      case 'PDFJS:Parent:getFindBar':
         return this._getFindBar(aMsg);
     }
   },
 
   /*
    * Internal
    */
 
@@ -188,18 +192,18 @@ let PdfjsChromeUtils = {
     suitcase.setFindBar(wrapper);
     return true;
   },
 
   _ensurePreferenceAllowed: function (aPrefName) {
     let unPrefixedName = aPrefName.split(PREF_PREFIX + '.');
     if (unPrefixedName[0] !== '' ||
         this._allowedPrefNames.indexOf(unPrefixedName[1]) === -1) {
-      let msg = "'" + aPrefName + "' ";
-      msg += "can't be accessed from content. See PdfjsChromeUtils." 
+      let msg = '"' + aPrefName + '" ' +
+                'can\'t be accessed from content. See PdfjsChromeUtils.';
       throw new Error(msg);
     }
   },
 
   _clearUserPref: function (aPrefName) {
     this._ensurePreferenceAllowed(aPrefName);
     Services.prefs.clearUserPref(aPrefName);
   },
@@ -229,18 +233,18 @@ let PdfjsChromeUtils = {
 
   /*
    * Svc.mime doesn't have profile information in the child, so
    * we bounce this pdfjs enabled configuration check over to the
    * parent.
    */
   isDefaultHandlerApp: function () {
     var handlerInfo = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf');
-    return !handlerInfo.alwaysAskBeforeHandling &&
-           handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally;
+    return (!handlerInfo.alwaysAskBeforeHandling &&
+            handlerInfo.preferredAction === Ci.nsIHandlerInfo.handleInternally);
   },
 
   /*
    * Display a notification warning when the renderer isn't sure
    * a pdf displayed correctly.
    */
   _displayWarning: function (aMsg) {
     let json = aMsg.data;
@@ -285,47 +289,47 @@ let PdfjsChromeUtils = {
  * directly on the findbar, so we wrap the findbar in a smaller
  * object here that supports the features pdf.js needs.
  */
 function PdfjsFindbarWrapper(aBrowser) {
   let tabbrowser = aBrowser.getTabBrowser();
   let tab;
   tab = tabbrowser.getTabForBrowser(aBrowser);
   this._findbar = tabbrowser.getFindBar(tab);
-};
+}
 
 PdfjsFindbarWrapper.prototype = {
   __exposedProps__: {
-    addEventListener: "r",
-    removeEventListener: "r",
-    updateControlState: "r",
+    addEventListener: 'r',
+    removeEventListener: 'r',
+    updateControlState: 'r',
   },
   _findbar: null,
 
   updateControlState: function (aResult, aFindPrevious) {
     this._findbar.updateControlState(aResult, aFindPrevious);
   },
 
   addEventListener: function (aType, aListener, aUseCapture, aWantsUntrusted) {
-    this._findbar.addEventListener(aType, aListener, aUseCapture, aWantsUntrusted);
+    this._findbar.addEventListener(aType, aListener, aUseCapture,
+                                   aWantsUntrusted);
   },
 
   removeEventListener: function (aType, aListener, aUseCapture) {
     this._findbar.removeEventListener(aType, aListener, aUseCapture);
   }
 };
 
 function PdfjsWindowWrapper(aBrowser) {
   this._window = aBrowser.ownerDocument.defaultView;
-};
+}
 
 PdfjsWindowWrapper.prototype = {
   __exposedProps__: {
-    valueOf: "r",
+    valueOf: 'r',
   },
   _window: null,
 
   valueOf: function () {
     return this._window.valueOf();
   }
 };
 
-
--- a/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm
@@ -1,22 +1,26 @@
+/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+/* jshint esnext:true */
+/* globals Components, Services, XPCOMUtils */
 
 'use strict';
 
 var EXPORTED_SYMBOLS = ['PdfjsContentUtils'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
@@ -28,120 +32,123 @@ Cu.import('resource://gre/modules/Servic
 let PdfjsContentUtils = {
   _mm: null,
 
   /*
    * Public API
    */
 
   get isRemote() {
-    return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
+    return (Services.appinfo.processType ===
+            Services.appinfo.PROCESS_TYPE_CONTENT);
   },
 
   init: function () {
     // child *process* mm, or when loaded into the parent for in-content
     // support the psuedo child process mm 'child PPMM'.
     if (!this._mm) {
-      this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
-      this._mm.addMessageListener("PDFJS:Child:refreshSettings", this);
-      Services.obs.addObserver(this, "quit-application", false);
+      this._mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+        getService(Ci.nsISyncMessageSender);
+      this._mm.addMessageListener('PDFJS:Child:refreshSettings', this);
+      Services.obs.addObserver(this, 'quit-application', false);
     }
   },
 
   uninit: function () {
     if (this._mm) {
-      this._mm.removeMessageListener("PDFJS:Child:refreshSettings", this);
-      Services.obs.removeObserver(this, "quit-application");
+      this._mm.removeMessageListener('PDFJS:Child:refreshSettings', this);
+      Services.obs.removeObserver(this, 'quit-application');
     }
     this._mm = null;
   },
 
   /*
    * prefs utilities - the child does not have write access to prefs.
    * note, the pref names here are cross-checked against a list of
    * approved pdfjs prefs in chrome utils.
    */
 
   clearUserPref: function (aPrefName) {
-    this._mm.sendSyncMessage("PDFJS:Parent:clearUserPref", {
+    this._mm.sendSyncMessage('PDFJS:Parent:clearUserPref', {
       name: aPrefName
     });
   },
 
   setIntPref: function (aPrefName, aPrefValue) {
-    this._mm.sendSyncMessage("PDFJS:Parent:setIntPref", {
+    this._mm.sendSyncMessage('PDFJS:Parent:setIntPref', {
       name: aPrefName,
       value: aPrefValue
     });
   },
 
   setBoolPref: function (aPrefName, aPrefValue) {
-    this._mm.sendSyncMessage("PDFJS:Parent:setBoolPref", {
+    this._mm.sendSyncMessage('PDFJS:Parent:setBoolPref', {
       name: aPrefName,
       value: aPrefValue
     });
   },
 
   setCharPref: function (aPrefName, aPrefValue) {
-    this._mm.sendSyncMessage("PDFJS:Parent:setCharPref", {
+    this._mm.sendSyncMessage('PDFJS:Parent:setCharPref', {
       name: aPrefName,
       value: aPrefValue
     });
   },
 
   setStringPref: function (aPrefName, aPrefValue) {
-    this._mm.sendSyncMessage("PDFJS:Parent:setStringPref", {
+    this._mm.sendSyncMessage('PDFJS:Parent:setStringPref', {
       name: aPrefName,
       value: aPrefValue
     });
   },
 
   /*
    * Forwards default app query to the parent where we check various
    * handler app settings only available in the parent process.
    */
   isDefaultHandlerApp: function () {
-    return this._mm.sendSyncMessage("PDFJS:Parent:isDefaultHandlerApp")[0];
+    return this._mm.sendSyncMessage('PDFJS:Parent:isDefaultHandlerApp')[0];
   },
 
   /*
    * Request the display of a notification warning in the associated window
    * when the renderer isn't sure a pdf displayed correctly.
    */
   displayWarning: function (aWindow, aMessage, aCallback, aLabel, accessKey) {
     // the child's dom frame mm associated with the window.
     let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDocShell)
                        .QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIContentFrameMessageManager);
-    winmm.sendAsyncMessage("PDFJS:Parent:displayWarning", {
+    winmm.sendAsyncMessage('PDFJS:Parent:displayWarning', {
       message: aMessage,
       label: aLabel,
       accessKey: accessKey
     }, {
       callback: aCallback
     });
   },
 
   /*
    * Events
    */
 
   observe: function(aSubject, aTopic, aData) {
-    if (aTopic == "quit-application") {
+    if (aTopic === 'quit-application') {
       this.uninit();
     }
   },
 
   receiveMessage: function (aMsg) {
     switch (aMsg.name) {
-      case "PDFJS:Child:refreshSettings":
+      case 'PDFJS:Child:refreshSettings':
         // Only react to this if we are remote.
-        if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-          let jsm = "resource://pdf.js/PdfJs.jsm";
+        if (Services.appinfo.processType ===
+            Services.appinfo.PROCESS_TYPE_CONTENT) {
+          let jsm = 'resource://pdf.js/PdfJs.jsm';
           let pdfjs = Components.utils.import(jsm, {}).PdfJs;
           pdfjs.updateRegistration();
         }
         break;
     }
   },
 
   /*
@@ -154,40 +161,44 @@ let PdfjsContentUtils = {
                         .sameTypeRootTreeItem
                         .QueryInterface(Ci.nsIDocShell)
                         .QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIContentFrameMessageManager);
     // Sync calls don't support cpow wrapping of returned results, so we
     // send over a small container for the object we want.
     let suitcase = {
       _window: null,
-      setChromeWindow: function (aObj) { this._window = aObj; }
-    }
-    if (!winmm.sendSyncMessage("PDFJS:Parent:getChromeWindow", {},
+      setChromeWindow: function (aObj) {
+        this._window = aObj;
+      }
+    };
+    if (!winmm.sendSyncMessage('PDFJS:Parent:getChromeWindow', {},
                                { suitcase: suitcase })[0]) {
-      Cu.reportError("A request for a CPOW wrapped chrome window " +
-                     "failed for unknown reasons.");
+      Cu.reportError('A request for a CPOW wrapped chrome window ' +
+                     'failed for unknown reasons.');
       return null;
     }
     return suitcase._window;
   },
 
   getFindBar: function (aWindow) {
     let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIDocShell)
                         .sameTypeRootTreeItem
                         .QueryInterface(Ci.nsIDocShell)
                         .QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIContentFrameMessageManager);
     let suitcase = {
       _findbar: null,
-      setFindBar: function (aObj) { this._findbar = aObj; }
-    }
-    if (!winmm.sendSyncMessage("PDFJS:Parent:getFindBar", {},
+      setFindBar: function (aObj) {
+        this._findbar = aObj;
+      }
+    };
+    if (!winmm.sendSyncMessage('PDFJS:Parent:getFindBar', {},
                                { suitcase: suitcase })[0]) {
-      Cu.reportError("A request for a CPOW wrapped findbar " +
-                     "failed for unknown reasons.");
+      Cu.reportError('A request for a CPOW wrapped findbar ' +
+                     'failed for unknown reasons.');
       return null;
     }
     return suitcase._findbar;
   }
 };
 
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.0.978';
-PDFJS.build = '20bf84a';
+PDFJS.version = '1.0.1040';
+PDFJS.build = '997096f';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2798,17 +2798,23 @@ var Metadata = PDFJS.Metadata = (functio
 // However, PDF needs a bit more state, which we store here.
 
 // Minimal font size that would be used during canvas fillText operations.
 var MIN_FONT_SIZE = 16;
 // Maximum font size that would be used during canvas fillText operations.
 var MAX_FONT_SIZE = 100;
 var MAX_GROUP_SIZE = 4096;
 
+// Heuristic value used when enforcing minimum line widths.
+var MIN_WIDTH_FACTOR = 0.65;
+
 var COMPILE_TYPE3_GLYPHS = true;
+var MAX_SIZE_TO_COMPILE = 1000;
+
+var FULL_CHUNK_HEIGHT = 16;
 
 function createScratchCanvas(width, height) {
   var canvas = document.createElement('canvas');
   canvas.width = width;
   canvas.height = height;
   return canvas;
 }
 
@@ -2932,17 +2938,17 @@ function addContextCurrentTransform(ctx)
 }
 
 var CachedCanvases = (function CachedCanvasesClosure() {
   var cache = {};
   return {
     getCanvas: function CachedCanvases_getCanvas(id, width, height,
                                                  trackTransform) {
       var canvasEntry;
-      if (id in cache) {
+      if (cache[id] !== undefined) {
         canvasEntry = cache[id];
         canvasEntry.canvas.width = width;
         canvasEntry.canvas.height = height;
         // reset canvas transform for emulated mozCurrentTransform, if needed
         canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0);
       } else {
         var canvas = createScratchCanvas(width, height);
         var ctx = canvas.getContext('2d');
@@ -3146,16 +3152,17 @@ var CanvasExtraState = (function CanvasE
     this.charSpacing = 0;
     this.wordSpacing = 0;
     this.textHScale = 1;
     this.textRenderingMode = TextRenderingMode.FILL;
     this.textRise = 0;
     // Default fore and background colors
     this.fillColor = '#000000';
     this.strokeColor = '#000000';
+    this.patternFill = false;
     // Note: fill alpha applies to all non-stroking operations
     this.fillAlpha = 1;
     this.strokeAlpha = 1;
     this.lineWidth = 1;
     this.activeSMask = null; // nonclonable field (see the save method below)
 
     this.old = old;
   }
@@ -3198,16 +3205,17 @@ var CanvasGraphics = (function CanvasGra
     this.baseTransformStack = [];
     this.groupLevel = 0;
     this.smaskStack = [];
     this.smaskCounter = 0;
     this.tempSMask = null;
     if (canvasCtx) {
       addContextCurrentTransform(canvasCtx);
     }
+    this.cachedGetSinglePixelWidth = null;
   }
 
   function putBinaryImageData(ctx, imgData) {
     if (typeof ImageData !== 'undefined' && imgData instanceof ImageData) {
       ctx.putImageData(imgData, 0, 0);
       return;
     }
 
@@ -3218,23 +3226,21 @@ var CanvasGraphics = (function CanvasGra
     // the data passed to putImageData()). |n| shouldn't be too small, however,
     // because too many putImageData() calls will slow things down.
     //
     // Note: as written, if the last chunk is partial, the putImageData() call
     // will (conceptually) put pixels past the bounds of the canvas.  But
     // that's ok; any such pixels are ignored.
 
     var height = imgData.height, width = imgData.width;
-    var fullChunkHeight = 16;
-    var fracChunks = height / fullChunkHeight;
-    var fullChunks = Math.floor(fracChunks);
-    var totalChunks = Math.ceil(fracChunks);
-    var partialChunkHeight = height - fullChunks * fullChunkHeight;
-
-    var chunkImgData = ctx.createImageData(width, fullChunkHeight);
+    var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
+    var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
+    var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
+
+    var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
     var srcPos = 0, destPos;
     var src = imgData.data;
     var dest = chunkImgData.data;
     var i, j, thisChunkHeight, elemsInThisChunk;
 
     // There are multiple forms in which the pixel data can be passed, and
     // imgData.kind tells us which one this is.
     if (imgData.kind === ImageKind.GRAYSCALE_1BPP) {
@@ -3244,17 +3250,17 @@ var CanvasGraphics = (function CanvasGra
         new Uint32ArrayView(dest);
       var dest32DataLength = dest32.length;
       var fullSrcDiff = (width + 7) >> 3;
       var white = 0xFFFFFFFF;
       var black = (PDFJS.isLittleEndian || !PDFJS.hasCanvasTypedArrays) ?
         0xFF000000 : 0x000000FF;
       for (i = 0; i < totalChunks; i++) {
         thisChunkHeight =
-          (i < fullChunks) ? fullChunkHeight : partialChunkHeight;
+          (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight;
         destPos = 0;
         for (j = 0; j < thisChunkHeight; j++) {
           var srcDiff = srcLength - srcPos;
           var k = 0;
           var kEnd = (srcDiff > fullSrcDiff) ? width : srcDiff * 8 - 7;
           var kEndUnrolled = kEnd & ~7;
           var mask = 0;
           var srcByte = 0;
@@ -3279,110 +3285,108 @@ var CanvasGraphics = (function CanvasGra
             mask >>= 1;
           }
         }
         // We ran out of input. Make all remaining pixels transparent.
         while (destPos < dest32DataLength) {
           dest32[destPos++] = 0;
         }
 
-        ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
+        ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
       }
     } else if (imgData.kind === ImageKind.RGBA_32BPP) {
       // RGBA, 32-bits per pixel.
 
       j = 0;
-      elemsInThisChunk = width * fullChunkHeight * 4;
+      elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
       for (i = 0; i < fullChunks; i++) {
         dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
         srcPos += elemsInThisChunk;
 
         ctx.putImageData(chunkImgData, 0, j);
-        j += fullChunkHeight;
+        j += FULL_CHUNK_HEIGHT;
       }
       if (i < totalChunks) {
         elemsInThisChunk = width * partialChunkHeight * 4;
         dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
         ctx.putImageData(chunkImgData, 0, j);
       }
 
     } else if (imgData.kind === ImageKind.RGB_24BPP) {
       // RGB, 24-bits per pixel.
-      thisChunkHeight = fullChunkHeight;
+      thisChunkHeight = FULL_CHUNK_HEIGHT;
       elemsInThisChunk = width * thisChunkHeight;
       for (i = 0; i < totalChunks; i++) {
         if (i >= fullChunks) {
-          thisChunkHeight =partialChunkHeight;
+          thisChunkHeight = partialChunkHeight;
           elemsInThisChunk = width * thisChunkHeight;
         }
 
         destPos = 0;
         for (j = elemsInThisChunk; j--;) {
           dest[destPos++] = src[srcPos++];
           dest[destPos++] = src[srcPos++];
           dest[destPos++] = src[srcPos++];
           dest[destPos++] = 255;
         }
-        ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
+        ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
       }
     } else {
       error('bad image kind: ' + imgData.kind);
     }
   }
 
   function putBinaryImageMask(ctx, imgData) {
     var height = imgData.height, width = imgData.width;
-    var fullChunkHeight = 16;
-    var fracChunks = height / fullChunkHeight;
-    var fullChunks = Math.floor(fracChunks);
-    var totalChunks = Math.ceil(fracChunks);
-    var partialChunkHeight = height - fullChunks * fullChunkHeight;
-
-    var chunkImgData = ctx.createImageData(width, fullChunkHeight);
+    var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
+    var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
+    var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
+
+    var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
     var srcPos = 0;
     var src = imgData.data;
     var dest = chunkImgData.data;
 
     for (var i = 0; i < totalChunks; i++) {
       var thisChunkHeight =
-        (i < fullChunks) ? fullChunkHeight : partialChunkHeight;
+        (i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight;
 
       // Expand the mask so it can be used by the canvas.  Any required
       // inversion has already been handled.
       var destPos = 3; // alpha component offset
       for (var j = 0; j < thisChunkHeight; j++) {
         var mask = 0;
         for (var k = 0; k < width; k++) {
           if (!mask) {
             var elem = src[srcPos++];
             mask = 128;
           }
           dest[destPos] = (elem & mask) ? 0 : 255;
           destPos += 4;
           mask >>= 1;
         }
       }
-      ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
+      ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
     }
   }
 
   function copyCtxState(sourceCtx, destCtx) {
     var properties = ['strokeStyle', 'fillStyle', 'fillRule', 'globalAlpha',
                       'lineWidth', 'lineCap', 'lineJoin', 'miterLimit',
                       'globalCompositeOperation', 'font'];
     for (var i = 0, ii = properties.length; i < ii; i++) {
       var property = properties[i];
-      if (property in sourceCtx) {
+      if (sourceCtx[property] !== undefined) {
         destCtx[property] = sourceCtx[property];
       }
     }
-    if ('setLineDash' in sourceCtx) {
+    if (sourceCtx.setLineDash !== undefined) {
       destCtx.setLineDash(sourceCtx.getLineDash());
       destCtx.lineDashOffset =  sourceCtx.lineDashOffset;
-    } else if ('mozDash' in sourceCtx) {
+    } else if (sourceCtx.mozDashOffset !== undefined) {
       destCtx.mozDash = sourceCtx.mozDash;
       destCtx.mozDashOffset = sourceCtx.mozDashOffset;
     }
   }
 
   function composeSMaskBackdrop(bytes, r0, g0, b0) {
     var length = bytes.length;
     for (var i = 3; i < length; i += 4) {
@@ -3599,17 +3603,17 @@ var CanvasGraphics = (function CanvasGra
     setLineJoin: function CanvasGraphics_setLineJoin(style) {
       this.ctx.lineJoin = LINE_JOIN_STYLES[style];
     },
     setMiterLimit: function CanvasGraphics_setMiterLimit(limit) {
       this.ctx.miterLimit = limit;
     },
     setDash: function CanvasGraphics_setDash(dashArray, dashPhase) {
       var ctx = this.ctx;
-      if ('setLineDash' in ctx) {
+      if (ctx.setLineDash !== undefined) {
         ctx.setLineDash(dashArray);
         ctx.lineDashOffset = dashPhase;
       } else {
         ctx.mozDash = dashArray;
         ctx.mozDashOffset = dashPhase;
       }
     },
     setRenderingIntent: function CanvasGraphics_setRenderingIntent(intent) {
@@ -3734,20 +3738,24 @@ var CanvasGraphics = (function CanvasGra
     restore: function CanvasGraphics_restore() {
       if (this.stateStack.length !== 0) {
         if (this.current.activeSMask !== null) {
           this.endSMaskGroup();
         }
 
         this.current = this.stateStack.pop();
         this.ctx.restore();
+
+        this.cachedGetSinglePixelWidth = null;
       }
     },
     transform: function CanvasGraphics_transform(a, b, c, d, e, f) {
       this.ctx.transform(a, b, c, d, e, f);
+
+      this.cachedGetSinglePixelWidth = null;
     },
 
     // Path
     constructPath: function CanvasGraphics_constructPath(ops, args) {
       var ctx = this.ctx;
       var current = this.current;
       var x = current.x, y = current.y;
       for (var i = 0, j = 0, ii = ops.length; i < ii; i++) {
@@ -3811,19 +3819,19 @@ var CanvasGraphics = (function CanvasGra
     },
     closePath: function CanvasGraphics_closePath() {
       this.ctx.closePath();
     },
     stroke: function CanvasGraphics_stroke(consumePath) {
       consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
       var ctx = this.ctx;
       var strokeColor = this.current.strokeColor;
-      if (this.current.lineWidth === 0) {
-        ctx.lineWidth = this.getSinglePixelWidth();
-      }
+      // Prevent drawing too thin lines by enforcing a minimum line width.
+      ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR,
+                               this.current.lineWidth);
       // For stroke we want to temporarily change the global alpha to the
       // stroking alpha.
       ctx.globalAlpha = this.current.strokeAlpha;
       if (strokeColor && strokeColor.hasOwnProperty('type') &&
           strokeColor.type === 'Pattern') {
         // for patterns, we transform to pattern space, calculate
         // the pattern, call stroke, and restore to user space
         ctx.save();
@@ -3842,20 +3850,20 @@ var CanvasGraphics = (function CanvasGra
     closeStroke: function CanvasGraphics_closeStroke() {
       this.closePath();
       this.stroke();
     },
     fill: function CanvasGraphics_fill(consumePath) {
       consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
       var ctx = this.ctx;
       var fillColor = this.current.fillColor;
+      var isPatternFill = this.current.patternFill;
       var needRestore = false;
 
-      if (fillColor && fillColor.hasOwnProperty('type') &&
-          fillColor.type === 'Pattern') {
+      if (isPatternFill) {
         ctx.save();
         ctx.fillStyle = fillColor.getPattern(ctx, this);
         needRestore = true;
       }
 
       if (this.pendingEOFill) {
         if (ctx.mozFillRule !== undefined) {
           ctx.mozFillRule = 'evenodd';
@@ -4138,17 +4146,23 @@ var CanvasGraphics = (function CanvasGra
         ctx.scale(textHScale, -1);
       } else {
         ctx.scale(textHScale, 1);
       }
 
       var lineWidth = current.lineWidth;
       var scale = current.textMatrixScale;
       if (scale === 0 || lineWidth === 0) {
-        lineWidth = this.getSinglePixelWidth();
+        var fillStrokeMode = current.textRenderingMode &
+          TextRenderingMode.FILL_STROKE_MASK;
+        if (fillStrokeMode === TextRenderingMode.STROKE ||
+            fillStrokeMode === TextRenderingMode.FILL_STROKE) {
+          this.cachedGetSinglePixelWidth = null;
+          lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR;
+        }
       } else {
         lineWidth /= scale;
       }
 
       if (fontSizeScale !== 1.0) {
         ctx.scale(fontSizeScale, fontSizeScale);
         lineWidth /= fontSizeScale;
       }
@@ -4319,26 +4333,28 @@ var CanvasGraphics = (function CanvasGra
       }
       return pattern;
     },
     setStrokeColorN: function CanvasGraphics_setStrokeColorN(/*...*/) {
       this.current.strokeColor = this.getColorN_Pattern(arguments);
     },
     setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) {
       this.current.fillColor = this.getColorN_Pattern(arguments);
+      this.current.patternFill = true;
     },
     setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) {
       var color = Util.makeCssRgb(r, g, b);
       this.ctx.strokeStyle = color;
       this.current.strokeColor = color;
     },
     setFillRGBColor: function CanvasGraphics_setFillRGBColor(r, g, b) {
       var color = Util.makeCssRgb(r, g, b);
       this.ctx.fillStyle = color;
       this.current.fillColor = color;
+      this.current.patternFill = false;
     },
 
     shadingFill: function CanvasGraphics_shadingFill(patternIR) {
       var ctx = this.ctx;
 
       this.save();
       var pattern = getShadingPatternFromIR(patternIR);
       ctx.fillStyle = pattern.getPattern(ctx, this, true);
@@ -4587,21 +4603,22 @@ var CanvasGraphics = (function CanvasGra
         });
       }
       this.restore();
     },
 
     paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
       var ctx = this.ctx;
       var width = img.width, height = img.height;
+      var fillColor = this.current.fillColor;
+      var isPatternFill = this.current.patternFill;
 
       var glyph = this.processingType3;
 
-      if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) {
-        var MAX_SIZE_TO_COMPILE = 1000;
+      if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) {
         if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) {
           glyph.compiled =
             compileType3Glyph({data: img.data, width: width, height: height});
         } else {
           glyph.compiled = null;
         }
       }
 
@@ -4613,79 +4630,77 @@ var CanvasGraphics = (function CanvasGra
       var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
       var maskCtx = maskCanvas.context;
       maskCtx.save();
 
       putBinaryImageMask(maskCtx, img);
 
       maskCtx.globalCompositeOperation = 'source-in';
 
-      var fillColor = this.current.fillColor;
-      maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
-                          fillColor.type === 'Pattern') ?
+      maskCtx.fillStyle = isPatternFill ?
                           fillColor.getPattern(maskCtx, this) : fillColor;
       maskCtx.fillRect(0, 0, width, height);
 
       maskCtx.restore();
 
       this.paintInlineImageXObject(maskCanvas.canvas);
     },
 
     paintImageMaskXObjectRepeat:
       function CanvasGraphics_paintImageMaskXObjectRepeat(imgData, scaleX,
                                                           scaleY, positions) {
       var width = imgData.width;
       var height = imgData.height;
-      var ctx = this.ctx;
+      var fillColor = this.current.fillColor;
+      var isPatternFill = this.current.patternFill;
 
       var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
       var maskCtx = maskCanvas.context;
       maskCtx.save();
 
       putBinaryImageMask(maskCtx, imgData);
 
       maskCtx.globalCompositeOperation = 'source-in';
 
-      var fillColor = this.current.fillColor;
-      maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
-        fillColor.type === 'Pattern') ?
-        fillColor.getPattern(maskCtx, this) : fillColor;
+      maskCtx.fillStyle = isPatternFill ?
+                          fillColor.getPattern(maskCtx, this) : fillColor;
       maskCtx.fillRect(0, 0, width, height);
 
       maskCtx.restore();
 
+      var ctx = this.ctx;
       for (var i = 0, ii = positions.length; i < ii; i += 2) {
         ctx.save();
         ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]);
         ctx.scale(1, -1);
         ctx.drawImage(maskCanvas.canvas, 0, 0, width, height,
           0, -1, 1, 1);
         ctx.restore();
       }
     },
 
     paintImageMaskXObjectGroup:
       function CanvasGraphics_paintImageMaskXObjectGroup(images) {
       var ctx = this.ctx;
 
+      var fillColor = this.current.fillColor;
+      var isPatternFill = this.current.patternFill;
       for (var i = 0, ii = images.length; i < ii; i++) {
         var image = images[i];
         var width = image.width, height = image.height;
 
         var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
         var maskCtx = maskCanvas.context;
         maskCtx.save();
 
         putBinaryImageMask(maskCtx, image);
 
         maskCtx.globalCompositeOperation = 'source-in';
 
-        var fillColor = this.current.fillColor;
-        maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
-                            fillColor.type === 'Pattern') ?
+        maskCtx.fillStyle = isPatternFill ?
                             fillColor.getPattern(maskCtx, this) : fillColor;
         maskCtx.fillRect(0, 0, width, height);
 
         maskCtx.restore();
 
         ctx.save();
         ctx.transform.apply(ctx, image.transform);
         ctx.scale(1, -1);
@@ -4878,21 +4893,24 @@ var CanvasGraphics = (function CanvasGra
         } else {
           ctx.clip();
         }
         this.pendingClip = null;
       }
       ctx.beginPath();
     },
     getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) {
-      var inverse = this.ctx.mozCurrentTransformInverse;
-      // max of the current horizontal and vertical scale
-      return Math.sqrt(Math.max(
-        (inverse[0] * inverse[0] + inverse[1] * inverse[1]),
-        (inverse[2] * inverse[2] + inverse[3] * inverse[3])));
+      if (this.cachedGetSinglePixelWidth === null) {
+        var inverse = this.ctx.mozCurrentTransformInverse;
+        // max of the current horizontal and vertical scale
+        this.cachedGetSinglePixelWidth = Math.sqrt(Math.max(
+          (inverse[0] * inverse[0] + inverse[1] * inverse[1]),
+          (inverse[2] * inverse[2] + inverse[3] * inverse[3])));
+      }
+      return this.cachedGetSinglePixelWidth;
     },
     getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
         var transform = this.ctx.mozCurrentTransform;
         return [
           transform[0] * x + transform[2] * y + transform[4],
           transform[1] * x + transform[3] * y + transform[5]
         ];
     }
@@ -5819,17 +5837,16 @@ var FontFaceObject = (function FontFaceO
       }
       return this.compiledGlyphs[character];
     }
   };
   return FontFaceObject;
 })();
 
 
-var HIGHLIGHT_OFFSET = 4; // px
 var ANNOT_MIN_SIZE = 10; // px
 
 var AnnotationUtils = (function AnnotationUtilsClosure() {
   // TODO(mack): This dupes some of the logic in CanvasGraphics.setFont()
   function setTextStyles(element, item, fontObj) {
 
     var style = element.style;
     style.fontSize = item.fontSize + 'px';
@@ -5846,51 +5863,46 @@ var AnnotationUtils = (function Annotati
 
     var fontName = fontObj.loadedName;
     var fontFamily = fontName ? '"' + fontName + '", ' : '';
     // Use a reasonable default font if the font doesn't specify a fallback
     var fallbackName = fontObj.fallbackName || 'Helvetica, sans-serif';
     style.fontFamily = fontFamily + fallbackName;
   }
 
-  // TODO(mack): Remove this, it's not really that helpful.
-  function getEmptyContainer(tagName, rect, borderWidth) {
-    var bWidth = borderWidth || 0;
-    var element = document.createElement(tagName);
-    element.style.borderWidth = bWidth + 'px';
-    var width = rect[2] - rect[0] - 2 * bWidth;
-    var height = rect[3] - rect[1] - 2 * bWidth;
-    element.style.width = width + 'px';
-    element.style.height = height + 'px';
-    return element;
-  }
-
-  function initContainer(item) {
-    var container = getEmptyContainer('section', item.rect, item.borderWidth);
-    container.style.backgroundColor = item.color;
-
-    var color = item.color;
-    item.colorCssRgb = Util.makeCssRgb(Math.round(color[0] * 255),
-                                       Math.round(color[1] * 255),
-                                       Math.round(color[2] * 255));
-
-    var highlight = document.createElement('div');
-    highlight.className = 'annotationHighlight';
-    highlight.style.left = highlight.style.top = -HIGHLIGHT_OFFSET + 'px';
-    highlight.style.right = highlight.style.bottom = -HIGHLIGHT_OFFSET + 'px';
-    highlight.setAttribute('hidden', true);
-
-    item.highlightElement = highlight;
-    container.appendChild(item.highlightElement);
-
+  function initContainer(item, drawBorder) {
+    var container = document.createElement('section');
+    var cstyle = container.style;
+    var width = item.rect[2] - item.rect[0];
+    var height = item.rect[3] - item.rect[1];
+
+    var bWidth = item.borderWidth || 0;
+    if (bWidth) {
+      width = width - 2 * bWidth;
+      height = height - 2 * bWidth;
+      cstyle.borderWidth = bWidth + 'px';
+      var color = item.color;
+      if (drawBorder && color) {
+        cstyle.borderStyle = 'solid';
+        cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255),
+                                             Math.round(color[1] * 255),
+                                             Math.round(color[2] * 255));
+      }
+    }
+    cstyle.width = width + 'px';
+    cstyle.height = height + 'px';
     return container;
   }
 
   function getHtmlElementForTextWidgetAnnotation(item, commonObjs) {
-    var element = getEmptyContainer('div', item.rect, 0);
+    var element = document.createElement('div');
+    var width = item.rect[2] - item.rect[0];
+    var height = item.rect[3] - item.rect[1];
+    element.style.width = width + 'px';
+    element.style.height = height + 'px';
     element.style.display = 'table';
 
     var content = document.createElement('div');
     content.textContent = item.fieldValue;
     var textAlignment = item.textAlignment;
     content.style.textAlign = ['left', 'center', 'right'][textAlignment];
     content.style.verticalAlign = 'middle';
     content.style.display = 'table-cell';
@@ -5910,17 +5922,17 @@ var AnnotationUtils = (function Annotati
     // sanity check because of OOo-generated PDFs
     if ((rect[3] - rect[1]) < ANNOT_MIN_SIZE) {
       rect[3] = rect[1] + ANNOT_MIN_SIZE;
     }
     if ((rect[2] - rect[0]) < ANNOT_MIN_SIZE) {
       rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
     }
 
-    var container = initContainer(item);
+    var container = initContainer(item, false);
     container.className = 'annotText';
 
     var image  = document.createElement('img');
     image.style.height = container.style.height;
     image.style.width = container.style.width;
     var iconName = item.name;
     image.src = PDFJS.imageResourcesPath + 'annotation-' +
       iconName.toLowerCase() + '.svg';
@@ -6019,22 +6031,19 @@ var AnnotationUtils = (function Annotati
     contentWrapper.appendChild(content);
     container.appendChild(image);
     container.appendChild(contentWrapper);
 
     return container;
   }
 
   function getHtmlElementForLinkAnnotation(item) {
-    var container = initContainer(item);
+    var container = initContainer(item, true);
     container.className = 'annotLink';
 
-    container.style.borderColor = item.colorCssRgb;
-    container.style.borderStyle = 'solid';
-
     var link = document.createElement('a');
     link.href = link.title = item.url || '';
 
     container.appendChild(link);
 
     return container;
   }
 
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.0.978';
-PDFJS.build = '20bf84a';
+PDFJS.version = '1.0.1040';
+PDFJS.build = '997096f';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2293,16 +2293,20 @@ var Page = (function PageClosure() {
 /**
  * The `PDFDocument` holds all the data of the PDF file. Compared to the
  * `PDFDoc`, this one doesn't have any job management code.
  * Right now there exists one PDFDocument on the main thread + one object
  * for each worker. If there is no worker support enabled, there are two
  * `PDFDocument` objects on the main thread created.
  */
 var PDFDocument = (function PDFDocumentClosure() {
+  var FINGERPRINT_FIRST_BYTES = 1024;
+  var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' +
+    '\x00\x00\x00\x00\x00\x00\x00\x00\x00';
+
   function PDFDocument(pdfManager, arg, password) {
     if (isStream(arg)) {
       init.call(this, pdfManager, arg, password);
     } else if (isArrayBuffer(arg)) {
       init.call(this, pdfManager, new Stream(arg), password);
     } else {
       error('PDFDocument: Unknown argument type');
     }
@@ -2504,26 +2508,35 @@ var PDFDocument = (function PDFDocumentC
               info('Bad value in document info for "' + key + '"');
             }
           }
         }
       }
       return shadow(this, 'documentInfo', docInfo);
     },
     get fingerprint() {
-      var xref = this.xref, hash, fileID = '';
+      var xref = this.xref, idArray, hash, fileID = '';
 
       if (xref.trailer.has('ID')) {
-        hash = stringToBytes(xref.trailer.get('ID')[0]);
-      } else {
-        hash = calculateMD5(this.stream.bytes.subarray(0, 100), 0, 100);
+        idArray = xref.trailer.get('ID');
+      }
+      if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) {
+        hash = stringToBytes(idArray[0]);
+      } else {
+        if (this.stream.ensureRange) {
+          this.stream.ensureRange(0,
+            Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end));
+        }
+        hash = calculateMD5(this.stream.bytes.subarray(0,
+          FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES);
       }
 
       for (var i = 0, n = hash.length; i < n; i++) {
-        fileID += hash[i].toString(16);
+        var hex = hash[i].toString(16);
+        fileID += hex.length === 1 ? '0' + hex : hex;
       }
 
       return shadow(this, 'fingerprint', fileID);
     },
 
     getPage: function PDFDocument_getPage(pageIndex) {
       return this.catalog.getPage(pageIndex);
     },
@@ -4369,39 +4382,53 @@ var Annotation = (function AnnotationClo
     var data = this.data = {};
 
     data.subtype = dict.get('Subtype').name;
     var rect = dict.get('Rect') || [0, 0, 0, 0];
     data.rect = Util.normalizeRect(rect);
     data.annotationFlags = dict.get('F');
 
     var color = dict.get('C');
-    if (isArray(color) && color.length === 3) {
-      // TODO(mack): currently only supporting rgb; need support different
-      // colorspaces
-      data.color = color;
-    } else {
+    if (!color) {
+      // The PDF spec does not mention how a missing color array is interpreted.
+      // Adobe Reader seems to default to black in this case.
       data.color = [0, 0, 0];
+    } else if (isArray(color)) {
+      switch (color.length) {
+        case 0:
+          // Empty array denotes transparent border.
+          data.color = null;
+          break;
+        case 1:
+          // TODO: implement DeviceGray
+          break;
+        case 3:
+          data.color = color;
+          break;
+        case 4:
+          // TODO: implement DeviceCMYK
+          break;
+      }
     }
 
     // Some types of annotations have border style dict which has more
     // info than the border array
     if (dict.has('BS')) {
       var borderStyle = dict.get('BS');
       data.borderWidth = borderStyle.has('W') ? borderStyle.get('W') : 1;
     } else {
       var borderArray = dict.get('Border') || [0, 0, 1];
       data.borderWidth = borderArray[2] || 0;
 
       // TODO: implement proper support for annotations with line dash patterns.
       var dashArray = borderArray[3];
       if (data.borderWidth > 0 && dashArray) {
         if (!isArray(dashArray)) {
           // Ignore the border if dashArray is not actually an array,
-          // this is consistent with the behaviour in Adobe Reader. 
+          // this is consistent with the behaviour in Adobe Reader.
           data.borderWidth = 0;
         } else {
           var dashArrayLength = dashArray.length;
           if (dashArrayLength > 0) {
             // According to the PDF specification: the elements in a dashArray
             // shall be numbers that are nonnegative and not all equal to zero.
             var isInvalid = false;
             var numPositive = 0;
@@ -4814,21 +4841,17 @@ var LinkAnnotation = (function LinkAnnot
   // Lets URLs beginning with 'www.' default to using the 'http://' protocol.
   function addDefaultProtocolToUrl(url) {
     if (url && url.indexOf('www.') === 0) {
       return ('http://' + url);
     }
     return url;
   }
 
-  Util.inherit(LinkAnnotation, InteractiveAnnotation, {
-    hasOperatorList: function LinkAnnotation_hasOperatorList() {
-      return false;
-    }
-  });
+  Util.inherit(LinkAnnotation, InteractiveAnnotation, { });
 
   return LinkAnnotation;
 })();
 
 
 var PDFFunction = (function PDFFunctionClosure() {
   var CONSTRUCT_SAMPLED = 0;
   var CONSTRUCT_INTERPOLATED = 2;
@@ -10115,17 +10138,17 @@ var PartialEvaluator = (function Partial
             operatorList.addOp(OPS.endGroup, [groupOptions]);
           }
         });
     },
 
     buildPaintImageXObject:
         function PartialEvaluator_buildPaintImageXObject(resources, image,
                                                          inline, operatorList,
-                                                         cacheKey, cache) {
+                                                         cacheKey, imageCache) {
       var self = this;
       var dict = image.dict;
       var w = dict.get('Width', 'W');
       var h = dict.get('Height', 'H');
 
       if (!(w && isNum(w)) || !(h && isNum(h))) {
         warn('Image dimensions are missing, or not numbers.');
         return;
@@ -10153,19 +10176,20 @@ var PartialEvaluator = (function Partial
 
         imgData = PDFImage.createMask(imgArray, width, height,
                                       image instanceof DecodeStream,
                                       inverseDecode);
         imgData.cached = true;
         args = [imgData];
         operatorList.addOp(OPS.paintImageMaskXObject, args);
         if (cacheKey) {
-          cache.key = cacheKey;
-          cache.fn = OPS.paintImageMaskXObject;
-          cache.args = args;
+          imageCache[cacheKey] = {
+            fn: OPS.paintImageMaskXObject,
+            args: args
+          };
         }
         return;
       }
 
       var softMask = (dict.get('SMask', 'SM') || false);
       var mask = (dict.get('Mask') || false);
 
       var SMALL_IMAGE_DIMENSIONS = 200;
@@ -10204,19 +10228,20 @@ var PartialEvaluator = (function Partial
             [imgData.data.buffer]);
         }).then(undefined, function (reason) {
           warn('Unable to decode image: ' + reason);
           self.handler.send('obj', [objId, self.pageIndex, 'Image', null]);
         });
 
       operatorList.addOp(OPS.paintImageXObject, args);
       if (cacheKey) {
-        cache.key = cacheKey;
-        cache.fn = OPS.paintImageXObject;
-        cache.args = args;
+        imageCache[cacheKey] = {
+          fn: OPS.paintImageXObject,
+          args: args
+        };
       }
     },
 
     handleSMask: function PartialEvaluator_handleSmask(smask, resources,
                                                        operatorList,
                                                        stateManager) {
       var smaskContent = smask.get('G');
       var smaskOptions = {
@@ -10600,18 +10625,18 @@ var PartialEvaluator = (function Partial
 
           switch (fn | 0) {
             case OPS.paintXObject:
               if (args[0].code) {
                 break;
               }
               // eagerly compile XForm objects
               var name = args[0].name;
-              if (imageCache.key === name) {
-                operatorList.addOp(imageCache.fn, imageCache.args);
+              if (imageCache[name] !== undefined) {
+                operatorList.addOp(imageCache[name].fn, imageCache[name].args);
                 args = null;
                 continue;
               }
 
               var xobj = xobjs.get(name);
               if (xobj) {
                 assert(isStream(xobj), 'XObject should be a stream');
 
@@ -10650,20 +10675,23 @@ var PartialEvaluator = (function Partial
                                         operatorList, stateManager.state).
                 then(function (loadedName) {
                   operatorList.addDependency(loadedName);
                   operatorList.addOp(OPS.setFont, [loadedName, fontSize]);
                   next(resolve, reject);
                 }, reject);
             case OPS.endInlineImage:
               var cacheKey = args[0].cacheKey;
-              if (cacheKey && imageCache.key === cacheKey) {
-                operatorList.addOp(imageCache.fn, imageCache.args);
-                args = null;
-                continue;
+              if (cacheKey) {
+                var cacheEntry = imageCache[cacheKey];
+                if (cacheEntry !== undefined) {
+                  operatorList.addOp(cacheEntry.fn, cacheEntry.args);
+                  args = null;
+                  continue;
+                }
               }
               self.buildPaintImageXObject(resources, args[0], true,
                 operatorList, cacheKey, imageCache);
               args = null;
               continue;
             case OPS.showText:
               args[0] = self.handleText(args[0], stateManager.state);
               break;
@@ -13820,19 +13848,22 @@ var stdFontMap = {
   'CourierNew': 'Courier',
   'CourierNew-Bold': 'Courier-Bold',
   'CourierNew-BoldItalic': 'Courier-BoldOblique',
   'CourierNew-Italic': 'Courier-Oblique',
   'CourierNewPS-BoldItalicMT': 'Courier-BoldOblique',
   'CourierNewPS-BoldMT': 'Courier-Bold',
   'CourierNewPS-ItalicMT': 'Courier-Oblique',
   'CourierNewPSMT': 'Courier',
+  'Helvetica': 'Helvetica',
   'Helvetica-Bold': 'Helvetica-Bold',
   'Helvetica-BoldItalic': 'Helvetica-BoldOblique',
+  'Helvetica-BoldOblique': 'Helvetica-BoldOblique',
   'Helvetica-Italic': 'Helvetica-Oblique',
+  'Helvetica-Oblique':'Helvetica-Oblique',
   'Symbol-Bold': 'Symbol',
   'Symbol-BoldItalic': 'Symbol',
   'Symbol-Italic': 'Symbol',
   'TimesNewRoman': 'Times-Roman',
   'TimesNewRoman-Bold': 'Times-Bold',
   'TimesNewRoman-BoldItalic': 'Times-BoldItalic',
   'TimesNewRoman-Italic': 'Times-Italic',
   'TimesNewRomanPS': 'Times-Roman',
@@ -13848,16 +13879,20 @@ var stdFontMap = {
   'TimesNewRomanPSMT-Italic': 'Times-Italic'
 };
 
 /**
  * Holds the map of the non-standard fonts that might be included as a standard
  * fonts without glyph data.
  */
 var nonStdFontMap = {
+  'CenturyGothic': 'Helvetica',
+  'CenturyGothic-Bold': 'Helvetica-Bold',
+  'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique',
+  'CenturyGothic-Italic': 'Helvetica-Oblique',
   'ComicSansMS': 'Comic Sans MS',
   'ComicSansMS-Bold': 'Comic Sans MS-Bold',
   'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic',
   'ComicSansMS-Italic': 'Comic Sans MS-Italic',
   'LucidaConsole': 'Courier',
   'LucidaConsole-Bold': 'Courier-Bold',
   'LucidaConsole-BoldItalic': 'Courier-BoldOblique',
   'LucidaConsole-Italic': 'Courier-Oblique',
@@ -13871,17 +13906,18 @@ var nonStdFontMap = {
   'MS-Mincho-Italic': 'MS Mincho-Italic',
   'MS-PGothic': 'MS PGothic',
   'MS-PGothic-Bold': 'MS PGothic-Bold',
   'MS-PGothic-BoldItalic': 'MS PGothic-BoldItalic',
   'MS-PGothic-Italic': 'MS PGothic-Italic',
   'MS-PMincho': 'MS PMincho',
   'MS-PMincho-Bold': 'MS PMincho-Bold',
   'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic',
-  'MS-PMincho-Italic': 'MS PMincho-Italic'
+  'MS-PMincho-Italic': 'MS PMincho-Italic',
+  'Wingdings': 'ZapfDingbats'
 };
 
 var serifFonts = {
   'Adobe Jenson': true, 'Adobe Text': true, 'Albertus': true,
   'Aldus': true, 'Alexandria': true, 'Algerian': true,
   'American Typewriter': true, 'Antiqua': true, 'Apex': true,
   'Arno': true, 'Aster': true, 'Aurora': true,
   'Baskerville': true, 'Bell': true, 'Bembo': true,
@@ -15954,17 +15990,18 @@ var Font = (function FontClosure() {
         // attempting to recover by assuming that no file exists.
         warn('Font file is empty in "' + name + '" (' + this.loadedName + ')');
       }
 
       this.missingFile = true;
       // The file data is not specified. Trying to fix the font name
       // to be used with the canvas.font.
       var fontName = name.replace(/[,_]/g, '-');
-      var isStandardFont = fontName in stdFontMap;
+      var isStandardFont = !!stdFontMap[fontName] ||
+        (nonStdFontMap[fontName] && !!stdFontMap[nonStdFontMap[fontName]]);
       fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
 
       this.bold = (fontName.search(/bold/gi) !== -1);
       this.italic = ((fontName.search(/oblique/gi) !== -1) ||
                      (fontName.search(/italic/gi) !== -1));
 
       // Use 'name' instead of 'fontName' here because the original
       // name ArialBlack for example will be replaced by Helvetica.
@@ -16000,16 +16037,20 @@ var Font = (function FontClosure() {
         for (charCode in properties.differences) {
           fontChar = GlyphsUnicode[properties.differences[charCode]];
           if (!fontChar) {
             continue;
           }
           this.toFontChar[charCode] = fontChar;
         }
       } else if (/Dingbats/i.test(fontName)) {
+        if (/Wingdings/i.test(name)) {
+          warn('Wingdings font without embedded font file, ' +
+               'falling back to the ZapfDingbats encoding.');
+        }
         var dingbats = Encodings.ZapfDingbatsEncoding;
         for (charCode in dingbats) {
           fontChar = DingbatsGlyphsUnicode[dingbats[charCode]];
           if (!fontChar) {
             continue;
           }
           this.toFontChar[charCode] = fontChar;
         }
@@ -16185,16 +16226,17 @@ var Font = (function FontClosure() {
       // canvas if left in their current position. Also, move characters if the
       // font was symbolic and there is only an identity unicode map since the
       // characters probably aren't in the correct position (fixes an issue
       // with firefox and thuluthfont).
       if ((usedFontCharCodes[fontCharCode] !== undefined ||
            fontCharCode <= 0x1f || // Control chars
            fontCharCode === 0x7F || // Control char
            fontCharCode === 0xAD || // Soft hyphen
+           fontCharCode === 0xA0 || // Non breaking space
            (fontCharCode >= 0x80 && fontCharCode <= 0x9F) || // Control chars
            // Prevent drawing characters in the specials unicode block.
            (fontCharCode >= 0xFFF0 && fontCharCode <= 0xFFFF) ||
            (isSymbolic && isIdentityUnicode)) &&
           nextAvailableFontCharCode <= PRIVATE_USE_OFFSET_END) { // Room left.
         // Loop to try and find a free spot in the private use area.
         do {
           fontCharCode = nextAvailableFontCharCode++;
@@ -18203,44 +18245,50 @@ function type1FontGlyphMapping(propertie
   if (properties.baseEncodingName) {
     // If a valid base encoding name was used, the mapping is initialized with
     // that.
     baseEncoding = Encodings[properties.baseEncodingName];
     for (charCode = 0; charCode < baseEncoding.length; charCode++) {
       glyphId = glyphNames.indexOf(baseEncoding[charCode]);
       if (glyphId >= 0) {
         charCodeToGlyphId[charCode] = glyphId;
+      } else {
+        charCodeToGlyphId[charCode] = 0; // notdef
       }
     }
   } else if (!!(properties.flags & FontFlags.Symbolic)) {
     // For a symbolic font the encoding should be the fonts built-in
     // encoding.
     for (charCode in builtInEncoding) {
       charCodeToGlyphId[charCode] = builtInEncoding[charCode];
     }
   } else {
     // For non-symbolic fonts that don't have a base encoding the standard
     // encoding should be used.
     baseEncoding = Encodings.StandardEncoding;
     for (charCode = 0; charCode < baseEncoding.length; charCode++) {
       glyphId = glyphNames.indexOf(baseEncoding[charCode]);
       if (glyphId >= 0) {
         charCodeToGlyphId[charCode] = glyphId;
+      } else {
+        charCodeToGlyphId[charCode] = 0; // notdef
       }
     }
   }
 
   // Lastly, merge in the differences.
   var differences = properties.differences;
   if (differences) {
     for (charCode in differences) {
       var glyphName = differences[charCode];
       glyphId = glyphNames.indexOf(glyphName);
       if (glyphId >= 0) {
         charCodeToGlyphId[charCode] = glyphId;
+      } else {
+        charCodeToGlyphId[charCode] = 0; // notdef
       }
     }
   }
   return charCodeToGlyphId;
 }
 
 /*
  * CharStrings are encoded following the the CharString Encoding sequence
@@ -29406,26 +29454,24 @@ var Metrics = {
 
 
 var EOF = {};
 
 function isEOF(v) {
   return (v === EOF);
 }
 
+var MAX_LENGTH_TO_CACHE = 1000;
+
 var Parser = (function ParserClosure() {
   function Parser(lexer, allowStreams, xref) {
     this.lexer = lexer;
     this.allowStreams = allowStreams;
     this.xref = xref;
-    this.imageCache = {
-      length: 0,
-      adler32: 0,
-      stream: null
-    };
+    this.imageCache = {};
     this.refill();
   }
 
   Parser.prototype = {
     refill: function Parser_refill() {
       this.buf1 = this.lexer.getObj();
       this.buf2 = this.lexer.getObj();
     },
@@ -29506,113 +29552,188 @@ var Parser = (function ParserClosure() {
           str = cipherTransform.decryptString(str);
         }
         return str;
       }
 
       // simple object
       return buf1;
     },
-    makeInlineImage: function Parser_makeInlineImage(cipherTransform) {
-      var lexer = this.lexer;
-      var stream = lexer.stream;
-
-      // parse dictionary
-      var dict = new Dict(null);
-      while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) {
-        if (!isName(this.buf1)) {
-          error('Dictionary key must be a name object');
-        }
-
-        var key = this.buf1.name;
-        this.shift();
-        if (isEOF(this.buf1)) {
-          break;
-        }
-        dict.set(key, this.getObj(cipherTransform));
-      }
-
-      // parse image stream
-      var startPos = stream.pos;
-
-      // searching for the /EI\s/
-      var state = 0, ch, i, ii;
-      var E = 0x45, I = 0x49, SPACE = 0x20, NL = 0xA, CR = 0xD;
+    /**
+     * Find the end of the stream by searching for the /EI\s/.
+     * @returns {number} The inline stream length.
+     */
+    findDefaultInlineStreamEnd:
+        function Parser_findDefaultInlineStreamEnd(stream) {
+      var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD;
+      var startPos = stream.pos, state = 0, ch, i, n, followingBytes;
       while ((ch = stream.getByte()) !== -1) {
         if (state === 0) {
           state = (ch === E) ? 1 : 0;
         } else if (state === 1) {
           state = (ch === I) ? 2 : 0;
         } else {
           assert(state === 2);
-          if (ch === SPACE || ch === NL || ch === CR) {
+          if (ch === SPACE || ch === LF || ch === CR) {
             // Let's check the next five bytes are ASCII... just be sure.
-            var n = 5;
-            var followingBytes = stream.peekBytes(n);
+            n = 5;
+            followingBytes = stream.peekBytes(n);
             for (i = 0; i < n; i++) {
               ch = followingBytes[i];
-              if (ch !== NL && ch !== CR && (ch < SPACE || ch > 0x7F)) {
+              if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) {
                 // Not a LF, CR, SPACE or any visible ASCII character, i.e.
                 // it's binary stuff. Resetting the state.
                 state = 0;
                 break;
               }
             }
             if (state === 2) {
-              break;  // finished!
+              break;  // Finished!
             }
           } else {
             state = 0;
           }
         }
       }
-
-      var length = (stream.pos - 4) - startPos;
+      return ((stream.pos - 4) - startPos);
+    },
+    /**
+     * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream.
+     * @returns {number} The inline stream length.
+     */
+    findASCII85DecodeInlineStreamEnd:
+        function Parser_findASCII85DecodeInlineStreamEnd(stream) {
+      var TILDE = 0x7E, GT = 0x3E;
+      var startPos = stream.pos, ch, length;
+      while ((ch = stream.getByte()) !== -1) {
+        if (ch === TILDE && stream.peekByte() === GT) {
+          stream.skip();
+          break;
+        }
+      }
+      length = stream.pos - startPos;
+      if (ch === -1) {
+        warn('Inline ASCII85Decode image stream: ' +
+             'EOD marker not found, searching for /EI/ instead.');
+        stream.skip(-length); // Reset the stream position.
+        return this.findDefaultInlineStreamEnd(stream);
+      }
+      this.inlineStreamSkipEI(stream);
+      return length;
+    },
+    /**
+     * Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream.
+     * @returns {number} The inline stream length.
+     */
+    findASCIIHexDecodeInlineStreamEnd:
+        function Parser_findASCIIHexDecodeInlineStreamEnd(stream) {
+      var GT = 0x3E;
+      var startPos = stream.pos, ch, length;
+      while ((ch = stream.getByte()) !== -1) {
+        if (ch === GT) {
+          break;
+        }
+      }
+      length = stream.pos - startPos;
+      if (ch === -1) {
+        warn('Inline ASCIIHexDecode image stream: ' +
+             'EOD marker not found, searching for /EI/ instead.');
+        stream.skip(-length); // Reset the stream position.
+        return this.findDefaultInlineStreamEnd(stream);
+      }
+      this.inlineStreamSkipEI(stream);
+      return length;
+    },
+    /**
+     * Skip over the /EI/ for streams where we search for an EOD marker.
+     */
+    inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) {
+      var E = 0x45, I = 0x49;
+      var state = 0, ch;
+      while ((ch = stream.getByte()) !== -1) {
+        if (state === 0) {
+          state = (ch === E) ? 1 : 0;
+        } else if (state === 1) {
+          state = (ch === I) ? 2 : 0;
+        } else if (state === 2) {
+          break;
+        }
+      }
+    },
+    makeInlineImage: function Parser_makeInlineImage(cipherTransform) {
+      var lexer = this.lexer;
+      var stream = lexer.stream;
+
+      // Parse dictionary.
+      var dict = new Dict(null);
+      while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) {
+        if (!isName(this.buf1)) {
+          error('Dictionary key must be a name object');
+        }
+        var key = this.buf1.name;
+        this.shift();
+        if (isEOF(this.buf1)) {
+          break;
+        }
+        dict.set(key, this.getObj(cipherTransform));
+      }
+
+      // Extract the name of the first (i.e. the current) image filter.
+      var filter = this.fetchIfRef(dict.get('Filter', 'F')), filterName;
+      if (isName(filter)) {
+        filterName = filter.name;
+      } else if (isArray(filter) && isName(filter[0])) {
+        filterName = filter[0].name;
+      }
+
+      // Parse image stream.
+      var startPos = stream.pos, length, i, ii;
+      if (filterName === 'ASCII85Decide' || filterName === 'A85') {
+        length = this.findASCII85DecodeInlineStreamEnd(stream);
+      } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') {
+        length = this.findASCIIHexDecodeInlineStreamEnd(stream);
+      } else {
+        length = this.findDefaultInlineStreamEnd(stream);
+      }
       var imageStream = stream.makeSubStream(startPos, length, dict);
 
-      // trying to cache repeat images, first we are trying to "warm up" caching
-      // using length, then comparing adler32
-      var MAX_LENGTH_TO_CACHE = 1000;
-      var cacheImage = false, adler32;
-      if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) {
+      // Cache all images below the MAX_LENGTH_TO_CACHE threshold by their
+      // adler32 checksum.
+      var adler32;
+      if (length < MAX_LENGTH_TO_CACHE) {
         var imageBytes = imageStream.getBytes();
         imageStream.reset();
 
         var a = 1;
         var b = 0;
         for (i = 0, ii = imageBytes.length; i < ii; ++i) {
-          a = (a + (imageBytes[i] & 0xff)) % 65521;
-          b = (b + a) % 65521;
-        }
-        adler32 = (b << 16) | a;
-
-        if (this.imageCache.stream && this.imageCache.adler32 === adler32) {
+          // No modulo required in the loop if imageBytes.length < 5552.
+          a += imageBytes[i] & 0xff;
+          b += a;
+        }
+        adler32 = ((b % 65521) << 16) | (a % 65521);
+
+        if (this.imageCache.adler32 === adler32) {
           this.buf2 = Cmd.get('EI');
           this.shift();
 
-          this.imageCache.stream.reset();
-          return this.imageCache.stream;
-        }
-        cacheImage = true;
-      }
-      if (!cacheImage && !this.imageCache.stream) {
-        this.imageCache.length = length;
-        this.imageCache.stream = null;
+          this.imageCache[adler32].reset();
+          return this.imageCache[adler32];
+        }
       }
 
       if (cipherTransform) {
         imageStream = cipherTransform.createStream(imageStream, length);
       }
 
       imageStream = this.filter(imageStream, dict, length);
       imageStream.dict = dict;
-      if (cacheImage) {
+      if (adler32 !== undefined) {
         imageStream.cacheKey = 'inline_' + length + '_' + adler32;
-        this.imageCache.adler32 = adler32;
-        this.imageCache.stream = imageStream;
+        this.imageCache[adler32] = imageStream;
       }
 
       this.buf2 = Cmd.get('EI');
       this.shift();
 
       return imageStream;
     },
     fetchIfRef: function Parser_fetchIfRef(obj) {
@@ -29750,32 +29871,16 @@ var Parser = (function ParserClosure() {
             }
             return new PredictorStream(
               new LZWStream(stream, maybeLength, earlyChange),
               maybeLength, params);
           }
           return new LZWStream(stream, maybeLength, earlyChange);
         }
         if (name === 'DCTDecode' || name === 'DCT') {
-          // According to the specification: for inline images, the ID operator
-          // shall be followed by a single whitespace character (unless it uses
-          // ASCII85Decode or ASCIIHexDecode filters).
-          // In practice this only seems to be followed for inline JPEG images,
-          // and generally ignoring the first byte of the stream if it is a
-          // whitespace char can even *cause* issues (e.g. in the CCITTFaxDecode
-          // filters used in issue2984.pdf).
-          // Hence when the first byte of the stream of an inline JPEG image is
-          // a whitespace character, we thus simply skip over it.
-          if (isCmd(this.buf1, 'ID')) {
-            var firstByte = stream.peekByte();
-            if (firstByte === 0x0A /* LF */ || firstByte === 0x0D /* CR */ ||
-                firstByte === 0x20 /* SPACE */) {
-              stream.skip();
-            }
-          }
           xrefStreamStats[StreamType.DCT] = true;
           return new JpegStream(stream, maybeLength, stream.dict, this.xref);
         }
         if (name === 'JPXDecode' || name === 'JPX') {
           xrefStreamStats[StreamType.JPX] = true;
           return new JpxStream(stream, maybeLength, stream.dict);
         }
         if (name === 'ASCII85Decode' || name === 'A85') {
@@ -31313,18 +31418,25 @@ var PredictorStream = (function Predicto
  * Depending on the type of JPEG a JpegStream is handled in different ways. For
  * JPEG's that are supported natively such as DeviceGray and DeviceRGB the image
  * data is stored and then loaded by the browser.  For unsupported JPEG's we use
  * a library to decode these images and the stream behaves like all the other
  * DecodeStreams.
  */
 var JpegStream = (function JpegStreamClosure() {
   function JpegStream(stream, maybeLength, dict, xref) {
-    // TODO: per poppler, some images may have 'junk' before that
-    // need to be removed
+    // Some images may contain 'junk' before the SOI (start-of-image) marker.
+    // Note: this seems to mainly affect inline images.
+    var ch;
+    while ((ch = stream.getByte()) !== -1) {
+      if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8).
+        stream.skip(-1); // Reset the stream position to the SOI.
+        break;
+      }
+    }
     this.stream = stream;
     this.maybeLength = maybeLength;
     this.dict = dict;
 
     DecodeStream.call(this, maybeLength);
   }
 
   JpegStream.prototype = Object.create(DecodeStream.prototype);
@@ -32489,17 +32601,17 @@ var CCITTFaxStream = (function CCITTFaxS
           blackPixels ^= 1;
         }
       }
 
       var gotEOL = false;
 
       if (!this.eoblock && this.row === this.rows - 1) {
         this.eof = true;
-      } else if (this.eoline || !this.byteAlign) {
+      } else {
         code1 = this.lookBits(12);
         if (this.eoline) {
           while (code1 !== EOF && code1 !== 1) {
             this.eatBits(1);
             code1 = this.lookBits(12);
           }
         } else {
           while (code1 === 0) {
@@ -34859,22 +34971,16 @@ var JpxImage = (function JpxImageClosure
                   precinctsSizes.push({
                     PPx: precinctsSize & 0xF,
                     PPy: precinctsSize >> 4
                   });
                 }
                 cod.precinctsSizes = precinctsSizes;
               }
               var unsupported = [];
-              if (cod.sopMarkerUsed) {
-                unsupported.push('sopMarkerUsed');
-              }
-              if (cod.ephMarkerUsed) {
-                unsupported.push('ephMarkerUsed');
-              }
               if (cod.selectiveArithmeticCodingBypass) {
                 unsupported.push('selectiveArithmeticCodingBypass');
               }
               if (cod.resetContextProbabilities) {
                 unsupported.push('resetContextProbabilities');
               }
               if (cod.terminationOnEachCodingPass) {
                 unsupported.push('terminationOnEachCodingPass');
@@ -35232,16 +35338,240 @@ var JpxImage = (function JpxImageClosure
           }
           i = 0;
         }
         l = 0;
       }
       throw new Error('JPX Error: Out of packets');
     };
   }
+  function ResolutionPositionComponentLayerIterator(context) {
+    var siz = context.SIZ;
+    var tileIndex = context.currentTile.index;
+    var tile = context.tiles[tileIndex];
+    var layersCount = tile.codingStyleDefaultParameters.layersCount;
+    var componentsCount = siz.Csiz;
+    var l, r, c, p;
+    var maxDecompositionLevelsCount = 0;
+    for (c = 0; c < componentsCount; c++) {
+      var component = tile.components[c];
+      maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount,
+        component.codingStyleParameters.decompositionLevelsCount);
+    }
+    var maxNumPrecinctsInLevel = new Int32Array(
+      maxDecompositionLevelsCount + 1);
+    for (r = 0; r <= maxDecompositionLevelsCount; ++r) {
+      var maxNumPrecincts = 0;
+      for (c = 0; c < componentsCount; ++c) {
+        var resolutions = tile.components[c].resolutions;
+        if (r < resolutions.length) {
+          maxNumPrecincts = Math.max(maxNumPrecincts,
+            resolutions[r].precinctParameters.numprecincts);
+        }
+      }
+      maxNumPrecinctsInLevel[r] = maxNumPrecincts;
+    }
+    l = 0;
+    r = 0;
+    c = 0;
+    p = 0;
+    
+    this.nextPacket = function JpxImage_nextPacket() {
+      // Section B.12.1.3 Resolution-position-component-layer
+      for (; r <= maxDecompositionLevelsCount; r++) {
+        for (; p < maxNumPrecinctsInLevel[r]; p++) {
+          for (; c < componentsCount; c++) {
+            var component = tile.components[c];
+            if (r > component.codingStyleParameters.decompositionLevelsCount) {
+              continue;
+            }
+            var resolution = component.resolutions[r];
+            var numprecincts = resolution.precinctParameters.numprecincts;
+            if (p >= numprecincts) {
+              continue;
+            }
+            for (; l < layersCount;) {
+              var packet = createPacket(resolution, p, l);
+              l++;
+              return packet;
+            }
+            l = 0;
+          }
+          c = 0;
+        }
+        p = 0;
+      }
+      throw new Error('JPX Error: Out of packets');
+    };
+  }
+  function PositionComponentResolutionLayerIterator(context) {
+    var siz = context.SIZ;
+    var tileIndex = context.currentTile.index;
+    var tile = context.tiles[tileIndex];
+    var layersCount = tile.codingStyleDefaultParameters.layersCount;
+    var componentsCount = siz.Csiz;
+    var precinctsSizes = getPrecinctSizesInImageScale(tile);
+    var precinctsIterationSizes = precinctsSizes;
+    var l = 0, r = 0, c = 0, px = 0, py = 0;
+
+    this.nextPacket = function JpxImage_nextPacket() {
+      // Section B.12.1.4 Position-component-resolution-layer
+      for (; py < precinctsIterationSizes.maxNumHigh; py++) {
+        for (; px < precinctsIterationSizes.maxNumWide; px++) {
+          for (; c < componentsCount; c++) {
+            var component = tile.components[c];
+            var decompositionLevelsCount =
+              component.codingStyleParameters.decompositionLevelsCount;
+            for (; r <= decompositionLevelsCount; r++) {
+              var resolution = component.resolutions[r];
+              var sizeInImageScale =
+                precinctsSizes.components[c].resolutions[r];
+              var k = getPrecinctIndexIfExist(
+                px,
+                py,
+                sizeInImageScale,
+                precinctsIterationSizes,
+                resolution);
+              if (k === null) {
+                continue;
+              }
+              for (; l < layersCount;) {
+                var packet = createPacket(resolution, k, l);
+                l++;
+                return packet;
+              }
+              l = 0;
+            }
+            r = 0;
+          }
+          c = 0;
+        }
+        px = 0;
+      }
+      throw new Error('JPX Error: Out of packets');
+    };
+  }
+  function ComponentPositionResolutionLayerIterator(context) {
+    var siz = context.SIZ;
+    var tileIndex = context.currentTile.index;
+    var tile = context.tiles[tileIndex];
+    var layersCount = tile.codingStyleDefaultParameters.layersCount;
+    var componentsCount = siz.Csiz;
+    var precinctsSizes = getPrecinctSizesInImageScale(tile);
+    var l = 0, r = 0, c = 0, px = 0, py = 0;
+    
+    this.nextPacket = function JpxImage_nextPacket() {
+      // Section B.12.1.5 Component-position-resolution-layer
+      for (; c < componentsCount; ++c) {
+        var component = tile.components[c];
+        var precinctsIterationSizes = precinctsSizes.components[c];
+        var decompositionLevelsCount =
+          component.codingStyleParameters.decompositionLevelsCount;
+        for (; py < precinctsIterationSizes.maxNumHigh; py++) {
+          for (; px < precinctsIterationSizes.maxNumWide; px++) {
+            for (; r <= decompositionLevelsCount; r++) {
+              var resolution = component.resolutions[r];
+              var sizeInImageScale = precinctsIterationSizes.resolutions[r];
+              var k = getPrecinctIndexIfExist(
+                px,
+                py,
+                sizeInImageScale,
+                precinctsIterationSizes,
+                resolution);
+              if (k === null) {
+                continue;
+              }
+              for (; l < layersCount;) {
+                var packet = createPacket(resolution, k, l);
+                l++;
+                return packet;
+              }
+              l = 0;
+            }
+            r = 0;
+          }
+          px = 0;
+        }
+        py = 0;
+      }
+      throw new Error('JPX Error: Out of packets');
+    };
+  }
+  function getPrecinctIndexIfExist(
+    pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) {
+    var posX = pxIndex * precinctIterationSizes.minWidth;
+    var posY = pyIndex * precinctIterationSizes.minHeight;
+    if (posX % sizeInImageScale.width !== 0 ||
+        posY % sizeInImageScale.height !== 0) {
+      return null;
+    }
+    var startPrecinctRowIndex =
+      (posY / sizeInImageScale.width) *
+      resolution.precinctParameters.numprecinctswide;
+    return (posX / sizeInImageScale.height) + startPrecinctRowIndex;
+  }
+  function getPrecinctSizesInImageScale(tile) {
+    var componentsCount = tile.components.length;
+    var minWidth = Number.MAX_VALUE;
+    var minHeight = Number.MAX_VALUE;
+    var maxNumWide = 0;
+    var maxNumHigh = 0;
+    var sizePerComponent = new Array(componentsCount);
+    for (var c = 0; c < componentsCount; c++) {
+      var component = tile.components[c];
+      var decompositionLevelsCount =
+        component.codingStyleParameters.decompositionLevelsCount;
+      var sizePerResolution = new Array(decompositionLevelsCount + 1);
+      var minWidthCurrentComponent = Number.MAX_VALUE;
+      var minHeightCurrentComponent = Number.MAX_VALUE;
+      var maxNumWideCurrentComponent = 0;
+      var maxNumHighCurrentComponent = 0;
+      var scale = 1;
+      for (var r = decompositionLevelsCount; r >= 0; --r) {
+        var resolution = component.resolutions[r];
+        var widthCurrentResolution =
+          scale * resolution.precinctParameters.precinctWidth;
+        var heightCurrentResolution =
+          scale * resolution.precinctParameters.precinctHeight;
+        minWidthCurrentComponent = Math.min(
+          minWidthCurrentComponent,
+          widthCurrentResolution);
+        minHeightCurrentComponent = Math.min(
+          minHeightCurrentComponent,
+          heightCurrentResolution);
+        maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent,
+          resolution.precinctParameters.numprecinctswide);
+        maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent,
+          resolution.precinctParameters.numprecinctshigh);
+        sizePerResolution[r] = {
+          width: widthCurrentResolution,
+          height: heightCurrentResolution
+        };
+        scale <<= 1;
+      }
+      minWidth = Math.min(minWidth, minWidthCurrentComponent);
+      minHeight = Math.min(minHeight, minHeightCurrentComponent);
+      maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent);
+      maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent);
+      sizePerComponent[c] = {
+        resolutions: sizePerResolution,
+        minWidth: minWidthCurrentComponent,
+        minHeight: minHeightCurrentComponent,
+        maxNumWide: maxNumWideCurrentComponent,
+        maxNumHigh: maxNumHighCurrentComponent
+      };
+    }
+    return {
+      components: sizePerComponent,
+      minWidth: minWidth,
+      minHeight: minHeight,
+      maxNumWide: maxNumWide,
+      maxNumHigh: maxNumHigh
+    };
+  }
   function buildPackets(context) {
     var siz = context.SIZ;
     var tileIndex = context.currentTile.index;
     var tile = context.tiles[tileIndex];
     var componentsCount = siz.Csiz;
     // Creating resolutions and sub-bands for each component
     for (var c = 0; c < componentsCount; c++) {
       var component = tile.components[c];
@@ -35324,16 +35654,28 @@ var JpxImage = (function JpxImageClosure
       case 0:
         tile.packetsIterator =
           new LayerResolutionComponentPositionIterator(context);
         break;
       case 1:
         tile.packetsIterator =
           new ResolutionLayerComponentPositionIterator(context);
         break;
+      case 2:
+        tile.packetsIterator =
+          new ResolutionPositionComponentLayerIterator(context);
+        break;
+      case 3:
+        tile.packetsIterator =
+          new PositionComponentResolutionLayerIterator(context);
+        break;
+      case 4:
+        tile.packetsIterator =
+          new ComponentPositionResolutionLayerIterator(context);
+        break;
       default:
         throw new Error('JPX Error: Unsupported progression order ' +
                         progressionOrder);
     }
   }
   function parseTilePackets(context, data, offset, dataLength) {
     var position = 0;
     var buffer, bufferSize = 0, skipNextBit = false;
@@ -35351,16 +35693,31 @@ var JpxImage = (function JpxImageClosure
         }
         if (b === 0xFF) {
           skipNextBit = true;
         }
       }
       bufferSize -= count;
       return (buffer >>> bufferSize) & ((1 << count) - 1);
     }
+    function skipMarkerIfEqual(value) {
+      if (data[offset + position - 1] === 0xFF &&
+          data[offset + position] === value) {
+        skipBytes(1);
+        return true;
+      } else if (data[offset + position] === 0xFF &&
+                 data[offset + position + 1] === value) {
+        skipBytes(2);
+        return true;
+      }
+      return false;
+    }
+    function skipBytes(count) {
+      position += count;
+    }
     function alignToByte() {
       bufferSize = 0;
       if (skipNextBit) {
         position++;
         skipNextBit = false;
       }
     }
     function readCodingpasses() {
@@ -35378,23 +35735,29 @@ var JpxImage = (function JpxImageClosure
       if (value < 31) {
         return value + 6;
       }
       value = readBits(7);
       return value + 37;
     }
     var tileIndex = context.currentTile.index;
     var tile = context.tiles[tileIndex];
+    var sopMarkerUsed = context.COD.sopMarkerUsed;
+    var ephMarkerUsed = context.COD.ephMarkerUsed;
     var packetsIterator = tile.packetsIterator;
     while (position < dataLength) {
       alignToByte();
+      if (sopMarkerUsed && skipMarkerIfEqual(0x91)) {
+        // Skip also marker segment length and packet sequence ID
+        skipBytes(4);
+      }
+      var packet = packetsIterator.nextPacket();
       if (!readBits(1)) {
         continue;
       }
-      var packet = packetsIterator.nextPacket();
       var layerNumber = packet.layerNumber;
       var queue = [], codeblock;
       for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) {
         codeblock = packet.codeblocks[i];
         var precinct = codeblock.precinct;
         var codeblockColumn = codeblock.cbx - precinct.cbxMin;
         var codeblockRow = codeblock.cby - precinct.cbyMin;
         var codeblockIncluded = false;
@@ -35463,16 +35826,19 @@ var JpxImage = (function JpxImageClosure
         var codedDataLength = readBits(bits);
         queue.push({
           codeblock: codeblock,
           codingpasses: codingpasses,
           dataLength: codedDataLength
         });
       }
       alignToByte();
+      if (ephMarkerUsed) {
+        skipMarkerIfEqual(0x92);
+      }
       while (queue.length > 0) {
         var packetItem = queue.shift();
         codeblock = packetItem.codeblock;
         if (codeblock['data'] === undefined) {
           codeblock.data = [];
         }
         codeblock.data.push({
           data: data,
--- a/browser/extensions/pdfjs/content/web/viewer.css
+++ b/browser/extensions/pdfjs/content/web/viewer.css
@@ -98,21 +98,16 @@
   border: 0;
 }
 
 :fullscreen .pdfViewer .page {
   margin-bottom: 100%;
   border: 0;
 }
 
-.pdfViewer .page .annotationHighlight {
-  position: absolute;
-  border: 2px #FFFF99 solid;
-}
-
 .pdfViewer .page .annotText > img {
   position: absolute;
   cursor: pointer;
 }
 
 .pdfViewer .page .annotTextContentWrapper {
   position: absolute;
   width: 20em;
@@ -806,17 +801,16 @@ html[dir='rtl'] .dropdownToolbarButton {
 html[dir='ltr'] .dropdownToolbarButton {
   background-position: 95%;
 }
 html[dir='rtl'] .dropdownToolbarButton {
   background-position: 5%;
 }
 
 .dropdownToolbarButton > select {
-  -moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */
   min-width: 140px;
   font-size: 12px;
   color: hsl(0,0%,95%);
   margin: 0;
   padding: 0;
   border: none;
   background: rgba(0,0,0,0); /* Opera does not support 'transparent' <select> background */
 }
@@ -1749,16 +1743,20 @@ html[dir='rtl'] #documentPropertiesOverl
     display: block;
   }
   #printContainer canvas {
     position: relative;
     top: 0;
     left: 0;
     display: block;
   }
+  #printContainer div {
+    page-break-after: always;
+    page-break-inside: avoid;
+  }
 }
 
 .visibleLargeView,
 .visibleMediumView,
 .visibleSmallView {
   display: none;
 }
 
new file mode 100644
--- /dev/null
+++ b/docshell/base/TimelineMarker.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=2 sw=2 tw=80 et:
+ *
+ * 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/. */
+
+#include "nsDocShell.h"
+#include "TimelineMarker.h"
+
+TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName,
+                               TracingMetadata aMetaData)
+  : mName(aName)
+  , mMetaData(aMetaData)
+{
+  MOZ_COUNT_CTOR(TimelineMarker);
+  MOZ_ASSERT(aName);
+  aDocShell->Now(&mTime);
+  if (aMetaData == TRACING_INTERVAL_START) {
+    CaptureStack();
+  }
+}
+
+TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName,
+                               TracingMetadata aMetaData,
+                               const nsAString& aCause)
+  : mName(aName)
+  , mMetaData(aMetaData)
+  , mCause(aCause)
+{
+  MOZ_COUNT_CTOR(TimelineMarker);
+  MOZ_ASSERT(aName);
+  aDocShell->Now(&mTime);
+  if (aMetaData == TRACING_INTERVAL_START) {
+    CaptureStack();
+  }
+}
+
+TimelineMarker::~TimelineMarker()
+{
+  MOZ_COUNT_DTOR(TimelineMarker);
+}
new file mode 100644
--- /dev/null
+++ b/docshell/base/TimelineMarker.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=2 sw=2 tw=80 et:
+ *
+ * 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/. */
+
+#ifndef TimelineMarker_h__
+#define TimelineMarker_h__
+
+#include "nsString.h"
+#include "GeckoProfiler.h"
+#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
+#include "nsContentUtils.h"
+#include "jsapi.h"
+
+class nsDocShell;
+
+// Objects of this type can be added to the timeline.  The class can
+// also be subclassed to let a given marker creator provide custom
+// details.
+class TimelineMarker
+{
+public:
+  TimelineMarker(nsDocShell* aDocShell, const char* aName,
+                 TracingMetadata aMetaData);
+
+  TimelineMarker(nsDocShell* aDocShell, const char* aName,
+                 TracingMetadata aMetaData,
+                 const nsAString& aCause);
+
+  virtual ~TimelineMarker();
+
+  // Check whether two markers should be considered the same,
+  // for the purpose of pairing start and end markers.  Normally
+  // this definition suffices.
+  virtual bool Equals(const TimelineMarker* other)
+  {
+    return strcmp(mName, other->mName) == 0;
+  }
+
+  // Add details specific to this marker type to aMarker.  The
+  // standard elements have already been set.  This method is
+  // called on both the starting and ending markers of a pair.
+  // Ordinarily the ending marker doesn't need to do anything
+  // here.
+  virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
+  {
+  }
+
+  virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
+  {
+    MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
+  }
+
+  const char* GetName() const
+  {
+    return mName;
+  }
+
+  TracingMetadata GetMetaData() const
+  {
+    return mMetaData;
+  }
+
+  DOMHighResTimeStamp GetTime() const
+  {
+    return mTime;
+  }
+
+  const nsString& GetCause() const
+  {
+    return mCause;
+  }
+
+  JSObject* GetStack()
+  {
+    if (mStackTrace) {
+      return mStackTrace->get();
+    }
+    return nullptr;
+  }
+
+protected:
+
+  void CaptureStack()
+  {
+    JSContext* ctx = nsContentUtils::GetCurrentJSContext();
+    if (ctx) {
+      JS::RootedObject stack(ctx);
+      if (JS::CaptureCurrentStack(ctx, &stack)) {
+        mStackTrace.emplace(ctx, stack.get());
+      } else {
+        JS_ClearPendingException(ctx);
+      }
+    }
+  }
+
+private:
+
+  const char* mName;
+  TracingMetadata mMetaData;
+  DOMHighResTimeStamp mTime;
+  nsString mCause;
+
+  // While normally it is not a good idea to make a persistent
+  // root, in this case changing nsDocShell to participate in
+  // cycle collection was deemed too invasive, the stack trace
+  // can't actually cause a cycle, and the markers are only held
+  // here temporarily to boot.
+  mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
+};
+
+#endif /* TimelineMarker_h__ */
--- a/docshell/base/moz.build
+++ b/docshell/base/moz.build
@@ -57,16 +57,17 @@ UNIFIED_SOURCES += [
     'nsDocShellEditorData.cpp',
     'nsDocShellEnumerator.cpp',
     'nsDocShellLoadInfo.cpp',
     'nsDocShellTransferableHooks.cpp',
     'nsDownloadHistory.cpp',
     'nsDSURIContentListener.cpp',
     'nsWebNavigationInfo.cpp',
     'SerializedLoadContext.cpp',
+    'TimelineMarker.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 MSVC_ENABLE_PGO = True
 
 include('/ipc/chromium/chromium-config.mozbuild')
 
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -27,16 +27,17 @@
 
 // Helper Classes
 #include "nsCOMPtr.h"
 #include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences
 #include "nsString.h"
 #include "nsAutoPtr.h"
 #include "nsThreadUtils.h"
 #include "nsContentUtils.h"
+#include "TimelineMarker.h"
 
 // Threshold value in ms for META refresh based redirects
 #define REFRESH_REDIRECT_TIMER 15000
 
 // Interfaces Needed
 #include "nsIDocCharset.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIRefreshURI.h"
@@ -255,135 +256,16 @@ public:
 
     // Notify Scroll observers when an async panning/zooming transform
     // has started being applied
     void NotifyAsyncPanZoomStarted(const mozilla::CSSIntPoint aScrollPos);
     // Notify Scroll observers when an async panning/zooming transform
     // is no longer applied
     void NotifyAsyncPanZoomStopped(const mozilla::CSSIntPoint aScrollPos);
 
-    // Objects of this type can be added to the timeline.  The class
-    // can also be subclassed to let a given marker creator provide
-    // custom details.
-    class TimelineMarker
-    {
-    public:
-        TimelineMarker(nsDocShell* aDocShell, const char* aName,
-                       TracingMetadata aMetaData)
-            : mName(aName)
-            , mMetaData(aMetaData)
-        {
-            MOZ_COUNT_CTOR(TimelineMarker);
-            MOZ_ASSERT(aName);
-            aDocShell->Now(&mTime);
-            if (aMetaData == TRACING_INTERVAL_START) {
-                CaptureStack();
-            }
-        }
-
-        TimelineMarker(nsDocShell* aDocShell, const char* aName,
-                       TracingMetadata aMetaData,
-                       const nsAString& aCause)
-            : mName(aName)
-            , mMetaData(aMetaData)
-            , mCause(aCause)
-        {
-            MOZ_COUNT_CTOR(TimelineMarker);
-            MOZ_ASSERT(aName);
-            aDocShell->Now(&mTime);
-            if (aMetaData == TRACING_INTERVAL_START) {
-                CaptureStack();
-            }
-        }
-
-        virtual ~TimelineMarker()
-        {
-            MOZ_COUNT_DTOR(TimelineMarker);
-        }
-
-        // Check whether two markers should be considered the same,
-        // for the purpose of pairing start and end markers.  Normally
-        // this definition suffices.
-        virtual bool Equals(const TimelineMarker* other)
-        {
-            return strcmp(mName, other->mName) == 0;
-        }
-
-        // Add details specific to this marker type to aMarker.  The
-        // standard elements have already been set.  This method is
-        // called on both the starting and ending markers of a pair.
-        // Ordinarily the ending marker doesn't need to do anything
-        // here.
-        virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
-        {
-        }
-
-        virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
-        {
-            MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
-        }
-
-        const char* GetName() const
-        {
-            return mName;
-        }
-
-        TracingMetadata GetMetaData() const
-        {
-            return mMetaData;
-        }
-
-        DOMHighResTimeStamp GetTime() const
-        {
-            return mTime;
-        }
-
-        const nsString& GetCause() const
-        {
-            return mCause;
-        }
-
-        JSObject* GetStack()
-        {
-            if (mStackTrace) {
-                return mStackTrace->get();
-            }
-            return nullptr;
-        }
-
-    protected:
-
-        void CaptureStack()
-        {
-            JSContext* ctx = nsContentUtils::GetCurrentJSContext();
-            if (ctx) {
-                JS::RootedObject stack(ctx);
-                if (JS::CaptureCurrentStack(ctx, &stack)) {
-                    mStackTrace.emplace(ctx, stack.get());
-                } else {
-                    JS_ClearPendingException(ctx);
-                }
-            }
-        }
-
-    private:
-
-        const char* mName;
-        TracingMetadata mMetaData;
-        DOMHighResTimeStamp mTime;
-        nsString mCause;
-
-        // While normally it is not a good idea to make a persistent
-        // root, in this case changing nsDocShell to participate in
-        // cycle collection was deemed too invasive, the stack trace
-        // can't actually cause a cycle, and the markers are only held
-        // here temporarily to boot.
-        mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
-    };
-
     // Add new profile timeline markers to this docShell. This will only add
     // markers if the docShell is currently recording profile timeline markers.
     // See nsIDocShell::recordProfileTimelineMarkers
     void AddProfileTimelineMarker(const char* aName,
                                   TracingMetadata aMetaData);
     void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker> &aMarker);
 
     // Global counter for how many docShells are currently recording profile
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -800,32 +800,32 @@ ReifyStack(nsIStackFrame* aStack, nsTArr
     NS_ENSURE_SUCCESS(rv, rv);
 
     stack.swap(caller);
   }
 
   return NS_OK;
 }
 
-class ConsoleTimelineMarker : public nsDocShell::TimelineMarker
+class ConsoleTimelineMarker : public TimelineMarker
 {
 public:
   ConsoleTimelineMarker(nsDocShell* aDocShell,
                         TracingMetadata aMetaData,
                         const nsAString& aCause)
-    : nsDocShell::TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause)
+    : TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause)
   {
     if (aMetaData == TRACING_INTERVAL_END) {
       CaptureStack();
     }
   }
 
-  virtual bool Equals(const nsDocShell::TimelineMarker* aOther)
+  virtual bool Equals(const TimelineMarker* aOther)
   {
-    if (!nsDocShell::TimelineMarker::Equals(aOther)) {
+    if (!TimelineMarker::Equals(aOther)) {
       return false;
     }
     // Console markers must have matching causes as well.
     return GetCause() == aOther->GetCause();
   }
 
   virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
   {
@@ -964,17 +964,17 @@ Console::Method(JSContext* aCx, MethodNa
       }
 
       if (isTimelineRecording && aData.Length() == 1) {
         JS::Rooted<JS::Value> value(aCx, aData[0]);
         JS::Rooted<JSString*> jsString(aCx, JS::ToString(aCx, value));
         if (jsString) {
           nsAutoJSString key;
           if (key.init(aCx, jsString)) {
-            mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
+            mozilla::UniquePtr<TimelineMarker> marker =
               MakeUnique<ConsoleTimelineMarker>(docShell,
                                                 aMethodName == MethodTime ? TRACING_INTERVAL_START : TRACING_INTERVAL_END,
                                                 key);
             docShell->AddProfileTimelineMarker(marker);
           }
         }
       }
 
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -1019,22 +1019,22 @@ EventListenerManager::GetDocShellForTarg
 
   if (doc) {
     docShell = doc->GetDocShell();
   }
 
   return docShell;
 }
 
-class EventTimelineMarker : public nsDocShell::TimelineMarker
+class EventTimelineMarker : public TimelineMarker
 {
 public:
   EventTimelineMarker(nsDocShell* aDocShell, TracingMetadata aMetaData,
                       uint16_t aPhase, const nsAString& aCause)
-    : nsDocShell::TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
+    : TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
     , mPhase(aPhase)
   {
   }
 
   virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
   {
     if (GetMetaData() == TRACING_INTERVAL_START) {
       aMarker.mType.Construct(GetCause());
@@ -1109,17 +1109,17 @@ EventListenerManager::HandleEventInterna
               docShell->GetRecordProfileTimelineMarkers(&isTimelineRecording);
             }
             if (isTimelineRecording) {
               nsDocShell* ds = static_cast<nsDocShell*>(docShell.get());
               nsAutoString typeStr;
               (*aDOMEvent)->GetType(typeStr);
               uint16_t phase;
               (*aDOMEvent)->GetEventPhase(&phase);
-              mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
+              mozilla::UniquePtr<TimelineMarker> marker =
                 MakeUnique<EventTimelineMarker>(ds, TRACING_INTERVAL_START,
                                                 phase, typeStr);
               ds->AddProfileTimelineMarker(marker);
             }
           }
 
           if (NS_FAILED(HandleEventSubType(listener, *aDOMEvent,
                                            aCurrentTarget))) {
--- a/layout/base/FrameLayerBuilder.cpp
+++ b/layout/base/FrameLayerBuilder.cpp
@@ -4466,21 +4466,21 @@ static void DrawForcedBackgroundColor(Dr
 {
   if (NS_GET_A(aBackgroundColor) > 0) {
     nsIntRect r = aLayer->GetVisibleRegion().GetBounds();
     ColorPattern color(ToDeviceColor(aBackgroundColor));
     aDrawTarget.FillRect(Rect(r.x, r.y, r.width, r.height), color);
   }
 }
 
-class LayerTimelineMarker : public nsDocShell::TimelineMarker
+class LayerTimelineMarker : public TimelineMarker
 {
 public:
   LayerTimelineMarker(nsDocShell* aDocShell, const nsIntRegion& aRegion)
-    : nsDocShell::TimelineMarker(aDocShell, "Layer", TRACING_EVENT)
+    : TimelineMarker(aDocShell, "Layer", TRACING_EVENT)
     , mRegion(aRegion)
   {
   }
 
   ~LayerTimelineMarker()
   {
   }
 
@@ -4648,17 +4648,17 @@ FrameLayerBuilder::DrawPaintedLayer(Pain
     FlashPaint(aContext);
   }
 
   if (presContext && presContext->GetDocShell() && isActiveLayerManager) {
     nsDocShell* docShell = static_cast<nsDocShell*>(presContext->GetDocShell());
     bool isRecording;
     docShell->GetRecordProfileTimelineMarkers(&isRecording);
     if (isRecording) {
-      mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
+      mozilla::UniquePtr<TimelineMarker> marker =
         MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw);
       docShell->AddProfileTimelineMarker(marker);
     }
   }
 
   if (!aRegionToInvalidate.IsEmpty()) {
     aLayer->AddInvalidRect(aRegionToInvalidate.GetBounds());
   }
--- a/mobile/android/base/menu/GeckoMenuItem.java
+++ b/mobile/android/base/menu/GeckoMenuItem.java
@@ -439,17 +439,19 @@ public class GeckoMenuItem implements Me
     @Override
     public MenuItem setTitleCondensed(CharSequence title) {
         mTitleCondensed = title;
         return this;
     }
 
     @Override
     public MenuItem setVisible(boolean visible) {
-        if (mVisible != visible) {
+        // Action views are not normal menu items and visibility can get out
+        // of sync unless we dispatch whenever required.
+        if (isActionItem() || mVisible != visible) {
             mVisible = visible;
             if (mShouldDispatchChanges) {
                 mMenu.onItemChanged(this);
             } else {
                 mDidChange = true;
             }
         }
         return this;
--- a/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml
+++ b/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml
@@ -1,17 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
                                            xmlns:gecko="http://schemas.android.com/apk/res-auto"
                                            style="@style/TabsItem"
-                                           android:focusable="true"
                                            android:id="@+id/info"
                                            android:layout_width="wrap_content"
                                            android:layout_height="wrap_content"
                                            android:gravity="center"
                                            android:orientation="vertical">
 
     <LinearLayout android:layout_width="fill_parent"
                   android:layout_height="wrap_content"
@@ -32,17 +31,17 @@
                android:singleLine="true"
                android:duplicateParentState="true"
                gecko:fadeWidth="15dp"
                android:paddingRight="5dp"/>
 
 
         <!-- Use of baselineAlignBottom only supported from API 11+ - if this needs to work on lower API versions
              we'll need to override getBaseLine() and return image height, but we assume this won't happen -->
-        <ImageButton android:id="@+id/close"
+        <ImageView android:id="@+id/close"
                      style="@style/TabsItemClose"
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
                      android:scaleType="center"
                      android:baselineAlignBottom="true"
                      android:background="@android:color/transparent"
                      android:contentDescription="@string/close_tab"
                      android:src="@drawable/new_tablet_tab_item_close_button"
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -22,16 +22,17 @@ import android.content.res.TypedArray;
 import android.graphics.PointF;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.animation.DecelerateInterpolator;
+import android.widget.AdapterView;
 import android.widget.Button;
 import android.widget.GridView;
 import com.nineoldandroids.animation.Animator;
 import com.nineoldandroids.animation.AnimatorSet;
 import com.nineoldandroids.animation.ObjectAnimator;
 import com.nineoldandroids.animation.PropertyValuesHolder;
 import com.nineoldandroids.animation.ValueAnimator;
 
@@ -89,48 +90,48 @@ class TabsGridLayout extends GridView
 
         final Resources resources = getResources();
         mColumnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width);
         setColumnWidth(mColumnWidth);
 
         final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding);
         final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top);
         setPadding(padding, paddingTop, padding, padding);
+
+        setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                TabsLayoutItemView tab = (TabsLayoutItemView) view;
+                Tabs.getInstance().selectTab(tab.getTabId());
+                autoHidePanel();
+            }
+        });
     }
 
     private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
 
         final private Button.OnClickListener mCloseClickListener;
-        final private View.OnClickListener mSelectClickListener;
 
         public TabsGridLayoutAdapter (Context context) {
             super(context, R.layout.new_tablet_tabs_item_cell);
 
             mCloseClickListener = new Button.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     closeTab(v);
                 }
             };
-
-            mSelectClickListener = new View.OnClickListener() {
-                @Override
-                public void onClick(View v) {
-                    TabsLayoutItemView tab = (TabsLayoutItemView) v;
-                    Tabs.getInstance().selectTab(tab.getTabId());
-                    autoHidePanel();
-                }
-            };
         }
 
         @Override
         TabsLayoutItemView newView(int position, ViewGroup parent) {
             final TabsLayoutItemView item = super.newView(position, parent);
-            item.setOnClickListener(mSelectClickListener);
+
             item.setCloseOnClickListener(mCloseClickListener);
+
             return item;
         }
 
         @Override
         public void bindView(TabsLayoutItemView view, Tab tab) {
             super.bindView(view, tab);
 
             // If we're recycling this view, there's a chance it was transformed during
--- a/mobile/android/base/tabs/TabsLayoutAdapter.java
+++ b/mobile/android/base/tabs/TabsLayoutAdapter.java
@@ -41,16 +41,17 @@ public class TabsLayoutAdapter extends B
         if (tabRemoved) {
             notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
         }
         return tabRemoved;
     }
 
     final void clear() {
         mTabs = null;
+
         notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
     }
 
     @Override
     public int getCount() {
         return (mTabs == null ? 0 : mTabs.size());
     }
 
@@ -67,16 +68,21 @@ public class TabsLayoutAdapter extends B
     final int getPositionForTab(Tab tab) {
         if (mTabs == null || tab == null)
             return -1;
 
         return mTabs.indexOf(tab);
     }
 
     @Override
+    public boolean isEnabled(int position) {
+        return true;
+    }
+
+    @Override
     final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
         final TabsLayoutItemView view;
         if (convertView == null) {
             view = newView(position, parent);
         } else {
             view = (TabsLayoutItemView) convertView;
         }
         final Tab tab = mTabs.get(position);
--- a/mobile/android/base/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/tabs/TabsLayoutItemView.java
@@ -27,17 +27,17 @@ public class TabsLayoutItemView extends 
                                 implements Checkable {
     private static final String LOGTAG = "Gecko" + TabsLayoutItemView.class.getSimpleName();
     private static final int[] STATE_CHECKED = { android.R.attr.state_checked };
     private boolean mChecked;
 
     private int mTabId;
     private TextView mTitle;
     private ImageView mThumbnail;
-    private ImageButton mCloseButton;
+    private ImageView mCloseButton;
     private TabThumbnailWrapper mThumbnailWrapper;
 
     public TabsLayoutItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
     @Override
     public int[] onCreateDrawableState(int extraSpace) {
@@ -46,16 +46,21 @@ public class TabsLayoutItemView extends 
         if (mChecked) {
             mergeDrawableStates(drawableState, STATE_CHECKED);
         }
 
         return drawableState;
     }
 
     @Override
+    public boolean isEnabled() {
+        return true;
+    }
+
+    @Override
     public boolean isChecked() {
         return mChecked;
     }
 
     @Override
     public void setChecked(boolean checked) {
         if (mChecked == checked) {
             return;
@@ -82,17 +87,17 @@ public class TabsLayoutItemView extends 
         mCloseButton.setOnClickListener(mOnClickListener);
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTitle = (TextView) findViewById(R.id.title);
         mThumbnail = (ImageView) findViewById(R.id.thumbnail);
-        mCloseButton = (ImageButton) findViewById(R.id.close);
+        mCloseButton = (ImageView) findViewById(R.id.close);
         mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
 
         if (NewTabletUI.isEnabled(getContext())) {
             growCloseButtonHitArea();
         }
     }
 
     private void growCloseButtonHitArea() {
--- a/toolkit/content/widgets/preferences.xml
+++ b/toolkit/content/widgets/preferences.xml
@@ -127,18 +127,22 @@
         if (!this.name)
           return;
 
         this.preferences.rootBranchInternal
             .addObserver(this.name, this.preferences, false);
         // In non-instant apply mode, we must try and use the last saved state
         // from any previous opens of a child dialog instead of the value from
         // preferences, to pick up any edits a user may have made. 
+
+        var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+                    .getService(Components.interfaces.nsIScriptSecurityManager);
         if (this.preferences.type == "child" && 
-            !this.instantApply && window.opener) {
+            !this.instantApply && window.opener &&
+            secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
           var pdoc = window.opener.document;
 
           // Try to find a preference element for the same preference.
           var preference = null;
           var parentPreferences = pdoc.getElementsByTagName("preferences");
           for (var k = 0; (k < parentPreferences.length && !preference); ++k) {
             var parentPrefs = parentPreferences[k]
                                     .getElementsByAttribute("name", this.name);
@@ -1048,17 +1052,20 @@
     </implementation>
     <handlers>
       <handler event="dialogaccept">
       <![CDATA[
         if (!this._fireEvent("beforeaccept", this)){
           return false;
         }
 
-        if (this.type == "child" && window.opener) {
+        var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
+                    .getService(Components.interfaces.nsIScriptSecurityManager);
+        if (this.type == "child" && window.opener &&
+            secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
           var psvc = Components.classes["@mozilla.org/preferences-service;1"]
                                .getService(Components.interfaces.nsIPrefBranch);
           var instantApply = psvc.getBoolPref("browser.preferences.instantApply");
           if (instantApply) {
             var panes = this.preferencePanes;
             for (var i = 0; i < panes.length; ++i)
               panes[i].writePreferences(true);
           }
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/director-manager.js
@@ -0,0 +1,780 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const events = require("sdk/event/core");
+const protocol = require("devtools/server/protocol");
+
+const { Cu, Ci } = require("chrome");
+
+const { on, once, off, emit } = events;
+const { method, Arg, Option, RetVal, types } = protocol;
+
+const { sandbox, evaluate } = require('sdk/loader/sandbox');
+const { Class } = require("sdk/core/heritage");
+
+const { PlainTextConsole } = require('sdk/console/plain-text');
+
+const { DirectorRegistry } = require("./director-registry");
+
+/**
+ * E10S child setup helper
+ */
+
+const {DebuggerServer} = require("devtools/server/main");
+
+/**
+ * Error Messages
+ */
+
+const ERR_MESSAGEPORT_FINALIZED = "message port finalized";
+
+const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id";
+const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id";
+
+/**
+ * Type describing a messageport event
+ */
+types.addDictType("messageportevent", {
+  isTrusted: "boolean",
+  data: "nullable:primitive",
+  origin: "nullable:string",
+  lastEventId: "nullable:string",
+  source: "messageport",
+  ports: "nullable:array:messageport"
+});
+
+/**
+ * A MessagePort Actor allowing communication through messageport events
+ * over the remote debugging protocol.
+ */
+let MessagePortActor = exports.MessagePortActor = protocol.ActorClass({
+  typeName: "messageport",
+
+  /**
+   * Create a MessagePort actor.
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param MessagePort port
+   *        The wrapped MessagePort.
+   */
+  initialize: function(conn, port) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+
+    // NOTE: can't get a weak reference because we need to subscribe events
+    // using port.onmessage or addEventListener
+    this.port = port;
+  },
+
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Sends a message on the wrapped message port.
+   *
+   * @param Object msg
+   *        The JSON serializable message event payload
+   */
+  postMessage: method(function (msg) {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    this.port.postMessage(msg);
+  }, {
+    oneway: true,
+    request: {
+      msg: Arg(0, "nullable:json")
+    }
+  }),
+
+  /**
+   * Starts to receive and send queued messages on this message port.
+   */
+  start: method(function () {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    // NOTE: set port.onmessage to a function is an implicit start
+    // and starts to send queued messages.
+    // On the client side we should set MessagePortClient.onmessage
+    // to a setter which register an handler to the message event
+    // and call the actor start method to start receiving messages
+    // from the MessagePort's queue.
+    this.port.onmessage = (evt) => {
+      var ports;
+
+      // TODO: test these wrapped ports
+      if (Array.isArray(evt.ports)) {
+        ports = evt.ports.map((port) => {
+          let actor = new MessagePortActor(this.conn, port);
+          this.manage(actor);
+          return actor;
+        });
+      }
+
+      emit(this, "message", {
+        isTrusted: evt.isTrusted,
+        data: evt.data,
+        origin: evt.origin,
+        lastEventId: evt.lastEventId,
+        source: this,
+        ports: ports
+      });
+    };
+  }, {
+    oneway: true,
+    request: {}
+  }),
+
+  /**
+   * Starts to receive and send queued messages on this message port, or
+   * raise an exception if the port is null
+   */
+  close: method(function () {
+    if (!this.port) {
+      console.error(ERR_MESSAGEPORT_FINALIZED);
+      return;
+    }
+
+    this.port.onmessage = null;
+    this.port.close();
+  }, {
+    oneway: true,
+    request: {}
+  }),
+
+  finalize: method(function () {
+    this.close();
+    this.port = null;
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "message": {
+      type: "message",
+      msg: Arg(0, "nullable:messageportevent")
+    }
+  }
+});
+
+/**
+ * The corresponding Front object for the MessagePortActor.
+ */
+let MessagePortFront = exports.MessagePortFront = protocol.FrontClass(MessagePortActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+
+/**
+ * Type describing a director-script error
+ */
+types.addDictType("director-script-error", {
+  directorScriptId: "string",
+  message: "string",
+  stack: "string",
+  fileName: "string",
+  lineNumber: "number",
+  columnNumber: "number"
+});
+
+/**
+ * Type describing a director-script attach event
+ */
+types.addDictType("director-script-attach", {
+  directorScriptId: "string",
+  url: "string",
+  innerId: "number",
+  port: "nullable:messageport"
+});
+
+/**
+ * Type describing a director-script detach event
+ */
+types.addDictType("director-script-detach", {
+  directorScriptId: "string",
+  innerId: "number"
+});
+
+/**
+ * The Director Script Actor manage javascript code running in a non-privileged sandbox with the same
+ * privileges of the target global (browser tab or a firefox os app).
+ *
+ * After retrieving an instance of this actor (from the tab director actor), you'll need to set it up
+ * by calling setup().
+ *
+ * After the setup, this actor will automatically attach/detach the content script (and optionally a
+ * directly connect the debugger client and the content script using a MessageChannel) on tab
+ * navigation.
+ */
+let DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClass({
+  typeName: "director-script",
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "error": {
+      type: "error",
+      data: Arg(0, "director-script-error")
+    },
+    "attach": {
+      type: "attach",
+      data: Arg(0, "director-script-attach")
+    },
+    "detach": {
+      type: "detach",
+      data: Arg(0, "director-script-detach")
+    }
+  },
+
+  /**
+   * Creates the director script actor
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param Actor tabActor
+   *        The tab (or root) actor.
+   * @param String scriptId
+   *        The director-script id.
+   * @param String scriptCode
+   *        The director-script javascript source.
+   * @param Object scriptOptions
+   *        The director-script options object.
+   */
+  initialize: function(conn, tabActor, { scriptId, scriptCode, scriptOptions }) {
+    protocol.Actor.prototype.initialize.call(this, conn, tabActor);
+
+    this.tabActor = tabActor;
+
+    this._scriptId = scriptId;
+    this._scriptCode = scriptCode;
+    this._scriptOptions = scriptOptions;
+    this._setupCalled = false;
+
+    this._onGlobalCreated   = this._onGlobalCreated.bind(this);
+    this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+
+    this.finalize();
+  },
+
+  /**
+   * Starts listening to the tab global created, in order to create the director-script sandbox
+   * using the configured scriptCode, attached/detached automatically to the tab
+   * window on tab navigation.
+   *
+   * @param Boolean reload
+   *        attach the page immediately or reload it first.
+   * @param Boolean skipAttach
+   *        skip the attach
+   */
+  setup: method(function ({ reload, skipAttach }) {
+    if (this._setupCalled) {
+      // do nothing
+      return;
+    }
+
+    this._setupCalled = true;
+
+    on(this.tabActor, "window-ready", this._onGlobalCreated);
+    on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+
+    // optional skip attach (needed by director-manager for director scripts bulk activation)
+    if (skipAttach) {
+      return;
+    }
+
+    if (reload) {
+      this.window.location.reload();
+    } else {
+      // fake a global created event to attach without reload
+      this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true });
+    }
+  }, {
+    request: {
+      reload: Option(0, "boolean"),
+      skipAttach: Option(0, "boolean")
+    },
+    oneway: true
+  }),
+
+  /**
+   * Get the attached MessagePort actor if any
+   */
+  getMessagePort: method(function () {
+    return this._messagePortActor;
+  }, {
+    request: { },
+    response: {
+      port: RetVal("nullable:messageport")
+    }
+  }),
+
+  /**
+   * Stop listening for document global changes, destroy the content worker and puts
+   * this actor to hibernation.
+   */
+  finalize: method(function () {
+    if (!this._setupCalled) {
+      return;
+    }
+
+    off(this.tabActor, "window-ready", this._onGlobalCreated);
+    off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+
+    this._onGlobalDestroyed({ id: this._lastAttachedWinId });
+
+    this._setupCalled = false;
+  }, {
+    oneway: true
+  }),
+
+  // local helpers
+  get window() {
+    return this.tabActor.window;
+  },
+
+  /* event handlers */
+  _onGlobalCreated: function({ id, window, isTopLevel }) {
+     if (!isTopLevel) {
+       // filter iframes
+       return;
+     }
+
+     if (this._lastAttachedWinId) {
+       // if we have received a global created without a previous global destroyed,
+       // it's time to cleanup the previous state
+       this._onGlobalDestroyed(this._lastAttachedWinId);
+     }
+
+     // TODO: check if we want to share a single sandbox per global
+     //       for multiple debugger clients
+
+     // create & attach the new sandbox
+     this._scriptSandbox = new DirectorScriptSandbox({
+       scriptId: this._scriptId,
+       scriptCode: this._scriptCode,
+       scriptOptions: this._scriptOptions
+     });
+
+     try {
+       // attach the global window
+       this._lastAttachedWinId = id;
+       var port = this._scriptSandbox.attach(window, id);
+       this._onDirectorScriptAttach(window, port);
+     } catch(e) {
+       this._onDirectorScriptError(e);
+     }
+  },
+  _onGlobalDestroyed: function({ id }) {
+     if (id !== this._lastAttachedWinId) {
+       // filter destroyed globals
+       return;
+     }
+
+     // unmanage and cleanup the messageport actor
+     if (this._messagePortActor) {
+       this.unmanage(this._messagePortActor);
+       this._messagePortActor = null;
+     }
+
+     // NOTE: destroy here the old worker
+     if (this._scriptSandbox) {
+       this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this));
+
+       // send a detach event to the debugger client
+       emit(this, "detach", {
+         directorScriptId: this._scriptId,
+         innerId: this._lastAttachedWinId
+       });
+
+       this._lastAttachedWinId = null;
+       this._scriptSandbox = null;
+     }
+  },
+  _onDirectorScriptError: function(error) {
+    // route the content script error to the debugger client
+    emit(this, "error", {
+      directorScriptId: this._scriptId,
+      message: error.toString(),
+      stack: error.stack,
+      fileName: error.fileName,
+      lineNumber: error.lineNumber,
+      columnNumber: error.columnNumber
+    });
+  },
+  _onDirectorScriptAttach: function(window, port) {
+    let portActor = new MessagePortActor(this.conn, port);
+    this.manage(portActor);
+    this._messagePortActor = portActor;
+
+    emit(this, "attach", {
+      directorScriptId: this._scriptId,
+      url: (window && window.location) ? window.location.toString() : "",
+      innerId: this._lastAttachedWinId,
+      port: this._messagePortActor
+    });
+  }
+});
+
+/**
+ * The corresponding Front object for the DirectorScriptActor.
+ */
+let DirectorScriptFront = exports.DirectorScriptFront = protocol.FrontClass(DirectorScriptActor, {
+  initialize: function (client, form) {
+    protocol.Front.prototype.initialize.call(this, client, form);
+  }
+});
+
+/**
+ * The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts.
+ */
+const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClass({
+  typeName: "director-manager",
+
+  /**
+   * Events emitted by this actor.
+   */
+  events: {
+    "director-script-error": {
+      type: "error",
+      data: Arg(0, "director-script-error")
+    },
+    "director-script-attach": {
+      type: "attach",
+      data: Arg(0, "director-script-attach")
+    },
+    "director-script-detach": {
+      type: "detach",
+      data: Arg(0, "director-script-detach")
+    }
+  },
+
+  /* init & destroy methods */
+  initialize: function(conn, tabActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+    this.tabActor = tabActor;
+    this._directorScriptActorsMap = new Map();
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  /**
+   * Retrieves the list of installed director-scripts.
+   */
+  list: method(function () {
+    var enabled_script_ids = [for (id of this._directorScriptActorsMap.keys()) id];
+
+    return {
+      installed: DirectorRegistry.list(),
+      enabled: enabled_script_ids
+    };
+  }, {
+    response: {
+      directorScripts: RetVal("json")
+    }
+  }),
+
+  /**
+   * Bulk enabling director-scripts.
+   *
+   * @param Array[String] selectedIds
+   *        The list of director-script ids to be enabled,
+   *        ["*"] will activate all the installed director-scripts
+   * @param Boolean reload
+   *        optionally reload the target window
+   */
+  enableByScriptIds: method(function(selectedIds, { reload }) {
+    if (selectedIds && selectedIds.length === 0) {
+      // filtered all director scripts ids
+      return;
+    }
+
+    for (let scriptId of DirectorRegistry.list()) {
+      // filter director script ids
+      if (selectedIds.indexOf("*") < 0 &&
+          selectedIds.indexOf(scriptId) < 0) {
+        continue;
+      }
+
+      let actor = this.getByScriptId(scriptId);
+
+      // skip attach if reload is true (activated director scripts
+      // will be automatically attached on the final reload)
+      actor.setup({ reload: false, skipAttach: reload });
+    }
+
+    if (reload) {
+      this.tabActor.window.location.reload();
+    }
+  }, {
+    oneway: true,
+    request: {
+      selectedIds: Arg(0, "array:string"),
+      reload: Option(1, "boolean")
+    }
+  }),
+
+  /**
+   * Bulk disabling director-scripts.
+   *
+   * @param Array[String] selectedIds
+   *        The list of director-script ids to be disable,
+   *        ["*"] will de-activate all the enable director-scripts
+   * @param Boolean reload
+   *        optionally reload the target window
+   */
+  disableByScriptIds: method(function(selectedIds, { reload }) {
+    if (selectedIds && selectedIds.length === 0) {
+      // filtered all director scripts ids
+      return;
+    }
+
+    for (let scriptId of this._directorScriptActorsMap.keys()) {
+      // filter director script ids
+      if (selectedIds.indexOf("*") < 0 &&
+          selectedIds.indexOf(scriptId) < 0) {
+        continue;
+      }
+
+      let actor = this._directorScriptActorsMap.get(scriptId);
+      this._directorScriptActorsMap.delete(scriptId);
+
+      // finalize the actor (which will produce director-script-detach event)
+      actor.finalize();
+      // unsubscribe event handlers on the disabled actor
+      off(actor);
+
+      this.unmanage(actor);
+    }
+
+    if (reload) {
+      this.tabActor.window.location.reload();
+    }
+  }, {
+    oneway: true,
+    request: {
+      selectedIds: Arg(0, "array:string"),
+      reload: Option(1, "boolean")
+    }
+  }),
+
+  /**
+   * Retrieves the actor instance of an installed director-script
+   * (and create the actor instance if it doesn't exists yet).
+   */
+  getByScriptId: method(function(scriptId) {
+    var id = scriptId;
+    // raise an unknown director-script id exception
+    if (!DirectorRegistry.checkInstalled(id)) {
+      console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id);
+      throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID);
+    }
+
+    // get a previous created actor instance
+    let actor = this._directorScriptActorsMap.get(id);
+
+    // create a new actor instance
+    if (!actor) {
+      let directorScriptDefinition = DirectorRegistry.get(id);
+
+      // test lazy director-script (e.g. uninstalled in the parent process)
+      if (!directorScriptDefinition) {
+
+        console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id);
+        throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID);
+      }
+
+      actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition);
+      this._directorScriptActorsMap.set(id, actor);
+
+      on(actor, "error", emit.bind(null, this, "director-script-error"));
+      on(actor, "attach", emit.bind(null, this, "director-script-attach"));
+      on(actor, "detach", emit.bind(null, this, "director-script-detach"));
+
+      this.manage(actor);
+    }
+
+    return actor;
+  }, {
+    request: {
+      scriptId: Arg(0, "string")
+    },
+    response: {
+      directorScript: RetVal("director-script")
+    }
+  }),
+
+  finalize: method(function() {
+    this.disableByScriptIds(["*"], false);
+  }, {
+    oneway: true
+  })
+});
+
+/**
+ * The corresponding Front object for the DirectorManagerActor.
+ */
+exports.DirectorManagerFront = protocol.FrontClass(DirectorManagerActor, {
+  initialize: function(client, { directorManagerActor }) {
+    protocol.Front.prototype.initialize.call(this, client, {
+      actor: directorManagerActor
+    });
+    this.manage(this);
+  }
+});
+
+/* private helpers */
+
+/**
+ * DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox
+ * to a target window.
+ */
+const DirectorScriptSandbox = Class({
+  initialize: function({scriptId, scriptCode, scriptOptions}) {
+    this._scriptId = scriptId;
+    this._scriptCode = scriptCode;
+    this._scriptOptions = scriptOptions;
+  },
+
+  attach: function(window, innerId) {
+    this._innerId = innerId,
+    this._window = window;
+    this._proto = Cu.createObjectIn(this._window);
+
+    var id = this._scriptId;
+    var uri = this._scriptCode;
+
+    this._sandbox = sandbox(window, {
+      sandboxName: uri,
+      sandboxPrototype: this._proto,
+      sameZoneAs: window,
+      wantXrays: true,
+      wantComponents: false,
+      wantExportHelpers: false,
+      metadata: {
+        URI: uri,
+        addonID: id,
+        SDKDirectorScript: true,
+        "inner-window-id": innerId
+      }
+    });
+
+    // create a CommonJS module object which match the interface from addon-sdk
+    // (addon-sdk/sources/lib/toolkit/loader.js#L678-L686)
+    var module = Cu.cloneInto(Object.create(null, {
+      id: { enumerable: true, value: id },
+      uri: { enumerable: true, value: uri },
+      exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) }
+    }), this._sandbox);
+
+    // create a console API object
+    let directorScriptConsole = new PlainTextConsole(null, this._innerId);
+
+    // inject CommonJS module globals into the sandbox prototype
+    Object.defineProperties(this._proto, {
+      module: { enumerable: true, value: module },
+      exports: { enumerable: true, value: module.exports },
+      console: {
+        enumerable: true,
+        value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    Object.defineProperties(this._sandbox, {
+      require: {
+        enumerable: true,
+        value: Cu.cloneInto(function() {
+          throw Error("NOT IMPLEMENTED");
+        }, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    // evaluate the director script source in the sandbox
+    evaluate(this._sandbox, this._scriptCode, this._scriptId);
+
+    // prepare the messageport connected to the debugger client
+    let { port1, port2 } = new this._window.MessageChannel();
+
+    // prepare the unload callbacks queue
+    var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = [];
+
+    // create the attach options
+    var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox);
+    Object.defineProperties(attachOptions, {
+      port: { enumerable: true, value: port1 },
+      window: { enumerable: true, value: window },
+      scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) },
+      onUnload: {
+        enumerable: true,
+        value: Cu.cloneInto(function (cb) {
+          // collect unload callbacks
+          if (typeof cb == "function") {
+            sandboxOnUnloadQueue.push(cb);
+          }
+        }, this._sandbox, { cloneFunctions: true })
+      }
+    });
+
+    // select the attach method
+    var exports = this._proto.module.exports;
+    if ("attachMethod" in this._scriptOptions) {
+      this._sandboxOnAttach = exports[this._scriptOptions.attachMethod];
+    } else {
+      this._sandboxOnAttach = exports;
+    }
+
+    if (typeof this._sandboxOnAttach !== "function") {
+      throw Error("the configured attachMethod '" +
+                  (this._scriptOptions.attachMethod || "module.exports") +
+                  "' is not exported by the directorScript");
+    }
+
+    // call the attach method
+    this._sandboxOnAttach.call(this._sandbox, attachOptions);
+
+    return port2;
+  },
+  destroy:  function(onError) {
+    // evaluate queue unload methods if any
+    while(this._sandboxOnUnloadQueue.length > 0) {
+      let cb = this._sandboxOnUnloadQueue.pop();
+
+      try {
+        cb();
+      } catch(e) {
+        console.error("Exception on DirectorScript Sandbox destroy", e);
+        onError(e);
+      }
+    }
+
+    Cu.nukeSandbox(this._sandbox);
+  }
+});
+
+function getWindowID(window) {
+  return window.QueryInterface(Ci.nsIInterfaceRequestor)
+               .getInterface(Ci.nsIDOMWindowUtils)
+               .currentInnerWindowID;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/actors/director-registry.js
@@ -0,0 +1,295 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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";
+
+const protocol = require("devtools/server/protocol");
+const { method, Arg, Option, RetVal } = protocol;
+
+const {DebuggerServer} = require("devtools/server/main");
+
+/**
+ * Error Messages
+ */
+
+const ERR_DIRECTOR_INSTALL_TWICE = "Trying to install a director-script twice";
+const ERR_DIRECTOR_INSTALL_EMPTY = "Trying to install an empty director-script";
+const ERR_DIRECTOR_UNINSTALL_UNKNOWN = "Trying to uninstall an unkown director-script";
+
+const ERR_DIRECTOR_PARENT_UNKNOWN_METHOD = "Unknown parent process method";
+const ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD = "Unexpected call to notImplemented method";
+const ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES = "Unexpected multiple replies to called parent method";
+const ERR_DIRECTOR_CHILD_NO_REPLY = "Unexpected no reply to called parent method";
+
+/**
+ * Director Registry
+ */
+
+// Map of director scripts ids to director script definitions
+var gDirectorScripts = Object.create(null);
+
+const DirectorRegistry = exports.DirectorRegistry = {
+  /**
+   * Register a Director Script with the debugger server.
+   * @param id string
+   *    The ID of a director script.
+   * @param directorScriptDef object
+   *    The definition of a director script.
+   */
+  install: function (id, scriptDef) {
+    if (id in gDirectorScripts) {
+      console.error(ERR_DIRECTOR_INSTALL_TWICE,id);
+      return false;
+    }
+
+    if (!scriptDef) {
+      console.error(ERR_DIRECTOR_INSTALL_EMPTY, id);
+      return false;
+    }
+
+    gDirectorScripts[id] = scriptDef;
+
+    return true;
+  },
+
+  /**
+   * Unregister a Director Script with the debugger server.
+   * @param id string
+   *    The ID of a director script.
+   */
+  uninstall: function(id) {
+    if (id in gDirectorScripts) {
+      delete gDirectorScripts[id];
+
+      return true;
+    }
+
+    console.error(ERR_DIRECTOR_UNINSTALL_UNKNOWN, id);
+
+    return false;
+  },
+
+  /**
+   * Returns true if a director script id has been registered.
+   * @param id string
+   *    The ID of a director script.
+   */
+  checkInstalled: function (id) {
+    return (this.list().indexOf(id) >= 0);
+  },
+
+  /**
+   * Returns a registered director script definition by id.
+   * @param id string
+   *    The ID of a director script.
+   */
+  get: function(id) {
+    return gDirectorScripts[id];
+  },
+
+  /**
+   * Returns an array of registered director script ids.
+   */
+  list: function() {
+    return Object.keys(gDirectorScripts);
+  },
+
+  /**
+   * Removes all the registered director scripts.
+   */
+  clear: function() {
+   gDirectorScripts = Object.create(null);
+  }
+};
+
+/**
+ * E10S parent/child setup helpers
+ */
+
+let gTrackedMessageManager = new Set();
+
+exports.setupParentProcess = function setupParentProcess({mm, childID}) {
+  // prevents multiple subscriptions on the same messagemanager
+  if (gTrackedMessageManager.has(mm)) {
+    return;
+  }
+  gTrackedMessageManager.add(mm);
+
+  // listen for director-script requests from the child process
+  mm.addMessageListener("debug:director-registry-request", handleChildRequest);
+
+  DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);
+
+  /* parent process helpers */
+
+  function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
+    // filter out not subscribed message managers
+    if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) {
+      return;
+    }
+
+    gTrackedMessageManager.delete(mm);
+
+    // unregister for director-script requests handlers from the parent process (if any)
+    mm.removeMessageListener("debug:director-registry-request", handleChildRequest);
+  }
+
+  function handleChildRequest(msg) {
+    switch (msg.json.method) {
+    case "get":
+      return DirectorRegistry.get(msg.json.args[0]);
+    case "list":
+      return DirectorRegistry.list();
+    default:
+      console.error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD, msg.json.method);
+      throw new Error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD);
+    }
+  }
+};
+
+// skip child setup if this actor module is not running in a child process
+if (DebuggerServer.isInChildProcess) {
+  setupChildProcess();
+}
+
+function setupChildProcess() {
+  const { sendSyncMessage } = DebuggerServer.parentMessageManager;
+
+  DebuggerServer.setupInParent({
+    module: "devtools/server/actors/director-registry",
+    setupParent: "setupParentProcess"
+  });
+
+  DirectorRegistry.install = notImplemented.bind(null, "install");
+  DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall");
+  DirectorRegistry.clear = notImplemented.bind(null, "clear");
+
+  DirectorRegistry.get = callParentProcess.bind(null, "get");
+  DirectorRegistry.list = callParentProcess.bind(null, "list");
+
+  /* child process helpers */
+
+  function notImplemented(method) {
+    console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method);
+    throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD);
+  }
+
+  function callParentProcess(method, ...args) {
+    var reply = sendSyncMessage("debug:director-registry-request", {
+      method: method,
+      args: args
+    });
+
+    if (reply.length === 0) {
+      console.error(ERR_DIRECTOR_CHILD_NO_REPLY);
+      throw Error(ERR_DIRECTOR_CHILD_NO_REPLY);
+    } else if (reply.length > 1) {
+      console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
+      throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
+    }
+
+    return reply[0];
+  };
+};
+
+/**
+ * The DirectorRegistry Actor is a global actor which manages install/uninstall of
+ * director scripts definitions.
+ */
+const DirectorRegistryActor = exports.DirectorRegistryActor = protocol.ActorClass({
+  typeName: "director-registry",
+
+  /* init & destroy methods */
+  initialize: function(conn, parentActor) {
+    protocol.Actor.prototype.initialize.call(this, conn);
+  },
+  destroy: function(conn) {
+    protocol.Actor.prototype.destroy.call(this, conn);
+    this.finalize();
+  },
+
+  finalize: method(function() {
+    // nothing to cleanup
+  }, {
+    oneway: true
+  }),
+
+  /**
+   * Install a new director-script definition.
+   *
+   * @param String id
+   *        The director-script definition identifier.
+   * @param String scriptCode
+   *        The director-script javascript source.
+   * @param Object scriptOptions
+   *        The director-script option object.
+   */
+  install: method(function(id, { scriptCode, scriptOptions }) {
+    // TODO: add more checks on id format?
+    if (!id || id.length === 0) {
+      throw Error("director-script id is mandatory");
+    }
+
+    if (!scriptCode) {
+      throw Error("director-script scriptCode is mandatory");
+    }
+
+    return DirectorRegistry.install(id, {
+      scriptId: id,
+      scriptCode: scriptCode,
+      scriptOptions: scriptOptions
+    });
+  }, {
+    request: {
+      scriptId: Arg(0, "string"),
+      scriptCode: Option(1, "string"),
+      scriptOptions: Option(1, "nullable:json")
+    },
+    response: {
+      success: RetVal("boolean")
+    }
+  }),
+
+  /**
+   * Uninstall a director-script definition.
+   *
+   * @param String id
+   *        The identifier of the director-script definition to be removed
+   */
+  uninstall: method(function (id) {
+    return DirectorRegistry.uninstall(id);
+  }, {
+    request: {
+      scritpId: Arg(0, "string")
+    },
+    response: {
+      success: RetVal("boolean")
+    }
+  }),
+
+  /**
+   * Retrieves the list of installed director-scripts.
+   */
+  list: method(function () {
+    return DirectorRegistry.list();
+  }, {
+    response: {
+      directorScripts: RetVal("array:string")
+    }
+  })
+});
+
+/**
+ * The corresponding Front object for the DirectorRegistryActor.
+ */
+exports.DirectorRegistryFront = protocol.FrontClass(DirectorRegistryActor, {
+  initialize: function(client, { directorRegistryActor }) {
+    protocol.Front.prototype.initialize.call(this, client, {
+      actor: directorRegistryActor
+    });
+    this.manage(this);
+  }
+});
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -153,16 +153,18 @@ RootActor.prototype = {
     // Whether the style rule actor implements the modifySelector method
     // that modifies the rule's selector
     selectorEditable: true,
     // Whether the page style actor implements the addNewRule method that
     // adds new rules to the page
     addNewRule: true,
     // Whether the dom node actor implements the getUniqueSelector method
     getUniqueSelector: true,
+    // Whether the director scripts are supported
+    directorScripts: true,
     // Whether the debugger server supports
     // blackboxing/pretty-printing (not supported in Fever Dream yet)
     noBlackBoxing: false,
     noPrettyPrinting: false,
     // Whether the page style actor implements the getUsedFontFaces method
     // that returns the font faces used on a node
     getUsedFontFaces: true
   },
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -234,34 +234,67 @@ var PageStyleActor = protocol.ActorClass
       filter: Option(1, "string"),
     },
     response: {
       computed: RetVal("json")
     }
   }),
 
   /**
+   * Get all the fonts from a page.
+   *
+   * @param object options
+   *   `includePreviews`: Whether to also return image previews of the fonts.
+   *   `previewText`: The text to display in the previews.
+   *   `previewFontSize`: The font size of the text in the previews.
+   *
+   * @returns object
+   *   object with 'fontFaces', a list of fonts that apply to this node.
+   */
+  getAllUsedFontFaces: method(function(options) {
+    let windows = this.inspector.tabActor.windows;
+    let fontsList = [];
+    for(let win of windows){
+      fontsList = [...fontsList,
+                   ...this.getUsedFontFaces(win.document.body, options)];
+    }
+    return fontsList;
+  },
+  {
+    request: {
+      includePreviews: Option(0, "boolean"),
+      previewText: Option(0, "string"),
+      previewFontSize: Option(0, "string"),
+      previewFillStyle: Option(0, "string")
+    },
+    response: {
+      fontFaces: RetVal("array:fontface")
+    }
+  }),
+
+  /**
    * Get the font faces used in an element.
    *
-   * @param NodeActor node
+   * @param NodeActor node / actual DOM node
    *    The node to get fonts from.
    * @param object options
    *   `includePreviews`: Whether to also return image previews of the fonts.
    *   `previewText`: The text to display in the previews.
    *   `previewFontSize`: The font size of the text in the previews.
    *
    * @returns object
    *   object with 'fontFaces', a list of fonts that apply to this node.
    */
   getUsedFontFaces: method(function(node, options) {
-    let contentDocument = node.rawNode.ownerDocument;
-
+    // node.rawNode is defined for NodeActor objects
+    let actualNode = node.rawNode || node;
+    let contentDocument = actualNode.ownerDocument;
     // We don't get fonts for a node, but for a range
     let rng = contentDocument.createRange();
-    rng.selectNodeContents(node.rawNode);
+    rng.selectNodeContents(actualNode);
     let fonts = DOMUtils.getUsedFontFaces(rng);
     let fontsArray = [];
 
     for (let i = 0; i < fonts.length; i++) {
       let font = fonts.item(i);
       let fontFace = {
         name: font.name,
         CSSFamilyName: font.CSSFamilyName,
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -384,16 +384,21 @@ var DebuggerServer = {
       constructor: "WebappsActor",
       type: { global: true }
     });
     this.registerModule("devtools/server/actors/device", {
       prefix: "device",
       constructor: "DeviceActor",
       type: { global: true }
     });
+    this.registerModule("devtools/server/actors/director-registry", {
+      prefix: "directorRegistry",
+      constructor: "DirectorRegistryActor",
+      type: { global: true }
+    });
   },
 
   /**
    * Install tab actors in documents loaded in content childs
    */
   addChildActors: function () {
     // In case of apps being loaded in parent process, DebuggerServer is already
     // initialized and browser actors are already loaded,
@@ -499,16 +504,21 @@ var DebuggerServer = {
       constructor: "MonitorActor",
       type: { global: true, tab: true }
     });
     this.registerModule("devtools/server/actors/timeline", {
       prefix: "timeline",
       constructor: "TimelineActor",
       type: { global: true, tab: true }
     });
+    this.registerModule("devtools/server/actors/director-manager", {
+      prefix: "directorManager",
+      constructor: "DirectorManagerActor",
+      type: { global: false, tab: true }
+    });
     if ("nsIProfiler" in Ci) {
       this.registerModule("devtools/server/actors/profiler", {
         prefix: "profiler",
         constructor: "ProfilerActor",
         type: { global: true, tab: true }
       });
     }
     this.registerModule("devtools/server/actors/animation", {
--- a/toolkit/devtools/server/moz.build
+++ b/toolkit/devtools/server/moz.build
@@ -37,16 +37,18 @@ EXTRA_JS_MODULES.devtools.server.actors 
     'actors/animation.js',
     'actors/call-watcher.js',
     'actors/canvas.js',
     'actors/child-process.js',
     'actors/childtab.js',
     'actors/common.js',
     'actors/csscoverage.js',
     'actors/device.js',
+    'actors/director-manager.js',
+    'actors/director-registry.js',
     'actors/eventlooplag.js',
     'actors/framerate.js',
     'actors/gcli.js',
     'actors/highlighter.js',
     'actors/inspector.js',
     'actors/layout.js',
     'actors/memory.js',
     'actors/monitor.js',
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -1,10 +1,12 @@
 [DEFAULT]
 support-files =
+  director-helpers.js
+  director-script-target.html
   inspector-helpers.js
   inspector-styles-data.css
   inspector-styles-data.html
   inspector-traversal-data.html
   nonchrome_unsafeDereference.html
   inspector_getImageData.html
   large-image.jpg
   memory-helpers.js
@@ -68,10 +70,13 @@ skip-if = buildapp == 'mulet'
 [test_memory_allocations_05.html]
 [test_memory_attach_01.html]
 [test_memory_attach_02.html]
 [test_memory_census.html]
 [test_memory_gc_01.html]
 [test_preference.html]
 [test_connectToChild.html]
 skip-if = buildapp == 'mulet'
+[test_director.html]
+[test_director_connectToChild.html]
++skip-if = buildapp == 'mulet'
 [test_attachProcess.html]
 skip-if = buildapp == 'mulet'
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/director-helpers.js
@@ -0,0 +1,100 @@
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
+Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+
+const Services = devtools.require("Services");
+
+// Always log packets when running tests.
+Services.prefs.setBoolPref("devtools.debugger.log", true);
+Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true);
+
+SimpleTest.registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.debugger.log");
+  Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled");
+});
+
+const {Class} = devtools.require("sdk/core/heritage");
+
+const {promiseInvoke} = devtools.require("devtools/async-utils");
+
+const { DirectorRegistry,
+        DirectorRegistryFront } = devtools.require("devtools/server/actors/director-registry");
+
+const { DirectorManagerFront } = devtools.require("devtools/server/actors/director-manager");
+const protocol = devtools.require("devtools/server/protocol");
+
+const {Task} = devtools.require("resource://gre/modules/Task.jsm");
+
+/***********************************
+ *  director helpers functions
+ **********************************/
+
+function waitForEvent(target, name) {
+  return new Promise((resolve, reject) => {
+      target.once(name, (...args) => { resolve(args); });
+  });
+}
+
+function* newConnectedDebuggerClient(opts) {
+  var transport = DebuggerServer.connectPipe();
+  var client = new DebuggerClient(transport);
+
+  yield promiseInvoke(client, client.connect);
+
+  var root = yield promiseInvoke(client, client.listTabs);
+
+  return {
+    client: client,
+    root: root,
+    transport: transport
+  };
+}
+
+function* installTestDirectorScript(client, root,  scriptId, scriptDefinition) {
+  var directorRegistryClient = new DirectorRegistryFront(client, root);
+
+  yield directorRegistryClient.install(scriptId, scriptDefinition);
+
+  directorRegistryClient.destroy();
+}
+
+function* getTestDirectorScript(manager, tab, scriptId) {
+  var directorScriptClient = yield manager.getByScriptId(scriptId);
+  return directorScriptClient;
+}
+
+function purgeInstalledDirectorScripts() {
+  DirectorRegistry.clear();
+}
+
+function* installDirectorScriptAndWaitAttachOrError({client, root, manager,
+                                                     scriptId, scriptDefinition}) {
+  yield installTestDirectorScript(client, root, scriptId, scriptDefinition);
+
+  var selectedTab = root.tabs[root.selected];
+  var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, scriptId);
+
+  var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+  var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+  testDirectorScriptClient.setup({reload: false});
+
+  var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
+                                            waitForDirectorScriptError]);
+
+  testDirectorScriptClient.finalize();
+
+  return receivedEvent;
+}
+
+function assertIsDirectorScriptError(error) {
+  ok(!!error, "received error should be defined");
+  ok(!!error.message, "errors should contain a message");
+  ok(!!error.stack, "errors should contain a stack trace");
+  ok(!!error.fileName, "errors should contain a fileName");
+  ok(typeof error.columnNumber == "number", "errors should contain a columnNumber");
+  ok(typeof error.lineNumber == "number", "errors should contain a lineNumber");
+
+  ok(!!error.directorScriptId, "errors should contain a directorScriptId");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/director-script-target.html
@@ -0,0 +1,15 @@
+<html>
+  <head>
+    <script>
+      // change the eval function to ensure the window object in the debug-script is correctly wrapped
+      window.eval = function () {
+        return "unsecure-eval-called";
+      };
+
+      var globalAccessibleVar = "global-value";
+    </script>
+  </head>
+  <body>
+    <h1>debug script target</h1>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_director.html
@@ -0,0 +1,479 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+  <script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  Task.spawn(function* () {
+    SimpleTest.waitForExplicitFinish();
+
+    var tests = [
+      runDirectorScriptModuleExports,
+      runDirectorScriptErrorOnNoAttachExports,
+      runDirectorScriptErrorOnLoadTest,
+      runDirectorScriptErrorOnRequire,
+      runDirectorScriptErrorOnUnloadTest,
+      runDirectorScriptSetupAndReceiveMessagePortTest,
+      runDirectorEnableDirectorScriptsTest,
+      runDirectorScriptDetachEventTest,
+      runDirectorScriptWindowEval
+    ].map((testCase) => {
+      return function* () {
+        setup();
+        yield testCase().then(null, (e) => {
+          console.error("Exception during testCase run", e);
+          ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+        });
+
+        teardown();
+      };
+    });
+
+    for (var test of tests) {
+      yield test();
+    }
+  }).then(
+    function success() {
+      SimpleTest.finish()
+    },
+    function error(e) {
+      console.error("Exception during testCase run", e);
+      ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+
+      SimpleTest.finish();
+    }
+  );
+};
+
+var targetWin = null;
+
+function setup() {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+
+    SimpleTest.registerCleanupFunction(teardown);
+  }
+}
+
+function teardown() {
+  purgeInstalledDirectorScripts();
+
+  DebuggerServer.destroy();
+  if (targetWin) {
+    targetWin.close();
+  }
+}
+
+/***********************************
+ *  test cases
+ **********************************/
+
+function runDirectorScriptModuleExports() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptModuleExports = {
+    scriptCode: "(" + (function() {
+       module.exports = function() {};
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  var testDirectorScriptAttachMethodOption = {
+    scriptCode: "(" + (function() {
+       exports.attach = function() {};
+    }).toString() + ")();",
+    scriptOptions: {
+       attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var receivedEvent1 = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptModuleExports",
+      scriptDefinition: testDirectorScriptModuleExports
+    });
+    ok(!!receivedEvent1.port, "received attach from testDirectorScriptModuleExports");
+
+    var receivedEvent2 = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptAttachMethodOption",
+      scriptDefinition: testDirectorScriptModuleExports
+    });
+    ok(!!receivedEvent2.port, "received attach event from testDirectorScriptAttachMethodOption");
+
+    client.close();
+   })
+}
+
+function runDirectorScriptErrorOnNoAttachExports() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnNoAttachExports = {
+    scriptCode: "(" + (function() {
+      // this director script should raise an error
+      // because it doesn't export any attach method
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var error = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptRaiseErrorOnNoAttachExports",
+      scriptDefinition: testDirectorScriptRaiseErrorOnNoAttachExports
+    });
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnRequire() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnRequire = {
+    scriptCode: "(" + (function() {
+      // this director script should raise an error
+      // because require raise a "not implemented" exception
+      console.log("PROVA", this)
+      require("fake_module");
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+
+    var error = yield installDirectorScriptAndWaitAttachOrError({
+      client: client, root: root, manager: manager,
+      scriptId: "testDirectorscriptRaiseErrorOnRequire",
+      scriptDefinition: testDirectorScriptRaiseErrorOnRequire
+    });
+
+    assertIsDirectorScriptError(error);
+    is(error.message, "Error: NOT IMPLEMENTED", "error message should contains the expected error message");
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnLoadTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnLoad = {
+    scriptCode: "(" + (function() {
+       // this will raise an exception on evaluating
+       // the director script
+       raise.an_error.during.content_script.load();
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptRaiseErrorOnLoad);
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+
+    var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+    // activate the director script without window reloading
+    testDirectorScriptClient.setup({reload: false});
+
+    var [error] = yield waitForDirectorScriptError;
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+function runDirectorScriptErrorOnUnloadTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptRaiseErrorOnUnload = {
+    scriptCode: "(" + (function() {
+       module.exports = function({onUnload}) {
+         // this will raise an exception on unload the director script
+         onUnload(function() {
+           raise_an_error_onunload();
+         });
+       };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptRaiseErrorOnUnload);
+
+    var selectedTab = root.tabs[root.selected];
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+    var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+
+    // activate the director script without window reloading
+    testDirectorScriptClient.setup({reload: false});
+
+    yield waitForDirectorScriptAttach;
+
+    var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
+
+    testDirectorScriptClient.finalize();
+
+    var [error] = yield waitForDirectorScriptError;
+
+    assertIsDirectorScriptError(error);
+
+    client.close();
+  });
+}
+
+
+function runDirectorScriptSetupAndReceiveMessagePortTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+        module.exports = function({port}) {
+          port.onmessage = function(evt) {
+            // echo messages
+            evt.source.postMessage(evt.data);
+          };
+        };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                  testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // get a testDirectorScriptClient
+    var manager = new DirectorManagerFront(client, selectedTab);
+    var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
+
+    var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
+
+    // activate the director script without window reloading
+    // (and wait for attach)
+    testDirectorScriptClient.setup({reload: false});
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    // call the connectPort method to get a MessagePortClient
+    var port = attachEvent.port;
+
+    ok(!!port && !!port.postMessage, "messageport actor client received");
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortMessage = waitForEvent(port, "message");
+    // needs to explicit start the port
+    port.start();
+
+    var msg = { k1: "v1", k2: [1, 2, 3] };
+    port.postMessage(msg);
+
+    var reply = yield waitForMessagePortMessage;
+
+    ok(JSON.stringify(reply[0].data) === JSON.stringify(msg),
+       "echo reply received on the MessagePortClient");
+
+    yield client.close();
+  });
+}
+
+function runDirectorEnableDirectorScriptsTest() {
+  targetWin = window.open("about:blank");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+      module.exports = function({port}) {
+        port.onmessage = function(evt) {
+          // echo messages
+          evt.source.postMessage(evt.data);
+        };
+      };
+    }).toString() + ")();",
+    scriptOptions: {}
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+
+    tabDirectorClient.enableByScriptIds(["*"], { reload: false });
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    is(attachEvent.directorScriptId, "testDirectorScript", "attach event should contains directorScriptId");
+
+    yield client.close();
+  });
+}
+
+function runDirectorScriptDetachEventTest() {
+  targetWin = window.open("director-script-target.html");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+        exports.attach = function({port, onUnload}) {
+            onUnload(function() {
+              port.postMessage("ONUNLOAD");
+            });
+        };
+    }).toString() + ")();",
+    scriptOptions: {
+      attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // NOTE: tab needs to be attached to receive director-script-detach events
+    yield promiseInvoke(client, client.attachTab, selectedTab.actor);
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+    var waitForDirectorScriptDetach = waitForEvent(tabDirectorClient, "director-script-detach");
+
+    tabDirectorClient.enableByScriptIds(["*"], {reload: true});
+
+    var [attachEvent] = yield waitForDirectorScriptAttach;
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortEvent = waitForEvent(attachEvent.port, "message");
+    // needs to explicit start the port
+    attachEvent.port.start();
+
+    tabDirectorClient.disableByScriptIds(["*"], {reload: false});
+
+    // changing the window location should generate a director-script-detach event
+    var [detachEvent] = yield waitForDirectorScriptDetach;
+
+    is(detachEvent.directorScriptId, "testDirectorScript", "detach event should contains directorScriptId");
+
+    var [portEvent] = yield waitForMessagePortEvent;
+
+    is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach");
+
+    yield client.close();
+  });
+}
+
+function runDirectorScriptWindowEval() {
+  targetWin = window.open("http://mochi.test:8888/chrome/toolkit/devtools/server/tests/mochitest/director-script-target.html");
+
+  var testDirectorScriptOptions = {
+    scriptCode: "(" + (function() {
+      exports.attach = function({window, port}) {
+        var onpageloaded = function() {
+          var globalVarValue = window.eval("window.globalAccessibleVar;");
+          port.postMessage(globalVarValue);
+        };
+
+        if (window.document.readyState === "complete") {
+          onpageloaded();
+        } else {
+          window.onload = onpageloaded;
+        }
+      };
+    }).toString() + ")();",
+    scriptOptions: {
+      attachMethod: "attach"
+    }
+  }
+
+  return Task.spawn(function* () {
+    var { client, root } = yield newConnectedDebuggerClient();
+
+    yield installTestDirectorScript(client, root, "testDirectorScript",
+                                 testDirectorScriptOptions);
+
+    var selectedTab = root.tabs[root.selected];
+
+    // NOTE: tab needs to be attached to receive director-script-detach events
+    yield promiseInvoke(client, client.attachTab, selectedTab.actor);
+
+    var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
+
+    var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
+    var waitForDirectorScriptError = waitForEvent(tabDirectorClient, "director-script-error");
+
+    tabDirectorClient.enableByScriptIds(["*"], {reload: false});
+
+    var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
+                                              waitForDirectorScriptError]);
+
+    ok(!!receivedEvent.port, "received director-script-attach");
+
+    // exchange messages over the MessagePort
+    var waitForMessagePortEvent = waitForEvent(receivedEvent.port, "message");
+    // needs to explicit start the port
+    receivedEvent.port.start();
+
+    var [portEvent] = yield waitForMessagePortEvent;
+
+    ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe");
+
+    is(portEvent.data, "global-value", "window.globalAccessibleVar should be accessible through window.eval");
+
+    yield client.close();
+  });
+}
+
+  </script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_director_connectToChild.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+</head>
+<body>
+<pre id="test">
+  <script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+window.onload = function() {
+  Task.spawn(function* () {
+    SimpleTest.waitForExplicitFinish();
+
+    var tests = [
+      runPropagateDirectorScriptsToChildTest,
+    ].map((testCase) => {
+      return function* () {
+        setup();
+        yield testCase().then(null, (e) => {
+          ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
+        });
+
+        teardown();
+      };
+    });
+
+    for (var test of tests) {
+      yield test();
+    }
+
+    SimpleTest.finish();
+  });
+};
+
+function setup() {
+  if (!DebuggerServer.initialized) {
+    DebuggerServer.init(() => true);
+    DebuggerServer.addBrowserActors();
+    SimpleTest.registerCleanupFunction(function() {
+      DebuggerServer.destroy();
+    });
+  }
+}
+
+function teardown() {
+  purgeInstalledDirectorScripts();
+  DebuggerServer.destroy();
+}
+
+/***********************************
+ *  test cases
+ **********************************/
+
+function runPropagateDirectorScriptsToChildTest() {
+  let iframe = document.createElement("iframe");
+  iframe.mozbrowser = true;
+
+  document.body.appendChild(iframe);
+
+  return Task.spawn(function* () {
+    var { client, root, transport } = yield newConnectedDebuggerClient();
+
+    var directorRegistryClient = new DirectorRegistryFront(client, root);
+
+    // install a director script
+    yield directorRegistryClient.install("testPropagatedDirectorScript", {
+      scriptCode: "console.log('director script test');",
+      scriptOptions: {}
+    });
+
+    var conn = transport._serverConnection;
+    var childActor = yield DebuggerServer.connectToChild(conn, iframe);
+
+    ok(typeof childActor.directorManagerActor !== "undefined",
+       "childActor.directorActor should be defined");
+
+    var childDirectorManagerClient = new DirectorManagerFront(client, childActor);
+
+    var directorScriptList = yield childDirectorManagerClient.list();
+
+    ok(directorScriptList.installed.length === 1 &&
+       directorScriptList.installed[0] === "testPropagatedDirectorScript",
+       "director scripts propagated correctly")
+
+    yield client.close();
+  });
+}
+  </script>
+</pre>
+</body>
+</html>