OLD | NEW |
---|---|
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
Jamie
2011/10/27 20:41:36
The diffs are not very useful for this file. Basic
garykac
2011/10/27 22:32:39
OK
| |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 'use strict'; | 5 'use strict'; |
6 | 6 |
7 /** @suppress {duplicate} */ | 7 /** @suppress {duplicate} */ |
8 var remoting = remoting || {}; | 8 var remoting = remoting || {}; |
9 | 9 |
10 /** | 10 /** @type {remoting.HostSession} */ remoting.hostSession = null; |
11 * Whether or not the plugin should scale itself. | |
12 * @type {boolean} | |
13 */ | |
14 remoting.scaleToFit = false; | |
15 | |
16 /** @type {remoting.ClientSession} */ | |
17 remoting.session = null; | |
18 | |
19 /** @type {string} */ remoting.accessCode = ''; | |
20 /** @type {number} */ remoting.accessCodeTimerId = 0; | |
21 /** @type {number} */ remoting.accessCodeExpiresIn = 0; | |
22 /** @type {remoting.AppMode} */ remoting.currentMode; | |
23 /** @type {string} */ remoting.hostJid = ''; | |
24 /** @type {string} */ remoting.hostPublicKey = ''; | |
25 /** @type {boolean} */ remoting.lastShareWasCancelled = false; | |
26 /** @type {boolean} */ remoting.timerRunning = false; | |
27 /** @type {string} */ remoting.username = ''; | |
28 | |
29 /** @enum {string} */ | |
30 remoting.AppMode = { | |
31 HOME: 'home', | |
32 UNAUTHENTICATED: 'auth', | |
33 CLIENT: 'client', | |
34 CLIENT_UNCONNECTED: 'client.unconnected', | |
35 CLIENT_CONNECTING: 'client.connecting', | |
36 CLIENT_CONNECT_FAILED: 'client.connect-failed', | |
37 CLIENT_SESSION_FINISHED: 'client.session-finished', | |
38 HOST: 'host', | |
39 HOST_WAITING_FOR_CODE: 'host.waiting-for-code', | |
40 HOST_WAITING_FOR_CONNECTION: 'host.waiting-for-connection', | |
41 HOST_SHARED: 'host.shared', | |
42 HOST_SHARE_FAILED: 'host.share-failed', | |
43 HOST_SHARE_FINISHED: 'host.share-finished', | |
44 IN_SESSION: 'in-session' | |
45 }; | |
46 | 11 |
47 (function() { | 12 (function() { |
48 | 13 |
49 window.addEventListener('blur', pluginLostFocus_, false); | |
50 | |
51 function pluginLostFocus_() { | |
52 // If the plug loses input focus, release all keys as a precaution against | |
53 // leaving them 'stuck down' on the host. | |
54 if (remoting.session && remoting.session.plugin) { | |
55 remoting.session.plugin.releaseAllKeys(); | |
56 } | |
57 } | |
58 | |
59 /** @type {string} */ | |
60 remoting.HOST_PLUGIN_ID = 'host-plugin-id'; | |
61 | |
62 /** @enum {string} */ | |
63 remoting.ClientError = { | |
64 NO_RESPONSE: /*i18n-content*/'ERROR_NO_RESPONSE', | |
65 INVALID_ACCESS_CODE: /*i18n-content*/'ERROR_INVALID_ACCESS_CODE', | |
66 MISSING_PLUGIN: /*i18n-content*/'ERROR_MISSING_PLUGIN', | |
67 OAUTH_FETCH_FAILED: /*i18n-content*/'ERROR_AUTHENTICATION_FAILED', | |
68 HOST_IS_OFFLINE: /*i18n-content*/'ERROR_HOST_IS_OFFLINE', | |
69 INCOMPATIBLE_PROTOCOL: /*i18n-content*/'ERROR_INCOMPATIBLE_PROTOCOL', | |
70 BAD_PLUGIN_VERSION: /*i18n-content*/'ERROR_BAD_PLUGIN_VERSION', | |
71 OTHER_ERROR: /*i18n-content*/'ERROR_GENERIC' | |
72 }; | |
73 | |
74 // Constants representing keys used for storing persistent application state. | |
75 var KEY_APP_MODE_ = 'remoting-app-mode'; | |
76 var KEY_EMAIL_ = 'remoting-email'; | |
77 var KEY_USE_P2P_API_ = 'remoting-use-p2p-api'; | |
78 | |
79 // Some constants for pretty-printing the access code. | |
80 /** @type {number} */ var kSupportIdLen = 7; | |
81 /** @type {number} */ var kHostSecretLen = 5; | |
82 /** @type {number} */ var kAccessCodeLen = kSupportIdLen + kHostSecretLen; | |
83 /** @type {number} */ var kDigitsPerGroup = 4; | |
84 | |
85 /** | 14 /** |
86 * @param {string} classes A space-separated list of classes. | 15 * Entry point for app initialization. |
87 * @param {string} cls The class to check for. | |
88 * @return {boolean} True if |cls| is found within |classes|. | |
89 */ | 16 */ |
90 function hasClass(classes, cls) { | |
91 return classes.match(new RegExp('(\\s|^)' + cls + '(\\s|$)')) != null; | |
92 } | |
93 | |
94 /** | |
95 * @param {Element} element The element to which to add the class. | |
96 * @param {string} cls The new class. | |
97 * @return {void} Nothing. | |
98 */ | |
99 function addClass(element, cls) { | |
100 if (!hasClass(element.className, cls)) { | |
101 var padded = element.className == '' ? '' : element.className + ' '; | |
102 element.className = padded + cls; | |
103 } | |
104 } | |
105 | |
106 /** | |
107 * @param {Element} element The element from which to remove the class. | |
108 * @param {string} cls The new class. | |
109 * @return {void} Nothing. | |
110 */ | |
111 function removeClass(element, cls) { | |
112 element.className = | |
113 element.className.replace(new RegExp('\\b' + cls + '\\b', 'g'), '') | |
114 .replace(' ', ' '); | |
115 } | |
116 | |
117 function retrieveEmail_(access_token) { | |
118 var headers = { | |
119 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
120 }; | |
121 | |
122 /** @param {XMLHttpRequest} xhr The XHR response. */ | |
123 var onResponse = function(xhr) { | |
124 if (xhr.status != 200) { | |
125 // TODO(ajwong): Have a better way of showing an error. | |
126 remoting.debug.log('Unable to get email'); | |
127 document.getElementById('current-email').innerText = '???'; | |
128 return; | |
129 } | |
130 | |
131 // TODO(ajwong): See if we can't find a JSON endpoint. | |
132 setEmail(xhr.responseText.split('&')[0].split('=')[1]); | |
133 }; | |
134 | |
135 // TODO(ajwong): Update to new v2 API. | |
136 remoting.xhr.get('https://www.googleapis.com/userinfo/email', | |
137 onResponse, '', headers); | |
138 } | |
139 | |
140 function refreshEmail_() { | |
141 if (!getEmail() && remoting.oauth2.isAuthenticated()) { | |
142 remoting.oauth2.callWithToken(retrieveEmail_); | |
143 } | |
144 } | |
145 | |
146 /** | |
147 * @param {string} value The email address to place in local storage. | |
148 * @return {void} Nothing. | |
149 */ | |
150 function setEmail(value) { | |
151 window.localStorage.setItem(KEY_EMAIL_, value); | |
152 document.getElementById('current-email').innerText = value; | |
153 } | |
154 | |
155 /** | |
156 * @return {?string} The email address associated with the auth credentials. | |
157 */ | |
158 function getEmail() { | |
159 var result = window.localStorage.getItem(KEY_EMAIL_); | |
160 return typeof result == 'string' ? result : null; | |
161 } | |
162 | |
163 function exchangedCodeForToken_() { | |
164 if (!remoting.oauth2.isAuthenticated()) { | |
165 alert('Your OAuth2 token was invalid. Please try again.'); | |
166 } | |
167 /** @param {string} token The auth token. */ | |
168 var retrieveEmail = function(token) { retrieveEmail_(token); } | |
169 remoting.oauth2.callWithToken(retrieveEmail); | |
170 } | |
171 | |
172 remoting.clearOAuth2 = function() { | |
173 remoting.oauth2.clear(); | |
174 window.localStorage.removeItem(KEY_EMAIL_); | |
175 remoting.setMode(remoting.AppMode.UNAUTHENTICATED); | |
176 } | |
177 | |
178 remoting.toggleDebugLog = function() { | |
179 var debugLog = document.getElementById('debug-log'); | |
180 if (debugLog.hidden) { | |
181 debugLog.hidden = false; | |
182 } else { | |
183 debugLog.hidden = true; | |
184 } | |
185 } | |
186 | |
187 remoting.init = function() { | 17 remoting.init = function() { |
188 l10n.localize(); | 18 l10n.localize(); |
189 var button = document.getElementById('toggle-scaling'); | 19 var button = document.getElementById('toggle-scaling'); |
190 button.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_SCALING'); | 20 button.title = chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_SCALING'); |
191 // Create global objects. | 21 // Create global objects. |
192 remoting.oauth2 = new remoting.OAuth2(); | 22 remoting.oauth2 = new remoting.OAuth2(); |
193 remoting.debug = | 23 remoting.debug = new remoting.DebugLog( |
194 new remoting.DebugLog(document.getElementById('debug-messages')); | 24 document.getElementById('debug-messages'), |
195 /** @type {XMLHttpRequest} */ | 25 document.getElementById('statistics')); |
196 remoting.supportHostsXhr = null; | |
197 | 26 |
198 refreshEmail_(); | 27 refreshEmail_(); |
199 var email = getEmail(); | 28 var email = remoting.oauth2.getCachedEmail(); |
200 if (email) { | 29 if (email) { |
201 document.getElementById('current-email').innerText = email; | 30 document.getElementById('current-email').innerText = email; |
202 } | 31 } |
203 | 32 |
204 remoting.setMode(getAppStartupMode()); | 33 remoting.setMode(getAppStartupMode_()); |
205 if (isHostModeSupported()) { | 34 if (isHostModeSupported_()) { |
206 var unsupported = document.getElementById('client-footer-text-cros'); | 35 var unsupported = document.getElementById('client-footer-text-cros'); |
207 unsupported.parentNode.removeChild(unsupported); | 36 unsupported.parentNode.removeChild(unsupported); |
208 } else { | 37 } else { |
209 var footer = document.getElementById('client-footer-text'); | 38 var footer = document.getElementById('client-footer-text'); |
210 footer.parentNode.removeChild(footer); | 39 footer.parentNode.removeChild(footer); |
211 document.getElementById('client-footer-text-cros').id = | 40 document.getElementById('client-footer-text-cros').id = |
212 'client-footer-text'; | 41 'client-footer-text'; |
213 } | 42 } |
214 } | |
215 | 43 |
216 /** | 44 window.addEventListener('blur', pluginLostFocus_, false); |
217 * Change the app's modal state to |mode|, which is considered to be a dotted | |
218 * hierachy of modes. For example, setMode('host.shared') will show any modal | |
219 * elements with an data-ui-mode attribute of 'host' or 'host.shared' and hide | |
220 * all others. | |
221 * | |
222 * @param {remoting.AppMode} mode The new modal state, expressed as a dotted | |
223 * hiearchy. | |
224 */ | |
225 remoting.setMode = function(mode) { | |
226 var modes = mode.split('.'); | |
227 for (var i = 1; i < modes.length; ++i) | |
228 modes[i] = modes[i - 1] + '.' + modes[i]; | |
229 var elements = document.querySelectorAll('[data-ui-mode]'); | |
230 for (var i = 0; i < elements.length; ++i) { | |
231 /** @type {Element} */ var element = elements[i]; | |
232 var hidden = true; | |
233 for (var m = 0; m < modes.length; ++m) { | |
234 if (hasClass(element.getAttribute('data-ui-mode'), modes[m])) { | |
235 hidden = false; | |
236 break; | |
237 } | |
238 } | |
239 element.hidden = hidden; | |
240 } | |
241 remoting.debug.log('App mode: ' + mode); | |
242 remoting.currentMode = mode; | |
243 if (mode == remoting.AppMode.IN_SESSION) { | |
244 document.removeEventListener('keydown', remoting.checkHotkeys, false); | |
245 } else { | |
246 document.addEventListener('keydown', remoting.checkHotkeys, false); | |
247 } | |
248 } | |
249 | |
250 /** | |
251 * Get the major mode that the app is running in. | |
252 * @return {string} The app's current major mode. | |
253 */ | |
254 remoting.getMajorMode = function() { | |
255 return remoting.currentMode.split('.')[0]; | |
256 } | |
257 | |
258 remoting.tryShare = function() { | |
259 remoting.debug.log('Attempting to share...'); | |
260 remoting.lastShareWasCancelled = false; | |
261 if (remoting.oauth2.needsNewAccessToken()) { | |
262 remoting.debug.log('Refreshing token...'); | |
263 remoting.oauth2.refreshAccessToken(function() { | |
264 if (remoting.oauth2.needsNewAccessToken()) { | |
265 // If we still need it, we're going to infinite loop. | |
266 showShareError_(/*i18n-content*/'ERROR_AUTHENTICATION_FAILED'); | |
267 throw 'Unable to get access token'; | |
268 } | |
269 remoting.tryShare(); | |
270 }); | |
271 return; | |
272 } | |
273 | |
274 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); | |
275 document.getElementById('cancel-button').disabled = false; | |
276 disableTimeoutCountdown_(); | |
277 | |
278 var div = document.getElementById('host-plugin-container'); | |
279 var plugin = /** @type {remoting.HostPlugin} */ | |
280 document.createElement('embed'); | |
281 plugin.type = remoting.PLUGIN_MIMETYPE; | |
282 plugin.id = remoting.HOST_PLUGIN_ID; | |
283 // Hiding the plugin means it doesn't load, so make it size zero instead. | |
284 plugin.width = 0; | |
285 plugin.height = 0; | |
286 div.appendChild(plugin); | |
287 onNatTraversalPolicyChanged_(true); // Hide warning by default. | |
288 plugin.onNatTraversalPolicyChanged = onNatTraversalPolicyChanged_; | |
289 plugin.onStateChanged = onStateChanged_; | |
290 plugin.logDebugInfo = debugInfoCallback_; | |
291 plugin.localize(chrome.i18n.getMessage); | |
292 plugin.connect(/** @type {string} */ (getEmail()), | |
293 'oauth2:' + remoting.oauth2.getAccessToken()); | |
294 } | |
295 | |
296 function disableTimeoutCountdown_() { | |
297 if (remoting.timerRunning) { | |
298 clearInterval(remoting.accessCodeTimerId); | |
299 remoting.timerRunning = false; | |
300 updateTimeoutStyles_(); | |
301 } | |
302 } | |
303 | |
304 var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD = 30; | |
305 var ACCESS_CODE_RED_THRESHOLD = 10; | |
306 | |
307 /** | |
308 * Show/hide or restyle various elements, depending on the remaining countdown | |
309 * and timer state. | |
310 * | |
311 * @return {boolean} True if the timeout is in progress, false if it has | |
312 * expired. | |
313 */ | |
314 function updateTimeoutStyles_() { | |
315 if (remoting.timerRunning) { | |
316 if (remoting.accessCodeExpiresIn <= 0) { | |
317 remoting.cancelShare(); | |
318 return false; | |
319 } | |
320 if (remoting.accessCodeExpiresIn <= ACCESS_CODE_RED_THRESHOLD) { | |
321 addClass(document.getElementById('access-code-display'), 'expiring'); | |
322 } else { | |
323 removeClass(document.getElementById('access-code-display'), 'expiring'); | |
324 } | |
325 } | |
326 document.getElementById('access-code-countdown').hidden = | |
327 (remoting.accessCodeExpiresIn > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD) || | |
328 !remoting.timerRunning; | |
329 return true; | |
330 } | |
331 | |
332 remoting.decrementAccessCodeTimeout_ = function() { | |
333 --remoting.accessCodeExpiresIn; | |
334 remoting.updateAccessCodeTimeoutElement_(); | |
335 } | |
336 | |
337 remoting.updateAccessCodeTimeoutElement_ = function() { | |
338 var pad = (remoting.accessCodeExpiresIn < 10) ? '0:0' : '0:'; | |
339 l10n.localizeElement(document.getElementById('seconds-remaining'), | |
340 pad + remoting.accessCodeExpiresIn); | |
341 if (!updateTimeoutStyles_()) { | |
342 disableTimeoutCountdown_(); | |
343 } | |
344 } | |
345 | |
346 /** | |
347 * Callback to show or hide the NAT traversal warning when the policy changes. | |
348 * @param {boolean} enabled True if NAT traversal is enabled. | |
349 * @return {void} Nothing. | |
350 */ | |
351 function onNatTraversalPolicyChanged_(enabled) { | |
352 var container = document.getElementById('nat-box-container'); | |
353 container.hidden = enabled; | |
354 } | |
355 | |
356 /** | |
357 * Callback for the host plugin to notify the web app of state changes. | |
358 * @param {number} state The new state of the plugin. | |
359 */ | |
360 function onStateChanged_(state) { | |
361 var plugin = /** @type {remoting.HostPlugin} */ | |
362 document.getElementById(remoting.HOST_PLUGIN_ID); | |
363 if (state == plugin.STARTING) { | |
364 // Nothing to do here. | |
365 remoting.debug.log('Host plugin state: STARTING'); | |
366 } else if (state == plugin.REQUESTED_ACCESS_CODE) { | |
367 // Nothing to do here. | |
368 remoting.debug.log('Host plugin state: REQUESTED_ACCESS_CODE'); | |
369 } else if (state == plugin.RECEIVED_ACCESS_CODE) { | |
370 remoting.debug.log('Host plugin state: RECEIVED_ACCESS_CODE'); | |
371 var accessCode = plugin.accessCode; | |
372 var accessCodeDisplay = document.getElementById('access-code-display'); | |
373 accessCodeDisplay.innerText = ''; | |
374 // Display the access code in groups of four digits for readability. | |
375 for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) { | |
376 var nextFourDigits = document.createElement('span'); | |
377 nextFourDigits.className = 'access-code-digit-group'; | |
378 nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup); | |
379 accessCodeDisplay.appendChild(nextFourDigits); | |
380 } | |
381 remoting.accessCodeExpiresIn = plugin.accessCodeLifetime; | |
382 if (remoting.accessCodeExpiresIn > 0) { // Check it hasn't expired. | |
383 remoting.accessCodeTimerId = setInterval( | |
384 'remoting.decrementAccessCodeTimeout_()', 1000); | |
385 remoting.timerRunning = true; | |
386 remoting.updateAccessCodeTimeoutElement_(); | |
387 updateTimeoutStyles_(); | |
388 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION); | |
389 } else { | |
390 // This can only happen if the cloud tells us that the code lifetime is | |
391 // <= 0s, which shouldn't happen so we don't care how clean this UX is. | |
392 remoting.debug.log('Access code already invalid on receipt!'); | |
393 remoting.cancelShare(); | |
394 } | |
395 } else if (state == plugin.CONNECTED) { | |
396 remoting.debug.log('Host plugin state: CONNECTED'); | |
397 var element = document.getElementById('host-shared-message'); | |
398 var client = plugin.client; | |
399 l10n.localizeElement(element, client); | |
400 remoting.setMode(remoting.AppMode.HOST_SHARED); | |
401 disableTimeoutCountdown_(); | |
402 } else if (state == plugin.DISCONNECTING) { | |
403 remoting.debug.log('Host plugin state: DISCONNECTING'); | |
404 } else if (state == plugin.DISCONNECTED) { | |
405 remoting.debug.log('Host plugin state: DISCONNECTED'); | |
406 if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) { | |
407 // If an error is being displayed, then the plugin should not be able to | |
408 // hide it by setting the state. Errors must be dismissed by the user | |
409 // clicking OK, which puts the app into mode HOME. | |
410 if (remoting.lastShareWasCancelled) { | |
411 remoting.setMode(remoting.AppMode.HOME); | |
412 } else { | |
413 remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED); | |
414 } | |
415 } | |
416 plugin.parentNode.removeChild(plugin); | |
417 } else if (state == plugin.ERROR) { | |
418 remoting.debug.log('Host plugin state: ERROR'); | |
419 showShareError_(/*i18n-content*/'ERROR_GENERIC'); | |
420 } else { | |
421 remoting.debug.log('Unknown state -> ' + state); | |
422 } | |
423 } | |
424 | |
425 /** | |
426 * This is the callback that the host plugin invokes to indicate that there | |
427 * is additional debug log info to display. | |
428 * @param {string} msg The message (which will not be localized) to be logged. | |
429 */ | |
430 function debugInfoCallback_(msg) { | |
431 remoting.debug.log('plugin: ' + msg); | |
432 } | |
433 | |
434 /** | |
435 * Show a host-side error message. | |
436 * | |
437 * @param {string} errorTag The error message to be localized and displayed. | |
438 * @return {void} Nothing. | |
439 */ | |
440 function showShareError_(errorTag) { | |
441 var errorDiv = document.getElementById('host-plugin-error'); | |
442 l10n.localizeElementFromTag(errorDiv, errorTag); | |
443 remoting.debug.log('Sharing error: ' + errorTag); | |
444 remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED); | |
445 } | |
446 | |
447 /** | |
448 * Cancel an active or pending share operation. | |
449 * | |
450 * @return {void} Nothing. | |
451 */ | |
452 remoting.cancelShare = function() { | |
453 remoting.debug.log('Canceling share...'); | |
454 remoting.lastShareWasCancelled = true; | |
455 var plugin = /** @type {remoting.HostPlugin} */ | |
456 document.getElementById(remoting.HOST_PLUGIN_ID); | |
457 try { | |
458 plugin.disconnect(); | |
459 } catch (error) { | |
460 // Hack to force JSCompiler type-safety. | |
461 var errorTyped = /** @type {{description: string}} */ error; | |
462 remoting.debug.log('Error disconnecting: ' + errorTyped.description + | |
463 '. The host plugin probably crashed.'); | |
464 // TODO(jamiewalch): Clean this up. We should have a class representing | |
465 // the host plugin, like we do for the client, which should handle crash | |
466 // reporting and it should use a more detailed error message than the | |
467 // default 'generic' one. See crbug.com/94624 | |
468 showShareError_(/*i18n-content*/'ERROR_GENERIC'); | |
469 } | |
470 disableTimeoutCountdown_(); | |
471 } | |
472 | |
473 /** | |
474 * Cancel an incomplete connect operation. | |
475 * | |
476 * @return {void} Nothing. | |
477 */ | |
478 remoting.cancelConnect = function() { | |
479 if (remoting.supportHostsXhr) { | |
480 remoting.supportHostsXhr.abort(); | |
481 remoting.supportHostsXhr = null; | |
482 } | |
483 if (remoting.session) { | |
484 remoting.session.removePlugin(); | |
485 remoting.session = null; | |
486 } | |
487 remoting.setMode(remoting.AppMode.HOME); | |
488 } | |
489 | |
490 function updateStatistics() { | |
491 if (!remoting.session) | |
492 return; | |
493 if (remoting.session.state != remoting.ClientSession.State.CONNECTED) | |
494 return; | |
495 var stats = remoting.session.stats(); | |
496 | |
497 var units = ''; | |
498 var videoBandwidth = stats['video_bandwidth']; | |
499 if (videoBandwidth < 1024) { | |
500 units = 'Bps'; | |
501 } else if (videoBandwidth < 1048576) { | |
502 units = 'KiBps'; | |
503 videoBandwidth = videoBandwidth / 1024; | |
504 } else if (videoBandwidth < 1073741824) { | |
505 units = 'MiBps'; | |
506 videoBandwidth = videoBandwidth / 1048576; | |
507 } else { | |
508 units = 'GiBps'; | |
509 videoBandwidth = videoBandwidth / 1073741824; | |
510 } | |
511 | |
512 var statistics = document.getElementById('statistics'); | |
513 statistics.innerText = | |
514 'Bandwidth: ' + videoBandwidth.toFixed(2) + units + | |
515 ', Frame Rate: ' + | |
516 (stats['video_frame_rate'] ? | |
517 stats['video_frame_rate'].toFixed(2) + ' fps' : 'n/a') + | |
518 ', Capture: ' + stats['capture_latency'].toFixed(2) + 'ms' + | |
519 ', Encode: ' + stats['encode_latency'].toFixed(2) + 'ms' + | |
520 ', Decode: ' + stats['decode_latency'].toFixed(2) + 'ms' + | |
521 ', Render: ' + stats['render_latency'].toFixed(2) + 'ms' + | |
522 ', Latency: ' + stats['roundtrip_latency'].toFixed(2) + 'ms'; | |
523 | |
524 // Update the stats once per second. | |
525 window.setTimeout(updateStatistics, 1000); | |
526 } | |
527 | |
528 function showToolbarPreview_() { | |
529 var toolbar = document.getElementById('session-toolbar'); | |
530 addClass(toolbar, 'toolbar-preview'); | |
531 window.setTimeout(removeClass, 3000, toolbar, 'toolbar-preview'); | |
532 } | |
533 | |
534 /** @param {number} oldState The previous state of the plugin. */ | |
535 function onClientStateChange_(oldState) { | |
536 if (!remoting.session) { | |
537 // If the connection has been cancelled, then we no longer have a reference | |
538 // to the session object and should ignore any state changes. | |
539 return; | |
540 } | |
541 var state = remoting.session.state; | |
542 if (state == remoting.ClientSession.State.CREATED) { | |
543 remoting.debug.log('Created plugin'); | |
544 } else if (state == remoting.ClientSession.State.BAD_PLUGIN_VERSION) { | |
545 showConnectError_(remoting.ClientError.BAD_PLUGIN_VERSION); | |
546 } else if (state == remoting.ClientSession.State.CONNECTING) { | |
547 remoting.debug.log('Connecting as ' + remoting.username); | |
548 } else if (state == remoting.ClientSession.State.INITIALIZING) { | |
549 remoting.debug.log('Initializing connection'); | |
550 } else if (state == remoting.ClientSession.State.CONNECTED) { | |
551 if (remoting.session) { | |
552 remoting.setMode(remoting.AppMode.IN_SESSION); | |
553 recenterToolbar_(); | |
554 showToolbarPreview_(); | |
555 updateStatistics(); | |
556 } | |
557 } else if (state == remoting.ClientSession.State.CLOSED) { | |
558 if (oldState == remoting.ClientSession.State.CONNECTED) { | |
559 remoting.session.removePlugin(); | |
560 remoting.session = null; | |
561 remoting.debug.log('Connection closed by host'); | |
562 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
563 } else { | |
564 // The transition from CONNECTING to CLOSED state may happen | |
565 // only with older client plugins. Current version should go the | |
566 // FAILED state when connection fails. | |
567 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
568 } | |
569 } else if (state == remoting.ClientSession.State.CONNECTION_FAILED) { | |
570 remoting.debug.log('Client plugin reported connection failed: ' + | |
571 remoting.session.error); | |
572 if (remoting.session.error == | |
573 remoting.ClientSession.ConnectionError.HOST_IS_OFFLINE) { | |
574 showConnectError_(remoting.ClientError.HOST_IS_OFFLINE); | |
575 } else if (remoting.session.error == | |
576 remoting.ClientSession.ConnectionError.SESSION_REJECTED) { | |
577 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
578 } else if (remoting.session.error == | |
579 remoting.ClientSession.ConnectionError.INCOMPATIBLE_PROTOCOL) { | |
580 showConnectError_(remoting.ClientError.INCOMPATIBLE_PROTOCOL); | |
581 } else if (remoting.session.error == | |
582 remoting.ClientSession.ConnectionError.NETWORK_FAILURE) { | |
583 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
584 } else { | |
585 showConnectError_(remoting.ClientError.OTHER_ERROR); | |
586 } | |
587 } else { | |
588 remoting.debug.log('Unexpected client plugin state: ' + state); | |
589 // This should only happen if the web-app and client plugin get out of | |
590 // sync, and even then the version check should allow compatibility. | |
591 showConnectError_(remoting.ClientError.MISSING_PLUGIN); | |
592 } | |
593 } | |
594 | |
595 function startSession_() { | |
596 remoting.debug.log('Starting session...'); | |
597 var accessCode = document.getElementById('access-code-entry'); | |
598 accessCode.value = ''; // The code has been validated and won't work again. | |
599 remoting.username = | |
600 /** @type {string} email must be non-NULL to get here */ getEmail(); | |
601 remoting.session = | |
602 new remoting.ClientSession(remoting.hostJid, remoting.hostPublicKey, | |
603 remoting.accessCode, remoting.username, | |
604 onClientStateChange_); | |
605 /** @param {string} token The auth token. */ | |
606 var createPluginAndConnect = function(token) { | |
607 remoting.session.createPluginAndConnect( | |
608 document.getElementById('session-mode'), | |
609 token); | |
610 }; | |
611 remoting.oauth2.callWithToken(createPluginAndConnect); | |
612 } | |
613 | |
614 /** | |
615 * Show a client-side error message. | |
616 * | |
617 * @param {remoting.ClientError} errorTag The error to be localized and | |
618 * displayed. | |
619 * @return {void} Nothing. | |
620 */ | |
621 function showConnectError_(errorTag) { | |
622 remoting.debug.log('Connection failed: ' + errorTag); | |
623 var errorDiv = document.getElementById('connect-error-message'); | |
624 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); | |
625 remoting.accessCode = ''; | |
626 if (remoting.session) { | |
627 remoting.session.disconnect(); | |
628 remoting.session = null; | |
629 } | |
630 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED); | |
631 } | |
632 | |
633 /** | |
634 * @param {XMLHttpRequest} xhr The XMLHttpRequest object. | |
635 * @return {void} Nothing. | |
636 */ | |
637 function parseServerResponse_(xhr) { | |
638 remoting.supportHostsXhr = null; | |
639 remoting.debug.log('parseServerResponse: status = ' + xhr.status); | |
640 if (xhr.status == 200) { | |
641 var host = /** @type {{data: {jabberId: string, publicKey: string}}} */ | |
642 JSON.parse(xhr.responseText); | |
643 if (host.data && host.data.jabberId && host.data.publicKey) { | |
644 remoting.hostJid = host.data.jabberId; | |
645 remoting.hostPublicKey = host.data.publicKey; | |
646 var split = remoting.hostJid.split('/'); | |
647 document.getElementById('connected-to').innerText = split[0]; | |
648 startSession_(); | |
649 return; | |
650 } | |
651 } | |
652 var errorMsg = remoting.ClientError.OTHER_ERROR; | |
653 if (xhr.status == 404) { | |
654 errorMsg = remoting.ClientError.INVALID_ACCESS_CODE; | |
655 } else if (xhr.status == 0) { | |
656 errorMsg = remoting.ClientError.NO_RESPONSE; | |
657 } else { | |
658 remoting.debug.log('The server responded: ' + xhr.responseText); | |
659 } | |
660 showConnectError_(errorMsg); | |
661 } | |
662 | |
663 /** @param {string} accessCode The access code, as entered by the user. | |
664 * @return {string} The normalized form of the code (whitespace removed). */ | |
665 function normalizeAccessCode_(accessCode) { | |
666 // Trim whitespace. | |
667 // TODO(sergeyu): Do we need to do any other normalization here? | |
668 return accessCode.replace(/\s/g, ''); | |
669 } | |
670 | |
671 /** @param {string} supportId The canonicalized support ID. */ | |
672 function resolveSupportId(supportId) { | |
673 var headers = { | |
674 'Authorization': 'OAuth ' + remoting.oauth2.getAccessToken() | |
675 }; | |
676 | |
677 remoting.supportHostsXhr = remoting.xhr.get( | |
678 'https://www.googleapis.com/chromoting/v1/support-hosts/' + | |
679 encodeURIComponent(supportId), | |
680 parseServerResponse_, | |
681 '', | |
682 headers); | |
683 } | |
684 | |
685 remoting.tryConnect = function() { | |
686 document.getElementById('cancel-button').disabled = false; | |
687 if (remoting.oauth2.needsNewAccessToken()) { | |
688 remoting.oauth2.refreshAccessToken(function(xhr) { | |
689 if (remoting.oauth2.needsNewAccessToken()) { | |
690 // Failed to get access token | |
691 remoting.debug.log('tryConnect: OAuth2 token fetch failed'); | |
692 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
693 return; | |
694 } | |
695 remoting.tryConnectWithAccessToken(); | |
696 }); | |
697 } else { | |
698 remoting.tryConnectWithAccessToken(); | |
699 } | |
700 } | |
701 | |
702 remoting.tryConnectWithAccessToken = function() { | |
703 if (!remoting.wcsLoader) { | |
704 remoting.wcsLoader = new remoting.WcsLoader(); | |
705 } | |
706 /** @param {function(string):void} setToken The callback function. */ | |
707 var callWithToken = function(setToken) { | |
708 remoting.oauth2.callWithToken(setToken); | |
709 }; | |
710 remoting.wcsLoader.start( | |
711 remoting.oauth2.getAccessToken(), | |
712 callWithToken, | |
713 remoting.tryConnectWithWcs); | |
714 } | |
715 | |
716 /** | |
717 * WcsLoader callback, called when the wcs script has been loaded, or on error. | |
718 * @param {boolean} success True if the script was loaded successfully. | |
719 */ | |
720 remoting.tryConnectWithWcs = function(success) { | |
721 if (success) { | |
722 var accessCode = document.getElementById('access-code-entry').value; | |
723 remoting.accessCode = normalizeAccessCode_(accessCode); | |
724 // At present, only 12-digit access codes are supported, of which the first | |
725 // 7 characters are the supportId. | |
726 if (remoting.accessCode.length != kAccessCodeLen) { | |
727 remoting.debug.log('Bad access code length'); | |
728 showConnectError_(remoting.ClientError.INVALID_ACCESS_CODE); | |
729 } else { | |
730 var supportId = remoting.accessCode.substring(0, kSupportIdLen); | |
731 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); | |
732 resolveSupportId(supportId); | |
733 } | |
734 } else { | |
735 showConnectError_(remoting.ClientError.OAUTH_FETCH_FAILED); | |
736 } | |
737 } | 45 } |
738 | 46 |
739 remoting.cancelPendingOperation = function() { | 47 remoting.cancelPendingOperation = function() { |
740 document.getElementById('cancel-button').disabled = true; | 48 document.getElementById('cancel-button').disabled = true; |
741 switch (remoting.getMajorMode()) { | 49 switch (remoting.getMajorMode()) { |
742 case remoting.AppMode.HOST: | 50 case remoting.AppMode.HOST: |
743 remoting.cancelShare(); | 51 remoting.cancelShare(); |
744 break; | 52 break; |
745 case remoting.AppMode.CLIENT: | 53 case remoting.AppMode.CLIENT: |
746 remoting.cancelConnect(); | 54 remoting.cancelConnect(); |
747 break; | 55 break; |
748 } | 56 } |
749 } | 57 } |
750 | 58 |
751 /** | 59 /** |
60 * If the client is connected, or the host is shared, prompt before closing. | |
61 * | |
62 * @return {?string} The prompt string if a connection is active. | |
63 */ | |
64 remoting.promptClose = function() { | |
65 switch (remoting.currentMode) { | |
66 case remoting.AppMode.CLIENT_CONNECTING: | |
67 case remoting.AppMode.HOST_WAITING_FOR_CODE: | |
68 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: | |
69 case remoting.AppMode.HOST_SHARED: | |
70 case remoting.AppMode.IN_SESSION: | |
71 var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); | |
72 return result; | |
73 default: | |
74 return null; | |
75 } | |
76 } | |
77 | |
78 /** | |
79 * Sign the user out of Chromoting by clearing the OAuth refresh token. | |
80 */ | |
81 remoting.clearOAuth2 = function() { | |
82 remoting.oauth2.clear(); | |
83 window.localStorage.removeItem(KEY_EMAIL_); | |
84 remoting.setMode(remoting.AppMode.UNAUTHENTICATED); | |
85 } | |
86 | |
87 /** | |
88 * Callback function called when the browser window loses focus. In this case, | |
89 * release all keys to prevent them becoming 'stuck down' on the host. | |
90 */ | |
91 function pluginLostFocus_() { | |
92 if (remoting.clientSession && remoting.clientSession.plugin) { | |
93 remoting.clientSession.plugin.releaseAllKeys(); | |
94 } | |
95 } | |
96 | |
97 /** | |
98 * If the user is authenticated, but there is no email address cached, get one. | |
99 */ | |
100 function refreshEmail_() { | |
101 if (!getEmail_() && remoting.oauth2.isAuthenticated()) { | |
102 remoting.oauth2.getEmail(setEmail_); | |
103 } | |
104 } | |
105 | |
106 /** The key under which the email address is stored. */ | |
107 var KEY_EMAIL_ = 'remoting-email'; | |
108 | |
109 /** | |
110 * Save the user's email address in local storage. | |
111 * | |
112 * @param {?string} email The email address to place in local storage. | |
113 * @return {void} Nothing. | |
114 */ | |
115 function setEmail_(email) { | |
116 if (email) { | |
117 document.getElementById('current-email').innerText = email; | |
118 } else { | |
119 // TODO(ajwong): Have a better way of showing an error. | |
120 document.getElementById('current-email').innerText = '???'; | |
121 } | |
122 } | |
123 | |
124 /** | |
125 * Read the user's email address from local storage. | |
126 * | |
127 * @return {?string} The email address associated with the auth credentials. | |
128 */ | |
129 function getEmail_() { | |
130 var result = window.localStorage.getItem(KEY_EMAIL_); | |
131 return typeof result == 'string' ? result : null; | |
132 } | |
133 /** | |
752 * Gets the major-mode that this application should start up in. | 134 * Gets the major-mode that this application should start up in. |
753 * | 135 * |
754 * @return {remoting.AppMode} The mode to start in. | 136 * @return {remoting.AppMode} The mode to start in. |
755 */ | 137 */ |
756 function getAppStartupMode() { | 138 function getAppStartupMode_() { |
757 if (!remoting.oauth2.isAuthenticated()) { | 139 if (!remoting.oauth2.isAuthenticated()) { |
758 return remoting.AppMode.UNAUTHENTICATED; | 140 return remoting.AppMode.UNAUTHENTICATED; |
759 } | 141 } |
760 if (isHostModeSupported()) { | 142 if (isHostModeSupported_()) { |
761 return remoting.AppMode.HOME; | 143 return remoting.AppMode.HOME; |
762 } else { | 144 } else { |
763 return remoting.AppMode.CLIENT_UNCONNECTED; | 145 return remoting.AppMode.CLIENT_UNCONNECTED; |
764 } | 146 } |
765 } | 147 } |
766 | 148 |
767 /** | 149 /** |
768 * Returns whether Host mode is supported on this platform. | 150 * Returns whether Host mode is supported on this platform. |
769 * | 151 * |
770 * @return {boolean} True if Host mode is supported. | 152 * @return {boolean} True if Host mode is supported. |
771 */ | 153 */ |
772 function isHostModeSupported() { | 154 function isHostModeSupported_() { |
773 // Currently, sharing on Chromebooks is not supported. | 155 // Currently, sharing on Chromebooks is not supported. |
774 return !navigator.userAgent.match(/\bCrOS\b/); | 156 return !navigator.userAgent.match(/\bCrOS\b/); |
775 } | 157 } |
776 | |
777 /** | |
778 * Enable or disable scale-to-fit. | |
779 * | |
780 * @param {Element} button The scale-to-fit button. The style of this button is | |
781 * updated to reflect the new scaling state. | |
782 * @return {void} Nothing. | |
783 */ | |
784 remoting.toggleScaleToFit = function(button) { | |
785 remoting.scaleToFit = !remoting.scaleToFit; | |
786 if (remoting.scaleToFit) { | |
787 addClass(button, 'toggle-button-active'); | |
788 } else { | |
789 removeClass(button, 'toggle-button-active'); | |
790 } | |
791 remoting.session.updateDimensions(); | |
792 } | |
793 | |
794 /** | |
795 * Update the remoting client layout in response to a resize event. | |
796 * | |
797 * @return {void} Nothing. | |
798 */ | |
799 remoting.onResize = function() { | |
800 if (remoting.session) | |
801 remoting.session.onWindowSizeChanged(); | |
802 recenterToolbar_(); | |
803 } | |
804 | |
805 /** | |
806 * Disconnect the remoting client. | |
807 * | |
808 * @return {void} Nothing. | |
809 */ | |
810 remoting.disconnect = function() { | |
811 if (remoting.session) { | |
812 remoting.session.disconnect(); | |
813 remoting.session = null; | |
814 remoting.debug.log('Disconnected.'); | |
815 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED); | |
816 } | |
817 } | |
818 | |
819 /** | |
820 * If the client is connected, or the host is shared, prompt before closing. | |
821 * | |
822 * @return {?string} The prompt string if a connection is active. | |
823 */ | |
824 remoting.promptClose = function() { | |
825 switch (remoting.currentMode) { | |
826 case remoting.AppMode.CLIENT_CONNECTING: | |
827 case remoting.AppMode.HOST_WAITING_FOR_CODE: | |
828 case remoting.AppMode.HOST_WAITING_FOR_CONNECTION: | |
829 case remoting.AppMode.HOST_SHARED: | |
830 case remoting.AppMode.IN_SESSION: | |
831 var result = chrome.i18n.getMessage(/*i18n-content*/'CLOSE_PROMPT'); | |
832 return result; | |
833 default: | |
834 return null; | |
835 } | |
836 } | |
837 | |
838 /** | |
839 * @param {Event} event The keyboard event. | |
840 * @return {void} Nothing. | |
841 */ | |
842 remoting.checkHotkeys = function(event) { | |
843 if (String.fromCharCode(event.which) == 'D') { | |
844 remoting.toggleDebugLog(); | |
845 } | |
846 } | |
847 | |
848 function recenterToolbar_() { | |
849 var toolbar = document.getElementById('session-toolbar'); | |
850 var toolbarX = (window.innerWidth - toolbar.clientWidth) / 2; | |
851 toolbar.style['left'] = toolbarX + 'px'; | |
852 } | |
853 | |
854 }()); | 158 }()); |
OLD | NEW |