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