Upstream version 10.38.208.0
[platform/framework/web/crosswalk.git] / src / remoting / webapp / client_session.js
index 2d3e88d..85610cd 100644 (file)
@@ -23,6 +23,8 @@
 var remoting = remoting || {};
 
 /**
+ * @param {HTMLElement} container Container element for the client view.
+ * @param {string} hostDisplayName A human-readable name for the host.
  * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
  * @param {function(boolean, function(string): void): void} fetchPin
  *     Called by Me2Me connections when a PIN needs to be obtained
@@ -44,17 +46,25 @@ var remoting = remoting || {};
  * @param {string} clientPairedSecret For paired Me2Me connections, the
  *     paired secret for this client, as issued by the host.
  * @constructor
+ * @extends {base.EventSource}
  */
-remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
-                                  authenticationMethods,
-                                  hostId, hostJid, hostPublicKey, mode,
-                                  clientPairingId, clientPairedSecret) {
+remoting.ClientSession = function(container, hostDisplayName, accessCode,
+                                  fetchPin, fetchThirdPartyToken,
+                                  authenticationMethods, hostId, hostJid,
+                                  hostPublicKey, mode, clientPairingId,
+                                  clientPairedSecret) {
   /** @private */
   this.state_ = remoting.ClientSession.State.CREATED;
 
   /** @private */
   this.error_ = remoting.Error.NONE;
 
+  /** @type {HTMLElement}
+    * @private */
+  this.container_ = container;
+
+  /** @private */
+  this.hostDisplayName_ = hostDisplayName;
   /** @private */
   this.hostJid_ = hostJid;
   /** @private */
@@ -89,15 +99,20 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
   /** @private */
   this.hasReceivedFrame_ = false;
   this.logToServer = new remoting.LogToServer();
-  /** @type {?function(remoting.ClientSession.State,
-                       remoting.ClientSession.State):void} */
-  this.onStateChange_ = null;
 
   /** @type {number?} @private */
   this.notifyClientResolutionTimer_ = null;
   /** @type {number?} @private */
   this.bumpScrollTimer_ = null;
 
+  // Bump-scroll test variables. Override to use a fake value for the width
+  // and height of the client plugin so that bump-scrolling can be tested
+  // without relying on the actual size of the host desktop.
+  /** @type {number} @private */
+  this.pluginWidthForBumpScrollTesting = 0;
+  /** @type {number} @private */
+  this.pluginHeightForBumpScrollTesting = 0;
+
   /**
    * Allow host-offline error reporting to be suppressed in situations where it
    * would not be useful, for example, when using a cached host JID.
@@ -111,9 +126,10 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
   /** @private */
   this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
   /** @private */
-  this.callSetScreenMode_ = this.onSetScreenMode_.bind(this);
+  this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
+      remoting.fullscreen);
   /** @private */
-  this.callToggleFullScreen_ = this.toggleFullScreen_.bind(this);
+  this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
 
   /** @private */
   this.screenOptionsMenu_ = new remoting.MenuButton(
@@ -124,6 +140,21 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
       document.getElementById('send-keys-menu')
   );
 
+  /** @type {HTMLMediaElement} @private */
+  this.video_ = null;
+
+  /** @type {Element} @private */
+  this.mouseCursorOverlay_ =
+      this.container_.querySelector('.mouse-cursor-overlay');
+
+  /** @type {Element} */
+  var img = this.mouseCursorOverlay_;
+  /** @param {Event} event @private */
+  this.updateMouseCursorPosition_ = function(event) {
+    img.style.top = event.y + 'px';
+    img.style.left = event.x + 'px';
+  };
+
   /** @type {HTMLElement} @private */
   this.resizeToClientButton_ =
       document.getElementById('screen-resize-to-client');
@@ -132,28 +163,38 @@ remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
   /** @type {HTMLElement} @private */
   this.fullScreenButton_ = document.getElementById('toggle-full-screen');
 
+  /** @type {remoting.GnubbyAuthHandler} @private */
+  this.gnubbyAuthHandler_ = null;
+
   if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
     // Resize-to-client is not supported for IT2Me hosts.
     this.resizeToClientButton_.hidden = true;
   } else {
     this.resizeToClientButton_.hidden = false;
-    this.resizeToClientButton_.addEventListener(
-        'click', this.callSetScreenMode_, false);
   }
 
