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.
7 * Class representing the host-list portion of the home screen UI.
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
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.
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
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
30 remoting.HostList = function(table, noHosts, errorMsg, errorButton,
40 * TODO(jamiewalch): This should be doable using CSS's sibling selector,
41 * but it doesn't work right now (crbug.com/135050).
43 this.noHosts_ = noHosts;
48 this.errorMsg_ = errorMsg;
53 this.errorButton_ = errorButton;
58 this.loadingIndicator_ = loadingIndicator;
60 * @type {Array<remoting.HostTableEntry>}
63 this.hostTableEntries_ = [];
65 * @type {Array<remoting.Host>}
75 * @type {remoting.Host?}
78 this.localHost_ = null;
80 * @type {remoting.HostController.State}
83 this.localHostState_ = remoting.HostController.State.UNKNOWN;
89 this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
91 this.errorButton_.addEventListener('click',
92 this.onErrorClick_.bind(this),
94 var reloadButton = this.loadingIndicator_.firstElementChild;
95 /** @type {remoting.HostList} */
97 /** @param {Event} event */
98 function refresh(event) {
99 event.preventDefault();
100 that.refresh(that.display.bind(that));
102 reloadButton.addEventListener('click', refresh, false);
106 * Load the host-list asynchronously from local storage.
108 * @param {function():void} onDone Completion callback.
110 remoting.HostList.prototype.load = function(onDone) {
111 // Load the cache of the last host-list, if present.
112 /** @type {remoting.HostList} */
114 /** @param {Object<string>} items */
115 var storeHostList = function(items) {
116 if (items[remoting.HostList.HOSTS_KEY]) {
117 var cached = base.jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
119 that.hosts_ = /** @type {Array<remoting.Host>} */ (cached);
121 console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
126 chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
130 * Search the host list for a host with the specified id.
132 * @param {string} hostId The unique id of the host.
133 * @return {remoting.Host?} The host, if any.
135 remoting.HostList.prototype.getHostForId = function(hostId) {
136 for (var i = 0; i < this.hosts_.length; ++i) {
137 if (this.hosts_[i].hostId == hostId) {
138 return this.hosts_[i];
145 * Get the host id corresponding to the specified host name.
147 * @param {string} hostName The name of the host.
148 * @return {string?} The host id, if a host with the given name exists.
150 remoting.HostList.prototype.getHostIdForName = function(hostName) {
151 for (var i = 0; i < this.hosts_.length; ++i) {
152 if (this.hosts_[i].hostName == hostName) {
153 return this.hosts_[i].hostId;
160 * Query the Remoting Directory for the user's list of hosts.
162 * @param {function(boolean):void} onDone Callback invoked with true on success
163 * or false on failure.
164 * @return {void} Nothing.
166 remoting.HostList.prototype.refresh = function(onDone) {
167 this.loadingIndicator_.classList.add('loading');
168 /** @type {remoting.HostList} */
170 /** @param {remoting.Error} error */
171 var onError = function(error) {
172 that.lastError_ = error;
175 remoting.hostListApi.get(this.onHostListResponse_.bind(this, onDone),
180 * Handle the results of the host list request. A success response will
181 * include a JSON-encoded list of host descriptions, which we display if we're
182 * able to successfully parse it.
184 * @param {function(boolean):void} onDone The callback passed to |refresh|.
185 * @param {Array<remoting.Host>} hosts The list of hosts for the user.
186 * @return {void} Nothing.
189 remoting.HostList.prototype.onHostListResponse_ = function(onDone, hosts) {
190 this.lastError_ = '';
194 this.loadingIndicator_.classList.remove('loading');
195 onDone(this.lastError_ == '');
199 * Sort the internal list of hosts.
201 * @suppress {reportUnknownTypes}
202 * @return {void} Nothing.
204 remoting.HostList.prototype.sortHosts_ = function() {
206 * Sort hosts, first by ONLINE/OFFLINE status and then by host-name.
208 * @param {remoting.Host} a
209 * @param {remoting.Host} b
212 var cmp = function(a, b) {
213 if (a.status < b.status) {
215 } else if (b.status < a.status) {
217 } else if (a.hostName.toLocaleLowerCase() <
218 b.hostName.toLocaleLowerCase()) {
220 } else if (a.hostName.toLocaleLowerCase() >
221 b.hostName.toLocaleLowerCase()) {
227 this.hosts_ = this.hosts_.sort(cmp);
231 * Display the list of hosts or error condition.
233 * @return {void} Nothing.
235 remoting.HostList.prototype.display = function() {
236 this.table_.innerText = '';
237 this.errorMsg_.innerText = '';
238 this.hostTableEntries_ = [];
240 var noHostsRegistered = (this.hosts_.length == 0);
241 this.table_.hidden = noHostsRegistered;
242 this.noHosts_.hidden = !noHostsRegistered;
244 if (this.lastError_ != '') {
245 l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
246 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
247 l10n.localizeElementFromTag(this.errorButton_,
248 /*i18n-content*/'SIGN_IN_BUTTON');
250 l10n.localizeElementFromTag(this.errorButton_,
251 /*i18n-content*/'RETRY');
254 for (var i = 0; i < this.hosts_.length; ++i) {
255 /** @type {remoting.Host} */
256 var host = this.hosts_[i];
257 // Validate the entry to make sure it has all the fields we expect and is
258 // not the local host (which is displayed separately). NB: if the host has
259 // never sent a heartbeat, then there will be no jabberId.
260 if (host.hostName && host.hostId && host.status && host.publicKey &&
261 (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
262 var hostTableEntry = new remoting.HostTableEntry(
263 host, this.webappMajorVersion_,
264 this.renameHost_.bind(this), this.deleteHost_.bind(this));
265 hostTableEntry.createDom();
266 this.hostTableEntries_[i] = hostTableEntry;
267 this.table_.appendChild(hostTableEntry.tableRow);
272 this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
274 // The local host cannot be stopped or started if the host controller is not
275 // implemented for this platform. Additionally, it cannot be started if there
276 // is an error (in many error states, the start operation will fail anyway,
277 // but even if it succeeds, the chance of a related but hard-to-diagnose
278 // future error is high).
279 var state = this.localHostState_;
280 var enabled = (state == remoting.HostController.State.STARTING) ||
281 (state == remoting.HostController.State.STARTED);
282 var canChangeLocalHostState =
283 (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
284 (state != remoting.HostController.State.UNKNOWN) &&
285 (state != remoting.HostController.State.NOT_INSTALLED ||
286 remoting.isMe2MeInstallable()) &&
287 (enabled || this.lastError_ == '');
289 remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
290 var element = document.getElementById('daemon-control');
291 element.hidden = !canChangeLocalHostState;
293 if (noHostsRegistered) {
294 this.showHostListEmptyMessage_(canChangeLocalHostState);
299 * Displays a message to the user when the host list is empty.
301 * @param {boolean} hostingSupported
305 remoting.HostList.prototype.showHostListEmptyMessage_ = function(
308 remoting.AppsV2Migration.hasHostsInV1App().then(
310 * @param {remoting.MigrationSettings} previousIdentity
311 * @this {remoting.HostList}
313 function(previousIdentity) {
314 that.noHosts_.innerHTML = remoting.AppsV2Migration.buildMigrationTips(
315 previousIdentity.email, previousIdentity.fullName);
318 var buttonLabel = l10n.getTranslationOrError(
319 /*i18n-content*/'HOME_DAEMON_START_BUTTON');
320 if (hostingSupported) {
321 that.noHosts_.innerText = l10n.getTranslationOrError(
322 /*i18n-content*/'HOST_LIST_EMPTY_HOSTING_SUPPORTED',
325 that.noHosts_.innerText = l10n.getTranslationOrError(
326 /*i18n-content*/'HOST_LIST_EMPTY_HOSTING_UNSUPPORTED',
334 * Remove a host from the list, and deregister it.
335 * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
336 * @return {void} Nothing.
339 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
340 this.table_.removeChild(hostTableEntry.tableRow);
341 var index = this.hostTableEntries_.indexOf(hostTableEntry);
343 this.hostTableEntries_.splice(index, 1);
345 remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
349 * Prepare a host for renaming by replacing its name with an edit box.
350 * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
351 * @return {void} Nothing.
354 remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
355 for (var i = 0; i < this.hosts_.length; ++i) {
356 if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
357 this.hosts_[i].hostName = hostTableEntry.host.hostName;
363 remoting.hostListApi.put(hostTableEntry.host.hostId,
364 hostTableEntry.host.hostName,
365 hostTableEntry.host.publicKey,
367 remoting.showErrorMessage);
372 * @param {string} hostId The id of the host to be removed.
373 * @return {void} Nothing.
375 remoting.HostList.unregisterHostById = function(hostId) {
376 remoting.hostListApi.remove(hostId, function() {}, remoting.showErrorMessage);
380 * Set tool-tips for the 'connect' action. We can't just set this on the
381 * parent element because the button has no tool-tip, and therefore would
382 * inherit connectStr.
384 * @return {void} Nothing.
387 remoting.HostList.prototype.setTooltips_ = function() {
389 if (this.localHost_) {
390 chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
391 this.localHost_.hostName);
393 document.getElementById('this-host-name').title = connectStr;
394 document.getElementById('this-host-icon').title = connectStr;
398 * Set the state of the local host and localHostId if any.
400 * @param {remoting.HostController.State} state State of the local host.
401 * @param {string?} hostId ID of the local host, or null.
402 * @return {void} Nothing.
404 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
405 this.localHostState_ = state;
406 this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
410 * Set the host object that corresponds to the local computer, if any.
412 * @param {remoting.Host?} host The host, or null if not registered.
413 * @return {void} Nothing.
416 remoting.HostList.prototype.setLocalHost_ = function(host) {
417 this.localHost_ = host;
419 /** @type {remoting.HostList} */
422 /** @param {remoting.HostTableEntry} host */
423 var renameHost = function(host) {
424 that.renameHost_(host);
427 if (!this.localHostTableEntry_) {
428 /** @type {remoting.HostTableEntry} @private */
429 this.localHostTableEntry_ = new remoting.HostTableEntry(
430 host, this.webappMajorVersion_, renameHost);
431 this.localHostTableEntry_.init(
432 document.getElementById('this-host-connect'),
433 document.getElementById('this-host-warning'),
434 document.getElementById('this-host-name'),
435 document.getElementById('this-host-rename'));
437 // TODO(jamiewalch): This is hack to prevent multiple click handlers being
438 // registered for the same DOM elements if this method is called more than
439 // once. A better solution would be to let HostTable create the daemon row
440 // like it creates the rows for non-local hosts.
441 this.localHostTableEntry_.host = host;
444 this.localHostTableEntry_ = null;
449 * Called by the HostControlled after the local host has been started.
451 * @param {string} hostName Host name.
452 * @param {string} hostId ID of the local host.
453 * @param {string} publicKey Public key.
454 * @return {void} Nothing.
456 remoting.HostList.prototype.onLocalHostStarted = function(
457 hostName, hostId, publicKey) {
458 // Create a dummy remoting.Host instance to represent the local host.
459 // Refreshing the list is no good in general, because the directory
460 // information won't be in sync for several seconds. We don't know the
461 // host JID, but it can be missing from the cache with no ill effects.
462 // It will be refreshed if the user tries to connect to the local host,
463 // and we hope that the directory will have been updated by that point.
464 var localHost = new remoting.Host();
465 localHost.hostName = hostName;
466 // Provide a version number to avoid warning about this dummy host being
468 localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
469 localHost.hostId = hostId;
470 localHost.publicKey = publicKey;
471 localHost.status = 'ONLINE';
472 this.hosts_.push(localHost);
474 this.setLocalHost_(localHost);
478 * Called when the user clicks the button next to the error message. The action
479 * depends on the error.
483 remoting.HostList.prototype.onErrorClick_ = function() {
484 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
485 remoting.handleAuthFailureAndRelaunch();
487 this.refresh(remoting.updateLocalHostState);
492 * Save the host list to local storage.
494 remoting.HostList.prototype.save_ = function() {
496 items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
497 chrome.storage.local.set(items);
498 if (this.hosts_.length !== 0) {
499 remoting.AppsV2Migration.saveUserInfo();
504 * Key name under which Me2Me hosts are cached.
506 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
508 /** @type {remoting.HostList} */
509 remoting.hostList = null;