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