• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 * Class representing the host-list portion of the home screen UI.
8 */
9
10'use strict';
11
12/** @suppress {duplicate} */
13var remoting = remoting || {};
14
15/**
16 * Create a host list consisting of the specified HTML elements, which should
17 * have a common parent that contains only host-list UI as it will be hidden
18 * if the host-list is empty.
19 *
20 * @constructor
21 * @param {Element} table The HTML <div> to contain host-list.
22 * @param {Element} noHosts The HTML <div> containing the "no hosts" message.
23 * @param {Element} errorMsg The HTML <div> to display error messages.
24 * @param {Element} errorButton The HTML <button> to display the error
25 *     resolution action.
26 * @param {HTMLElement} loadingIndicator The HTML <span> to update while the
27 *     host list is being loaded. The first element of this span should be
28 *     the reload button.
29 */
30remoting.HostList = function(table, noHosts, errorMsg, errorButton,
31                             loadingIndicator) {
32  /**
33   * @type {Element}
34   * @private
35   */
36  this.table_ = table;
37  /**
38   * @type {Element}
39   * @private
40   * TODO(jamiewalch): This should be doable using CSS's sibling selector,
41   * but it doesn't work right now (crbug.com/135050).
42   */
43  this.noHosts_ = noHosts;
44  /**
45   * @type {Element}
46   * @private
47   */
48  this.errorMsg_ = errorMsg;
49  /**
50   * @type {Element}
51   * @private
52   */
53  this.errorButton_ = errorButton;
54  /**
55   * @type {HTMLElement}
56   * @private
57   */
58  this.loadingIndicator_ = loadingIndicator;
59  /**
60   * @type {Array.<remoting.HostTableEntry>}
61   * @private
62   */
63  this.hostTableEntries_ = [];
64  /**
65   * @type {Array.<remoting.Host>}
66   * @private
67   */
68  this.hosts_ = [];
69  /**
70   * @type {string}
71   * @private
72   */
73  this.lastError_ = '';
74  /**
75   * @type {remoting.Host?}
76   * @private
77   */
78  this.localHost_ = null;
79  /**
80   * @type {remoting.HostController.State}
81   * @private
82   */
83  this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
84  /**
85   * @type {number}
86   * @private
87   */
88  this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
89
90  this.errorButton_.addEventListener('click',
91                                     this.onErrorClick_.bind(this),
92                                     false);
93  var reloadButton = this.loadingIndicator_.firstElementChild;
94  /** @type {remoting.HostList} */
95  var that = this;
96  /** @param {Event} event */
97  function refresh(event) {
98    event.preventDefault();
99    that.refresh(that.display.bind(that));
100  };
101  reloadButton.addEventListener('click', refresh, false);
102};
103
104/**
105 * Load the host-list asynchronously from local storage.
106 *
107 * @param {function():void} onDone Completion callback.
108 */
109remoting.HostList.prototype.load = function(onDone) {
110  // Load the cache of the last host-list, if present.
111  /** @type {remoting.HostList} */
112  var that = this;
113  /** @param {Object.<string>} items */
114  var storeHostList = function(items) {
115    if (items[remoting.HostList.HOSTS_KEY]) {
116      var cached = jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
117      if (cached) {
118        that.hosts_ = /** @type {Array} */ cached;
119      } else {
120        console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
121      }
122    }
123    onDone();
124  };
125  chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
126};
127
128/**
129 * Search the host list for a host with the specified id.
130 *
131 * @param {string} hostId The unique id of the host.
132 * @return {remoting.Host?} The host, if any.
133 */
134remoting.HostList.prototype.getHostForId = function(hostId) {
135  for (var i = 0; i < this.hosts_.length; ++i) {
136    if (this.hosts_[i].hostId == hostId) {
137      return this.hosts_[i];
138    }
139  }
140  return null;
141};
142
143/**
144 * Get the host id corresponding to the specified host name.
145 *
146 * @param {string} hostName The name of the host.
147 * @return {string?} The host id, if a host with the given name exists.
148 */
149remoting.HostList.prototype.getHostIdForName = function(hostName) {
150  for (var i = 0; i < this.hosts_.length; ++i) {
151    if (this.hosts_[i].hostName == hostName) {
152      return this.hosts_[i].hostId;
153    }
154  }
155  return null;
156};
157
158/**
159 * Query the Remoting Directory for the user's list of hosts.
160 *
161 * @param {function(boolean):void} onDone Callback invoked with true on success
162 *     or false on failure.
163 * @return {void} Nothing.
164 */
165remoting.HostList.prototype.refresh = function(onDone) {
166  this.loadingIndicator_.classList.add('loading');
167  /** @param {XMLHttpRequest} xhr The response from the server. */
168  var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone);
169  /** @type {remoting.HostList} */
170  var that = this;
171  /** @param {string} token The OAuth2 token. */
172  var getHosts = function(token) {
173    var headers = { 'Authorization': 'OAuth ' + token };
174    remoting.xhr.get(
175        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
176        parseHostListResponse, '', headers);
177  };
178  /** @param {remoting.Error} error */
179  var onError = function(error) {
180    that.lastError_ = error;
181    onDone(false);
182  };
183  remoting.identity.callWithToken(getHosts, onError);
184};
185
186/**
187 * Handle the results of the host list request.  A success response will
188 * include a JSON-encoded list of host descriptions, which we display if we're
189 * able to successfully parse it.
190 *
191 * @param {function(boolean):void} onDone The callback passed to |refresh|.
192 * @param {XMLHttpRequest} xhr The XHR object for the host list request.
193 * @return {void} Nothing.
194 * @private
195 */
196remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
197  this.lastError_ = '';
198  try {
199    if (xhr.status == 200) {
200      var response =
201          /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
202      if (response && response.data) {
203        if (response.data.items) {
204          this.hosts_ = response.data.items;
205          /**
206           * @param {remoting.Host} a
207           * @param {remoting.Host} b
208           */
209          var cmp = function(a, b) {
210            if (a.status < b.status) {
211              return 1;
212            } else if (b.status < a.status) {
213              return -1;
214            }
215            return 0;
216          };
217          this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
218        } else {
219          this.hosts_ = [];
220        }
221      } else {
222        this.lastError_ = remoting.Error.UNEXPECTED;
223        console.error('Invalid "hosts" response from server.');
224      }
225    } else {
226      // Some other error.
227      console.error('Bad status on host list query: ', xhr);
228      if (xhr.status == 0) {
229        this.lastError_ = remoting.Error.NETWORK_FAILURE;
230      } else if (xhr.status == 401) {
231        this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
232      } else if (xhr.status == 502 || xhr.status == 503) {
233        this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
234      } else {
235        this.lastError_ = remoting.Error.UNEXPECTED;
236      }
237    }
238  } catch (er) {
239    var typed_er = /** @type {Object} */ (er);
240    console.error('Error processing response: ', xhr, typed_er);
241    this.lastError_ = remoting.Error.UNEXPECTED;
242  }
243  this.save_();
244  this.loadingIndicator_.classList.remove('loading');
245  onDone(this.lastError_ == '');
246};
247
248/**
249 * Display the list of hosts or error condition.
250 *
251 * @return {void} Nothing.
252 */
253remoting.HostList.prototype.display = function() {
254  this.table_.innerText = '';
255  this.errorMsg_.innerText = '';
256  this.hostTableEntries_ = [];
257
258  var noHostsRegistered = (this.hosts_.length == 0);
259  this.table_.hidden = noHostsRegistered;
260  this.noHosts_.hidden = !noHostsRegistered;
261
262  if (this.lastError_ != '') {
263    l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
264    if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
265      l10n.localizeElementFromTag(this.errorButton_,
266                                  /*i18n-content*/'SIGN_IN_BUTTON');
267    } else {
268      l10n.localizeElementFromTag(this.errorButton_,
269                                  /*i18n-content*/'RETRY');
270    }
271  } else {
272    for (var i = 0; i < this.hosts_.length; ++i) {
273      /** @type {remoting.Host} */
274      var host = this.hosts_[i];
275      // Validate the entry to make sure it has all the fields we expect and is
276      // not the local host (which is displayed separately). NB: if the host has
277      // never sent a heartbeat, then there will be no jabberId.
278      if (host.hostName && host.hostId && host.status && host.publicKey &&
279          (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
280        var hostTableEntry = new remoting.HostTableEntry(
281            host, this.webappMajorVersion_,
282            this.renameHost_.bind(this), this.deleteHost_.bind(this));
283        hostTableEntry.createDom();
284        this.hostTableEntries_[i] = hostTableEntry;
285        this.table_.appendChild(hostTableEntry.tableRow);
286      }
287    }
288  }
289
290  this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
291
292  // The local host cannot be stopped or started if the host controller is not
293  // implemented for this platform. Additionally, it cannot be started if there
294  // is an error (in many error states, the start operation will fail anyway,
295  // but even if it succeeds, the chance of a related but hard-to-diagnose
296  // future error is high).
297  var state = this.localHostState_;
298  var enabled = (state == remoting.HostController.State.STARTING) ||
299      (state == remoting.HostController.State.STARTED);
300  var canChangeLocalHostState =
301      (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
302      (enabled || this.lastError_ == '');
303
304  remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
305  var element = document.getElementById('daemon-control');
306  element.hidden = !canChangeLocalHostState;
307  element = document.getElementById('host-list-empty-hosting-supported');
308  element.hidden = !canChangeLocalHostState;
309  element = document.getElementById('host-list-empty-hosting-unsupported');
310  element.hidden = canChangeLocalHostState;
311};
312
313/**
314 * Remove a host from the list, and deregister it.
315 * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
316 * @return {void} Nothing.
317 * @private
318 */
319remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
320  this.table_.removeChild(hostTableEntry.tableRow);
321  var index = this.hostTableEntries_.indexOf(hostTableEntry);
322  if (index != -1) {
323    this.hostTableEntries_.splice(index, 1);
324  }
325  remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
326};
327
328/**
329 * Prepare a host for renaming by replacing its name with an edit box.
330 * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
331 * @return {void} Nothing.
332 * @private
333 */
334remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
335  for (var i = 0; i < this.hosts_.length; ++i) {
336    if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
337      this.hosts_[i].hostName = hostTableEntry.host.hostName;
338      break;
339    }
340  }
341  this.save_();
342
343  /** @param {string?} token */
344  var renameHost = function(token) {
345    if (token) {
346      var headers = {
347        'Authorization': 'OAuth ' + token,
348        'Content-type' : 'application/json; charset=UTF-8'
349      };
350      var newHostDetails = { data: {
351        hostId: hostTableEntry.host.hostId,
352        hostName: hostTableEntry.host.hostName,
353        publicKey: hostTableEntry.host.publicKey
354      } };
355      remoting.xhr.put(
356          remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
357          hostTableEntry.host.hostId,
358          function(xhr) {},
359          JSON.stringify(newHostDetails),
360          headers);
361    } else {
362      console.error('Could not rename host. Authentication failure.');
363    }
364  }
365  remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
366};
367
368/**
369 * Unregister a host.
370 * @param {string} hostId The id of the host to be removed.
371 * @return {void} Nothing.
372 */
373remoting.HostList.unregisterHostById = function(hostId) {
374  /** @param {string} token The OAuth2 token. */
375  var deleteHost = function(token) {
376    var headers = { 'Authorization': 'OAuth ' + token };
377    remoting.xhr.remove(
378        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
379        function() {}, '', headers);
380  }
381  remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
382};
383
384/**
385 * Set tool-tips for the 'connect' action. We can't just set this on the
386 * parent element because the button has no tool-tip, and therefore would
387 * inherit connectStr.
388 *
389 * @return {void} Nothing.
390 * @private
391 */
392remoting.HostList.prototype.setTooltips_ = function() {
393  var connectStr = '';
394  if (this.localHost_) {
395    chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
396                           this.localHost_.hostName);
397  }
398  document.getElementById('this-host-name').title = connectStr;
399  document.getElementById('this-host-icon').title = connectStr;
400};
401
402/**
403 * Set the state of the local host and localHostId if any.
404 *
405 * @param {remoting.HostController.State} state State of the local host.
406 * @param {string?} hostId ID of the local host, or null.
407 * @return {void} Nothing.
408 */
409remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
410  this.localHostState_ = state;
411  this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
412}
413
414/**
415 * Set the host object that corresponds to the local computer, if any.
416 *
417 * @param {remoting.Host?} host The host, or null if not registered.
418 * @return {void} Nothing.
419 * @private
420 */
421remoting.HostList.prototype.setLocalHost_ = function(host) {
422  this.localHost_ = host;
423  this.setTooltips_();
424  /** @type {remoting.HostList} */
425  var that = this;
426  if (host) {
427    /** @param {remoting.HostTableEntry} host */
428    var renameHost = function(host) {
429      that.renameHost_(host);
430      that.setTooltips_();
431    };
432    if (!this.localHostTableEntry_) {
433      /** @type {remoting.HostTableEntry} @private */
434      this.localHostTableEntry_ = new remoting.HostTableEntry(
435          host, this.webappMajorVersion_, renameHost);
436      this.localHostTableEntry_.init(
437          document.getElementById('this-host-connect'),
438          document.getElementById('this-host-warning'),
439          document.getElementById('this-host-name'),
440          document.getElementById('this-host-rename'));
441    } else {
442      // TODO(jamiewalch): This is hack to prevent multiple click handlers being
443      // registered for the same DOM elements if this method is called more than
444      // once. A better solution would be to let HostTable create the daemon row
445      // like it creates the rows for non-local hosts.
446      this.localHostTableEntry_.host = host;
447    }
448  } else {
449    this.localHostTableEntry_ = null;
450  }
451}
452
453/**
454 * Called by the HostControlled after the local host has been started.
455 *
456 * @param {string} hostName Host name.
457 * @param {string} hostId ID of the local host.
458 * @param {string} publicKey Public key.
459 * @return {void} Nothing.
460 */
461remoting.HostList.prototype.onLocalHostStarted = function(
462    hostName, hostId, publicKey) {
463  // Create a dummy remoting.Host instance to represent the local host.
464  // Refreshing the list is no good in general, because the directory
465  // information won't be in sync for several seconds. We don't know the
466  // host JID, but it can be missing from the cache with no ill effects.
467  // It will be refreshed if the user tries to connect to the local host,
468  // and we hope that the directory will have been updated by that point.
469  var localHost = new remoting.Host();
470  localHost.hostName = hostName;
471  // Provide a version number to avoid warning about this dummy host being
472  // out-of-date.
473  localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
474  localHost.hostId = hostId;
475  localHost.publicKey = publicKey;
476  localHost.status = 'ONLINE';
477  this.hosts_.push(localHost);
478  this.save_();
479  this.setLocalHost_(localHost);
480};
481
482/**
483 * Called when the user clicks the button next to the error message. The action
484 * depends on the error.
485 *
486 * @private
487 */
488remoting.HostList.prototype.onErrorClick_ = function() {
489  if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
490    remoting.oauth2.doAuthRedirect();
491  } else {
492    this.refresh(remoting.updateLocalHostState);
493  }
494};
495
496/**
497 * Save the host list to local storage.
498 */
499remoting.HostList.prototype.save_ = function() {
500  var items = {};
501  items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
502  chrome.storage.local.set(items);
503};
504
505/**
506 * Key name under which Me2Me hosts are cached.
507 */
508remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
509
510/** @type {remoting.HostList} */
511remoting.hostList = null;
512