| OLD | NEW |
| (Empty) |
| 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 * Functions related to the 'client screen' for Chromoting. | |
| 8 */ | |
| 9 | |
| 10 'use strict'; | |
| 11 | |
| 12 /** @suppress {duplicate} */ | |
| 13 var remoting = remoting || {}; | |
| 14 | |
| 15 /** | |
| 16 * @type {boolean} Whether or not the plugin should scale itself. | |
| 17 */ | |
| 18 remoting.scaleToFit = false; | |
| 19 | |
| 20 /** | |
| 21 * @type {remoting.ClientSession} The client session object, set once the | |
| 22 * access code has been successfully verified. | |
| 23 */ | |
| 24 remoting.clientSession = null; | |
| 25 | |
| 26 /** | |
| 27 * @type {string} The normalized access code. | |
| 28 */ | |
| 29 remoting.accessCode = ''; | |
| 30 | |
| 31 /** | |
| 32 * @type {string} The host's JID, returned by the server. | |
| 33 */ | |
| 34 remoting.hostJid = ''; | |
| 35 | |
| 36 /** | |
| 37 * @type {string} For Me2Me connections, the id of the current host. | |
| 38 */ | |
| 39 remoting.hostId = ''; | |
| 40 | |
| 41 /** | |
| 42 * @type {boolean} For Me2Me connections. Set to true if connection | |
| 43 * must be retried on failure. | |
| 44 */ | |
| 45 remoting.retryIfOffline = false; | |
| 46 | |
| 47 /** | |
| 48 * @type {string} The host's public key, returned by the server. | |
| 49 */ | |
| 50 remoting.hostPublicKey = ''; | |
| 51 | |
| 52 /** | |
| 53 * @type {XMLHttpRequest} The XHR object corresponding to the current | |
| 54 * support-hosts request, if there is one outstanding. | |
| 55 * @private | |
| 56 */ | |
| 57 remoting.supportHostsXhr_ = null; | |
| 58 | |
| 59 /** | |
| 60 * @enum {string} | |
| 61 */ | |
| 62 remoting.ConnectionType = { | |
| 63 It2Me: 'It2Me', | |
| 64 Me2Me: 'Me2Me' | |
| 65 }; | |
| 66 | |
| 67 /** | |
| 68 * @type {remoting.ConnectionType?} | |
| 69 */ | |
| 70 remoting.currentConnectionType = null; | |
| 71 | |
| 72 /** | |
| 73 * Entry point for the 'connect' functionality. This function defers to the | |
| 74 * WCS loader to call it back with an access token. | |
| 75 */ | |
| 76 remoting.connectIt2Me = function() { | |
| 77 remoting.currentConnectionType = remoting.ConnectionType.It2Me; | |
| 78 document.getElementById('cancel-button').disabled = false; | |
| 79 remoting.WcsLoader.load(connectIt2MeWithAccessToken_); | |
| 80 }; | |
| 81 | |
| 82 /** | |
| 83 * Cancel an incomplete connect operation. | |
| 84 * | |
| 85 * @return {void} Nothing. | |
| 86 */ | |
| 87 remoting.cancelConnect = function() { | |
| 88 if (remoting.supportHostsXhr_) { | |
| 89 remoting.supportHostsXhr_.abort(); | |
| 90 remoting.supportHostsXhr_ = null; | |
| 91 } | |
| 92 if (remoting.clientSession) { | |
| 93 remoting.clientSession.removePlugin(); | |
| 94 remoting.clientSession = null; | |
| 95 } | |
| 96 remoting.setMode(remoting.AppMode.HOME); | |
| 97 }; | |
| 98 | |
| 99 /** | |
| 100 * Enable or disable scale-to-fit. | |
| 101 * | |
| 102 * @param {Event} event The click event. The style of the target is updated to | |
| 103 * reflect the new scaling state. | |
| 104 * @return {void} Nothing. | |
| 105 */ | |
| 106 remoting.toggleScaleToFit = function(event) { | |
| 107 var button = /** @type Element */(event.target); | |
| 108 remoting.scaleToFit = !remoting.scaleToFit; | |
| 109 if (remoting.scaleToFit) { | |
| 110 addClass(button, 'toggle-button-active'); | |
| 111 } else { | |
| 112 removeClass(button, 'toggle-button-active'); | |
| 113 } | |
| 114 remoting.clientSession.updateDimensions(); | |
| 115 }; | |
| 116 | |
| 117 /** | |
| 118 * Update the remoting client layout in response to a resize event. | |
| 119 * | |
| 120 * @return {void} Nothing. | |
| 121 */ | |
| 122 remoting.onResize = function() { | |
| 123 if (remoting.clientSession) | |
| 124 remoting.clientSession.onResize(); | |
| 125 }; | |
| 126 | |
| 127 /** | |
| 128 * Disconnect the remoting client. | |
| 129 * | |
| 130 * @return {void} Nothing. | |
| 131 */ | |
| 132 remoting.disconnect = function() { | |
| 133 if (remoting.clientSession) { | |
| 134 remoting.clientSession.disconnect(); | |
| 135 remoting.clientSession = null; | |
| 136 remoting.debug.log('Disconnected.'); | |
| 137 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) { | |
| 138 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); | |
| 139 } else { | |
| 140 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); | |
| 141 } | |
| 142 } | |
| 143 }; | |
| 144 | |
| 145 /** | |
| 146 * If WCS was successfully loaded, proceed with the connection, otherwise | |
| 147 * report an error. | |
| 148 * | |
| 149 * @param {string?} token The OAuth2 access token, or null if an error occurred. | |
| 150 * @return {void} Nothing. | |
| 151 */ | |
| 152 function connectIt2MeWithAccessToken_(token) { | |
| 153 if (token) { | |
| 154 var accessCode = document.getElementById('access-code-entry').value; | |
| 155 remoting.accessCode = normalizeAccessCode_(accessCode); | |
| 156 // At present, only 12-digit access codes are supported, of which the first | |
| 157 // 7 characters are the supportId. | |
| 158 var kSupportIdLen = 7; | |
| 159 var kHostSecretLen = 5; | |
| 160 var kAccessCodeLen = kSupportIdLen + kHostSecretLen; | |
| 161 if (remoting.accessCode.length != kAccessCodeLen) { | |
| 162 remoting.debug.log('Bad access code length'); | |
| 163 showConnectError_(remoting.Error.INVALID_ACCESS_CODE); | |
| 164 } else { | |
| 165 var supportId = remoting.accessCode.substring(0, kSupportIdLen); | |
| 166 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
| 167 resolveSupportId(supportId); | |
| 168 } | |
| 169 } else { | |
| 170 showConnectError_(remoting.Error.AUTHENTICATION_FAILED); | |
| 171 } | |
| 172 } | |
| 173 | |
| 174 /** | |
| 175 * Callback function called when the state of the client plugin changes. The | |
| 176 * current state is available via the |state| member variable. | |
| 177 * | |
| 178 * @param {number} oldState The previous state of the plugin. | |
| 179 * @param {number} newState The current state of the plugin. | |
| 180 */ | |
| 181 // TODO(jamiewalch): Make this pass both the current and old states to avoid | |
| 182 // race conditions. | |
| 183 function onClientStateChange_(oldState, newState) { | |
| 184 if (!remoting.clientSession) { | |
| 185 // If the connection has been cancelled, then we no longer have a reference | |
| 186 // to the session object and should ignore any state changes. | |
| 187 return; | |
| 188 } | |
| 189 if (newState == remoting.ClientSession.State.CREATED) { | |
| 190 remoting.debug.log('Created plugin'); | |
| 191 | |
| 192 } else if (newState == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { | |
| 193 showConnectError_(remoting.Error.BAD_PLUGIN_VERSION); | |
| 194 | |
| 195 } else if (newState == remoting.ClientSession.State.CONNECTING) { | |
| 196 remoting.debug.log('Connecting as ' + remoting.oauth2.getCachedEmail()); | |
| 197 | |
| 198 } else if (newState == remoting.ClientSession.State.INITIALIZING) { | |
| 199 remoting.debug.log('Initializing connection'); | |
| 200 | |
| 201 } else if (newState == remoting.ClientSession.State.CONNECTED) { | |
| 202 if (remoting.clientSession) { | |
| 203 remoting.setMode(remoting.AppMode.IN_SESSION); | |
| 204 remoting.toolbar.center(); | |
| 205 remoting.toolbar.preview(); | |
| 206 updateStatistics_(); | |
| 207 } | |
| 208 | |
| 209 } else if (newState == remoting.ClientSession.State.CLOSED) { | |
| 210 if (oldState == remoting.ClientSession.State.CONNECTED) { | |
| 211 remoting.clientSession.removePlugin(); | |
| 212 remoting.clientSession = null; | |
| 213 remoting.debug.log('Connection closed by host'); | |
| 214 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) { | |
| 215 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); | |
| 216 } else { | |
| 217 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); | |
| 218 } | |
| 219 } else { | |
| 220 // The transition from CONNECTING to CLOSED state may happen | |
| 221 // only with older client plugins. Current version should go the | |
| 222 // FAILED state when connection fails. | |
| 223 showConnectError_(remoting.Error.INVALID_ACCESS_CODE); | |
| 224 } | |
| 225 | |
| 226 } else if (newState == remoting.ClientSession.State.CONNECTION_FAILED) { | |
| 227 remoting.debug.log('Client plugin reported connection failed: ' + | |
| 228 remoting.clientSession.error); | |
| 229 if (remoting.clientSession.error == | |
| 230 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { | |
| 231 retryConnectOrReportOffline_(); | |
| 232 } else if (remoting.clientSession.error == | |
| 233 remoting.ClientSession.ConnectionError.SESSION_REJECTED) { | |
| 234 showConnectError_(remoting.Error.INVALID_ACCESS_CODE); | |
| 235 } else if (remoting.clientSession.error == | |
| 236 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { | |
| 237 showConnectError_(remoting.Error.INCOMPATIBLE_PROTOCOL); | |
| 238 } else if (remoting.clientSession.error == | |
| 239 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { | |
| 240 showConnectError_(remoting.Error.GENERIC); | |
| 241 } else { | |
| 242 showConnectError_(remoting.Error.GENERIC); | |
| 243 } | |
| 244 | |
| 245 } else { | |
| 246 remoting.debug.log('Unexpected client plugin state: ' + newState); | |
| 247 // This should only happen if the web-app and client plugin get out of | |
| 248 // sync, and even then the version check should allow compatibility. | |
| 249 showConnectError_(remoting.Error.MISSING_PLUGIN); | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 /** | |
| 254 * If we have a hostId to retry, try refreshing it and connecting again. If not, | |
| 255 * then show the 'host offline' error message. | |
| 256 * | |
| 257 * @return {void} Nothing. | |
| 258 */ | |
| 259 function retryConnectOrReportOffline_() { | |
| 260 if (remoting.hostId && remoting.retryIfOffline) { | |
| 261 console.log('Connection failed. Retrying.'); | |
| 262 /** @param {boolean} success True if the refresh was successful. */ | |
| 263 var onDone = function(success) { | |
| 264 if (success) { | |
| 265 remoting.connectMe2Me(remoting.hostId, false); | |
| 266 } else { | |
| 267 showConnectError_(remoting.Error.HOST_IS_OFFLINE); | |
| 268 } | |
| 269 }; | |
| 270 remoting.hostList.refresh(onDone); | |
| 271 } else { | |
| 272 console.log('Connection failed. Not retrying.'); | |
| 273 showConnectError_(remoting.Error.HOST_IS_OFFLINE); | |
| 274 } | |
| 275 } | |
| 276 | |
| 277 /** | |
| 278 * Create the client session object and initiate the connection. | |
| 279 * | |
| 280 * @return {void} Nothing. | |
| 281 */ | |
| 282 function startSession_() { | |
| 283 remoting.debug.log('Starting session...'); | |
| 284 var accessCode = document.getElementById('access-code-entry'); | |
| 285 accessCode.value = ''; // The code has been validated and won't work again. | |
| 286 remoting.clientSession = | |
| 287 new remoting.ClientSession( | |
| 288 remoting.hostJid, remoting.hostPublicKey, | |
| 289 remoting.accessCode, | |
| 290 /** @type {string} */ (remoting.oauth2.getCachedEmail()), | |
| 291 onClientStateChange_); | |
| 292 /** @param {string} token The auth token. */ | |
| 293 var createPluginAndConnect = function(token) { | |
| 294 remoting.clientSession.createPluginAndConnect( | |
| 295 document.getElementById('session-mode'), | |
| 296 token); | |
| 297 }; | |
| 298 remoting.oauth2.callWithToken(createPluginAndConnect); | |
| 299 } | |
| 300 | |
| 301 /** | |
| 302 * Show a client-side error message. | |
| 303 * | |
| 304 * @param {remoting.Error} errorTag The error to be localized and | |
| 305 * displayed. | |
| 306 * @return {void} Nothing. | |
| 307 */ | |
| 308 function showConnectError_(errorTag) { | |
| 309 remoting.debug.log('Connection failed: ' + errorTag); | |
| 310 var errorDiv = document.getElementById('connect-error-message'); | |
| 311 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); | |
| 312 remoting.accessCode = ''; | |
| 313 if (remoting.clientSession) { | |
| 314 remoting.clientSession.disconnect(); | |
| 315 remoting.clientSession = null; | |
| 316 } | |
| 317 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) { | |
| 318 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME); | |
| 319 } else { | |
| 320 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME); | |
| 321 } | |
| 322 } | |
| 323 | |
| 324 /** | |
| 325 * Parse the response from the server to a request to resolve a support id. | |
| 326 * | |
| 327 * @param {XMLHttpRequest} xhr The XMLHttpRequest object. | |
| 328 * @return {void} Nothing. | |
| 329 */ | |
| 330 function parseServerResponse_(xhr) { | |
| 331 remoting.supportHostsXhr_ = null; | |
| 332 remoting.debug.log('parseServerResponse: status = ' + xhr.status); | |
| 333 if (xhr.status == 200) { | |
| 334 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ | |
| 335 JSON.parse(xhr.responseText); | |
| 336 if (host.data && host.data.jabberId && host.data.publicKey) { | |
| 337 remoting.hostJid = host.data.jabberId; | |
| 338 remoting.hostPublicKey = host.data.publicKey; | |
| 339 var split = remoting.hostJid.split('/'); | |
| 340 document.getElementById('connected-to').innerText = split[0]; | |
| 341 startSession_(); | |
| 342 return; | |
| 343 } | |
| 344 } | |
| 345 var errorMsg = remoting.Error.GENERIC; | |
| 346 if (xhr.status == 404) { | |
| 347 errorMsg = remoting.Error.INVALID_ACCESS_CODE; | |
| 348 } else if (xhr.status == 0) { | |
| 349 errorMsg = remoting.Error.NO_RESPONSE; | |
| 350 } else if (xhr.status == 503) { | |
| 351 errorMsg = remoting.Error.SERVICE_UNAVAILABLE; | |
| 352 } else { | |
| 353 remoting.debug.log('The server responded: ' + xhr.responseText); | |
| 354 } | |
| 355 showConnectError_(errorMsg); | |
| 356 } | |
| 357 | |
| 358 /** | |
| 359 * Normalize the access code entered by the user. | |
| 360 * | |
| 361 * @param {string} accessCode The access code, as entered by the user. | |
| 362 * @return {string} The normalized form of the code (whitespace removed). | |
| 363 */ | |
| 364 function normalizeAccessCode_(accessCode) { | |
| 365 // Trim whitespace. | |
| 366 // TODO(sergeyu): Do we need to do any other normalization here? | |
| 367 return accessCode.replace(/\s/g, ''); | |
| 368 } | |
| 369 | |
| 370 /** | |
| 371 * Initiate a request to the server to resolve a support ID. | |
| 372 * | |
| 373 * @param {string} supportId The canonicalized support ID. | |
| 374 */ | |
| 375 function resolveSupportId(supportId) { | |
| 376 var headers = { | |
| 377 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
| 378 }; | |
| 379 | |
| 380 remoting.supportHostsXhr_ = remoting.xhr.get( | |
| 381 'https://www.googleapis.com/chromoting/v1/support-hosts/' + | |
| 382 encodeURIComponent(supportId), | |
| 383 parseServerResponse_, | |
| 384 '', | |
| 385 headers); | |
| 386 } | |
| 387 | |
| 388 /** | |
| 389 * Timer callback to update the statistics panel. | |
| 390 */ | |
| 391 function updateStatistics_() { | |
| 392 if (!remoting.clientSession || | |
| 393 remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) { | |
| 394 return; | |
| 395 } | |
| 396 var stats = remoting.clientSession.stats(); | |
| 397 remoting.debug.updateStatistics(stats); | |
| 398 remoting.clientSession.logStatistics(stats); | |
| 399 // Update the stats once per second. | |
| 400 window.setTimeout(updateStatistics_, 1000); | |
| 401 } | |
| 402 | |
| 403 | |
| 404 /** | |
| 405 * Shows PIN entry screen. | |
| 406 * | |
| 407 * @param {string} hostId The unique id of the host. | |
| 408 * @param {boolean} retryIfOffline If true and the host can't be contacted, | |
| 409 * refresh the host list and try again. This allows bookmarked hosts to | |
| 410 * work even if they reregister with Talk and get a different Jid. | |
| 411 * @return {void} Nothing. | |
| 412 */ | |
| 413 remoting.connectMe2Me = function(hostId, retryIfOffline) { | |
| 414 remoting.currentConnectionType = remoting.ConnectionType.Me2Me; | |
| 415 remoting.hostId = hostId; | |
| 416 remoting.retryIfOffline = retryIfOffline; | |
| 417 | |
| 418 // TODO(jamiewalch): Reinstate the PIN screen once it's supported. | |
| 419 // remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT); | |
| 420 remoting.connectMe2MeWithPin(); | |
| 421 } | |
| 422 | |
| 423 /** | |
| 424 * Start a connection to the specified host, using the cached details | |
| 425 * and the PIN entered by the user. | |
| 426 * | |
| 427 * @return {void} Nothing. | |
| 428 */ | |
| 429 remoting.connectMe2MeWithPin = function() { | |
| 430 remoting.debug.log('Connecting to host...'); | |
| 431 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
| 432 | |
| 433 var host = remoting.hostList.getHostForId(remoting.hostId); | |
| 434 if (!host) { | |
| 435 retryConnectOrReportOffline_(); | |
| 436 return; | |
| 437 } | |
| 438 remoting.hostJid = host.jabberId; | |
| 439 remoting.hostPublicKey = host.publicKey; | |
| 440 document.getElementById('connected-to').innerText = host.hostName; | |
| 441 document.title = document.title + ': ' + host.hostName; | |
| 442 | |
| 443 remoting.WcsLoader.load(connectMe2MeWithAccessToken_); | |
| 444 }; | |
| 445 | |
| 446 /** | |
| 447 * Continue making the connection to a host, once WCS has initialized. | |
| 448 * | |
| 449 * @param {string?} token The OAuth2 access token, or null if an error occurred. | |
| 450 * @return {void} Nothing. | |
| 451 */ | |
| 452 function connectMe2MeWithAccessToken_(token) { | |
| 453 if (token) { | |
| 454 /** @type {string} */ | |
| 455 var pin = document.getElementById('pin-entry').value; | |
| 456 document.getElementById('pin-entry').value = ''; | |
| 457 | |
| 458 remoting.clientSession = | |
| 459 new remoting.ClientSession( | |
| 460 remoting.hostJid, remoting.hostPublicKey, | |
| 461 pin, /** @type {string} */ (remoting.oauth2.getCachedEmail()), | |
| 462 onClientStateChange_); | |
| 463 remoting.clientSession.createPluginAndConnect( | |
| 464 document.getElementById('session-mode'), | |
| 465 token); | |
| 466 } else { | |
| 467 showConnectError_(remoting.Error.AUTHENTICATION_FAILED); | |
| 468 } | |
| 469 } | |
| OLD | NEW |