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