Ignore non-active fullscreen windows for shelf state.
[chromium-blink-merge.git] / remoting / webapp / host_list.js
blob43ce0924c293e389e97e81b27f204013c32fdb2e
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.
5 /**
6 * @fileoverview
7 * Class representing the host-list portion of the home screen UI.
8 */
10 'use strict';
12 /** @suppress {duplicate} */
13 var remoting = remoting || {};
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.
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.
30 remoting.HostList = function(table, noHosts, errorMsg, errorButton,
31 loadingIndicator) {
32 /**
33 * @type {Element}
34 * @private
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).
43 this.noHosts_ = noHosts;
44 /**
45 * @type {Element}
46 * @private
48 this.errorMsg_ = errorMsg;
49 /**
50 * @type {Element}
51 * @private
53 this.errorButton_ = errorButton;
54 /**
55 * @type {HTMLElement}
56 * @private
58 this.loadingIndicator_ = loadingIndicator;
59 /**
60 * @type {Array.<remoting.HostTableEntry>}
61 * @private
63 this.hostTableEntries_ = [];
64 /**
65 * @type {Array.<remoting.Host>}
66 * @private
68 this.hosts_ = [];
69 /**
70 * @type {string}
71 * @private
73 this.lastError_ = '';
74 /**
75 * @type {remoting.Host?}
76 * @private
78 this.localHost_ = null;
79 /**
80 * @type {remoting.HostController.State}
81 * @private
83 this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
84 /**
85 * @type {number}
86 * @private
88 this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
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));
101 reloadButton.addEventListener('click', refresh, false);
105 * Load the host-list asynchronously from local storage.
107 * @param {function():void} onDone Completion callback.
109 remoting.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);
123 onDone();
125 chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
129 * Search the host list for a host with the specified id.
131 * @param {string} hostId The unique id of the host.
132 * @return {remoting.Host?} The host, if any.
134 remoting.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];
140 return null;
144 * Get the host id corresponding to the specified host name.
146 * @param {string} hostName The name of the host.
147 * @return {string?} The host id, if a host with the given name exists.
149 remoting.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;
155 return null;
159 * Query the Remoting Directory for the user's list of hosts.
161 * @param {function(boolean):void} onDone Callback invoked with true on success
162 * or false on failure.
163 * @return {void} Nothing.
165 remoting.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);
178 /** @param {remoting.Error} error */
179 var onError = function(error) {
180 that.lastError_ = error;
181 onDone(false);
183 remoting.identity.callWithToken(getHosts, onError);
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.
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
196 remoting.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;
206 * @param {remoting.Host} a
207 * @param {remoting.Host} b
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;
215 return 0;
217 this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
218 } else {
219 this.hosts_ = [];
221 } else {
222 this.lastError_ = remoting.Error.UNEXPECTED;
223 console.error('Invalid "hosts" response from server.');
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;
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;
243 this.save_();
244 this.loadingIndicator_.classList.remove('loading');
245 onDone(this.lastError_ == '');
249 * Display the list of hosts or error condition.
251 * @return {void} Nothing.
253 remoting.HostList.prototype.display = function() {
254 this.table_.innerText = '';
255 this.errorMsg_.innerText = '';
256 this.hostTableEntries_ = [];
258 var noHostsRegistered = (this.hosts_.length == 0);
259 this.table_.hidden = noHostsRegistered;
260 this.noHosts_.hidden = !noHostsRegistered;
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');
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);
290 this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
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_ == '');
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;
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
319 remoting.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);
325 remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
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
334 remoting.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;
341 this.save_();
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'
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.');
365 remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
369 * Unregister a host.
370 * @param {string} hostId The id of the host to be removed.
371 * @return {void} Nothing.
373 remoting.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);
381 remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
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.
389 * @return {void} Nothing.
390 * @private
392 remoting.HostList.prototype.setTooltips_ = function() {
393 var connectStr = '';
394 if (this.localHost_) {
395 chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
396 this.localHost_.hostName);
398 document.getElementById('this-host-name').title = connectStr;
399 document.getElementById('this-host-icon').title = connectStr;
403 * Set the state of the local host and localHostId if any.
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.
409 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
410 this.localHostState_ = state;
411 this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
415 * Set the host object that corresponds to the local computer, if any.
417 * @param {remoting.Host?} host The host, or null if not registered.
418 * @return {void} Nothing.
419 * @private
421 remoting.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_();
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;
448 } else {
449 this.localHostTableEntry_ = null;
454 * Called by the HostControlled after the local host has been started.
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.
461 remoting.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);
483 * Called when the user clicks the button next to the error message. The action
484 * depends on the error.
486 * @private
488 remoting.HostList.prototype.onErrorClick_ = function() {
489 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
490 remoting.oauth2.doAuthRedirect();
491 } else {
492 this.refresh(remoting.updateLocalHostState);
497 * Save the host list to local storage.
499 remoting.HostList.prototype.save_ = function() {
500 var items = {};
501 items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
502 chrome.storage.local.set(items);
506 * Key name under which Me2Me hosts are cached.
508 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
510 /** @type {remoting.HostList} */
511 remoting.hostList = null;