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  */
     27 remoting.HostList = function(table, noHosts, errorMsg, errorButton) {
     28   /**
     29    * @type {Element}
     30    * @private
     31    */
     32   this.table_ = table;
     33   /**
     34    * @type {Element}
     35    * @private
     36    * TODO(jamiewalch): This should be doable using CSS's sibling selector,
     37    * but it doesn't work right now (crbug.com/135050).
     38    */
     39   this.noHosts_ = noHosts;
     40   /**
     41    * @type {Element}
     42    * @private
     43    */
     44   this.errorMsg_ = errorMsg;
     45   /**
     46    * @type {Element}
     47    * @private
     48    */
     49   this.errorButton_ = errorButton;
     50   /**
     51    * @type {Array.<remoting.HostTableEntry>}
     52    * @private
     53    */
     54   this.hostTableEntries_ = [];
     55   /**
     56    * @type {Array.<remoting.Host>}
     57    * @private
     58    */
     59   this.hosts_ = [];
     60   /**
     61    * @type {string}
     62    * @private
     63    */
     64   this.lastError_ = '';
     65   /**
     66    * @type {remoting.Host?}
     67    * @private
     68    */
     69   this.localHost_ = null;
     70   /**
     71    * @type {remoting.HostController.State}
     72    * @private
     73    */
     74   this.localHostState_ = remoting.HostController.State.NOT_IMPLEMENTED;
     75   /**
     76    * @type {number}
     77    * @private
     78    */
     79   this.webappMajorVersion_ = parseInt(chrome.runtime.getManifest().version, 10);
     80 
     81   this.errorButton_.addEventListener('click',
     82                                      this.onErrorClick_.bind(this),
     83                                      false);
     84 };
     85 
     86 /**
     87  * Load the host-list asynchronously from local storage.
     88  *
     89  * @param {function():void} onDone Completion callback.
     90  */
     91 remoting.HostList.prototype.load = function(onDone) {
     92   // Load the cache of the last host-list, if present.
     93   /** @type {remoting.HostList} */
     94   var that = this;
     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]);
     99       if (cached) {
    100         that.hosts_ = /** @type {Array} */ cached;
    101       } else {
    102         console.error('Invalid value for ' + remoting.HostList.HOSTS_KEY);
    103       }
    104     }
    105     onDone();
    106   };
    107   chrome.storage.local.get(remoting.HostList.HOSTS_KEY, storeHostList);
    108 };
    109 
    110 /**
    111  * Search the host list for a host with the specified id.
    112  *
    113  * @param {string} hostId The unique id of the host.
    114  * @return {remoting.Host?} The host, if any.
    115  */
    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];
    120     }
    121   }
    122   return null;
    123 };
    124 
    125 /**
    126  * Query the Remoting Directory for the user's list of hosts.
    127  *
    128  * @param {function(boolean):void} onDone Callback invoked with true on success
    129  *     or false on failure.
    130  * @return {void} Nothing.
    131  */
    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} */
    136   var that = this;
    137   /** @param {string} token The OAuth2 token. */
    138   var getHosts = function(token) {
    139     var headers = { 'Authorization': 'OAuth ' + token };
    140     remoting.xhr.get(
    141         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
    142         parseHostListResponse, '', headers);
    143   };
    144   /** @param {remoting.Error} error */
    145   var onError = function(error) {
    146     that.lastError_ = error;
    147     onDone(false);
    148   };
    149   remoting.identity.callWithToken(getHosts, onError);
    150 };
    151 
    152 /**
    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.
    156  *
    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.
    160  * @private
    161  */
    162 remoting.HostList.prototype.parseHostListResponse_ = function(onDone, xhr) {
    163   this.lastError_ = '';
    164   try {
    165     if (xhr.status == 200) {
    166       var response =
    167           /** @type {{data: {items: Array}}} */ jsonParseSafe(xhr.responseText);
    168       if (response && response.data) {
    169         if (response.data.items) {
    170           this.hosts_ = response.data.items;
    171           /**
    172            * @param {remoting.Host} a
    173            * @param {remoting.Host} b
    174            */
    175           var cmp = function(a, b) {
    176             if (a.status < b.status) {
    177               return 1;
    178             } else if (b.status < a.status) {
    179               return -1;
    180             }
    181             return 0;
    182           };
    183           this.hosts_ = /** @type {Array} */ this.hosts_.sort(cmp);
    184         } else {
    185           this.hosts_ = [];
    186         }
    187       } else {
    188         this.lastError_ = remoting.Error.UNEXPECTED;
    189         console.error('Invalid "hosts" response from server.');
    190       }
    191     } else {
    192       // Some other error.
    193       console.error('Bad status on host list query: ', xhr);
    194       if (xhr.status == 0) {
    195         this.lastError_ = remoting.Error.NETWORK_FAILURE;
    196       } else if (xhr.status == 401) {
    197         this.lastError_ = remoting.Error.AUTHENTICATION_FAILED;
    198       } else if (xhr.status == 502 || xhr.status == 503) {
    199         this.lastError_ = remoting.Error.SERVICE_UNAVAILABLE;
    200       } else {
    201         this.lastError_ = remoting.Error.UNEXPECTED;
    202       }
    203     }
    204   } catch (er) {
    205     var typed_er = /** @type {Object} */ (er);
    206     console.error('Error processing response: ', xhr, typed_er);
    207     this.lastError_ = remoting.Error.UNEXPECTED;
    208   }
    209   this.save_();
    210   onDone(this.lastError_ == '');
    211 };
    212 
    213 /**
    214  * Display the list of hosts or error condition.
    215  *
    216  * @return {void} Nothing.
    217  */
    218 remoting.HostList.prototype.display = function() {
    219   this.table_.innerText = '';
    220   this.errorMsg_.innerText = '';
    221   this.hostTableEntries_ = [];
    222 
    223   var noHostsRegistered = (this.hosts_.length == 0);
    224   this.table_.hidden = noHostsRegistered;
    225   this.noHosts_.hidden = !noHostsRegistered;
    226 
    227   if (this.lastError_ != '') {
    228     l10n.localizeElementFromTag(this.errorMsg_, this.lastError_);
    229     if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
    230       l10n.localizeElementFromTag(this.errorButton_,
    231                                   /*i18n-content*/'SIGN_IN_BUTTON');
    232     } else {
    233       l10n.localizeElementFromTag(this.errorButton_,
    234                                   /*i18n-content*/'RETRY');
    235     }
    236   } else {
    237     for (var i = 0; i < this.hosts_.length; ++i) {
    238       /** @type {remoting.Host} */
    239       var host = this.hosts_[i];
    240       // Validate the entry to make sure it has all the fields we expect and is
    241       // not the local host (which is displayed separately). NB: if the host has
    242       // never sent a heartbeat, then there will be no jabberId.
    243       if (host.hostName && host.hostId && host.status && host.publicKey &&
    244           (!this.localHost_ || host.hostId != this.localHost_.hostId)) {
    245         var hostTableEntry = new remoting.HostTableEntry(
    246             host, this.webappMajorVersion_,
    247             this.renameHost_.bind(this), this.deleteHost_.bind(this));
    248         hostTableEntry.createDom();
    249         this.hostTableEntries_[i] = hostTableEntry;
    250         this.table_.appendChild(hostTableEntry.tableRow);
    251       }
    252     }
    253   }
    254 
    255   this.errorMsg_.parentNode.hidden = (this.lastError_ == '');
    256 
    257   // The local host cannot be stopped or started if the host controller is not
    258   // implemented for this platform. Additionally, it cannot be started if there
    259   // is an error (in many error states, the start operation will fail anyway,
    260   // but even if it succeeds, the chance of a related but hard-to-diagnose
    261   // future error is high).
    262   var state = this.localHostState_;
    263   var enabled = (state == remoting.HostController.State.STARTING) ||
    264       (state == remoting.HostController.State.STARTED);
    265   var canChangeLocalHostState =
    266       (state != remoting.HostController.State.NOT_IMPLEMENTED) &&
    267       (enabled || this.lastError_ == '');
    268 
    269   remoting.updateModalUi(enabled ? 'enabled' : 'disabled', 'data-daemon-state');
    270   var element = document.getElementById('daemon-control');
    271   element.hidden = !canChangeLocalHostState;
    272   element = document.getElementById('host-list-empty-hosting-supported');
    273   element.hidden = !canChangeLocalHostState;
    274   element = document.getElementById('host-list-empty-hosting-unsupported');
    275   element.hidden = canChangeLocalHostState;
    276 };
    277 
    278 /**
    279  * Remove a host from the list, and deregister it.
    280  * @param {remoting.HostTableEntry} hostTableEntry The host to be removed.
    281  * @return {void} Nothing.
    282  * @private
    283  */
    284 remoting.HostList.prototype.deleteHost_ = function(hostTableEntry) {
    285   this.table_.removeChild(hostTableEntry.tableRow);
    286   var index = this.hostTableEntries_.indexOf(hostTableEntry);
    287   if (index != -1) {
    288     this.hostTableEntries_.splice(index, 1);
    289   }
    290   remoting.HostList.unregisterHostById(hostTableEntry.host.hostId);
    291 };
    292 
    293 /**
    294  * Prepare a host for renaming by replacing its name with an edit box.
    295  * @param {remoting.HostTableEntry} hostTableEntry The host to be renamed.
    296  * @return {void} Nothing.
    297  * @private
    298  */
    299 remoting.HostList.prototype.renameHost_ = function(hostTableEntry) {
    300   for (var i = 0; i < this.hosts_.length; ++i) {
    301     if (this.hosts_[i].hostId == hostTableEntry.host.hostId) {
    302       this.hosts_[i].hostName = hostTableEntry.host.hostName;
    303       break;
    304     }
    305   }
    306   this.save_();
    307 
    308   /** @param {string?} token */
    309   var renameHost = function(token) {
    310     if (token) {
    311       var headers = {
    312         'Authorization': 'OAuth ' + token,
    313         'Content-type' : 'application/json; charset=UTF-8'
    314       };
    315       var newHostDetails = { data: {
    316         hostId: hostTableEntry.host.hostId,
    317         hostName: hostTableEntry.host.hostName,
    318         publicKey: hostTableEntry.host.publicKey
    319       } };
    320       remoting.xhr.put(
    321           remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' +
    322           hostTableEntry.host.hostId,
    323           function(xhr) {},
    324           JSON.stringify(newHostDetails),
    325           headers);
    326     } else {
    327       console.error('Could not rename host. Authentication failure.');
    328     }
    329   }
    330   remoting.identity.callWithToken(renameHost, remoting.showErrorMessage);
    331 };
    332 
    333 /**
    334  * Unregister a host.
    335  * @param {string} hostId The id of the host to be removed.
    336  * @return {void} Nothing.
    337  */
    338 remoting.HostList.unregisterHostById = function(hostId) {
    339   /** @param {string} token The OAuth2 token. */
    340   var deleteHost = function(token) {
    341     var headers = { 'Authorization': 'OAuth ' + token };
    342     remoting.xhr.remove(
    343         remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
    344         function() {}, '', headers);
    345   }
    346   remoting.identity.callWithToken(deleteHost, remoting.showErrorMessage);
    347 };
    348 
    349 /**
    350  * Set tool-tips for the 'connect' action. We can't just set this on the
    351  * parent element because the button has no tool-tip, and therefore would
    352  * inherit connectStr.
    353  *
    354  * @return {void} Nothing.
    355  * @private
    356  */
    357 remoting.HostList.prototype.setTooltips_ = function() {
    358   var connectStr = '';
    359   if (this.localHost_) {
    360     chrome.i18n.getMessage(/*i18n-content*/'TOOLTIP_CONNECT',
    361                            this.localHost_.hostName);
    362   }
    363   document.getElementById('this-host-name').title = connectStr;
    364   document.getElementById('this-host-icon').title = connectStr;
    365 };
    366 
    367 /**
    368  * Set the state of the local host and localHostId if any.
    369  *
    370  * @param {remoting.HostController.State} state State of the local host.
    371  * @param {string?} hostId ID of the local host, or null.
    372  * @return {void} Nothing.
    373  */
    374 remoting.HostList.prototype.setLocalHostStateAndId = function(state, hostId) {
    375   this.localHostState_ = state;
    376   this.setLocalHost_(hostId ? this.getHostForId(hostId) : null);
    377 }
    378 
    379 /**
    380  * Set the host object that corresponds to the local computer, if any.
    381  *
    382  * @param {remoting.Host?} host The host, or null if not registered.
    383  * @return {void} Nothing.
    384  * @private
    385  */
    386 remoting.HostList.prototype.setLocalHost_ = function(host) {
    387   this.localHost_ = host;
    388   this.setTooltips_();
    389   /** @type {remoting.HostList} */
    390   var that = this;
    391   if (host) {
    392     /** @param {remoting.HostTableEntry} host */
    393     var renameHost = function(host) {
    394       that.renameHost_(host);
    395       that.setTooltips_();
    396     };
    397     if (!this.localHostTableEntry_) {
    398       /** @type {remoting.HostTableEntry} @private */
    399       this.localHostTableEntry_ = new remoting.HostTableEntry(
    400           host, this.webappMajorVersion_, renameHost);
    401       this.localHostTableEntry_.init(
    402           document.getElementById('this-host-connect'),
    403           document.getElementById('this-host-warning'),
    404           document.getElementById('this-host-name'),
    405           document.getElementById('this-host-rename'));
    406     } else {
    407       // TODO(jamiewalch): This is hack to prevent multiple click handlers being
    408       // registered for the same DOM elements if this method is called more than
    409       // once. A better solution would be to let HostTable create the daemon row
    410       // like it creates the rows for non-local hosts.
    411       this.localHostTableEntry_.host = host;
    412     }
    413   } else {
    414     this.localHostTableEntry_ = null;
    415   }
    416 }
    417 
    418 /**
    419  * Called by the HostControlled after the local host has been started.
    420  *
    421  * @param {string} hostName Host name.
    422  * @param {string} hostId ID of the local host.
    423  * @param {string} publicKey Public key.
    424  * @return {void} Nothing.
    425  */
    426 remoting.HostList.prototype.onLocalHostStarted = function(
    427     hostName, hostId, publicKey) {
    428   // Create a dummy remoting.Host instance to represent the local host.
    429   // Refreshing the list is no good in general, because the directory
    430   // information won't be in sync for several seconds. We don't know the
    431   // host JID, but it can be missing from the cache with no ill effects.
    432   // It will be refreshed if the user tries to connect to the local host,
    433   // and we hope that the directory will have been updated by that point.
    434   var localHost = new remoting.Host();
    435   localHost.hostName = hostName;
    436   // Provide a version number to avoid warning about this dummy host being
    437   // out-of-date.
    438   localHost.hostVersion = String(this.webappMajorVersion_) + ".x"
    439   localHost.hostId = hostId;
    440   localHost.publicKey = publicKey;
    441   localHost.status = 'ONLINE';
    442   this.hosts_.push(localHost);
    443   this.save_();
    444   this.setLocalHost_(localHost);
    445 };
    446 
    447 /**
    448  * Called when the user clicks the button next to the error message. The action
    449  * depends on the error.
    450  *
    451  * @private
    452  */
    453 remoting.HostList.prototype.onErrorClick_ = function() {
    454   if (this.lastError_ == remoting.Error.AUTHENTICATION_FAILED) {
    455     remoting.oauth2.doAuthRedirect();
    456   } else {
    457     this.refresh(remoting.updateLocalHostState);
    458   }
    459 };
    460 
    461 /**
    462  * Save the host list to local storage.
    463  */
    464 remoting.HostList.prototype.save_ = function() {
    465   var items = {};
    466   items[remoting.HostList.HOSTS_KEY] = JSON.stringify(this.hosts_);
    467   chrome.storage.local.set(items);
    468 };
    469 
    470 /**
    471  * Key name under which Me2Me hosts are cached.
    472  */
    473 remoting.HostList.HOSTS_KEY = 'me2me-cached-hosts';
    474 
    475 /** @type {remoting.HostList} */
    476 remoting.hostList = null;
    477