client_screen.js revision 3551c9c881056c480085172ff9840cab31610854
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 connection
17 *     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  if (!remoting.connector) {
32    remoting.connector = new remoting.SessionConnector(
33        document.getElementById('session-mode'),
34        remoting.onConnected,
35        showConnectError_);
36  }
37  var accessCode = document.getElementById('access-code-entry').value;
38  remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
39  remoting.connector.connectIT2Me(accessCode);
40};
41
42/**
43 * Update the remoting client layout in response to a resize event.
44 *
45 * @return {void} Nothing.
46 */
47remoting.onResize = function() {
48  if (remoting.clientSession) {
49    remoting.clientSession.onResize();
50  }
51};
52
53/**
54 * Handle changes in the visibility of the window, for example by pausing video.
55 *
56 * @return {void} Nothing.
57 */
58remoting.onVisibilityChanged = function() {
59  if (remoting.clientSession) {
60    remoting.clientSession.pauseVideo(document.webkitHidden);
61  }
62}
63
64/**
65 * Disconnect the remoting client.
66 *
67 * @return {void} Nothing.
68 */
69remoting.disconnect = function() {
70  if (!remoting.clientSession) {
71    return;
72  }
73  if (remoting.clientSession.getMode() == remoting.ClientSession.Mode.IT2ME) {
74    remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
75  } else {
76    remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
77  }
78  remoting.clientSession.disconnect(true);
79  remoting.clientSession = null;
80  console.log('Disconnected.');
81};
82
83/**
84 * Sends a Ctrl-Alt-Del sequence to the remoting client.
85 *
86 * @return {void} Nothing.
87 */
88remoting.sendCtrlAltDel = function() {
89  if (remoting.clientSession) {
90    console.log('Sending Ctrl-Alt-Del.');
91    remoting.clientSession.sendCtrlAltDel();
92  }
93};
94
95/**
96 * Sends a Print Screen keypress to the remoting client.
97 *
98 * @return {void} Nothing.
99 */
100remoting.sendPrintScreen = function() {
101  if (remoting.clientSession) {
102    console.log('Sending Print Screen.');
103    remoting.clientSession.sendPrintScreen();
104  }
105};
106
107/**
108 * Callback function called when the state of the client plugin changes. The
109 * current state is available via the |state| member variable.
110 *
111 * @param {number} oldState The previous state of the plugin.
112 * @param {number} newState The current state of the plugin.
113 */
114function onClientStateChange_(oldState, newState) {
115  switch (newState) {
116    case remoting.ClientSession.State.CLOSED:
117      console.log('Connection closed by host');
118      if (remoting.clientSession.getMode() ==
119          remoting.ClientSession.Mode.IT2ME) {
120        remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
121      } else {
122        remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
123      }
124      break;
125
126    case remoting.ClientSession.State.FAILED:
127      var error = remoting.clientSession.getError();
128      console.error('Client plugin reported connection failed: ' + error);
129      if (error == null) {
130        error = remoting.Error.UNEXPECTED;
131      }
132      showConnectError_(error);
133      break;
134
135    default:
136      console.error('Unexpected client plugin state: ' + newState);
137      // This should only happen if the web-app and client plugin get out of
138      // sync, so MISSING_PLUGIN is a suitable error.
139      showConnectError_(remoting.Error.MISSING_PLUGIN);
140      break;
141  }
142  remoting.clientSession.disconnect(false);
143  remoting.clientSession.removePlugin();
144  remoting.clientSession = null;
145}
146
147/**
148 * Show a client-side error message.
149 *
150 * @param {remoting.Error} errorTag The error to be localized and
151 *     displayed.
152 * @return {void} Nothing.
153 */
154function showConnectError_(errorTag) {
155  console.error('Connection failed: ' + errorTag);
156  var errorDiv = document.getElementById('connect-error-message');
157  l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag));
158  remoting.accessCode = '';
159  var mode = remoting.clientSession ? remoting.clientSession.getMode()
160                                    : remoting.connector.getConnectionMode();
161  if (mode == remoting.ClientSession.Mode.IT2ME) {
162    remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
163  } else {
164    remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
165  }
166}
167
168/**
169 * Set the text on the buttons shown under the error message so that they are
170 * easy to understand in the case where a successful connection failed, as
171 * opposed to the case where a connection never succeeded.
172 */
173function setConnectionInterruptedButtonsText_() {
174  var button1 = document.getElementById('client-reconnect-button');
175  l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT');
176  button1.removeAttribute('autofocus');
177  var button2 = document.getElementById('client-finished-me2me-button');
178  l10n.localizeElementFromTag(button2, /*i18n-content*/'OK');
179  button2.setAttribute('autofocus', 'autofocus');
180}
181
182/**
183 * Timer callback to update the statistics panel.
184 */
185function updateStatistics_() {
186  if (!remoting.clientSession ||
187      remoting.clientSession.getState() !=
188      remoting.ClientSession.State.CONNECTED) {
189    return;
190  }
191  var perfstats = remoting.clientSession.getPerfStats();
192  remoting.stats.update(perfstats);
193  remoting.clientSession.logStatistics(perfstats);
194  // Update the stats once per second.
195  window.setTimeout(updateStatistics_, 1000);
196}
197
198/**
199 * Entry-point for Me2Me connections, handling showing of the host-upgrade nag
200 * dialog if necessary.
201 *
202 * @param {string} hostId The unique id of the host.
203 * @return {void} Nothing.
204 */
205remoting.connectMe2Me = function(hostId) {
206  var host = remoting.hostList.getHostForId(hostId);
207  if (!host) {
208    showConnectError_(remoting.Error.HOST_IS_OFFLINE);
209    return;
210  }
211  var webappVersion = chrome.runtime.getManifest().version;
212  if (remoting.Host.needsUpdate(host, webappVersion)) {
213    var needsUpdateMessage =
214        document.getElementById('host-needs-update-message');
215    l10n.localizeElementFromTag(needsUpdateMessage,
216                                /*i18n-content*/'HOST_NEEDS_UPDATE_TITLE',
217                                host.hostName);
218    /** @type {Element} */
219    var connect = document.getElementById('host-needs-update-connect-button');
220    /** @type {Element} */
221    var cancel = document.getElementById('host-needs-update-cancel-button');
222    /** @param {Event} event */
223    var onClick = function(event) {
224      connect.removeEventListener('click', onClick, false);
225      cancel.removeEventListener('click', onClick, false);
226      if (event.target == connect) {
227        remoting.connectMe2MeHostVersionAcknowledged_(host);
228      } else {
229        remoting.setMode(remoting.AppMode.HOME);
230      }
231    }
232    connect.addEventListener('click', onClick, false);
233    cancel.addEventListener('click', onClick, false);
234    remoting.setMode(remoting.AppMode.CLIENT_HOST_NEEDS_UPGRADE);
235  } else {
236    remoting.connectMe2MeHostVersionAcknowledged_(host);
237  }
238};
239
240/**
241 * Shows PIN entry screen localized to include the host name, and registers
242 * a host-specific one-shot event handler for the form submission.
243 *
244 * @param {remoting.Host} host The Me2Me host to which to connect.
245 * @return {void} Nothing.
246 */
247remoting.connectMe2MeHostVersionAcknowledged_ = function(host) {
248  if (!remoting.connector) {
249    remoting.connector = new remoting.SessionConnector(
250        document.getElementById('session-mode'),
251        remoting.onConnected,
252        showConnectError_);
253  }
254  remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
255
256  /**
257   * @param {string} tokenUrl Token-issue URL received from the host.
258   * @param {string} scope OAuth scope to request the token for.
259   * @param {string} hostPublicKey Host public key (DER and Base64 encoded).
260   * @param {function(string, string):void} onThirdPartyTokenFetched Callback.
261   */
262  var fetchThirdPartyToken = function(
263      tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) {
264    var thirdPartyTokenFetcher = new remoting.ThirdPartyTokenFetcher(
265        tokenUrl, hostPublicKey, scope, host.tokenUrlPatterns,
266        onThirdPartyTokenFetched);
267    thirdPartyTokenFetcher.fetchToken();
268  };
269
270  /**
271   * @param {boolean} supportsPairing
272   * @param {function(string):void} onPinFetched
273   */
274  var requestPin = function(supportsPairing, onPinFetched) {
275    /** @type {Element} */
276    var pinForm = document.getElementById('pin-form');
277    /** @type {Element} */
278    var pinCancel = document.getElementById('cancel-pin-entry-button');
279    /** @type {Element} */
280    var rememberPin = document.getElementById('remember-pin');
281    /** @type {Element} */
282    var rememberPinCheckbox = document.getElementById('remember-pin-checkbox');
283    /**
284     * Event handler for both the 'submit' and 'cancel' actions. Using
285     * a single handler for both greatly simplifies the task of making
286     * them one-shot. If separate handlers were used, each would have
287     * to unregister both itself and the other.
288     *
289     * @param {Event} event The click or submit event.
290     */
291    var onSubmitOrCancel = function(event) {
292      pinForm.removeEventListener('submit', onSubmitOrCancel, false);
293      pinCancel.removeEventListener('click', onSubmitOrCancel, false);
294      var pinField = document.getElementById('pin-entry');
295      var pin = pinField.value;
296      pinField.value = '';
297      if (event.target == pinForm) {
298        event.preventDefault();
299        remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
300        onPinFetched(pin);
301        if (/** @type {boolean} */(rememberPinCheckbox.checked)) {
302          remoting.connector.pairingRequested = true;
303        }
304      } else {
305        remoting.setMode(remoting.AppMode.HOME);
306      }
307    };
308    pinForm.addEventListener('submit', onSubmitOrCancel, false);
309    pinCancel.addEventListener('click', onSubmitOrCancel, false);
310    rememberPin.hidden = !supportsPairing;
311    rememberPinCheckbox.checked = false;
312    var message = document.getElementById('pin-message');
313    l10n.localizeElement(message, host.hostName);
314    remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT);
315  };
316
317  /** @param {Object} settings */
318  var connectMe2MeHostSettingsRetrieved = function(settings) {
319    /** @type {string} */
320    var clientId = '';
321    /** @type {string} */
322    var sharedSecret = '';
323    var pairingInfo = /** @type {Object} */ (settings['pairingInfo']);
324    if (pairingInfo) {
325      clientId = /** @type {string} */ (pairingInfo['clientId']);
326      sharedSecret = /** @type {string} */ (pairingInfo['sharedSecret']);
327    }
328    remoting.connector.connectMe2Me(host, requestPin, fetchThirdPartyToken,
329                                    clientId, sharedSecret);
330  }
331
332  remoting.HostSettings.load(host.hostId, connectMe2MeHostSettingsRetrieved);
333};
334
335/** @param {remoting.ClientSession} clientSession */
336remoting.onConnected = function(clientSession) {
337  remoting.clientSession = clientSession;
338  remoting.clientSession.setOnStateChange(onClientStateChange_);
339  setConnectionInterruptedButtonsText_();
340  var connectedTo = document.getElementById('connected-to');
341  connectedTo.innerText = remoting.connector.getHostDisplayName();
342  document.getElementById('access-code-entry').value = '';
343  remoting.setMode(remoting.AppMode.IN_SESSION);
344  remoting.toolbar.center();
345  remoting.toolbar.preview();
346  remoting.clipboard.startSession();
347  updateStatistics_();
348  if (remoting.connector.pairingRequested) {
349    /**
350     * @param {string} clientId
351     * @param {string} sharedSecret
352     */
353    var onPairingComplete = function(clientId, sharedSecret) {
354      var pairingInfo = {
355        pairingInfo: {
356          clientId: clientId,
357          sharedSecret: sharedSecret
358        }
359      };
360      remoting.HostSettings.save(remoting.connector.getHostId(), pairingInfo);
361      remoting.connector.updatePairingInfo(clientId, sharedSecret);
362    };
363    // Use the platform name as a proxy for the local computer name.
364    // TODO(jamiewalch): Use a descriptive name for the local computer, for
365    // example, its Chrome Sync name.
366    var clientName = '';
367    if (navigator.platform.indexOf('Mac') != -1) {
368      clientName = 'Mac';
369    } else if (navigator.platform.indexOf('Win32') != -1) {
370      clientName = 'Windows';
371    } else if (navigator.userAgent.match(/\bCrOS\b/)) {
372      clientName = 'ChromeOS';
373    } else if (navigator.platform.indexOf('Linux') != -1) {
374      clientName = 'Linux';
375    } else {
376      console.log('Unrecognized client platform. Using navigator.platform.');
377      clientName = navigator.platform;
378    }
379    clientSession.requestPairing(clientName, onPairingComplete);
380  }
381};
382