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