blob: 45229051daf795d4c2dfc115bbcd9c8164185649 [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.
* @param {function(!remoting.Error)} onError Function to call when an error
* occurs.
* @param {function(string)} handleConnect Function to call to connect to the
* host with |hostId|.
*/
remoting.HostList = function(table, noHosts, errorMsg, errorButton,
loadingIndicator, onError, handleConnect) {
/** @private {Element} */
this.table_ = table;
/**
* TODO(jamiewalch): This should be doable using CSS's sibling selector,
* but it doesn't work right now (crbug.com/135050).
* @private {Element}
*/
this.noHosts_ = noHosts;
/** @private {Element} */
this.errorMsg_ = errorMsg;
/** @private {Element} */
this.errorButton_ = errorButton;
/** @private {HTMLElement} */
this.loadingIndicator_ = loadingIndicator;
/** @private */
this.onError_ = remoting.Error.handler(onError);
/** @private */
this.handleConnect_ = handleConnect;
/** @private {Array<remoting.HostTableEntry>} */
this.hostTableEntries_ = [];
/** @private {Array<remoting.Host>} */
this.hosts_ = [];
/** @private {!remoting.Error} */
this.lastError_ = remoting.Error.none();
/** @private {remoting.LocalHostSection} */
this.localHostSection_ = new remoting.LocalHostSection(
/** @type {HTMLElement} */ (document.querySelector('.daemon-control')),
new remoting.LocalHostSection.Controller(
this, new remoting.HostSetupDialog(remoting.hostController, onError),
handleConnect));
/** @private {number} */
this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
/**
* The timestamp (in milliseconds since 01/01/1970) till the last host list
* refresh.
*
* @private {number}
*/
this.lastHostListRefresh_ = 0;
/** @private {Promise} */
this.pendingRefresh_ = null;
var reloadButton = this.loadingIndicator_.firstElementChild;
/** @type {remoting.HostList} */
var that = this;
/** @param {Event} event */
function refresh(event) {
event.preventDefault();
that.refreshAndDisplay();
}
/** @private */
this.eventHooks_ = new base.Disposables(
new base.DomEventHook(reloadButton, 'click', refresh, false),
new base.DomEventHook(window, 'focus', this.onFocus_.bind(this), false),
new base.DomEventHook(this.errorButton_, 'click',
this.onErrorClick_.bind(this), 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 = base.jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
if (cached && cached instanceof Array) {
that.hosts_ = cached
.map((host) => remoting.Host.fromObject(host))
.filter((host) => Boolean(host));
} 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.
*
* @return {Promise} A Promise that resolves when the host list refreshes or
* rejects if it fails.
*/
remoting.HostList.prototype.refresh = function() {
this.loadingIndicator_.classList.add('loading');
if (!this.pendingRefresh_) {
/** @type {remoting.HostList} */
var that = this;
this.pendingRefresh_ = remoting.HostListApi.getInstance().get().then(
function(hosts) {
that.pendingRefresh_ = null;
that.onHostListResponse_(hosts);
}).catch(function (/** !remoting.Error */ error) {
that.pendingRefresh_ = null;
that.lastError_ = error;
throw error;
});
}
return this.pendingRefresh_;
};
/**
* 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 {Array<remoting.Host>} hosts The list of hosts for the user.
* @return {boolean}
* @private
*/
remoting.HostList.prototype.onHostListResponse_ = function(hosts) {
this.lastError_ = remoting.Error.none();
this.lastHostListRefresh_ = Date.now();
this.hosts_ = hosts;
this.sortHosts_();
this.save_();
this.loadingIndicator_.classList.remove('loading');
return true;
};
/**
* @return {Promise} A promise that resolves when the host list finishes
* updating its UI.
*/
remoting.HostList.prototype.refreshAndDisplay = function() {
return this.refresh().then(this.display.bind(this));
};
/**
* Auto refreshes the host list when the current window receives focus.
*
* @private
*/
remoting.HostList.prototype.onFocus_ = function() {
// Rate limit the refresh to avoid spamming the directory service.
if ((Date.now() - this.lastHostListRefresh_) < 3000) {
return;
}
if (remoting.currentMode == remoting.AppMode.IN_SESSION) {
return;
}
this.refreshAndDisplay();
};
/**
* Sort the internal list of hosts.
*
* @suppress {reportUnknownTypes}
* @return {void} Nothing.
*/
remoting.HostList.prototype.sortHosts_ = function() {
/**
* Sort hosts, first by ONLINE/OFFLINE status and then by host-name.
*
* @param {remoting.Host} a
* @param {remoting.Host} b
* @return {number}
*/
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_ = this.hosts_.sort(cmp);
};
/**
* 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_.isNone()) {
l10n.localizeElementFromTag(this.errorMsg_, this.lastError_.getTag());
if (this.lastError_.hasTag(remoting.Error.Tag.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 &&
(host.hostId != this.localHostSection_.getHostId())) {
var hostTableEntry = new remoting.HostTableEntry(
this.webappMajorVersion_,
this.handleConnect_,
this.renameHost.bind(this),
this.deleteHost_.bind(this));
hostTableEntry.setHost(host);
this.hostTableEntries_[i] = hostTableEntry;
this.table_.appendChild(hostTableEntry.element());
}
}
}
this.errorMsg_.parentNode.hidden = this.lastError_.isNone();
if (noHostsRegistered) {
this.showHostListEmptyMessage_(this.localHostSection_.canChangeState());
}
};
/**
* @return {number} Time in ms since the last host list refresh
*/
remoting.HostList.prototype.getHostStatusUpdateElapsedTime = function() {
return Date.now() - this.lastHostListRefresh_;
};
/**
* Displays a message to the user when the host list is empty.
*
* @param {boolean} hostingSupported
* @return {void}
* @private
*/
remoting.HostList.prototype.showHostListEmptyMessage_ = function(
hostingSupported) {
var that = this;
remoting.AppsV2Migration.hasHostsInV1App().then(
/**
* @param {remoting.MigrationSettings} previousIdentity
*/
function(previousIdentity) {
that.noHosts_.innerHTML = remoting.AppsV2Migration.buildMigrationTips(
previousIdentity.email, previousIdentity.fullName);
},
function() {
var buttonLabel = l10n.getTranslationOrError(
/*i18n-content*/'HOME_DAEMON_START_BUTTON');
if (hostingSupported) {
that.noHosts_.innerText = l10n.getTranslationOrError(
/*i18n-content*/'HOST_LIST_EMPTY_HOSTING_SUPPORTED',
[buttonLabel]);
} else {
that.noHosts_.innerText = l10n.getTranslationOrError(
/*i18n-content*/'HOST_LIST_EMPTY_HOSTING_UNSUPPORTED',
[buttonLabel]);
}
}
);
};
/**
* 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.element());
var index = this.hostTableEntries_.indexOf(hostTableEntry);
if (index != -1) {
this.hostTableEntries_.splice(index, 1);
}
remoting.HostListApi.getInstance().remove(hostTableEntry.host.hostId).
catch(this.onError_);
};
/**
* 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.
*/
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_();
var hostListApi = remoting.HostListApi.getInstance();
hostListApi.put(hostTableEntry.host.hostId,
hostTableEntry.host.hostName,
hostTableEntry.host.publicKey).
catch(this.onError_);
};
/**
* Unregister a host.
* @param {string} hostId The id of the host to be removed.
* @param {function(void)=} opt_onDone
* @return {void} Nothing.
*/
remoting.HostList.prototype.unregisterHostById = function(hostId, opt_onDone) {
var that = this;
var onDone = opt_onDone || base.doNothing;
var host = this.getHostForId(hostId);
if (!host) {
console.log('Skipping host un-registration as the host is not registered ' +
'under the current account');
onDone();
return;
}
remoting.HostListApi.getInstance().remove(hostId).then(
this.refresh.bind(this)
).then(
this.display.bind(this)
).then(
onDone
).catch(this.onError_);
};
/**
* 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) {
var host = hostId ? this.getHostForId(hostId) : null;
this.localHostSection_.setModel(host, state, !this.lastError_.isNone());
};
/**
* 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(hostId);
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.publicKey = publicKey;
localHost.status = 'ONLINE';
this.hosts_.push(localHost);
this.save_();
this.localHostSection_.setModel(localHost,
remoting.HostController.State.STARTED,
!this.lastError_.isNone());
};
/**
* 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_.hasTag(remoting.Error.Tag.AUTHENTICATION_FAILED)) {
remoting.handleAuthFailureAndRelaunch();
} else {
this.refresh().then(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);
if (this.hosts_.length !== 0) {
remoting.AppsV2Migration.saveUserInfo();
}
};
/**
* Key name under which Me2Me hosts are cached.
*/
remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
/** @type {remoting.HostList} */
remoting.hostList = null;