Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(917)

Side by Side Diff: remoting/webapp/me2mom/remoting.js

Issue 8416007: Refactored web-app (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Added consistency comment. Moved debug log keyboard shortcut handling. Created 9 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 }());
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698