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