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