// 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. */ remoting.HostList = function(table, noHosts, errorMsg, errorButton) { /** * @type {Element} * @private */ this.table_ = table; /** * @type {Element} * @private * TODO(jamiewalch): This should be doable using CSS's sibling selector, * but it doesn't work right now (crbug.com/135050). */ this.noHosts_ = noHosts; /** * @type {Element} * @private */ this.errorMsg_ = errorMsg; /** * @type {Element} * @private */ this.errorButton_ = errorButton; /** * @type {Array.<remoting.HostTableEntry>} * @private */ this.hostTableEntries_ = []; /** * @type {Array.<remoting.Host>} * @private */ this.hosts_ = []; /** * @type {string} * @private */ this.lastError_ = ''; this.errorButton_.addEventListener('click', this.onErrorClick_.bind(this), false); // Load the cache of the last host-list, if present. var cachedStr = /** @type {string} */ (window.localStorage.getItem(remoting.HostList.HOSTS_KEY)); if (cachedStr) { var cached = jsonParseSafe(cachedStr); if (cached) { this.hosts_ = /** @type {Array} */ cached; } else { console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY); } } }; /** * 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; }; /** * Query the Remoting Directory for the user's list of hosts. * * @param {function(boolean):void} onDone Callback invoked with true on success * or false on failure. * @return {void} Nothing. */ remoting.HostList.prototype.refresh = function(onDone) { /** @param {XMLHttpRequest} xhr The response from the server. */ var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone); /** @type {remoting.HostList} */ var that = this; /** @param {string} token The OAuth2 token. */ var getHosts = function(token) { var headers = { 'Authorization': 'OAuth ' + token }; remoting.xhr.get( 'https://www.googleapis.com/chromoting/v1/@me/hosts', parseHostListResponse, '', headers); }; /** @param {remoting.Error} error */ var onError = function(error) { that.lastError_ = error; onDone(false); }; this.hosts_ = []; this.lastError_ = ''; remoting.oauth2.callWithToken(getHosts, onError); }; /** * 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 {function(boolean):void} onDone The callback passed to |refresh|. * @param {XMLHttpRequest} xhr The XHR object for the host list request. * @return {void} Nothing. * @private */ remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) { try { if (xhr.status == 200) { var response = /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText); if (response && response.data) { if (response.data.items) { this.hosts_ = response.data.items; /** * @param {remoting.Host} a * @param {remoting.Host} b */ var cmp = function(a, b) { if (a.status < b.status) { return 1; } else if (b.status < a.status) { return -1; } return 0; }; this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp); } else { this.hosts_ = []; } } else { console.error('Invalid "hosts" response from server.'); } } else { // Some other error. console.error('Bad status on host list query: ', xhr); if (xhr.status == 401) { this.lastError_ = remoting.Error.AUTHENTICATION_FAILED; } else if (xhr.status == 503) { this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE; } else { this.lastError_ = remoting.Error.UNEXPECTED; } } } catch (er) { var typed_er = /** @type {Object} */ (er); console.error('Error processing response: ', xhr, typed_er); this.lastError_ = remoting.Error.UNEXPECTED; } window.localStorage.setItem(remoting.HostList.HOSTS_KEY, JSON.stringify(this.hosts_)); onDone(this.lastError_ == ''); }; /** * Display the list of hosts or error condition. * * @param {string?} thisHostId The id of this host, or null if not registered. * @return {void} Nothing. */ remoting.HostList.prototype.display = function(thisHostId) { this.table_.innerText = ''; this.errorMsg_.innerText = ''; this.hostTableEntries_ = []; var noHostsRegistered = (this.hosts_.length == 0); this.table_.hidden = noHostsRegistered; this.noHosts_.hidden = !noHostsRegistered; 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 != thisHostId) { var hostTableEntry = new remoting.HostTableEntry(); hostTableEntry.create(host, this.renameHost.bind(this), this.deleteHost_.bind(this)); this.hostTableEntries_[i] = hostTableEntry; this.table_.appendChild(hostTableEntry.tableRow); } } if (this.lastError_ != '') { l10n.localizeElementFromTag(this.errorMsg_, this.lastError_); if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) { l10n.localizeElementFromTag(this.errorButton_, /*i18n-content*/'SIGN_IN_BUTTON'); } else { l10n.localizeElementFromTag(this.errorButton_, /*i18n-content*/'RETRY'); } } this.errorMsg_.parentNode.hidden = (this.lastError_ == ''); }; /** * 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.tableRow); var index = this.hostTableEntries_.indexOf(hostTableEntry); if (index != -1) { this.hostTableEntries_.splice(index, 1); } remoting.HostList.unregisterHostById(hostTableEntry.host.hostId); }; /** * Unregister a host. * @param {string} hostId The id of the host to be removed. * @return {void} Nothing. */ remoting.HostList.unregisterHostById = function(hostId) { /** @param {string} token The OAuth2 token. */ var deleteHost = function(token) { var headers = { 'Authorization': 'OAuth ' + token }; remoting.xhr.remove( 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostId, function() {}, '', headers); } remoting.oauth2.callWithToken(deleteHost, remoting.defaultOAuthErrorHandler); }; /** * 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; } } window.localStorage.setItem(remoting.HostList.HOSTS_KEY, JSON.stringify(this.hosts_)); /** @param {string?} token */ var renameHost = function(token) { if (token) { var headers = { 'Authorization': 'OAuth ' + token, 'Content-type' : 'application/json; charset=UTF-8' }; var newHostDetails = { data: { hostId: hostTableEntry.host.hostId, hostName: hostTableEntry.host.hostName, publicKey: hostTableEntry.host.publicKey } }; remoting.xhr.put( 'https://www.googleapis.com/chromoting/v1/@me/hosts/' + hostTableEntry.host.hostId, function(xhr) {}, JSON.stringify(newHostDetails), headers); } else { console.error('Could not rename host. Authentication failure.'); } } remoting.oauth2.callWithToken(renameHost, remoting.defaultOAuthErrorHandler); }; /** * Add a host to the list. This is called when the local host is started to * avoid having to refresh the host list and deal with replication delays. * * @param {remoting.Host} localHost The local Me2Me host. * @return {void} Nothing. */ remoting.HostList.prototype.addHost = function(localHost) { this.hosts_.push(localHost); window.localStorage.setItem(remoting.HostList.HOSTS_KEY, JSON.stringify(this.hosts_)); }; /** * 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_ == remoting.Error.AUTHENTICATION_FAILED) { remoting.oauth2.doAuthRedirect(); } else { this.lastError_ = ''; this.display(null); this.refresh(remoting.extractThisHostAndDisplay); } } /** * Key name under which Me2Me hosts are cached. */ remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts'; /** @type {remoting.HostList} */ remoting.hostList = null;