Home | History | Annotate | Download | only in webapp
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview
      7  * Class handling creation and teardown of a remoting client session.
      8  *
      9  * The ClientSession class controls lifetime of the client plugin
     10  * object and provides the plugin with the functionality it needs to
     11  * establish connection. Specifically it:
     12  *  - Delivers incoming/outgoing signaling messages,
     13  *  - Adjusts plugin size and position when destop resolution changes,
     14  *
     15  * This class should not access the plugin directly, instead it should
     16  * do it through ClientPlugin class which abstracts plugin version
     17  * differences.
     18  */
     19 
     20 'use strict';
     21 
     22 /** @suppress {duplicate} */
     23 var remoting = remoting || {};
     24 
     25 /**
     26  * @param {string} accessCode The IT2Me access code. Blank for Me2Me.
     27  * @param {function(boolean, function(string): void): void} fetchPin
     28  *     Called by Me2Me connections when a PIN needs to be obtained
     29  *     interactively.
     30  * @param {function(string, string, string,
     31  *                  function(string, string): void): void}
     32  *     fetchThirdPartyToken Called by Me2Me connections when a third party
     33  *     authentication token must be obtained.
     34  * @param {string} authenticationMethods Comma-separated list of
     35  *     authentication methods the client should attempt to use.
     36  * @param {string} hostId The host identifier for Me2Me, or empty for IT2Me.
     37  *     Mixed into authentication hashes for some authentication methods.
     38  * @param {string} hostJid The jid of the host to connect to.
     39  * @param {string} hostPublicKey The base64 encoded version of the host's
     40  *     public key.
     41  * @param {remoting.ClientSession.Mode} mode The mode of this connection.
     42  * @param {string} clientPairingId For paired Me2Me connections, the
     43  *     pairing id for this client, as issued by the host.
     44  * @param {string} clientPairedSecret For paired Me2Me connections, the
     45  *     paired secret for this client, as issued by the host.
     46  * @constructor
     47  * @extends {base.EventSource}
     48  */
     49 remoting.ClientSession = function(accessCode, fetchPin, fetchThirdPartyToken,
     50                                   authenticationMethods,
     51                                   hostId, hostJid, hostPublicKey, mode,
     52                                   clientPairingId, clientPairedSecret) {
     53   /** @private */
     54   this.state_ = remoting.ClientSession.State.CREATED;
     55 
     56   /** @private */
     57   this.error_ = remoting.Error.NONE;
     58 
     59   /** @private */
     60   this.hostJid_ = hostJid;
     61   /** @private */
     62   this.hostPublicKey_ = hostPublicKey;
     63   /** @private */
     64   this.accessCode_ = accessCode;
     65   /** @private */
     66   this.fetchPin_ = fetchPin;
     67   /** @private */
     68   this.fetchThirdPartyToken_ = fetchThirdPartyToken;
     69   /** @private */
     70   this.authenticationMethods_ = authenticationMethods;
     71   /** @private */
     72   this.hostId_ = hostId;
     73   /** @private */
     74   this.mode_ = mode;
     75   /** @private */
     76   this.clientPairingId_ = clientPairingId;
     77   /** @private */
     78   this.clientPairedSecret_ = clientPairedSecret;
     79   /** @private */
     80   this.sessionId_ = '';
     81   /** @type {remoting.ClientPlugin}
     82     * @private */
     83   this.plugin_ = null;
     84   /** @private */
     85   this.shrinkToFit_ = true;
     86   /** @private */
     87   this.resizeToClient_ = true;
     88   /** @private */
     89   this.remapKeys_ = '';
     90   /** @private */
     91   this.hasReceivedFrame_ = false;
     92   this.logToServer = new remoting.LogToServer();
     93 
     94   /** @type {number?} @private */
     95   this.notifyClientResolutionTimer_ = null;
     96   /** @type {number?} @private */
     97   this.bumpScrollTimer_ = null;
     98 
     99   /**
    100    * Allow host-offline error reporting to be suppressed in situations where it
    101    * would not be useful, for example, when using a cached host JID.
    102    *
    103    * @type {boolean} @private
    104    */
    105   this.logHostOfflineErrors_ = true;
    106 
    107   /** @private */
    108   this.callPluginLostFocus_ = this.pluginLostFocus_.bind(this);
    109   /** @private */
    110   this.callPluginGotFocus_ = this.pluginGotFocus_.bind(this);
    111   /** @private */
    112   this.callSetScreenMode_ = this.onSetScreenMode_.bind(this);
    113   /** @private */
    114   this.callToggleFullScreen_ = remoting.fullscreen.toggle.bind(
    115       remoting.fullscreen);
    116   /** @private */
    117   this.callOnFullScreenChanged_ = this.onFullScreenChanged_.bind(this)
    118 
    119   /** @private */
    120   this.screenOptionsMenu_ = new remoting.MenuButton(
    121       document.getElementById('screen-options-menu'),
    122       this.onShowOptionsMenu_.bind(this));
    123   /** @private */
    124   this.sendKeysMenu_ = new remoting.MenuButton(
    125       document.getElementById('send-keys-menu')
    126   );
    127 
    128   /** @type {HTMLMediaElement} @private */
    129   this.video_ = null;
    130 
    131   /** @type {Element} @private */
    132   this.container_ = document.getElementById('video-container');
    133 
    134   /** @type {Element} @private */
    135   this.mouseCursorOverlay_ =
    136       this.container_.querySelector('.mouse-cursor-overlay');
    137 
    138   /** @type {Element} */
    139   var img = this.mouseCursorOverlay_;
    140   /** @param {Event} event @private */
    141   this.updateMouseCursorPosition_ = function(event) {
    142     img.style.top = event.y + 'px';
    143     img.style.left = event.x + 'px';
    144   };
    145 
    146   /** @type {HTMLElement} @private */
    147   this.resizeToClientButton_ =
    148       document.getElementById('screen-resize-to-client');
    149   /** @type {HTMLElement} @private */
    150   this.shrinkToFitButton_ = document.getElementById('screen-shrink-to-fit');
    151   /** @type {HTMLElement} @private */
    152   this.fullScreenButton_ = document.getElementById('toggle-full-screen');
    153 
    154   /** @type {remoting.GnubbyAuthHandler} @private */
    155   this.gnubbyAuthHandler_ = null;
    156 
    157   if (this.mode_ == remoting.ClientSession.Mode.IT2ME) {
    158     // Resize-to-client is not supported for IT2Me hosts.
    159     this.resizeToClientButton_.hidden = true;
    160   } else {
    161     this.resizeToClientButton_.hidden = false;
    162     this.resizeToClientButton_.addEventListener(
    163         'click', this.callSetScreenMode_, false);
    164   }
    165 
    166   this.shrinkToFitButton_.addEventListener(
    167       'click', this.callSetScreenMode_, false);
    168   this.fullScreenButton_.addEventListener(
    169       'click', this.callToggleFullScreen_, false);
    170   this.defineEvents(Object.keys(remoting.ClientSession.Events));
    171 };
    172 
    173 base.extend(remoting.ClientSession, base.EventSource);
    174 
    175 /** @enum {string} */
    176 remoting.ClientSession.Events = {
    177   stateChanged: 'stateChanged',
    178   videoChannelStateChanged: 'videoChannelStateChanged'
    179 };
    180 
    181 /**
    182  * Called when the window or desktop size or the scaling settings change,
    183  * to set the scroll-bar visibility.
    184  *
    185  * TODO(jamiewalch): crbug.com/252796: Remove this once crbug.com/240772 is
    186  * fixed.
    187  */
    188 remoting.ClientSession.prototype.updateScrollbarVisibility = function() {
    189   var needsVerticalScroll = false;
    190   var needsHorizontalScroll = false;
    191   if (!this.shrinkToFit_) {
    192     // Determine whether or not horizontal or vertical scrollbars are
    193     // required, taking into account their width.
    194     var clientArea = this.getClientArea_();
    195     needsVerticalScroll = clientArea.height < this.plugin_.desktopHeight;
    196     needsHorizontalScroll = clientArea.width < this.plugin_.desktopWidth;
    197     var kScrollBarWidth = 16;
    198     if (needsHorizontalScroll && !needsVerticalScroll) {
    199       needsVerticalScroll =
    200           clientArea.height - kScrollBarWidth < this.plugin_.desktopHeight;
    201     } else if (!needsHorizontalScroll && needsVerticalScroll) {
    202       needsHorizontalScroll =
    203           clientArea.width - kScrollBarWidth < this.plugin_.desktopWidth;
    204     }
    205   }
    206 
    207   var scroller = document.getElementById('scroller');
    208   if (needsHorizontalScroll) {
    209     scroller.classList.remove('no-horizontal-scroll');
    210   } else {
    211     scroller.classList.add('no-horizontal-scroll');
    212   }
    213   if (needsVerticalScroll) {
    214     scroller.classList.remove('no-vertical-scroll');
    215   } else {
    216     scroller.classList.add('no-vertical-scroll');
    217   }
    218 };
    219 
    220 // Note that the positive values in both of these enums are copied directly
    221 // from chromoting_scriptable_object.h and must be kept in sync. The negative
    222 // values represent state transitions that occur within the web-app that have
    223 // no corresponding plugin state transition.
    224 /** @enum {number} */
    225 remoting.ClientSession.State = {
    226   CONNECTION_CANCELED: -3,  // Connection closed (gracefully) before connecting.
    227   CONNECTION_DROPPED: -2,  // Succeeded, but subsequently closed with an error.
    228   CREATED: -1,
    229   UNKNOWN: 0,
    230   CONNECTING: 1,
    231   INITIALIZING: 2,
    232   CONNECTED: 3,
    233   CLOSED: 4,
    234   FAILED: 5
    235 };
    236 
    237 /**
    238  * @param {string} state The state name.
    239  * @return {remoting.ClientSession.State} The session state enum value.
    240  */
    241 remoting.ClientSession.State.fromString = function(state) {
    242   if (!remoting.ClientSession.State.hasOwnProperty(state)) {
    243     throw "Invalid ClientSession.State: " + state;
    244   }
    245   return remoting.ClientSession.State[state];
    246 };
    247 
    248 /**
    249   @constructor
    250   @param {remoting.ClientSession.State} current
    251   @param {remoting.ClientSession.State} previous
    252 */
    253 remoting.ClientSession.StateEvent = function(current, previous) {
    254   /** @type {remoting.ClientSession.State} */
    255   this.previous = previous
    256 
    257   /** @type {remoting.ClientSession.State} */
    258   this.current = current;
    259 };
    260 
    261 /** @enum {number} */
    262 remoting.ClientSession.ConnectionError = {
    263   UNKNOWN: -1,
    264   NONE: 0,
    265   HOST_IS_OFFLINE: 1,
    266   SESSION_REJECTED: 2,
    267   INCOMPATIBLE_PROTOCOL: 3,
    268   NETWORK_FAILURE: 4,
    269   HOST_OVERLOAD: 5
    270 };
    271 
    272 /**
    273  * @param {string} error The connection error name.
    274  * @return {remoting.ClientSession.ConnectionError} The connection error enum.
    275  */
    276 remoting.ClientSession.ConnectionError.fromString = function(error) {
    277   if (!remoting.ClientSession.ConnectionError.hasOwnProperty(error)) {
    278     console.error('Unexpected ClientSession.ConnectionError string: ', error);
    279     return remoting.ClientSession.ConnectionError.UNKNOWN;
    280   }
    281   return remoting.ClientSession.ConnectionError[error];
    282 }
    283 
    284 // The mode of this session.
    285 /** @enum {number} */
    286 remoting.ClientSession.Mode = {
    287   IT2ME: 0,
    288   ME2ME: 1
    289 };
    290 
    291 /**
    292  * Type used for performance statistics collected by the plugin.
    293  * @constructor
    294  */
    295 remoting.ClientSession.PerfStats = function() {};
    296 /** @type {number} */
    297 remoting.ClientSession.PerfStats.prototype.videoBandwidth;
    298 /** @type {number} */
    299 remoting.ClientSession.PerfStats.prototype.videoFrameRate;
    300 /** @type {number} */
    301 remoting.ClientSession.PerfStats.prototype.captureLatency;
    302 /** @type {number} */
    303 remoting.ClientSession.PerfStats.prototype.encodeLatency;
    304 /** @type {number} */
    305 remoting.ClientSession.PerfStats.prototype.decodeLatency;
    306 /** @type {number} */
    307 remoting.ClientSession.PerfStats.prototype.renderLatency;
    308 /** @type {number} */
    309 remoting.ClientSession.PerfStats.prototype.roundtripLatency;
    310 
    311 // Keys for connection statistics.
    312 remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
    313 remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
    314 remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
    315 remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
    316 remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
    317 remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
    318 remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
    319 
    320 // Keys for per-host settings.
    321 remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
    322 remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
    323 remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
    324 
    325 /**
    326  * The id of the client plugin
    327  *
    328  * @const
    329  */
    330 remoting.ClientSession.prototype.PLUGIN_ID = 'session-client-plugin';
    331 
    332 /**
    333  * Set of capabilities for which hasCapability_() can be used to test.
    334  *
    335  * @enum {string}
    336  */
    337 remoting.ClientSession.Capability = {
    338   // When enabled this capability causes the client to send its screen
    339   // resolution to the host once connection has been established. See
    340   // this.plugin_.notifyClientResolution().
    341   SEND_INITIAL_RESOLUTION: 'sendInitialResolution',
    342   RATE_LIMIT_RESIZE_REQUESTS: 'rateLimitResizeRequests'
    343 };
    344 
    345 /**
    346  * The set of capabilities negotiated between the client and host.
    347  * @type {Array.<string>}
    348  * @private
    349  */
    350 remoting.ClientSession.prototype.capabilities_ = null;
    351 
    352 /**
    353  * @param {remoting.ClientSession.Capability} capability The capability to test
    354  *     for.
    355  * @return {boolean} True if the capability has been negotiated between
    356  *     the client and host.
    357  * @private
    358  */
    359 remoting.ClientSession.prototype.hasCapability_ = function(capability) {
    360   if (this.capabilities_ == null)
    361     return false;
    362 
    363   return this.capabilities_.indexOf(capability) > -1;
    364 };
    365 
    366 /**
    367  * @param {Element} container The element to add the plugin to.
    368  * @param {string} id Id to use for the plugin element .
    369  * @param {function(string, string):boolean} onExtensionMessage The handler for
    370  *     protocol extension messages. Returns true if a message is recognized;
    371  *     false otherwise.
    372  * @return {remoting.ClientPlugin} Create plugin object for the locally
    373  * installed plugin.
    374  */
    375 remoting.ClientSession.prototype.createClientPlugin_ =
    376     function(container, id, onExtensionMessage) {
    377   var plugin = /** @type {remoting.ViewerPlugin} */
    378       document.createElement('embed');
    379 
    380   plugin.id = id;
    381   if (remoting.settings.CLIENT_PLUGIN_TYPE == 'pnacl') {
    382     plugin.src = 'remoting_client_pnacl.nmf';
    383     plugin.type = 'application/x-pnacl';
    384   } else if (remoting.settings.CLIENT_PLUGIN_TYPE == 'nacl') {
    385     plugin.src = 'remoting_client_nacl.nmf';
    386     plugin.type = 'application/x-nacl';
    387   } else {
    388     plugin.src = 'about://none';
    389     plugin.type = 'application/vnd.chromium.remoting-viewer';
    390   }
    391 
    392   plugin.width = 0;
    393   plugin.height = 0;
    394   plugin.tabIndex = 0;  // Required, otherwise focus() doesn't work.
    395   container.appendChild(plugin);
    396 
    397   return new remoting.ClientPlugin(plugin, onExtensionMessage);
    398 };
    399 
    400 /**
    401  * Callback function called when the plugin element gets focus.
    402  */
    403 remoting.ClientSession.prototype.pluginGotFocus_ = function() {
    404   remoting.clipboard.initiateToHost();
    405 };
    406 
    407 /**
    408  * Callback function called when the plugin element loses focus.
    409  */
    410 remoting.ClientSession.prototype.pluginLostFocus_ = function() {
    411   if (this.plugin_) {
    412     // Release all keys to prevent them becoming 'stuck down' on the host.
    413     this.plugin_.releaseAllKeys();
    414     if (this.plugin_.element()) {
    415       // Focus should stay on the element, not (for example) the toolbar.
    416       // Due to crbug.com/246335, we can't restore the focus immediately,
    417       // otherwise the plugin gets confused about whether or not it has focus.
    418       window.setTimeout(
    419           this.plugin_.element().focus.bind(this.plugin_.element()),
    420           0);
    421     }
    422   }
    423 };
    424 
    425 /**
    426  * Adds <embed> element to |container| and readies the sesion object.
    427  *
    428  * @param {Element} container The element to add the plugin to.
    429  * @param {function(string, string):boolean} onExtensionMessage The handler for
    430  *     protocol extension messages. Returns true if a message is recognized;
    431  *     false otherwise.
    432  */
    433 remoting.ClientSession.prototype.createPluginAndConnect =
    434     function(container, onExtensionMessage) {
    435   this.plugin_ = this.createClientPlugin_(container, this.PLUGIN_ID,
    436                                           onExtensionMessage);
    437   remoting.HostSettings.load(this.hostId_,
    438                              this.onHostSettingsLoaded_.bind(this));
    439 };
    440 
    441 /**
    442  * @param {Object.<string>} options The current options for the host, or {}
    443  *     if this client has no saved settings for the host.
    444  * @private
    445  */
    446 remoting.ClientSession.prototype.onHostSettingsLoaded_ = function(options) {
    447   if (remoting.ClientSession.KEY_REMAP_KEYS in options &&
    448       typeof(options[remoting.ClientSession.KEY_REMAP_KEYS]) ==
    449           'string') {
    450     this.remapKeys_ = /** @type {string} */
    451         options[remoting.ClientSession.KEY_REMAP_KEYS];
    452   }
    453   if (remoting.ClientSession.KEY_RESIZE_TO_CLIENT in options &&
    454       typeof(options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT]) ==
    455           'boolean') {
    456     this.resizeToClient_ = /** @type {boolean} */
    457         options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT];
    458   }
    459   if (remoting.ClientSession.KEY_SHRINK_TO_FIT in options &&
    460       typeof(options[remoting.ClientSession.KEY_SHRINK_TO_FIT]) ==
    461           'boolean') {
    462     this.shrinkToFit_ = /** @type {boolean} */
    463         options[remoting.ClientSession.KEY_SHRINK_TO_FIT];
    464   }
    465 
    466   /** @param {boolean} result */
    467   this.plugin_.initialize(this.onPluginInitialized_.bind(this));
    468 };
    469 
    470 /**
    471  * Constrains the focus to the plugin element.
    472  * @private
    473  */
    474 remoting.ClientSession.prototype.setFocusHandlers_ = function() {
    475   this.plugin_.element().addEventListener(
    476       'focus', this.callPluginGotFocus_, false);
    477   this.plugin_.element().addEventListener(
    478       'blur', this.callPluginLostFocus_, false);
    479   this.plugin_.element().focus();
    480 };
    481 
    482 /**
    483  * @param {remoting.Error} error
    484  */
    485 remoting.ClientSession.prototype.resetWithError_ = function(error) {
    486   this.plugin_.cleanup();
    487   delete this.plugin_;
    488   this.error_ = error;
    489   this.setState_(remoting.ClientSession.State.FAILED);
    490 }
    491 
    492 /**
    493  * @param {boolean} initialized
    494  */
    495 remoting.ClientSession.prototype.onPluginInitialized_ = function(initialized) {
    496   if (!initialized) {
    497     console.error('ERROR: remoting plugin not loaded');
    498     this.resetWithError_(remoting.Error.MISSING_PLUGIN);
    499     return;
    500   }
    501 
    502   if (!this.plugin_.isSupportedVersion()) {
    503     this.resetWithError_(remoting.Error.BAD_PLUGIN_VERSION);
    504     return;
    505   }
    506 
    507   // Show the Send Keys menu only if the plugin has the injectKeyEvent feature,
    508   // and the Ctrl-Alt-Del button only in Me2Me mode.
    509   if (!this.plugin_.hasFeature(
    510           remoting.ClientPlugin.Feature.INJECT_KEY_EVENT)) {
    511     var sendKeysElement = document.getElementById('send-keys-menu');
    512     sendKeysElement.hidden = true;
    513   } else if (this.mode_ != remoting.ClientSession.Mode.ME2ME) {
    514     var sendCadElement = document.getElementById('send-ctrl-alt-del');
    515     sendCadElement.hidden = true;
    516   }
    517 
    518   // Apply customized key remappings if the plugin supports remapKeys.
    519   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.REMAP_KEY)) {
    520     this.applyRemapKeys_(true);
    521   }
    522 
    523 
    524   // Enable MediaSource-based rendering on Chrome 37 and above.
    525   var chromeVersionMajor =
    526       parseInt((remoting.getChromeVersion() || '0').split('.')[0], 10);
    527   if (chromeVersionMajor >= 37 &&
    528       this.plugin_.hasFeature(
    529           remoting.ClientPlugin.Feature.MEDIA_SOURCE_RENDERING)) {
    530     this.video_ = /** @type {HTMLMediaElement} */(
    531         document.getElementById('mediasource-video-output'));
    532     // Make sure that the <video> element is hidden until we get the first
    533     // frame.
    534     this.video_.style.width = '0px';
    535     this.video_.style.height = '0px';
    536 
    537     var renderer = new remoting.MediaSourceRenderer(this.video_);
    538     this.plugin_.enableMediaSourceRendering(renderer);
    539     /** @type {HTMLElement} */(document.getElementById('video-container'))
    540         .classList.add('mediasource-rendering');
    541   } else {
    542     /** @type {HTMLElement} */(document.getElementById('video-container'))
    543         .classList.remove('mediasource-rendering');
    544   }
    545 
    546   /** @param {string} msg The IQ stanza to send. */
    547   this.plugin_.onOutgoingIqHandler = this.sendIq_.bind(this);
    548   /** @param {string} msg The message to log. */
    549   this.plugin_.onDebugMessageHandler = function(msg) {
    550     console.log('plugin: ' + msg.trimRight());
    551   };
    552 
    553   this.plugin_.onConnectionStatusUpdateHandler =
    554       this.onConnectionStatusUpdate_.bind(this);
    555   this.plugin_.onConnectionReadyHandler = this.onConnectionReady_.bind(this);
    556   this.plugin_.onDesktopSizeUpdateHandler =
    557       this.onDesktopSizeChanged_.bind(this);
    558   this.plugin_.onSetCapabilitiesHandler = this.onSetCapabilities_.bind(this);
    559   this.plugin_.onGnubbyAuthHandler = this.processGnubbyAuthMessage_.bind(this);
    560   this.plugin_.updateMouseCursorImage = this.updateMouseCursorImage_.bind(this);
    561   this.initiateConnection_();
    562 };
    563 
    564 /**
    565  * Deletes the <embed> element from the container, without sending a
    566  * session_terminate request.  This is to be called when the session was
    567  * disconnected by the Host.
    568  *
    569  * @return {void} Nothing.
    570  */
    571 remoting.ClientSession.prototype.removePlugin = function() {
    572   if (this.plugin_) {
    573     this.plugin_.element().removeEventListener(
    574         'focus', this.callPluginGotFocus_, false);
    575     this.plugin_.element().removeEventListener(
    576         'blur', this.callPluginLostFocus_, false);
    577     this.plugin_.cleanup();
    578     this.plugin_ = null;
    579   }
    580 
    581   // Delete event handlers that aren't relevent when not connected.
    582   this.resizeToClientButton_.removeEventListener(
    583       'click', this.callSetScreenMode_, false);
    584   this.shrinkToFitButton_.removeEventListener(
    585       'click', this.callSetScreenMode_, false);
    586   this.fullScreenButton_.removeEventListener(
    587       'click', this.callToggleFullScreen_, false);
    588 
    589   // Leave full-screen mode, and stop listening for related events.
    590   var listener = this.callOnFullScreenChanged_;
    591   remoting.fullscreen.syncWithMaximize(false);
    592   remoting.fullscreen.activate(
    593       false,
    594       function() {
    595         remoting.fullscreen.removeListener(listener);
    596       });
    597   if (remoting.windowFrame) {
    598     remoting.windowFrame.setConnected(false);
    599   }
    600 
    601   // Remove mediasource-rendering class from video-contained - this will also
    602   // hide the <video> element.
    603   /** @type {HTMLElement} */(document.getElementById('video-container'))
    604       .classList.remove('mediasource-rendering');
    605 
    606   this.container_.removeEventListener('mousemove',
    607                                       this.updateMouseCursorPosition_,
    608                                       true);
    609 };
    610 
    611 /**
    612  * Disconnect the current session with a particular |error|.  The session will
    613  * raise a |stateChanged| event in response to it.  The caller should then call
    614  * |cleanup| to remove and destroy the <embed> element.
    615  *
    616  * @param {remoting.Error} error The reason for the disconnection.  Use
    617  *    remoting.Error.NONE if there is no error.
    618  * @return {void} Nothing.
    619  */
    620 remoting.ClientSession.prototype.disconnect = function(error) {
    621   var state = (error == remoting.Error.NONE) ?
    622                   remoting.ClientSession.State.CLOSED :
    623                   remoting.ClientSession.State.FAILED;
    624 
    625   // The plugin won't send a state change notification, so we explicitly log
    626   // the fact that the connection has closed.
    627   this.logToServer.logClientSessionStateChange(state, error, this.mode_);
    628   this.error_ = error;
    629   this.setState_(state);
    630 };
    631 
    632 /**
    633  * Deletes the <embed> element from the container and disconnects.
    634  *
    635  * @return {void} Nothing.
    636  */
    637 remoting.ClientSession.prototype.cleanup = function() {
    638   remoting.wcsSandbox.setOnIq(null);
    639   this.sendIq_(
    640       '<cli:iq ' +
    641           'to="' + this.hostJid_ + '" ' +
    642           'type="set" ' +
    643           'id="session-terminate" ' +
    644           'xmlns:cli="jabber:client">' +
    645         '<jingle ' +
    646             'xmlns="urn:xmpp:jingle:1" ' +
    647             'action="session-terminate" ' +
    648             'sid="' + this.sessionId_ + '">' +
    649           '<reason><success/></reason>' +
    650         '</jingle>' +
    651       '</cli:iq>');
    652   this.removePlugin();
    653 };
    654 
    655 /**
    656  * @return {remoting.ClientSession.Mode} The current state.
    657  */
    658 remoting.ClientSession.prototype.getMode = function() {
    659   return this.mode_;
    660 };
    661 
    662 /**
    663  * @return {remoting.ClientSession.State} The current state.
    664  */
    665 remoting.ClientSession.prototype.getState = function() {
    666   return this.state_;
    667 };
    668 
    669 /**
    670  * @return {remoting.Error} The current error code.
    671  */
    672 remoting.ClientSession.prototype.getError = function() {
    673   return this.error_;
    674 };
    675 
    676 /**
    677  * Sends a key combination to the remoting client, by sending down events for
    678  * the given keys, followed by up events in reverse order.
    679  *
    680  * @private
    681  * @param {[number]} keys Key codes to be sent.
    682  * @return {void} Nothing.
    683  */
    684 remoting.ClientSession.prototype.sendKeyCombination_ = function(keys) {
    685   for (var i = 0; i < keys.length; i++) {
    686     this.plugin_.injectKeyEvent(keys[i], true);
    687   }
    688   for (var i = 0; i < keys.length; i++) {
    689     this.plugin_.injectKeyEvent(keys[i], false);
    690   }
    691 }
    692 
    693 /**
    694  * Sends a Ctrl-Alt-Del sequence to the remoting client.
    695  *
    696  * @return {void} Nothing.
    697  */
    698 remoting.ClientSession.prototype.sendCtrlAltDel = function() {
    699   this.sendKeyCombination_([0x0700e0, 0x0700e2, 0x07004c]);
    700 }
    701 
    702 /**
    703  * Sends a Print Screen keypress to the remoting client.
    704  *
    705  * @return {void} Nothing.
    706  */
    707 remoting.ClientSession.prototype.sendPrintScreen = function() {
    708   this.sendKeyCombination_([0x070046]);
    709 }
    710 
    711 /**
    712  * Sets and stores the key remapping setting for the current host.
    713  *
    714  * @param {string} remappings Comma separated list of key remappings.
    715  */
    716 remoting.ClientSession.prototype.setRemapKeys = function(remappings) {
    717   // Cancel any existing remappings and apply the new ones.
    718   this.applyRemapKeys_(false);
    719   this.remapKeys_ = remappings;
    720   this.applyRemapKeys_(true);
    721 
    722   // Save the new remapping setting.
    723   var options = {};
    724   options[remoting.ClientSession.KEY_REMAP_KEYS] = this.remapKeys_;
    725   remoting.HostSettings.save(this.hostId_, options);
    726 }
    727 
    728 /**
    729  * Applies the configured key remappings to the session, or resets them.
    730  *
    731  * @param {boolean} apply True to apply remappings, false to cancel them.
    732  */
    733 remoting.ClientSession.prototype.applyRemapKeys_ = function(apply) {
    734   // By default, under ChromeOS, remap the right Control key to the right
    735   // Win / Cmd key.
    736   var remapKeys = this.remapKeys_;
    737   if (remapKeys == '' && remoting.runningOnChromeOS()) {
    738     remapKeys = '0x0700e4>0x0700e7';
    739   }
    740 
    741   if (remapKeys == '') {
    742     return;
    743   }
    744 
    745   var remappings = remapKeys.split(',');
    746   for (var i = 0; i < remappings.length; ++i) {
    747     var keyCodes = remappings[i].split('>');
    748     if (keyCodes.length != 2) {
    749       console.log('bad remapKey: ' + remappings[i]);
    750       continue;
    751     }
    752     var fromKey = parseInt(keyCodes[0], 0);
    753     var toKey = parseInt(keyCodes[1], 0);
    754     if (!fromKey || !toKey) {
    755       console.log('bad remapKey code: ' + remappings[i]);
    756       continue;
    757     }
    758     if (apply) {
    759       console.log('remapKey 0x' + fromKey.toString(16) +
    760                   '>0x' + toKey.toString(16));
    761       this.plugin_.remapKey(fromKey, toKey);
    762     } else {
    763       console.log('cancel remapKey 0x' + fromKey.toString(16));
    764       this.plugin_.remapKey(fromKey, fromKey);
    765     }
    766   }
    767 }
    768 
    769 /**
    770  * Callback for the two "screen mode" related menu items: Resize desktop to
    771  * fit and Shrink to fit.
    772  *
    773  * @param {Event} event The click event indicating which mode was selected.
    774  * @return {void} Nothing.
    775  * @private
    776  */
    777 remoting.ClientSession.prototype.onSetScreenMode_ = function(event) {
    778   var shrinkToFit = this.shrinkToFit_;
    779   var resizeToClient = this.resizeToClient_;
    780   if (event.target == this.shrinkToFitButton_) {
    781     shrinkToFit = !shrinkToFit;
    782   }
    783   if (event.target == this.resizeToClientButton_) {
    784     resizeToClient = !resizeToClient;
    785   }
    786   this.setScreenMode_(shrinkToFit, resizeToClient);
    787 };
    788 
    789 /**
    790  * Set the shrink-to-fit and resize-to-client flags and save them if this is
    791  * a Me2Me connection.
    792  *
    793  * @param {boolean} shrinkToFit True if the remote desktop should be scaled
    794  *     down if it is larger than the client window; false if scroll-bars
    795  *     should be added in this case.
    796  * @param {boolean} resizeToClient True if window resizes should cause the
    797  *     host to attempt to resize its desktop to match the client window size;
    798  *     false to disable this behaviour for subsequent window resizes--the
    799  *     current host desktop size is not restored in this case.
    800  * @return {void} Nothing.
    801  * @private
    802  */
    803 remoting.ClientSession.prototype.setScreenMode_ =
    804     function(shrinkToFit, resizeToClient) {
    805   if (resizeToClient && !this.resizeToClient_) {
    806     var clientArea = this.getClientArea_();
    807     this.plugin_.notifyClientResolution(clientArea.width,
    808                                         clientArea.height,
    809                                         window.devicePixelRatio);
    810   }
    811 
    812   // If enabling shrink, reset bump-scroll offsets.
    813   var needsScrollReset = shrinkToFit && !this.shrinkToFit_;
    814 
    815   this.shrinkToFit_ = shrinkToFit;
    816   this.resizeToClient_ = resizeToClient;
    817   this.updateScrollbarVisibility();
    818 
    819   if (this.hostId_ != '') {
    820     var options = {};
    821     options[remoting.ClientSession.KEY_SHRINK_TO_FIT] = this.shrinkToFit_;
    822     options[remoting.ClientSession.KEY_RESIZE_TO_CLIENT] = this.resizeToClient_;
    823     remoting.HostSettings.save(this.hostId_, options);
    824   }
    825 
    826   this.updateDimensions();
    827   if (needsScrollReset) {
    828     this.resetScroll_();
    829   }
    830 
    831 }
    832 
    833 /**
    834  * Called when the client receives its first frame.
    835  *
    836  * @return {void} Nothing.
    837  */
    838 remoting.ClientSession.prototype.onFirstFrameReceived = function() {
    839   this.hasReceivedFrame_ = true;
    840 };
    841 
    842 /**
    843  * @return {boolean} Whether the client has received a video buffer.
    844  */
    845 remoting.ClientSession.prototype.hasReceivedFrame = function() {
    846   return this.hasReceivedFrame_;
    847 };
    848 
    849 /**
    850  * Sends an IQ stanza via the http xmpp proxy.
    851  *
    852  * @private
    853  * @param {string} msg XML string of IQ stanza to send to server.
    854  * @return {void} Nothing.
    855  */
    856 remoting.ClientSession.prototype.sendIq_ = function(msg) {
    857   // Extract the session id, so we can close the session later.
    858   var parser = new DOMParser();
    859   var iqNode = parser.parseFromString(msg, 'text/xml').firstChild;
    860   var jingleNode = iqNode.firstChild;
    861   if (jingleNode) {
    862     var action = jingleNode.getAttribute('action');
    863     if (jingleNode.nodeName == 'jingle' && action == 'session-initiate') {
    864       this.sessionId_ = jingleNode.getAttribute('sid');
    865     }
    866   }
    867 
    868   // HACK: Add 'x' prefix to the IDs of the outgoing messages to make sure that
    869   // stanza IDs used by host and client do not match. This is necessary to
    870   // workaround bug in the signaling endpoint used by chromoting.
    871   // TODO(sergeyu): Remove this hack once the server-side bug is fixed.
    872   var type = iqNode.getAttribute('type');
    873   if (type == 'set') {
    874     var id = iqNode.getAttribute('id');
    875     iqNode.setAttribute('id', 'x' + id);
    876     msg = (new XMLSerializer()).serializeToString(iqNode);
    877   }
    878 
    879   console.log(remoting.timestamp(), remoting.formatIq.prettifySendIq(msg));
    880 
    881   // Send the stanza.
    882   remoting.wcsSandbox.sendIq(msg);
    883 };
    884 
    885 remoting.ClientSession.prototype.initiateConnection_ = function() {
    886   /** @type {remoting.ClientSession} */
    887   var that = this;
    888 
    889   remoting.wcsSandbox.connect(onWcsConnected, this.resetWithError_.bind(this));
    890 
    891   /** @param {string} localJid Local JID. */
    892   function onWcsConnected(localJid) {
    893     that.connectPluginToWcs_(localJid);
    894     that.getSharedSecret_(onSharedSecretReceived.bind(null, localJid));
    895   }
    896 
    897   /** @param {string} localJid Local JID.
    898     * @param {string} sharedSecret Shared secret. */
    899   function onSharedSecretReceived(localJid, sharedSecret) {
    900     that.plugin_.connect(
    901         that.hostJid_, that.hostPublicKey_, localJid, sharedSecret,
    902         that.authenticationMethods_, that.hostId_, that.clientPairingId_,
    903         that.clientPairedSecret_);
    904   };
    905 }
    906 
    907 /**
    908  * Connects the plugin to WCS.
    909  *
    910  * @private
    911  * @param {string} localJid Local JID.
    912  * @return {void} Nothing.
    913  */
    914 remoting.ClientSession.prototype.connectPluginToWcs_ = function(localJid) {
    915   remoting.formatIq.setJids(localJid, this.hostJid_);
    916   var forwardIq = this.plugin_.onIncomingIq.bind(this.plugin_);
    917   /** @param {string} stanza The IQ stanza received. */
    918   var onIncomingIq = function(stanza) {
    919     // HACK: Remove 'x' prefix added to the id in sendIq_().
    920     try {
    921       var parser = new DOMParser();
    922       var iqNode = parser.parseFromString(stanza, 'text/xml').firstChild;
    923       var type = iqNode.getAttribute('type');
    924       var id = iqNode.getAttribute('id');
    925       if (type != 'set' && id.charAt(0) == 'x') {
    926         iqNode.setAttribute('id', id.substr(1));
    927         stanza = (new XMLSerializer()).serializeToString(iqNode);
    928       }
    929     } catch (err) {
    930       // Pass message as is when it is malformed.
    931     }
    932 
    933     console.log(remoting.timestamp(),
    934                 remoting.formatIq.prettifyReceiveIq(stanza));
    935     forwardIq(stanza);
    936   };
    937   remoting.wcsSandbox.setOnIq(onIncomingIq);
    938 }
    939 
    940 /**
    941  * Gets shared secret to be used for connection.
    942  *
    943  * @param {function(string)} callback Callback called with the shared secret.
    944  * @return {void} Nothing.
    945  * @private
    946  */
    947 remoting.ClientSession.prototype.getSharedSecret_ = function(callback) {
    948   /** @type remoting.ClientSession */
    949   var that = this;
    950   if (this.plugin_.hasFeature(remoting.ClientPlugin.Feature.THIRD_PARTY_AUTH)) {
    951     /** @type{function(string, string, string): void} */
    952     var fetchThirdPartyToken = function(tokenUrl, hostPublicKey, scope) {
    953       that.fetchThirdPartyToken_(
    954           tokenUrl, hostPublicKey, scope,
    955           that.plugin_.onThirdPartyTokenFetched.bind(that.plugin_));
    956     };
    957     this.plugin_.fetchThirdPartyTokenHandler = fetchThirdPartyToken;
    958   }
    959   if (this.accessCode_) {
    960     // Shared secret was already supplied before connecting (It2Me case).
    961     callback(this.accessCode_);
    962   } else if (this.plugin_.hasFeature(
    963       remoting.ClientPlugin.Feature.ASYNC_PIN)) {
    964     // Plugin supports asynchronously asking for the PIN.
    965     this.plugin_.useAsyncPinDialog();
    966     /** @param {boolean} pairingSupported */
    967     var fetchPin = function(pairingSupported) {
    968       that.fetchPin_(pairingSupported,
    969                      that.plugin_.onPinFetched.bind(that.plugin_));
    970     };
    971     this.plugin_.fetchPinHandler = fetchPin;
    972     callback('');
    973   } else {
    974     // Clients that don't support asking for a PIN asynchronously also don't
    975     // support pairing, so request the PIN now without offering to remember it.
    976     this.fetchPin_(false, callback);
    977   }
    978 };
    979 
    980 /**
    981  * Callback that the plugin invokes to indicate that the connection
    982  * status has changed.
    983  *
    984  * @private
    985  * @param {number} status The plugin's status.
    986  * @param {number} error The plugin's error state, if any.
    987  */
    988 remoting.ClientSession.prototype.onConnectionStatusUpdate_ =
    989     function(status, error) {
    990   if (status == remoting.ClientSession.State.CONNECTED) {
    991     this.setFocusHandlers_();
    992     this.onDesktopSizeChanged_();
    993     if (this.resizeToClient_) {
    994       var clientArea = this.getClientArea_();
    995       this.plugin_.notifyClientResolution(clientArea.width,
    996                                           clientArea.height,
    997                                           window.devicePixelRatio);
    998     }
    999     // Activate full-screen related UX.
   1000     remoting.fullscreen.addListener(this.callOnFullScreenChanged_);
   1001     remoting.fullscreen.syncWithMaximize(true);
   1002     if (remoting.windowFrame) {
   1003       remoting.windowFrame.setConnected(true);
   1004     }
   1005 
   1006     this.container_.addEventListener('mousemove',
   1007                                      this.updateMouseCursorPosition_,
   1008                                      true);
   1009 
   1010   } else if (status == remoting.ClientSession.State.FAILED) {
   1011     switch (error) {
   1012       case remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE:
   1013         this.error_ = remoting.Error.HOST_IS_OFFLINE;
   1014         break;
   1015       case remoting.ClientSession.ConnectionError.SESSION_REJECTED:
   1016         this.error_ = remoting.Error.INVALID_ACCESS_CODE;
   1017         break;
   1018       case remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL:
   1019         this.error_ = remoting.Error.INCOMPATIBLE_PROTOCOL;
   1020         break;
   1021       case remoting.ClientSession.ConnectionError.NETWORK_FAILURE:
   1022         this.error_ = remoting.Error.P2P_FAILURE;
   1023         break;
   1024       case remoting.ClientSession.ConnectionError.HOST_OVERLOAD:
   1025         this.error_ = remoting.Error.HOST_OVERLOAD;
   1026         break;
   1027       default:
   1028         this.error_ = remoting.Error.UNEXPECTED;
   1029     }
   1030   }
   1031   this.setState_(/** @type {remoting.ClientSession.State} */ (status));
   1032 };
   1033 
   1034 /**
   1035  * Callback that the plugin invokes to indicate when the connection is
   1036  * ready.
   1037  *
   1038  * @private
   1039  * @param {boolean} ready True if the connection is ready.
   1040  */
   1041 remoting.ClientSession.prototype.onConnectionReady_ = function(ready) {
   1042   var container = /** @type {HTMLMediaElement} */(
   1043       document.getElementById('video-container'));
   1044   if (!ready) {
   1045     container.classList.add('session-client-inactive');
   1046   } else {
   1047     container.classList.remove('session-client-inactive');
   1048   }
   1049 
   1050   this.raiseEvent(remoting.ClientSession.Events.videoChannelStateChanged,
   1051                   ready);
   1052 };
   1053 
   1054 /**
   1055  * Called when the client-host capabilities negotiation is complete.
   1056  *
   1057  * @param {!Array.<string>} capabilities The set of capabilities negotiated
   1058  *     between the client and host.
   1059  * @return {void} Nothing.
   1060  * @private
   1061  */
   1062 remoting.ClientSession.prototype.onSetCapabilities_ = function(capabilities) {
   1063   if (this.capabilities_ != null) {
   1064     console.error('onSetCapabilities_() is called more than once');
   1065     return;
   1066   }
   1067 
   1068   this.capabilities_ = capabilities;
   1069   if (this.hasCapability_(
   1070       remoting.ClientSession.Capability.SEND_INITIAL_RESOLUTION)) {
   1071     var clientArea = this.getClientArea_();
   1072     this.plugin_.notifyClientResolution(clientArea.width,
   1073                                         clientArea.height,
   1074                                         window.devicePixelRatio);
   1075   }
   1076 };
   1077 
   1078 /**
   1079  * @private
   1080  * @param {remoting.ClientSession.State} newState The new state for the session.
   1081  * @return {void} Nothing.
   1082  */
   1083 remoting.ClientSession.prototype.setState_ = function(newState) {
   1084   var oldState = this.state_;
   1085   this.state_ = newState;
   1086   var state = this.state_;
   1087   if (oldState == remoting.ClientSession.State.CONNECTING) {
   1088     if (this.state_ == remoting.ClientSession.State.CLOSED) {
   1089       state = remoting.ClientSession.State.CONNECTION_CANCELED;
   1090     } else if (this.state_ == remoting.ClientSession.State.FAILED &&
   1091         this.error_ == remoting.Error.HOST_IS_OFFLINE &&
   1092         !this.logHostOfflineErrors_) {
   1093       // The application requested host-offline errors to be suppressed, for
   1094       // example, because this connection attempt is using a cached host JID.
   1095       console.log('Suppressing host-offline error.');
   1096       state = remoting.ClientSession.State.CONNECTION_CANCELED;
   1097     }
   1098   } else if (oldState == remoting.ClientSession.State.CONNECTED &&
   1099              this.state_ == remoting.ClientSession.State.FAILED) {
   1100     state = remoting.ClientSession.State.CONNECTION_DROPPED;
   1101   }
   1102   this.logToServer.logClientSessionStateChange(state, this.error_, this.mode_);
   1103   if (this.state_ == remoting.ClientSession.State.CONNECTED) {
   1104     this.createGnubbyAuthHandler_();
   1105   }
   1106 
   1107   this.raiseEvent(remoting.ClientSession.Events.stateChanged,
   1108     new remoting.ClientSession.StateEvent(newState, oldState)
   1109   );
   1110 };
   1111 
   1112 /**
   1113  * This is a callback that gets called when the window is resized.
   1114  *
   1115  * @return {void} Nothing.
   1116  */
   1117 remoting.ClientSession.prototype.onResize = function() {
   1118   this.updateDimensions();
   1119 
   1120   if (this.notifyClientResolutionTimer_) {
   1121     window.clearTimeout(this.notifyClientResolutionTimer_);
   1122     this.notifyClientResolutionTimer_ = null;
   1123   }
   1124 
   1125   // Defer notifying the host of the change until the window stops resizing, to
   1126   // avoid overloading the control channel with notifications.
   1127   if (this.resizeToClient_) {
   1128     var kResizeRateLimitMs = 1000;
   1129     if (this.hasCapability_(
   1130         remoting.ClientSession.Capability.RATE_LIMIT_RESIZE_REQUESTS)) {
   1131       kResizeRateLimitMs = 250;
   1132     }
   1133     var clientArea = this.getClientArea_();
   1134     this.notifyClientResolutionTimer_ = window.setTimeout(
   1135         this.plugin_.notifyClientResolution.bind(this.plugin_,
   1136                                                  clientArea.width,
   1137                                                  clientArea.height,
   1138                                                  window.devicePixelRatio),
   1139         kResizeRateLimitMs);
   1140   }
   1141 
   1142   // If bump-scrolling is enabled, adjust the plugin margins to fully utilize
   1143   // the new window area.
   1144   this.resetScroll_();
   1145 
   1146   this.updateScrollbarVisibility();
   1147 };
   1148 
   1149 /**
   1150  * Requests that the host pause or resume video updates.
   1151  *
   1152  * @param {boolean} pause True to pause video, false to resume.
   1153  * @return {void} Nothing.
   1154  */
   1155 remoting.ClientSession.prototype.pauseVideo = function(pause) {
   1156   if (this.plugin_) {
   1157     this.plugin_.pauseVideo(pause);
   1158   }
   1159 };
   1160 
   1161 /**
   1162  * Requests that the host pause or resume audio.
   1163  *
   1164  * @param {boolean} pause True to pause audio, false to resume.
   1165  * @return {void} Nothing.
   1166  */
   1167 remoting.ClientSession.prototype.pauseAudio = function(pause) {
   1168   if (this.plugin_) {
   1169     this.plugin_.pauseAudio(pause)
   1170   }
   1171 }
   1172 
   1173 /**
   1174  * This is a callback that gets called when the plugin notifies us of a change
   1175  * in the size of the remote desktop.
   1176  *
   1177  * @private
   1178  * @return {void} Nothing.
   1179  */
   1180 remoting.ClientSession.prototype.onDesktopSizeChanged_ = function() {
   1181   console.log('desktop size changed: ' +
   1182               this.plugin_.desktopWidth + 'x' +
   1183               this.plugin_.desktopHeight +' @ ' +
   1184               this.plugin_.desktopXDpi + 'x' +
   1185               this.plugin_.desktopYDpi + ' DPI');
   1186   this.updateDimensions();
   1187   this.updateScrollbarVisibility();
   1188 };
   1189 
   1190 /**
   1191  * Refreshes the plugin's dimensions, taking into account the sizes of the
   1192  * remote desktop and client window, and the current scale-to-fit setting.
   1193  *
   1194  * @return {void} Nothing.
   1195  */
   1196 remoting.ClientSession.prototype.updateDimensions = function() {
   1197   if (this.plugin_.desktopWidth == 0 ||
   1198       this.plugin_.desktopHeight == 0) {
   1199     return;
   1200   }
   1201 
   1202   var clientArea = this.getClientArea_();
   1203   var desktopWidth = this.plugin_.desktopWidth;
   1204   var desktopHeight = this.plugin_.desktopHeight;
   1205 
   1206   // When configured to display a host at its original size, we aim to display
   1207   // it as close to its physical size as possible, without losing data:
   1208   // - If client and host have matching DPI, render the host pixel-for-pixel.
   1209   // - If the host has higher DPI then still render pixel-for-pixel.
   1210   // - If the host has lower DPI then let Chrome up-scale it to natural size.
   1211 
   1212   // We specify the plugin dimensions in Density-Independent Pixels, so to
   1213   // render pixel-for-pixel we need to down-scale the host dimensions by the
   1214   // devicePixelRatio of the client. To match the host pixel density, we choose
   1215   // an initial scale factor based on the client devicePixelRatio and host DPI.
   1216 
   1217   // Determine the effective device pixel ratio of the host, based on DPI.
   1218   var hostPixelRatioX = Math.ceil(this.plugin_.desktopXDpi / 96);
   1219   var hostPixelRatioY = Math.ceil(this.plugin_.desktopYDpi / 96);
   1220   var hostPixelRatio = Math.min(hostPixelRatioX, hostPixelRatioY);
   1221 
   1222   // Down-scale by the smaller of the client and host ratios.
   1223   var scale = 1.0 / Math.min(window.devicePixelRatio, hostPixelRatio);
   1224 
   1225   if (this.shrinkToFit_) {
   1226     // Reduce the scale, if necessary, to fit the whole desktop in the window.
   1227     var scaleFitWidth = Math.min(scale, 1.0 * clientArea.width / desktopWidth);
   1228     var scaleFitHeight =
   1229         Math.min(scale, 1.0 * clientArea.height / desktopHeight);
   1230     scale = Math.min(scaleFitHeight, scaleFitWidth);
   1231 
   1232     // If we're running full-screen then try to handle common side-by-side
   1233     // multi-monitor combinations more intelligently.
   1234     if (remoting.fullscreen.isActive()) {
   1235       // If the host has two monitors each the same size as the client then
   1236       // scale-to-fit will have the desktop occupy only 50% of the client area,
   1237       // in which case it would be preferable to down-scale less and let the
   1238       // user bump-scroll around ("scale-and-pan").
   1239       // Triggering scale-and-pan if less than 65% of the client area would be
   1240       // used adds enough fuzz to cope with e.g. 1280x800 client connecting to
   1241       // a (2x1280)x1024 host nicely.
   1242       // Note that we don't need to account for scrollbars while fullscreen.
   1243       if (scale <= scaleFitHeight * 0.65) {
   1244         scale = scaleFitHeight;
   1245       }
   1246       if (scale <= scaleFitWidth * 0.65) {
   1247         scale = scaleFitWidth;
   1248       }
   1249     }
   1250   }
   1251 
   1252   var pluginWidth = Math.round(desktopWidth * scale);
   1253   var pluginHeight = Math.round(desktopHeight * scale);
   1254 
   1255   if (this.video_) {
   1256     this.video_.style.width = pluginWidth + 'px';
   1257     this.video_.style.height = pluginHeight + 'px';
   1258   }
   1259 
   1260   // Resize the plugin if necessary.
   1261   // TODO(wez): Handle high-DPI to high-DPI properly (crbug.com/135089).
   1262   this.plugin_.element().style.width = pluginWidth + 'px';
   1263   this.plugin_.element().style.height = pluginHeight + 'px';
   1264 
   1265   // Position the container.
   1266   // Note that clientWidth/Height take into account scrollbars.
   1267   var clientWidth = document.documentElement.clientWidth;
   1268   var clientHeight = document.documentElement.clientHeight;
   1269   var parentNode = this.plugin_.element().parentNode;
   1270 
   1271   console.log('plugin dimensions: ' +
   1272               parentNode.style.left + ',' +
   1273               parentNode.style.top + '-' +
   1274               pluginWidth + 'x' + pluginHeight + '.');
   1275 };
   1276 
   1277 /**
   1278  * Returns an associative array with a set of stats for this connection.
   1279  *
   1280  * @return {remoting.ClientSession.PerfStats} The connection statistics.
   1281  */
   1282 remoting.ClientSession.prototype.getPerfStats = function() {
   1283   return this.plugin_.getPerfStats();
   1284 };
   1285 
   1286 /**
   1287  * Logs statistics.
   1288  *
   1289  * @param {remoting.ClientSession.PerfStats} stats
   1290  */
   1291 remoting.ClientSession.prototype.logStatistics = function(stats) {
   1292   this.logToServer.logStatistics(stats, this.mode_);
   1293 };
   1294 
   1295 /**
   1296  * Enable or disable logging of connection errors due to a host being offline.
   1297  * For example, if attempting a connection using a cached JID, host-offline
   1298  * errors should not be logged because the JID will be refreshed and the
   1299  * connection retried.
   1300  *
   1301  * @param {boolean} enable True to log host-offline errors; false to suppress.
   1302  */
   1303 remoting.ClientSession.prototype.logHostOfflineErrors = function(enable) {
   1304   this.logHostOfflineErrors_ = enable;
   1305 };
   1306 
   1307 /**
   1308  * Request pairing with the host for PIN-less authentication.
   1309  *
   1310  * @param {string} clientName The human-readable name of the client.
   1311  * @param {function(string, string):void} onDone Callback to receive the
   1312  *     client id and shared secret when they are available.
   1313  */
   1314 remoting.ClientSession.prototype.requestPairing = function(clientName, onDone) {
   1315   if (this.plugin_) {
   1316     this.plugin_.requestPairing(clientName, onDone);
   1317   }
   1318 };
   1319 
   1320 /**
   1321  * Called when the full-screen status has changed, either via the
   1322  * remoting.Fullscreen class, or via a system event such as the Escape key
   1323  *
   1324  * @param {boolean} fullscreen True if the app is entering full-screen mode;
   1325  *     false if it is leaving it.
   1326  * @private
   1327  */
   1328 remoting.ClientSession.prototype.onFullScreenChanged_ = function (fullscreen) {
   1329   var htmlNode = /** @type {HTMLElement} */ (document.documentElement);
   1330   this.enableBumpScroll_(fullscreen);
   1331   if (fullscreen) {
   1332     htmlNode.classList.add('full-screen');
   1333   } else {
   1334     htmlNode.classList.remove('full-screen');
   1335   }
   1336 };
   1337 
   1338 /**
   1339  * Updates the options menu to reflect the current scale-to-fit and full-screen
   1340  * settings.
   1341  * @return {void} Nothing.
   1342  * @private
   1343  */
   1344 remoting.ClientSession.prototype.onShowOptionsMenu_ = function() {
   1345   remoting.MenuButton.select(this.resizeToClientButton_, this.resizeToClient_);
   1346   remoting.MenuButton.select(this.shrinkToFitButton_, this.shrinkToFit_);
   1347   remoting.MenuButton.select(this.fullScreenButton_,
   1348                              remoting.fullscreen.isActive());
   1349 };
   1350 
   1351 /**
   1352  * Scroll the client plugin by the specified amount, keeping it visible.
   1353  * Note that this is only used in content full-screen mode (not windowed or
   1354  * browser full-screen modes), where window.scrollBy and the scrollTop and
   1355  * scrollLeft properties don't work.
   1356  * @param {number} dx The amount by which to scroll horizontally. Positive to
   1357  *     scroll right; negative to scroll left.
   1358  * @param {number} dy The amount by which to scroll vertically. Positive to
   1359  *     scroll down; negative to scroll up.
   1360  * @return {boolean} True if the requested scroll had no effect because both
   1361  *     vertical and horizontal edges of the screen have been reached.
   1362  * @private
   1363  */
   1364 remoting.ClientSession.prototype.scroll_ = function(dx, dy) {
   1365   var plugin = this.plugin_.element();
   1366   var style = plugin.style;
   1367 
   1368   /**
   1369    * Helper function for x- and y-scrolling
   1370    * @param {number|string} curr The current margin, eg. "10px".
   1371    * @param {number} delta The requested scroll amount.
   1372    * @param {number} windowBound The size of the window, in pixels.
   1373    * @param {number} pluginBound The size of the plugin, in pixels.
   1374    * @param {{stop: boolean}} stop Reference parameter used to indicate when
   1375    *     the scroll has reached one of the edges and can be stopped in that
   1376    *     direction.
   1377    * @return {string} The new margin value.
   1378    */
   1379   var adjustMargin = function(curr, delta, windowBound, pluginBound, stop) {
   1380     var minMargin = Math.min(0, windowBound - pluginBound);
   1381     var result = (curr ? parseFloat(curr) : 0) - delta;
   1382     result = Math.min(0, Math.max(minMargin, result));
   1383     stop.stop = (result == 0 || result == minMargin);
   1384     return result + 'px';
   1385   };
   1386 
   1387   var stopX = { stop: false };
   1388   var clientArea = this.getClientArea_();
   1389   style.marginLeft = adjustMargin(style.marginLeft, dx,
   1390                                   clientArea.width, plugin.clientWidth, stopX);
   1391 
   1392   var stopY = { stop: false };
   1393   style.marginTop = adjustMargin(
   1394       style.marginTop, dy, clientArea.height, plugin.clientHeight, stopY);
   1395   return stopX.stop && stopY.stop;
   1396 };
   1397 
   1398 remoting.ClientSession.prototype.resetScroll_ = function() {
   1399   if (this.plugin_) {
   1400     var plugin = this.plugin_.element();
   1401     plugin.style.marginTop = '0px';
   1402     plugin.style.marginLeft = '0px';
   1403   }
   1404 };
   1405 
   1406 /**
   1407  * Enable or disable bump-scrolling. When disabling bump scrolling, also reset
   1408  * the scroll offsets to (0, 0).
   1409  * @private
   1410  * @param {boolean} enable True to enable bump-scrolling, false to disable it.
   1411  */
   1412 remoting.ClientSession.prototype.enableBumpScroll_ = function(enable) {
   1413   var element = /*@type{HTMLElement} */ document.documentElement;
   1414   if (enable) {
   1415     /** @type {null|function(Event):void} */
   1416     this.onMouseMoveRef_ = this.onMouseMove_.bind(this);
   1417     element.addEventListener('mousemove', this.onMouseMoveRef_, false);
   1418   } else {
   1419     element.removeEventListener('mousemove', this.onMouseMoveRef_, false);
   1420     this.onMouseMoveRef_ = null;
   1421     this.resetScroll_();
   1422   }
   1423 };
   1424 
   1425 /**
   1426  * @param {Event} event The mouse event.
   1427  * @private
   1428  */
   1429 remoting.ClientSession.prototype.onMouseMove_ = function(event) {
   1430   if (this.bumpScrollTimer_) {
   1431     window.clearTimeout(this.bumpScrollTimer_);
   1432     this.bumpScrollTimer_ = null;
   1433   }
   1434 
   1435   /**
   1436    * Compute the scroll speed based on how close the mouse is to the edge.
   1437    * @param {number} mousePos The mouse x- or y-coordinate
   1438    * @param {number} size The width or height of the content area.
   1439    * @return {number} The scroll delta, in pixels.
   1440    */
   1441   var computeDelta = function(mousePos, size) {
   1442     var threshold = 10;
   1443     if (mousePos >= size - threshold) {
   1444       return 1 + 5 * (mousePos - (size - threshold)) / threshold;
   1445     } else if (mousePos <= threshold) {
   1446       return -1 - 5 * (threshold - mousePos) / threshold;
   1447     }
   1448     return 0;
   1449   };
   1450 
   1451   var clientArea = this.getClientArea_();
   1452   var dx = computeDelta(event.x, clientArea.width);
   1453   var dy = computeDelta(event.y, clientArea.height);
   1454 
   1455   if (dx != 0 || dy != 0) {
   1456     /** @type {remoting.ClientSession} */
   1457     var that = this;
   1458     /**
   1459      * Scroll the view, and schedule a timer to do so again unless we've hit
   1460      * the edges of the screen. This timer is cancelled when the mouse moves.
   1461      * @param {number} expected The time at which we expect to be called.
   1462      */
   1463     var repeatScroll = function(expected) {
   1464       /** @type {number} */
   1465       var now = new Date().getTime();
   1466       /** @type {number} */
   1467       var timeout = 10;
   1468       var lateAdjustment = 1 + (now - expected) / timeout;
   1469       if (!that.scroll_(lateAdjustment * dx, lateAdjustment * dy)) {
   1470         that.bumpScrollTimer_ = window.setTimeout(
   1471             function() { repeatScroll(now + timeout); },
   1472             timeout);
   1473       }
   1474     };
   1475     repeatScroll(new Date().getTime());
   1476   }
   1477 };
   1478 
   1479 /**
   1480  * Sends a clipboard item to the host.
   1481  *
   1482  * @param {string} mimeType The MIME type of the clipboard item.
   1483  * @param {string} item The clipboard item.
   1484  */
   1485 remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
   1486   if (!this.plugin_)
   1487     return;
   1488   this.plugin_.sendClipboardItem(mimeType, item);
   1489 };
   1490 
   1491 /**
   1492  * Send a gnubby-auth extension message to the host.
   1493  * @param {Object} data The gnubby-auth message data.
   1494  */
   1495 remoting.ClientSession.prototype.sendGnubbyAuthMessage = function(data) {
   1496   if (!this.plugin_)
   1497     return;
   1498   this.plugin_.sendClientMessage('gnubby-auth', JSON.stringify(data));
   1499 };
   1500 
   1501 /**
   1502  * Process a remote gnubby auth request.
   1503  * @param {string} data Remote gnubby request data.
   1504  * @private
   1505  */
   1506 remoting.ClientSession.prototype.processGnubbyAuthMessage_ = function(data) {
   1507   if (this.gnubbyAuthHandler_) {
   1508     try {
   1509       this.gnubbyAuthHandler_.onMessage(data);
   1510     } catch (err) {
   1511       console.error('Failed to process gnubby message: ',
   1512           /** @type {*} */ (err));
   1513     }
   1514   } else {
   1515     console.error('Received unexpected gnubby message');
   1516   }
   1517 };
   1518 
   1519 /**
   1520  * Create a gnubby auth handler and inform the host that gnubby auth is
   1521  * supported.
   1522  * @private
   1523  */
   1524 remoting.ClientSession.prototype.createGnubbyAuthHandler_ = function() {
   1525   if (this.mode_ == remoting.ClientSession.Mode.ME2ME) {
   1526     this.gnubbyAuthHandler_ = new remoting.GnubbyAuthHandler(this);
   1527     // TODO(psj): Move to more generic capabilities mechanism.
   1528     this.sendGnubbyAuthMessage({'type': 'control', 'option': 'auth-v1'});
   1529   }
   1530 };
   1531 
   1532 /**
   1533  * @return {{width: number, height: number}} The height of the window's client
   1534  *     area. This differs between apps v1 and apps v2 due to the custom window
   1535  *     borders used by the latter.
   1536  * @private
   1537  */
   1538 remoting.ClientSession.prototype.getClientArea_ = function() {
   1539   return remoting.windowFrame ?
   1540       remoting.windowFrame.getClientArea() :
   1541       { 'width': window.innerWidth, 'height': window.innerHeight };
   1542 };
   1543 
   1544 /**
   1545  * @param {string} url
   1546  * @param {number} hotspotX
   1547  * @param {number} hotspotY
   1548  */
   1549 remoting.ClientSession.prototype.updateMouseCursorImage_ =
   1550     function(url, hotspotX, hotspotY) {
   1551   this.mouseCursorOverlay_.hidden = !url;
   1552   if (url) {
   1553     this.mouseCursorOverlay_.style.marginLeft = '-' + hotspotX + 'px';
   1554     this.mouseCursorOverlay_.style.marginTop = '-' + hotspotY + 'px';
   1555     this.mouseCursorOverlay_.src = url;
   1556   }
   1557  };
   1558