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