OLD | NEW |
| (Empty) |
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 })(); | |
OLD | NEW |