Javascript  |  512行  |  16.4 KB

// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview
 * Class representing the host-list portion of the home screen UI.
 */

'use strict';

/** @suppress {duplicate} */
var remoting = remoting || {};

/**
 * Create a host list consisting of the specified HTML elements, which should
 * have a common parent that contains only host-list UI as it will be hidden
 * if the host-list is empty.
 *
 * @constructor
 * @param {Element} table The HTML <div> to contain host-list.
 * @param {Element} noHosts The HTML <div> containing the "no hosts" message.
 * @param {Element} errorMsg The HTML <div> to display error messages.
 * @param {Element} errorButton The HTML <button> to display the error
 *     resolution action.
 * @param {HTMLElement} loadingIndicator The HTML <span> to update while the
 *     host list is being loaded. The first element of this span should be
 *     the reload button.
 */
remoting.HostList = function(table, noHosts, errorMsg, errorButton,
                             loadingIndicator) {
  /**
   * @type {Element}
   * @private
   */
  this.table_ = table;
  /**
   * @type {Element}
   * @private
   * TODO(jamiewalch): This should be doable using CSS's sibling selector,
   * but it doesn't work right now (crbug.com/135050).
   */
  this.noHosts_ = noHosts;
  /**
   * @type {Element}
   * @private
   */
  this.errorMsg_ = errorMsg;
  /**
   * @type {Element}
   * @private
   */
  this.errorButton_ = errorButton;
  /**
   * @type {HTMLElement}
   * @private
   */
  this.loadingIndicator_ = loadingIndicator;
  /**
   * @type {Array.<remoting.HostTableEntry>}
   * @private
   */
  this.hostTableEntries_ = [];
  /**
   * @type {Array.<remoting.Host>}
   * @private
   */
  this.hosts_ = [];
  /**
   * @type {string}
   * @private
   */
  this.lastError_ = '';
  /**
   * @type {remoting.Host?}
   * @private
   */
  this.localHost_ = null;
  /**
   * @type {remoting.HostController.State}
   * @private
   */
  this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
  /**
   * @type {number}
   * @private
   */
  this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);

  this.errorButton_.addEventListener('click',
                                     this.onErrorClick_.bind(this),
                                     false);
  var reloadButton = this.loadingIndicator_.firstElementChild;
  /** @type {remoting.HostList} */
  var that = this;
  /** @param {Event} event */
  function refresh(event) {
    event.preventDefault();
    that.refresh(that.display.bind(that));
  };
  reloadButton.addEventListener('click', refresh, false);
};

/**
 * Load the host-list asynchronously from local storage.
 *
 * @param {function():void} onDone Completion callback.
 */
remoting.HostList.prototype.load = function(onDone) {
  // Load the cache of the last host-list, if present.
  /** @type {remoting.HostList} */
  var that = this;
  /** @param {Object.<string>} items */
  var storeHostList = function(items) {
    if (items[remoting.HostList.HOSTS_KEY]) {
      var cached = jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
      if (cached) {
        that.hosts_ = /** @type {Array} */ cached;
      } else {
        console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
      }
    }
    onDone();
  };
  chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
};

/**
 * Search the host list for a host with the specified id.
 *
 * @param {string} hostId The unique id of the host.
 * @return {remoting.Host?} The host, if any.
 */
remoting.HostList.prototype.getHostForId = function(hostId) {
  for (var i = 0; i < this.hosts_.length; ++i) {
    if (this.hosts_[i].hostId == hostId) {
      return this.hosts_[i];
    }
  }
  return null;
};

/**
 * Get the host id corresponding to the specified host name.
 *
 * @param {string} hostName The name of the host.
 * @return {string?} The host id, if a host with the given name exists.
 */
remoting.HostList.prototype.getHostIdForName = function(hostName) {
  for (var i = 0; i < this.hosts_.length; ++i) {
    if (this.hosts_[i].hostName == hostName) {
      return this.hosts_[i].hostId;
    }
  }
  return null;
};

/**
 * Query the Remoting Directory for the user's list of hosts.
 *
 * @param {function(boolean):void} onDone Callback invoked with true on success
 *     or false on failure.
 * @return {void} Nothing.
 */
remoting.HostList.prototype.refresh = function(onDone) {
  this.loadingIndicator_.classList.add('loading');
  /** @param {XMLHttpRequest} xhr The response from the server. */
  var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone);
  /** @type {remoting.HostList} */
  var that = this;
  /** @param {string} token The OAuth2 token. */
  var getHosts = function(token) {
    var headers = { 'Authorization': 'OAuth ' + token };
    remoting.xhr.get(
        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
        parseHostListResponse, '', headers);
  };
  /** @param {remoting.Error} error */
  var onError = function(error) {
    that.lastError_ = error;
    onDone(false);
  };
  remoting.identity.callWithToken(getHosts, onError);
};

