blob: ce7c18ecd3e7c28b2e55a0973ffcd4e01a6b5215 [file] [log] [blame]
// 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.UNKNOWN;
/**
* @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;
} else if (a.hostName.toLocaleLowerCase() <
b.hostName.toLocaleLowerCase()) {
return -1;
} else if (a.hostName.toLocaleLowerCase() >
b.hostName.toLocaleLowerCase()) {
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) &&
(state != remoting.HostController.State.UNKNOWN) &&
(state != remoting.HostController.State.NOT_INSTALLED ||
remoting.isMe2MeInstallable()) &&
(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;