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