Separate Simple Backend creation from initialization.
[chromium-blink-merge.git] / remoting / webapp / host_list.js
blobf1282d546c7ca2318a4513d32c7f0ab1295fa8ac
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.
27 remoting.HostList = function(table, noHosts, errorMsg, errorButton) {
28 /**
29 * @type {Element}
30 * @private
32 this.table_ = table;
33 /**
34 * @type {Element}
35 * @private
36 * TODO(jamiewalch): This should be doable using CSS's sibling selector,
37 * but it doesn't work right now (crbug.com/135050).
39 this.noHosts_ = noHosts;
40 /**
41 * @type {Element}
42 * @private
44 this.errorMsg_ = errorMsg;
45 /**
46 * @type {Element}
47 * @private
49 this.errorButton_ = errorButton;
50 /**
51 * @type {Array.<remoting.HostTableEntry>}
52 * @private
54 this.hostTableEntries_ = [];
55 /**
56 * @type {Array.<remoting.Host>}
57 * @private
59 this.hosts_ = [];
60 /**
61 * @type {string}
62 * @private
64 this.lastError_ = '';
65 /**
66 * @type {remoting.Host?}
67 * @private
69 this.localHost_ = null;
70 /**
71 * @type {remoting.HostController.State}
72 * @private
74 this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
75 /**
76 * @type {number}
77 * @private
79 this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
81 this.errorButton_.addEventListener('click',
82 this.onErrorClick_.bind(this),
83 false);
86 /**
87 * Load the host-list asynchronously from local storage.
89 * @param {function():void} onDone Completion callback.
91 remoting.HostList.prototype.load = function(onDone) {
92 // Load the cache of the last host-list, if present.
93 /** @type {remoting.HostList} */
94 var that = this;
95 /** @param {Object.<string>} items */
96 var storeHostList = function(items) {
97 if (items[remoting.HostList.HOSTS_KEY]) {
98 var cached = jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
99 if (cached) {
100 that.hosts_ = /** @type {Array} */ cached;
101 } else {
102 console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
105 onDone();
107 remoting.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
111 * Search the host list for a host with the specified id.
113 * @param {string} hostId The unique id of the host.
114 * @return {remoting.Host?} The host, if any.
116 remoting.HostList.prototype.getHostForId = function(hostId) {
117 for (var i = 0; i < this.hosts_.length; ++i) {
118 if (this.hosts_[i].hostId == hostId) {
119 return this.hosts_[i];
122 return null;
126 * Query the Remoting Directory for the user's list of hosts.
128 * @param {function(boolean):void} onDone Callback invoked with true on success
129 * or false on failure.
130 * @return {void} Nothing.
132 remoting.HostList.prototype.refresh = function(onDone) {
133 /** @param {XMLHttpRequest} xhr The response from the server. */
134 var parseHostListResponse = this.parseHostListResponse_.bind(this, onDone);
135 /** @type {remoting.HostList} */
136 var that = this;
137 /** @param {string} token The OAuth2 token. */
138 var getHosts = function(token) {
139 var headers = { 'Authorization': 'OAuth ' + token };
140 remoting.xhr.get(
141 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
142 parseHostListResponse, '', headers);
144 /** @param {remoting.Error} error */
145 var onError = function(error) {
146 that.lastError_ = error;
147 onDone(false);
149 remoting.identity.callWithToken(getHosts, onError);
153 * Handle the results of the host list request. A success response will
154 * include a JSON-encoded list of host descriptions, which we display if we're
155 * able to successfully parse it.
157 * @param {function(boolean):void} onDone The callback passed to |refresh|.
158 * @param {XMLHttpRequest} xhr The XHR object for the host list request.
159 * @return {void} Nothing.
160 * @private
162 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
163 this.lastError_ = '';
164 try {
165 if (xhr.status == 200) {
166 var response =
167 /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
168 if (response && response.data) {
169 if (response.data.items) {
170 this.hosts_ = response.data.items;
172 * @param {remoting.Host} a
173 * @param {remoting.Host} b
175 var cmp = function(a, b) {
176 if (a.status < b.status) {
177 return 1;
178 } else if (b.status < a.status) {
179 return -1;
181 return 0;
183 this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
185 } else {
186 this.lastError_ = remoting.Error.UNEXPECTED;
187 console.error('Invalid "hosts" response from server.');
189 } else {
190 // Some other error.
191 console.error('Bad status on host list query: ', xhr);
192 if (xhr.status == 0) {
193 this.lastError_ = remoting.Error.NETWORK_FAILURE;
194 } else if (xhr.status == 401) {
195 this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
196 } else if (xhr.status == 502 || xhr.status == 503) {
197 this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
198 } else {
199 this.lastError_ = remoting.Error.UNEXPECTED;
202 } catch (er) {
203 var typed_er = /** @type {Object} */ (er);
204 console.error('Error processing response: ', xhr, typed_er);
205 this.lastError_ = remoting.Error.UNEXPECTED;
207 this.save_();
208 onDone(this.lastError_ == '');
212 * Display the list of hosts or error condition.
214 * @return {void} Nothing.
216 remoting.HostList.prototype.display = function() {
217 this.table_.innerText = '';
218 this.errorMsg_.innerText = '';
219 this.hostTableEntries_ = [];
221 var noHostsRegistered = (this.hosts_.length == 0);
222 this.table_.hidden = noHostsRegistered;
223 this.noHosts_.hidden = !noHostsRegistered;
225 if (this.lastError_ != '') {
226 l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
227 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
228 l10n.localizeElementFromTag(this.errorButton_,
229 /*i18n-content*/'SIGN_IN_BUTTON');
230 } else {
231 l10n.localizeElementFromTag(this.errorButton_,
232 /*i18n-content*/'RETRY');
234 } else {
235 for (var i = 0; i < this.hosts_.length; ++i) {
236 /** @type {remoting.Host} */
237 var host = this.hosts_[i];
238 // Validate the entry to make sure it has all the fields we expect and is
239 // not the local host (which is displayed separately). NB: if the host has
240 // never sent a heartbeat, then there will be no jabberId.
241 if (host.hostName && host.hostId && host.status && host.publicKey &&
242 (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
243 var hostTableEntry = new remoting.HostTableEntry(
244 host, this.webappMajorVersion_,
245 this.renameHost_.bind(this), this.deleteHost_.bind(this));
246 hostTableEntry.createDom();
247 this.hostTableEntries_[i] = hostTableEntry;
248 this.table_.appendChild(hostTableEntry.tableRow);
253 this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
255 // The local host cannot be stopped or started if the host controller is not
256 // implemented for this platform. Additionally, it cannot be started if there
257 // is an error (in many error states, the start operation will fail anyway,
258 // but even if it succeeds, the chance of a related but hard-to-diagnose
259 // future error is high).
260 var state = this.localHostState_;
261 var enabled = (state == remoting.HostController.State.STARTING) ||
262 (state == remoting.HostController.State.STARTED);
263 var canChangeLocalHostState =
264 (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
265 (enabled || this.lastError_ == '');
267 remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
268 var element = document.getElementById('daemon-control');
269 element.hidden = !canChangeLocalHostState;
270 element = document.getElementById('host-list-empty-hosting-supported');
271 element.hidden = !canChangeLocalHostState;
272 element = document.getElementById('host-list-empty-hosting-unsupported');
273 element.hidden = canChangeLocalHostState;
277 * Remove a host from the list, and deregister it.
278 * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
279 * @return {void} Nothing.
280 * @private
282 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
283 this.table_.removeChild(hostTableEntry.tableRow);
284 var index = this.hostTableEntries_.indexOf(hostTableEntry);
285 if (index != -1) {
286 this.hostTableEntries_.splice(index, 1);
288 remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
292 * Prepare a host for renaming by replacing its name with an edit box.
293 * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
294 * @return {void} Nothing.
295 * @private
297 remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
298 for (var i = 0; i < this.hosts_.length; ++i) {
299 if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
300 this.hosts_[i].hostName = hostTableEntry.host.hostName;
301 break;
304 this.save_();
306 /** @param {string?} token */
307 var renameHost = function(token) {
308 if (token) {
309 var headers = {
310 'Authorization': 'OAuth ' + token,
311 'Content-type' : 'application/json; charset=UTF-8'
313 var newHostDetails = { data: {
314 hostId: hostTableEntry.host.hostId,
315 hostName: hostTableEntry.host.hostName,
316 publicKey: hostTableEntry.host.publicKey
317 } };
318 remoting.xhr.put(
319 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
320 hostTableEntry.host.hostId,
321 function(xhr) {},
322 JSON.stringify(newHostDetails),
323 headers);
324 } else {
325 console.error('Could not rename host. Authentication failure.');
328 remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
332 * Unregister a host.
333 * @param {string} hostId The id of the host to be removed.
334 * @return {void} Nothing.
336 remoting.HostList.unregisterHostById = function(hostId) {
337 /** @param {string} token The OAuth2 token. */
338 var deleteHost = function(token) {
339 var headers = { 'Authorization': 'OAuth ' + token };
340 remoting.xhr.remove(
341 remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
342 function() {}, '', headers);
344 remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
348 * Set tool-tips for the 'connect' action. We can't just set this on the
349 * parent element because the button has no tool-tip, and therefore would
350 * inherit connectStr.
352 * @return {void} Nothing.
353 * @private
355 remoting.HostList.prototype.setTooltips_ = function() {
356 var connectStr = '';
357 if (this.localHost_) {
358 chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
359 this.localHost_.hostName);
361 document.getElementById('this-host-name').title = connectStr;
362 document.getElementById('this-host-icon').title = connectStr;
366 * Set the state of the local host and localHostId if any.
368 * @param {remoting.HostController.State} state State of the local host.
369 * @param {string?} hostId ID of the local host, or null.
370 * @return {void} Nothing.
372 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
373 this.localHostState_ = state;
374 this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
378 * Set the host object that corresponds to the local computer, if any.
380 * @param {remoting.Host?} host The host, or null if not registered.
381 * @return {void} Nothing.
382 * @private
384 remoting.HostList.prototype.setLocalHost_ = function(host) {
385 this.localHost_ = host;
386 this.setTooltips_();
387 /** @type {remoting.HostList} */
388 var that = this;
389 if (host) {
390 /** @param {remoting.HostTableEntry} host */
391 var renameHost = function(host) {
392 that.renameHost_(host);
393 that.setTooltips_();
395 if (!this.localHostTableEntry_) {
396 /** @type {remoting.HostTableEntry} @private */
397 this.localHostTableEntry_ = new remoting.HostTableEntry(
398 host, this.webappMajorVersion_, renameHost);
399 this.localHostTableEntry_.init(
400 document.getElementById('this-host-connect'),
401 document.getElementById('this-host-warning'),
402 document.getElementById('this-host-name'),
403 document.getElementById('this-host-rename'));
404 } else {
405 // TODO(jamiewalch): This is hack to prevent multiple click handlers being
406 // registered for the same DOM elements if this method is called more than
407 // once. A better solution would be to let HostTable create the daemon row
408 // like it creates the rows for non-local hosts.
409 this.localHostTableEntry_.host = host;
411 } else {
412 this.localHostTableEntry_ = null;
417 * Called by the HostControlled after the local host has been started.
419 * @param {string} hostName Host name.
420 * @param {string} hostId ID of the local host.
421 * @param {string} publicKey Public key.
422 * @return {void} Nothing.
424 remoting.HostList.prototype.onLocalHostStarted = function(
425 hostName, hostId, publicKey) {
426 // Create a dummy remoting.Host instance to represent the local host.
427 // Refreshing the list is no good in general, because the directory
428 // information won't be in sync for several seconds. We don't know the
429 // host JID, but it can be missing from the cache with no ill effects.
430 // It will be refreshed if the user tries to connect to the local host,
431 // and we hope that the directory will have been updated by that point.
432 var localHost = new remoting.Host();
433 localHost.hostName = hostName;
434 // Provide a version number to avoid warning about this dummy host being
435 // out-of-date.
436 localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
437 localHost.hostId = hostId;
438 localHost.publicKey = publicKey;
439 localHost.status = 'ONLINE';
440 this.hosts_.push(localHost);
441 this.save_();
442 this.setLocalHost_(localHost);
446 * Called when the user clicks the button next to the error message. The action
447 * depends on the error.
449 * @private
451 remoting.HostList.prototype.onErrorClick_ = function() {
452 if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
453 remoting.oauth2.doAuthRedirect();
454 } else {
455 this.refresh(remoting.updateLocalHostState);
460 * Save the host list to local storage.
462 remoting.HostList.prototype.save_ = function() {
463 var items = {};
464 items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
465 remoting.storage.local.set(items);
469 * Key name under which Me2Me hosts are cached.
471 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
473 /** @type {remoting.HostList} */
474 remoting.hostList = null;