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