Home | History | Annotate | Download | only in webapp
      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.
      4 
      5 /**
      6  * @fileoverview
      7  * Class representing the host-list portion of the home screen UI.
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     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;
     84 
     85   /**
     86    * @type {number}
     87    * @private
     88    */
     89   this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
     90 
     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);
    103 };
    104 
    105 /**
    106  * Load the host-list asynchronously from local storage.
    107  *
    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);
    127 };
    128 
    129 /**
    130  * Search the host list for a host with the specified id.
    131  *
    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;
    142 };
    143 
    144 /**
    145  * Get the host id corresponding to the specified host name.
    146  *
    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;
    157 };
    158 
    159 /**
    160  * Query the Remoting Directory for the user's list of hosts.
    161  *
    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);
    185 };
    186 
    187 /**
    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.
    191  *
    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_ == '');
    253 };
    254 
    255 /**
    256  * Display the list of hosts or error condition.
    257  *
    258  * @return {void} Nothing.
    259  */
    260 remoting.HostList.prototype.display = function() {
    261   this.table_.innerText = '';
    262   this.errorMsg_.innerText = '';
    263   this.hostTableEntries_ = [];
    264 
    265   var noHostsRegistered = (this.hosts_.length == 0);
    266   this.table_.hidden = noHostsRegistered;
    267   this.noHosts_.hidden = !noHostsRegistered;
    268 
    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   }
    296 
    297   this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
    298 
    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_ == '');
    313 
    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;
    321 };
    322 
    323 /**
    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);
    336 };
    337 
    338 /**
    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_();
    352 
    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);
    376 };
    377 
    378 /**
    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);
    392 };
    393 
    394 /**
    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.
    398  *
    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;
    410 };
    411 
    412 /**
    413  * Set the state of the local host and localHostId if any.
    414  *
    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);
    422 }
    423 
    424 /**
    425  * Set the host object that corresponds to the local computer, if any.
    426  *
    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   }
    461 }
    462 
    463 /**
    464  * Called by the HostControlled after the local host has been started.
    465  *
    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);
    490 };
    491 
    492 /**
    493  * Called when the user clicks the button next to the error message. The action
    494  * depends on the error.
    495  *
    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   }
    504 };
    505 
    506 /**
    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);
    513 };
    514 
    515 /**
    516  * Key name under which Me2Me hosts are cached.
    517  */
    518 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
    519 
    520 /** @type {remoting.HostList} */
    521 remoting.hostList = null;
    522