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.NOT_IMPLEMENTED;
88 this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
90 this.errorButton_.addEventListener('click',
91 this.onErrorClick_.bind(this),
93 var reloadButton = this.loadingIndicator_.firstElementChild;
94 /** @type {remoting.HostList} */
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} */
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]);
118 that.hosts_ = /** @type {Array} */ cached;
120 console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
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];
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;
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} */
171 /** @param {string} token The OAuth2 token. */
172 var getHosts = function(token) {
173 var headers = { 'Authorization': 'OAuth ' + token };
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;
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.
196 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
197 this.lastError_ = '';
199 if (xhr.status == 200) {
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) {
212 } else if (b.status < a.status) {
217 this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
222 this.lastError_ = remoting.Error.UNEXPECTED;
223 console.error('Invalid "hosts" response from server.');
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;
235 this.lastError_ = remoting.Error.UNEXPECTED;
239 var typed_er = /** @type {Object} */ (er);
240 console.error('Error processing response: ', xhr, typed_er);
241 this.lastError_ = remoting.Error.UNEXPECTED;
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');
268 l10n.localizeElementFromTag(this.errorButton_,
269 /*i18n-content*/'RETRY');
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.
319 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
320 this.table_.removeChild(hostTableEntry.tableRow);
321 var index = this.hostTableEntries_.indexOf(hostTableEntry);
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.
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;
343 /** @param {string?} token */
344 var renameHost = function(token) {
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
356 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
357 hostTableEntry.host.hostId,
359 JSON.stringify(newHostDetails),
362 console.error('Could not rename host. Authentication failure.');
365 remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
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 };
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.
392 remoting.HostList.prototype.setTooltips_ = function() {
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.
421 remoting.HostList.prototype.setLocalHost_ = function(host) {
422 this.localHost_ = host;
424 /** @type {remoting.HostList} */
427 /** @param {remoting.HostTableEntry} host */
428 var renameHost = function(host) {
429 that.renameHost_(host);
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'));
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;
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
473 localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
474 localHost.hostId = hostId;
475 localHost.publicKey = publicKey;
476 localHost.status = 'ONLINE';
477 this.hosts_.push(localHost);
479 this.setLocalHost_(localHost);
483 * Called when the user clicks the button next to the error message. The action
484 * depends on the error.
488 remoting.HostList.prototype.onErrorClick_ = function() {
489 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
490 remoting.oauth2.doAuthRedirect();
492 this.refresh(remoting.updateLocalHostState);
497 * Save the host list to local storage.
499 remoting.HostList.prototype.save_ = function() {
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;