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