Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / remoting / webapp / host_list.js
blobce7c18ecd3e7c28b2e55a0973ffcd4e01a6b5215
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.UNKNOWN;
85   /**
86    * @type {number}
87    * @private
88    */
89   this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
91   this.errorButton_.addEventListener('click',
92                                      this.onErrorClick_.bind(this),
93                                      false);
94   var reloadButton = this.loadingIndicator_.firstElementChild;
95   /** @type {remoting.HostList} */
96   var that = this;
97   /** @param {Event} event */
98   function refresh(event) {
99     event.preventDefault();
100     that.refresh(that.display.bind(that));
101   };
102   reloadButton.addEventListener('click', refresh, false);
106  * Load the host-list asynchronously from local storage.
108  * @param {function():void} onDone Completion callback.
109  */
110 remoting.HostList.prototype.load = function(onDone) {
111   // Load the cache of the last host-list, if present.
112   /** @type {remoting.HostList} */
113   var that = this;
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]);
118       if (cached) {
119         that.hosts_ = /** @type {Array} */ cached;
120       } else {
121         console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
122       }
123     }
124     onDone();
125   };
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.
134  */
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];
139     }
140   }
141   return null;
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.
149  */
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;
154     }
155   }
156   return null;
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.
165  */
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} */
171   var that = this;
172   /** @param {string} token The OAuth2 token. */
173   var getHosts = function(token) {
174     var headers = { 'Authorization': 'OAuth ' + token };
175     remoting.xhr.get(
176         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
177         parseHostListResponse, '', headers);
178   };
179   /** @param {remoting.Error} error */
180   var onError = function(error) {
181     that.lastError_ = error;
182     onDone(false);
183   };
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.
195  * @private
196  */
197 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
198   this.lastError_ = '';
199   try {
200     if (xhr.status == 200) {
201       var response =
202           /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
203       if (response && response.data) {
204         if (response.data.items) {
205           this.hosts_ = response.data.items;
206           /**
207            * @param {remoting.Host} a
208            * @param {remoting.Host} b
209            */
210           var cmp = function(a, b) {
211             if (a.status < b.status) {
212               return 1;
213             } else if (b.status < a.status) {
214               return -1;
215             } else if (a.hostName.toLocaleLowerCase() <
216                        b.hostName.toLocaleLowerCase()) {
217               return -1;
218             } else if (a.hostName.toLocaleLowerCase() >
219                        b.hostName.toLocaleLowerCase()) {
220               return 1;
221             }
222             return 0;
223           };
224           this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
225         } else {
226           this.hosts_ = [];
227         }
228       } else {
229         this.lastError_ = remoting.Error.UNEXPECTED;
230         console.error('Invalid "hosts" response from server.');
231       }
232     } else {
233       // Some other error.
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;
241       } else {
242         this.lastError_ = remoting.Error.UNEXPECTED;
243       }
244     }
245   } catch (er) {
246     var typed_er = /** @type {Object} */ (er);
247     console.error('Error processing response: ', xhr, typed_er);
248     this.lastError_ = remoting.Error.UNEXPECTED;
249   }
250   this.save_();
251   this.loadingIndicator_.classList.remove('loading');
252   onDone(this.lastError_ == '');
256  * Display the list of hosts or error condition.
258  * @return {void} Nothing.
259  */
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');
274     } else {
275       l10n.localizeElementFromTag(this.errorButton_,
276                                   /*i18n-content*/'RETRY');
277     }
278   } else {
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);
293       }
294     }
295   }
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.
327  * @private
328  */
329 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
330   this.table_.removeChild(hostTableEntry.tableRow);
331   var index = this.hostTableEntries_.indexOf(hostTableEntry);
332   if (index != -1) {
333     this.hostTableEntries_.splice(index, 1);
334   }
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.
342  * @private
343  */
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;
348       break;
349     }
350   }
351   this.save_();
353   /** @param {string?} token */
354   var renameHost = function(token) {
355     if (token) {
356       var headers = {
357         'Authorization': 'OAuth ' + token,
358         'Content-type' : 'application/json; charset=UTF-8'
359       };
360       var newHostDetails = { data: {
361         hostId: hostTableEntry.host.hostId,
362         hostName: hostTableEntry.host.hostName,
363         publicKey: hostTableEntry.host.publicKey
364       } };
365       remoting.xhr.put(
366           remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
367           hostTableEntry.host.hostId,
368           function(xhr) {},
369           JSON.stringify(newHostDetails),
370           headers);
371     } else {
372       console.error('Could not rename host. Authentication failure.');
373     }
374   }
375   remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
379  * Unregister a host.
380  * @param {string} hostId The id of the host to be removed.
381  * @return {void} Nothing.
382  */
383 remoting.HostList.unregisterHostById = function(hostId) {
384   /** @param {string} token The OAuth2 token. */
385   var deleteHost = function(token) {
386     var headers = { 'Authorization': 'OAuth ' + token };
387     remoting.xhr.remove(
388         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
389         function() {}, '', headers);
390   }
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.
400  * @private
401  */
402 remoting.HostList.prototype.setTooltips_ = function() {
403   var connectStr = '';
404   if (this.localHost_) {
405     chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
406                            this.localHost_.hostName);
407   }
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.
418  */
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.
429  * @private
430  */
431 remoting.HostList.prototype.setLocalHost_ = function(host) {
432   this.localHost_ = host;
433   this.setTooltips_();
434   /** @type {remoting.HostList} */
435   var that = this;
436   if (host) {
437     /** @param {remoting.HostTableEntry} host */
438     var renameHost = function(host) {
439       that.renameHost_(host);
440       that.setTooltips_();
441     };
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'));
451     } else {
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;
457     }
458   } else {
459     this.localHostTableEntry_ = null;
460   }
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.
470  */
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
482   // out-of-date.
483   localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
484   localHost.hostId = hostId;
485   localHost.publicKey = publicKey;
486   localHost.status = 'ONLINE';
487   this.hosts_.push(localHost);
488   this.save_();
489   this.setLocalHost_(localHost);
493  * Called when the user clicks the button next to the error message. The action
494  * depends on the error.
496  * @private
497  */
498 remoting.HostList.prototype.onErrorClick_ = function() {
499   if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
500     remoting.oauth2.doAuthRedirect();
501   } else {
502     this.refresh(remoting.updateLocalHostState);
503   }
507  * Save the host list to local storage.
508  */
509 remoting.HostList.prototype.save_ = function() {
510   var items = {};
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.
517  */
518 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
520 /** @type {remoting.HostList} */
521 remoting.hostList = null;