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