Add a Connect_Remote_Host test.
[chromium-blink-merge.git] / remoting / webapp / host_list.js
blob43ce0924c293e389e97e81b27f204013c32fdb2e
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.
19  *
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.
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
28  *     the reload button.
29  */
30 remoting.HostList = function(table, noHosts, errorMsg, errorButton,
31                              loadingIndicator) {
32   /**
33    * @type {Element}
34    * @private
35    */
36   this.table_ = table;
37   /**
38    * @type {Element}
39    * @private
40    * TODO(jamiewalch): This should be doable using CSS's sibling selector,
41    * but it doesn't work right now (crbug.com/135050).
42    */
43   this.noHosts_ = noHosts;
44   /**
45    * @type {Element}
46    * @private
47    */
48   this.errorMsg_ = errorMsg;
49   /**
50    * @type {Element}
51    * @private
52    */
53   this.errorButton_ = errorButton;
54   /**
55    * @type {HTMLElement}
56    * @private
57    */
58   this.loadingIndicator_ = loadingIndicator;
59   /**
60    * @type {Array.<remoting.HostTableEntry>}
61    * @private
62    */
63   this.hostTableEntries_ = [];
64   /**
65    * @type {Array.<remoting.Host>}
66    * @private
67    */
68   this.hosts_ = [];
69   /**
70    * @type {string}
71    * @private
72    */
73   this.lastError_ = '';
74   /**
75    * @type {remoting.Host?}
76    * @private
77    */
78   this.localHost_ = null;
79   /**
80    * @type {remoting.HostController.State}
81    * @private
82    */
83   this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
84   /**
85    * @type {number}
86    * @private
87    */
88   this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
90   this.errorButton_.addEventListener('click',
91                                      this.onErrorClick_.bind(this),
92                                      false);
93   var reloadButton = this.loadingIndicator_.firstElementChild;
94   /** @type {remoting.HostList} */
95   var that = this;
96   /** @param {Event} event */
97   function refresh(event) {
98     event.preventDefault();
99     that.refresh(that.display.bind(that));
100   };
101   reloadButton.addEventListener('click', refresh, false);
105  * Load the host-list asynchronously from local storage.
107  * @param {function():void} onDone Completion callback.
108  */
109 remoting.HostList.prototype.load = function(onDone) {
110   // Load the cache of the last host-list, if present.
111   /** @type {remoting.HostList} */
112   var that = this;
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]);
117       if (cached) {
118         that.hosts_ = /** @type {Array} */ cached;
119       } else {
120         console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
121       }
122     }
123     onDone();
124   };
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.
133  */
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];
138     }
139   }
140   return null;
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.
148  */
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;
153     }
154   }
155   return null;
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.
164  */
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} */
170   var that = this;
171   /** @param {string} token The OAuth2 token. */
172   var getHosts = function(token) {
173     var headers = { 'Authorization': 'OAuth ' + token };
174     remoting.xhr.get(
175         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
176         parseHostListResponse, '', headers);
177   };
178   /** @param {remoting.Error} error */
179   var onError = function(error) {
180     that.lastError_ = error;
181     onDone(false);
182   };
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.
194  * @private
195  */
196 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
197   this.lastError_ = '';
198   try {
199     if (xhr.status == 200) {
200       var response =
201           /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
202       if (response && response.data) {
203         if (response.data.items) {
204           this.hosts_ = response.data.items;
205           /**
206            * @param {remoting.Host} a
207            * @param {remoting.Host} b
208            */
209           var cmp = function(a, b) {
210             if (a.status < b.status) {
211               return 1;
212             } else if (b.status < a.status) {
213               return -1;
214             }
215             return 0;
216           };
217           this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
218         } else {
219           this.hosts_ = [];
220         }
221       } else {
222         this.lastError_ = remoting.Error.UNEXPECTED;
223         console.error('Invalid "hosts" response from server.');
224       }
225     } else {
226       // Some other error.
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;
234       } else {
235         this.lastError_ = remoting.Error.UNEXPECTED;
236       }
237     }
238   } catch (er) {
239     var typed_er = /** @type {Object} */ (er);
240     console.error('Error processing response: ', xhr, typed_er);
241     this.lastError_ = remoting.Error.UNEXPECTED;
242   }
243   this.save_();
244   this.loadingIndicator_.classList.remove('loading');
245   onDone(this.lastError_ == '');
249  * Display the list of hosts or error condition.
251  * @return {void} Nothing.
252  */
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');
267     } else {
268       l10n.localizeElementFromTag(this.errorButton_,
269                                   /*i18n-content*/'RETRY');
270     }
271   } else {
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);
286       }
287     }
288   }
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.
317  * @private
318  */
319 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
320   this.table_.removeChild(hostTableEntry.tableRow);
321   var index = this.hostTableEntries_.indexOf(hostTableEntry);
322   if (index != -1) {
323     this.hostTableEntries_.splice(index, 1);
324   }
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.
332  * @private
333  */
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;
338       break;
339     }
340   }
341   this.save_();
343   /** @param {string?} token */
344   var renameHost = function(token) {
345     if (token) {
346       var headers = {
347         'Authorization': 'OAuth ' + token,
348         'Content-type' : 'application/json; charset=UTF-8'
349       };
350       var newHostDetails = { data: {
351         hostId: hostTableEntry.host.hostId,
352         hostName: hostTableEntry.host.hostName,
353         publicKey: hostTableEntry.host.publicKey
354       } };
355       remoting.xhr.put(
356           remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
357           hostTableEntry.host.hostId,
358           function(xhr) {},
359           JSON.stringify(newHostDetails),
360           headers);
361     } else {
362       console.error('Could not rename host. Authentication failure.');
363     }
364   }
365   remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
369  * Unregister a host.
370  * @param {string} hostId The id of the host to be removed.
371  * @return {void} Nothing.
372  */
373 remoting.HostList.unregisterHostById = function(hostId) {
374   /** @param {string} token The OAuth2 token. */
375   var deleteHost = function(token) {
376     var headers = { 'Authorization': 'OAuth ' + token };
377     remoting.xhr.remove(
378         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
379         function() {}, '', headers);
380   }
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.
390  * @private
391  */
392 remoting.HostList.prototype.setTooltips_ = function() {
393   var connectStr = '';
394   if (this.localHost_) {
395     chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
396                            this.localHost_.hostName);
397   }
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.
408  */
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.
419  * @private
420  */
421 remoting.HostList.prototype.setLocalHost_ = function(host) {
422   this.localHost_ = host;
423   this.setTooltips_();
424   /** @type {remoting.HostList} */
425   var that = this;
426   if (host) {
427     /** @param {remoting.HostTableEntry} host */
428     var renameHost = function(host) {
429       that.renameHost_(host);
430       that.setTooltips_();
431     };
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'));
441     } else {
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;
447     }
448   } else {
449     this.localHostTableEntry_ = null;
450   }
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.
460  */
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
472   // out-of-date.
473   localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
474   localHost.hostId = hostId;
475   localHost.publicKey = publicKey;
476   localHost.status = 'ONLINE';
477   this.hosts_.push(localHost);
478   this.save_();
479   this.setLocalHost_(localHost);
483  * Called when the user clicks the button next to the error message. The action
484  * depends on the error.
486  * @private
487  */
488 remoting.HostList.prototype.onErrorClick_ = function() {
489   if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
490     remoting.oauth2.doAuthRedirect();
491   } else {
492     this.refresh(remoting.updateLocalHostState);
493   }
497  * Save the host list to local storage.
498  */
499 remoting.HostList.prototype.save_ = function() {
500   var items = {};
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.
507  */
508 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
510 /** @type {remoting.HostList} */
511 remoting.hostList = null;