• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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