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.
7 * Functions related to the 'client screen' for Chromoting.
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
18 * @type {boolean} Whether or not the plugin should scale itself.
20 remoting.scaleToFit = false;
23 * @type {remoting.ClientSession} The client session object, set once the
24 * access code has been successfully verified.
26 remoting.clientSession = null;
29 * @type {string} The normalized access code.
31 remoting.accessCode = '';
34 * @type {string} The host's JID, returned by the server.
36 remoting.hostJid = '';
39 * @type {string} The host's public key, returned by the server.
41 remoting.hostPublicKey = '';
44 * @type {XMLHttpRequest} The XHR object corresponding to the current
45 * support-hosts request, if there is one outstanding.
47 remoting.supportHostsXhr_ = null;
52 remoting.ConnectionType = {
58 * @type {remoting.ConnectionType?}
60 remoting.currentConnectionType = null;
63 * Entry point for the 'connect' functionality. This function checks for the
64 * existence of an OAuth2 token, and either requests one asynchronously, or
65 * calls through directly to tryConnectWithAccessToken_.
67 remoting.tryConnect = function() {
68 document.getElementById('cancel-button').disabled = false;
69 if (remoting.oauth2.needsNewAccessToken()) {
70 remoting.oauth2.refreshAccessToken(function(xhr) {
71 if (remoting.oauth2.needsNewAccessToken()) {
72 // Failed to get access token
73 remoting.debug.log('tryConnect: OAuth2 token fetch failed');
74 showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
77 tryConnectWithAccessToken_();
80 tryConnectWithAccessToken_();
85 * Cancel an incomplete connect operation.
87 * @return {void} Nothing.
89 remoting.cancelConnect = function() {
90 if (remoting.supportHostsXhr_) {
91 remoting.supportHostsXhr_.abort();
92 remoting.supportHostsXhr_ = null;
94 if (remoting.clientSession) {
95 remoting.clientSession.removePlugin();
96 remoting.clientSession = null;
98 remoting.setMode(remoting.AppMode.HOME);
102 * Enable or disable scale-to-fit.
104 * @param {Element} button The scale-to-fit button. The style of this button is
105 * updated to reflect the new scaling state.
106 * @return {void} Nothing.
108 remoting.toggleScaleToFit = function(button) {
109 remoting.scaleToFit = !remoting.scaleToFit;
110 if (remoting.scaleToFit) {
111 addClass(button, 'toggle-button-active');
113 removeClass(button, 'toggle-button-active');
115 remoting.clientSession.updateDimensions();
119 * Update the remoting client layout in response to a resize event.
121 * @return {void} Nothing.
123 remoting.onResize = function() {
124 if (remoting.clientSession)
125 remoting.clientSession.onWindowSizeChanged();
126 remoting.toolbar.center();
130 * Disconnect the remoting client.
132 * @return {void} Nothing.
134 remoting.disconnect = function() {
135 if (remoting.clientSession) {
136 remoting.clientSession.disconnect();
137 remoting.clientSession = null;
138 remoting.debug.log('Disconnected.');
139 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
140 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
142 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
148 * Second stage of the 'connect' functionality. Once an access token is
149 * available, load the WCS widget asynchronously and call through to
150 * tryConnectWithWcs_ when ready.
152 function tryConnectWithAccessToken_() {
153 if (!remoting.wcsLoader) {
154 remoting.wcsLoader = new remoting.WcsLoader();
156 /** @param {function(string):void} setToken The callback function. */
157 var callWithToken = function(setToken) {
158 remoting.oauth2.callWithToken(setToken);
160 remoting.wcsLoader.start(
161 remoting.oauth2.getAccessToken(),
167 * Final stage of the 'connect' functionality, called when the wcs widget has
168 * been loaded, or on error.
170 * @param {boolean} success True if the script was loaded successfully.
172 function tryConnectWithWcs_(success) {
174 var accessCode = document.getElementById('access-code-entry').value;
175 remoting.accessCode = normalizeAccessCode_(accessCode);
176 // At present, only 12-digit access codes are supported, of which the first
177 // 7 characters are the supportId.
178 var kSupportIdLen = 7;
179 var kHostSecretLen = 5;
180 var kAccessCodeLen = kSupportIdLen + kHostSecretLen;
181 if (remoting.accessCode.length != kAccessCodeLen) {
182 remoting.debug.log('Bad access code length');
183 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
185 var supportId = remoting.accessCode.substring(0, kSupportIdLen);
186 remoting.currentConnectionType = remoting.ConnectionType.It2Me;
187 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
188 resolveSupportId(supportId);
191 showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
196 * Callback function called when the state of the client plugin changes. The
197 * current state is available via the |state| member variable.
199 * @param {number} oldState The previous state of the plugin.
200 * @param {number} newState The current state of the plugin.
202 // TODO(jamiewalch): Make this pass both the current and old states to avoid
204 function onClientStateChange_(oldState, newState) {
205 if (!remoting.clientSession) {
206 // If the connection has been cancelled, then we no longer have a reference
207 // to the session object and should ignore any state changes.
210 if (newState == remoting.ClientSession.State.CREATED) {
211 remoting.debug.log('Created plugin');
213 } else if (newState == remoting.ClientSession.State.BAD_PLUGIN_VERSION) {
214 showConnectError_(remoting.Error.BAD_PLUGIN_VERSION);
216 } else if (newState == remoting.ClientSession.State.CONNECTING) {
217 remoting.debug.log('Connecting as ' + remoting.oauth2.getCachedEmail());
219 } else if (newState == remoting.ClientSession.State.INITIALIZING) {
220 remoting.debug.log('Initializing connection');
222 } else if (newState == remoting.ClientSession.State.CONNECTED) {
223 if (remoting.clientSession) {
224 remoting.setMode(remoting.AppMode.IN_SESSION);
225 remoting.toolbar.center();
226 remoting.toolbar.preview();
230 } else if (newState == remoting.ClientSession.State.CLOSED) {
231 if (oldState == remoting.ClientSession.State.CONNECTED) {
232 remoting.clientSession.removePlugin();
233 remoting.clientSession = null;
234 remoting.debug.log('Connection closed by host');
235 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
236 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
238 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
241 // The transition from CONNECTING to CLOSED state may happen
242 // only with older client plugins. Current version should go the
243 // FAILED state when connection fails.
244 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
247 } else if (newState == remoting.ClientSession.State.CONNECTION_FAILED) {
248 remoting.debug.log('Client plugin reported connection failed: ' +
249 remoting.clientSession.error);
250 if (remoting.clientSession.error ==
251 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) {
252 showConnectError_(remoting.Error.HOST_IS_OFFLINE);
253 } else if (remoting.clientSession.error ==
254 remoting.ClientSession.ConnectionError.SESSION_REJECTED) {
255 showConnectError_(remoting.Error.INVALID_ACCESS_CODE);
256 } else if (remoting.clientSession.error ==
257 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) {
258 showConnectError_(remoting.Error.INCOMPATIBLE_PROTOCOL);
259 } else if (remoting.clientSession.error ==
260 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) {
261 showConnectError_(remoting.Error.GENERIC);
263 showConnectError_(remoting.Error.GENERIC);
267 remoting.debug.log('Unexpected client plugin state: ' + newState);
268 // This should only happen if the web-app and client plugin get out of
269 // sync, and even then the version check should allow compatibility.
270 showConnectError_(remoting.Error.MISSING_PLUGIN);
275 * Create the client session object and initiate the connection.
277 * @return {void} Nothing.
279 function startSession_() {
280 remoting.debug.log('Starting session...');
281 var accessCode = document.getElementById('access-code-entry');
282 accessCode.value = ''; // The code has been validated and won't work again.
283 remoting.clientSession =
284 new remoting.ClientSession(
285 remoting.hostJid, remoting.hostPublicKey,
287 /** @type {string} */ (remoting.oauth2.getCachedEmail()),
288 onClientStateChange_);
289 /** @param {string} token The auth token. */
290 var createPluginAndConnect = function(token) {
291 remoting.clientSession.createPluginAndConnect(
292 document.getElementById('session-mode'),
295 remoting.oauth2.callWithToken(createPluginAndConnect);
299 * Show a client-side error message.
301 * @param {remoting.Error} errorTag The error to be localized and
303 * @return {void} Nothing.
305 function showConnectError_(errorTag) {
306 remoting.debug.log('Connection failed: ' + errorTag);
307 var errorDiv = document.getElementById('connect-error-message');
308 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag));
309 remoting.accessCode = '';
310 if (remoting.clientSession) {
311 remoting.clientSession.disconnect();
312 remoting.clientSession = null;
314 if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
315 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
317 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
322 * Parse the response from the server to a request to resolve a support id.
324 * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
325 * @return {void} Nothing.
327 function parseServerResponse_(xhr) {
328 remoting.supportHostsXhr_ = null;
329 remoting.debug.log('parseServerResponse: status = ' + xhr.status);
330 if (xhr.status == 200) {
331 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */
332 JSON.parse(xhr.responseText);
333 if (host.data && host.data.jabberId && host.data.publicKey) {
334 remoting.hostJid = host.data.jabberId;
335 remoting.hostPublicKey = host.data.publicKey;
336 var split = remoting.hostJid.split('/');
337 document.getElementById('connected-to').innerText = split[0];
342 var errorMsg = remoting.Error.GENERIC;
343 if (xhr.status == 404) {
344 errorMsg = remoting.Error.INVALID_ACCESS_CODE;
345 } else if (xhr.status == 0) {
346 errorMsg = remoting.Error.NO_RESPONSE;
348 remoting.debug.log('The server responded: ' + xhr.responseText);
350 showConnectError_(errorMsg);
354 * Normalize the access code entered by the user.
356 * @param {string} accessCode The access code, as entered by the user.
357 * @return {string} The normalized form of the code (whitespace removed).
359 function normalizeAccessCode_(accessCode) {
361 // TODO(sergeyu): Do we need to do any other normalization here?
362 return accessCode.replace(/\s/g, '');
366 * Initiate a request to the server to resolve a support ID.
368 * @param {string} supportId The canonicalized support ID.
370 function resolveSupportId(supportId) {
372 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken()
375 remoting.supportHostsXhr_ = remoting.xhr.get(
376 'https://www.googleapis.com/chromoting/v1/support-hosts/' +
377 encodeURIComponent(supportId),
378 parseServerResponse_,
384 * Timer callback to update the statistics panel.
386 function updateStatistics_() {
387 if (!remoting.clientSession ||
388 remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) {
391 remoting.debug.updateStatistics(remoting.clientSession.stats());
392 // Update the stats once per second.
393 window.setTimeout(updateStatistics_, 1000);
398 * Start a connection to the specified host, using the stored details.
400 * @param {string} hostJid The jabber Id of the host.
401 * @param {string} hostPublicKey The public key of the host.
402 * @param {string} hostName The name of the host.
403 * @return {void} Nothing.
405 remoting.connectHost = function(hostJid, hostPublicKey, hostName) {
406 // TODO(jamiewalch): Instead of passing the jid in the URL, cache it in local
407 // storage so that host bookmarks can be implemented efficiently.
408 remoting.hostJid = hostJid;
409 remoting.hostPublicKey = hostPublicKey;
410 document.getElementById('connected-to').innerText = hostName;
411 document.title = document.title + ': ' + hostName;
413 remoting.debug.log('Connecting to host...');
414 remoting.currentConnectionType = remoting.ConnectionType.Me2Me;
415 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
417 if (!remoting.wcsLoader) {
418 remoting.wcsLoader = new remoting.WcsLoader();
420 /** @param {function(string):void} setToken The callback function. */
421 var callWithToken = function(setToken) {
422 remoting.oauth2.callWithToken(setToken);
424 remoting.wcsLoader.startAsync(callWithToken, remoting.connectHostWithWcs);
428 * Continue making the connection to a host, once WCS has initialized.
430 * @return {void} Nothing.
432 remoting.connectHostWithWcs = function() {
433 remoting.clientSession =
434 new remoting.ClientSession(
435 remoting.hostJid, remoting.hostPublicKey,
436 '', /** @type {string} */ (remoting.oauth2.getCachedEmail()),
437 onClientStateChange_);
438 /** @param {string} token The auth token. */
439 var createPluginAndConnect = function(token) {
440 remoting.clientSession.createPluginAndConnect(
441 document.getElementById('session-mode'),
445 remoting.oauth2.callWithToken(createPluginAndConnect);