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