Home | History | Annotate | Download | only in hotword_helper
      1 // Copyright (c) 2014 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 'use strict';
      6 
      7 /**
      8  * @fileoverview This is the audio client content script injected into eligible
      9  *  Google.com and New tab pages for interaction between the Webpage and the
     10  *  Hotword extension.
     11  */
     12 
     13 
     14 
     15 (function() {
     16   /**
     17    * @constructor
     18    */
     19   var AudioClient = function() {
     20     /** @private {Element} */
     21     this.speechOverlay_ = null;
     22 
     23     /** @private {number} */
     24     this.checkSpeechUiRetries_ = 0;
     25 
     26     /**
     27      * Port used to communicate with the audio manager.
     28      * @private {?Port}
     29      */
     30     this.port_ = null;
     31 
     32     /**
     33      * Keeps track of the effects of different commands. Used to verify that
     34      * proper UIs are shown to the user.
     35      * @private {Object.<AudioClient.CommandToPage, Object>}
     36      */
     37     this.uiStatus_ = null;
     38 
     39     /**
     40      * Bound function used to handle commands sent from the page to this script.
     41      * @private {Function}
     42      */
     43     this.handleCommandFromPageFunc_ = null;
     44   };
     45 
     46 
     47   /**
     48    * Messages sent to the page to control the voice search UI.
     49    * @enum {string}
     50    */
     51   AudioClient.CommandToPage = {
     52     HOTWORD_VOICE_TRIGGER: 'vt',
     53     HOTWORD_STARTED: 'hs',
     54     HOTWORD_ENDED: 'hd',
     55     HOTWORD_TIMEOUT: 'ht',
     56     HOTWORD_ERROR: 'he'
     57   };
     58 
     59 
     60   /**
     61    * Messages received from the page used to indicate voice search state.
     62    * @enum {string}
     63    */
     64   AudioClient.CommandFromPage = {
     65     SPEECH_START: 'ss',
     66     SPEECH_END: 'se',
     67     SPEECH_RESET: 'sr',
     68     SHOWING_HOTWORD_START: 'shs',
     69     SHOWING_ERROR_MESSAGE: 'sem',
     70     SHOWING_TIMEOUT_MESSAGE: 'stm',
     71     CLICKED_RESUME: 'hcc',
     72     CLICKED_RESTART: 'hcr',
     73     CLICKED_DEBUG: 'hcd'
     74   };
     75 
     76 
     77   /**
     78    * Errors that are sent to the hotword extension.
     79    * @enum {string}
     80    */
     81   AudioClient.Error = {
     82     NO_SPEECH_UI: 'ac1',
     83     NO_HOTWORD_STARTED_UI: 'ac2',
     84     NO_HOTWORD_TIMEOUT_UI: 'ac3',
     85     NO_HOTWORD_ERROR_UI: 'ac4'
     86   };
     87 
     88 
     89   /**
     90    * @const {string}
     91    * @private
     92    */
     93   AudioClient.HOTWORD_EXTENSION_ID_ = 'bepbmhgboaologfdajaanbcjmnhjmhfn';
     94 
     95 
     96   /**
     97    * Number of times to retry checking a transient error.
     98    * @const {number}
     99    * @private
    100    */
    101   AudioClient.MAX_RETRIES = 3;
    102 
    103 
    104   /**
    105    * Delay to wait in milliseconds before rechecking for any transient errors.
    106    * @const {number}
    107    * @private
    108    */
    109   AudioClient.RETRY_TIME_MS_ = 2000;
    110 
    111 
    112   /**
    113    * DOM ID for the speech UI overlay.
    114    * @const {string}
    115    * @private
    116    */
    117   AudioClient.SPEECH_UI_OVERLAY_ID_ = 'spch';
    118 
    119 
    120   /**
    121    * @const {string}
    122    * @private
    123    */
    124   AudioClient.HELP_CENTER_URL_ =
    125       'https://support.google.com/chrome/?p=ui_hotword_search';
    126 
    127 
    128   /**
    129    * @const {string}
    130    * @private
    131    */
    132   AudioClient.CLIENT_PORT_NAME_ = 'chwcpn';
    133 
    134   /**
    135    * Existence of the Audio Client.
    136    * @const {string}
    137    * @private
    138    */
    139   AudioClient.EXISTS_ = 'chwace';
    140 
    141 
    142   /**
    143    * Checks for the presence of speech overlay UI DOM elements.
    144    * @private
    145    */
    146   AudioClient.prototype.checkSpeechOverlayUi_ = function() {
    147     if (!this.speechOverlay_) {
    148       window.setTimeout(this.delayedCheckSpeechOverlayUi_.bind(this),
    149                         AudioClient.RETRY_TIME_MS_);
    150     } else {
    151       this.checkSpeechUiRetries_ = 0;
    152     }
    153   };
    154 
    155 
    156   /**
    157    * Function called to check for the speech UI overlay after some time has
    158    * passed since an initial check. Will either retry triggering the speech
    159    * or sends an error message depending on the number of retries.
    160    * @private
    161    */
    162   AudioClient.prototype.delayedCheckSpeechOverlayUi_ = function() {
    163     this.speechOverlay_ = document.getElementById(
    164         AudioClient.SPEECH_UI_OVERLAY_ID_);
    165     if (!this.speechOverlay_) {
    166       if (this.checkSpeechUiRetries_++ < AudioClient.MAX_RETRIES) {
    167         this.sendCommandToPage_(AudioClient.CommandToPage.VOICE_TRIGGER);
    168         this.checkSpeechOverlayUi_();
    169       } else {
    170         this.sendCommandToExtension_(AudioClient.Error.NO_SPEECH_UI);
    171       }
    172     } else {
    173       this.checkSpeechUiRetries_ = 0;
    174     }
    175   };
    176 
    177 
    178   /**
    179    * Checks that the triggered UI is actually displayed.
    180    * @param {AudioClient.CommandToPage} command Command that was send.
    181    * @private
    182    */
    183   AudioClient.prototype.checkUi_ = function(command) {
    184     this.uiStatus_[command].timeoutId =
    185         window.setTimeout(this.failedCheckUi_.bind(this, command),
    186                           AudioClient.RETRY_TIME_MS_);
    187   };
    188 
    189 
    190   /**
    191    * Function called when the UI verification is not called in time. Will either
    192    * retry the command or sends an error message, depending on the number of
    193    * retries for the command.
    194    * @param {AudioClient.CommandToPage} command Command that was sent.
    195    * @private
    196    */
    197   AudioClient.prototype.failedCheckUi_ = function(command) {
    198     if (this.uiStatus_[command].tries++ < AudioClient.MAX_RETRIES) {
    199       this.sendCommandToPage_(command);
    200       this.checkUi_(command);
    201     } else {
    202       this.sendCommandToExtension_(this.uiStatus_[command].error);
    203     }
    204   };
    205 
    206 
    207   /**
    208    * Confirm that an UI element has been shown.
    209    * @param {AudioClient.CommandToPage} command UI to confirm.
    210    * @private
    211    */
    212   AudioClient.prototype.verifyUi_ = function(command) {
    213     if (this.uiStatus_[command].timeoutId) {
    214       window.clearTimeout(this.uiStatus_[command].timeoutId);
    215       this.uiStatus_[command].timeoutId = null;
    216       this.uiStatus_[command].tries = 0;
    217     }
    218   };
    219 
    220 
    221   /**
    222    * Sends a command to the audio manager.
    223    * @param {string} commandStr command to send to plugin.
    224    * @private
    225    */
    226   AudioClient.prototype.sendCommandToExtension_ = function(commandStr) {
    227     if (this.port_)
    228       this.port_.postMessage({'cmd': commandStr});
    229   };
    230 
    231 
    232   /**
    233    * Handles a message from the audio manager.
    234    * @param {{cmd: string}} commandObj Command from the audio manager.
    235    * @private
    236    */
    237   AudioClient.prototype.handleCommandFromExtension_ = function(commandObj) {
    238     var command = commandObj['cmd'];
    239     if (command) {
    240       switch (command) {
    241         case AudioClient.CommandToPage.HOTWORD_VOICE_TRIGGER:
    242           this.sendCommandToPage_(command);
    243           this.checkSpeechOverlayUi_();
    244           break;
    245         case AudioClient.CommandToPage.HOTWORD_STARTED:
    246           this.sendCommandToPage_(command);
    247           this.checkUi_(command);
    248           break;
    249         case AudioClient.CommandToPage.HOTWORD_ENDED:
    250           this.sendCommandToPage_(command);
    251           break;
    252         case AudioClient.CommandToPage.HOTWORD_TIMEOUT:
    253           this.sendCommandToPage_(command);
    254           this.checkUi_(command);
    255           break;
    256         case AudioClient.CommandToPage.HOTWORD_ERROR:
    257           this.sendCommandToPage_(command);
    258           this.checkUi_(command);
    259           break;
    260       }
    261     }
    262   };
    263 
    264 
    265   /**
    266    * @param {AudioClient.CommandToPage} commandStr Command to send.
    267    * @private
    268    */
    269   AudioClient.prototype.sendCommandToPage_ = function(commandStr) {
    270     window.postMessage({'type': commandStr}, '*');
    271   };
    272 
    273 
    274   /**
    275    * Handles a message from the html window.
    276    * @param {!MessageEvent} messageEvent Message event from the window.
    277    * @private
    278    */
    279   AudioClient.prototype.handleCommandFromPage_ = function(messageEvent) {
    280     if (messageEvent.source == window && messageEvent.data.type) {
    281       var command = messageEvent.data.type;
    282       switch (command) {
    283         case AudioClient.CommandFromPage.SPEECH_START:
    284           this.speechActive_ = true;
    285           this.sendCommandToExtension_(command);
    286           break;
    287         case AudioClient.CommandFromPage.SPEECH_END:
    288           this.speechActive_ = false;
    289           this.sendCommandToExtension_(command);
    290           break;
    291         case AudioClient.CommandFromPage.SPEECH_RESET:
    292           this.speechActive_ = false;
    293           this.sendCommandToExtension_(command);
    294           break;
    295         case 'SPEECH_RESET':  // Legacy, for embedded NTP.
    296           this.speechActive_ = false;
    297           this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_END);
    298           break;
    299         case AudioClient.CommandFromPage.CLICKED_RESUME:
    300           this.sendCommandToExtension_(command);
    301           break;
    302         case AudioClient.CommandFromPage.CLICKED_RESTART:
    303           this.sendCommandToExtension_(command);
    304           break;
    305         case AudioClient.CommandFromPage.CLICKED_DEBUG:
    306           window.open(AudioClient.HELP_CENTER_URL_, '_blank');
    307           break;
    308         case AudioClient.CommandFromPage.SHOWING_HOTWORD_START:
    309           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_STARTED);
    310           break;
    311         case AudioClient.CommandFromPage.SHOWING_ERROR_MESSAGE:
    312           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_ERROR);
    313           break;
    314         case AudioClient.CommandFromPage.SHOWING_TIMEOUT_MESSAGE:
    315           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_TIMEOUT);
    316           break;
    317       }
    318     }
    319   };
    320 
    321 
    322   /**
    323    * Initialize the content script.
    324    */
    325   AudioClient.prototype.initialize = function() {
    326     if (AudioClient.EXISTS_ in window)
    327       return;
    328     window[AudioClient.EXISTS_] = true;
    329 
    330     // UI verification object.
    331     this.uiStatus_ = {};
    332     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_STARTED] = {
    333       timeoutId: null,
    334       tries: 0,
    335       error: AudioClient.Error.NO_HOTWORD_STARTED_UI
    336     };
    337     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_TIMEOUT] = {
    338       timeoutId: null,
    339       tries: 0,
    340       error: AudioClient.Error.NO_HOTWORD_TIMEOUT_UI
    341     };
    342     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_ERROR] = {
    343       timeoutId: null,
    344       tries: 0,
    345       error: AudioClient.Error.NO_HOTWORD_ERROR_UI
    346     };
    347 
    348     this.handleCommandFromPageFunc_ = this.handleCommandFromPage_.bind(this);
    349     window.addEventListener('message', this.handleCommandFromPageFunc_, false);
    350     this.initPort_();
    351   };
    352 
    353 
    354   /**
    355    * Initialize the communications port with the audio manager. This
    356    * function will be also be called again if the audio-manager
    357    * disconnects for some reason (such as the extension
    358    * background.html page being reloaded).
    359    * @private
    360    */
    361   AudioClient.prototype.initPort_ = function() {
    362     this.port_ = chrome.runtime.connect(
    363         AudioClient.HOTWORD_EXTENSION_ID_,
    364         {'name': AudioClient.CLIENT_PORT_NAME_});
    365     // Note that this listen may have to be destroyed manually if AudioClient
    366     // is ever destroyed on this tab.
    367     this.port_.onDisconnect.addListener(
    368         (function(e) {
    369           if (this.handleCommandFromPageFunc_) {
    370             window.removeEventListener(
    371                 'message', this.handleCommandFromPageFunc_, false);
    372           }
    373           delete window[AudioClient.EXISTS_];
    374         }).bind(this));
    375 
    376     // See note above.
    377     this.port_.onMessage.addListener(
    378         this.handleCommandFromExtension_.bind(this));
    379 
    380     if (this.speechActive_)
    381       this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_START);
    382   };
    383 
    384 
    385   // Initializes as soon as the code is ready, do not wait for the page.
    386   new AudioClient().initialize();
    387 })();
    388