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 'host screen' for Chromoting.
      8  */
      9 
     10 'use strict';
     11 
     12 /** @suppress {duplicate} */
     13 var remoting = remoting || {};
     14 
     15 /**
     16  * @type {boolean} Whether or not the last share was cancelled by the user.
     17  *     This controls what screen is shown when the host signals completion.
     18  * @private
     19  */
     20 var lastShareWasCancelled_ = false;
     21 
     22 /**
     23  * Start a host session. This is the main entry point for the host screen,
     24  * called directly from the onclick action of a button on the home screen.
     25  * It first verifies that the native host components are installed and asks
     26  * to install them if necessary.
     27  */
     28 remoting.tryShare = function() {
     29   /** @type {remoting.It2MeHostFacade} */
     30   var hostFacade = new remoting.It2MeHostFacade();
     31 
     32   /** @type {remoting.HostInstallDialog} */
     33   var hostInstallDialog = null;
     34 
     35   var tryInitializeFacade = function() {
     36     hostFacade.initialize(onFacadeInitialized, onFacadeInitializationFailed);
     37   }
     38 
     39   var onFacadeInitialized = function () {
     40     // Host already installed.
     41     remoting.startHostUsingFacade_(hostFacade);
     42   };
     43 
     44   var onFacadeInitializationFailed = function() {
     45     // If we failed to initialize the dispatcher then prompt the user to install
     46     // the host manually.
     47     var hasHostDialog = (hostInstallDialog != null);  /** jscompile hack */
     48     if (!hasHostDialog) {
     49       hostInstallDialog = new remoting.HostInstallDialog();
     50       hostInstallDialog.show(tryInitializeFacade, onInstallError);
     51     } else {
     52       hostInstallDialog.tryAgain();
     53     }
     54   };
     55 
     56   /** @param {remoting.Error} error */
     57   var onInstallError = function(error) {
     58     if (error == remoting.Error.CANCELLED) {
     59       remoting.setMode(remoting.AppMode.HOME);
     60     } else {
     61       showShareError_(error);
     62     }
     63   }
     64 
     65   tryInitializeFacade();
     66 };
     67 
     68 /**
     69  * @param {remoting.It2MeHostFacade} hostFacade An initialized It2MeHostFacade.
     70  */
     71 remoting.startHostUsingFacade_ = function(hostFacade) {
     72   console.log('Attempting to share...');
     73   remoting.identity.callWithToken(
     74       remoting.tryShareWithToken_.bind(null, hostFacade),
     75       remoting.showErrorMessage);
     76 }
     77 
     78 /**
     79  * @param {remoting.It2MeHostFacade} hostFacade An initialized
     80  *     It2MeHostFacade.
     81  * @param {string} token The OAuth access token.
     82  * @private
     83  */
     84 remoting.tryShareWithToken_ = function(hostFacade, token) {
     85   lastShareWasCancelled_ = false;
     86   onNatTraversalPolicyChanged_(true);  // Hide warning by default.
     87   remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CODE);
     88   document.getElementById('cancel-share-button').disabled = false;
     89   disableTimeoutCountdown_();
     90 
     91   remoting.hostSession = new remoting.HostSession();
     92   var email = /** @type {string} */remoting.identity.getCachedEmail();
     93   remoting.hostSession.connect(
     94       hostFacade, email, token, onHostStateChanged_,
     95       onNatTraversalPolicyChanged_, logDebugInfo_, it2meConnectFailed_);
     96 };
     97 
     98 /**
     99  * Callback for the host plugin to notify the web app of state changes.
    100  * @param {remoting.HostSession.State} state The new state of the plugin.
    101  * @return {void} Nothing.
    102  * @private
    103  */
    104 function onHostStateChanged_(state) {
    105   if (state == remoting.HostSession.State.STARTING) {
    106     // Nothing to do here.
    107     console.log('Host state: STARTING');
    108 
    109   } else if (state == remoting.HostSession.State.REQUESTED_ACCESS_CODE) {
    110     // Nothing to do here.
    111     console.log('Host state: REQUESTED_ACCESS_CODE');
    112 
    113   } else if (state == remoting.HostSession.State.RECEIVED_ACCESS_CODE) {
    114     console.log('Host state: RECEIVED_ACCESS_CODE');
    115     var accessCode = remoting.hostSession.getAccessCode();
    116     var accessCodeDisplay = document.getElementById('access-code-display');
    117     accessCodeDisplay.innerText = '';
    118     // Display the access code in groups of four digits for readability.
    119     var kDigitsPerGroup = 4;
    120     for (var i = 0; i < accessCode.length; i += kDigitsPerGroup) {
    121       var nextFourDigits = document.createElement('span');
    122       nextFourDigits.className = 'access-code-digit-group';
    123       nextFourDigits.innerText = accessCode.substring(i, i + kDigitsPerGroup);
    124       accessCodeDisplay.appendChild(nextFourDigits);
    125     }
    126     accessCodeExpiresIn_ = remoting.hostSession.getAccessCodeLifetime();
    127     if (accessCodeExpiresIn_ > 0) {  // Check it hasn't expired.
    128       accessCodeTimerId_ = setInterval(decrementAccessCodeTimeout_, 1000);
    129       timerRunning_ = true;
    130       updateAccessCodeTimeoutElement_();
    131       updateTimeoutStyles_();
    132       remoting.setMode(remoting.AppMode.HOST_WAITING_FOR_CONNECTION);
    133     } else {
    134       // This can only happen if the cloud tells us that the code lifetime is
    135       // <= 0s, which shouldn't happen so we don't care how clean this UX is.
    136       console.error('Access code already invalid on receipt!');
    137       remoting.cancelShare();
    138     }
    139 
    140   } else if (state == remoting.HostSession.State.CONNECTED) {
    141     console.log('Host state: CONNECTED');
    142     var element = document.getElementById('host-shared-message');
    143     var client = remoting.hostSession.getClient();
    144     l10n.localizeElement(element, client);
    145     remoting.setMode(remoting.AppMode.HOST_SHARED);
    146     disableTimeoutCountdown_();
    147 
    148   } else if (state == remoting.HostSession.State.DISCONNECTING) {
    149     console.log('Host state: DISCONNECTING');
    150 
    151   } else if (state == remoting.HostSession.State.DISCONNECTED) {
    152     console.log('Host state: DISCONNECTED');
    153     if (remoting.currentMode != remoting.AppMode.HOST_SHARE_FAILED) {
    154       // If an error is being displayed, then the plugin should not be able to
    155       // hide it by setting the state. Errors must be dismissed by the user
    156       // clicking OK, which puts the app into mode HOME.
    157       if (lastShareWasCancelled_) {
    158         remoting.setMode(remoting.AppMode.HOME);
    159       } else {
    160         remoting.setMode(remoting.AppMode.HOST_SHARE_FINISHED);
    161       }
    162     }
    163   } else if (state == remoting.HostSession.State.ERROR) {
    164     console.error('Host state: ERROR');
    165     showShareError_(remoting.Error.UNEXPECTED);
    166   } else if (state == remoting.HostSession.State.INVALID_DOMAIN_ERROR) {
    167     console.error('Host state: INVALID_DOMAIN_ERROR');
    168     showShareError_(remoting.Error.INVALID_HOST_DOMAIN);
    169   } else {
    170     console.error('Unknown state -> ' + state);
    171   }
    172 }
    173 
    174 /**
    175  * This is the callback that the host plugin invokes to indicate that there
    176  * is additional debug log info to display.
    177  * @param {string} msg The message (which will not be localized) to be logged.
    178  * @private
    179  */
    180 function logDebugInfo_(msg) {
    181   console.log('plugin: ' + msg);
    182 }
    183 
    184 /**
    185  * Show a host-side error message.
    186  *
    187  * @param {string} errorTag The error message to be localized and displayed.
    188  * @return {void} Nothing.
    189  * @private
    190  */
    191 function showShareError_(errorTag) {
    192   var errorDiv = document.getElementById('host-plugin-error');
    193   l10n.localizeElementFromTag(errorDiv, errorTag);
    194   console.error('Sharing error: ' + errorTag);
    195   remoting.setMode(remoting.AppMode.HOST_SHARE_FAILED);
    196 }
    197 
    198 /**
    199  * Show a sharing error with error code UNEXPECTED .
    200  *
    201  * @return {void} Nothing.
    202  * @private
    203  */
    204 function it2meConnectFailed_() {
    205   // TODO (weitaosu): Instruct the user to install the native messaging host.
    206   // We probably want to add a new error code (with the corresponding error
    207   // message for sharing error.
    208   console.error('Cannot share desktop.');
    209   showShareError_(remoting.Error.UNEXPECTED);
    210 }
    211 
    212 /**
    213  * Cancel an active or pending it2me share operation.
    214  *
    215  * @return {void} Nothing.
    216  */
    217 remoting.cancelShare = function() {
    218   document.getElementById('cancel-share-button').disabled = true;
    219   console.log('Canceling share...');
    220   remoting.lastShareWasCancelled = true;
    221   try {
    222     remoting.hostSession.disconnect();
    223   } catch (error) {
    224     // Hack to force JSCompiler type-safety.
    225     var errorTyped = /** @type {{description: string}} */ error;
    226     console.error('Error disconnecting: ' + errorTyped.description +
    227                   '. The host probably crashed.');
    228     // TODO(jamiewalch): Clean this up. We should have a class representing
    229     // the host plugin, like we do for the client, which should handle crash
    230     // reporting and it should use a more detailed error message than the
    231     // default 'generic' one. See crbug.com/94624
    232     showShareError_(remoting.Error.UNEXPECTED);
    233   }
    234   disableTimeoutCountdown_();
    235 };
    236 
    237 /**
    238  * @type {boolean} Whether or not the access code timeout countdown is running.
    239  * @private
    240  */
    241 var timerRunning_ = false;
    242 
    243 /**
    244  * @type {number} The id of the access code expiry countdown timer.
    245  * @private
    246  */
    247 var accessCodeTimerId_ = 0;
    248 
    249 /**
    250  * @type {number} The number of seconds until the access code expires.
    251  * @private
    252  */
    253 var accessCodeExpiresIn_ = 0;
    254 
    255 /**
    256  * The timer callback function
    257  * @return {void} Nothing.
    258  * @private
    259  */
    260 function decrementAccessCodeTimeout_() {
    261   --accessCodeExpiresIn_;
    262   updateAccessCodeTimeoutElement_();
    263 };
    264 
    265 /**
    266  * Stop the access code timeout countdown if it is running.
    267  * @return {void} Nothing.
    268  * @private
    269  */
    270 function disableTimeoutCountdown_() {
    271   if (timerRunning_) {
    272     clearInterval(accessCodeTimerId_);
    273     timerRunning_ = false;
    274     updateTimeoutStyles_();
    275   }
    276 }
    277 
    278 /**
    279  * Constants controlling the access code timer countdown display.
    280  * @private
    281  */
    282 var ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_ = 30;
    283 var ACCESS_CODE_RED_THRESHOLD_ = 10;
    284 
    285 /**
    286  * Show/hide or restyle various elements, depending on the remaining countdown
    287  * and timer state.
    288  *
    289  * @return {boolean} True if the timeout is in progress, false if it has
    290  * expired.
    291  * @private
    292  */
    293 function updateTimeoutStyles_() {
    294   if (timerRunning_) {
    295     if (accessCodeExpiresIn_ <= 0) {
    296       remoting.cancelShare();
    297       return false;
    298     }
    299     var accessCode = document.getElementById('access-code-display');
    300     if (accessCodeExpiresIn_ <= ACCESS_CODE_RED_THRESHOLD_) {
    301       accessCode.classList.add('expiring');
    302     } else {
    303       accessCode.classList.remove('expiring');
    304     }
    305   }
    306   document.getElementById('access-code-countdown').hidden =
    307       (accessCodeExpiresIn_ > ACCESS_CODE_TIMER_DISPLAY_THRESHOLD_) ||
    308       !timerRunning_;
    309   return true;
    310 }
    311 
    312 /**
    313  * Update the text and appearance of the access code timeout element to
    314  * reflect the time remaining.
    315  * @return {void} Nothing.
    316  * @private
    317  */
    318 function updateAccessCodeTimeoutElement_() {
    319   var pad = (accessCodeExpiresIn_ < 10) ? '0:0' : '0:';
    320   l10n.localizeElement(document.getElementById('seconds-remaining'),
    321                        pad + accessCodeExpiresIn_);
    322   if (!updateTimeoutStyles_()) {
    323     disableTimeoutCountdown_();
    324   }
    325 }
    326 
    327 /**
    328  * Callback to show or hide the NAT traversal warning when the policy changes.
    329  * @param {boolean} enabled True if NAT traversal is enabled.
    330  * @return {void} Nothing.
    331  * @private
    332  */
    333 function onNatTraversalPolicyChanged_(enabled) {
    334   var natBox = document.getElementById('nat-box');
    335   if (enabled) {
    336     natBox.classList.add('traversal-enabled');
    337   } else {
    338     natBox.classList.remove('traversal-enabled');
    339   }
    340 }
    341