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