Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / hotword / audio_client.js
blobf74f84b15f372f3d49363ddb4cf2fd6560e07640
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.
5 'use strict';
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  */
13 (function() {
14   /**
15    * @constructor
16    */
17   var AudioClient = function() {
18     /** @private {Element} */
19     this.speechOverlay_ = null;
21     /** @private {number} */
22     this.checkSpeechUiRetries_ = 0;
24     /**
25      * Port used to communicate with the audio manager.
26      * @private {?Port}
27      */
28     this.port_ = null;
30     /**
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>}
34      */
35     this.uiStatus_ = null;
37     /**
38      * Bound function used to handle commands sent from the page to this script.
39      * @private {Function}
40      */
41     this.handleCommandFromPageFunc_ = null;
42   };
44   /**
45    * Messages sent to the page to control the voice search UI.
46    * @enum {string}
47    */
48   AudioClient.CommandToPage = {
49     HOTWORD_VOICE_TRIGGER: 'vt',
50     HOTWORD_STARTED: 'hs',
51     HOTWORD_ENDED: 'hd',
52     HOTWORD_TIMEOUT: 'ht',
53     HOTWORD_ERROR: 'he'
54   };
56   /**
57    * Messages received from the page used to indicate voice search state.
58    * @enum {string}
59    */
60   AudioClient.CommandFromPage = {
61     SPEECH_START: 'ss',
62     SPEECH_END: 'se',
63     SPEECH_RESET: 'sr',
64     SHOWING_HOTWORD_START: 'shs',
65     SHOWING_ERROR_MESSAGE: 'sem',
66     SHOWING_TIMEOUT_MESSAGE: 'stm',
67     CLICKED_RESUME: 'hcc',
68     CLICKED_RESTART: 'hcr',
69     CLICKED_DEBUG: 'hcd'
70   };
72   /**
73    * Errors that are sent to the hotword extension.
74    * @enum {string}
75    */
76   AudioClient.Error = {
77     NO_SPEECH_UI: 'ac1',
78     NO_HOTWORD_STARTED_UI: 'ac2',
79     NO_HOTWORD_TIMEOUT_UI: 'ac3',
80     NO_HOTWORD_ERROR_UI: 'ac4'
81   };
83   /**
84    * @const {string}
85    * @private
86    */
87   AudioClient.HOTWORD_EXTENSION_ID_ = 'nbpagnldghgfoolbancepceaanlmhfmd';
89   /**
90    * Number of times to retry checking a transient error.
91    * @const {number}
92    * @private
93    */
94   AudioClient.MAX_RETRIES = 3;
96   /**
97    * Delay to wait in milliseconds before rechecking for any transient errors.
98    * @const {number}
99    * @private
100    */
101   AudioClient.RETRY_TIME_MS_ = 2000;
103   /**
104    * DOM ID for the speech UI overlay.
105    * @const {string}
106    * @private
107    */
108   AudioClient.SPEECH_UI_OVERLAY_ID_ = 'spch';
110   /**
111    * @const {string}
112    * @private
113    */
114   AudioClient.HELP_CENTER_URL_ =
115       'https://support.google.com/chrome/?p=ui_hotword_search';
117   /**
118    * @const {string}
119    * @private
120    */
121   AudioClient.CLIENT_PORT_NAME_ = 'chwcpn';
123   /**
124    * Existence of the Audio Client.
125    * @const {string}
126    * @private
127    */
128   AudioClient.EXISTS_ = 'chwace';
130   /**
131    * Checks for the presence of speech overlay UI DOM elements.
132    * @private
133    */
134   AudioClient.prototype.checkSpeechOverlayUi_ = function() {
135     if (!this.speechOverlay_) {
136       window.setTimeout(this.delayedCheckSpeechOverlayUi_.bind(this),
137                         AudioClient.RETRY_TIME_MS_);
138     } else {
139       this.checkSpeechUiRetries_ = 0;
140     }
141   };
143   /**
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.
147    * @private
148    */
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_();
156       } else {
157         this.sendCommandToExtension_(AudioClient.Error.NO_SPEECH_UI);
158       }
159     } else {
160       this.checkSpeechUiRetries_ = 0;
161     }
162   };
164   /**
165    * Checks that the triggered UI is actually displayed.
166    * @param {AudioClient.CommandToPage} command Command that was send.
167    * @private
168    */
169   AudioClient.prototype.checkUi_ = function(command) {
170     this.uiStatus_[command].timeoutId =
171         window.setTimeout(this.failedCheckUi_.bind(this, command),
172                           AudioClient.RETRY_TIME_MS_);
173   };
175   /**
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.
180    * @private
181    */
182   AudioClient.prototype.failedCheckUi_ = function(command) {
183     if (this.uiStatus_[command].tries++ < AudioClient.MAX_RETRIES) {
184       this.sendCommandToPage_(command);
185       this.checkUi_(command);
186     } else {
187       this.sendCommandToExtension_(this.uiStatus_[command].error);
188     }
189   };
191   /**
192    * Confirm that an UI element has been shown.
193    * @param {AudioClient.CommandToPage} command UI to confirm.
194    * @private
195    */
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;
201     }
202   };
204   /**
205    * Sends a command to the audio manager.
206    * @param {string} commandStr command to send to plugin.
207    * @private
208    */
209   AudioClient.prototype.sendCommandToExtension_ = function(commandStr) {
210     if (this.port_)
211       this.port_.postMessage({'cmd': commandStr});
212   };
214   /**
215    * Handles a message from the audio manager.
216    * @param {{cmd: string}} commandObj Command from the audio manager.
217    * @private
218    */
219   AudioClient.prototype.handleCommandFromExtension_ = function(commandObj) {
220     var command = commandObj['cmd'];
221     if (command) {
222       switch (command) {
223         case AudioClient.CommandToPage.HOTWORD_VOICE_TRIGGER:
224           this.sendCommandToPage_(command);
225           this.checkSpeechOverlayUi_();
226           break;
227         case AudioClient.CommandToPage.HOTWORD_STARTED:
228           this.sendCommandToPage_(command);
229           this.checkUi_(command);
230           break;
231         case AudioClient.CommandToPage.HOTWORD_ENDED:
232           this.sendCommandToPage_(command);
233           break;
234         case AudioClient.CommandToPage.HOTWORD_TIMEOUT:
235           this.sendCommandToPage_(command);
236           this.checkUi_(command);
237           break;
238         case AudioClient.CommandToPage.HOTWORD_ERROR:
239           this.sendCommandToPage_(command);
240           this.checkUi_(command);
241           break;
242       }
243     }
244   };
246   /**
247    * @param {AudioClient.CommandToPage} commandStr Command to send.
248    * @private
249    */
250   AudioClient.prototype.sendCommandToPage_ = function(commandStr) {
251     window.postMessage({'type': commandStr}, '*');
252   };
254   /**
255    * Handles a message from the html window.
256    * @param {!MessageEvent} messageEvent Message event from the window.
257    * @private
258    */
259   AudioClient.prototype.handleCommandFromPage_ = function(messageEvent) {
260     if (messageEvent.source == window && messageEvent.data.type) {
261       var command = messageEvent.data.type;
262       switch (command) {
263         case AudioClient.CommandFromPage.SPEECH_START:
264           this.speechActive_ = true;
265           this.sendCommandToExtension_(command);
266           break;
267         case AudioClient.CommandFromPage.SPEECH_END:
268           this.speechActive_ = false;
269           this.sendCommandToExtension_(command);
270           break;
271         case AudioClient.CommandFromPage.SPEECH_RESET:
272           this.speechActive_ = false;
273           this.sendCommandToExtension_(command);
274           break;
275         case 'SPEECH_RESET':  // Legacy, for embedded NTP.
276           this.speechActive_ = false;
277           this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_END);
278           break;
279         case AudioClient.CommandFromPage.CLICKED_RESUME:
280           this.sendCommandToExtension_(command);
281           break;
282         case AudioClient.CommandFromPage.CLICKED_RESTART:
283           this.sendCommandToExtension_(command);
284           break;
285         case AudioClient.CommandFromPage.CLICKED_DEBUG:
286           window.open(AudioClient.HELP_CENTER_URL_, '_blank');
287           break;
288         case AudioClient.CommandFromPage.SHOWING_HOTWORD_START:
289           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_STARTED);
290           break;
291         case AudioClient.CommandFromPage.SHOWING_ERROR_MESSAGE:
292           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_ERROR);
293           break;
294         case AudioClient.CommandFromPage.SHOWING_TIMEOUT_MESSAGE:
295           this.verifyUi_(AudioClient.CommandToPage.HOTWORD_TIMEOUT);
296           break;
297       }
298     }
299   };
301   /**
302    * Initialize the content script.
303    */
304   AudioClient.prototype.initialize = function() {
305     if (AudioClient.EXISTS_ in window)
306       return;
307     window[AudioClient.EXISTS_] = true;
309     // UI verification object.
310     this.uiStatus_ = {};
311     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_STARTED] = {
312       timeoutId: null,
313       tries: 0,
314       error: AudioClient.Error.NO_HOTWORD_STARTED_UI
315     };
316     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_TIMEOUT] = {
317       timeoutId: null,
318       tries: 0,
319       error: AudioClient.Error.NO_HOTWORD_TIMEOUT_UI
320     };
321     this.uiStatus_[AudioClient.CommandToPage.HOTWORD_ERROR] = {
322       timeoutId: null,
323       tries: 0,
324       error: AudioClient.Error.NO_HOTWORD_ERROR_UI
325     };
327     this.handleCommandFromPageFunc_ = this.handleCommandFromPage_.bind(this);
328     window.addEventListener('message', this.handleCommandFromPageFunc_, false);
329     this.initPort_();
330   };
332   /**
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).
337    * @private
338    */
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(
346         (function(e) {
347           if (this.handleCommandFromPageFunc_) {
348             window.removeEventListener(
349                 'message', this.handleCommandFromPageFunc_, false);
350           }
351           delete window[AudioClient.EXISTS_];
352         }).bind(this));
354     // See note above.
355     this.port_.onMessage.addListener(
356         this.handleCommandFromExtension_.bind(this));
358     if (this.speechActive_) {
359       this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_START);
360     } else {
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);
369     }
370   };
372   // Initializes as soon as the code is ready, do not wait for the page.
373   new AudioClient().initialize();
374 })();