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