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
27 remoting
.HostList = function(table
, noHosts
, errorMsg
, errorButton
) {
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
;
44 this.errorMsg_
= errorMsg
;
49 this.errorButton_
= errorButton
;
51 * @type {Array.<remoting.HostTableEntry>}
54 this.hostTableEntries_
= [];
56 * @type {Array.<remoting.Host>}
66 * @type {remoting.Host?}
69 this.localHost_
= null;
71 * @type {remoting.HostController.State}
74 this.localHostState_
= remoting
.HostController
.State
.NOT_IMPLEMENTED
;
79 this.webappMajorVersion_
= parseInt(chrome
.runtime
.getManifest().version
, 10);
81 this.errorButton_
.addEventListener('click',
82 this.onErrorClick_
.bind(this),
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} */
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
]);
100 that
.hosts_
= /** @type {Array} */ cached
;
102 console
.error('Invalid value for ' + remoting
.HostList
.HOSTS_KEY
);
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
];
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} */
137 /** @param {string} token The OAuth2 token. */
138 var getHosts = function(token
) {
139 var headers
= { 'Authorization': 'OAuth ' + token
};
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
;
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.
162 remoting
.HostList
.prototype.parseHostListResponse_ = function(onDone
, xhr
) {
163 this.lastError_
= '';
165 if (xhr
.status
== 200) {
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
) {
178 } else if (b
.status
< a
.status
) {
183 this.hosts_
= /** @type {Array} */ this.hosts_
.sort(cmp
);
186 this.lastError_
= remoting
.Error
.UNEXPECTED
;
187 console
.error('Invalid "hosts" response from server.');
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
;
199 this.lastError_
= remoting
.Error
.UNEXPECTED
;
203 var typed_er
= /** @type {Object} */ (er
);
204 console
.error('Error processing response: ', xhr
, typed_er
);
205 this.lastError_
= remoting
.Error
.UNEXPECTED
;
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');
231 l10n
.localizeElementFromTag(this.errorButton_
,
232 /*i18n-content*/'RETRY');
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.
282 remoting
.HostList
.prototype.deleteHost_ = function(hostTableEntry
) {
283 this.table_
.removeChild(hostTableEntry
.tableRow
);
284 var index
= this.hostTableEntries_
.indexOf(hostTableEntry
);
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.
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
;
306 /** @param {string?} token */
307 var renameHost = function(token
) {
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
319 remoting
.settings
.DIRECTORY_API_BASE_URL
+ '/@me/hosts/' +
320 hostTableEntry
.host
.hostId
,
322 JSON
.stringify(newHostDetails
),
325 console
.error('Could not rename host. Authentication failure.');
328 remoting
.identity
.callWithToken(renameHost
, remoting
.showErrorMessage
);
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
};
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.
355 remoting
.HostList
.prototype.setTooltips_ = function() {
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.
384 remoting
.HostList
.prototype.setLocalHost_ = function(host
) {
385 this.localHost_
= host
;
387 /** @type {remoting.HostList} */
390 /** @param {remoting.HostTableEntry} host */
391 var renameHost = function(host
) {
392 that
.renameHost_(host
);
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'));
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
;
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
436 localHost
.hostVersion
= String(this.webappMajorVersion_
) + ".x"
437 localHost
.hostId
= hostId
;
438 localHost
.publicKey
= publicKey
;
439 localHost
.status
= 'ONLINE';
440 this.hosts_
.push(localHost
);
442 this.setLocalHost_(localHost
);
446 * Called when the user clicks the button next to the error message. The action
447 * depends on the error.
451 remoting
.HostList
.prototype.onErrorClick_ = function() {
452 if (this.lastError_
== remoting
.Error
.AUTHENTICATION_FAILED
) {
453 remoting
.oauth2
.doAuthRedirect();
455 this.refresh(remoting
.updateLocalHostState
);
460 * Save the host list to local storage.
462 remoting
.HostList
.prototype.save_ = function() {
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;