-  this.shrinkToFitButton_.addEventListener(
-      'click', this.callSetScreenMode_, false);
   this.fullScreenButton_.addEventListener(
       'click', this.callToggleFullScreen_, false);
+  this.defineEvents(Object.keys(remoting.ClientSession.Events));
+};
+
+base.extend(remoting.ClientSession, base.EventSource);
+
+/** @enum {string} */
+remoting.ClientSession.Events = {
+  stateChanged: 'stateChanged',
+  videoChannelStateChanged: 'videoChannelStateChanged',
+  bumpScrollStarted: 'bumpScrollStarted',
+  bumpScrollStopped: 'bumpScrollStopped'
 };
 
 /**
- * @param {?function(remoting.ClientSession.State,
-                     remoting.ClientSession.State):void} onStateChange
- *     The callback to invoke when the session changes state.
+ * Get host display name.
+ *
+ * @return {string}
  */
-remoting.ClientSession.prototype.setOnStateChange = function(onStateChange) {
-  this.onStateChange_ = onStateChange;
+remoting.ClientSession.prototype.getHostDisplayName = function() {
+  return this.hostDisplayName_;
 };
 
 /**
@@ -169,31 +210,46 @@ remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
   if (!this.shrinkToFit_) {
     // Determine whether or not horizontal or vertical scrollbars are
     // required, taking into account their width.
-    needsVerticalScroll = window.innerHeight < this.plugin_.desktopHeight;
-    needsHorizontalScroll = window.innerWidth < this.plugin_.desktopWidth;
+    var clientArea = this.getClientArea_();
+    needsVerticalScroll = clientArea.height < this.plugin_.desktopHeight;
+    needsHorizontalScroll = clientArea.width < this.plugin_.desktopWidth;
     var kScrollBarWidth = 16;
     if (needsHorizontalScroll && !needsVerticalScroll) {
       needsVerticalScroll =
-          window.innerHeight - kScrollBarWidth < this.plugin_.desktopHeight;
+          clientArea.height - kScrollBarWidth < this.plugin_.desktopHeight;
     } else if (!needsHorizontalScroll && needsVerticalScroll) {
       needsHorizontalScroll =
-          window.innerWidth - kScrollBarWidth < this.plugin_.desktopWidth;
+          clientArea.width - kScrollBarWidth < this.plugin_.desktopWidth;
     }
   }
 
-  var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
+  var scroller = document.getElementById('scroller');
   if (needsHorizontalScroll) {
-    htmlNode.classList.remove('no-horizontal-scroll');
+    scroller.classList.remove('no-horizontal-scroll');
   } else {
-    htmlNode.classList.add('no-horizontal-scroll');
+    scroller.classList.add('no-horizontal-scroll');
   }
   if (needsVerticalScroll) {
-    htmlNode.classList.remove('no-vertical-scroll');
+    scroller.classList.remove('no-vertical-scroll');
   } else {
-    htmlNode.classList.add('no-vertical-scroll');
+    scroller.classList.add('no-vertical-scroll');
   }
 };
 
+/**
+ * @return {boolean} True if shrink-to-fit is enabled; false otherwise.
+ */
+remoting.ClientSession.prototype.getShrinkToFit = function() {
+  return this.shrinkToFit_;
+};
+
+/**
+ * @return {boolean} True if resize-to-client is enabled; false otherwise.
+ */
+remoting.ClientSession.prototype.getResizeToClient = function() {
+  return this.resizeToClient_;
+};
+
 // Note that the positive values in both of these enums are copied directly
 // from chromoting_scriptable_object.h and must be kept in sync. The negative
 // values represent state transitions that occur within the web-app that have
@@ -211,6 +267,30 @@ remoting.ClientSession.State = {
   FAILED: 5
 };
 
+/**
+ * @param {string} state The state name.
+ * @return {remoting.ClientSession.State} The session state enum value.
+ */
+remoting.ClientSession.State.fromString = function(state) {
+  if (!remoting.ClientSession.State.hasOwnProperty(state)) {
+    throw "Invalid ClientSession.State: " + state;
+  }
+  return remoting.ClientSession.State[state];
+};
+
+/**
+  @constructor
+  @param {remoting.ClientSession.State} current
+  @param {remoting.ClientSession.State} previous
+*/
+remoting.ClientSession.StateEvent = function(current, previous) {
+  /** @type {remoting.ClientSession.State} */
+  this.previous = previous
+
+  /** @type {remoting.ClientSession.State} */
+  this.current = current;
+};
+
 /** @enum {number} */
 remoting.ClientSession.ConnectionError = {
   UNKNOWN: -1,
@@ -222,6 +302,18 @@ remoting.ClientSession.ConnectionError = {
   HOST_OVERLOAD: 5
 };
 
+/**
+ * @param {string} error The connection error name.
+ * @return {remoting.ClientSession.ConnectionError} The connection error enum.
+ */
+remoting.ClientSession.ConnectionError.fromString = function(error) {
+  if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
+    console.error('Unexpected ClientSession.ConnectionError string: ', error);
+    return remoting.ClientSession.ConnectionError.UNKNOWN;
+  }
+  return remoting.ClientSession.ConnectionError[error];
+}
+
 // The mode of this session.
 /** @enum {number} */
 remoting.ClientSession.Mode = {
@@ -264,13 +356,6 @@ remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
 
 /**
- * The id of the client plugin
- *
- * @const
- */
-remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
-
-/**
  * Set of capabilities for which hasCapability_() can be used to test.
  *
  * @enum {string}
@@ -280,7 +365,8 @@ remoting.ClientSession.Capability = {
   // resolution to the host once connection has been established. See
   // this.plugin_.notifyClientResolution().
   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
-  RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests'
+  RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests',
+  VIDEO_RECORDER: 'videoRecorder'
 };
 
 /**
@@ -305,27 +391,6 @@ remoting.ClientSession.prototype.hasCapability_ = function(capability) {
 };
 
 /**
- * @param {Element} container The element to add the plugin to.
- * @param {string} id Id to use for the plugin element .
- * @return {remoting.ClientPlugin} Create plugin object for the locally
- * installed plugin.
- */
-remoting.ClientSession.prototype.createClientPlugin_ = function(container, id) {
-  var plugin = /** @type {remoting.ViewerPlugin} */
-      document.createElement('embed');
-
-  plugin.id = id;
-  plugin.src = 'about://none';
-  plugin.type = 'application/vnd.chromium.remoting-viewer';
-  plugin.width = 0;
-  plugin.height = 0;
-  plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
-  container.appendChild(plugin);
-
-  return new remoting.ClientPlugin(plugin);
-};
-
-/**
  * Callback function called when the plugin element gets focus.
  */
 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
@@ -341,7 +406,10 @@ remoting.ClientSession.prototype.pluginLostFocus_ = function() {
     this.plugin_.releaseAllKeys();
     if (this.plugin_.element()) {
       // Focus should stay on the element, not (for example) the toolbar.
-      this.plugin_.element().focus();
+      // Due to crbug.com/246335, we can't restore the focus immediately,
+      // otherwise the plugin gets confused about whether or not it has focus.
+      window.setTimeout(
+          this.plugin_.element().focus.bind(this.plugin_.element()), 0);
     }
   }
 };
@@ -349,11 +417,15 @@ remoting.ClientSession.prototype.pluginLostFocus_ = function() {
 /**
  * Adds <embed> element to |container| and readies the sesion object.
  *
- * @param {Element} container The element to add the plugin to.
+ * @param {function(string, string):boolean} onExtensionMessage The handler for
+ *     protocol extension messages. Returns true if a message is recognized;
+ *     false otherwise.
  */
 remoting.ClientSession.prototype.createPluginAndConnect =
-    function(container) {
-  this.plugin_ = this.createClientPlugin_(container, this.PLUGIN_ID);
+    function(onExtensionMessage) {
+  this.plugin_ = new remoting.ClientPlugin(
+      this.container_.querySelector('.client-plugin-container'),
+      onExtensionMessage);
   remoting.HostSettings.load(this.hostId_,
                              this.onHostSettingsLoaded_.bind(this));
 };
@@ -404,7 +476,7 @@ remoting.ClientSession.prototype.setFocusHandlers_ = function() {
  */
 remoting.ClientSession.prototype.resetWithError_ = function(error) {
   this.plugin_.cleanup();
-  delete this.plugin_;
+  this.plugin_ = null;
   this.error_ = error;
   this.setState_(remoting.ClientSession.State.FAILED);
 }
@@ -440,21 +512,42 @@ remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
     this.applyRemapKeys_(true);
   }
 
+
+  // Enable MediaSource-based rendering on Chrome 37 and above.
+  var chromeVersionMajor =
+      parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
+  if (chromeVersionMajor >= 37 &&
+      this.plugin_.hasFeature(
+          remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
+    this.video_ = /** @type {HTMLMediaElement} */(
+        this.container_.querySelector('video'));
+    // Make sure that the <video> element is hidden until we get the first
+    // frame.
+    this.video_.style.width = '0px';
+    this.video_.style.height = '0px';
+
+    var renderer = new remoting.MediaSourceRenderer(this.video_);
+    this.plugin_.enableMediaSourceRendering(renderer);
+    this.container_.classList.add('mediasource-rendering');
+  } else {
+    this.container_.classList.remove('mediasource-rendering');
+  }
+
   /** @param {string} msg The IQ stanza to send. */
   this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this);
   /** @param {string} msg The message to log. */
   this.plugin_.onDebugMessageHandler = function(msg) {
-    console.log('plugin: ' + msg);
+    console.log('plugin: ' + msg.trimRight());
   };
 
   this.plugin_.onConnectionStatusUpdateHandler =
       this.onConnectionStatusUpdate_.bind(this);
-  this.plugin_.onConnectionReadyHandler =
-      this.onConnectionReady_.bind(this);
+  this.plugin_.onConnectionReadyHandler = this.onConnectionReady_.bind(this);
   this.plugin_.onDesktopSizeUpdateHandler =
       this.onDesktopSizeChanged_.bind(this);
-  this.plugin_.onSetCapabilitiesHandler =
-      this.onSetCapabilities_.bind(this);
+  this.plugin_.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this);
+  this.plugin_.onGnubbyAuthHandler = this.processGnubbyAuthMessage_.bind(this);
+  this.plugin_.updateMouseCursorImage = this.updateMouseCursorImage_.bind(this);
   this.initiateConnection_();
 };
 
@@ -476,31 +569,59 @@ remoting.ClientSession.prototype.removePlugin = function() {
   }
 
   // Delete event handlers that aren't relevent when not connected.
-  this.resizeToClientButton_.removeEventListener(
-      'click', this.callSetScreenMode_, false);
-  this.shrinkToFitButton_.removeEventListener(
-      'click', this.callSetScreenMode_, false);
   this.fullScreenButton_.removeEventListener(
       'click', this.callToggleFullScreen_, false);
 
-  // In case the user had selected full-screen mode, cancel it now.
-  document.webkitCancelFullScreen();
+  // Leave full-screen mode, and stop listening for related events.
+  var listener = this.callOnFullScreenChanged_;
+  remoting.fullscreen.syncWithMaximize(false);
+  remoting.fullscreen.activate(
+      false,
+      function() {
+        remoting.fullscreen.removeListener(listener);
+      });
+  if (remoting.windowFrame) {
+    remoting.windowFrame.setClientSession(null);
+  } else {
+    remoting.toolbar.setClientSession(null);
+  }
+
+  // Remove mediasource-rendering class from the container - this will also
+  // hide the <video> element.
+  this.container_.classList.remove('mediasource-rendering');
+
+  this.container_.removeEventListener('mousemove',
+                                      this.updateMouseCursorPosition_,
+                                      true);
+};
+
+/**
+ * Disconnect the current session with a particular |error|.  The session will
+ * raise a |stateChanged| event in response to it.  The caller should then call
+ * |cleanup| to remove and destroy the <embed> element.
+ *
+ * @param {remoting.Error} error The reason for the disconnection.  Use
+ *    remoting.Error.NONE if there is no error.
+ * @return {void} Nothing.
+ */
+remoting.ClientSession.prototype.disconnect = function(error) {
+  var state = (error == remoting.Error.NONE) ?
+                  remoting.ClientSession.State.CLOSED :
+                  remoting.ClientSession.State.FAILED;
+
+  // The plugin won't send a state change notification, so we explicitly log
+  // the fact that the connection has closed.
+  this.logToServer.logClientSessionStateChange(state, error, this.mode_);
+  this.error_ = error;
+  this.setState_(state);
 };
 
 /**
  * Deletes the <embed> element from the container and disconnects.
  *
- * @param {boolean} isUserInitiated True for user-initiated disconnects, False
- *     for disconnects due to connection failures.
  * @return {void} Nothing.
  */
