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