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