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