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