-remoting.ClientSession.prototype.disconnect = function(isUserInitiated) {
-  if (isUserInitiated) {
-    // The plugin won't send a state change notification, so we explicitly log
-    // the fact that the connection has closed.
-    this.logToServer.logClientSessionStateChange(
-        remoting.ClientSession.State.CLOSED, remoting.Error.NONE, this.mode_);
-  }
+remoting.ClientSession.prototype.cleanup = function() {
   remoting.wcsSandbox.setOnIq(null);
   this.sendIq_(
       '<cli:iq ' +
@@ -562,6 +683,7 @@ remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
  * @return {void} Nothing.
  */
 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
+  console.log('Sending Ctrl-Alt-Del.');
   this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
 }
 
@@ -571,6 +693,7 @@ remoting.ClientSession.prototype.sendCtrlAltDel = function() {
  * @return {void} Nothing.
  */
 remoting.ClientSession.prototype.sendPrintScreen = function() {
+  console.log('Sending Print Screen.');
   this.sendKeyCombination_([0x070046]);
 }
 
@@ -604,6 +727,10 @@ remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
     remapKeys = '0x0700e4>0x0700e7';
   }
 
+  if (remapKeys == '') {
+    return;
+  }
+
   var remappings = remapKeys.split(',');
   for (var i = 0; i < remappings.length; ++i) {
     var keyCodes = remappings[i].split('>');
@@ -629,26 +756,6 @@ remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
 }
 
 /**
- * Callback for the two "screen mode" related menu items: Resize desktop to
- * fit and Shrink to fit.
- *
- * @param {Event} event The click event indicating which mode was selected.
- * @return {void} Nothing.
- * @private
- */
-remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
-  var shrinkToFit = this.shrinkToFit_;
-  var resizeToClient = this.resizeToClient_;
-  if (event.target == this.shrinkToFitButton_) {
-    shrinkToFit = !shrinkToFit;
-  }
-  if (event.target == this.resizeToClientButton_) {
-    resizeToClient = !resizeToClient;
-  }
-  this.setScreenMode_(shrinkToFit, resizeToClient);
-};
-
-/**
  * Set the shrink-to-fit and resize-to-client flags and save them if this is
  * a Me2Me connection.
  *
@@ -660,14 +767,14 @@ remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
  *     false to disable this behaviour for subsequent window resizes--the
  *     current host desktop size is not restored in this case.
  * @return {void} Nothing.
- * @private
  */
-remoting.ClientSession.prototype.setScreenMode_ =
+remoting.ClientSession.prototype.setScreenMode =
     function(shrinkToFit, resizeToClient) {
   if (resizeToClient && !this.resizeToClient_) {
-    this.plugin_.notifyClientResolution(window.innerWidth,
-                                       window.innerHeight,
-                                       window.devicePixelRatio);
+    var clientArea = this.getClientArea_();
+    this.plugin_.notifyClientResolution(clientArea.width,
+                                        clientArea.height,
+                                        window.devicePixelRatio);
   }
 
   // If enabling shrink, reset bump-scroll offsets.
@@ -686,7 +793,7 @@ remoting.ClientSession.prototype.setScreenMode_ =
 
   this.updateDimensions();
   if (needsScrollReset) {
-    this.scroll_(0, 0);
+    this.resetScroll_();
   }
 
 }
@@ -852,10 +959,24 @@ remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
     this.setFocusHandlers_();
     this.onDesktopSizeChanged_();
     if (this.resizeToClient_) {
-      this.plugin_.notifyClientResolution(window.innerWidth,
-                                         window.innerHeight,
-                                         window.devicePixelRatio);
+      var clientArea = this.getClientArea_();
+      this.plugin_.notifyClientResolution(clientArea.width,
+                                          clientArea.height,
+                                          window.devicePixelRatio);
+    }
+    // Activate full-screen related UX.
+    remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
+    remoting.fullscreen.syncWithMaximize(true);
+    if (remoting.windowFrame) {
+      remoting.windowFrame.setClientSession(this);
+    } else {
+      remoting.toolbar.setClientSession(this);
     }
+
+    this.container_.addEventListener('mousemove',
+                                     this.updateMouseCursorPosition_,
+                                     true);
+
   } else if (status == remoting.ClientSession.State.FAILED) {
     switch (error) {
       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
@@ -889,10 +1010,13 @@ remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
  */
 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
   if (!ready) {
-    this.plugin_.element().classList.add("session-client-inactive");
+    this.container_.classList.add('session-client-inactive');
   } else {
-    this.plugin_.element().classList.remove("session-client-inactive");
+    this.container_.classList.remove('session-client-inactive');
   }
+
+  this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
+                  ready);
 };
 
 /**
@@ -912,9 +1036,10 @@ remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
   this.capabilities_ = capabilities;
   if (this.hasCapability_(
       remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
-    this.plugin_.notifyClientResolution(window.innerWidth,
-                                       window.innerHeight,
-                                       window.devicePixelRatio);
+    var clientArea = this.getClientArea_();
+    this.plugin_.notifyClientResolution(clientArea.width,
+                                        clientArea.height,
+                                        window.devicePixelRatio);
   }
 };
 
@@ -943,9 +1068,13 @@ remoting.ClientSession.prototype.setState_ = function(newState) {
     state = remoting.ClientSession.State.CONNECTION_DROPPED;
   }
   this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
-  if (this.onStateChange_) {
-    this.onStateChange_(oldState, newState);
+  if (this.state_ == remoting.ClientSession.State.CONNECTED) {
+    this.createGnubbyAuthHandler_();
   }
+
+  this.raiseEvent(remoting.ClientSession.Events.stateChanged,
+    new remoting.ClientSession.StateEvent(newState, oldState)
+  );
 };
 
 /**
@@ -969,17 +1098,18 @@ remoting.ClientSession.prototype.onResize = function() {
         remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
       kResizeRateLimitMs = 250;
     }
+    var clientArea = this.getClientArea_();
     this.notifyClientResolutionTimer_ = window.setTimeout(
         this.plugin_.notifyClientResolution.bind(this.plugin_,
-                                                 window.innerWidth,
-                                                 window.innerHeight,
+                                                 clientArea.width,
+                                                 clientArea.height,
                                                  window.devicePixelRatio),
         kResizeRateLimitMs);
   }
 
   // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
   // the new window area.
-  this.scroll_(0, 0);
+  this.resetScroll_();
 
   this.updateScrollbarVisibility();
 };
@@ -992,9 +1122,9 @@ remoting.ClientSession.prototype.onResize = function() {
  */
 remoting.ClientSession.prototype.pauseVideo = function(pause) {
   if (this.plugin_) {
-    this.plugin_.pauseVideo(pause)
+    this.plugin_.pauseVideo(pause);
   }
-}
+};
 
 /**
  * Requests that the host pause or resume audio.
@@ -1037,8 +1167,7 @@ remoting.ClientSession.prototype.updateDimensions = function() {
     return;
   }
 
-  var windowWidth = window.innerWidth;
-  var windowHeight = window.innerHeight;
+  var clientArea = this.getClientArea_();
   var desktopWidth = this.plugin_.desktopWidth;
   var desktopHeight = this.plugin_.desktopHeight;
 
@@ -1063,13 +1192,14 @@ remoting.ClientSession.prototype.updateDimensions = function() {
 
   if (this.shrinkToFit_) {
     // Reduce the scale, if necessary, to fit the whole desktop in the window.
-    var scaleFitWidth = Math.min(scale, 1.0 * windowWidth / desktopWidth);
-    var scaleFitHeight = Math.min(scale, 1.0 * windowHeight / desktopHeight);
+    var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
+    var scaleFitHeight =
+        Math.min(scale, 1.0 * clientArea.height / desktopHeight);
     scale = Math.min(scaleFitHeight, scaleFitWidth);
 
     // If we're running full-screen then try to handle common side-by-side
     // multi-monitor combinations more intelligently.
-    if (document.webkitIsFullScreen) {
+    if (remoting.fullscreen.isActive()) {
       // If the host has two monitors each the same size as the client then
       // scale-to-fit will have the desktop occupy only 50% of the client area,
       // in which case it would be preferable to down-scale less and let the
@@ -1087,13 +1217,18 @@ remoting.ClientSession.prototype.updateDimensions = function() {
     }
   }
 
-  var pluginWidth = desktopWidth * scale;
-  var pluginHeight = desktopHeight * scale;
+  var pluginWidth = Math.round(desktopWidth * scale);
+  var pluginHeight = Math.round(desktopHeight * scale);
+
+  if (this.video_) {
+    this.video_.style.width = pluginWidth + 'px';
+    this.video_.style.height = pluginHeight + 'px';
+  }
 
   // Resize the plugin if necessary.
   // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
-  this.plugin_.element().width = pluginWidth;
-  this.plugin_.element().height = pluginHeight;
+  this.plugin_.element().style.width = pluginWidth + 'px';
+  this.plugin_.element().style.height = pluginHeight + 'px';
 
   // Position the container.
   // Note that clientWidth/Height take into account scrollbars.
@@ -1151,23 +1286,20 @@ remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
 };
 
 /**
- * Toggles between full-screen and windowed mode.
- * @return {void} Nothing.
+ * Called when the full-screen status has changed, either via the
+ * remoting.Fullscreen class, or via a system event such as the Escape key
+ *
+ * @param {boolean} fullscreen True if the app is entering full-screen mode;
+ *     false if it is leaving it.
  * @private
  */
