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