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; |
| 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 |