• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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} */
23var 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 */
48remoting.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 */
155remoting.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 */
166remoting.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} */
202remoting.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} */
215remoting.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} */
227remoting.ClientSession.Mode = {
228  IT2ME: 0,
229  ME2ME: 1
230};
231
232/**
233 * Type used for performance statistics collected by the plugin.
234 * @constructor
235 */
236remoting.ClientSession.PerfStats = function() {};
237/** @type {number} */
238remoting.ClientSession.PerfStats.prototype.videoBandwidth;
239/** @type {number} */
240remoting.ClientSession.PerfStats.prototype.videoFrameRate;
241/** @type {number} */
242remoting.ClientSession.PerfStats.prototype.captureLatency;
243/** @type {number} */
244remoting.ClientSession.PerfStats.prototype.encodeLatency;
245/** @type {number} */
246remoting.ClientSession.PerfStats.prototype.decodeLatency;
247/** @type {number} */
248remoting.ClientSession.PerfStats.prototype.renderLatency;
249/** @type {number} */
250remoting.ClientSession.PerfStats.prototype.roundtripLatency;
251
252// Keys for connection statistics.
253remoting.ClientSession.STATS_KEY_VIDEO_BANDWIDTH = 'videoBandwidth';
254remoting.ClientSession.STATS_KEY_VIDEO_FRAME_RATE = 'videoFrameRate';
255remoting.ClientSession.STATS_KEY_CAPTURE_LATENCY = 'captureLatency';
256remoting.ClientSession.STATS_KEY_ENCODE_LATENCY = 'encodeLatency';
257remoting.ClientSession.STATS_KEY_DECODE_LATENCY = 'decodeLatency';
258remoting.ClientSession.STATS_KEY_RENDER_LATENCY = 'renderLatency';
259remoting.ClientSession.STATS_KEY_ROUNDTRIP_LATENCY = 'roundtripLatency';
260
261// Keys for per-host settings.
262remoting.ClientSession.KEY_REMAP_KEYS = 'remapKeys';
263remoting.ClientSession.KEY_RESIZE_TO_CLIENT = 'resizeToClient';
264remoting.ClientSession.KEY_SHRINK_TO_FIT = 'shrinkToFit';
265
266/**
267 * The id of the client plugin
268 *
269 * @const
270 */
271remoting.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 */
278remoting.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 */
291remoting.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 */
300remoting.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 */
313remoting.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 */
331remoting.ClientSession.prototype.pluginGotFocus_ = function() {
332  remoting.clipboard.initiateToHost();
333};
334
335/**
336 * Callback function called when the plugin element loses focus.
337 */
338remoting.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 */
354remoting.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 */
366remoting.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 */
394remoting.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 */
405remoting.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 */
415remoting.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 */
468remoting.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 */
497remoting.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 */
524remoting.ClientSession.prototype.getMode = function() {
525  return this.mode_;
526};
527
528/**
529 * @return {remoting.ClientSession.State} The current state.
530 */
531remoting.ClientSession.prototype.getState = function() {
532  return this.state_;
533};
534
535/**
536 * @return {remoting.Error} The current error code.
537 */
538remoting.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 */
550remoting.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 */
564remoting.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 */
573remoting.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 */
582remoting.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 */
599remoting.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 */
639remoting.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 */
665remoting.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 */
699remoting.ClientSession.prototype.onFirstFrameReceived = function() {
700  this.hasReceivedFrame_ = true;
701};
702
703/**
704 * @return {boolean} Whether the client has received a video buffer.
705 */
706remoting.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 */
717remoting.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
746remoting.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 */
775remoting.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 */
808remoting.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 */
849remoting.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 */
890remoting.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 */
906remoting.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 */
926remoting.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 */
956remoting.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 */
993remoting.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 */
1005remoting.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 */
1018remoting.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 */
1034remoting.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 */
1127remoting.ClientSession.prototype.getPerfStats = function() {
1128  return this.plugin_.getPerfStats();
1129};
1130
1131/**
1132 * Logs statistics.
1133 *
1134 * @param {remoting.ClientSession.PerfStats} stats
1135 */
1136remoting.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 */
1148remoting.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 */
1159remoting.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 */
1170remoting.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 */
1189remoting.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 */
1209remoting.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 */
1247remoting.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 */
1266remoting.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 */
1326remoting.ClientSession.prototype.sendClipboardItem = function(mimeType, item) {
1327  if (!this.plugin_)
1328    return;
1329  this.plugin_.sendClipboardItem(mimeType, item)
1330};
1331
1332