1 // Copyright 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
17 var AudioClient = function() {
18 /** @private {Element} */
19 this.speechOverlay_
= null;
21 /** @private {number} */
22 this.checkSpeechUiRetries_
= 0;
25 * Port used to communicate with the audio manager.
31 * Keeps track of the effects of different commands. Used to verify that
32 * proper UIs are shown to the user.
33 * @private {Object<AudioClient.CommandToPage, Object>}
35 this.uiStatus_
= null;
38 * Bound function used to handle commands sent from the page to this script.
41 this.handleCommandFromPageFunc_
= null;
45 * Messages sent to the page to control the voice search UI.
48 AudioClient
.CommandToPage
= {
49 HOTWORD_VOICE_TRIGGER
: 'vt',
50 HOTWORD_STARTED
: 'hs',
52 HOTWORD_TIMEOUT
: 'ht',
57 * Messages received from the page used to indicate voice search state.
60 AudioClient
.CommandFromPage
= {
64 SHOWING_HOTWORD_START
: 'shs',
65 SHOWING_ERROR_MESSAGE
: 'sem',
66 SHOWING_TIMEOUT_MESSAGE
: 'stm',
67 CLICKED_RESUME
: 'hcc',
68 CLICKED_RESTART
: 'hcr',
73 * Errors that are sent to the hotword extension.
78 NO_HOTWORD_STARTED_UI
: 'ac2',
79 NO_HOTWORD_TIMEOUT_UI
: 'ac3',
80 NO_HOTWORD_ERROR_UI
: 'ac4'
87 AudioClient
.HOTWORD_EXTENSION_ID_
= 'nbpagnldghgfoolbancepceaanlmhfmd';
90 * Number of times to retry checking a transient error.
94 AudioClient
.MAX_RETRIES
= 3;
97 * Delay to wait in milliseconds before rechecking for any transient errors.
101 AudioClient
.RETRY_TIME_MS_
= 2000;
104 * DOM ID for the speech UI overlay.
108 AudioClient
.SPEECH_UI_OVERLAY_ID_
= 'spch';
114 AudioClient
.HELP_CENTER_URL_
=
115 'https://support.google.com/chrome/?p=ui_hotword_search';
121 AudioClient
.CLIENT_PORT_NAME_
= 'chwcpn';
124 * Existence of the Audio Client.
128 AudioClient
.EXISTS_
= 'chwace';
131 * Checks for the presence of speech overlay UI DOM elements.
134 AudioClient
.prototype.checkSpeechOverlayUi_ = function() {
135 if (!this.speechOverlay_
) {
136 window
.setTimeout(this.delayedCheckSpeechOverlayUi_
.bind(this),
137 AudioClient
.RETRY_TIME_MS_
);
139 this.checkSpeechUiRetries_
= 0;
144 * Function called to check for the speech UI overlay after some time has
145 * passed since an initial check. Will either retry triggering the speech
146 * or sends an error message depending on the number of retries.
149 AudioClient
.prototype.delayedCheckSpeechOverlayUi_ = function() {
150 this.speechOverlay_
= document
.getElementById(
151 AudioClient
.SPEECH_UI_OVERLAY_ID_
);
152 if (!this.speechOverlay_
) {
153 if (this.checkSpeechUiRetries_
++ < AudioClient
.MAX_RETRIES
) {
154 this.sendCommandToPage_(AudioClient
.CommandToPage
.VOICE_TRIGGER
);
155 this.checkSpeechOverlayUi_();
157 this.sendCommandToExtension_(AudioClient
.Error
.NO_SPEECH_UI
);
160 this.checkSpeechUiRetries_
= 0;
165 * Checks that the triggered UI is actually displayed.
166 * @param {AudioClient.CommandToPage} command Command that was send.
169 AudioClient
.prototype.checkUi_ = function(command
) {
170 this.uiStatus_
[command
].timeoutId
=
171 window
.setTimeout(this.failedCheckUi_
.bind(this, command
),
172 AudioClient
.RETRY_TIME_MS_
);
176 * Function called when the UI verification is not called in time. Will either
177 * retry the command or sends an error message, depending on the number of
178 * retries for the command.
179 * @param {AudioClient.CommandToPage} command Command that was sent.
182 AudioClient
.prototype.failedCheckUi_ = function(command
) {
183 if (this.uiStatus_
[command
].tries
++ < AudioClient
.MAX_RETRIES
) {
184 this.sendCommandToPage_(command
);
185 this.checkUi_(command
);
187 this.sendCommandToExtension_(this.uiStatus_
[command
].error
);
192 * Confirm that an UI element has been shown.
193 * @param {AudioClient.CommandToPage} command UI to confirm.
196 AudioClient
.prototype.verifyUi_ = function(command
) {
197 if (this.uiStatus_
[command
].timeoutId
) {
198 window
.clearTimeout(this.uiStatus_
[command
].timeoutId
);
199 this.uiStatus_
[command
].timeoutId
= null;
200 this.uiStatus_
[command
].tries
= 0;
205 * Sends a command to the audio manager.
206 * @param {string} commandStr command to send to plugin.
209 AudioClient
.prototype.sendCommandToExtension_ = function(commandStr
) {
211 this.port_
.postMessage({'cmd': commandStr
});
215 * Handles a message from the audio manager.
216 * @param {{cmd: string}} commandObj Command from the audio manager.
219 AudioClient
.prototype.handleCommandFromExtension_ = function(commandObj
) {
220 var command
= commandObj
['cmd'];
223 case AudioClient
.CommandToPage
.HOTWORD_VOICE_TRIGGER
:
224 this.sendCommandToPage_(command
);
225 this.checkSpeechOverlayUi_();
227 case AudioClient
.CommandToPage
.HOTWORD_STARTED
:
228 this.sendCommandToPage_(command
);
229 this.checkUi_(command
);
231 case AudioClient
.CommandToPage
.HOTWORD_ENDED
:
232 this.sendCommandToPage_(command
);
234 case AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
:
235 this.sendCommandToPage_(command
);
236 this.checkUi_(command
);
238 case AudioClient
.CommandToPage
.HOTWORD_ERROR
:
239 this.sendCommandToPage_(command
);
240 this.checkUi_(command
);
247 * @param {AudioClient.CommandToPage} commandStr Command to send.
250 AudioClient
.prototype.sendCommandToPage_ = function(commandStr
) {
251 window
.postMessage({'type': commandStr
}, '*');
255 * Handles a message from the html window.
256 * @param {!MessageEvent} messageEvent Message event from the window.
259 AudioClient
.prototype.handleCommandFromPage_ = function(messageEvent
) {
260 if (messageEvent
.source
== window
&& messageEvent
.data
.type
) {
261 var command
= messageEvent
.data
.type
;
263 case AudioClient
.CommandFromPage
.SPEECH_START
:
264 this.speechActive_
= true;
265 this.sendCommandToExtension_(command
);
267 case AudioClient
.CommandFromPage
.SPEECH_END
:
268 this.speechActive_
= false;
269 this.sendCommandToExtension_(command
);
271 case AudioClient
.CommandFromPage
.SPEECH_RESET
:
272 this.speechActive_
= false;
273 this.sendCommandToExtension_(command
);
275 case 'SPEECH_RESET': // Legacy, for embedded NTP.
276 this.speechActive_
= false;
277 this.sendCommandToExtension_(AudioClient
.CommandFromPage
.SPEECH_END
);
279 case AudioClient
.CommandFromPage
.CLICKED_RESUME
:
280 this.sendCommandToExtension_(command
);
282 case AudioClient
.CommandFromPage
.CLICKED_RESTART
:
283 this.sendCommandToExtension_(command
);
285 case AudioClient
.CommandFromPage
.CLICKED_DEBUG
:
286 window
.open(AudioClient
.HELP_CENTER_URL_
, '_blank');
288 case AudioClient
.CommandFromPage
.SHOWING_HOTWORD_START
:
289 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_STARTED
);
291 case AudioClient
.CommandFromPage
.SHOWING_ERROR_MESSAGE
:
292 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_ERROR
);
294 case AudioClient
.CommandFromPage
.SHOWING_TIMEOUT_MESSAGE
:
295 this.verifyUi_(AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
);
302 * Initialize the content script.
304 AudioClient
.prototype.initialize = function() {
305 if (AudioClient
.EXISTS_
in window
)
307 window
[AudioClient
.EXISTS_
] = true;
309 // UI verification object.
311 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_STARTED
] = {
314 error
: AudioClient
.Error
.NO_HOTWORD_STARTED_UI
316 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_TIMEOUT
] = {
319 error
: AudioClient
.Error
.NO_HOTWORD_TIMEOUT_UI
321 this.uiStatus_
[AudioClient
.CommandToPage
.HOTWORD_ERROR
] = {
324 error
: AudioClient
.Error
.NO_HOTWORD_ERROR_UI
327 this.handleCommandFromPageFunc_
= this.handleCommandFromPage_
.bind(this);
328 window
.addEventListener('message', this.handleCommandFromPageFunc_
, false);
333 * Initialize the communications port with the audio manager. This
334 * function will be also be called again if the audio-manager
335 * disconnects for some reason (such as the extension
336 * background.html page being reloaded).
339 AudioClient
.prototype.initPort_ = function() {
340 this.port_
= chrome
.runtime
.connect(
341 AudioClient
.HOTWORD_EXTENSION_ID_
,
342 {'name': AudioClient
.CLIENT_PORT_NAME_
});
343 // Note that this listen may have to be destroyed manually if AudioClient
344 // is ever destroyed on this tab.
345 this.port_
.onDisconnect
.addListener(
347 if (this.handleCommandFromPageFunc_
) {
348 window
.removeEventListener(
349 'message', this.handleCommandFromPageFunc_
, false);
351 delete window
[AudioClient
.EXISTS_
];
355 this.port_
.onMessage
.addListener(
356 this.handleCommandFromExtension_
.bind(this));
358 if (this.speechActive_
) {
359 this.sendCommandToExtension_(AudioClient
.CommandFromPage
.SPEECH_START
);
361 // It's possible for this script to be injected into the page after it has
362 // completed loaded (i.e. when prerendering). In this case, this script
363 // won't receive a SPEECH_RESET from the page to forward onto the
364 // extension. To make up for this, always send a SPEECH_RESET. This means
365 // in most cases, the extension will receive SPEECH_RESET twice, one from
366 // this sendCommandToExtension_ and the one forwarded from the page. But
367 // that's OK and the extension can handle it.
368 this.sendCommandToExtension_(AudioClient
.CommandFromPage
.SPEECH_RESET
);
372 // Initializes as soon as the code is ready, do not wait for the page.
373 new AudioClient().initialize();