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.
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
19 var AudioClient = function() {
20 /** @private {Element} */
21 this.speechOverlay_
= null;
23 /** @private {number} */
24 this.checkSpeechUiRetries_
= 0;
27 * Port used to communicate with the audio manager.
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>}
37 this.uiStatus_
= null;
40 * Bound function used to handle commands sent from the page to this script.
43 this.handleCommandFromPageFunc_
= null;
48 * Messages sent to the page to control the voice search UI.
51 AudioClient
.CommandToPage
= {
52 HOTWORD_VOICE_TRIGGER
: 'vt',
53 HOTWORD_STARTED
: 'hs',
55 HOTWORD_TIMEOUT
: 'ht',
61 * Messages received from the page used to indicate voice search state.
64 AudioClient
.CommandFromPage
= {
68 SHOWING_HOTWORD_START
: 'shs',
69 SHOWING_ERROR_MESSAGE
: 'sem',
70 SHOWING_TIMEOUT_MESSAGE
: 'stm',
71 CLICKED_RESUME
: 'hcc',
72 CLICKED_RESTART
: 'hcr',
78 * Errors that are sent to the hotword extension.
83 NO_HOTWORD_STARTED_UI
: 'ac2',
84 NO_HOTWORD_TIMEOUT_UI
: 'ac3',
85 NO_HOTWORD_ERROR_UI
: 'ac4'
93 AudioClient
.HOTWORD_EXTENSION_ID_
= 'bepbmhgboaologfdajaanbcjmnhjmhfn';
97 * Number of times to retry checking a transient error.
101 AudioClient
.MAX_RETRIES
= 3;
105 * Delay to wait in milliseconds before rechecking for any transient errors.
109 AudioClient
.RETRY_TIME_MS_
= 2000;
113 * DOM ID for the speech UI overlay.
117 AudioClient
.SPEECH_UI_OVERLAY_ID_
= 'spch';
124 AudioClient
.HELP_CENTER_URL_
=
125 'https://support.google.com/chrome/?p=ui_hotword_search';
132 AudioClient
.CLIENT_PORT_NAME_
= 'chwcpn';
135 * Existence of the Audio Client.
139 AudioClient
.EXISTS_
= 'chwace';
143 * Checks for the presence of speech overlay UI DOM elements.
146 AudioClient
.prototype.checkSpeechOverlayUi_ = function() {
147 if (!this.speechOverlay_
) {
148 window
.setTimeout(this.delayedCheckSpeechOverlayUi_
.bind(this),
149 AudioClient
.RETRY_TIME_MS_
);
151 this.checkSpeechUiRetries_
= 0;
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.
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_();
170 this.sendCommandToExtension_(AudioClient
.Error
.NO_SPEECH_UI
);
173 this.checkSpeechUiRetries_
= 0;
179 * Checks that the triggered UI is actually displayed.
180 * @param {AudioClient.CommandToPage} command Command that was send.
183 AudioClient
.prototype.checkUi_ = function(command
) {
184 this.uiStatus_
[command
].timeoutId
=
185 window
.setTimeout(this.failedCheckUi_
.bind(this, command
),
186 AudioClient
.RETRY_TIME_MS_
);
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.
197 AudioClient
.prototype.failedCheckUi_ = function(command
) {
198 if (this.uiStatus_
[command
].tries
++ < AudioClient
.MAX_RETRIES
) {
199 this.sendCommandToPage_(command
);
200 this.checkUi_(command
);
202 this.sendCommandToExtension_(this.uiStatus_
[command
].error
);
208 * Confirm that an UI element has been shown.
209 * @param {AudioClient.CommandToPage} command UI to confirm.
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;
222 * Sends a command to the audio manager.
223 * @param {string} commandStr command to send to plugin.
226 AudioClient
.prototype.sendCommandToExtension_ = function(commandStr
) {
228 this.port_
.postMessage({'cmd': commandStr
});
233 * Handles a message from the audio manager.
234 * @param {{cmd: string}} commandObj Command from the audio manager.
237 AudioClient
.prototype.handleCommandFromExtension_ = function(commandObj
) {
238 var command
= commandObj
['cmd'];
241 case AudioClient
.CommandToPage
.HOTWORD_VOICE_TRIGGER
:
242 this.sendCommandToPage_(command
);
243 this.checkSpeechOverlayUi_();
245 case AudioClient
.CommandToPage
.HOTWORD_STARTED
:
246 this.sendCommandToPage_(command
);
247 this.checkUi_(command
);
249 case AudioClient
.CommandToPage
.HOTWORD_ENDED
:
250 this.sendCommandToPage_(command
);
252 case AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
:
253 this.sendCommandToPage_(command
);
254 this.checkUi_(command
);
256 case AudioClient
.CommandToPage
.HOTWORD_ERROR
:
257 this.sendCommandToPage_(command
);
258 this.checkUi_(command
);
266 * @param {AudioClient.CommandToPage} commandStr Command to send.
269 AudioClient
.prototype.sendCommandToPage_ = function(commandStr
) {
270 window
.postMessage({'type': commandStr
}, '*');
275 * Handles a message from the html window.
276 * @param {!MessageEvent} messageEvent Message event from the window.
279 AudioClient
.prototype.handleCommandFromPage_ = function(messageEvent
) {
280 if (messageEvent
.source
== window
&& messageEvent
.data
.type
) {
281 var command
= messageEvent
.data
.type
;
283 case AudioClient
.CommandFromPage
.SPEECH_START
:
284 this.speechActive_
= true;
285 this.sendCommandToExtension_(command
);
287 case AudioClient
.CommandFromPage
.SPEECH_END
:
288 this.speechActive_
= false;
289 this.sendCommandToExtension_(command
);
291 case AudioClient
.CommandFromPage
.SPEECH_RESET
:
292 this.speechActive_
= false;
293 this.sendCommandToExtension_(command
);
295 case 'SPEECH_RESET': // Legacy, for embedded NTP.
296 this.speechActive_
= false;
297 this.sendCommandToExtension_(AudioClient
.CommandFromPage
.SPEECH_END
);
299 case AudioClient
.CommandFromPage
.CLICKED_RESUME
:
300 this.sendCommandToExtension_(command
);
302 case AudioClient
.CommandFromPage
.CLICKED_RESTART
:
303 this.sendCommandToExtension_(command
);
305 case AudioClient
.CommandFromPage
.CLICKED_DEBUG
:
306 window
.open(AudioClient
.HELP_CENTER_URL_
, '_blank');
308 case AudioClient
.CommandFromPage
.SHOWING_HOTWORD_START
:
309 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_STARTED
);
311 case AudioClient
.CommandFromPage
.SHOWING_ERROR_MESSAGE
:
312 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_ERROR
);
314 case AudioClient
.CommandFromPage
.SHOWING_TIMEOUT_MESSAGE
:
315 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
);
323 * Initialize the content script.
325 AudioClient
.prototype.initialize = function() {
326 if (AudioClient
.EXISTS_
in window
)
328 window
[AudioClient
.EXISTS_
] = true;
330 // UI verification object.
332 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_STARTED
] = {
335 error
: AudioClient
.Error
.NO_HOTWORD_STARTED_UI
337 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
] = {
340 error
: AudioClient
.Error
.NO_HOTWORD_TIMEOUT_UI
342 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_ERROR
] = {
345 error
: AudioClient
.Error
.NO_HOTWORD_ERROR_UI
348 this.handleCommandFromPageFunc_
= this.handleCommandFromPage_
.bind(this);
349 window
.addEventListener('message', this.handleCommandFromPageFunc_
, false);
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).
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(
369 if (this.handleCommandFromPageFunc_
) {
370 window
.removeEventListener(
371 'message', this.handleCommandFromPageFunc_
, false);
373 delete window
[AudioClient
.EXISTS_
];
377 this.port_
.onMessage
.addListener(
378 this.handleCommandFromExtension_
.bind(this));
380 if (this.speechActive_
)
381 this.sendCommandToExtension_(AudioClient
.CommandFromPage
.SPEECH_START
);
385 // Initializes as soon as the code is ready, do not wait for the page.
386 new AudioClient().initialize();