| // 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. |
| * @param {HTMLElement} loadingIndicator The HTML <span> to update while the |
| * host list is being loaded. The first element of this span should be |
| * the reload button. |
| */ |
| remoting.HostList = function(table, noHosts, errorMsg, errorButton, |
| loadingIndicator) { |
| /** |
| * @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 {HTMLElement} |
| * @private |
| */ |
| this.loadingIndicator_ = loadingIndicator; |
| /** |
| * @type {Array.<remoting.HostTableEntry>} |
| * @private |
| */ |
| this.hostTableEntries_ = []; |
| /** |
| * @type {Array.<remoting.Host>} |
| * @private |
| */ |
| this.hosts_ = []; |
| /** |
| * @type {string} |
| * @private |
| */ |
| this.lastError_ = ''; |
| /** |
| * @type {remoting.Host?} |
| * @private |
| */ |
| this.localHost_ = null; |
| /** |
| * @type {remoting.HostController.State} |
| * @private |
| */ |
| this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED; |
| /** |
| * @type {number} |
| * @private |
| */ |
| this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10); |
| |
| this.errorButton_.addEventListener('click', |
| this.onErrorClick_.bind(this), |
| false); |
| var reloadButton = this.loadingIndicator_.firstElementChild; |
| /** @type {remoting.HostList} */ |
| var that = this; |
| /** @param {Event} event */ |
| function refresh(event) { |
| event.preventDefault(); |
| that.refresh(that.display.bind(that)); |
| }; |
| reloadButton.addEventListener('click', refresh, false); |
| }; |
| |
| /** |
| * Load the host-list asynchronously from local storage. |
| * |
| * @param {function():void} onDone Completion callback. |
| */ |
| remoting.HostList.prototype.load = function(onDone) { |
| // Load the cache of the last host-list, if present. |
| /** @type {remoting.HostList} */ |
| var that = this; |
| /** @param {Object.<string>} items */ |
| var storeHostList = function(items) { |
| if (items[remoting.HostList.HOSTS_KEY]) { |
| var cached = jsonParseSafe(items[remoting.HostList.HOSTS_KEY]); |
| if (cached) { |
| that.hosts_ = /** @type {Array} */ cached; |
| } else { |
| console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY); |
| } |
| } |
| onDone(); |
| }; |
| chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList); |
| }; |
| |
| /** |
| * 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; |
| }; |
| |
| /** |
| * Get the host id corresponding to the specified host name. |
| * |
| * @param {string} hostName The name of the host. |
| * @return {string?} The host id, if a host with the given name exists. |
| */ |
| remoting.HostList.prototype.getHostIdForName = function(hostName) { |
| for (var i = 0; i < this.hosts_.length; ++i) { |
| if (this.hosts_[i].hostName == hostName) { |
| return this.hosts_[i].hostId; |
| } |
| } |
| 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) { |
| this.loadingIndicator_.classList.add('loading'); |
| /** @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( |
| remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts', |
| parseHostListResponse, '', headers); |
| }; |
| /** @param {remoting.Error} error */ |
| var onError = function(error) { |
| that.lastError_ = error; |
| onDone(false); |
| }; |
| remoting.identity.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) { |
| this.lastError_ = ''; |
| 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 { |
| this.lastError_ = remoting.Error.UNEXPECTED; |
| console.error('Invalid "hosts" response from server.'); |
| } |
| } else { |
| // Some other error. |
| console.error('Bad status on host list query: ', xhr); |
| if (xhr.status == 0) { |
| this.lastError_ = remoting.Error.NETWORK_FAILURE; |
| } else if (xhr.status == 401) { |
| this.lastError_ = remoting.Error.AUTHENTICATION_FAILED; |
| } else if (xhr.status == 502 || 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; |
| } |
| this.save_(); |
| this.loadingIndicator_.classList.remove('loading'); |
| onDone(this.lastError_ == ''); |
| }; |
| |
| /** |
| * Display the list of hosts or error condition. |
| * |
| * @return {void} Nothing. |
| */ |
| remoting.HostList.prototype.display = function() { |
| this.table_.innerText = ''; |
| this.errorMsg_.innerText = ''; |
| this.hostTableEntries_ = []; |
| |
| var noHostsRegistered = (this.hosts_.length == 0); |
| this.table_.hidden = noHostsRegistered; |
| this.noHosts_.hidden = !noHostsRegistered; |
| |
| 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'); |
| } |
| } else { |
| 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 && |
| (!this.localHost_ || host.hostId != this.localHost_.hostId)) { |
| var hostTableEntry = new remoting.HostTableEntry( |
| host, this.webappMajorVersion_, |
| this.renameHost_.bind(this), this.deleteHost_.bind(this)); |
| hostTableEntry.createDom(); |
| this.hostTableEntries_[i] = hostTableEntry; |
| this.table_.appendChild(hostTableEntry.tableRow); |
| } |
| } |
| } |
| |
| this.errorMsg_.parentNode.hidden = (this.lastError_ == ''); |
| |
| // The local host cannot be stopped or started if the host controller is not |
| // implemented for this platform. Additionally, it cannot be started if there |
| // is an error (in many error states, the start operation will fail anyway, |
| // but even if it succeeds, the chance of a related but hard-to-diagnose |
| // future error is high). |
| var state = this.localHostState_; |
| var enabled = (state == remoting.HostController.State.STARTING) || |
| (state == remoting.HostController.State.STARTED); |
| var canChangeLocalHostState = |
| (state != remoting.HostController.State.NOT_IMPLEMENTED) && |
| (enabled || this.lastError_ == ''); |
| |
| remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state'); |
| var element = document.getElementById('daemon-control'); |
| element.hidden = !canChangeLocalHostState; |
| element = document.getElementById('host-list-empty-hosting-supported'); |
| element.hidden = !canChangeLocalHostState; |
| element = document.getElementById('host-list-empty-hosting-unsupported'); |
| element.hidden = canChangeLocalHostState; |
| }; |
| |
| /** |
| * 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); |
| }; |
| |
| /** |
| * 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. |
| * @private |
| */ |
| 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; |
| } |
| } |
| this.save_(); |
| |
| /** @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( |
| remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + |
| hostTableEntry.host.hostId, |
| function(xhr) {}, |
| JSON.stringify(newHostDetails), |
| headers); |
| } else { |
| console.error('Could not rename host. Authentication failure.'); |
| } |
| } |
| remoting.identity.callWithToken(renameHost, remoting.showErrorMessage); |
| }; |
| |
| /** |
| * 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( |
| remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId, |
| function() {}, '', headers); |
| } |
| remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage); |
| }; |
| |
| /** |
| * Set tool-tips for the 'connect' action. We can't just set this on the |
| * parent element because the button has no tool-tip, and therefore would |
| * inherit connectStr. |
| * |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.HostList.prototype.setTooltips_ = function() { |
| var connectStr = ''; |
| if (this.localHost_) { |
| chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT', |
| this.localHost_.hostName); |
| } |
| document.getElementById('this-host-name').title = connectStr; |
| document.getElementById('this-host-icon').title = connectStr; |
| }; |
| |
| /** |
| * Set the state of the local host and localHostId if any. |
| * |
| * @param {remoting.HostController.State} state State of the local host. |
| * @param {string?} hostId ID of the local host, or null. |
| * @return {void} Nothing. |
| */ |
| remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) { |
| this.localHostState_ = state; |
| this.setLocalHost_(hostId ? this.getHostForId(hostId) : null); |
| } |
| |
| /** |
| * Set the host object that corresponds to the local computer, if any. |
| * |
| * @param {remoting.Host?} host The host, or null if not registered. |
| * @return {void} Nothing. |
| * @private |
| */ |
| remoting.HostList.prototype.setLocalHost_ = function(host) { |
| this.localHost_ = host; |
| this.setTooltips_(); |
| /** @type {remoting.HostList} */ |
| var that = this; |
| if (host) { |
| /** @param {remoting.HostTableEntry} host */ |
| var renameHost = function(host) { |
| that.renameHost_(host); |
| that.setTooltips_(); |
| }; |
| if (!this.localHostTableEntry_) { |
| /** @type {remoting.HostTableEntry} @private */ |
| this.localHostTableEntry_ = new remoting.HostTableEntry( |
| host, this.webappMajorVersion_, renameHost); |
| this.localHostTableEntry_.init( |
| document.getElementById('this-host-connect'), |
| document.getElementById('this-host-warning'), |
| document.getElementById('this-host-name'), |
| document.getElementById('this-host-rename')); |
| } else { |
| // TODO(jamiewalch): This is hack to prevent multiple click handlers being |
| // registered for the same DOM elements if this method is called more than |
| // once. A better solution would be to let HostTable create the daemon row |
| // like it creates the rows for non-local hosts. |
| this.localHostTableEntry_.host = host; |
| } |
| } else { |
| this.localHostTableEntry_ = null; |
| } |
| } |
| |
| /** |
| * Called by the HostControlled after the local host has been started. |
| * |
| * @param {string} hostName Host name. |
| * @param {string} hostId ID of the local host. |
| * @param {string} publicKey Public key. |
| * @return {void} Nothing. |
| */ |
| remoting.HostList.prototype.onLocalHostStarted = function( |
| hostName, hostId, publicKey) { |
| // Create a dummy remoting.Host instance to represent the local host. |
| // Refreshing the list is no good in general, because the directory |
| // information won't be in sync for several seconds. We don't know the |
| // host JID, but it can be missing from the cache with no ill effects. |
| // It will be refreshed if the user tries to connect to the local host, |
| // and we hope that the directory will have been updated by that point. |
| var localHost = new remoting.Host(); |
| localHost.hostName = hostName; |
| // Provide a version number to avoid warning about this dummy host being |
| // out-of-date. |
| localHost.hostVersion = String(this.webappMajorVersion_) + ".x" |
| localHost.hostId = hostId; |
| localHost.publicKey = publicKey; |
| localHost.status = 'ONLINE'; |
| this.hosts_.push(localHost); |
| this.save_(); |
| this.setLocalHost_(localHost); |
| }; |
| |
| /** |
| * 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.refresh(remoting.updateLocalHostState); |
| } |
| }; |
| |
| /** |
| * Save the host list to local storage. |
| */ |
| remoting.HostList.prototype.save_ = function() { |
| var items = {}; |
| items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_); |
| chrome.storage.local.set(items); |
| }; |
| |
| /** |
| * Key name under which Me2Me hosts are cached. |
| */ |
| remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts'; |
| |
| /** @type {remoting.HostList} */ |
| remoting.hostList = null; |