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