diff options
author | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-28 01:29:01 +0000 |
---|---|---|
committer | jamiewalch@chromium.org <jamiewalch@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-28 01:29:01 +0000 |
commit | 2fa3126208495ca01c7f9f1ed353d8b45a775087 (patch) | |
tree | 235a1c9a281ad8d988a656b37d600c8d7297f2f6 /remoting | |
parent | c9f0a0e41e82a5dbd60213d6a894f47c757c5bef (diff) | |
download | chromium_src-2fa3126208495ca01c7f9f1ed353d8b45a775087.zip chromium_src-2fa3126208495ca01c7f9f1ed353d8b45a775087.tar.gz chromium_src-2fa3126208495ca01c7f9f1ed353d8b45a775087.tar.bz2 |
Refactored web-app
BUG=None
TEST=Everything still works!
Review URL: http://codereview.chromium.org/8416007
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@107673 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/host/plugin/host_script_object.h | 2 | ||||
-rw-r--r-- | remoting/remoting.gyp | 9 | ||||
-rw-r--r-- | remoting/webapp/me2mom/choice.html | 5 | ||||
-rw-r--r-- | remoting/webapp/me2mom/client_screen.js | 401 | ||||
-rw-r--r-- | remoting/webapp/me2mom/client_session.js | 2 | ||||
-rw-r--r-- | remoting/webapp/me2mom/debug_log.js | 77 | ||||
-rw-r--r-- | remoting/webapp/me2mom/host_screen.js | 269 | ||||
-rw-r--r-- | remoting/webapp/me2mom/host_session.js | 116 | ||||
-rw-r--r-- | remoting/webapp/me2mom/oauth2.js | 61 | ||||
-rw-r--r-- | remoting/webapp/me2mom/remoting.js | 822 | ||||
-rw-r--r-- | remoting/webapp/me2mom/ui_mode.js | 78 | ||||
-rw-r--r-- | remoting/webapp/me2mom/util.js | 40 | ||||
-rw-r--r-- | remoting/webapp/me2mom/wcs_loader.js | 2 |
13 files changed, 1111 insertions, 773 deletions
diff --git a/remoting/host/plugin/host_script_object.h b/remoting/host/plugin/host_script_object.h index bfcdf31..da387ce 100644 --- a/remoting/host/plugin/host_script_object.h +++ b/remoting/host/plugin/host_script_object.h @@ -84,6 +84,8 @@ class HostNPScriptObject : public HostStatusObserver { void PostLogDebugInfo(const std::string& message); private: + // These state values are duplicated in the JS code. Remember to update both + // copies when making changes. enum State { kDisconnected, kStarting, diff --git a/remoting/remoting.gyp b/remoting/remoting.gyp index f6ad2ad..a454229 100644 --- a/remoting/remoting.gyp +++ b/remoting/remoting.gyp @@ -97,12 +97,15 @@ 'resources/icon_warning.png', 'webapp/me2mom/choice.css', 'webapp/me2mom/choice.html', + 'webapp/me2mom/client_screen.js', 'webapp/me2mom/client_session.js', 'webapp/me2mom/cs_oauth2_trampoline.js', 'webapp/me2mom/debug_log.css', 'webapp/me2mom/debug_log.js', 'webapp/me2mom/dividerbottom.png', 'webapp/me2mom/dividertop.png', + 'webapp/me2mom/host_screen.js', + 'webapp/me2mom/host_session.js', 'webapp/me2mom/l10n.js', 'webapp/me2mom/main.css', 'webapp/me2mom/manifest.json', @@ -113,6 +116,8 @@ 'webapp/me2mom/scale-to-fit.png', 'webapp/me2mom/spinner.gif', 'webapp/me2mom/toolbar.css', + 'webapp/me2mom/ui_mode.js', + 'webapp/me2mom/util.js', 'webapp/me2mom/wcs.js', 'webapp/me2mom/wcs_loader.js', 'webapp/me2mom/xhr.js', @@ -324,6 +329,8 @@ 'webapp/me2mom/choice.html', 'webapp/me2mom/manifest.json', 'webapp/me2mom/remoting.js', + 'webapp/me2mom/client_screen.js', + 'webapp/me2mom/host_screen.js', 'host/plugin/host_script_object.cc', ], 'outputs': [ @@ -337,6 +344,8 @@ 'webapp/me2mom/choice.html', 'webapp/me2mom/manifest.json', 'webapp/me2mom/remoting.js', + 'webapp/me2mom/client_screen.js', + 'webapp/me2mom/host_screen.js', 'host/plugin/host_script_object.cc', ], }, diff --git a/remoting/webapp/me2mom/choice.html b/remoting/webapp/me2mom/choice.html index c08b375..9f53088 100644 --- a/remoting/webapp/me2mom/choice.html +++ b/remoting/webapp/me2mom/choice.html @@ -15,12 +15,17 @@ found in the LICENSE file. <link rel="stylesheet" href="main.css" /> <link rel="stylesheet" href="choice.css" /> <link rel="stylesheet" href="toolbar.css" /> + <script src="client_screen.js"></script> <script src="client_session.js"></script> <script src="debug_log.js"></script> + <script src="host_screen.js"></script> + <script src="host_session.js"></script> <script src="l10n.js"></script> <script src="oauth2.js"></script> <script src="plugin_settings.js"></script> <script src="remoting.js"></script> + <script src="ui_mode.js"></script> + <script src="util.js"></script> <script src="xhr.js"></script> <script src="wcs.js"></script> <script src="wcs_loader.js"></script> diff --git a/remoting/webapp/me2mom/client_screen.js b/remoting/webapp/me2mom/client_screen.js new file mode 100644 index 0000000..6065672 --- /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; + +/** + * @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'; +} + + +}()); diff --git a/remoting/webapp/me2mom/client_session.js b/remoting/webapp/me2mom/client_session.js index 5afa4d2..9ec8b4c 100644 --- a/remoting/webapp/me2mom/client_session.js +++ b/remoting/webapp/me2mom/client_session.js @@ -4,7 +4,7 @@ /** * @fileoverview - * Session class that handles creation and teardown of a remoting session. + * Class handling creation and teardown of a remoting client session. * * This abstracts a <embed> element and controls the plugin which does the * actual remoting work. There should be no UI code inside this class. It diff --git a/remoting/webapp/me2mom/debug_log.js b/remoting/webapp/me2mom/debug_log.js index 3d79fdd..94cc6e5 100644 --- a/remoting/webapp/me2mom/debug_log.js +++ b/remoting/webapp/me2mom/debug_log.js @@ -15,13 +15,17 @@ var remoting = remoting || {}; /** * @constructor * @param {Element} logElement The HTML div to which to add log messages. + * @param {Element} statsElement The HTML div to which to update stats. */ -remoting.DebugLog = function(logElement) { - this.debugLog = logElement; +remoting.DebugLog = function(logElement, statsElement) { + this.logElement = logElement; + this.statsElement = statsElement; }; -/** Maximum number of lines to record in the debug log. Only the most - * recent <n> lines are displayed. */ +/** + * Maximum number of lines to record in the debug log. Only the most + * recent <n> lines are displayed. + */ remoting.DebugLog.prototype.MAX_DEBUG_LOG_SIZE = 1000; /** @@ -31,18 +35,75 @@ remoting.DebugLog.prototype.MAX_DEBUG_LOG_SIZE = 1000; */ remoting.DebugLog.prototype.log = function(message) { // Remove lines from top if we've hit our max log size. - if (this.debugLog.childNodes.length == this.MAX_DEBUG_LOG_SIZE) { - this.debugLog.removeChild(this.debugLog.firstChild); + if (this.logElement.childNodes.length == this.MAX_DEBUG_LOG_SIZE) { + this.logElement.removeChild(this.logElement.firstChild); } // Add the new <p> to the end of the debug log. var p = document.createElement('p'); p.appendChild(document.createTextNode(message)); - this.debugLog.appendChild(p); + this.logElement.appendChild(p); // Scroll to bottom of div - this.debugLog.scrollTop = this.debugLog.scrollHeight; + this.logElement.scrollTop = this.logElement.scrollHeight; +}; + +/** + * Show or hide the debug log. + */ +remoting.DebugLog.prototype.toggle = function() { + var debugLog = /** @type {Element} */ this.logElement.parentNode; + if (debugLog.hidden) { + debugLog.hidden = false; + } else { + debugLog.hidden = true; + } +}; + +/** + * Update the statistics panel. + * @param {Object.<string, number>} stats The connection statistics. + */ +remoting.DebugLog.prototype.updateStatistics = function(stats) { + var units = ''; + var videoBandwidth = stats['video_bandwidth']; + if (videoBandwidth < 1024) { + units = 'Bps'; + } else if (videoBandwidth < 1048576) { + units = 'KiBps'; + videoBandwidth = videoBandwidth / 1024; + } else if (videoBandwidth < 1073741824) { + units = 'MiBps'; + videoBandwidth = videoBandwidth / 1048576; + } else { + units = 'GiBps'; + videoBandwidth = videoBandwidth / 1073741824; + } + + var statistics = document.getElementById('statistics'); + this.statsElement.innerText = + 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + + ', Frame Rate: ' + + (stats['video_frame_rate'] ? + stats['video_frame_rate'].toFixed(2) + ' fps' : 'n/a') + + ', Capture: ' + stats['capture_latency'].toFixed(2) + 'ms' + + ', Encode: ' + stats['encode_latency'].toFixed(2) + 'ms' + + ', Decode: ' + stats['decode_latency'].toFixed(2) + 'ms' + + ', Render: ' + stats['render_latency'].toFixed(2) + 'ms' + + ', Latency: ' + stats['roundtrip_latency'].toFixed(2) + 'ms'; }; +/** + * Check for the debug toggle hot-key. + * + * @param {Event} event The keyboard event. + * @return {void} Nothing. + */ +remoting.DebugLog.onKeydown = function(event) { + if (String.fromCharCode(event.which) == 'D') { + remoting.debug.toggle(); + } +} + /** @type {remoting.DebugLog} */ remoting.debug = null; diff --git a/remoting/webapp/me2mom/host_screen.js b/remoting/webapp/me2mom/host_screen.js new file mode 100644 index 0000000..87f614a --- /dev/null +++ b/remoting/webapp/me2mom/host_screen.js @@ -0,0 +1,269 @@ +// 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 'host screen' for Chromoting. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +(function() { + +/** + * @type {boolean} Whether or not the last share was cancelled by the user. + * This controls what screen is shown when the host plugin signals + * completion. + */ +var lastShareWasCancelled_ = false; + +/** + * Start a host session. This is the main entry point for the host screen, + * called directly from the onclick action of a button on the home screen. + */ +remoting.tryShare = function() { + remoting.debug.log('Attempting to share...'); + lastShareWasCancelled_ = false; + if (remoting.oauth2.needsNewAccessToken()) { + remoting.debug.log('Refreshing token...'); + remoting.oauth2.refreshAccessToken(function() { + if (remoting.oauth2.needsNewAccessToken()) { + // If we still need it, we're going to infinite loop. + showShareError_(/*i18n-content*/'ERROR_AUTHENTICATION_FAILED'); + throw 'Unable to get access token'; + } + remoting.tryShare(); + }); + return; + } + + onNatTraversalPolicyChanged_(true); // Hide warning by default. + remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); + document.getElementById('cancel-button').disabled = false; + disableTimeoutCountdown_(); + + var div = document.getElementById('host-plugin-container'); + remoting.hostSession = new remoting.HostSession(); + remoting.hostSession.createPluginAndConnect( + document.getElementById('host-plugin-container'), + /** @type {string} */(remoting.oauth2.getCachedEmail()), + remoting.oauth2.getAccessToken(), + onNatTraversalPolicyChanged_, + onHostStateChanged_, + logDebugInfo_); +}; + +/** + * Callback for the host plugin to notify the web app of state changes. + * @param {remoting.HostSession.State} state The new state of the plugin. + */ +function onHostStateChanged_(state) { + if (state == remoting.HostSession.State.STARTING) { + // Nothing to do here. + remoting.debug.log('Host plugin state: STARTING'); + + } else if (state == remoting.HostSession.State.REQUESTED_ACCESS_CODE) { + // Nothing to do here. + remoting.debug.log('Host plugin state: REQUESTED_ACCESS_CODE'); + + } else if (state == remoting.HostSession.State.RECEIVED_ACCESS_CODE) { + remoting.debug.log('Host plugin state: RECEIVED_ACCESS_CODE'); + var accessCode = remoting.hostSession.getAccessCode(); + var accessCodeDisplay = document.getElementById('access-code-display'); + accessCodeDisplay.innerText = ''; + // Display the access code in groups of four digits for readability. + var kDigitsPerGroup = 4; + for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) { + var nextFourDigits = document.createElement('span'); + nextFourDigits.className = 'access-code-digit-group'; + nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup); + accessCodeDisplay.appendChild(nextFourDigits); + } + accessCodeExpiresIn_ = remoting.hostSession.getAccessCodeLifetime(); + if (accessCodeExpiresIn_ > 0) { // Check it hasn't expired. + accessCodeTimerId_ = setInterval( + 'remoting.decrementAccessCodeTimeout_()', 1000); + timerRunning_ = true; + updateAccessCodeTimeoutElement_(); + updateTimeoutStyles_(); + remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION); + } else { + // This can only happen if the cloud tells us that the code lifetime is + // <= 0s, which shouldn't happen so we don't care how clean this UX is. + remoting.debug.log('Access code already invalid on receipt!'); + remoting.cancelShare(); + } + + } else if (state == remoting.HostSession.State.CONNECTED) { + remoting.debug.log('Host plugin state: CONNECTED'); + var element = document.getElementById('host-shared-message'); + var client = remoting.hostSession.getClient(); + l10n.localizeElement(element, client); + remoting.setMode(remoting.AppMode.HOST_SHARED); + disableTimeoutCountdown_(); + + } else if (state == remoting.HostSession.State.DISCONNECTING) { + remoting.debug.log('Host plugin state: DISCONNECTING'); + + } else if (state == remoting.HostSession.State.DISCONNECTED) { + remoting.debug.log('Host plugin state: DISCONNECTED'); + if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) { + // If an error is being displayed, then the plugin should not be able to + // hide it by setting the state. Errors must be dismissed by the user + // clicking OK, which puts the app into mode HOME. + if (lastShareWasCancelled_) { + remoting.setMode(remoting.AppMode.HOME); + } else { + remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED); + } + } + remoting.hostSession.removePlugin(); + + } else if (state == remoting.HostSession.State.ERROR) { + remoting.debug.log('Host plugin state: ERROR'); + showShareError_(/*i18n-content*/'ERROR_GENERIC'); + } else { + remoting.debug.log('Unknown state -> ' + state); + } +} + +/** + * This is the callback that the host plugin invokes to indicate that there + * is additional debug log info to display. + * @param {string} msg The message (which will not be localized) to be logged. + */ +function logDebugInfo_(msg) { + remoting.debug.log('plugin: ' + msg); +} + +/** + * Show a host-side error message. + * + * @param {string} errorTag The error message to be localized and displayed. + * @return {void} Nothing. + */ +function showShareError_(errorTag) { + var errorDiv = document.getElementById('host-plugin-error'); + l10n.localizeElementFromTag(errorDiv, errorTag); + remoting.debug.log('Sharing error: ' + errorTag); + remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED); +} + +/** + * Cancel an active or pending share operation. + * + * @return {void} Nothing. + */ +remoting.cancelShare = function() { + remoting.debug.log('Canceling share...'); + remoting.lastShareWasCancelled = true; + try { + remoting.hostSession.disconnect(); + } catch (error) { + // Hack to force JSCompiler type-safety. + var errorTyped = /** @type {{description: string}} */ error; + remoting.debug.log('Error disconnecting: ' + errorTyped.description + + '. The host plugin probably crashed.'); + // TODO(jamiewalch): Clean this up. We should have a class representing + // the host plugin, like we do for the client, which should handle crash + // reporting and it should use a more detailed error message than the + // default 'generic' one. See crbug.com/94624 + showShareError_(/*i18n-content*/'ERROR_GENERIC'); + } + disableTimeoutCountdown_(); +}; + +/** + * @type {boolean} Whether or not the access code timeout countdown is running. + */ +var timerRunning_ = false; + +/** + * @type {number} The id of the access code expiry countdown timer. + */ +var accessCodeTimerId_ = 0; + +/** + * @type {number} The number of seconds until the access code expires. + */ +var accessCodeExpiresIn_ = 0; + +/** + * The timer callback function, which needs to be visible from the global + * namespace. + */ +remoting.decrementAccessCodeTimeout_ = function() { + --accessCodeExpiresIn_; + updateAccessCodeTimeoutElement_(); +}; + +/** + * Stop the access code timeout countdown if it is running. + */ +function disableTimeoutCountdown_() { + if (timerRunning_) { + clearInterval(accessCodeTimerId_); + timerRunning_ = false; + updateTimeoutStyles_(); + } +} + +/** + * Constants controlling the access code timer countdown display. + */ +var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_ = 30; +var ACCESS_CODE_RED_THRESHOLD_ = 10; + +/** + * Show/hide or restyle various elements, depending on the remaining countdown + * and timer state. + * + * @return {boolean} True if the timeout is in progress, false if it has + * expired. + */ +function updateTimeoutStyles_() { + if (timerRunning_) { + if (accessCodeExpiresIn_ <= 0) { + remoting.cancelShare(); + return false; + } + if (accessCodeExpiresIn_ <= ACCESS_CODE_RED_THRESHOLD_) { + addClass(document.getElementById('access-code-display'), 'expiring'); + } else { + removeClass(document.getElementById('access-code-display'), 'expiring'); + } + } + document.getElementById('access-code-countdown').hidden = + (accessCodeExpiresIn_ > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_) || + !timerRunning_; + return true; +} + +/** + * Update the text and appearance of the access code timeout element to + * reflect the time remaining. + */ +function updateAccessCodeTimeoutElement_() { + var pad = (accessCodeExpiresIn_ < 10) ? '0:0' : '0:'; + l10n.localizeElement(document.getElementById('seconds-remaining'), + pad + accessCodeExpiresIn_); + if (!updateTimeoutStyles_()) { + disableTimeoutCountdown_(); + } +} + +/** + * Callback to show or hide the NAT traversal warning when the policy changes. + * @param {boolean} enabled True if NAT traversal is enabled. + * @return {void} Nothing. + */ +function onNatTraversalPolicyChanged_(enabled) { + var container = document.getElementById('nat-box-container'); + container.hidden = enabled; +} + +}()); diff --git a/remoting/webapp/me2mom/host_session.js b/remoting/webapp/me2mom/host_session.js new file mode 100644 index 0000000..7dfb95a --- /dev/null +++ b/remoting/webapp/me2mom/host_session.js @@ -0,0 +1,116 @@ +// 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 + * Class handling creation and teardown of a remoting host session. + * + * This abstracts a <embed> element and controls the plugin which does the + * actual remoting work. There should be no UI code inside this class. It + * should be purely thought of as a controller of sorts. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** + * @constructor + */ +remoting.HostSession = function() { + /** @private */ + this.HOST_PLUGIN_ID_ = 'host-plugin-id'; +}; + +/** @type {remoting.HostPlugin} */ +remoting.HostSession.prototype.plugin = null; + +// Note that these values are copied directly from host_script_object.h and +// must be kept in sync. +/** @enum {number} */ +remoting.HostSession.State = { + UNKNOWN: -1, + DISCONNECTED: 0, + STARTING: 1, + REQUESTED_ACCESS_CODE: 2, + RECEIVED_ACCESS_CODE: 3, + CONNECTED: 4, + DISCONNECTING: 5, + ERROR: 6 +}; + +/** + * Create the host plugin and initiate a connection. + * @param {Element} container The parent element to which to add the plugin. + * @param {string} email The user's email address. + * @param {string} accessToken A valid OAuth2 access token. + * @param {function(boolean):void} onNatTraversalPolicyChanged Callback + * for notification of changes to the NAT traversal policy. + * @param {function(remoting.HostSession.State):void} onStateChanged + * Callback for notifications of changes to the host plugin's state. + * @param {function(string):void} logDebugInfo Callback allowing the plugin + * to log messages to the debug log. + */ +remoting.HostSession.prototype.createPluginAndConnect = + function(container, email, accessToken, + onNatTraversalPolicyChanged, onStateChanged, logDebugInfo) { + this.plugin = /** @type {remoting.HostPlugin} */ + document.createElement('embed'); + this.plugin.type = remoting.PLUGIN_MIMETYPE; + this.plugin.id = this.HOST_PLUGIN_ID_; + // Hiding the plugin means it doesn't load, so make it size zero instead. + this.plugin.width = 0; + this.plugin.height = 0; + container.appendChild(this.plugin); + this.plugin.onNatTraversalPolicyChanged = onNatTraversalPolicyChanged; + this.plugin.onStateChanged = onStateChanged; + this.plugin.logDebugInfo = logDebugInfo; + this.plugin.localize(chrome.i18n.getMessage); + this.plugin.connect(email, 'oauth2:' + accessToken); +}; + +/** + * Get the access code generated by the host plugin. Valid only after the + * plugin state is RECEIVED_ACCESS_CODE. + * @return {string} The access code. + */ +remoting.HostSession.prototype.getAccessCode = function() { + return this.plugin.accessCode; +}; + +/** + * Get the lifetime for the access code. Valid only after the plugin state is + * RECEIVED_ACCESS_CODE. + * @return {number} The access code lifetime, in seconds. + */ +remoting.HostSession.prototype.getAccessCodeLifetime = function() { + return this.plugin.accessCodeLifetime; +}; + +/** + * Get the email address of the connected client. Valid only after the plugin + * state is CONNECTED. + * @return {string} The client's email address. + */ +remoting.HostSession.prototype.getClient = function() { + return this.plugin.client; +}; + +/** + * Disconnect the client. + * @return {void} Nothing. + */ +remoting.HostSession.prototype.disconnect = function() { + this.plugin.disconnect(); +}; + + +/** + * Remove the plugin element from the document. + * @return {void} Nothing. + */ +remoting.HostSession.prototype.removePlugin = function() { + this.plugin.parentNode.removeChild(this.plugin); +}; diff --git a/remoting/webapp/me2mom/oauth2.js b/remoting/webapp/me2mom/oauth2.js index ee7cc73..7010715 100644 --- a/remoting/webapp/me2mom/oauth2.js +++ b/remoting/webapp/me2mom/oauth2.js @@ -29,20 +29,26 @@ remoting.OAuth2 = function() { remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; /** @private */ remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; +/** @private */ +remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email'; // Constants for parameters used in retrieving the OAuth2 credentials. -/** @private */ remoting.OAuth2.prototype.CLIENT_ID_ = +/** @private */ +remoting.OAuth2.prototype.CLIENT_ID_ = '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + 'apps.googleusercontent.com'; /** @private */ remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; -/** @private */ remoting.OAuth2.prototype.SCOPE_ = +/** @private */ +remoting.OAuth2.prototype.SCOPE_ = 'https://www.googleapis.com/auth/chromoting ' + 'https://www.googleapis.com/auth/googletalk ' + 'https://www.googleapis.com/auth/userinfo#email'; -/** @private */ remoting.OAuth2.prototype.REDIRECT_URI_ = +/** @private */ +remoting.OAuth2.prototype.REDIRECT_URI_ = 'https://talkgadget.google.com/talkgadget/blank'; -/** @private */ remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = +/** @private */ +remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = 'https://accounts.google.com/o/oauth2/token'; /** @return {boolean} True if the app is already authenticated. */ @@ -60,6 +66,7 @@ remoting.OAuth2.prototype.isAuthenticated = function() { */ remoting.OAuth2.prototype.clear = function() { window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); + window.localStorage.removeItem(this.KEY_EMAIL_); this.clearAccessToken(); }; @@ -291,3 +298,49 @@ remoting.OAuth2.prototype.callWithToken = function(myfunc) { myfunc(this.getAccessToken()); }; + +/** + * Get the user's email address. + * + * @param {function(?string):void} setEmail Callback invoked when the email + * address is available, or on error. + * @return {void} Nothing. + */ +remoting.OAuth2.prototype.getEmail = function(setEmail) { + /** @type {remoting.OAuth2} */ + var that = this; + /** @param {XMLHttpRequest} xhr The XHR response. */ + var onResponse = function(xhr) { + that.email = null; + if (xhr.status == 200) { + // TODO(ajwong): See if we can't find a JSON endpoint. + that.email = xhr.responseText.split('&')[0].split('=')[1]; + } + window.localStorage.setItem(that.KEY_EMAIL_, that.email); + setEmail(that.email); + }; + + /** @param {string} token The access token. */ + var getEmailFromToken = function(token) { + var headers = { 'Authorization': 'OAuth ' + token }; + // TODO(ajwong): Update to new v2 API. + remoting.xhr.get('https://www.googleapis.com/userinfo/email', + onResponse, '', headers); + }; + + this.callWithToken(getEmailFromToken); +}; + +/** + * If the user's email address is cached, return it, otherwise return null. + * + * @return {?string} The email address, if it has been cached by a previous call + * to getEmail, otherwise null. + */ +remoting.OAuth2.prototype.getCachedEmail = function() { + var value = window.localStorage.getItem(this.KEY_EMAIL_); + if (typeof value == 'string') { + return value; + } + return null; +}; diff --git a/remoting/webapp/me2mom/remoting.js b/remoting/webapp/me2mom/remoting.js index 2f4250b..3e86b05 100644 --- a/remoting/webapp/me2mom/remoting.js +++ b/remoting/webapp/me2mom/remoting.js @@ -7,202 +7,31 @@ /** @suppress {duplicate} */ var remoting = remoting || {}; -/** - * Whether or not the plugin should scale itself. - * @type {boolean} - */ -remoting.scaleToFit = false; - -/** @type {remoting.ClientSession} */ -remoting.session = null; - -/** @type {string} */ remoting.accessCode = ''; -/** @type {number} */ remoting.accessCodeTimerId = 0; -/** @type {number} */ remoting.accessCodeExpiresIn = 0; -/** @type {remoting.AppMode} */ remoting.currentMode; -/** @type {string} */ remoting.hostJid = ''; -/** @type {string} */ remoting.hostPublicKey = ''; -/** @type {boolean} */ remoting.lastShareWasCancelled = false; -/** @type {boolean} */ remoting.timerRunning = false; -/** @type {string} */ remoting.username = ''; - -/** @enum {string} */ -remoting.AppMode = { - HOME: 'home', - UNAUTHENTICATED: 'auth', - CLIENT: 'client', - CLIENT_UNCONNECTED: 'client.unconnected', - CLIENT_CONNECTING: 'client.connecting', - CLIENT_CONNECT_FAILED: 'client.connect-failed', - CLIENT_SESSION_FINISHED: 'client.session-finished', - HOST: 'host', - HOST_WAITING_FOR_CODE: 'host.waiting-for-code', - HOST_WAITING_FOR_CONNECTION: 'host.waiting-for-connection', - HOST_SHARED: 'host.shared', - HOST_SHARE_FAILED: 'host.share-failed', - HOST_SHARE_FINISHED: 'host.share-finished', - IN_SESSION: 'in-session' -}; +/** @type {remoting.HostSession} */ remoting.hostSession = null; (function() { -window.addEventListener('blur', pluginLostFocus_, false); - -function pluginLostFocus_() { - // If the plug loses input focus, release all keys as a precaution against - // leaving them 'stuck down' on the host. - if (remoting.session && remoting.session.plugin) { - remoting.session.plugin.releaseAllKeys(); - } -} - -/** @type {string} */ -remoting.HOST_PLUGIN_ID = 'host-plugin-id'; - -/** @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' -}; - -// Constants representing keys used for storing persistent application state. -var KEY_APP_MODE_ = 'remoting-app-mode'; -var KEY_EMAIL_ = 'remoting-email'; -var KEY_USE_P2P_API_ = 'remoting-use-p2p-api'; - -// Some constants for pretty-printing the access code. -/** @type {number} */ var kSupportIdLen = 7; -/** @type {number} */ var kHostSecretLen = 5; -/** @type {number} */ var kAccessCodeLen = kSupportIdLen + kHostSecretLen; -/** @type {number} */ var kDigitsPerGroup = 4; - /** - * @param {string} classes A space-separated list of classes. - * @param {string} cls The class to check for. - * @return {boolean} True if |cls| is found within |classes|. + * Entry point for app initialization. */ -function hasClass(classes, cls) { - return classes.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) != null; -} - -/** - * @param {Element} element The element to which to add the class. - * @param {string} cls The new class. - * @return {void} Nothing. - */ -function addClass(element, cls) { - if (!hasClass(element.className, cls)) { - var padded = element.className == '' ? '' : element.className + ' '; - element.className = padded + cls; - } -} - -/** - * @param {Element} element The element from which to remove the class. - * @param {string} cls The new class. - * @return {void} Nothing. - */ -function removeClass(element, cls) { - element.className = - element.className.replace(new RegExp('\\b' + cls + '\\b', 'g'), '') - .replace(' ', ' '); -} - -function retrieveEmail_(access_token) { - var headers = { - 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() - }; - - /** @param {XMLHttpRequest} xhr The XHR response. */ - var onResponse = function(xhr) { - if (xhr.status != 200) { - // TODO(ajwong): Have a better way of showing an error. - remoting.debug.log('Unable to get email'); - document.getElementById('current-email').innerText = '???'; - return; - } - - // TODO(ajwong): See if we can't find a JSON endpoint. - setEmail(xhr.responseText.split('&')[0].split('=')[1]); - }; - - // TODO(ajwong): Update to new v2 API. - remoting.xhr.get('https://www.googleapis.com/userinfo/email', - onResponse, '', headers); -} - -function refreshEmail_() { - if (!getEmail() && remoting.oauth2.isAuthenticated()) { - remoting.oauth2.callWithToken(retrieveEmail_); - } -} - -/** - * @param {string} value The email address to place in local storage. - * @return {void} Nothing. - */ -function setEmail(value) { - window.localStorage.setItem(KEY_EMAIL_, value); - document.getElementById('current-email').innerText = value; -} - -/** - * @return {?string} The email address associated with the auth credentials. - */ -function getEmail() { - var result = window.localStorage.getItem(KEY_EMAIL_); - return typeof result == 'string' ? result : null; -} - -function exchangedCodeForToken_() { - if (!remoting.oauth2.isAuthenticated()) { - alert('Your OAuth2 token was invalid. Please try again.'); - } - /** @param {string} token The auth token. */ - var retrieveEmail = function(token) { retrieveEmail_(token); } - remoting.oauth2.callWithToken(retrieveEmail); -} - -remoting.clearOAuth2 = function() { - remoting.oauth2.clear(); - window.localStorage.removeItem(KEY_EMAIL_); - remoting.setMode(remoting.AppMode.UNAUTHENTICATED); -} - -remoting.toggleDebugLog = function() { - var debugLog = document.getElementById('debug-log'); - if (debugLog.hidden) { - debugLog.hidden = false; - } else { - debugLog.hidden = true; - } -} - remoting.init = function() { l10n.localize(); var button = document.getElementById('toggle-scaling'); button.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_SCALING'); // Create global objects. remoting.oauth2 = new remoting.OAuth2(); - remoting.debug = - new remoting.DebugLog(document.getElementById('debug-messages')); - /** @type {XMLHttpRequest} */ - remoting.supportHostsXhr = null; + remoting.debug = new remoting.DebugLog( + document.getElementById('debug-messages'), + document.getElementById('statistics')); refreshEmail_(); - var email = getEmail(); + var email = remoting.oauth2.getCachedEmail(); if (email) { document.getElementById('current-email').innerText = email; } - remoting.setMode(getAppStartupMode()); - if (isHostModeSupported()) { + remoting.setMode(getAppStartupMode_()); + if (isHostModeSupported_()) { var unsupported = document.getElementById('client-footer-text-cros'); unsupported.parentNode.removeChild(unsupported); } else { @@ -211,553 +40,106 @@ remoting.init = function() { document.getElementById('client-footer-text-cros').id = 'client-footer-text'; } -} - -/** - * Change the app's modal state to |mode|, which is considered to be a dotted - * hierachy of modes. For example, setMode('host.shared') will show any modal - * elements with an data-ui-mode attribute of 'host' or 'host.shared' and hide - * all others. - * - * @param {remoting.AppMode} mode The new modal state, expressed as a dotted - * hiearchy. - */ -remoting.setMode = function(mode) { - var modes = mode.split('.'); - for (var i = 1; i < modes.length; ++i) - modes[i] = modes[i - 1] + '.' + modes[i]; - var elements = document.querySelectorAll('[data-ui-mode]'); - for (var i = 0; i < elements.length; ++i) { - /** @type {Element} */ var element = elements[i]; - var hidden = true; - for (var m = 0; m < modes.length; ++m) { - if (hasClass(element.getAttribute('data-ui-mode'), modes[m])) { - hidden = false; - break; - } - } - element.hidden = hidden; - } - remoting.debug.log('App mode: ' + mode); - remoting.currentMode = mode; - if (mode == remoting.AppMode.IN_SESSION) { - document.removeEventListener('keydown', remoting.checkHotkeys, false); - } else { - document.addEventListener('keydown', remoting.checkHotkeys, false); - } -} - -/** - * Get the major mode that the app is running in. - * @return {string} The app's current major mode. - */ -remoting.getMajorMode = function() { - return remoting.currentMode.split('.')[0]; -} - -remoting.tryShare = function() { - remoting.debug.log('Attempting to share...'); - remoting.lastShareWasCancelled = false; - if (remoting.oauth2.needsNewAccessToken()) { - remoting.debug.log('Refreshing token...'); - remoting.oauth2.refreshAccessToken(function() { - if (remoting.oauth2.needsNewAccessToken()) { - // If we still need it, we're going to infinite loop. - showShareError_(/*i18n-content*/'ERROR_AUTHENTICATION_FAILED'); - throw 'Unable to get access token'; - } - remoting.tryShare(); - }); - return; - } - - remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); - document.getElementById('cancel-button').disabled = false; - disableTimeoutCountdown_(); - - var div = document.getElementById('host-plugin-container'); - var plugin = /** @type {remoting.HostPlugin} */ - document.createElement('embed'); - plugin.type = remoting.PLUGIN_MIMETYPE; - plugin.id = remoting.HOST_PLUGIN_ID; - // Hiding the plugin means it doesn't load, so make it size zero instead. - plugin.width = 0; - plugin.height = 0; - div.appendChild(plugin); - onNatTraversalPolicyChanged_(true); // Hide warning by default. - plugin.onNatTraversalPolicyChanged = onNatTraversalPolicyChanged_; - plugin.onStateChanged = onStateChanged_; - plugin.logDebugInfo = debugInfoCallback_; - plugin.localize(chrome.i18n.getMessage); - plugin.connect(/** @type {string} */ (getEmail()), - 'oauth2:' + remoting.oauth2.getAccessToken()); -} -function disableTimeoutCountdown_() { - if (remoting.timerRunning) { - clearInterval(remoting.accessCodeTimerId); - remoting.timerRunning = false; - updateTimeoutStyles_(); - } + window.addEventListener('blur', pluginLostFocus_, false); } -var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD = 30; -var ACCESS_CODE_RED_THRESHOLD = 10; - -/** - * Show/hide or restyle various elements, depending on the remaining countdown - * and timer state. - * - * @return {boolean} True if the timeout is in progress, false if it has - * expired. - */ -function updateTimeoutStyles_() { - if (remoting.timerRunning) { - if (remoting.accessCodeExpiresIn <= 0) { +remoting.cancelPendingOperation = function() { + document.getElementById('cancel-button').disabled = true; + switch (remoting.getMajorMode()) { + case remoting.AppMode.HOST: remoting.cancelShare(); - return false; - } - if (remoting.accessCodeExpiresIn <= ACCESS_CODE_RED_THRESHOLD) { - addClass(document.getElementById('access-code-display'), 'expiring'); - } else { - removeClass(document.getElementById('access-code-display'), 'expiring'); - } - } - document.getElementById('access-code-countdown').hidden = - (remoting.accessCodeExpiresIn > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD) || - !remoting.timerRunning; - return true; -} - -remoting.decrementAccessCodeTimeout_ = function() { - --remoting.accessCodeExpiresIn; - remoting.updateAccessCodeTimeoutElement_(); -} - -remoting.updateAccessCodeTimeoutElement_ = function() { - var pad = (remoting.accessCodeExpiresIn < 10) ? '0:0' : '0:'; - l10n.localizeElement(document.getElementById('seconds-remaining'), - pad + remoting.accessCodeExpiresIn); - if (!updateTimeoutStyles_()) { - disableTimeoutCountdown_(); + break; + case remoting.AppMode.CLIENT: + remoting.cancelConnect(); + break; } } /** - * Callback to show or hide the NAT traversal warning when the policy changes. - * @param {boolean} enabled True if NAT traversal is enabled. - * @return {void} Nothing. - */ -function onNatTraversalPolicyChanged_(enabled) { - var container = document.getElementById('nat-box-container'); - container.hidden = enabled; -} - -/** - * Callback for the host plugin to notify the web app of state changes. - * @param {number} state The new state of the plugin. + * If the client is connected, or the host is shared, prompt before closing. + * + * @return {?string} The prompt string if a connection is active. */ -function onStateChanged_(state) { - var plugin = /** @type {remoting.HostPlugin} */ - document.getElementById(remoting.HOST_PLUGIN_ID); - if (state == plugin.STARTING) { - // Nothing to do here. - remoting.debug.log('Host plugin state: STARTING'); - } else if (state == plugin.REQUESTED_ACCESS_CODE) { - // Nothing to do here. - remoting.debug.log('Host plugin state: REQUESTED_ACCESS_CODE'); - } else if (state == plugin.RECEIVED_ACCESS_CODE) { - remoting.debug.log('Host plugin state: RECEIVED_ACCESS_CODE'); - var accessCode = plugin.accessCode; - var accessCodeDisplay = document.getElementById('access-code-display'); - accessCodeDisplay.innerText = ''; - // Display the access code in groups of four digits for readability. - for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) { - var nextFourDigits = document.createElement('span'); - nextFourDigits.className = 'access-code-digit-group'; - nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup); - accessCodeDisplay.appendChild(nextFourDigits); - } - remoting.accessCodeExpiresIn = plugin.accessCodeLifetime; - if (remoting.accessCodeExpiresIn > 0) { // Check it hasn't expired. - remoting.accessCodeTimerId = setInterval( - 'remoting.decrementAccessCodeTimeout_()', 1000); - remoting.timerRunning = true; - remoting.updateAccessCodeTimeoutElement_(); - updateTimeoutStyles_(); - remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION); - } else { - // This can only happen if the cloud tells us that the code lifetime is - // <= 0s, which shouldn't happen so we don't care how clean this UX is. - remoting.debug.log('Access code already invalid on receipt!'); - remoting.cancelShare(); - } - } else if (state == plugin.CONNECTED) { - remoting.debug.log('Host plugin state: CONNECTED'); - var element = document.getElementById('host-shared-message'); - var client = plugin.client; - l10n.localizeElement(element, client); - remoting.setMode(remoting.AppMode.HOST_SHARED); - disableTimeoutCountdown_(); - } else if (state == plugin.DISCONNECTING) { - remoting.debug.log('Host plugin state: DISCONNECTING'); - } else if (state == plugin.DISCONNECTED) { - remoting.debug.log('Host plugin state: DISCONNECTED'); - if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) { - // If an error is being displayed, then the plugin should not be able to - // hide it by setting the state. Errors must be dismissed by the user - // clicking OK, which puts the app into mode HOME. - if (remoting.lastShareWasCancelled) { - remoting.setMode(remoting.AppMode.HOME); - } else { - remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED); - } - } - plugin.parentNode.removeChild(plugin); - } else if (state == plugin.ERROR) { - remoting.debug.log('Host plugin state: ERROR'); - showShareError_(/*i18n-content*/'ERROR_GENERIC'); - } else { - remoting.debug.log('Unknown state -> ' + state); +remoting.promptClose = function() { + switch (remoting.currentMode) { + case remoting.AppMode.CLIENT_CONNECTING: + case remoting.AppMode.HOST_WAITING_FOR_CODE: + case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: + case remoting.AppMode.HOST_SHARED: + case remoting.AppMode.IN_SESSION: + var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); + return result; + default: + return null; } } /** - * This is the callback that the host plugin invokes to indicate that there - * is additional debug log info to display. - * @param {string} msg The message (which will not be localized) to be logged. - */ -function debugInfoCallback_(msg) { - remoting.debug.log('plugin: ' + msg); -} - -/** - * Show a host-side error message. - * - * @param {string} errorTag The error message to be localized and displayed. - * @return {void} Nothing. + * Sign the user out of Chromoting by clearing the OAuth refresh token. */ -function showShareError_(errorTag) { - var errorDiv = document.getElementById('host-plugin-error'); - l10n.localizeElementFromTag(errorDiv, errorTag); - remoting.debug.log('Sharing error: ' + errorTag); - remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED); +remoting.clearOAuth2 = function() { + remoting.oauth2.clear(); + window.localStorage.removeItem(KEY_EMAIL_); + remoting.setMode(remoting.AppMode.UNAUTHENTICATED); } /** - * Cancel an active or pending share operation. - * - * @return {void} Nothing. + * Callback function called when the browser window loses focus. In this case, + * release all keys to prevent them becoming 'stuck down' on the host. */ -remoting.cancelShare = function() { - remoting.debug.log('Canceling share...'); - remoting.lastShareWasCancelled = true; - var plugin = /** @type {remoting.HostPlugin} */ - document.getElementById(remoting.HOST_PLUGIN_ID); - try { - plugin.disconnect(); - } catch (error) { - // Hack to force JSCompiler type-safety. - var errorTyped = /** @type {{description: string}} */ error; - remoting.debug.log('Error disconnecting: ' + errorTyped.description + - '. The host plugin probably crashed.'); - // TODO(jamiewalch): Clean this up. We should have a class representing - // the host plugin, like we do for the client, which should handle crash - // reporting and it should use a more detailed error message than the - // default 'generic' one. See crbug.com/94624 - showShareError_(/*i18n-content*/'ERROR_GENERIC'); +function pluginLostFocus_() { + if (remoting.clientSession && remoting.clientSession.plugin) { + remoting.clientSession.plugin.releaseAllKeys(); } - disableTimeoutCountdown_(); } /** - * Cancel an incomplete connect operation. - * - * @return {void} Nothing. + * If the user is authenticated, but there is no email address cached, get one. */ -remoting.cancelConnect = function() { - if (remoting.supportHostsXhr) { - remoting.supportHostsXhr.abort(); - remoting.supportHostsXhr = null; - } - if (remoting.session) { - remoting.session.removePlugin(); - remoting.session = null; - } - remoting.setMode(remoting.AppMode.HOME); -} - -function updateStatistics() { - if (!remoting.session) - return; - if (remoting.session.state != remoting.ClientSession.State.CONNECTED) - return; - var stats = remoting.session.stats(); - - var units = ''; - var videoBandwidth = stats['video_bandwidth']; - if (videoBandwidth < 1024) { - units = 'Bps'; - } else if (videoBandwidth < 1048576) { - units = 'KiBps'; - videoBandwidth = videoBandwidth / 1024; - } else if (videoBandwidth < 1073741824) { - units = 'MiBps'; - videoBandwidth = videoBandwidth / 1048576; - } else { - units = 'GiBps'; - videoBandwidth = videoBandwidth / 1073741824; - } - - var statistics = document.getElementById('statistics'); - statistics.innerText = - 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + - ', Frame Rate: ' + - (stats['video_frame_rate'] ? - stats['video_frame_rate'].toFixed(2) + ' fps' : 'n/a') + - ', Capture: ' + stats['capture_latency'].toFixed(2) + 'ms' + - ', Encode: ' + stats['encode_latency'].toFixed(2) + 'ms' + - ', Decode: ' + stats['decode_latency'].toFixed(2) + 'ms' + - ', Render: ' + stats['render_latency'].toFixed(2) + 'ms' + - ', Latency: ' + stats['roundtrip_latency'].toFixed(2) + 'ms'; - - // Update the stats once per second. - window.setTimeout(updateStatistics, 1000); -} - -function showToolbarPreview_() { - var toolbar = document.getElementById('session-toolbar'); - addClass(toolbar, 'toolbar-preview'); - window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); -} - -/** @param {number} oldState The previous state of the plugin. */ -function onClientStateChange_(oldState) { - if (!remoting.session) { - // 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.session.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.username); - } else if (state == remoting.ClientSession.State.INITIALIZING) { - remoting.debug.log('Initializing connection'); - } else if (state == remoting.ClientSession.State.CONNECTED) { - if (remoting.session) { - remoting.setMode(remoting.AppMode.IN_SESSION); - recenterToolbar_(); - showToolbarPreview_(); - updateStatistics(); - } - } else if (state == remoting.ClientSession.State.CLOSED) { - if (oldState == remoting.ClientSession.State.CONNECTED) { - remoting.session.removePlugin(); - remoting.session = 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.session.error); - if (remoting.session.error == - remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { - showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); - } else if (remoting.session.error == - remoting.ClientSession.ConnectionError.SESSION_REJECTED) { - showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); - } else if (remoting.session.error == - remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { - showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); - } else if (remoting.session.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); +function refreshEmail_() { + if (!getEmail_() && remoting.oauth2.isAuthenticated()) { + remoting.oauth2.getEmail(setEmail_); } } -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.username = - /** @type {string} email must be non-NULL to get here */ getEmail(); - remoting.session = - new remoting.ClientSession(remoting.hostJid, remoting.hostPublicKey, - remoting.accessCode, remoting.username, - onClientStateChange_); - /** @param {string} token The auth token. */ - var createPluginAndConnect = function(token) { - remoting.session.createPluginAndConnect( - document.getElementById('session-mode'), - token); - }; - remoting.oauth2.callWithToken(createPluginAndConnect); -} +/** The key under which the email address is stored. */ +var KEY_EMAIL_ = 'remoting-email'; /** - * Show a client-side error message. + * Save the user's email address in local storage. * - * @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.session) { - remoting.session.disconnect(); - remoting.session = null; - } - remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); -} - -/** - * @param {XMLHttpRequest} xhr The XMLHttpRequest object. + * @param {?string} email The email address to place in local storage. * @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); -} - -/** @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, ''); -} - -/** @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); -} - -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; - } - remoting.tryConnectWithAccessToken(); - }); +function setEmail_(email) { + if (email) { + document.getElementById('current-email').innerText = email; } else { - remoting.tryConnectWithAccessToken(); + // TODO(ajwong): Have a better way of showing an error. + document.getElementById('current-email').innerText = '???'; } } -remoting.tryConnectWithAccessToken = function() { - 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, - remoting.tryConnectWithWcs); -} - /** - * WcsLoader callback, called when the wcs script has been loaded, or on error. - * @param {boolean} success True if the script was loaded successfully. + * Read the user's email address from local storage. + * + * @return {?string} The email address associated with the auth credentials. */ -remoting.tryConnectWithWcs = function(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. - 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); - } -} - -remoting.cancelPendingOperation = function() { - document.getElementById('cancel-button').disabled = true; - switch (remoting.getMajorMode()) { - case remoting.AppMode.HOST: - remoting.cancelShare(); - break; - case remoting.AppMode.CLIENT: - remoting.cancelConnect(); - break; - } +function getEmail_() { + var result = window.localStorage.getItem(KEY_EMAIL_); + return typeof result == 'string' ? result : null; } - /** * Gets the major-mode that this application should start up in. * * @return {remoting.AppMode} The mode to start in. */ -function getAppStartupMode() { +function getAppStartupMode_() { if (!remoting.oauth2.isAuthenticated()) { return remoting.AppMode.UNAUTHENTICATED; } - if (isHostModeSupported()) { + if (isHostModeSupported_()) { return remoting.AppMode.HOME; } else { return remoting.AppMode.CLIENT_UNCONNECTED; @@ -769,86 +151,8 @@ function getAppStartupMode() { * * @return {boolean} True if Host mode is supported. */ -function isHostModeSupported() { +function isHostModeSupported_() { // Currently, sharing on Chromebooks is not supported. return !navigator.userAgent.match(/\bCrOS\b/); } - -/** - * 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.session.updateDimensions(); -} - -/** - * Update the remoting client layout in response to a resize event. - * - * @return {void} Nothing. - */ -remoting.onResize = function() { - if (remoting.session) - remoting.session.onWindowSizeChanged(); - recenterToolbar_(); -} - -/** - * Disconnect the remoting client. - * - * @return {void} Nothing. - */ -remoting.disconnect = function() { - if (remoting.session) { - remoting.session.disconnect(); - remoting.session = null; - remoting.debug.log('Disconnected.'); - remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); - } -} - -/** - * If the client is connected, or the host is shared, prompt before closing. - * - * @return {?string} The prompt string if a connection is active. - */ -remoting.promptClose = function() { - switch (remoting.currentMode) { - case remoting.AppMode.CLIENT_CONNECTING: - case remoting.AppMode.HOST_WAITING_FOR_CODE: - case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: - case remoting.AppMode.HOST_SHARED: - case remoting.AppMode.IN_SESSION: - var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); - return result; - default: - return null; - } -} - -/** - * @param {Event} event The keyboard event. - * @return {void} Nothing. - */ -remoting.checkHotkeys = function(event) { - if (String.fromCharCode(event.which) == 'D') { - remoting.toggleDebugLog(); - } -} - -function recenterToolbar_() { - var toolbar = document.getElementById('session-toolbar'); - var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; - toolbar.style['left'] = toolbarX + 'px'; -} - }()); diff --git a/remoting/webapp/me2mom/ui_mode.js b/remoting/webapp/me2mom/ui_mode.js new file mode 100644 index 0000000..e3d54a6 --- /dev/null +++ b/remoting/webapp/me2mom/ui_mode.js @@ -0,0 +1,78 @@ +// 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 controlling the modal ui state of the app. + */ + +'use strict'; + +/** @suppress {duplicate} */ +var remoting = remoting || {}; + +/** @enum {string} */ +remoting.AppMode = { + HOME: 'home', + UNAUTHENTICATED: 'auth', + CLIENT: 'client', + CLIENT_UNCONNECTED: 'client.unconnected', + CLIENT_CONNECTING: 'client.connecting', + CLIENT_CONNECT_FAILED: 'client.connect-failed', + CLIENT_SESSION_FINISHED: 'client.session-finished', + HOST: 'host', + HOST_WAITING_FOR_CODE: 'host.waiting-for-code', + HOST_WAITING_FOR_CONNECTION: 'host.waiting-for-connection', + HOST_SHARED: 'host.shared', + HOST_SHARE_FAILED: 'host.share-failed', + HOST_SHARE_FINISHED: 'host.share-finished', + IN_SESSION: 'in-session' +}; + +/** + * @type {remoting.AppMode} The current app mode + */ +remoting.currentMode; + +/** + * Change the app's modal state to |mode|, which is considered to be a dotted + * hierachy of modes. For example, setMode('host.shared') will show any modal + * elements with an data-ui-mode attribute of 'host' or 'host.shared' and hide + * all others. + * + * @param {remoting.AppMode} mode The new modal state, expressed as a dotted + * hiearchy. + */ +remoting.setMode = function(mode) { + var modes = mode.split('.'); + for (var i = 1; i < modes.length; ++i) + modes[i] = modes[i - 1] + '.' + modes[i]; + var elements = document.querySelectorAll('[data-ui-mode]'); + for (var i = 0; i < elements.length; ++i) { + var element = /** @type {Element} */ elements[i]; + var hidden = true; + for (var m = 0; m < modes.length; ++m) { + if (hasClass(element.getAttribute('data-ui-mode'), modes[m])) { + hidden = false; + break; + } + } + element.hidden = hidden; + } + remoting.debug.log('App mode: ' + mode); + remoting.currentMode = mode; + if (mode == remoting.AppMode.IN_SESSION) { + document.removeEventListener('keydown', remoting.DebugLog.onKeydown, false); + } else { + document.addEventListener('keydown', remoting.DebugLog.onKeydown, false); + } +}; + +/** + * Get the major mode that the app is running in. + * @return {string} The app's current major mode. + */ +remoting.getMajorMode = function() { + return remoting.currentMode.split('.')[0]; +}; diff --git a/remoting/webapp/me2mom/util.js b/remoting/webapp/me2mom/util.js new file mode 100644 index 0000000..0c15c0c --- /dev/null +++ b/remoting/webapp/me2mom/util.js @@ -0,0 +1,40 @@ +// 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 + * Simple utility functions for Chromoting. + */ + +/** + * @param {string} classes A space-separated list of classes. + * @param {string} cls The class to check for. + * @return {boolean} True if |cls| is found within |classes|. + */ +function hasClass(classes, cls) { + return classes.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) != null; +} + +/** + * @param {Element} element The element to which to add the class. + * @param {string} cls The new class. + * @return {void} Nothing. + */ +function addClass(element, cls) { + if (!hasClass(element.className, cls)) { + var padded = element.className == '' ? '' : element.className + ' '; + element.className = padded + cls; + } +} + +/** + * @param {Element} element The element from which to remove the class. + * @param {string} cls The new class. + * @return {void} Nothing. + */ +function removeClass(element, cls) { + element.className = + element.className.replace(new RegExp('\\b' + cls + '\\b', 'g'), '') + .replace(' ', ' '); +} diff --git a/remoting/webapp/me2mom/wcs_loader.js b/remoting/webapp/me2mom/wcs_loader.js index 241e3e8..8b931f11 100644 --- a/remoting/webapp/me2mom/wcs_loader.js +++ b/remoting/webapp/me2mom/wcs_loader.js @@ -143,5 +143,5 @@ remoting.WcsLoader.prototype.constructWcs_ = function() { remoting.WcsLoader.prototype.onWcsReady_ = function() { this.loadState_ = this.LoadState_.READY; this.onReady_(true); - this.onReady_ = function() {}; + this.onReady_ = function(success) {}; }; |