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.
7 * Class representing the host-list portion of the home screen UI.
12 /** @suppress {duplicate} */
13 var remoting
= remoting
|| {};
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.
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
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
30 remoting
.HostList = function(table
, noHosts
, errorMsg
, errorButton
,
40 * TODO(jamiewalch): This should be doable using CSS's sibling selector,
41 * but it doesn't work right now (crbug.com/135050).
43 this.noHosts_
= noHosts
;
48 this.errorMsg_
= errorMsg
;
53 this.errorButton_
= errorButton
;
58 this.loadingIndicator_
= loadingIndicator
;
60 * @type {Array.<remoting.HostTableEntry>}
63 this.hostTableEntries_
= [];
65 * @type {Array.<remoting.Host>}
75 * @type {remoting.Host?}
78 this.localHost_
= null;
80 * @type {remoting.HostController.State}
83 this.localHostState_
= remoting
.HostController
.State
.NOT_IMPLEMENTED
;
88 this.webappMajorVersion_
= parseInt(chrome
.runtime
.getManifest().version
, 10);
90 this.errorButton_
.addEventListener('click',
91 this.onErrorClick_
.bind(this),
93 var reloadButton
= this.loadingIndicator_
.firstElementChild
;
94 /** @type {remoting.HostList} */
96 /** @param {Event} event */
97 function refresh(event
) {
98 event
.preventDefault();
99 that
.refresh(that
.display
.bind(that
));
101 reloadButton
.addEventListener('click', refresh
, false);
105 * Load the host-list asynchronously from local storage.
107 * @param {function():void} onDone Completion callback.
109 remoting
.HostList
.prototype.load = function(onDone
) {
110 // Load the cache of the last host-list, if present.
111 /** @type {remoting.HostList} */
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
]);
118 that
.hosts_
= /** @type {Array} */ cached
;
120 console
.error('Invalid value for ' + remoting
.HostList
.HOSTS_KEY
);
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.
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
];
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.
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
;
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.
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} */
171 /** @param {string} token The OAuth2 token. */
172 var getHosts = function(token
) {
173 var headers
= { 'Authorization': 'OAuth ' + token
};
175 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/@me/hosts',
176 parseHostListResponse
, '', headers
);
178 /** @param {remoting.Error} error */
179 var onError = function(error
) {
180 that
.lastError_
= error
;
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.
196 remoting
.HostList
.prototype.parseHostListResponse_ = function(onDone
, xhr
) {
197 this.lastError_
= '';
199 if (xhr
.status
== 200) {
201 /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr
.responseText
);
202 if (response
&& response
.data
) {
203 if (response
.data
.items
) {
204 this.hosts_
= response
.data
.items
;
206 * @param {remoting.Host} a
207 * @param {remoting.Host} b
209 var cmp = function(a
, b
) {
210 if (a
.status
< b
.status
) {
212 } else if (b
.status
< a
.status
) {
217 this.hosts_
= /** @type {Array} */ this.hosts_
.sort(cmp
);
222 this.lastError_
= remoting
.Error
.UNEXPECTED
;
223 console
.error('Invalid "hosts" response from server.');
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
;
235 this.lastError_
= remoting
.Error
.UNEXPECTED
;
239 var typed_er
= /** @type {Object} */ (er
);
240 console
.error('Error processing response: ', xhr
, typed_er
);
241 this.lastError_
= remoting
.Error
.UNEXPECTED
;
244 this.loadingIndicator_
.classList
.remove('loading');
245 onDone(this.lastError_
== '');
249 * Display the list of hosts or error condition.
251 * @return {void} Nothing.
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');
268 l10n
.localizeElementFromTag(this.errorButton_
,
269 /*i18n-content*/'RETRY');
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
);
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.
319 remoting
.HostList
.prototype.deleteHost_ = function(hostTableEntry
) {
320 this.table_
.removeChild(hostTableEntry
.tableRow
);
321 var index
= this.hostTableEntries_
.indexOf(hostTableEntry
);
323 this.hostTableEntries_
.splice(index
, 1);
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.
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
;
343 /** @param {string?} token */
344 var renameHost = function(token
) {
347 'Authorization': 'OAuth ' + token
,
348 'Content-type' : 'application/json; charset=UTF-8'
350 var newHostDetails
= { data
: {
351 hostId
: hostTableEntry
.host
.hostId
,
352 hostName
: hostTableEntry
.host
.hostName
,
353 publicKey
: hostTableEntry
.host
.publicKey
356 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/@me/hosts/' +
357 hostTableEntry
.host
.hostId
,
359 JSON
.stringify(newHostDetails
),
362 console
.error('Could not rename host. Authentication failure.');
365 remoting
.identity
.callWithToken(renameHost
, remoting
.showErrorMessage
);
370 * @param {string} hostId The id of the host to be removed.
371 * @return {void} Nothing.
373 remoting
.HostList
.unregisterHostById = function(hostId
) {
374 /** @param {string} token The OAuth2 token. */
375 var deleteHost = function(token
) {
376 var headers
= { 'Authorization': 'OAuth ' + token
};
378 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/@me/hosts/' + hostId
,
379 function() {}, '', headers
);
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.
392 remoting
.HostList
.prototype.setTooltips_ = function() {
394 if (this.localHost_
) {
395 chrome
.i18n
.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
396 this.localHost_
.hostName
);
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.
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.
421 remoting
.HostList
.prototype.setLocalHost_ = function(host
) {
422 this.localHost_
= host
;
424 /** @type {remoting.HostList} */
427 /** @param {remoting.HostTableEntry} host */
428 var renameHost = function(host
) {
429 that
.renameHost_(host
);
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'));
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
;
449 this.localHostTableEntry_
= null;
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.
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
473 localHost
.hostVersion
= String(this.webappMajorVersion_
) + ".x"
474 localHost
.hostId
= hostId
;
475 localHost
.publicKey
= publicKey
;
476 localHost
.status
= 'ONLINE';
477 this.hosts_
.push(localHost
);
479 this.setLocalHost_(localHost
);
483 * Called when the user clicks the button next to the error message. The action
484 * depends on the error.
488 remoting
.HostList
.prototype.onErrorClick_ = function() {
489 if (this.lastError_
== remoting
.Error
.AUTHENTICATION_FAILED
) {
490 remoting
.oauth2
.doAuthRedirect();
492 this.refresh(remoting
.updateLocalHostState
);
497 * Save the host list to local storage.
499 remoting
.HostList
.prototype.save_ = function() {
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.
508 remoting
.HostList
.HOSTS_KEY
= 'me2me-cached-hosts';
510 /** @type {remoting.HostList} */
511 remoting
.hostList
= null;