OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 /** | |
6 * @fileoverview | |
7 * Functions related to the 'client screen' for Chromoting. | |
8 */ | |
9 | |
10 'use strict'; | |
11 | |
12 /** @suppress {duplicate} */ | |
13 var remoting = remoting || {}; | |
14 | |
15 /** @enum {string} */ | |
16 remoting.ClientError = { | |
17 NO_RESPONSE: /*i18n-content*/'ERROR_NO_RESPONSE', | |
18 INVALID_ACCESS_CODE: /*i18n-content*/'ERROR_INVALID_ACCESS_CODE', | |
19 MISSING_PLUGIN: /*i18n-content*/'ERROR_MISSING_PLUGIN', | |
20 OAUTH_FETCH_FAILED: /*i18n-content*/'ERROR_AUTHENTICATION_FAILED', | |
21 HOST_IS_OFFLINE: /*i18n-content*/'ERROR_HOST_IS_OFFLINE', | |
22 INCOMPATIBLE_PROTOCOL: /*i18n-content*/'ERROR_INCOMPATIBLE_PROTOCOL', | |
23 BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', | |
24 OTHER_ERROR: /*i18n-content*/'ERROR_GENERIC' | |
25 }; | |
26 | |
27 (function() { | |
28 | |
29 /** | |
30 * @type {boolean} Whether or not the plugin should scale itself. | |
31 */ | |
32 remoting.scaleToFit = false; | |
garykac
2011/10/27 22:32:39
nit: I assume these should have underscores at the
Jamie
2011/10/27 22:51:37
No, anything exported to the remoting namespace is
| |
33 | |
34 /** | |
35 * @type {remoting.ClientSession} The client session object, set once the | |
36 * access code has been successfully verified. | |
37 */ | |
38 remoting.clientSession = null; | |
39 | |
40 /** | |
41 * @type {string} The normalized access code. | |
42 */ | |
43 remoting.accessCode = ''; | |
44 | |
45 /** | |
46 * @type {string} The host's JID, returned by the server. | |
47 */ | |
48 remoting.hostJid = ''; | |
49 | |
50 /** | |
51 * @type {string} The host's public key, returned by the server. | |
52 */ | |
53 remoting.hostPublicKey = ''; | |
54 | |
55 /** | |
56 * @type {XMLHttpRequest} The XHR object corresponding to the current | |
57 * support-hosts request, if there is one outstanding. | |
58 */ | |
59 remoting.supportHostsXhr_ = null; | |
60 | |
61 /** | |
62 * Entry point for the 'connect' functionality. This function checks for the | |
63 * existence of an OAuth2 token, and either requests one asynchronously, or | |
64 * calls through directly to tryConnectWithAccessToken_. | |
65 */ | |
66 remoting.tryConnect = function() { | |
67 document.getElementById('cancel-button').disabled = false; | |
68 if (remoting.oauth2.needsNewAccessToken()) { | |
69 remoting.oauth2.refreshAccessToken(function(xhr) { | |
70 if (remoting.oauth2.needsNewAccessToken()) { | |
71 // Failed to get access token | |
72 remoting.debug.log('tryConnect: OAuth2 token fetch failed'); | |
73 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
74 return; | |
75 } | |
76 tryConnectWithAccessToken_(); | |
77 }); | |
78 } else { | |
79 tryConnectWithAccessToken_(); | |
80 } | |
81 } | |
82 | |
83 /** | |
84 * Cancel an incomplete connect operation. | |
85 * | |
86 * @return {void} Nothing. | |
87 */ | |
88 remoting.cancelConnect = function() { | |
89 if (remoting.supportHostsXhr_) { | |
90 remoting.supportHostsXhr_.abort(); | |
91 remoting.supportHostsXhr_ = null; | |
92 } | |
93 if (remoting.clientSession) { | |
94 remoting.clientSession.removePlugin(); | |
95 remoting.clientSession = null; | |
96 } | |
97 remoting.setMode(remoting.AppMode.HOME); | |
98 } | |
99 | |
100 /** | |
101 * Enable or disable scale-to-fit. | |
102 * | |
103 * @param {Element} button The scale-to-fit button. The style of this button is | |
104 * updated to reflect the new scaling state. | |
105 * @return {void} Nothing. | |
106 */ | |
107 remoting.toggleScaleToFit = function(button) { | |
108 remoting.scaleToFit = !remoting.scaleToFit; | |
109 if (remoting.scaleToFit) { | |
110 addClass(button, 'toggle-button-active'); | |
111 } else { | |
112 removeClass(button, 'toggle-button-active'); | |
113 } | |
114 remoting.clientSession.updateDimensions(); | |
115 } | |
116 | |
117 /** | |
118 * Update the remoting client layout in response to a resize event. | |
119 * | |
120 * @return {void} Nothing. | |
121 */ | |
122 remoting.onResize = function() { | |
123 if (remoting.clientSession) | |
124 remoting.clientSession.onWindowSizeChanged(); | |
125 recenterToolbar_(); | |
126 } | |
127 | |
128 /** | |
129 * Disconnect the remoting client. | |
130 * | |
131 * @return {void} Nothing. | |
132 */ | |
133 remoting.disconnect = function() { | |
134 if (remoting.clientSession) { | |
135 remoting.clientSession.disconnect(); | |
136 remoting.clientSession = null; | |
137 remoting.debug.log('Disconnected.'); | |
138 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
139 } | |
140 } | |
141 | |
142 /** | |
143 * Second stage of the 'connect' functionality. Once an access token is | |
144 * available, load the WCS widget asynchronously and call through to | |
145 * tryConnectWithWcs_ when ready. | |
146 */ | |
147 function tryConnectWithAccessToken_() { | |
148 if (!remoting.wcsLoader) { | |
149 remoting.wcsLoader = new remoting.WcsLoader(); | |
150 } | |
151 /** @param {function(string):void} setToken The callback function. */ | |
152 var callWithToken = function(setToken) { | |
153 remoting.oauth2.callWithToken(setToken); | |
154 }; | |
155 remoting.wcsLoader.start( | |
156 remoting.oauth2.getAccessToken(), | |
157 callWithToken, | |
158 tryConnectWithWcs_); | |
159 } | |
160 | |
161 /** | |
162 * Final stage of the 'connect' functionality, called when the wcs widget has | |
163 * been loaded, or on error. | |
164 * | |
165 * @param {boolean} success True if the script was loaded successfully. | |
166 */ | |
167 function tryConnectWithWcs_(success) { | |
168 if (success) { | |
169 var accessCode = document.getElementById('access-code-entry').value; | |
170 remoting.accessCode = normalizeAccessCode_(accessCode); | |
171 // At present, only 12-digit access codes are supported, of which the first | |
172 // 7 characters are the supportId. | |
173 var kSupportIdLen = 7; | |
174 var kHostSecretLen = 5; | |
175 var kAccessCodeLen = kSupportIdLen + kHostSecretLen; | |
176 if (remoting.accessCode.length != kAccessCodeLen) { | |
177 remoting.debug.log('Bad access code length'); | |
178 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
179 } else { | |
180 var supportId = remoting.accessCode.substring(0, kSupportIdLen); | |
181 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
182 resolveSupportId(supportId); | |
183 } | |
184 } else { | |
185 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
186 } | |
187 } | |
188 | |
189 /** | |
190 * Callback function called when the state of the client plugin changes. The | |
191 * current state is available via the |state| member variable. | |
192 * | |
193 * @param {number} oldState The previous state of the plugin. | |
194 */ | |
195 // TODO(jamiewalch): Make this pass both the current and old states to avoid | |
196 // race conditions. | |
197 function onClientStateChange_(oldState) { | |
198 if (!remoting.clientSession) { | |
199 // If the connection has been cancelled, then we no longer have a reference | |
200 // to the session object and should ignore any state changes. | |
201 return; | |
202 } | |
203 var state = remoting.clientSession.state; | |
204 if (state == remoting.ClientSession.State.CREATED) { | |
205 remoting.debug.log('Created plugin'); | |
206 | |
207 } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { | |
208 showConnectError_(remoting.ClientError.BAD_PLUGIN_VERSION); | |
209 | |
210 } else if (state == remoting.ClientSession.State.CONNECTING) { | |
211 remoting.debug.log('Connecting as ' + remoting.oauth2.getCachedEmail()); | |
212 | |
213 } else if (state == remoting.ClientSession.State.INITIALIZING) { | |
214 remoting.debug.log('Initializing connection'); | |
215 | |
216 } else if (state == remoting.ClientSession.State.CONNECTED) { | |
217 if (remoting.clientSession) { | |
218 remoting.setMode(remoting.AppMode.IN_SESSION); | |
219 recenterToolbar_(); | |
220 showToolbarPreview_(); | |
221 updateStatistics_(); | |
222 } | |
223 | |
224 } else if (state == remoting.ClientSession.State.CLOSED) { | |
225 if (oldState == remoting.ClientSession.State.CONNECTED) { | |
226 remoting.clientSession.removePlugin(); | |
227 remoting.clientSession = null; | |
228 remoting.debug.log('Connection closed by host'); | |
229 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
230 } else { | |
231 // The transition from CONNECTING to CLOSED state may happen | |
232 // only with older client plugins. Current version should go the | |
233 // FAILED state when connection fails. | |
234 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
235 } | |
236 | |
237 } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { | |
238 remoting.debug.log('Client plugin reported connection failed: ' + | |
239 remoting.clientSession.error); | |
240 if (remoting.clientSession.error == | |
241 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { | |
242 showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); | |
243 } else if (remoting.clientSession.error == | |
244 remoting.ClientSession.ConnectionError.SESSION_REJECTED) { | |
245 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
246 } else if (remoting.clientSession.error == | |
247 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { | |
248 showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); | |
249 } else if (remoting.clientSession.error == | |
250 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { | |
251 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
252 } else { | |
253 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
254 } | |
255 | |
256 } else { | |
257 remoting.debug.log('Unexpected client plugin state: ' + state); | |
258 // This should only happen if the web-app and client plugin get out of | |
259 // sync, and even then the version check should allow compatibility. | |
260 showConnectError_(remoting.ClientError.MISSING_PLUGIN); | |
261 } | |
262 } | |
263 | |
264 /** | |
265 * Create the client session object and initiate the connection. | |
266 * | |
267 * @return {void} Nothing. | |
268 */ | |
269 function startSession_() { | |
270 remoting.debug.log('Starting session...'); | |
271 var accessCode = document.getElementById('access-code-entry'); | |
272 accessCode.value = ''; // The code has been validated and won't work again. | |
273 remoting.clientSession = | |
274 new remoting.ClientSession( | |
275 remoting.hostJid, remoting.hostPublicKey, | |
276 remoting.accessCode, | |
277 /** @type {string} */ (remoting.oauth2.getCachedEmail()), | |
278 onClientStateChange_); | |
279 /** @param {string} token The auth token. */ | |
280 var createPluginAndConnect = function(token) { | |
281 remoting.clientSession.createPluginAndConnect( | |
282 document.getElementById('session-mode'), | |
283 token); | |
284 }; | |
285 remoting.oauth2.callWithToken(createPluginAndConnect); | |
286 } | |
287 | |
288 /** | |
289 * Show a client-side error message. | |
290 * | |
291 * @param {remoting.ClientError} errorTag The error to be localized and | |
292 * displayed. | |
293 * @return {void} Nothing. | |
294 */ | |
295 function showConnectError_(errorTag) { | |
296 remoting.debug.log('Connection failed: ' + errorTag); | |
297 var errorDiv = document.getElementById('connect-error-message'); | |
298 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); | |
299 remoting.accessCode = ''; | |
300 if (remoting.clientSession) { | |
301 remoting.clientSession.disconnect(); | |
302 remoting.clientSession = null; | |
303 } | |
304 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); | |
305 } | |
306 | |
307 /** | |
308 * Parse the response from the server to a request to resolve a support id. | |
309 * | |
310 * @param {XMLHttpRequest} xhr The XMLHttpRequest object. | |
311 * @return {void} Nothing. | |
312 */ | |
313 function parseServerResponse_(xhr) { | |
314 remoting.supportHostsXhr_ = null; | |
315 remoting.debug.log('parseServerResponse: status = ' + xhr.status); | |
316 if (xhr.status == 200) { | |
317 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ | |
318 JSON.parse(xhr.responseText); | |
319 if (host.data && host.data.jabberId && host.data.publicKey) { | |
320 remoting.hostJid = host.data.jabberId; | |
321 remoting.hostPublicKey = host.data.publicKey; | |
322 var split = remoting.hostJid.split('/'); | |
323 document.getElementById('connected-to').innerText = split[0]; | |
324 startSession_(); | |
325 return; | |
326 } | |
327 } | |
328 var errorMsg = remoting.ClientError.OTHER_ERROR; | |
329 if (xhr.status == 404) { | |
330 errorMsg = remoting.ClientError.INVALID_ACCESS_CODE; | |
331 } else if (xhr.status == 0) { | |
332 errorMsg = remoting.ClientError.NO_RESPONSE; | |
333 } else { | |
334 remoting.debug.log('The server responded: ' + xhr.responseText); | |
335 } | |
336 showConnectError_(errorMsg); | |
337 } | |
338 | |
339 /** | |
340 * Normalize the access code entered by the user. | |
341 * | |
342 * @param {string} accessCode The access code, as entered by the user. | |
343 * @return {string} The normalized form of the code (whitespace removed). | |
344 */ | |
345 function normalizeAccessCode_(accessCode) { | |
346 // Trim whitespace. | |
347 // TODO(sergeyu): Do we need to do any other normalization here? | |
348 return accessCode.replace(/\s/g, ''); | |
349 } | |
350 | |
351 /** | |
352 * Initiate a request to the server to resolve a support ID. | |
353 * | |
354 * @param {string} supportId The canonicalized support ID. | |
355 */ | |
356 function resolveSupportId(supportId) { | |
357 var headers = { | |
358 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
359 }; | |
360 | |
361 remoting.supportHostsXhr_ = remoting.xhr.get( | |
362 'https://www.googleapis.com/chromoting/v1/support-hosts/' + | |
363 encodeURIComponent(supportId), | |
364 parseServerResponse_, | |
365 '', | |
366 headers); | |
367 } | |
368 | |
369 /** | |
370 * Timer callback to update the statistics panel. | |
371 */ | |
372 function updateStatistics_() { | |
373 if (!remoting.clientSession || | |
374 remoting.clientSession.state != remoting.ClientSession.State.CONNECTED) { | |
375 return; | |
376 } | |
377 remoting.debug.updateStatistics(remoting.clientSession.stats()); | |
378 // Update the stats once per second. | |
379 window.setTimeout(updateStatistics_, 1000); | |
380 } | |
381 | |
382 /** | |
383 * Force-show the tool-bar for three seconds to aid discoverability. | |
384 */ | |
385 function showToolbarPreview_() { | |
386 var toolbar = document.getElementById('session-toolbar'); | |
387 addClass(toolbar, 'toolbar-preview'); | |
388 window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); | |
389 } | |
390 | |
391 /** | |
392 * Update the horizontal position of the tool-bar to center it. | |
393 */ | |
394 function recenterToolbar_() { | |
395 var toolbar = document.getElementById('session-toolbar'); | |
396 var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; | |
397 toolbar.style['left'] = toolbarX + 'px'; | |
398 } | |
399 | |
400 | |
401 }()); | |
OLD | NEW |