Chromium Code Reviews| Index: remoting/webapp/me2mom/client_screen.js |
| diff --git a/remoting/webapp/me2mom/client_screen.js b/remoting/webapp/me2mom/client_screen.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..6065672b9e34e6a95b4974e480b33f5ae55e87b7 |
| --- /dev/null |
| +++ b/remoting/webapp/me2mom/client_screen.js |
| @@ -0,0 +1,401 @@ |
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * @fileoverview |
| + * Functions related to the 'client screen' for Chromoting. |
| + */ |
| + |
| +'use strict'; |
| + |
| +/** @suppress {duplicate} */ |
| +var remoting = remoting || {}; |
| + |
| +/** @enum {string} */ |
| +remoting.ClientError = { |
| + NO_RESPONSE: /*i18n-content*/'ERROR_NO_RESPONSE', |
| + INVALID_ACCESS_CODE: /*i18n-content*/'ERROR_INVALID_ACCESS_CODE', |
| + MISSING_PLUGIN: /*i18n-content*/'ERROR_MISSING_PLUGIN', |
| + OAUTH_FETCH_FAILED: /*i18n-content*/'ERROR_AUTHENTICATION_FAILED', |
| + HOST_IS_OFFLINE: /*i18n-content*/'ERROR_HOST_IS_OFFLINE', |
| + INCOMPATIBLE_PROTOCOL: /*i18n-content*/'ERROR_INCOMPATIBLE_PROTOCOL', |
| + BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', |
| + OTHER_ERROR: /*i18n-content*/'ERROR_GENERIC' |
| +}; |
| + |
| +(function() { |
| + |
| +/** |
| + * @type {boolean} Whether or not the plugin should scale itself. |
| + */ |
| +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
|
| + |
| +/** |
| + * @type {remoting.ClientSession} The client session object, set once the |
| + * access code has been successfully verified. |
| + */ |
| +remoting.clientSession = null; |
| + |
| +/** |
| + * @type {string} The normalized access code. |
| + */ |
| +remoting.accessCode = ''; |
| + |
| +/** |
| + * @type {string} The host's JID, returned by the server. |
| + */ |
| +remoting.hostJid = ''; |
| + |
| +/** |
| + * @type {string} The host's public key, returned by the server. |
| + */ |
| +remoting.hostPublicKey = ''; |
| + |
| +/** |
| + * @type {XMLHttpRequest} The XHR object corresponding to the current |
| + * support-hosts request, if there is one outstanding. |
| + */ |
| +remoting.supportHostsXhr_ = null; |
| + |
| +/** |
| + * Entry point for the 'connect' functionality. This function checks for the |
| + * existence of an OAuth2 token, and either requests one asynchronously, or |
| + * calls through directly to tryConnectWithAccessToken_. |
| + */ |
| +remoting.tryConnect = function() { |
| + document.getElementById('cancel-button').disabled = false; |
| + if (remoting.oauth2.needsNewAccessToken()) { |
| + remoting.oauth2.refreshAccessToken(function(xhr) { |
| + if (remoting.oauth2.needsNewAccessToken()) { |
| + // Failed to get access token |
| + remoting.debug.log('tryConnect: OAuth2 token fetch failed'); |
| + showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); |
| + return; |
| + } |
| + tryConnectWithAccessToken_(); |
| + }); |
| + } else { |
| + tryConnectWithAccessToken_(); |
| + } |
| +} |
| + |
| +/** |
| + * Cancel an incomplete connect operation. |
| + * |
| + * @return {void} Nothing. |
| + */ |
| +remoting.cancelConnect = function() { |
| + if (remoting.supportHostsXhr_) { |
| + remoting.supportHostsXhr_.abort(); |
| + remoting.supportHostsXhr_ = null; |
| + } |
| + if (remoting.clientSession) { |
| + remoting.clientSession.removePlugin(); |
| + remoting.clientSession = null; |
| + } |
| + remoting.setMode(remoting.AppMode.HOME); |
| +} |
| + |
| +/** |
| + * Enable or disable scale-to-fit. |
| + * |
| + * @param {Element} button The scale-to-fit button. The style of this button is |
| + * updated to reflect the new scaling state. |
| + * @return {void} Nothing. |
| + */ |
| +remoting.toggleScaleToFit = function(button) { |
| + remoting.scaleToFit = !remoting.scaleToFit; |
| + if (remoting.scaleToFit) { |
| + addClass(button, 'toggle-button-active'); |
| + } else { |
| + removeClass(button, 'toggle-button-active'); |
| + } |
| + remoting.clientSession.updateDimensions(); |
| +} |
| + |
| +/** |
| + * Update the remoting client layout in response to a resize event. |
| + * |
| + * @return {void} Nothing. |
| + */ |
| +remoting.onResize = function() { |
| + if (remoting.clientSession) |
| + remoting.clientSession.onWindowSizeChanged(); |
| + recenterToolbar_(); |
| +} |
| + |
| +/** |
| + * Disconnect the remoting client. |
| + * |
| + * @return {void} Nothing. |
| + */ |
| +remoting.disconnect = function() { |
| + if (remoting.clientSession) { |
| + remoting.clientSession.disconnect(); |
| + remoting.clientSession = null; |
| + remoting.debug.log('Disconnected.'); |
| + remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); |
| + } |
| +} |
| + |
| +/** |
| + * Second stage of the 'connect' functionality. Once an access token is |
| + * available, load the WCS widget asynchronously and call through to |
| + * tryConnectWithWcs_ when ready. |
| + */ |
| +function tryConnectWithAccessToken_() { |
| + if (!remoting.wcsLoader) { |
| + remoting.wcsLoader = new remoting.WcsLoader(); |
| + } |
| + /** @param {function(string):void} setToken The callback function. */ |
| + var callWithToken = function(setToken) { |
| + remoting.oauth2.callWithToken(setToken); |
| + }; |
| + remoting.wcsLoader.start( |
| + remoting.oauth2.getAccessToken(), |
| + callWithToken, |
| + tryConnectWithWcs_); |
| +} |
| + |
| +/** |
| + * Final stage of the 'connect' functionality, called when the wcs widget has |
| + * been loaded, or on error. |
| + * |
| + * @param {boolean} success True if the script was loaded successfully. |
| + */ |
| +function tryConnectWithWcs_(success) { |
| + if (success) { |
| + var accessCode = document.getElementById('access-code-entry').value; |
| + remoting.accessCode = normalizeAccessCode_(accessCode); |
| + // At present, only 12-digit access codes are supported, of which the first |
| + // 7 characters are the supportId. |
| + var kSupportIdLen = 7; |
| + var kHostSecretLen = 5; |
| + var kAccessCodeLen = kSupportIdLen + kHostSecretLen; |
| + if (remoting.accessCode.length != kAccessCodeLen) { |
| + remoting.debug.log('Bad access code length'); |
| + showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); |
| + } else { |
| + var supportId = remoting.accessCode.substring(0, kSupportIdLen); |
| + remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); |
| + resolveSupportId(supportId); |
| + } |
| + } else { |
| + showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); |
| + } |
| +} |
| + |
| +/** |
| + * Callback function called when the state of the client plugin changes. The |
| + * current state is available via the |state| member variable. |
| + * |
| + * @param {number} oldState The previous state of the plugin. |
| + */ |
| +// TODO(jamiewalch): Make this pass both the current and old states to avoid |
| +// race conditions. |
| +function onClientStateChange_(oldState) { |
| + if (!remoting.clientSession) { |
| + // If the connection has been cancelled, then we no longer have a reference |
| + // to the session object and should ignore any state changes. |
| + return; |
| + } |
| + var state = remoting.clientSession.state; |
| + if (state == remoting.ClientSession.State.CREATED) { |
| + remoting.debug.log('Created plugin'); |
| + |
| + } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { |
| + showConnectError_(remoting.ClientError.BAD_PLUGIN_VERSION); |
| + |
| + } else if (state == remoting.ClientSession.State.CONNECTING) { |
| + remoting.debug.log('Connecting as ' + remoting.oauth2.getCachedEmail()); |
| + |
| + } else if (state == remoting.ClientSession.State.INITIALIZING) { |
| + remoting.debug.log('Initializing connection'); |
| + |
| + } else if (state == remoting.ClientSession.State.CONNECTED) { |
| + if (remoting.clientSession) { |
| + remoting.setMode(remoting.AppMode.IN_SESSION); |
| + recenterToolbar_(); |
| + showToolbarPreview_(); |
| + updateStatistics_(); |
| + } |
| + |
| + } else if (state == remoting.ClientSession.State.CLOSED) { |
| + if (oldState == remoting.ClientSession.State.CONNECTED) { |
| + remoting.clientSession.removePlugin(); |
| + remoting.clientSession = null; |
| + remoting.debug.log('Connection closed by host'); |
| + remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); |
| + } else { |
| + // The transition from CONNECTING to CLOSED state may happen |
| + // only with older client plugins. Current version should go the |
| + // FAILED state when connection fails. |
| + showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); |
| + } |
| + |
| + } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { |
| + remoting.debug.log('Client plugin reported connection failed: ' + |
| + remoting.clientSession.error); |
| + if (remoting.clientSession.error == |
| + remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { |
| + showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); |
| + } else if (remoting.clientSession.error == |
| + remoting.ClientSession.ConnectionError.SESSION_REJECTED) { |
| + showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); |
| + } else if (remoting.clientSession.error == |
| + remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { |
| + showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); |
| + } else if (remoting.clientSession.error == |
| + remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { |
| + showConnectError_(remoting.ClientError.OTHER_ERROR); |
| + } else { |
| + showConnectError_(remoting.ClientError.OTHER_ERROR); |
| + } |
| + |
| + } else { |
| + remoting.debug.log('Unexpected client plugin state: ' + state); |
| + // This should only happen if the web-app and client plugin get out of |
| + // sync, and even then the version check should allow compatibility. |
| + showConnectError_(remoting.ClientError.MISSING_PLUGIN); |
| + } |
| +} |
| + |
| +/** |
| + * Create the client session object and initiate the connection. |
| + * |
| + * @return {void} Nothing. |
| + */ |
| +function startSession_() { |
| + remoting.debug.log('Starting session...'); |
| + var accessCode = document.getElementById('access-code-entry'); |
| + accessCode.value = ''; // The code has been validated and won't work again. |
| + remoting.clientSession = |
| + new remoting.ClientSession( |
| + remoting.hostJid, remoting.hostPublicKey, |
| + remoting.accessCode, |
| + /** @type {string} */ (remoting.oauth2.getCachedEmail()), |
| + onClientStateChange_); |
| + /** @param {string} token The auth token. */ |
| + var createPluginAndConnect = function(token) { |
| + remoting.clientSession.createPluginAndConnect( |
| + document.getElementById('session-mode'), |
| + token); |
| + }; |
| + remoting.oauth2.callWithToken(createPluginAndConnect); |
| +} |
| + |
| +/** |
| + * Show a client-side error message. |
| + * |
| + * @param {remoting.ClientError} errorTag The error to be localized and |
| + * displayed. |
| + * @return {void} Nothing. |
| + */ |
| +function showConnectError_(errorTag) { |
| + remoting.debug.log('Connection failed: ' + errorTag); |
| + var errorDiv = document.getElementById('connect-error-message'); |
| + l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); |
| + remoting.accessCode = ''; |
| + if (remoting.clientSession) { |
| + remoting.clientSession.disconnect(); |
| + remoting.clientSession = null; |
| + } |
| + remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); |
| +} |
| + |
| +/** |
| + * Parse the response from the server to a request to resolve a support id. |
| + * |
| + * @param {XMLHttpRequest} xhr The XMLHttpRequest object. |
| + * @return {void} Nothing. |
| + */ |
| +function parseServerResponse_(xhr) { |
| + remoting.supportHostsXhr_ = null; |
| + remoting.debug.log('parseServerResponse: status = ' + xhr.status); |
| + if (xhr.status == 200) { |
| + var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ |
| + JSON.parse(xhr.responseText); |
| + if (host.data && host.data.jabberId && host.data.publicKey) { |
| + remoting.hostJid = host.data.jabberId; |
| + remoting.hostPublicKey = host.data.publicKey; |
| + var split = remoting.hostJid.split('/'); |
| + document.getElementById('connected-to').innerText = split[0]; |
| + startSession_(); |
| + return; |
| + } |
| + } |
| + var errorMsg = remoting.ClientError.OTHER_ERROR; |
| + if (xhr.status == 404) { |
| + errorMsg = remoting.ClientError.INVALID_ACCESS_CODE; |
| + } else if (xhr.status == 0) { |
| + errorMsg = remoting.ClientError.NO_RESPONSE; |
| + } else { |
| + remoting.debug.log('The server responded: ' + xhr.responseText); |
| + } |
| + showConnectError_(errorMsg); |
| +} |
| + |
| +/** |
| + * Normalize the access code entered by the user. |
| + * |
| + * @param {string} accessCode The access code, as entered by the user. |
| + * @return {string} The normalized form of the code (whitespace removed). |
| + */ |
| +function normalizeAccessCode_(accessCode) { |
| + // Trim whitespace. |
| + // TODO(sergeyu): Do we need to do any other normalization here? |
| + return accessCode.replace(/\s/g, ''); |
| +} |
| + |
| +/** |
| + * Initiate a request to the server to resolve a support ID. |
| + * |
| + * @param {string} supportId The canonicalized support ID. |
| + */ |
| +function resolveSupportId(supportId) { |
| + var headers = { |
| + 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() |
| + }; |
| + |
| + remoting.supportHostsXhr_ = remoting.xhr.get( |
| + 'https://www.googleapis.com/chromoting/v1/support-hosts/' + |
| + encodeURIComponent(supportId), |
| + parseServerResponse_, |
| + '', |
| + headers); |
| +} |
| + |
| +/** |
| + * Timer callback to update the statistics panel. |
| + */ |
| +function updateStatistics_() { |
| + if (!remoting.clientSession || |
| + remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) { |
| + return; |
| + } |
| + remoting.debug.updateStatistics(remoting.clientSession.stats()); |
| + // Update the stats once per second. |
| + window.setTimeout(updateStatistics_, 1000); |
| +} |
| + |
| +/** |
| + * Force-show the tool-bar for three seconds to aid discoverability. |
| + */ |
| +function showToolbarPreview_() { |
| + var toolbar = document.getElementById('session-toolbar'); |
| + addClass(toolbar, 'toolbar-preview'); |
| + window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); |
| +} |
| + |
| +/** |
| + * Update the horizontal position of the tool-bar to center it. |
| + */ |
| +function recenterToolbar_() { |
| + var toolbar = document.getElementById('session-toolbar'); |
| + var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; |
| + toolbar.style['left'] = toolbarX + 'px'; |
| +} |
| + |
| + |
| +}()); |