[Chromoting] Move app-specific code out of remoting.js
[chromium-blink-merge.git] / remoting / webapp / crd / js / host_list.js
blob5d7d875471b9b4af938bbc0419f12e97b3c403dc
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  * @param {function(!remoting.Error)} onError Function to call when an error
30  *     occurs.
31  */
32 remoting.HostList = function(table, noHosts, errorMsg, errorButton,
33                              loadingIndicator, onError) {
34   /** @private {Element} */
35   this.table_ = table;
36   /**
37    * TODO(jamiewalch): This should be doable using CSS's sibling selector,
38    * but it doesn't work right now (crbug.com/135050).
39    * @private {Element}
40    */
41   this.noHosts_ = noHosts;
42   /** @private {Element} */
43   this.errorMsg_ = errorMsg;
44   /** @private {Element} */
45   this.errorButton_ = errorButton;
46   /** @private {HTMLElement} */
47   this.loadingIndicator_ = loadingIndicator;
48   this.onError_ = onError;
50   /** @private {Array<remoting.HostTableEntry>} */
51   this.hostTableEntries_ = [];
52   /** @private {Array<remoting.Host>} */
53   this.hosts_ = [];
54   /** @private {!remoting.Error} */
55   this.lastError_ = remoting.Error.none();
56   /** @private {remoting.LocalHostSection} */
57   this.localHostSection_ = new remoting.LocalHostSection(
58       /** @type {HTMLElement} */ (document.querySelector('.daemon-control')),
59       new remoting.LocalHostSection.Controller(
60           this,
61           new remoting.HostSetupDialog(remoting.hostController, onError)));
63   /** @private {number} */
64   this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
66   this.errorButton_.addEventListener('click',
67                                      this.onErrorClick_.bind(this),
68                                      false);
69   var reloadButton = this.loadingIndicator_.firstElementChild;
70   /** @type {remoting.HostList} */
71   var that = this;
72   /** @param {Event} event */
73   function refresh(event) {
74     event.preventDefault();
75     that.refresh(that.display.bind(that));
76   }
77   reloadButton.addEventListener('click', refresh, false);
80 /**
81  * Load the host-list asynchronously from local storage.
82  *
83  * @param {function():void} onDone Completion callback.
84  */
85 remoting.HostList.prototype.load = function(onDone) {
86   // Load the cache of the last host-list, if present.
87   /** @type {remoting.HostList} */
88   var that = this;
89   /** @param {Object<string>} items */
90   var storeHostList = function(items) {
91     if (items[remoting.HostList.HOSTS_KEY]) {
92       var cached = base.jsonParseSafe(items[remoting.HostList.HOSTS_KEY]);
93       if (cached) {
94         that.hosts_ = /** @type {Array<remoting.Host>} */ (cached);
95       } else {
96         console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
97       }
98     }
99     onDone();
100   };
101   chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
105  * Search the host list for a host with the specified id.
107  * @param {string} hostId The unique id of the host.
108  * @return {remoting.Host?} The host, if any.
109  */
110 remoting.HostList.prototype.getHostForId = function(hostId) {
111   for (var i = 0; i < this.hosts_.length; ++i) {
112     if (this.hosts_[i].hostId == hostId) {
113       return this.hosts_[i];
114     }
115   }
116   return null;
120  * Get the host id corresponding to the specified host name.
122  * @param {string} hostName The name of the host.
123  * @return {string?} The host id, if a host with the given name exists.
124  */
125 remoting.HostList.prototype.getHostIdForName = function(hostName) {
126   for (var i = 0; i < this.hosts_.length; ++i) {
127     if (this.hosts_[i].hostName == hostName) {
128       return this.hosts_[i].hostId;
129     }
130   }
131   return null;
135  * Query the Remoting Directory for the user's list of hosts.
137  * @param {function(boolean):void} onDone Callback invoked with true on success
138  *     or false on failure.
139  * @return {void} Nothing.
140  */
141 remoting.HostList.prototype.refresh = function(onDone) {
142   this.loadingIndicator_.classList.add('loading');
143   /** @type {remoting.HostList} */
144   var that = this;
145   /** @param {!remoting.Error} error */
146   var onError = function(error) {
147     that.lastError_ = error;
148     onDone(false);
149   };
150   remoting.hostListApi.get(this.onHostListResponse_.bind(this, onDone),
151                            onError);
155  * Handle the results of the host list request.  A success response will
156  * include a JSON-encoded list of host descriptions, which we display if we're
157  * able to successfully parse it.
159  * @param {function(boolean):void} onDone The callback passed to |refresh|.
160  * @param {Array<remoting.Host>} hosts The list of hosts for the user.
161  * @return {void} Nothing.
162  * @private
163  */
164 remoting.HostList.prototype.onHostListResponse_ = function(onDone, hosts) {
165   this.lastError_ = remoting.Error.none();
166   this.hosts_ = hosts;
167   this.sortHosts_();
168   this.save_();
169   this.loadingIndicator_.classList.remove('loading');
170   onDone(true);
174  * Sort the internal list of hosts.
176  * @suppress {reportUnknownTypes}
177  * @return {void} Nothing.
178  */
179 remoting.HostList.prototype.sortHosts_ = function() {
180   /**
181    * Sort hosts, first by ONLINE/OFFLINE status and then by host-name.
182    *
183    * @param {remoting.Host} a
184    * @param {remoting.Host} b
185    * @return {number}
186    */
187   var cmp = function(a, b) {
188     if (a.status < b.status) {
189       return 1;
190     } else if (b.status < a.status) {
191       return -1;
192     } else if (a.hostName.toLocaleLowerCase() <
193                b.hostName.toLocaleLowerCase()) {
194       return -1;
195     } else if (a.hostName.toLocaleLowerCase() >
196                b.hostName.toLocaleLowerCase()) {
197       return 1;
198     }
199     return 0;
200   };
202   this.hosts_ = this.hosts_.sort(cmp);
206  * Display the list of hosts or error condition.
208  * @return {void} Nothing.
209  */
210 remoting.HostList.prototype.display = function() {
211   this.table_.innerText = '';
212   this.errorMsg_.innerText = '';
213   this.hostTableEntries_ = [];
215   var noHostsRegistered = (this.hosts_.length == 0);
216   this.table_.hidden = noHostsRegistered;
217   this.noHosts_.hidden = !noHostsRegistered;
219   if (!this.lastError_.isNone()) {
220     l10n.localizeElementFromTag(this.errorMsg_, this.lastError_.getTag());
221     if (this.lastError_.hasTag(remoting.Error.Tag.AUTHENTICATION_FAILED)) {
222       l10n.localizeElementFromTag(this.errorButton_,
223                                   /*i18n-content*/'SIGN_IN_BUTTON');
224     } else {
225       l10n.localizeElementFromTag(this.errorButton_,
226                                   /*i18n-content*/'RETRY');
227     }
228   } else {
229     for (var i = 0; i < this.hosts_.length; ++i) {
230       /** @type {remoting.Host} */
231       var host = this.hosts_[i];
232       // Validate the entry to make sure it has all the fields we expect and is
233       // not the local host (which is displayed separately). NB: if the host has
234       // never sent a heartbeat, then there will be no jabberId.
235       if (host.hostName && host.hostId && host.status && host.publicKey &&
236           (host.hostId != this.localHostSection_.getHostId())) {
237         var hostTableEntry = new remoting.HostTableEntry(
238             this.webappMajorVersion_,
239             remoting.connectMe2Me,
240             this.renameHost.bind(this),
241             this.deleteHost_.bind(this));
242         hostTableEntry.setHost(host);
243         this.hostTableEntries_[i] = hostTableEntry;
244         this.table_.appendChild(hostTableEntry.element());
245       }
246     }
247   }
249   this.errorMsg_.parentNode.hidden = this.lastError_.isNone();
250   if (noHostsRegistered) {
251     this.showHostListEmptyMessage_(this.localHostSection_.canChangeState());
252   }
256  * Displays a message to the user when the host list is empty.
258  * @param {boolean} hostingSupported
259  * @return {void}
260  * @private
261  */
262 remoting.HostList.prototype.showHostListEmptyMessage_ = function(
263     hostingSupported) {
264   var that = this;
265   remoting.AppsV2Migration.hasHostsInV1App().then(
266     /**
267      * @param {remoting.MigrationSettings} previousIdentity
268      * @this {remoting.HostList}
269      */
270     function(previousIdentity) {
271       that.noHosts_.innerHTML = remoting.AppsV2Migration.buildMigrationTips(
272           previousIdentity.email, previousIdentity.fullName);
273     },
274     function() {
275       var buttonLabel = l10n.getTranslationOrError(
276           /*i18n-content*/'HOME_DAEMON_START_BUTTON');
277       if (hostingSupported) {
278         that.noHosts_.innerText = l10n.getTranslationOrError(
279             /*i18n-content*/'HOST_LIST_EMPTY_HOSTING_SUPPORTED',
280             [buttonLabel]);
281       } else {
282         that.noHosts_.innerText = l10n.getTranslationOrError(
283             /*i18n-content*/'HOST_LIST_EMPTY_HOSTING_UNSUPPORTED',
284             [buttonLabel]);
285       }
286     }
287   );
291  * Remove a host from the list, and deregister it.
292  * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
293  * @return {void} Nothing.
294  * @private
295  */
296 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
297   this.table_.removeChild(hostTableEntry.element());
298   var index = this.hostTableEntries_.indexOf(hostTableEntry);
299   if (index != -1) {
300     this.hostTableEntries_.splice(index, 1);
301   }
302   remoting.hostListApi.remove(hostTableEntry.host.hostId, base.doNothing,
303                               this.onError_);
307  * Prepare a host for renaming by replacing its name with an edit box.
308  * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
309  * @return {void} Nothing.
310  */
311 remoting.HostList.prototype.renameHost = function(hostTableEntry) {
312   for (var i = 0; i < this.hosts_.length; ++i) {
313     if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
314       this.hosts_[i].hostName = hostTableEntry.host.hostName;
315       break;
316     }
317   }
318   this.save_();
320   remoting.hostListApi.put(hostTableEntry.host.hostId,
321                            hostTableEntry.host.hostName,
322                            hostTableEntry.host.publicKey,
323                            function() {},
324                            this.onError_);
328  * Unregister a host.
329  * @param {string} hostId The id of the host to be removed.
330  * @param {function(void)=} opt_onDone
331  * @return {void} Nothing.
332  */
333 remoting.HostList.prototype.unregisterHostById = function(hostId, opt_onDone) {
334   var that = this;
335   var onDone = opt_onDone || base.doNothing;
337   var host = this.getHostForId(hostId);
338   if (!host) {
339     console.log('Skipping host un-registration as the host is not registered ' +
340                 'under the current account');
341     onDone();
342     return;
343   }
345   var onRemoved = function() {
346     that.refresh(function() {
347       that.display();
348       onDone();
349     });
350   };
351   remoting.hostListApi.remove(hostId, onRemoved, this.onError_);
355  * Set the state of the local host and localHostId if any.
357  * @param {remoting.HostController.State} state State of the local host.
358  * @param {string?} hostId ID of the local host, or null.
359  * @return {void} Nothing.
360  */
361 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
362   var host = hostId ? this.getHostForId(hostId) : null;
363   this.localHostSection_.setModel(host, state, !this.lastError_.isNone());
367  * Called by the HostControlled after the local host has been started.
369  * @param {string} hostName Host name.
370  * @param {string} hostId ID of the local host.
371  * @param {string} publicKey Public key.
372  * @return {void} Nothing.
373  */
374 remoting.HostList.prototype.onLocalHostStarted = function(
375     hostName, hostId, publicKey) {
376   // Create a dummy remoting.Host instance to represent the local host.
377   // Refreshing the list is no good in general, because the directory
378   // information won't be in sync for several seconds. We don't know the
379   // host JID, but it can be missing from the cache with no ill effects.
380   // It will be refreshed if the user tries to connect to the local host,
381   // and we hope that the directory will have been updated by that point.
382   var localHost = new remoting.Host();
383   localHost.hostName = hostName;
384   // Provide a version number to avoid warning about this dummy host being
385   // out-of-date.
386   localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
387   localHost.hostId = hostId;
388   localHost.publicKey = publicKey;
389   localHost.status = 'ONLINE';
390   this.hosts_.push(localHost);
391   this.save_();
392   this.localHostSection_.setModel(localHost,
393                                   remoting.HostController.State.STARTED,
394                                   !this.lastError_.isNone());
398  * Called when the user clicks the button next to the error message. The action
399  * depends on the error.
401  * @private
402  */
403 remoting.HostList.prototype.onErrorClick_ = function() {
404   if (this.lastError_.hasTag(remoting.Error.Tag.AUTHENTICATION_FAILED)) {
405     remoting.handleAuthFailureAndRelaunch();
406   } else {
407     this.refresh(remoting.updateLocalHostState);
408   }
412  * Save the host list to local storage.
413  */
414 remoting.HostList.prototype.save_ = function() {
415   var items = {};
416   items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
417   chrome.storage.local.set(items);
418   if (this.hosts_.length !== 0) {
419     remoting.AppsV2Migration.saveUserInfo();
420   }
424  * Key name under which Me2Me hosts are cached.
425  */
426 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
428 /** @type {remoting.HostList} */
429 remoting.hostList = null;