Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
|
Jamie
2011/10/27 20:41:36
The diffs are not very useful for this file. Basic
garykac
2011/10/27 22:32:39
OK
| |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 'use strict'; | 5 'use strict'; |
| 6 | 6 |
| 7 /** @suppress {duplicate} */ | 7 /** @suppress {duplicate} */ |
| 8 var remoting = remoting || {}; | 8 var remoting = remoting || {}; |
| 9 | 9 |
| 10 /** | 10 /** @type {remoting.HostSession} */ remoting.hostSession = null; |
| 11 * Whether or not the plugin should scale itself. | |
| 12 * @type {boolean} | |
| 13 */ | |
| 14 remoting.scaleToFit = false; | |
| 15 | |
| 16 /** @type {remoting.ClientSession} */ | |
| 17 remoting.session = null; | |
| 18 | |
| 19 /** @type {string} */ remoting.accessCode = ''; | |
| 20 /** @type {number} */ remoting.accessCodeTimerId = 0; | |
| 21 /** @type {number} */ remoting.accessCodeExpiresIn = 0; | |
| 22 /** @type {remoting.AppMode} */ remoting.currentMode; | |
| 23 /** @type {string} */ remoting.hostJid = ''; | |
| 24 /** @type {string} */ remoting.hostPublicKey = ''; | |
| 25 /** @type {boolean} */ remoting.lastShareWasCancelled = false; | |
| 26 /** @type {boolean} */ remoting.timerRunning = false; | |
| 27 /** @type {string} */ remoting.username = ''; | |
| 28 | |
| 29 /** @enum {string} */ | |
| 30 remoting.AppMode = { | |
| 31 HOME: 'home', | |
| 32 UNAUTHENTICATED: 'auth', | |
| 33 CLIENT: 'client', | |
| 34 CLIENT_UNCONNECTED: 'client.unconnected', | |
| 35 CLIENT_CONNECTING: 'client.connecting', | |
| 36 CLIENT_CONNECT_FAILED: 'client.connect-failed', | |
| 37 CLIENT_SESSION_FINISHED: 'client.session-finished', | |
| 38 HOST: 'host', | |
| 39 HOST_WAITING_FOR_CODE: 'host.waiting-for-code', | |
| 40 HOST_WAITING_FOR_CONNECTION: 'host.waiting-for-connection', | |
| 41 HOST_SHARED: 'host.shared', | |
| 42 HOST_SHARE_FAILED: 'host.share-failed', | |
| 43 HOST_SHARE_FINISHED: 'host.share-finished', | |
| 44 IN_SESSION: 'in-session' | |
| 45 }; | |
| 46 | 11 |
| 47 (function() { | 12 (function() { |
| 48 | 13 |
| 49 window.addEventListener('blur', pluginLostFocus_, false); | |
| 50 | |
| 51 function pluginLostFocus_() { | |
| 52 // If the plug loses input focus, release all keys as a precaution against | |
| 53 // leaving them 'stuck down' on the host. | |
| 54 if (remoting.session && remoting.session.plugin) { | |
| 55 remoting.session.plugin.releaseAllKeys(); | |
| 56 } | |
| 57 } | |
| 58 | |
| 59 /** @type {string} */ | |
| 60 remoting.HOST_PLUGIN_ID = 'host-plugin-id'; | |
| 61 | |
| 62 /** @enum {string} */ | |
| 63 remoting.ClientError = { | |
| 64 NO_RESPONSE: /*i18n-content*/'ERROR_NO_RESPONSE', | |
| 65 INVALID_ACCESS_CODE: /*i18n-content*/'ERROR_INVALID_ACCESS_CODE', | |
| 66 MISSING_PLUGIN: /*i18n-content*/'ERROR_MISSING_PLUGIN', | |
| 67 OAUTH_FETCH_FAILED: /*i18n-content*/'ERROR_AUTHENTICATION_FAILED', | |
| 68 HOST_IS_OFFLINE: /*i18n-content*/'ERROR_HOST_IS_OFFLINE', | |
| 69 INCOMPATIBLE_PROTOCOL: /*i18n-content*/'ERROR_INCOMPATIBLE_PROTOCOL', | |
| 70 BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', | |
| 71 OTHER_ERROR: /*i18n-content*/'ERROR_GENERIC' | |
| 72 }; | |
| 73 | |
| 74 // Constants representing keys used for storing persistent application state. | |
| 75 var KEY_APP_MODE_ = 'remoting-app-mode'; | |
| 76 var KEY_EMAIL_ = 'remoting-email'; | |
| 77 var KEY_USE_P2P_API_ = 'remoting-use-p2p-api'; | |
| 78 | |
| 79 // Some constants for pretty-printing the access code. | |
| 80 /** @type {number} */ var kSupportIdLen = 7; | |
| 81 /** @type {number} */ var kHostSecretLen = 5; | |
| 82 /** @type {number} */ var kAccessCodeLen = kSupportIdLen + kHostSecretLen; | |
| 83 /** @type {number} */ var kDigitsPerGroup = 4; | |
| 84 | |
| 85 /** | 14 /** |
| 86 * @param {string} classes A space-separated list of classes. | 15 * Entry point for app initialization. |
| 87 * @param {string} cls The class to check for. | |
| 88 * @return {boolean} True if |cls| is found within |classes|. | |
| 89 */ | 16 */ |
| 90 function hasClass(classes, cls) { | |
| 91 return classes.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) != null; | |
| 92 } | |
| 93 | |
| 94 /** | |
| 95 * @param {Element} element The element to which to add the class. | |
| 96 * @param {string} cls The new class. | |
| 97 * @return {void} Nothing. | |
| 98 */ | |
| 99 function addClass(element, cls) { | |
| 100 if (!hasClass(element.className, cls)) { | |
| 101 var padded = element.className == '' ? '' : element.className + ' '; | |
| 102 element.className = padded + cls; | |
| 103 } | |
| 104 } | |
| 105 | |
| 106 /** | |
| 107 * @param {Element} element The element from which to remove the class. | |
| 108 * @param {string} cls The new class. | |
| 109 * @return {void} Nothing. | |
| 110 */ | |
| 111 function removeClass(element, cls) { | |
| 112 element.className = | |
| 113 element.className.replace(new RegExp('\\b' + cls + '\\b', 'g'), '') | |
| 114 .replace(' ', ' '); | |
| 115 } | |
| 116 | |
| 117 function retrieveEmail_(access_token) { | |
| 118 var headers = { | |
| 119 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
| 120 }; | |
| 121 | |
| 122 /** @param {XMLHttpRequest} xhr The XHR response. */ | |
| 123 var onResponse = function(xhr) { | |
| 124 if (xhr.status != 200) { | |
| 125 // TODO(ajwong): Have a better way of showing an error. | |
| 126 remoting.debug.log('Unable to get email'); | |
| 127 document.getElementById('current-email').innerText = '???'; | |
| 128 return; | |
| 129 } | |
| 130 | |
| 131 // TODO(ajwong): See if we can't find a JSON endpoint. | |
| 132 setEmail(xhr.responseText.split('&')[0].split('=')[1]); | |
| 133 }; | |
| 134 | |
| 135 // TODO(ajwong): Update to new v2 API. | |
| 136 remoting.xhr.get('https://www.googleapis.com/userinfo/email', | |
| 137 onResponse, '', headers); | |
| 138 } | |
| 139 | |
| 140 function refreshEmail_() { | |
| 141 if (!getEmail() && remoting.oauth2.isAuthenticated()) { | |
| 142 remoting.oauth2.callWithToken(retrieveEmail_); | |
| 143 } | |
| 144 } | |
| 145 | |
| 146 /** | |
| 147 * @param {string} value The email address to place in local storage. | |
| 148 * @return {void} Nothing. | |
| 149 */ | |
| 150 function setEmail(value) { | |
| 151 window.localStorage.setItem(KEY_EMAIL_, value); | |
| 152 document.getElementById('current-email').innerText = value; | |
| 153 } | |
| 154 | |
| 155 /** | |
| 156 * @return {?string} The email address associated with the auth credentials. | |
| 157 */ | |
| 158 function getEmail() { | |
| 159 var result = window.localStorage.getItem(KEY_EMAIL_); | |
| 160 return typeof result == 'string' ? result : null; | |
| 161 } | |
| 162 | |
| 163 function exchangedCodeForToken_() { | |
| 164 if (!remoting.oauth2.isAuthenticated()) { | |
| 165 alert('Your OAuth2 token was invalid. Please try again.'); | |
| 166 } | |
| 167 /** @param {string} token The auth token. */ | |
| 168 var retrieveEmail = function(token) { retrieveEmail_(token); } | |
| 169 remoting.oauth2.callWithToken(retrieveEmail); | |
| 170 } | |
| 171 | |
| 172 remoting.clearOAuth2 = function() { | |
| 173 remoting.oauth2.clear(); | |
| 174 window.localStorage.removeItem(KEY_EMAIL_); | |
| 175 remoting.setMode(remoting.AppMode.UNAUTHENTICATED); | |
| 176 } | |
| 177 | |
| 178 remoting.toggleDebugLog = function() { | |
| 179 var debugLog = document.getElementById('debug-log'); | |
| 180 if (debugLog.hidden) { | |
| 181 debugLog.hidden = false; | |
| 182 } else { | |
| 183 debugLog.hidden = true; | |
| 184 } | |
| 185 } | |
| 186 | |
| 187 remoting.init = function() { | 17 remoting.init = function() { |
| 188 l10n.localize(); | 18 l10n.localize(); |
| 189 var button = document.getElementById('toggle-scaling'); | 19 var button = document.getElementById('toggle-scaling'); |
| 190 button.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_SCALING'); | 20 button.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_SCALING'); |
| 191 // Create global objects. | 21 // Create global objects. |
| 192 remoting.oauth2 = new remoting.OAuth2(); | 22 remoting.oauth2 = new remoting.OAuth2(); |
| 193 remoting.debug = | 23 remoting.debug = new remoting.DebugLog( |
| 194 new remoting.DebugLog(document.getElementById('debug-messages')); | 24 document.getElementById('debug-messages'), |
| 195 /** @type {XMLHttpRequest} */ | 25 document.getElementById('statistics')); |
| 196 remoting.supportHostsXhr = null; | |
| 197 | 26 |
| 198 refreshEmail_(); | 27 refreshEmail_(); |
| 199 var email = getEmail(); | 28 var email = remoting.oauth2.getCachedEmail(); |
| 200 if (email) { | 29 if (email) { |
| 201 document.getElementById('current-email').innerText = email; | 30 document.getElementById('current-email').innerText = email; |
| 202 } | 31 } |
| 203 | 32 |
| 204 remoting.setMode(getAppStartupMode()); | 33 remoting.setMode(getAppStartupMode_()); |
| 205 if (isHostModeSupported()) { | 34 if (isHostModeSupported_()) { |
| 206 var unsupported = document.getElementById('client-footer-text-cros'); | 35 var unsupported = document.getElementById('client-footer-text-cros'); |
| 207 unsupported.parentNode.removeChild(unsupported); | 36 unsupported.parentNode.removeChild(unsupported); |
| 208 } else { | 37 } else { |
| 209 var footer = document.getElementById('client-footer-text'); | 38 var footer = document.getElementById('client-footer-text'); |
| 210 footer.parentNode.removeChild(footer); | 39 footer.parentNode.removeChild(footer); |
| 211 document.getElementById('client-footer-text-cros').id = | 40 document.getElementById('client-footer-text-cros').id = |
| 212 'client-footer-text'; | 41 'client-footer-text'; |
| 213 } | 42 } |
| 214 } | |
| 215 | 43 |
| 216 /** | 44 window.addEventListener('blur', pluginLostFocus_, false); |
| 217 * Change the app's modal state to |mode|, which is considered to be a dotted | |
| 218 * hierachy of modes. For example, setMode('host.shared') will show any modal | |
| 219 * elements with an data-ui-mode attribute of 'host' or 'host.shared' and hide | |
| 220 * all others. | |
| 221 * | |
| 222 * @param {remoting.AppMode} mode The new modal state, expressed as a dotted | |
| 223 * hiearchy. | |
| 224 */ | |
| 225 remoting.setMode = function(mode) { | |
| 226 var modes = mode.split('.'); | |
| 227 for (var i = 1; i < modes.length; ++i) | |
| 228 modes[i] = modes[i - 1] + '.' + modes[i]; | |
| 229 var elements = document.querySelectorAll('[data-ui-mode]'); | |
| 230 for (var i = 0; i < elements.length; ++i) { | |
| 231 /** @type {Element} */ var element = elements[i]; | |
| 232 var hidden = true; | |
| 233 for (var m = 0; m < modes.length; ++m) { | |
| 234 if (hasClass(element.getAttribute('data-ui-mode'), modes[m])) { | |
| 235 hidden = false; | |
| 236 break; | |
| 237 } | |
| 238 } | |
| 239 element.hidden = hidden; | |
| 240 } | |
| 241 remoting.debug.log('App mode: ' + mode); | |
| 242 remoting.currentMode = mode; | |
| 243 if (mode == remoting.AppMode.IN_SESSION) { | |
| 244 document.removeEventListener('keydown', remoting.checkHotkeys, false); | |
| 245 } else { | |
| 246 document.addEventListener('keydown', remoting.checkHotkeys, false); | |
| 247 } | |
| 248 } | |
| 249 | |
| 250 /** | |
| 251 * Get the major mode that the app is running in. | |
| 252 * @return {string} The app's current major mode. | |
| 253 */ | |
| 254 remoting.getMajorMode = function() { | |
| 255 return remoting.currentMode.split('.')[0]; | |
| 256 } | |
| 257 | |
| 258 remoting.tryShare = function() { | |
| 259 remoting.debug.log('Attempting to share...'); | |
| 260 remoting.lastShareWasCancelled = false; | |
| 261 if (remoting.oauth2.needsNewAccessToken()) { | |
| 262 remoting.debug.log('Refreshing token...'); | |
| 263 remoting.oauth2.refreshAccessToken(function() { | |
| 264 if (remoting.oauth2.needsNewAccessToken()) { | |
| 265 // If we still need it, we're going to infinite loop. | |
| 266 showShareError_(/*i18n-content*/'ERROR_AUTHENTICATION_FAILED'); | |
| 267 throw 'Unable to get access token'; | |
| 268 } | |
| 269 remoting.tryShare(); | |
| 270 }); | |
| 271 return; | |
| 272 } | |
| 273 | |
| 274 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); | |
| 275 document.getElementById('cancel-button').disabled = false; | |
| 276 disableTimeoutCountdown_(); | |
| 277 | |
| 278 var div = document.getElementById('host-plugin-container'); | |
| 279 var plugin = /** @type {remoting.HostPlugin} */ | |
| 280 document.createElement('embed'); | |
| 281 plugin.type = remoting.PLUGIN_MIMETYPE; | |
| 282 plugin.id = remoting.HOST_PLUGIN_ID; | |
| 283 // Hiding the plugin means it doesn't load, so make it size zero instead. | |
| 284 plugin.width = 0; | |
| 285 plugin.height = 0; | |
| 286 div.appendChild(plugin); | |
| 287 onNatTraversalPolicyChanged_(true); // Hide warning by default. | |
| 288 plugin.onNatTraversalPolicyChanged = onNatTraversalPolicyChanged_; | |
| 289 plugin.onStateChanged = onStateChanged_; | |
| 290 plugin.logDebugInfo = debugInfoCallback_; | |
| 291 plugin.localize(chrome.i18n.getMessage); | |
| 292 plugin.connect(/** @type {string} */ (getEmail()), | |
| 293 'oauth2:' + remoting.oauth2.getAccessToken()); | |
| 294 } | |
| 295 | |
| 296 function disableTimeoutCountdown_() { | |
| 297 if (remoting.timerRunning) { | |
| 298 clearInterval(remoting.accessCodeTimerId); | |
| 299 remoting.timerRunning = false; | |
| 300 updateTimeoutStyles_(); | |
| 301 } | |
| 302 } | |
| 303 | |
| 304 var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD = 30; | |
| 305 var ACCESS_CODE_RED_THRESHOLD = 10; | |
| 306 | |
| 307 /** | |
| 308 * Show/hide or restyle various elements, depending on the remaining countdown | |
| 309 * and timer state. | |
| 310 * | |
| 311 * @return {boolean} True if the timeout is in progress, false if it has | |
| 312 * expired. | |
| 313 */ | |
| 314 function updateTimeoutStyles_() { | |
| 315 if (remoting.timerRunning) { | |
| 316 if (remoting.accessCodeExpiresIn <= 0) { | |
| 317 remoting.cancelShare(); | |
| 318 return false; | |
| 319 } | |
| 320 if (remoting.accessCodeExpiresIn <= ACCESS_CODE_RED_THRESHOLD) { | |
| 321 addClass(document.getElementById('access-code-display'), 'expiring'); | |
| 322 } else { | |
| 323 removeClass(document.getElementById('access-code-display'), 'expiring'); | |
| 324 } | |
| 325 } | |
| 326 document.getElementById('access-code-countdown').hidden = | |
| 327 (remoting.accessCodeExpiresIn > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD) || | |
| 328 !remoting.timerRunning; | |
| 329 return true; | |
| 330 } | |
| 331 | |
| 332 remoting.decrementAccessCodeTimeout_ = function() { | |
| 333 --remoting.accessCodeExpiresIn; | |
| 334 remoting.updateAccessCodeTimeoutElement_(); | |
| 335 } | |
| 336 | |
| 337 remoting.updateAccessCodeTimeoutElement_ = function() { | |
| 338 var pad = (remoting.accessCodeExpiresIn < 10) ? '0:0' : '0:'; | |
| 339 l10n.localizeElement(document.getElementById('seconds-remaining'), | |
| 340 pad + remoting.accessCodeExpiresIn); | |
| 341 if (!updateTimeoutStyles_()) { | |
| 342 disableTimeoutCountdown_(); | |
| 343 } | |
| 344 } | |
| 345 | |
| 346 /** | |
| 347 * Callback to show or hide the NAT traversal warning when the policy changes. | |
| 348 * @param {boolean} enabled True if NAT traversal is enabled. | |
| 349 * @return {void} Nothing. | |
| 350 */ | |
| 351 function onNatTraversalPolicyChanged_(enabled) { | |
| 352 var container = document.getElementById('nat-box-container'); | |
| 353 container.hidden = enabled; | |
| 354 } | |
| 355 | |
| 356 /** | |
| 357 * Callback for the host plugin to notify the web app of state changes. | |
| 358 * @param {number} state The new state of the plugin. | |
| 359 */ | |
| 360 function onStateChanged_(state) { | |
| 361 var plugin = /** @type {remoting.HostPlugin} */ | |
| 362 document.getElementById(remoting.HOST_PLUGIN_ID); | |
| 363 if (state == plugin.STARTING) { | |
| 364 // Nothing to do here. | |
| 365 remoting.debug.log('Host plugin state: STARTING'); | |
| 366 } else if (state == plugin.REQUESTED_ACCESS_CODE) { | |
| 367 // Nothing to do here. | |
| 368 remoting.debug.log('Host plugin state: REQUESTED_ACCESS_CODE'); | |
| 369 } else if (state == plugin.RECEIVED_ACCESS_CODE) { | |
| 370 remoting.debug.log('Host plugin state: RECEIVED_ACCESS_CODE'); | |
| 371 var accessCode = plugin.accessCode; | |
| 372 var accessCodeDisplay = document.getElementById('access-code-display'); | |
| 373 accessCodeDisplay.innerText = ''; | |
| 374 // Display the access code in groups of four digits for readability. | |
| 375 for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) { | |
| 376 var nextFourDigits = document.createElement('span'); | |
| 377 nextFourDigits.className = 'access-code-digit-group'; | |
| 378 nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup); | |
| 379 accessCodeDisplay.appendChild(nextFourDigits); | |
| 380 } | |
| 381 remoting.accessCodeExpiresIn = plugin.accessCodeLifetime; | |
| 382 if (remoting.accessCodeExpiresIn > 0) { // Check it hasn't expired. | |
| 383 remoting.accessCodeTimerId = setInterval( | |
| 384 'remoting.decrementAccessCodeTimeout_()', 1000); | |
| 385 remoting.timerRunning = true; | |
| 386 remoting.updateAccessCodeTimeoutElement_(); | |
| 387 updateTimeoutStyles_(); | |
| 388 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION); | |
| 389 } else { | |
| 390 // This can only happen if the cloud tells us that the code lifetime is | |
| 391 // <= 0s, which shouldn't happen so we don't care how clean this UX is. | |
| 392 remoting.debug.log('Access code already invalid on receipt!'); | |
| 393 remoting.cancelShare(); | |
| 394 } | |
| 395 } else if (state == plugin.CONNECTED) { | |
| 396 remoting.debug.log('Host plugin state: CONNECTED'); | |
| 397 var element = document.getElementById('host-shared-message'); | |
| 398 var client = plugin.client; | |
| 399 l10n.localizeElement(element, client); | |
| 400 remoting.setMode(remoting.AppMode.HOST_SHARED); | |
| 401 disableTimeoutCountdown_(); | |
| 402 } else if (state == plugin.DISCONNECTING) { | |
| 403 remoting.debug.log('Host plugin state: DISCONNECTING'); | |
| 404 } else if (state == plugin.DISCONNECTED) { | |
| 405 remoting.debug.log('Host plugin state: DISCONNECTED'); | |
| 406 if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) { | |
| 407 // If an error is being displayed, then the plugin should not be able to | |
| 408 // hide it by setting the state. Errors must be dismissed by the user | |
| 409 // clicking OK, which puts the app into mode HOME. | |
| 410 if (remoting.lastShareWasCancelled) { | |
| 411 remoting.setMode(remoting.AppMode.HOME); | |
| 412 } else { | |
| 413 remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED); | |
| 414 } | |
| 415 } | |
| 416 plugin.parentNode.removeChild(plugin); | |
| 417 } else if (state == plugin.ERROR) { | |
| 418 remoting.debug.log('Host plugin state: ERROR'); | |
| 419 showShareError_(/*i18n-content*/'ERROR_GENERIC'); | |
| 420 } else { | |
| 421 remoting.debug.log('Unknown state -> ' + state); | |
| 422 } | |
| 423 } | |
| 424 | |
| 425 /** | |
| 426 * This is the callback that the host plugin invokes to indicate that there | |
| 427 * is additional debug log info to display. | |
| 428 * @param {string} msg The message (which will not be localized) to be logged. | |
| 429 */ | |
| 430 function debugInfoCallback_(msg) { | |
| 431 remoting.debug.log('plugin: ' + msg); | |
| 432 } | |
| 433 | |
| 434 /** | |
| 435 * Show a host-side error message. | |
| 436 * | |
| 437 * @param {string} errorTag The error message to be localized and displayed. | |
| 438 * @return {void} Nothing. | |
| 439 */ | |
| 440 function showShareError_(errorTag) { | |
| 441 var errorDiv = document.getElementById('host-plugin-error'); | |
| 442 l10n.localizeElementFromTag(errorDiv, errorTag); | |
| 443 remoting.debug.log('Sharing error: ' + errorTag); | |
| 444 remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED); | |
| 445 } | |
| 446 | |
| 447 /** | |
| 448 * Cancel an active or pending share operation. | |
| 449 * | |
| 450 * @return {void} Nothing. | |
| 451 */ | |
| 452 remoting.cancelShare = function() { | |
| 453 remoting.debug.log('Canceling share...'); | |
| 454 remoting.lastShareWasCancelled = true; | |
| 455 var plugin = /** @type {remoting.HostPlugin} */ | |
| 456 document.getElementById(remoting.HOST_PLUGIN_ID); | |
| 457 try { | |
| 458 plugin.disconnect(); | |
| 459 } catch (error) { | |
| 460 // Hack to force JSCompiler type-safety. | |
| 461 var errorTyped = /** @type {{description: string}} */ error; | |
| 462 remoting.debug.log('Error disconnecting: ' + errorTyped.description + | |
| 463 '. The host plugin probably crashed.'); | |
| 464 // TODO(jamiewalch): Clean this up. We should have a class representing | |
| 465 // the host plugin, like we do for the client, which should handle crash | |
| 466 // reporting and it should use a more detailed error message than the | |
| 467 // default 'generic' one. See crbug.com/94624 | |
| 468 showShareError_(/*i18n-content*/'ERROR_GENERIC'); | |
| 469 } | |
| 470 disableTimeoutCountdown_(); | |
| 471 } | |
| 472 | |
| 473 /** | |
| 474 * Cancel an incomplete connect operation. | |
| 475 * | |
| 476 * @return {void} Nothing. | |
| 477 */ | |
| 478 remoting.cancelConnect = function() { | |
| 479 if (remoting.supportHostsXhr) { | |
| 480 remoting.supportHostsXhr.abort(); | |
| 481 remoting.supportHostsXhr = null; | |
| 482 } | |
| 483 if (remoting.session) { | |
| 484 remoting.session.removePlugin(); | |
| 485 remoting.session = null; | |
| 486 } | |
| 487 remoting.setMode(remoting.AppMode.HOME); | |
| 488 } | |
| 489 | |
| 490 function updateStatistics() { | |
| 491 if (!remoting.session) | |
| 492 return; | |
| 493 if (remoting.session.state != remoting.ClientSession.State.CONNECTED) | |
| 494 return; | |
| 495 var stats = remoting.session.stats(); | |
| 496 | |
| 497 var units = ''; | |
| 498 var videoBandwidth = stats['video_bandwidth']; | |
| 499 if (videoBandwidth < 1024) { | |
| 500 units = 'Bps'; | |
| 501 } else if (videoBandwidth < 1048576) { | |
| 502 units = 'KiBps'; | |
| 503 videoBandwidth = videoBandwidth / 1024; | |
| 504 } else if (videoBandwidth < 1073741824) { | |
| 505 units = 'MiBps'; | |
| 506 videoBandwidth = videoBandwidth / 1048576; | |
| 507 } else { | |
| 508 units = 'GiBps'; | |
| 509 videoBandwidth = videoBandwidth / 1073741824; | |
| 510 } | |
| 511 | |
| 512 var statistics = document.getElementById('statistics'); | |
| 513 statistics.innerText = | |
| 514 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + | |
| 515 ', Frame Rate: ' + | |
| 516 (stats['video_frame_rate'] ? | |
| 517 stats['video_frame_rate'].toFixed(2) + ' fps' : 'n/a') + | |
| 518 ', Capture: ' + stats['capture_latency'].toFixed(2) + 'ms' + | |
| 519 ', Encode: ' + stats['encode_latency'].toFixed(2) + 'ms' + | |
| 520 ', Decode: ' + stats['decode_latency'].toFixed(2) + 'ms' + | |
| 521 ', Render: ' + stats['render_latency'].toFixed(2) + 'ms' + | |
| 522 ', Latency: ' + stats['roundtrip_latency'].toFixed(2) + 'ms'; | |
| 523 | |
| 524 // Update the stats once per second. | |
| 525 window.setTimeout(updateStatistics, 1000); | |
| 526 } | |
| 527 | |
| 528 function showToolbarPreview_() { | |
| 529 var toolbar = document.getElementById('session-toolbar'); | |
| 530 addClass(toolbar, 'toolbar-preview'); | |
| 531 window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); | |
| 532 } | |
| 533 | |
| 534 /** @param {number} oldState The previous state of the plugin. */ | |
| 535 function onClientStateChange_(oldState) { | |
| 536 if (!remoting.session) { | |
| 537 // If the connection has been cancelled, then we no longer have a reference | |
| 538 // to the session object and should ignore any state changes. | |
| 539 return; | |
| 540 } | |
| 541 var state = remoting.session.state; | |
| 542 if (state == remoting.ClientSession.State.CREATED) { | |
| 543 remoting.debug.log('Created plugin'); | |
| 544 } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { | |
| 545 showConnectError_(remoting.ClientError.BAD_PLUGIN_VERSION); | |
| 546 } else if (state == remoting.ClientSession.State.CONNECTING) { | |
| 547 remoting.debug.log('Connecting as ' + remoting.username); | |
| 548 } else if (state == remoting.ClientSession.State.INITIALIZING) { | |
| 549 remoting.debug.log('Initializing connection'); | |
| 550 } else if (state == remoting.ClientSession.State.CONNECTED) { | |
| 551 if (remoting.session) { | |
| 552 remoting.setMode(remoting.AppMode.IN_SESSION); | |
| 553 recenterToolbar_(); | |
| 554 showToolbarPreview_(); | |
| 555 updateStatistics(); | |
| 556 } | |
| 557 } else if (state == remoting.ClientSession.State.CLOSED) { | |
| 558 if (oldState == remoting.ClientSession.State.CONNECTED) { | |
| 559 remoting.session.removePlugin(); | |
| 560 remoting.session = null; | |
| 561 remoting.debug.log('Connection closed by host'); | |
| 562 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
| 563 } else { | |
| 564 // The transition from CONNECTING to CLOSED state may happen | |
| 565 // only with older client plugins. Current version should go the | |
| 566 // FAILED state when connection fails. | |
| 567 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 568 } | |
| 569 } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { | |
| 570 remoting.debug.log('Client plugin reported connection failed: ' + | |
| 571 remoting.session.error); | |
| 572 if (remoting.session.error == | |
| 573 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { | |
| 574 showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); | |
| 575 } else if (remoting.session.error == | |
| 576 remoting.ClientSession.ConnectionError.SESSION_REJECTED) { | |
| 577 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 578 } else if (remoting.session.error == | |
| 579 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { | |
| 580 showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); | |
| 581 } else if (remoting.session.error == | |
| 582 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { | |
| 583 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
| 584 } else { | |
| 585 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
| 586 } | |
| 587 } else { | |
| 588 remoting.debug.log('Unexpected client plugin state: ' + state); | |
| 589 // This should only happen if the web-app and client plugin get out of | |
| 590 // sync, and even then the version check should allow compatibility. | |
| 591 showConnectError_(remoting.ClientError.MISSING_PLUGIN); | |
| 592 } | |
| 593 } | |
| 594 | |
| 595 function startSession_() { | |
| 596 remoting.debug.log('Starting session...'); | |
| 597 var accessCode = document.getElementById('access-code-entry'); | |
| 598 accessCode.value = ''; // The code has been validated and won't work again. | |
| 599 remoting.username = | |
| 600 /** @type {string} email must be non-NULL to get here */ getEmail(); | |
| 601 remoting.session = | |
| 602 new remoting.ClientSession(remoting.hostJid, remoting.hostPublicKey, | |
| 603 remoting.accessCode, remoting.username, | |
| 604 onClientStateChange_); | |
| 605 /** @param {string} token The auth token. */ | |
| 606 var createPluginAndConnect = function(token) { | |
| 607 remoting.session.createPluginAndConnect( | |
| 608 document.getElementById('session-mode'), | |
| 609 token); | |
| 610 }; | |
| 611 remoting.oauth2.callWithToken(createPluginAndConnect); | |
| 612 } | |
| 613 | |
| 614 /** | |
| 615 * Show a client-side error message. | |
| 616 * | |
| 617 * @param {remoting.ClientError} errorTag The error to be localized and | |
| 618 * displayed. | |
| 619 * @return {void} Nothing. | |
| 620 */ | |
| 621 function showConnectError_(errorTag) { | |
| 622 remoting.debug.log('Connection failed: ' + errorTag); | |
| 623 var errorDiv = document.getElementById('connect-error-message'); | |
| 624 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); | |
| 625 remoting.accessCode = ''; | |
| 626 if (remoting.session) { | |
| 627 remoting.session.disconnect(); | |
| 628 remoting.session = null; | |
| 629 } | |
| 630 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); | |
| 631 } | |
| 632 | |
| 633 /** | |
| 634 * @param {XMLHttpRequest} xhr The XMLHttpRequest object. | |
| 635 * @return {void} Nothing. | |
| 636 */ | |
| 637 function parseServerResponse_(xhr) { | |
| 638 remoting.supportHostsXhr = null; | |
| 639 remoting.debug.log('parseServerResponse: status = ' + xhr.status); | |
| 640 if (xhr.status == 200) { | |
| 641 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ | |
| 642 JSON.parse(xhr.responseText); | |
| 643 if (host.data && host.data.jabberId && host.data.publicKey) { | |
| 644 remoting.hostJid = host.data.jabberId; | |
| 645 remoting.hostPublicKey = host.data.publicKey; | |
| 646 var split = remoting.hostJid.split('/'); | |
| 647 document.getElementById('connected-to').innerText = split[0]; | |
| 648 startSession_(); | |
| 649 return; | |
| 650 } | |
| 651 } | |
| 652 var errorMsg = remoting.ClientError.OTHER_ERROR; | |
| 653 if (xhr.status == 404) { | |
| 654 errorMsg = remoting.ClientError.INVALID_ACCESS_CODE; | |
| 655 } else if (xhr.status == 0) { | |
| 656 errorMsg = remoting.ClientError.NO_RESPONSE; | |
| 657 } else { | |
| 658 remoting.debug.log('The server responded: ' + xhr.responseText); | |
| 659 } | |
| 660 showConnectError_(errorMsg); | |
| 661 } | |
| 662 | |
| 663 /** @param {string} accessCode The access code, as entered by the user. | |
| 664 * @return {string} The normalized form of the code (whitespace removed). */ | |
| 665 function normalizeAccessCode_(accessCode) { | |
| 666 // Trim whitespace. | |
| 667 // TODO(sergeyu): Do we need to do any other normalization here? | |
| 668 return accessCode.replace(/\s/g, ''); | |
| 669 } | |
| 670 | |
| 671 /** @param {string} supportId The canonicalized support ID. */ | |
| 672 function resolveSupportId(supportId) { | |
| 673 var headers = { | |
| 674 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
| 675 }; | |
| 676 | |
| 677 remoting.supportHostsXhr = remoting.xhr.get( | |
| 678 'https://www.googleapis.com/chromoting/v1/support-hosts/' + | |
| 679 encodeURIComponent(supportId), | |
| 680 parseServerResponse_, | |
| 681 '', | |
| 682 headers); | |
| 683 } | |
| 684 | |
| 685 remoting.tryConnect = function() { | |
| 686 document.getElementById('cancel-button').disabled = false; | |
| 687 if (remoting.oauth2.needsNewAccessToken()) { | |
| 688 remoting.oauth2.refreshAccessToken(function(xhr) { | |
| 689 if (remoting.oauth2.needsNewAccessToken()) { | |
| 690 // Failed to get access token | |
| 691 remoting.debug.log('tryConnect: OAuth2 token fetch failed'); | |
| 692 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
| 693 return; | |
| 694 } | |
| 695 remoting.tryConnectWithAccessToken(); | |
| 696 }); | |
| 697 } else { | |
| 698 remoting.tryConnectWithAccessToken(); | |
| 699 } | |
| 700 } | |
| 701 | |
| 702 remoting.tryConnectWithAccessToken = function() { | |
| 703 if (!remoting.wcsLoader) { | |
| 704 remoting.wcsLoader = new remoting.WcsLoader(); | |
| 705 } | |
| 706 /** @param {function(string):void} setToken The callback function. */ | |
| 707 var callWithToken = function(setToken) { | |
| 708 remoting.oauth2.callWithToken(setToken); | |
| 709 }; | |
| 710 remoting.wcsLoader.start( | |
| 711 remoting.oauth2.getAccessToken(), | |
| 712 callWithToken, | |
| 713 remoting.tryConnectWithWcs); | |
| 714 } | |
| 715 | |
| 716 /** | |
| 717 * WcsLoader callback, called when the wcs script has been loaded, or on error. | |
| 718 * @param {boolean} success True if the script was loaded successfully. | |
| 719 */ | |
| 720 remoting.tryConnectWithWcs = function(success) { | |
| 721 if (success) { | |
| 722 var accessCode = document.getElementById('access-code-entry').value; | |
| 723 remoting.accessCode = normalizeAccessCode_(accessCode); | |
| 724 // At present, only 12-digit access codes are supported, of which the first | |
| 725 // 7 characters are the supportId. | |
| 726 if (remoting.accessCode.length != kAccessCodeLen) { | |
| 727 remoting.debug.log('Bad access code length'); | |
| 728 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 729 } else { | |
| 730 var supportId = remoting.accessCode.substring(0, kSupportIdLen); | |
| 731 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
| 732 resolveSupportId(supportId); | |
| 733 } | |
| 734 } else { | |
| 735 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
| 736 } | |
| 737 } | 45 } |
| 738 | 46 |
| 739 remoting.cancelPendingOperation = function() { | 47 remoting.cancelPendingOperation = function() { |
| 740 document.getElementById('cancel-button').disabled = true; | 48 document.getElementById('cancel-button').disabled = true; |
| 741 switch (remoting.getMajorMode()) { | 49 switch (remoting.getMajorMode()) { |
| 742 case remoting.AppMode.HOST: | 50 case remoting.AppMode.HOST: |
| 743 remoting.cancelShare(); | 51 remoting.cancelShare(); |
| 744 break; | 52 break; |
| 745 case remoting.AppMode.CLIENT: | 53 case remoting.AppMode.CLIENT: |
| 746 remoting.cancelConnect(); | 54 remoting.cancelConnect(); |
| 747 break; | 55 break; |
| 748 } | 56 } |
| 749 } | 57 } |
| 750 | 58 |
| 751 /** | 59 /** |
| 60 * If the client is connected, or the host is shared, prompt before closing. | |
| 61 * | |
| 62 * @return {?string} The prompt string if a connection is active. | |
| 63 */ | |
| 64 remoting.promptClose = function() { | |
| 65 switch (remoting.currentMode) { | |
| 66 case remoting.AppMode.CLIENT_CONNECTING: | |
| 67 case remoting.AppMode.HOST_WAITING_FOR_CODE: | |
| 68 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: | |
| 69 case remoting.AppMode.HOST_SHARED: | |
| 70 case remoting.AppMode.IN_SESSION: | |
| 71 var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); | |
| 72 return result; | |
| 73 default: | |
| 74 return null; | |
| 75 } | |
| 76 } | |
| 77 | |
| 78 /** | |
| 79 * Sign the user out of Chromoting by clearing the OAuth refresh token. | |
| 80 */ | |
| 81 remoting.clearOAuth2 = function() { | |
| 82 remoting.oauth2.clear(); | |
| 83 window.localStorage.removeItem(KEY_EMAIL_); | |
| 84 remoting.setMode(remoting.AppMode.UNAUTHENTICATED); | |
| 85 } | |
| 86 | |
| 87 /** | |
| 88 * Callback function called when the browser window loses focus. In this case, | |
| 89 * release all keys to prevent them becoming 'stuck down' on the host. | |
| 90 */ | |
| 91 function pluginLostFocus_() { | |
| 92 if (remoting.clientSession && remoting.clientSession.plugin) { | |
| 93 remoting.clientSession.plugin.releaseAllKeys(); | |
| 94 } | |
| 95 } | |
| 96 | |
| 97 /** | |
| 98 * If the user is authenticated, but there is no email address cached, get one. | |
| 99 */ | |
| 100 function refreshEmail_() { | |
| 101 if (!getEmail_() && remoting.oauth2.isAuthenticated()) { | |
| 102 remoting.oauth2.getEmail(setEmail_); | |
| 103 } | |
| 104 } | |
| 105 | |
| 106 /** The key under which the email address is stored. */ | |
| 107 var KEY_EMAIL_ = 'remoting-email'; | |
| 108 | |
| 109 /** | |
| 110 * Save the user's email address in local storage. | |
| 111 * | |
| 112 * @param {?string} email The email address to place in local storage. | |
| 113 * @return {void} Nothing. | |
| 114 */ | |
| 115 function setEmail_(email) { | |
| 116 if (email) { | |
| 117 document.getElementById('current-email').innerText = email; | |
| 118 } else { | |
| 119 // TODO(ajwong): Have a better way of showing an error. | |
| 120 document.getElementById('current-email').innerText = '???'; | |
| 121 } | |
| 122 } | |
| 123 | |
| 124 /** | |
| 125 * Read the user's email address from local storage. | |
| 126 * | |
| 127 * @return {?string} The email address associated with the auth credentials. | |
| 128 */ | |
| 129 function getEmail_() { | |
| 130 var result = window.localStorage.getItem(KEY_EMAIL_); | |
| 131 return typeof result == 'string' ? result : null; | |
| 132 } | |
| 133 /** | |
| 752 * Gets the major-mode that this application should start up in. | 134 * Gets the major-mode that this application should start up in. |
| 753 * | 135 * |
| 754 * @return {remoting.AppMode} The mode to start in. | 136 * @return {remoting.AppMode} The mode to start in. |
| 755 */ | 137 */ |
| 756 function getAppStartupMode() { | 138 function getAppStartupMode_() { |
| 757 if (!remoting.oauth2.isAuthenticated()) { | 139 if (!remoting.oauth2.isAuthenticated()) { |
| 758 return remoting.AppMode.UNAUTHENTICATED; | 140 return remoting.AppMode.UNAUTHENTICATED; |
| 759 } | 141 } |
| 760 if (isHostModeSupported()) { | 142 if (isHostModeSupported_()) { |
| 761 return remoting.AppMode.HOME; | 143 return remoting.AppMode.HOME; |
| 762 } else { | 144 } else { |
| 763 return remoting.AppMode.CLIENT_UNCONNECTED; | 145 return remoting.AppMode.CLIENT_UNCONNECTED; |
| 764 } | 146 } |
| 765 } | 147 } |
| 766 | 148 |
| 767 /** | 149 /** |
| 768 * Returns whether Host mode is supported on this platform. | 150 * Returns whether Host mode is supported on this platform. |
| 769 * | 151 * |
| 770 * @return {boolean} True if Host mode is supported. | 152 * @return {boolean} True if Host mode is supported. |
| 771 */ | 153 */ |
| 772 function isHostModeSupported() { | 154 function isHostModeSupported_() { |
| 773 // Currently, sharing on Chromebooks is not supported. | 155 // Currently, sharing on Chromebooks is not supported. |
| 774 return !navigator.userAgent.match(/\bCrOS\b/); | 156 return !navigator.userAgent.match(/\bCrOS\b/); |
| 775 } | 157 } |
| 776 | |
| 777 /** | |
| 778 * Enable or disable scale-to-fit. | |
| 779 * | |
| 780 * @param {Element} button The scale-to-fit button. The style of this button is | |
| 781 * updated to reflect the new scaling state. | |
| 782 * @return {void} Nothing. | |
| 783 */ | |
| 784 remoting.toggleScaleToFit = function(button) { | |
| 785 remoting.scaleToFit = !remoting.scaleToFit; | |
| 786 if (remoting.scaleToFit) { | |
| 787 addClass(button, 'toggle-button-active'); | |
| 788 } else { | |
| 789 removeClass(button, 'toggle-button-active'); | |
| 790 } | |
| 791 remoting.session.updateDimensions(); | |
| 792 } | |
| 793 | |
| 794 /** | |
| 795 * Update the remoting client layout in response to a resize event. | |
| 796 * | |
| 797 * @return {void} Nothing. | |
| 798 */ | |
| 799 remoting.onResize = function() { | |
| 800 if (remoting.session) | |
| 801 remoting.session.onWindowSizeChanged(); | |
| 802 recenterToolbar_(); | |
| 803 } | |
| 804 | |
| 805 /** | |
| 806 * Disconnect the remoting client. | |
| 807 * | |
| 808 * @return {void} Nothing. | |
| 809 */ | |
| 810 remoting.disconnect = function() { | |
| 811 if (remoting.session) { | |
| 812 remoting.session.disconnect(); | |
| 813 remoting.session = null; | |
| 814 remoting.debug.log('Disconnected.'); | |
| 815 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
| 816 } | |
| 817 } | |
| 818 | |
| 819 /** | |
| 820 * If the client is connected, or the host is shared, prompt before closing. | |
| 821 * | |
| 822 * @return {?string} The prompt string if a connection is active. | |
| 823 */ | |
| 824 remoting.promptClose = function() { | |
| 825 switch (remoting.currentMode) { | |
| 826 case remoting.AppMode.CLIENT_CONNECTING: | |
| 827 case remoting.AppMode.HOST_WAITING_FOR_CODE: | |
| 828 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: | |
| 829 case remoting.AppMode.HOST_SHARED: | |
| 830 case remoting.AppMode.IN_SESSION: | |
| 831 var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); | |
| 832 return result; | |
| 833 default: | |
| 834 return null; | |
| 835 } | |
| 836 } | |
| 837 | |
| 838 /** | |
| 839 * @param {Event} event The keyboard event. | |
| 840 * @return {void} Nothing. | |
| 841 */ | |
| 842 remoting.checkHotkeys = function(event) { | |
| 843 if (String.fromCharCode(event.which) == 'D') { | |
| 844 remoting.toggleDebugLog(); | |
| 845 } | |
| 846 } | |
| 847 | |
| 848 function recenterToolbar_() { | |
| 849 var toolbar = document.getElementById('session-toolbar'); | |
| 850 var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; | |
| 851 toolbar.style['left'] = toolbarX + 'px'; | |
| 852 } | |
| 853 | |
| 854 }()); | 158 }()); |
| OLD | NEW |