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