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 = jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
119 that.hosts_ = /** @type {Array} */ 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 /** @param {XMLHttpRequest} xhr The response from the server. */
169 var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone);
170 /** @type {remoting.HostList} */
172 /** @param {string} token The OAuth2 token. */
173 var getHosts = function(token) {
174 var headers = { 'Authorization': 'OAuth ' + token };
176 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
177 parseHostListResponse, '', headers);
179 /** @param {remoting.Error} error */
180 var onError = function(error) {
181 that.lastError_ = error;
184 remoting.identity.callWithToken(getHosts, onError);
188 * Handle the results of the host list request. A success response will
189 * include a JSON-encoded list of host descriptions, which we display if we're
190 * able to successfully parse it.
192 * @param {function(boolean):void} onDone The callback passed to |refresh|.
193 * @param {XMLHttpRequest} xhr The XHR object for the host list request.
194 * @return {void} Nothing.
197 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
198 this.lastError_ = '';
200 if (xhr.status == 200) {
202 /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
203 if (response && response.data) {
204 if (response.data.items) {
205 this.hosts_ = response.data.items;
207 * @param {remoting.Host} a
208 * @param {remoting.Host} b
210 var cmp = function(a, b) {
211 if (a.status < b.status) {
213 } else if (b.status < a.status) {
215 } else if (a.hostName.toLocaleLowerCase() <
216 b.hostName.toLocaleLowerCase()) {
218 } else if (a.hostName.toLocaleLowerCase() >
219 b.hostName.toLocaleLowerCase()) {
224 this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
229 this.lastError_ = remoting.Error.UNEXPECTED;
230 console.error('Invalid "hosts" response from server.');
234 console.error('Bad status on host list query: ', xhr);
235 if (xhr.status == 0) {
236 this.lastError_ = remoting.Error.NETWORK_FAILURE;
237 } else if (xhr.status == 401) {
238 this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
239 } else if (xhr.status == 502 || xhr.status == 503) {
240 this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
242 this.lastError_ = remoting.Error.UNEXPECTED;
246 var typed_er = /** @type {Object} */ (er);
247 console.error('Error processing response: ', xhr, typed_er);
248 this.lastError_ = remoting.Error.UNEXPECTED;
251 this.loadingIndicator_.classList.remove('loading');
252 onDone(this.lastError_ == '');
256 * Display the list of hosts or error condition.
258 * @return {void} Nothing.
260 remoting.HostList.prototype.display = function() {
261 this.table_.innerText = '';
262 this.errorMsg_.innerText = '';
263 this.hostTableEntries_ = [];
265 var noHostsRegistered = (this.hosts_.length == 0);
266 this.table_.hidden = noHostsRegistered;
267 this.noHosts_.hidden = !noHostsRegistered;
269 if (this.lastError_ != '') {
270 l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
271 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
272 l10n.localizeElementFromTag(this.errorButton_,
273 /*i18n-content*/'SIGN_IN_BUTTON');
275 l10n.localizeElementFromTag(this.errorButton_,
276 /*i18n-content*/'RETRY');
279 for (var i = 0; i < this.hosts_.length; ++i) {
280 /** @type {remoting.Host} */
281 var host = this.hosts_[i];
282 // Validate the entry to make sure it has all the fields we expect and is
283 // not the local host (which is displayed separately). NB: if the host has
284 // never sent a heartbeat, then there will be no jabberId.
285 if (host.hostName && host.hostId && host.status && host.publicKey &&
286 (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
287 var hostTableEntry = new remoting.HostTableEntry(
288 host, this.webappMajorVersion_,
289 this.renameHost_.bind(this), this.deleteHost_.bind(this));
290 hostTableEntry.createDom();
291 this.hostTableEntries_[i] = hostTableEntry;
292 this.table_.appendChild(hostTableEntry.tableRow);
297 this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
299 // The local host cannot be stopped or started if the host controller is not
300 // implemented for this platform. Additionally, it cannot be started if there
301 // is an error (in many error states, the start operation will fail anyway,
302 // but even if it succeeds, the chance of a related but hard-to-diagnose
303 // future error is high).
304 var state = this.localHostState_;
305 var enabled = (state == remoting.HostController.State.STARTING) ||
306 (state == remoting.HostController.State.STARTED);
307 var canChangeLocalHostState =
308 (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
309 (state != remoting.HostController.State.UNKNOWN) &&
310 (state != remoting.HostController.State.NOT_INSTALLED ||
311 remoting.isMe2MeInstallable()) &&
312 (enabled || this.lastError_ == '');
314 remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
315 var element = document.getElementById('daemon-control');
316 element.hidden = !canChangeLocalHostState;
317 element = document.getElementById('host-list-empty-hosting-supported');
318 element.hidden = !canChangeLocalHostState;
319 element = document.getElementById('host-list-empty-hosting-unsupported');
320 element.hidden = canChangeLocalHostState;
324 * Remove a host from the list, and deregister it.
325 * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
326 * @return {void} Nothing.
329 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
330 this.table_.removeChild(hostTableEntry.tableRow);
331 var index = this.hostTableEntries_.indexOf(hostTableEntry);
333 this.hostTableEntries_.splice(index, 1);
335 remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
339 * Prepare a host for renaming by replacing its name with an edit box.
340 * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
341 * @return {void} Nothing.
344 remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
345 for (var i = 0; i < this.hosts_.length; ++i) {
346 if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
347 this.hosts_[i].hostName = hostTableEntry.host.hostName;
353 /** @param {string?} token */
354 var renameHost = function(token) {
357 'Authorization': 'OAuth ' + token,
358 'Content-type' : 'application/json; charset=UTF-8'
360 var newHostDetails = { data: {
361 hostId: hostTableEntry.host.hostId,
362 hostName: hostTableEntry.host.hostName,
363 publicKey: hostTableEntry.host.publicKey
366 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
367 hostTableEntry.host.hostId,
369 JSON.stringify(newHostDetails),
372 console.error('Could not rename host. Authentication failure.');
375 remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
380 * @param {string} hostId The id of the host to be removed.
381 * @return {void} Nothing.
383 remoting.HostList.unregisterHostById = function(hostId) {
384 /** @param {string} token The OAuth2 token. */
385 var deleteHost = function(token) {
386 var headers = { 'Authorization': 'OAuth ' + token };
388 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
389 function() {}, '', headers);
391 remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
395 * Set tool-tips for the 'connect' action. We can't just set this on the
396 * parent element because the button has no tool-tip, and therefore would
397 * inherit connectStr.
399 * @return {void} Nothing.
402 remoting.HostList.prototype.setTooltips_ = function() {
404 if (this.localHost_) {
405 chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
406 this.localHost_.hostName);
408 document.getElementById('this-host-name').title = connectStr;
409 document.getElementById('this-host-icon').title = connectStr;
413 * Set the state of the local host and localHostId if any.
415 * @param {remoting.HostController.State} state State of the local host.
416 * @param {string?} hostId ID of the local host, or null.
417 * @return {void} Nothing.
419 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
420 this.localHostState_ = state;
421 this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
425 * Set the host object that corresponds to the local computer, if any.
427 * @param {remoting.Host?} host The host, or null if not registered.
428 * @return {void} Nothing.
431 remoting.HostList.prototype.setLocalHost_ = function(host) {
432 this.localHost_ = host;
434 /** @type {remoting.HostList} */
437 /** @param {remoting.HostTableEntry} host */
438 var renameHost = function(host) {
439 that.renameHost_(host);
442 if (!this.localHostTableEntry_) {
443 /** @type {remoting.HostTableEntry} @private */
444 this.localHostTableEntry_ = new remoting.HostTableEntry(
445 host, this.webappMajorVersion_, renameHost);
446 this.localHostTableEntry_.init(
447 document.getElementById('this-host-connect'),
448 document.getElementById('this-host-warning'),
449 document.getElementById('this-host-name'),
450 document.getElementById('this-host-rename'));
452 // TODO(jamiewalch): This is hack to prevent multiple click handlers being
453 // registered for the same DOM elements if this method is called more than
454 // once. A better solution would be to let HostTable create the daemon row
455 // like it creates the rows for non-local hosts.
456 this.localHostTableEntry_.host = host;
459 this.localHostTableEntry_ = null;
464 * Called by the HostControlled after the local host has been started.
466 * @param {string} hostName Host name.
467 * @param {string} hostId ID of the local host.
468 * @param {string} publicKey Public key.
469 * @return {void} Nothing.
471 remoting.HostList.prototype.onLocalHostStarted = function(
472 hostName, hostId, publicKey) {
473 // Create a dummy remoting.Host instance to represent the local host.
474 // Refreshing the list is no good in general, because the directory
475 // information won't be in sync for several seconds. We don't know the
476 // host JID, but it can be missing from the cache with no ill effects.
477 // It will be refreshed if the user tries to connect to the local host,
478 // and we hope that the directory will have been updated by that point.
479 var localHost = new remoting.Host();
480 localHost.hostName = hostName;
481 // Provide a version number to avoid warning about this dummy host being
483 localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
484 localHost.hostId = hostId;
485 localHost.publicKey = publicKey;
486 localHost.status = 'ONLINE';
487 this.hosts_.push(localHost);
489 this.setLocalHost_(localHost);
493 * Called when the user clicks the button next to the error message. The action
494 * depends on the error.
498 remoting.HostList.prototype.onErrorClick_ = function() {
499 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
500 remoting.oauth2.doAuthRedirect();
502 this.refresh(remoting.updateLocalHostState);
507 * Save the host list to local storage.
509 remoting.HostList.prototype.save_ = function() {
511 items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
512 chrome.storage.local.set(items);
516 * Key name under which Me2Me hosts are cached.
518 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
520 /** @type {remoting.HostList} */
521 remoting.hostList = null;