/**
 * Handle the results of the host list request.  A success response will
 * include a JSON-encoded list of host descriptions, which we display if we're
 * able to successfully parse it.
 *
 * @param {function(boolean):void} onDone The callback passed to |refresh|.
 * @param {XMLHttpRequest} xhr The XHR object for the host list request.
 * @return {void} Nothing.
 * @private
 */
remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
  this.lastError_ = '';
  try {
    if (xhr.status == 200) {
      var response =
          /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
      if (response && response.data) {
        if (response.data.items) {
          this.hosts_ = response.data.items;
          /**
           * @param {remoting.Host} a
           * @param {remoting.Host} b
           */
          var cmp = function(a, b) {
            if (a.status < b.status) {
              return 1;
            } else if (b.status < a.status) {
              return -1;
            }
            return 0;
          };
          this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
        } else {
          this.hosts_ = [];
        }
      } else {
        this.lastError_ = remoting.Error.UNEXPECTED;
        console.error('Invalid "hosts" response from server.');
      }
    } else {
      // Some other error.
      console.error('Bad status on host list query: ', xhr);
      if (xhr.status == 0) {
        this.lastError_ = remoting.Error.NETWORK_FAILURE;
      } else if (xhr.status == 401) {
        this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
      } else if (xhr.status == 502 || xhr.status == 503) {
        this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
      } else {
        this.lastError_ = remoting.Error.UNEXPECTED;
      }
    }
  } catch (er) {
    var typed_er = /** @type {Object} */ (er);
    console.error('Error processing response: ', xhr, typed_er);
    this.lastError_ = remoting.Error.UNEXPECTED;
  }
  this.save_();
  this.loadingIndicator_.classList.remove('loading');
  onDone(this.lastError_ == '');
};

/**
 * Display the list of hosts or error condition.
 *
 * @return {void} Nothing.
 */
remoting.HostList.prototype.display = function() {
  this.table_.innerText = '';
  this.errorMsg_.innerText = '';
  this.hostTableEntries_ = [];

  var noHostsRegistered = (this.hosts_.length == 0);
  this.table_.hidden = noHostsRegistered;
  this.noHosts_.hidden = !noHostsRegistered;

  if (this.lastError_ != '') {
    l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
    if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
      l10n.localizeElementFromTag(this.errorButton_,
                                  /*i18n-content*/'SIGN_IN_BUTTON');
    } else {
      l10n.localizeElementFromTag(this.errorButton_,
                                  /*i18n-content*/'RETRY');
    }
  } else {
    for (var i = 0; i < this.hosts_.length; ++i) {
      /** @type {remoting.Host} */
      var host = this.hosts_[i];
      // Validate the entry to make sure it has all the fields we expect and is
      // not the local host (which is displayed separately). NB: if the host has
      // never sent a heartbeat, then there will be no jabberId.
      if (host.hostName && host.hostId && host.status && host.publicKey &&
          (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
        var hostTableEntry = new remoting.HostTableEntry(
            host, this.webappMajorVersion_,
            this.renameHost_.bind(this), this.deleteHost_.bind(this));
        hostTableEntry.createDom();
        this.hostTableEntries_[i] = hostTableEntry;
        this.table_.appendChild(hostTableEntry.tableRow);
      }
    }
  }

  this.errorMsg_.parentNode.hidden = (this.lastError_ == '');

  // The local host cannot be stopped or started if the host controller is not
  // implemented for this platform. Additionally, it cannot be started if there
  // is an error (in many error states, the start operation will fail anyway,
  // but even if it succeeds, the chance of a related but hard-to-diagnose
  // future error is high).
  var state = this.localHostState_;
  var enabled = (state == remoting.HostController.State.STARTING) ||
      (state == remoting.HostController.State.STARTED);
  var canChangeLocalHostState =
      (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
      (enabled || this.lastError_ == '');

  remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
  var element = document.getElementById('daemon-control');
  element.hidden = !canChangeLocalHostState;
  element = document.getElementById('host-list-empty-hosting-supported');
  element.hidden = !canChangeLocalHostState;
  element = document.getElementById('host-list-empty-hosting-unsupported');
  element.hidden = canChangeLocalHostState;
};

/**
 * Remove a host from the list, and deregister it.
 * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
 * @return {void} Nothing.
 * @private
 */
remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
  this.table_.removeChild(hostTableEntry.tableRow);
  var index = this.hostTableEntries_.indexOf(hostTableEntry);
  if (index != -1) {
    this.hostTableEntries_.splice(index, 1);
  }
  remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
};

/**
 * Prepare a host for renaming by replacing its name with an edit box.
 * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
 * @return {void} Nothing.
 * @private
 */
remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
  for (var i = 0; i < this.hosts_.length; ++i) {
    if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
      this.hosts_[i].hostName = hostTableEntry.host.hostName;
      break;
    }
  }
  this.save_();

  /** @param {string?} token */
  var renameHost = function(token) {
    if (token) {
      var headers = {
        'Authorization': 'OAuth ' + token,
        'Content-type' : 'application/json; charset=UTF-8'
      };
      var newHostDetails = { data: {
        hostId: hostTableEntry.host.hostId,
        hostName: hostTableEntry.host.hostName,
        publicKey: hostTableEntry.host.publicKey
      } };
      remoting.xhr.put(
          remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
          hostTableEntry.host.hostId,
          function(xhr) {},
          JSON.stringify(newHostDetails),
          headers);
    } else {
      console.error('Could not rename host. Authentication failure.');
    }
  }
  remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
};

/**
 * Unregister a host.
 * @param {string} hostId The id of the host to be removed.
 * @return {void} Nothing.
 */
remoting.HostList.unregisterHostById = function(hostId) {
  /** @param {string} token The OAuth2 token. */
  var deleteHost = function(token) {
    var headers = { 'Authorization': 'OAuth ' + token };
    remoting.xhr.remove(
        remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
        function() {}, '', headers);
  }
  remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
};

/**
 * Set tool-tips for the 'connect' action. We can't just set this on the
 * parent element because the button has no tool-tip, and therefore would
 * inherit connectStr.
 *
 * @return {void} Nothing.
 * @private
 */
remoting.HostList.prototype.setTooltips_ = function() {
  var connectStr = '';
  if (this.localHost_) {
    chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
                           this.localHost_.hostName);
  }
  document.getElementById('this-host-name').title = connectStr;
  document.getElementById('this-host-icon').title = connectStr;
};

/**
 * Set the state of the local host and localHostId if any.
 *
 * @param {remoting.HostController.State} state State of the local host.
 * @param {string?} hostId ID of the local host, or null.
 * @return {void} Nothing.
 */
remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
  this.localHostState_ = state;
  this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
}

/**
 * Set the host object that corresponds to the local computer, if any.
 *
 * @param {remoting.Host?} host The host, or null if not registered.
 * @return {void} Nothing.
 * @private
 */
remoting.HostList.prototype.setLocalHost_ = function(host) {
  this.localHost_ = host;
  this.setTooltips_();
  /** @type {remoting.HostList} */
  var that = this;
  if (host) {
    /** @param {remoting.HostTableEntry} host */
    var renameHost = function(host) {
      that.renameHost_(host);
      that.setTooltips_();
    };
    if (!this.localHostTableEntry_) {
      /** @type {remoting.HostTableEntry} @private */
      this.localHostTableEntry_ = new remoting.HostTableEntry(
          host, this.webappMajorVersion_, renameHost);
      this.localHostTableEntry_.init(
          document.getElementById('this-host-connect'),
          document.getElementById('this-host-warning'),
          document.getElementById('this-host-name'),
          document.getElementById('this-host-rename'));
    } else {
      // TODO(jamiewalch): This is hack to prevent multiple click handlers being
      // registered for the same DOM elements if this method is called more than
      // once. A better solution would be to let HostTable create the daemon row
      // like it creates the rows for non-local hosts.
      this.localHostTableEntry_.host = host;
    }
  } else {
    this.localHostTableEntry_ = null;
  }
}

/**
 * Called by the HostControlled after the local host has been started.
 *
 * @param {string} hostName Host name.
 * @param {string} hostId ID of the local host.
 * @param {string} publicKey Public key.
 * @return {void} Nothing.
 */
remoting.HostList.prototype.onLocalHostStarted = function(
    hostName, hostId, publicKey) {
  // Create a dummy remoting.Host instance to represent the local host.
  // Refreshing the list is no good in general, because the directory
  // information won't be in sync for several seconds. We don't know the
  // host JID, but it can be missing from the cache with no ill effects.
  // It will be refreshed if the user tries to connect to the local host,
  // and we hope that the directory will have been updated by that point.
  var localHost = new remoting.Host();
  localHost.hostName = hostName;
  // Provide a version number to avoid warning about this dummy host being
  // out-of-date.
  localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
  localHost.hostId = hostId;
  localHost.publicKey = publicKey;
  localHost.status = 'ONLINE';
  this.hosts_.push(localHost);
  this.save_();
  this.setLocalHost_(localHost);
};

/**
 * Called when the user clicks the button next to the error message. The action
 * depends on the error.
 *
 * @private
 */
remoting.HostList.prototype.onErrorClick_ = function() {
  if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
    remoting.oauth2.doAuthRedirect();
  } else {
    this.refresh(remoting.updateLocalHostState);
  }
};

/**
 * Save the host list to local storage.
 */
remoting.HostList.prototype.save_ = function() {
  var items = {};
  items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
  chrome.storage.local.set(items);
};

/**
 * Key name under which Me2Me hosts are cached.
 */
remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';

/** @type {remoting.HostList} */
remoting.hostList = null;