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 'host screen' for Chromoting. 8 */ 9 10'use strict'; 11 12/** @suppress {duplicate} */ 13var remoting = remoting || {}; 14 15/** 16 * @type {boolean} Whether or not the last share was cancelled by the user. 17 * This controls what screen is shown when the host signals completion. 18 * @private 19 */ 20var lastShareWasCancelled_ = false; 21 22/** 23 * Start a host session. This is the main entry point for the host screen, 24 * called directly from the onclick action of a button on the home screen. 25 * It first verifies that the native host components are installed and asks 26 * to install them if necessary. 27 */ 28remoting.tryShare = function() { 29 /** @type {remoting.It2MeHostFacade} */ 30 var hostFacade = new remoting.It2MeHostFacade(); 31 32 /** @type {remoting.HostInstallDialog} */ 33 var hostInstallDialog = null; 34 35 var tryInitializeFacade = function() { 36 hostFacade.initialize(onFacadeInitialized, onFacadeInitializationFailed); 37 } 38 39 var onFacadeInitialized = function () { 40 // Host already installed. 41 remoting.startHostUsingFacade_(hostFacade); 42 }; 43 44 var onFacadeInitializationFailed = function() { 45 // If we failed to initialize the dispatcher then prompt the user to install 46 // the host manually. 47 var hasHostDialog = (hostInstallDialog != null); /** jscompile hack */ 48 if (!hasHostDialog) { 49 hostInstallDialog = new remoting.HostInstallDialog(); 50 hostInstallDialog.show(tryInitializeFacade, onInstallError); 51 } else { 52 hostInstallDialog.tryAgain(); 53 } 54 }; 55 56 /** @param {remoting.Error} error */ 57 var onInstallError = function(error) { 58 if (error == remoting.Error.CANCELLED) { 59 remoting.setMode(remoting.AppMode.HOME); 60 } else { 61 showShareError_(error); 62 } 63 } 64 65 tryInitializeFacade(); 66}; 67 68/** 69 * @param {remoting.It2MeHostFacade} hostFacade An initialized It2MeHostFacade. 70 */ 71remoting.startHostUsingFacade_ = function(hostFacade) { 72 console.log('Attempting to share...'); 73 remoting.identity.callWithToken( 74 remoting.tryShareWithToken_.bind(null, hostFacade), 75 remoting.showErrorMessage); 76} 77 78/** 79 * @param {remoting.It2MeHostFacade} hostFacade An initialized 80 * It2MeHostFacade. 81 * @param {string} token The OAuth access token. 82 * @private 83 */ 84remoting.tryShareWithToken_ = function(hostFacade, token) { 85 lastShareWasCancelled_ = false; 86 onNatTraversalPolicyChanged_(true); // Hide warning by default. 87 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE); 88 document.getElementById('cancel-share-button').disabled = false; 89 disableTimeoutCountdown_(); 90 91 remoting.hostSession = new remoting.HostSession(); 92 var email = /** @type {string} */remoting.identity.getCachedEmail(); 93 remoting.hostSession.connect( 94 hostFacade, email, token, onHostStateChanged_, 95 onNatTraversalPolicyChanged_, logDebugInfo_, it2meConnectFailed_); 96}; 97 98/** 99 * Callback for the host plugin to notify the web app of state changes. 100 * @param {remoting.HostSession.State} state The new state of the plugin. 101 * @return {void} Nothing. 102 * @private 103 */ 104function onHostStateChanged_(state) { 105 if (state == remoting.HostSession.State.STARTING) { 106 // Nothing to do here. 107 console.log('Host state: STARTING'); 108 109 } else if (state == remoting.HostSession.State.REQUESTED_ACCESS_CODE) { 110 // Nothing to do here. 111 console.log('Host state: REQUESTED_ACCESS_CODE'); 112 113 } else if (state == remoting.HostSession.State.RECEIVED_ACCESS_CODE) { 114 console.log('Host state: RECEIVED_ACCESS_CODE'); 115 var accessCode = remoting.hostSession.getAccessCode(); 116 var accessCodeDisplay = document.getElementById('access-code-display'); 117 accessCodeDisplay.innerText = ''; 118 // Display the access code in groups of four digits for readability. 119 var kDigitsPerGroup = 4; 120 for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) { 121 var nextFourDigits = document.createElement('span'); 122 nextFourDigits.className = 'access-code-digit-group'; 123 nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup); 124 accessCodeDisplay.appendChild(nextFourDigits); 125 } 126 accessCodeExpiresIn_ = remoting.hostSession.getAccessCodeLifetime(); 127 if (accessCodeExpiresIn_ > 0) { // Check it hasn't expired. 128 accessCodeTimerId_ = setInterval(decrementAccessCodeTimeout_, 1000); 129 timerRunning_ = true; 130 updateAccessCodeTimeoutElement_(); 131 updateTimeoutStyles_(); 132 remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION); 133 } else { 134 // This can only happen if the cloud tells us that the code lifetime is 135 // <= 0s, which shouldn't happen so we don't care how clean this UX is. 136 console.error('Access code already invalid on receipt!'); 137 remoting.cancelShare(); 138 } 139 140 } else if (state == remoting.HostSession.State.CONNECTED) { 141 console.log('Host state: CONNECTED'); 142 var element = document.getElementById('host-shared-message'); 143 var client = remoting.hostSession.getClient(); 144 l10n.localizeElement(element, client); 145 remoting.setMode(remoting.AppMode.HOST_SHARED); 146 disableTimeoutCountdown_(); 147 148 } else if (state == remoting.HostSession.State.DISCONNECTING) { 149 console.log('Host state: DISCONNECTING'); 150 151 } else if (state == remoting.HostSession.State.DISCONNECTED) { 152 console.log('Host state: DISCONNECTED'); 153 if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) { 154 // If an error is being displayed, then the plugin should not be able to 155 // hide it by setting the state. Errors must be dismissed by the user 156 // clicking OK, which puts the app into mode HOME. 157 if (lastShareWasCancelled_) { 158 remoting.setMode(remoting.AppMode.HOME); 159 } else { 160 remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED); 161 } 162 } 163 } else if (state == remoting.HostSession.State.ERROR) { 164 console.error('Host state: ERROR'); 165 showShareError_(remoting.Error.UNEXPECTED); 166 } else if (state == remoting.HostSession.State.INVALID_DOMAIN_ERROR) { 167 console.error('Host state: INVALID_DOMAIN_ERROR'); 168 showShareError_(remoting.Error.INVALID_HOST_DOMAIN); 169 } else { 170 console.error('Unknown state -> ' + state); 171 } 172} 173 174/** 175 * This is the callback that the host plugin invokes to indicate that there 176 * is additional debug log info to display. 177 * @param {string} msg The message (which will not be localized) to be logged. 178 * @private 179 */ 180function logDebugInfo_(msg) { 181 console.log('plugin: ' + msg); 182} 183 184/** 185 * Show a host-side error message. 186 * 187 * @param {string} errorTag The error message to be localized and displayed. 188 * @return {void} Nothing. 189 * @private 190 */ 191function showShareError_(errorTag) { 192 var errorDiv = document.getElementById('host-plugin-error'); 193 l10n.localizeElementFromTag(errorDiv, errorTag); 194 console.error('Sharing error: ' + errorTag); 195 remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED); 196} 197 198/** 199 * Show a sharing error with error code UNEXPECTED . 200 * 201 * @return {void} Nothing. 202 * @private 203 */ 204function it2meConnectFailed_() { 205 // TODO (weitaosu): Instruct the user to install the native messaging host. 206 // We probably want to add a new error code (with the corresponding error 207 // message for sharing error. 208 console.error('Cannot share desktop.'); 209 showShareError_(remoting.Error.UNEXPECTED); 210} 211 212/** 213 * Cancel an active or pending it2me share operation. 214 * 215 * @return {void} Nothing. 216 */ 217remoting.cancelShare = function() { 218 document.getElementById('cancel-share-button').disabled = true; 219 console.log('Canceling share...'); 220 remoting.lastShareWasCancelled = true; 221 try { 222 remoting.hostSession.disconnect(); 223 } catch (error) { 224 // Hack to force JSCompiler type-safety. 225 var errorTyped = /** @type {{description: string}} */ error; 226 console.error('Error disconnecting: ' + errorTyped.description + 227 '. The host probably crashed.'); 228 // TODO(jamiewalch): Clean this up. We should have a class representing 229 // the host plugin, like we do for the client, which should handle crash 230 // reporting and it should use a more detailed error message than the 231 // default 'generic' one. See crbug.com/94624 232 showShareError_(remoting.Error.UNEXPECTED); 233 } 234 disableTimeoutCountdown_(); 235}; 236 237/** 238 * @type {boolean} Whether or not the access code timeout countdown is running. 239 * @private 240 */ 241var timerRunning_ = false; 242 243/** 244 * @type {number} The id of the access code expiry countdown timer. 245 * @private 246 */ 247var accessCodeTimerId_ = 0; 248 249/** 250 * @type {number} The number of seconds until the access code expires. 251 * @private 252 */ 253var accessCodeExpiresIn_ = 0; 254 255/** 256 * The timer callback function 257 * @return {void} Nothing. 258 * @private 259 */ 260function decrementAccessCodeTimeout_() { 261 --accessCodeExpiresIn_; 262 updateAccessCodeTimeoutElement_(); 263}; 264 265/** 266 * Stop the access code timeout countdown if it is running. 267 * @return {void} Nothing. 268 * @private 269 */ 270function disableTimeoutCountdown_() { 271 if (timerRunning_) { 272 clearInterval(accessCodeTimerId_); 273 timerRunning_ = false; 274 updateTimeoutStyles_(); 275 } 276} 277 278/** 279 * Constants controlling the access code timer countdown display. 280 * @private 281 */ 282var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_ = 30; 283var ACCESS_CODE_RED_THRESHOLD_ = 10; 284 285/** 286 * Show/hide or restyle various elements, depending on the remaining countdown 287 * and timer state. 288 * 289 * @return {boolean} True if the timeout is in progress, false if it has 290 * expired. 291 * @private 292 */ 293function updateTimeoutStyles_() { 294 if (timerRunning_) { 295 if (accessCodeExpiresIn_ <= 0) { 296 remoting.cancelShare(); 297 return false; 298 } 299 var accessCode = document.getElementById('access-code-display'); 300 if (accessCodeExpiresIn_ <= ACCESS_CODE_RED_THRESHOLD_) { 301 accessCode.classList.add('expiring'); 302 } else { 303 accessCode.classList.remove('expiring'); 304 } 305 } 306 document.getElementById('access-code-countdown').hidden = 307 (accessCodeExpiresIn_ > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_) || 308 !timerRunning_; 309 return true; 310} 311 312/** 313 * Update the text and appearance of the access code timeout element to 314 * reflect the time remaining. 315 * @return {void} Nothing. 316 * @private 317 */ 318function updateAccessCodeTimeoutElement_() { 319 var pad = (accessCodeExpiresIn_ < 10) ? '0:0' : '0:'; 320 l10n.localizeElement(document.getElementById('seconds-remaining'), 321 pad + accessCodeExpiresIn_); 322 if (!updateTimeoutStyles_()) { 323 disableTimeoutCountdown_(); 324 } 325} 326 327/** 328 * Callback to show or hide the NAT traversal warning when the policy changes. 329 * @param {boolean} enabled True if NAT traversal is enabled. 330 * @return {void} Nothing. 331 * @private 332 */ 333function onNatTraversalPolicyChanged_(enabled) { 334 var natBox = document.getElementById('nat-box'); 335 if (enabled) { 336 natBox.classList.add('traversal-enabled'); 337 } else { 338 natBox.classList.remove('traversal-enabled'); 339 } 340} 341