1 // Copyright (c) 2012 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 /** 16 * @type {remoting.SessionConnector} The connector object, set when a connection 17 * is initiated. 18 */ 19 remoting.connector = null; 20 21 /** 22 * @type {remoting.ClientSession} The client session object, set once the 23 * connector has invoked its onOk callback. 24 */ 25 remoting.clientSession = null; 26 27 /** 28 * Initiate an IT2Me connection. 29 */ 30 remoting.connectIT2Me = function() { 31 remoting.ensureSessionConnector_(); 32 var accessCode = document.getElementById('access-code-entry').value; 33 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); 34 remoting.connector.connectIT2Me(accessCode); 35 }; 36 37 /** 38 * Update the remoting client layout in response to a resize event. 39 * 40 * @return {void} Nothing. 41 */ 42 remoting.onResize = function() { 43 if (remoting.clientSession) { 44 remoting.clientSession.onResize(); 45 } 46 }; 47 48 /** 49 * Handle changes in the visibility of the window, for example by pausing video. 50 * 51 * @return {void} Nothing. 52 */ 53 remoting.onVisibilityChanged = function() { 54 if (remoting.clientSession) { 55 remoting.clientSession.pauseVideo( 56 ('hidden' in document) ? document.hidden : document.webkitHidden); 57 } 58 } 59 60 /** 61 * Disconnect the remoting client. 62 * 63 * @return {void} Nothing. 64 */ 65 remoting.disconnect = function() { 66 if (!remoting.clientSession) { 67 return; 68 } 69 if (remoting.clientSession.getMode() == remoting.ClientSession.Mode.IT2ME) { 70 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); 71 } else { 72 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); 73 } 74 remoting.clientSession.disconnect(remoting.Error.NONE); 75 remoting.clientSession = null; 76 console.log('Disconnected.'); 77 }; 78 79 /** 80 * Sends a Ctrl-Alt-Del sequence to the remoting client. 81 * 82 * @return {void} Nothing. 83 */ 84 remoting.sendCtrlAltDel = function() { 85 if (remoting.clientSession) { 86 console.log('Sending Ctrl-Alt-Del.'); 87 remoting.clientSession.sendCtrlAltDel(); 88 } 89 }; 90 91 /** 92 * Sends a Print Screen keypress to the remoting client. 93 * 94 * @return {void} Nothing. 95 */ 96 remoting.sendPrintScreen = function() { 97 if (remoting.clientSession) { 98 console.log('Sending Print Screen.'); 99 remoting.clientSession.sendPrintScreen(); 100 } 101 }; 102 103 /** 104 * Callback function called when the state of the client plugin changes. The 105 * current and previous states are available via the |state| member variable. 106 * 107 * @param {remoting.ClientSession.StateEvent} state 108 */ 109 function onClientStateChange_(state) { 110 switch (state.current) { 111 case remoting.ClientSession.State.CLOSED: 112 console.log('Connection closed by host'); 113 if (remoting.clientSession.getMode() == 114 remoting.ClientSession.Mode.IT2ME) { 115 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME); 116 } else { 117 remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME); 118 } 119 break; 120 121 case remoting.ClientSession.State.FAILED: 122 var error = remoting.clientSession.getError(); 123 console.error('Client plugin reported connection failed: ' + error); 124 if (error == null) { 125 error = remoting.Error.UNEXPECTED; 126 } 127 showConnectError_(error); 128 break; 129 130 default: 131 console.error('Unexpected client plugin state: ' + state.current); 132 // This should only happen if the web-app and client plugin get out of 133 // sync, so MISSING_PLUGIN is a suitable error. 134 showConnectError_(remoting.Error.MISSING_PLUGIN); 135 break; 136 } 137 138 remoting.clientSession.removeEventListener('stateChanged', 139 onClientStateChange_); 140 remoting.clientSession.cleanup(); 141 remoting.clientSession = null; 142 } 143 144 /** 145 * Show a client-side error message. 146 * 147 * @param {remoting.Error} errorTag The error to be localized and 148 * displayed. 149 * @return {void} Nothing. 150 */ 151 function showConnectError_(errorTag) { 152 console.error('Connection failed: ' + errorTag); 153 var errorDiv = document.getElementById('connect-error-message'); 154 l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag)); 155 remoting.accessCode = ''; 156 var mode = remoting.clientSession ? remoting.clientSession.getMode() 157 : remoting.connector.getConnectionMode(); 158 if (mode == remoting.ClientSession.Mode.IT2ME) { 159 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME); 160 } else { 161 remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME); 162 } 163 } 164 165 /** 166 * Set the text on the buttons shown under the error message so that they are 167 * easy to understand in the case where a successful connection failed, as 168 * opposed to the case where a connection never succeeded. 169 */ 170 function setConnectionInterruptedButtonsText_() { 171 var button1 = document.getElementById('client-reconnect-button'); 172 l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT'); 173 button1.removeAttribute('autofocus'); 174 var button2 = document.getElementById('client-finished-me2me-button'); 175 l10n.localizeElementFromTag(button2, /*i18n-content*/'OK'); 176 button2.setAttribute('autofocus', 'autofocus'); 177 } 178 179 /** 180 * Timer callback to update the statistics panel. 181 */ 182 function updateStatistics_() { 183 if (!remoting.clientSession || 184 remoting.clientSession.getState() != 185 remoting.ClientSession.State.CONNECTED) { 186 return; 187 } 188 var perfstats = remoting.clientSession.getPerfStats(); 189 remoting.stats.update(perfstats); 190 remoting.clientSession.logStatistics(perfstats); 191 // Update the stats once per second. 192 window.setTimeout(updateStatistics_, 1000); 193 } 194 195 /** 196 * Entry-point for Me2Me connections, handling showing of the host-upgrade nag 197 * dialog if necessary. 198 * 199 * @param {string} hostId The unique id of the host. 200 * @return {void} Nothing. 201 */ 202 remoting.connectMe2Me = function(hostId) { 203 var host = remoting.hostList.getHostForId(hostId); 204 if (!host) { 205 showConnectError_(remoting.Error.HOST_IS_OFFLINE); 206 return; 207 } 208 var webappVersion = chrome.runtime.getManifest().version; 209 if (remoting.Host.needsUpdate(host, webappVersion)) { 210 var needsUpdateMessage = 211 document.getElementById('host-needs-update-message'); 212 l10n.localizeElementFromTag(needsUpdateMessage, 213 /*i18n-content*/'HOST_NEEDS_UPDATE_TITLE', 214 host.hostName); 215 /** @type {Element} */ 216 var connect = document.getElementById('host-needs-update-connect-button'); 217 /** @type {Element} */ 218 var cancel = document.getElementById('host-needs-update-cancel-button'); 219 /** @param {Event} event */ 220 var onClick = function(event) { 221 connect.removeEventListener('click', onClick, false); 222 cancel.removeEventListener('click', onClick, false); 223 if (event.target == connect) { 224 remoting.connectMe2MeHostVersionAcknowledged_(host); 225 } else { 226 remoting.setMode(remoting.AppMode.HOME); 227 } 228 } 229 connect.addEventListener('click', onClick, false); 230 cancel.addEventListener('click', onClick, false); 231 remoting.setMode(remoting.AppMode.CLIENT_HOST_NEEDS_UPGRADE); 232 } else { 233 remoting.connectMe2MeHostVersionAcknowledged_(host); 234 } 235 }; 236 237 /** 238 * Shows PIN entry screen localized to include the host name, and registers 239 * a host-specific one-shot event handler for the form submission. 240 * 241 * @param {remoting.Host} host The Me2Me host to which to connect. 242 * @return {void} Nothing. 243 */ 244 remoting.connectMe2MeHostVersionAcknowledged_ = function(host) { 245 remoting.ensureSessionConnector_(); 246 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); 247 248 /** 249 * @param {string} tokenUrl Token-issue URL received from the host. 250 * @param {string} scope OAuth scope to request the token for. 251 * @param {string} hostPublicKey Host public key (DER and Base64 encoded). 252 * @param {function(string, string):void} onThirdPartyTokenFetched Callback. 253 */ 254 var fetchThirdPartyToken = function( 255 tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) { 256 var thirdPartyTokenFetcher = new remoting.ThirdPartyTokenFetcher( 257 tokenUrl, hostPublicKey, scope, host.tokenUrlPatterns, 258 onThirdPartyTokenFetched); 259 thirdPartyTokenFetcher.fetchToken(); 260 }; 261 262 /** 263 * @param {boolean} supportsPairing 264 * @param {function(string):void} onPinFetched 265 */ 266 var requestPin = function(supportsPairing, onPinFetched) { 267 /** @type {Element} */ 268 var pinForm = document.getElementById('pin-form'); 269 /** @type {Element} */ 270 var pinCancel = document.getElementById('cancel-pin-entry-button'); 271 /** @type {Element} */ 272 var rememberPin = document.getElementById('remember-pin'); 273 /** @type {Element} */ 274 var rememberPinCheckbox = document.getElementById('remember-pin-checkbox'); 275 /** 276 * Event handler for both the 'submit' and 'cancel' actions. Using 277 * a single handler for both greatly simplifies the task of making 278 * them one-shot. If separate handlers were used, each would have 279 * to unregister both itself and the other. 280 * 281 * @param {Event} event The click or submit event. 282 */ 283 var onSubmitOrCancel = function(event) { 284 pinForm.removeEventListener('submit', onSubmitOrCancel, false); 285 pinCancel.removeEventListener('click', onSubmitOrCancel, false); 286 var pinField = document.getElementById('pin-entry'); 287 var pin = pinField.value; 288 pinField.value = ''; 289 if (event.target == pinForm) { 290 event.preventDefault(); 291 292 // Set the focus away from the password field. This has to be done 293 // before the password field gets hidden, to work around a Blink 294 // clipboard-handling bug - http://crbug.com/281523. 295 document.getElementById('pin-connect-button').focus(); 296 297 remoting.setMode(remoting.AppMode.CLIENT_CONNECTING); 298 onPinFetched(pin); 299 if (/** @type {boolean} */(rememberPinCheckbox.checked)) { 300 remoting.connector.pairingRequested = true; 301 } 302 } else { 303 remoting.setMode(remoting.AppMode.HOME); 304 } 305 }; 306 pinForm.addEventListener('submit', onSubmitOrCancel, false); 307 pinCancel.addEventListener('click', onSubmitOrCancel, false); 308 rememberPin.hidden = !supportsPairing; 309 rememberPinCheckbox.checked = false; 310 var message = document.getElementById('pin-message'); 311 l10n.localizeElement(message, host.hostName); 312 remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT); 313 }; 314 315 /** @param {Object} settings */ 316 var connectMe2MeHostSettingsRetrieved = function(settings) { 317 /** @type {string} */ 318 var clientId = ''; 319 /** @type {string} */ 320 var sharedSecret = ''; 321 var pairingInfo = /** @type {Object} */ (settings['pairingInfo']); 322 if (pairingInfo) { 323 clientId = /** @type {string} */ (pairingInfo['clientId']); 324 sharedSecret = /** @type {string} */ (pairingInfo['sharedSecret']); 325 } 326 remoting.connector.connectMe2Me(host, requestPin, fetchThirdPartyToken, 327 clientId, sharedSecret); 328 } 329 330 remoting.HostSettings.load(host.hostId, connectMe2MeHostSettingsRetrieved); 331 }; 332 333 /** @param {remoting.ClientSession} clientSession */ 334 remoting.onConnected = function(clientSession) { 335 remoting.clientSession = clientSession; 336 remoting.clientSession.addEventListener('stateChanged', onClientStateChange_); 337 setConnectionInterruptedButtonsText_(); 338 var connectedTo = document.getElementById('connected-to'); 339 connectedTo.innerText = remoting.connector.getHostDisplayName(); 340 document.getElementById('access-code-entry').value = ''; 341 remoting.setMode(remoting.AppMode.IN_SESSION); 342 remoting.toolbar.center(); 343 remoting.toolbar.preview(); 344 remoting.clipboard.startSession(); 345 updateStatistics_(); 346 if (remoting.connector.pairingRequested) { 347 /** 348 * @param {string} clientId 349 * @param {string} sharedSecret 350 */ 351 var onPairingComplete = function(clientId, sharedSecret) { 352 var pairingInfo = { 353 pairingInfo: { 354 clientId: clientId, 355 sharedSecret: sharedSecret 356 } 357 }; 358 remoting.HostSettings.save(remoting.connector.getHostId(), pairingInfo); 359 remoting.connector.updatePairingInfo(clientId, sharedSecret); 360 }; 361 // Use the platform name as a proxy for the local computer name. 362 // TODO(jamiewalch): Use a descriptive name for the local computer, for 363 // example, its Chrome Sync name. 364 var clientName = ''; 365 if (navigator.platform.indexOf('Mac') != -1) { 366 clientName = 'Mac'; 367 } else if (navigator.platform.indexOf('Win32') != -1) { 368 clientName = 'Windows'; 369 } else if (navigator.userAgent.match(/\bCrOS\b/)) { 370 clientName = 'ChromeOS'; 371 } else if (navigator.platform.indexOf('Linux') != -1) { 372 clientName = 'Linux'; 373 } else { 374 console.log('Unrecognized client platform. Using navigator.platform.'); 375 clientName = navigator.platform; 376 } 377 clientSession.requestPairing(clientName, onPairingComplete); 378 } 379 }; 380 381 /** 382 * Extension message handler. 383 * 384 * @param {string} type The type of the extension message. 385 * @param {string} data The payload of the extension message. 386 * @return {boolean} Return true if the extension message was recognized. 387 */ 388 remoting.onExtensionMessage = function(type, data) { 389 return false; 390 }; 391 392 /** 393 * Create a session connector if one doesn't already exist. 394 */ 395 remoting.ensureSessionConnector_ = function() { 396 if (!remoting.connector) { 397 remoting.connector = new remoting.SessionConnector( 398 document.getElementById('client-plugin-container'), 399 remoting.onConnected, 400 showConnectError_, remoting.onExtensionMessage); 401 } 402 }; 403