-remoting.ClientSession.prototype.toggleFullScreen_ = function() {
-  var htmlNode = /** @type {HTMLElement} */ (document.body.parentNode);
-  if (document.webkitIsFullScreen) {
-    document.webkitCancelFullScreen();
-    this.enableBumpScroll_(false);
-    htmlNode.classList.remove('full-screen');
-  } else {
-    document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
-    // Don't enable bump scrolling immediately because it can result in
-    // onMouseMove firing before the webkitIsFullScreen property can be
-    // read safely (crbug.com/132180).
-    window.setTimeout(this.enableBumpScroll_.bind(this, true), 0);
+remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
+  var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
+  this.enableBumpScroll_(fullscreen);
+  if (fullscreen) {
     htmlNode.classList.add('full-screen');
+  } else {
+    htmlNode.classList.remove('full-screen');
   }
 };
 
@@ -1181,7 +1313,7 @@ remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
   remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_);
   remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_);
   remoting.MenuButton.select(this.fullScreenButton_,
-      document.webkitIsFullScreen);
+                             remoting.fullscreen.isActive());
 };
 
 /**
@@ -1198,9 +1330,6 @@ remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
  * @private
  */
 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
-  var plugin = this.plugin_.element();
-  var style = plugin.style;
-
   /**
    * Helper function for x- and y-scrolling
    * @param {number|string} curr The current margin, eg. "10px".
@@ -1217,17 +1346,28 @@ remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
     var result = (curr ? parseFloat(curr) : 0) - delta;
     result = Math.min(0, Math.max(minMargin, result));
     stop.stop = (result == 0 || result == minMargin);
-    return result + "px";
+    return result + 'px';
   };
 
+  var plugin = this.plugin_.element();
+  var style = this.container_.style;
+
   var stopX = { stop: false };
-  style.marginLeft = adjustMargin(style.marginLeft, dx,
-                                  window.innerWidth, plugin.width, stopX);
+  var clientArea = this.getClientArea_();
+  style.marginLeft = adjustMargin(style.marginLeft, dx, clientArea.width,
+      this.pluginWidthForBumpScrollTesting || plugin.clientWidth, stopX);
+
   var stopY = { stop: false };
-  style.marginTop = adjustMargin(style.marginTop, dy,
-                                 window.innerHeight, plugin.height, stopY);
+  style.marginTop = adjustMargin(
+      style.marginTop, dy, clientArea.height,
+      this.pluginHeightForBumpScrollTesting || plugin.clientHeight, stopY);
   return stopX.stop && stopY.stop;
-}
+};
+
+remoting.ClientSession.prototype.resetScroll_ = function() {
+  this.container_.style.marginTop = '0px';
+  this.container_.style.marginLeft = '0px';
+};
 
 /**
  * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
@@ -1236,17 +1376,15 @@ remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
  * @param {boolean} enable True to enable bump-scrolling, false to disable it.
  */
 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
