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