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  * Functions related to the 'client screen' for Chromoting.
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     15 /**
     16  * @type {remoting.SessionConnector} The connector object, set when a connection
     17  *     is initiated.
     18  */
     19 remoting.connector = null;
     20 
     21 /**
     22  * @type {remoting.ClientSession} The client session object, set once the
     23  *     connector has invoked its onOk callback.
     24  */
     25 remoting.clientSession = null;
     26 
     27 /**
     28  * Initiate an IT2Me connection.
     29  */
     30 remoting.connectIT2Me = function() {
     31   if (!remoting.connector) {
     32     remoting.connector = new remoting.SessionConnector(
     33         document.getElementById('session-mode'),
     34         remoting.onConnected,
     35         showConnectError_);
     36   }
     37   var accessCode = document.getElementById('access-code-entry').value;
     38   remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
     39   remoting.connector.connectIT2Me(accessCode);
     40 };
     41 
     42 /**
     43  * Update the remoting client layout in response to a resize event.
     44  *
     45  * @return {void} Nothing.
     46  */
     47 remoting.onResize = function() {
     48   if (remoting.clientSession) {
     49     remoting.clientSession.onResize();
     50   }
     51 };
     52 
     53 /**
     54  * Handle changes in the visibility of the window, for example by pausing video.
     55  *
     56  * @return {void} Nothing.
     57  */
     58 remoting.onVisibilityChanged = function() {
     59   if (remoting.clientSession) {
     60     remoting.clientSession.pauseVideo(
     61       ('hidden' in document) ? document.hidden : document.webkitHidden);
     62   }
     63 }
     64 
     65 /**
     66  * Disconnect the remoting client.
     67  *
     68  * @return {void} Nothing.
     69  */
     70 remoting.disconnect = function() {
     71   if (!remoting.clientSession) {
     72     return;
     73   }
     74   if (remoting.clientSession.getMode() == remoting.ClientSession.Mode.IT2ME) {
     75     remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
     76   } else {
     77     remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
     78   }
     79   remoting.clientSession.disconnect(true);
     80   remoting.clientSession = null;
     81   console.log('Disconnected.');
     82 };
     83 
     84 /**
     85  * Sends a Ctrl-Alt-Del sequence to the remoting client.
     86  *
     87  * @return {void} Nothing.
     88  */
     89 remoting.sendCtrlAltDel = function() {
     90   if (remoting.clientSession) {
     91     console.log('Sending Ctrl-Alt-Del.');
     92     remoting.clientSession.sendCtrlAltDel();
     93   }
     94 };
     95 
     96 /**
     97  * Sends a Print Screen keypress to the remoting client.
     98  *
     99  * @return {void} Nothing.
    100  */
    101 remoting.sendPrintScreen = function() {
    102   if (remoting.clientSession) {
    103     console.log('Sending Print Screen.');
    104     remoting.clientSession.sendPrintScreen();
    105   }
    106 };
    107 
    108 /**
    109  * Callback function called when the state of the client plugin changes. The
    110  * current state is available via the |state| member variable.
    111  *
    112  * @param {number} oldState The previous state of the plugin.
    113  * @param {number} newState The current state of the plugin.
    114  */
    115 function onClientStateChange_(oldState, newState) {
    116   switch (newState) {
    117     case remoting.ClientSession.State.CLOSED:
    118       console.log('Connection closed by host');
    119       if (remoting.clientSession.getMode() ==
    120           remoting.ClientSession.Mode.IT2ME) {
    121         remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_IT2ME);
    122       } else {
    123         remoting.setMode(remoting.AppMode.CLIENT_SESSION_FINISHED_ME2ME);
    124       }
    125       break;
    126 
    127     case remoting.ClientSession.State.FAILED:
    128       var error = remoting.clientSession.getError();
    129       console.error('Client plugin reported connection failed: ' + error);
    130       if (error == null) {
    131         error = remoting.Error.UNEXPECTED;
    132       }
    133       showConnectError_(error);
    134       break;
    135 
    136     default:
    137       console.error('Unexpected client plugin state: ' + newState);
    138       // This should only happen if the web-app and client plugin get out of
    139       // sync, so MISSING_PLUGIN is a suitable error.
    140       showConnectError_(remoting.Error.MISSING_PLUGIN);
    141       break;
    142   }
    143   remoting.clientSession.disconnect(false);
    144   remoting.clientSession.removePlugin();
    145   remoting.clientSession = null;
    146 }
    147 
    148 /**
    149  * Show a client-side error message.
    150  *
    151  * @param {remoting.Error} errorTag The error to be localized and
    152  *     displayed.
    153  * @return {void} Nothing.
    154  */
    155 function showConnectError_(errorTag) {
    156   console.error('Connection failed: ' + errorTag);
    157   var errorDiv = document.getElementById('connect-error-message');
    158   l10n.localizeElementFromTag(errorDiv, /** @type {string} */ (errorTag));
    159   remoting.accessCode = '';
    160   var mode = remoting.clientSession ? remoting.clientSession.getMode()
    161                                     : remoting.connector.getConnectionMode();
    162   if (mode == remoting.ClientSession.Mode.IT2ME) {
    163     remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_IT2ME);
    164   } else {
    165     remoting.setMode(remoting.AppMode.CLIENT_CONNECT_FAILED_ME2ME);
    166   }
    167 }
    168 
    169 /**
    170  * Set the text on the buttons shown under the error message so that they are
    171  * easy to understand in the case where a successful connection failed, as
    172  * opposed to the case where a connection never succeeded.
    173  */
    174 function setConnectionInterruptedButtonsText_() {
    175   var button1 = document.getElementById('client-reconnect-button');
    176   l10n.localizeElementFromTag(button1, /*i18n-content*/'RECONNECT');
    177   button1.removeAttribute('autofocus');
    178   var button2 = document.getElementById('client-finished-me2me-button');
    179   l10n.localizeElementFromTag(button2, /*i18n-content*/'OK');
    180   button2.setAttribute('autofocus', 'autofocus');
    181 }
    182 
    183 /**
    184  * Timer callback to update the statistics panel.
    185  */
    186 function updateStatistics_() {
    187   if (!remoting.clientSession ||
    188       remoting.clientSession.getState() !=
    189       remoting.ClientSession.State.CONNECTED) {
    190     return;
    191   }
    192   var perfstats = remoting.clientSession.getPerfStats();
    193   remoting.stats.update(perfstats);
    194   remoting.clientSession.logStatistics(perfstats);
    195   // Update the stats once per second.
    196   window.setTimeout(updateStatistics_, 1000);
    197 }
    198 
    199 /**
    200  * Entry-point for Me2Me connections, handling showing of the host-upgrade nag
    201  * dialog if necessary.
    202  *
    203  * @param {string} hostId The unique id of the host.
    204  * @return {void} Nothing.
    205  */
    206 remoting.connectMe2Me = function(hostId) {
    207   var host = remoting.hostList.getHostForId(hostId);
    208   if (!host) {
    209     showConnectError_(remoting.Error.HOST_IS_OFFLINE);
    210     return;
    211   }
    212   var webappVersion = chrome.runtime.getManifest().version;
    213   if (remoting.Host.needsUpdate(host, webappVersion)) {
    214     var needsUpdateMessage =
    215         document.getElementById('host-needs-update-message');
    216     l10n.localizeElementFromTag(needsUpdateMessage,
    217                                 /*i18n-content*/'HOST_NEEDS_UPDATE_TITLE',
    218                                 host.hostName);
    219     /** @type {Element} */
    220     var connect = document.getElementById('host-needs-update-connect-button');
    221     /** @type {Element} */
    222     var cancel = document.getElementById('host-needs-update-cancel-button');
    223     /** @param {Event} event */
    224     var onClick = function(event) {
    225       connect.removeEventListener('click', onClick, false);
    226       cancel.removeEventListener('click', onClick, false);
    227       if (event.target == connect) {
    228         remoting.connectMe2MeHostVersionAcknowledged_(host);
    229       } else {
    230         remoting.setMode(remoting.AppMode.HOME);
    231       }
    232     }
    233     connect.addEventListener('click', onClick, false);
    234     cancel.addEventListener('click', onClick, false);
    235     remoting.setMode(remoting.AppMode.CLIENT_HOST_NEEDS_UPGRADE);
    236   } else {
    237     remoting.connectMe2MeHostVersionAcknowledged_(host);
    238   }
    239 };
    240 
    241 /**
    242  * Shows PIN entry screen localized to include the host name, and registers
    243  * a host-specific one-shot event handler for the form submission.
    244  *
    245  * @param {remoting.Host} host The Me2Me host to which to connect.
    246  * @return {void} Nothing.
    247  */
    248 remoting.connectMe2MeHostVersionAcknowledged_ = function(host) {
    249   if (!remoting.connector) {
    250     remoting.connector = new remoting.SessionConnector(
    251         document.getElementById('session-mode'),
    252         remoting.onConnected,
    253         showConnectError_);
    254   }
    255   remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
    256 
    257   /**
    258    * @param {string} tokenUrl Token-issue URL received from the host.
    259    * @param {string} scope OAuth scope to request the token for.
    260    * @param {string} hostPublicKey Host public key (DER and Base64 encoded).
    261    * @param {function(string, string):void} onThirdPartyTokenFetched Callback.
    262    */
    263   var fetchThirdPartyToken = function(
    264       tokenUrl, hostPublicKey, scope, onThirdPartyTokenFetched) {
    265     var thirdPartyTokenFetcher = new remoting.ThirdPartyTokenFetcher(
    266         tokenUrl, hostPublicKey, scope, host.tokenUrlPatterns,
    267         onThirdPartyTokenFetched);
    268     thirdPartyTokenFetcher.fetchToken();
    269   };
    270 
    271   /**
    272    * @param {boolean} supportsPairing
    273    * @param {function(string):void} onPinFetched
    274    */
    275   var requestPin = function(supportsPairing, onPinFetched) {
    276     /** @type {Element} */
    277     var pinForm = document.getElementById('pin-form');
    278     /** @type {Element} */
    279     var pinCancel = document.getElementById('cancel-pin-entry-button');
    280     /** @type {Element} */
    281     var rememberPin = document.getElementById('remember-pin');
    282     /** @type {Element} */
    283     var rememberPinCheckbox = document.getElementById('remember-pin-checkbox');
    284     /**
    285      * Event handler for both the 'submit' and 'cancel' actions. Using
    286      * a single handler for both greatly simplifies the task of making
    287      * them one-shot. If separate handlers were used, each would have
    288      * to unregister both itself and the other.
    289      *
    290      * @param {Event} event The click or submit event.
    291      */
    292     var onSubmitOrCancel = function(event) {
    293       pinForm.removeEventListener('submit', onSubmitOrCancel, false);
    294       pinCancel.removeEventListener('click', onSubmitOrCancel, false);
    295       var pinField = document.getElementById('pin-entry');
    296       var pin = pinField.value;
    297       pinField.value = '';
    298       if (event.target == pinForm) {
    299         event.preventDefault();
    300 
    301         // Set the focus away from the password field. This has to be done
    302         // before the password field gets hidden, to work around a Blink
    303         // clipboard-handling bug - http://crbug.com/281523.
    304         document.getElementById('pin-connect-button').focus();
    305 
    306         remoting.setMode(remoting.AppMode.CLIENT_CONNECTING);
    307         onPinFetched(pin);
    308         if (/** @type {boolean} */(rememberPinCheckbox.checked)) {
    309           remoting.connector.pairingRequested = true;
    310         }
    311       } else {
    312         remoting.setMode(remoting.AppMode.HOME);
    313       }
    314     };
    315     pinForm.addEventListener('submit', onSubmitOrCancel, false);
    316     pinCancel.addEventListener('click', onSubmitOrCancel, false);
    317     rememberPin.hidden = !supportsPairing;
    318     rememberPinCheckbox.checked = false;
    319     var message = document.getElementById('pin-message');
    320     l10n.localizeElement(message, host.hostName);
    321     remoting.setMode(remoting.AppMode.CLIENT_PIN_PROMPT);
    322   };
    323 
    324   /** @param {Object} settings */
    325   var connectMe2MeHostSettingsRetrieved = function(settings) {
    326     /** @type {string} */
    327     var clientId = '';
    328     /** @type {string} */
    329     var sharedSecret = '';
    330     var pairingInfo = /** @type {Object} */ (settings['pairingInfo']);
    331     if (pairingInfo) {
    332       clientId = /** @type {string} */ (pairingInfo['clientId']);
    333       sharedSecret = /** @type {string} */ (pairingInfo['sharedSecret']);
    334     }
    335     remoting.connector.connectMe2Me(host, requestPin, fetchThirdPartyToken,
    336                                     clientId, sharedSecret);
    337   }
    338 
    339   remoting.HostSettings.load(host.hostId, connectMe2MeHostSettingsRetrieved);
    340 };
    341 
    342 /** @param {remoting.ClientSession} clientSession */
    343 remoting.onConnected = function(clientSession) {
    344   remoting.clientSession = clientSession;
    345   remoting.clientSession.setOnStateChange(onClientStateChange_);
    346   setConnectionInterruptedButtonsText_();
    347   var connectedTo = document.getElementById('connected-to');
    348   connectedTo.innerText = remoting.connector.getHostDisplayName();
    349   document.getElementById('access-code-entry').value = '';
    350   remoting.setMode(remoting.AppMode.IN_SESSION);
    351   remoting.toolbar.center();
    352   remoting.toolbar.preview();
    353   remoting.clipboard.startSession();
    354   updateStatistics_();
    355   if (remoting.connector.pairingRequested) {
    356     /**
    357      * @param {string} clientId
    358      * @param {string} sharedSecret
    359      */
    360     var onPairingComplete = function(clientId, sharedSecret) {
    361       var pairingInfo = {
    362         pairingInfo: {
    363           clientId: clientId,
    364           sharedSecret: sharedSecret
    365         }
    366       };
    367       remoting.HostSettings.save(remoting.connector.getHostId(), pairingInfo);
    368       remoting.connector.updatePairingInfo(clientId, sharedSecret);
    369     };
    370     // Use the platform name as a proxy for the local computer name.
    371     // TODO(jamiewalch): Use a descriptive name for the local computer, for
    372     // example, its Chrome Sync name.
    373     var clientName = '';
    374     if (navigator.platform.indexOf('Mac') != -1) {
    375       clientName = 'Mac';
    376     } else if (navigator.platform.indexOf('Win32') != -1) {
    377       clientName = 'Windows';
    378     } else if (navigator.userAgent.match(/\bCrOS\b/)) {
    379       clientName = 'ChromeOS';
    380     } else if (navigator.platform.indexOf('Linux') != -1) {
    381       clientName = 'Linux';
    382     } else {
    383       console.log('Unrecognized client platform. Using navigator.platform.');
    384       clientName = navigator.platform;
    385     }
    386     clientSession.requestPairing(clientName, onPairingComplete);
    387   }
    388 };
    389