+  var element = /*@type{HTMLElement} */ document.documentElement;
   if (enable) {
     /** @type {null|function(Event):void} */
     this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
-    this.plugin_.element().addEventListener(
-        'mousemove', this.onMouseMoveRef_, false);
+    element.addEventListener('mousemove', this.onMouseMoveRef_, false);
   } else {
-    this.plugin_.element().removeEventListener(
-        'mousemove', this.onMouseMoveRef_, false);
+    element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
     this.onMouseMoveRef_ = null;
-    this.plugin_.element().style.marginLeft = 0;
-    this.plugin_.element().style.marginTop = 0;
+    this.resetScroll_();
   }
 };
 
@@ -1259,11 +1397,6 @@ remoting.ClientSession.prototype.onMouseMove_ = function(event) {
     window.clearTimeout(this.bumpScrollTimer_);
     this.bumpScrollTimer_ = null;
   }
-  // It's possible to leave content full-screen mode without using the Screen
-  // Options menu, so we disable bump scrolling as soon as we detect this.
-  if (!document.webkitIsFullScreen) {
-    this.enableBumpScroll_(false);
-  }
 
   /**
    * Compute the scroll speed based on how close the mouse is to the edge.
@@ -1281,10 +1414,12 @@ remoting.ClientSession.prototype.onMouseMove_ = function(event) {
     return 0;
   };
 
-  var dx = computeDelta(event.x, window.innerWidth);
-  var dy = computeDelta(event.y, window.innerHeight);
+  var clientArea = this.getClientArea_();
+  var dx = computeDelta(event.x, clientArea.width);
+  var dy = computeDelta(event.y, clientArea.height);
 
   if (dx != 0 || dy != 0) {
+    this.raiseEvent(remoting.ClientSession.Events.bumpScrollStarted);
     /** @type {remoting.ClientSession} */
     var that = this;
     /**
@@ -1298,7 +1433,9 @@ remoting.ClientSession.prototype.onMouseMove_ = function(event) {
       /** @type {number} */
       var timeout = 10;
       var lateAdjustment = 1 + (now - expected) / timeout;
