Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright (c) 2011 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 /** @enum {string} */ | |
| 16 remoting.ClientError = { | |
| 17 NO_RESPONSE: /*i18n-content*/'ERROR_NO_RESPONSE', | |
| 18 INVALID_ACCESS_CODE: /*i18n-content*/'ERROR_INVALID_ACCESS_CODE', | |
| 19 MISSING_PLUGIN: /*i18n-content*/'ERROR_MISSING_PLUGIN', | |
| 20 OAUTH_FETCH_FAILED: /*i18n-content*/'ERROR_AUTHENTICATION_FAILED', | |
| 21 HOST_IS_OFFLINE: /*i18n-content*/'ERROR_HOST_IS_OFFLINE', | |
| 22 INCOMPATIBLE_PROTOCOL: /*i18n-content*/'ERROR_INCOMPATIBLE_PROTOCOL', | |
| 23 BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', | |
| 24 OTHER_ERROR: /*i18n-content*/'ERROR_GENERIC' | |
| 25 }; | |
| 26 | |
| 27 (function() { | |
| 28 | |
| 29 /** | |
| 30 * @type {boolean} Whether or not the plugin should scale itself. | |
| 31 */ | |
| 32 remoting.scaleToFit = false; | |
|
garykac
2011/10/27 22:32:39
nit: I assume these should have underscores at the
Jamie
2011/10/27 22:51:37
No, anything exported to the remoting namespace is
| |
| 33 | |
| 34 /** | |
| 35 * @type {remoting.ClientSession} The client session object, set once the | |
| 36 * access code has been successfully verified. | |
| 37 */ | |
| 38 remoting.clientSession = null; | |
| 39 | |
| 40 /** | |
| 41 * @type {string} The normalized access code. | |
| 42 */ | |
| 43 remoting.accessCode = ''; | |
| 44 | |
| 45 /** | |
| 46 * @type {string} The host's JID, returned by the server. | |
| 47 */ | |
| 48 remoting.hostJid = ''; | |
| 49 | |
| 50 /** | |
| 51 * @type {string} The host's public key, returned by the server. | |
| 52 */ | |
| 53 remoting.hostPublicKey = ''; | |
| 54 | |
| 55 /** | |
| 56 * @type {XMLHttpRequest} The XHR object corresponding to the current | |
| 57 * support-hosts request, if there is one outstanding. | |
| 58 */ | |
| 59 remoting.supportHostsXhr_ = null; | |
| 60 | |
| 61 /** | |
| 62 * Entry point for the 'connect' functionality. This function checks for the | |
| 63 * existence of an OAuth2 token, and either requests one asynchronously, or | |
| 64 * calls through directly to tryConnectWithAccessToken_. | |
| 65 */ | |
| 66 remoting.tryConnect = function() { | |
| 67 document.getElementById('cancel-button').disabled = false; | |
| 68 if (remoting.oauth2.needsNewAccessToken()) { | |
| 69 remoting.oauth2.refreshAccessToken(function(xhr) { | |
| 70 if (remoting.oauth2.needsNewAccessToken()) { | |
| 71 // Failed to get access token | |
| 72 remoting.debug.log('tryConnect: OAuth2 token fetch failed'); | |
| 73 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
| 74 return; | |
| 75 } | |
| 76 tryConnectWithAccessToken_(); | |
| 77 }); | |
| 78 } else { | |
| 79 tryConnectWithAccessToken_(); | |
| 80 } | |
| 81 } | |
| 82 | |
| 83 /** | |
| 84 * Cancel an incomplete connect operation. | |
| 85 * | |
| 86 * @return {void} Nothing. | |
| 87 */ | |
| 88 remoting.cancelConnect = function() { | |
| 89 if (remoting.supportHostsXhr_) { | |
| 90 remoting.supportHostsXhr_.abort(); | |
| 91 remoting.supportHostsXhr_ = null; | |
| 92 } | |
| 93 if (remoting.clientSession) { | |
| 94 remoting.clientSession.removePlugin(); | |
| 95 remoting.clientSession = null; | |
| 96 } | |
| 97 remoting.setMode(remoting.AppMode.HOME); | |
| 98 } | |
| 99 | |
| 100 /** | |
| 101 * Enable or disable scale-to-fit. | |
| 102 * | |
| 103 * @param {Element} button The scale-to-fit button. The style of this button is | |
| 104 * updated to reflect the new scaling state. | |
| 105 * @return {void} Nothing. | |
| 106 */ | |
| 107 remoting.toggleScaleToFit = function(button) { | |
| 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.onWindowSizeChanged(); | |
| 125 recenterToolbar_(); | |
| 126 } | |
| 127 | |
| 128 /** | |
| 129 * Disconnect the remoting client. | |
| 130 * | |
| 131 * @return {void} Nothing. | |
| 132 */ | |
| 133 remoting.disconnect = function() { | |
| 134 if (remoting.clientSession) { | |
| 135 remoting.clientSession.disconnect(); | |
| 136 remoting.clientSession = null; | |
| 137 remoting.debug.log('Disconnected.'); | |
| 138 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 /** | |
| 143 * Second stage of the 'connect' functionality. Once an access token is | |
| 144 * available, load the WCS widget asynchronously and call through to | |
| 145 * tryConnectWithWcs_ when ready. | |
| 146 */ | |
| 147 function tryConnectWithAccessToken_() { | |
| 148 if (!remoting.wcsLoader) { | |
| 149 remoting.wcsLoader = new remoting.WcsLoader(); | |
| 150 } | |
| 151 /** @param {function(string):void} setToken The callback function. */ | |
| 152 var callWithToken = function(setToken) { | |
| 153 remoting.oauth2.callWithToken(setToken); | |
| 154 }; | |
| 155 remoting.wcsLoader.start( | |
| 156 remoting.oauth2.getAccessToken(), | |
| 157 callWithToken, | |
| 158 tryConnectWithWcs_); | |
| 159 } | |
| 160 | |
| 161 /** | |
| 162 * Final stage of the 'connect' functionality, called when the wcs widget has | |
| 163 * been loaded, or on error. | |
| 164 * | |
| 165 * @param {boolean} success True if the script was loaded successfully. | |
| 166 */ | |
| 167 function tryConnectWithWcs_(success) { | |
| 168 if (success) { | |
| 169 var accessCode = document.getElementById('access-code-entry').value; | |
| 170 remoting.accessCode = normalizeAccessCode_(accessCode); | |
| 171 // At present, only 12-digit access codes are supported, of which the first | |
| 172 // 7 characters are the supportId. | |
| 173 var kSupportIdLen = 7; | |
| 174 var kHostSecretLen = 5; | |
| 175 var kAccessCodeLen = kSupportIdLen + kHostSecretLen; | |
| 176 if (remoting.accessCode.length != kAccessCodeLen) { | |
| 177 remoting.debug.log('Bad access code length'); | |
| 178 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 179 } else { | |
| 180 var supportId = remoting.accessCode.substring(0, kSupportIdLen); | |
| 181 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
| 182 resolveSupportId(supportId); | |
| 183 } | |
| 184 } else { | |
| 185 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
| 186 } | |
| 187 } | |
| 188 | |
| 189 /** | |
| 190 * Callback function called when the state of the client plugin changes. The | |
| 191 * current state is available via the |state| member variable. | |
| 192 * | |
| 193 * @param {number} oldState The previous state of the plugin. | |
| 194 */ | |
| 195 // TODO(jamiewalch): Make this pass both the current and old states to avoid | |
| 196 // race conditions. | |
| 197 function onClientStateChange_(oldState) { | |
| 198 if (!remoting.clientSession) { | |
| 199 // If the connection has been cancelled, then we no longer have a reference | |
| 200 // to the session object and should ignore any state changes. | |
| 201 return; | |
| 202 } | |
| 203 var state = remoting.clientSession.state; | |
| 204 if (state == remoting.ClientSession.State.CREATED) { | |
| 205 remoting.debug.log('Created plugin'); | |
| 206 | |
| 207 } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { | |
| 208 showConnectError_(remoting.ClientError.BAD_PLUGIN_VERSION); | |
| 209 | |
| 210 } else if (state == remoting.ClientSession.State.CONNECTING) { | |
| 211 remoting.debug.log('Connecting as ' + remoting.oauth2.getCachedEmail()); | |
| 212 | |
| 213 } else if (state == remoting.ClientSession.State.INITIALIZING) { | |
| 214 remoting.debug.log('Initializing connection'); | |
| 215 | |
| 216 } else if (state == remoting.ClientSession.State.CONNECTED) { | |
| 217 if (remoting.clientSession) { | |
| 218 remoting.setMode(remoting.AppMode.IN_SESSION); | |
| 219 recenterToolbar_(); | |
| 220 showToolbarPreview_(); | |
| 221 updateStatistics_(); | |
| 222 } | |
| 223 | |
| 224 } else if (state == remoting.ClientSession.State.CLOSED) { | |
| 225 if (oldState == remoting.ClientSession.State.CONNECTED) { | |
| 226 remoting.clientSession.removePlugin(); | |
| 227 remoting.clientSession = null; | |
| 228 remoting.debug.log('Connection closed by host'); | |
| 229 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
| 230 } else { | |
| 231 // The transition from CONNECTING to CLOSED state may happen | |
| 232 // only with older client plugins. Current version should go the | |
| 233 // FAILED state when connection fails. | |
| 234 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 235 } | |
| 236 | |
| 237 } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { | |
| 238 remoting.debug.log('Client plugin reported connection failed: ' + | |
| 239 remoting.clientSession.error); | |
| 240 if (remoting.clientSession.error == | |
| 241 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { | |
| 242 showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); | |
| 243 } else if (remoting.clientSession.error == | |
| 244 remoting.ClientSession.ConnectionError.SESSION_REJECTED) { | |
| 245 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
| 246 } else if (remoting.clientSession.error == | |
| 247 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { | |
| 248 showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); | |
| 249 } else if (remoting.clientSession.error == | |
| 250 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { | |
| 251 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
| 252 } else { | |
| 253 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
| 254 } | |
| 255 | |
| 256 } else { | |
| 257 remoting.debug.log('Unexpected client plugin state: ' + state); | |
| 258 // This should only happen if the web-app and client plugin get out of | |
| 259 // sync, and even then the version check should allow compatibility. | |
| 260 showConnectError_(remoting.ClientError.MISSING_PLUGIN); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 /** | |
| 265 * Create the client session object and initiate the connection. | |
| 266 * | |
| 267 * @return {void} Nothing. | |
| 268 */ | |
| 269 function startSession_() { | |
| 270 remoting.debug.log('Starting session...'); | |
| 271 var accessCode = document.getElementById('access-code-entry'); | |
| 272 accessCode.value = ''; // The code has been validated and won't work again. | |
| 273 remoting.clientSession = | |
| 274 new remoting.ClientSession( | |
| 275 remoting.hostJid, remoting.hostPublicKey, | |
| 276 remoting.accessCode, | |
| 277 /** @type {string} */ (remoting.oauth2.getCachedEmail()), | |
| 278 onClientStateChange_); | |
| 279 /** @param {string} token The auth token. */ | |
| 280 var createPluginAndConnect = function(token) { | |
| 281 remoting.clientSession.createPluginAndConnect( | |
| 282 document.getElementById('session-mode'), | |
| 283 token); | |
| 284 }; | |
| 285 remoting.oauth2.callWithToken(createPluginAndConnect); | |
| 286 } | |
| 287 | |
| 288 /** | |
| 289 * Show a client-side error message. | |
| 290 * | |
| 291 * @param {remoting.ClientError} errorTag The error to be localized and | |
| 292 * displayed. | |
| 293 * @return {void} Nothing. | |
| 294 */ | |
| 295 function showConnectError_(errorTag) { | |
| 296 remoting.debug.log('Connection failed: ' + errorTag); | |
| 297 var errorDiv = document.getElementById('connect-error-message'); | |
| 298 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); | |
| 299 remoting.accessCode = ''; | |
| 300 if (remoting.clientSession) { | |
| 301 remoting.clientSession.disconnect(); | |
| 302 remoting.clientSession = null; | |
| 303 } | |
| 304 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); | |
| 305 } | |
| 306 | |
| 307 /** | |
| 308 * Parse the response from the server to a request to resolve a support id. | |
| 309 * | |
| 310 * @param {XMLHttpRequest} xhr The XMLHttpRequest object. | |
| 311 * @return {void} Nothing. | |
| 312 */ | |
| 313 function parseServerResponse_(xhr) { | |
| 314 remoting.supportHostsXhr_ = null; | |
| 315 remoting.debug.log('parseServerResponse: status = ' + xhr.status); | |
| 316 if (xhr.status == 200) { | |
| 317 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ | |
| 318 JSON.parse(xhr.responseText); | |
| 319 if (host.data && host.data.jabberId && host.data.publicKey) { | |
| 320 remoting.hostJid = host.data.jabberId; | |
| 321 remoting.hostPublicKey = host.data.publicKey; | |
| 322 var split = remoting.hostJid.split('/'); | |
| 323 document.getElementById('connected-to').innerText = split[0]; | |
| 324 startSession_(); | |
| 325 return; | |
| 326 } | |
| 327 } | |
| 328 var errorMsg = remoting.ClientError.OTHER_ERROR; | |
| 329 if (xhr.status == 404) { | |
| 330 errorMsg = remoting.ClientError.INVALID_ACCESS_CODE; | |
| 331 } else if (xhr.status == 0) { | |
| 332 errorMsg = remoting.ClientError.NO_RESPONSE; | |
| 333 } else { | |
| 334 remoting.debug.log('The server responded: ' + xhr.responseText); | |
| 335 } | |
| 336 showConnectError_(errorMsg); | |
| 337 } | |
| 338 | |
| 339 /** | |
| 340 * Normalize the access code entered by the user. | |
| 341 * | |
| 342 * @param {string} accessCode The access code, as entered by the user. | |
| 343 * @return {string} The normalized form of the code (whitespace removed). | |
| 344 */ | |
| 345 function normalizeAccessCode_(accessCode) { | |
| 346 // Trim whitespace. | |
| 347 // TODO(sergeyu): Do we need to do any other normalization here? | |
| 348 return accessCode.replace(/\s/g, ''); | |
| 349 } | |
| 350 | |
| 351 /** | |
| 352 * Initiate a request to the server to resolve a support ID. | |
| 353 * | |
| 354 * @param {string} supportId The canonicalized support ID. | |
| 355 */ | |
| 356 function resolveSupportId(supportId) { | |
| 357 var headers = { | |
| 358 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
| 359 }; | |
| 360 | |
| 361 remoting.supportHostsXhr_ = remoting.xhr.get( | |
| 362 'https://www.googleapis.com/chromoting/v1/support-hosts/' + | |
| 363 encodeURIComponent(supportId), | |
| 364 parseServerResponse_, | |
| 365 '', | |
| 366 headers); | |
| 367 } | |
| 368 | |
| 369 /** | |
| 370 * Timer callback to update the statistics panel. | |
| 371 */ | |
| 372 function updateStatistics_() { | |
| 373 if (!remoting.clientSession || | |
| 374 remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) { | |
| 375 return; | |
| 376 } | |
| 377 remoting.debug.updateStatistics(remoting.clientSession.stats()); | |
| 378 // Update the stats once per second. | |
| 379 window.setTimeout(updateStatistics_, 1000); | |
| 380 } | |
| 381 | |
| 382 /** | |
| 383 * Force-show the tool-bar for three seconds to aid discoverability. | |
| 384 */ | |
| 385 function showToolbarPreview_() { | |
| 386 var toolbar = document.getElementById('session-toolbar'); | |
| 387 addClass(toolbar, 'toolbar-preview'); | |
| 388 window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); | |
| 389 } | |
| 390 | |
| 391 /** | |
| 392 * Update the horizontal position of the tool-bar to center it. | |
| 393 */ | |
| 394 function recenterToolbar_() { | |
| 395 var toolbar = document.getElementById('session-toolbar'); | |
| 396 var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; | |
| 397 toolbar.style['left'] = toolbarX + 'px'; | |
| 398 } | |
| 399 | |
| 400 | |
| 401 }()); | |
| OLD | NEW |