Fix tool-bar auto-hide behaviour.
[chromium-blink-merge.git] / remoting / webapp / me2mom / client_screen.js
blob0090c53c2ef7e3e36cb2acb6ac948e8539f5fb01
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.
5 /**
6  * @fileoverview
7  * Functions related to the 'client screen' for Chromoting.
8  */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
15 (function() {
17 /**
18  * @type {boolean} Whether or not the plugin should scale itself.
19  */
20 remoting.scaleToFit = false;
22 /**
23  * @type {remoting.ClientSession} The client session object, set once the
24  *     access code has been successfully verified.
25  */
26 remoting.clientSession = null;
28 /**
29  * @type {string} The normalized access code.
30  */
31 remoting.accessCode = '';
33 /**
34  * @type {string} The host's JID, returned by the server.
35  */
36 remoting.hostJid = '';
38 /**
39  * @type {string} The host's public key, returned by the server.
40  */
41 remoting.hostPublicKey = '';
43 /**
44  * @type {XMLHttpRequest} The XHR object corresponding to the current
45  *     support-hosts request, if there is one outstanding.
46  */
47 remoting.supportHostsXhr_ = null;
49 /**
50  * @enum {string}
51  */
52 remoting.ConnectionType = {
53   It2Me: 'It2Me',
54   Me2Me: 'Me2Me'
57 /**
58  * @type {remoting.ConnectionType?}
59  */
60 remoting.currentConnectionType = null;
62 /**
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_.
66  */
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);
75         return;
76       }
77       tryConnectWithAccessToken_();
78     });
79   } else {
80     tryConnectWithAccessToken_();
81   }
84 /**
85  * Cancel an incomplete connect operation.
86  *
87  * @return {void} Nothing.
88  */
89 remoting.cancelConnect = function() {
90   if (remoting.supportHostsXhr_) {
91     remoting.supportHostsXhr_.abort();
92     remoting.supportHostsXhr_ = null;
93   }
94   if (remoting.clientSession) {
95     remoting.clientSession.removePlugin();
96     remoting.clientSession = null;
97   }
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.
107  */
108 remoting.toggleScaleToFit = function(button) {
109   remoting.scaleToFit = !remoting.scaleToFit;
110   if (remoting.scaleToFit) {
111     addClass(button, 'toggle-button-active');
112   } else {
113     removeClass(button, 'toggle-button-active');
114   }
115   remoting.clientSession.updateDimensions();
119  * Update the remoting client layout in response to a resize event.
121  * @return {void} Nothing.
122  */
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.
133  */
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);
141     } else {
142       remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
143     }
144   }
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.
151  */
152 function tryConnectWithAccessToken_() {
153   if (!remoting.wcsLoader) {
154     remoting.wcsLoader = new remoting.WcsLoader();
155   }
156   /** @param {function(string):void} setToken The callback function. */
157   var callWithToken = function(setToken) {
158     remoting.oauth2.callWithToken(setToken);
159   };
160   remoting.wcsLoader.start(
161       remoting.oauth2.getAccessToken(),
162       callWithToken,
163       tryConnectWithWcs_);
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.
171  */
172 function tryConnectWithWcs_(success) {
173   if (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);
184     } else {
185       var supportId = remoting.accessCode.substring(0, kSupportIdLen);
186       remoting.currentConnectionType = remoting.ConnectionType.It2Me;
187       remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
188       resolveSupportId(supportId);
189     }
190   } else {
191     showConnectError_(remoting.Error.AUTHENTICATION_FAILED);
192   }
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.
201  */
202 // TODO(jamiewalch): Make this pass both the current and old states to avoid
203 // race conditions.
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.
208     return;
209   }
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();
227       updateStatistics_();
228     }
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);
237       } else {
238         remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
239       }
240     } else {
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);
245     }
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);
262     } else {
263       showConnectError_(remoting.Error.GENERIC);
264     }
266   } else {
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);
271   }
275  * Create the client session object and initiate the connection.
277  * @return {void} Nothing.
278  */
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,
286           remoting.accessCode,
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'),
293         token);
294   };
295   remoting.oauth2.callWithToken(createPluginAndConnect);
299  * Show a client-side error message.
301  * @param {remoting.Error} errorTag The error to be localized and
302  *     displayed.
303  * @return {void} Nothing.
304  */
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;
313   }
314   if (remoting.currentConnectionType == remoting.ConnectionType.It2Me) {
315     remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
316   } else {
317     remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
318   }
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.
326  */
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];
338       startSession_();
339       return;
340     }
341   }
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;
347   } else {
348     remoting.debug.log('The server responded: ' + xhr.responseText);
349   }
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).
358  */
359 function normalizeAccessCode_(accessCode) {
360   // Trim whitespace.
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.
369  */
370 function resolveSupportId(supportId) {
371   var headers = {
372     'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken()
373   };
375   remoting.supportHostsXhr_ = remoting.xhr.get(
376       'https://www.googleapis.com/chromoting/v1/support-hosts/' +
377           encodeURIComponent(supportId),
378       parseServerResponse_,
379       '',
380       headers);
384  * Timer callback to update the statistics panel.
385  */
386 function updateStatistics_() {
387   if (!remoting.clientSession ||
388       remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) {
389     return;
390   }
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.
404  */
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();
419   }
420   /** @param {function(string):void} setToken The callback function. */
421   var callWithToken = function(setToken) {
422     remoting.oauth2.callWithToken(setToken);
423   };
424   remoting.wcsLoader.startAsync(callWithToken, remoting.connectHostWithWcs);
428  * Continue making the connection to a host, once WCS has initialized.
430  * @return {void} Nothing.
431  */
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'),
442         token);
443   };
445   remoting.oauth2.callWithToken(createPluginAndConnect);
448 }());