-      if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
+      if (that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
+        that.raiseEvent(remoting.ClientSession.Events.bumpScrollStopped);
+      } else {
         that.bumpScrollTimer_ = window.setTimeout(
             function() { repeatScroll(now + timeout); },
             timeout);
@@ -1317,5 +1454,84 @@ remoting.ClientSession.prototype.onMouseMove_ = function(event) {
 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
   if (!this.plugin_)
     return;
-  this.plugin_.sendClipboardItem(mimeType, item)
+  this.plugin_.sendClipboardItem(mimeType, item);
+};
+
+/**
+ * Send a gnubby-auth extension message to the host.
+ * @param {Object} data The gnubby-auth message data.
+ */
+remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
+  if (!this.plugin_)
+    return;
+  this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
+};
+
+/**
+ * Process a remote gnubby auth request.
+ * @param {string} data Remote gnubby request data.
+ * @private
+ */
+remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
+  if (this.gnubbyAuthHandler_) {
+    try {
+      this.gnubbyAuthHandler_.onMessage(data);
+    } catch (err) {
+      console.error('Failed to process gnubby message: ',
+          /** @type {*} */ (err));
+    }
+  } else {
+    console.error('Received unexpected gnubby message');
+  }
+};
+
+/**
+ * Create a gnubby auth handler and inform the host that gnubby auth is
+ * supported.
+ * @private
+ */
+remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
+  if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
+    this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
+    // TODO(psj): Move to more generic capabilities mechanism.
+    this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
+  }
+};
+
+/**
+ * @return {{width: number, height: number}} The height of the window's client
+ *     area. This differs between apps v1 and apps v2 due to the custom window
+ *     borders used by the latter.
+ * @private
+ */
+remoting.ClientSession.prototype.getClientArea_ = function() {
+  return remoting.windowFrame ?
+      remoting.windowFrame.getClientArea() :
+      { 'width': window.innerWidth, 'height': window.innerHeight };
+};
+
+/**
+ * @param {string} url
+ * @param {number} hotspotX
+ * @param {number} hotspotY
+ */
+remoting.ClientSession.prototype.updateMouseCursorImage_ =
+    function(url, hotspotX, hotspotY) {
+  this.mouseCursorOverlay_.hidden = !url;
+  if (url) {
+    this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
+    this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
+    this.mouseCursorOverlay_.src = url;
+  }
+};
+
+/**
+ * @return {{top: number, left:number}} The top-left corner of the plugin.
+ */
+remoting.ClientSession.prototype.getPluginPositionForTesting = function() {
+  var style = this.container_.style;
+  return {
+    top: parseFloat(style.marginTop),
+    left: parseFloat(style.marginLeft)
+  };
 };