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