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