· 6 years ago · Oct 11, 2019, 04:50 PM
1//// Made this one myself ;)
2
3'use strict';
4
5/**
6 * Get the preferred language for UI localization. Represents Chrome's UI
7 * language, which might not coincide with the user's "preferred" language
8 * in the Settings. For more details, see:
9 * - https://developer.mozilla.org/en/docs/Web/API/NavigatorLanguage/language
10 * - https://developer.mozilla.org/en/docs/Web/API/NavigatorLanguage/languages
11 *
12 * The returned value is a language version string as defined in
13 * <a href="http://www.ietf.org/rfc/bcp/bcp47.txt">BCP 47</a>.
14 * Examples: "en", "en-US", "cs-CZ", etc.
15 */
16function getChromeUILanguage() {
17 // In Chrome, |window.navigator.language| is not guaranteed to be equal to
18 // |window.navigator.languages[0]|.
19 return window.navigator.language;
20}
21
22/**
23 * The different types of user action and error events that are logged
24 * from Voice Search. This enum is used to transfer information to
25 * the renderer and is not used as a UMA enum histogram's logged value.
26 * Note: Keep in sync with common/ntp_logging_events.h
27 * @enum {!number}
28 * @const
29 */
30const LOG_TYPE = {
31 // Activated by clicking on the fakebox icon.
32 ACTION_ACTIVATE_FAKEBOX: 13,
33 // Activated by keyboard shortcut.
34 ACTION_ACTIVATE_KEYBOARD: 14,
35 // Close the voice overlay by a user's explicit action.
36 ACTION_CLOSE_OVERLAY: 15,
37 // Submitted voice query.
38 ACTION_QUERY_SUBMITTED: 16,
39 // Clicked on support link in error message.
40 ACTION_SUPPORT_LINK_CLICKED: 17,
41 // Retried by clicking Try Again link.
42 ACTION_TRY_AGAIN_LINK: 18,
43 // Retried by clicking microphone button.
44 ACTION_TRY_AGAIN_MIC_BUTTON: 10,
45 // Errors received from the Speech Recognition API.
46 ERROR_NO_SPEECH: 20,
47 ERROR_ABORTED: 21,
48 ERROR_AUDIO_CAPTURE: 22,
49 ERROR_NETWORK: 23,
50 ERROR_NOT_ALLOWED: 24,
51 ERROR_SERVICE_NOT_ALLOWED: 25,
52 ERROR_BAD_GRAMMAR: 26,
53 ERROR_LANGUAGE_NOT_SUPPORTED: 27,
54 ERROR_NO_MATCH: 28,
55 ERROR_OTHER: 29
56};
57
58/**
59 * Enum for keyboard event codes.
60 * @enum {!string}
61 * @const
62 */
63const KEYCODE = {
64 ENTER: 'Enter',
65 ESC: 'Escape',
66 NUMPAD_ENTER: 'NumpadEnter',
67 PERIOD: 'Period',
68 SPACE: 'Space',
69 TAB: 'Tab'
70};
71
72/**
73 * The set of possible recognition errors.
74 * @enum {!number}
75 * @const
76 */
77const RecognitionError = {
78 NO_SPEECH: 0,
79 ABORTED: 1,
80 AUDIO_CAPTURE: 2,
81 NETWORK: 3,
82 NOT_ALLOWED: 4,
83 SERVICE_NOT_ALLOWED: 5,
84 BAD_GRAMMAR: 6,
85 LANGUAGE_NOT_SUPPORTED: 7,
86 NO_MATCH: 8,
87 OTHER: 9
88};
89
90/**
91 * Provides methods for communicating with the <a
92 * href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API">
93 * Web Speech API</a>, error handling and executing search queries.
94 */
95const speech = {};
96
97/**
98 * Localized translations for messages used in the Speech UI.
99 * @type {{
100 * audioError: string,
101 * details: string,
102 * languageError: string,
103 * learnMore: string,
104 * listening: string,
105 * networkError: string,
106 * noTranslation: string,
107 * noVoice: string,
108 * otherError: string,
109 * permissionError: string,
110 * ready: string,
111 * tryAgain: string,
112 * waiting: string
113 * }}
114 */
115speech.messages = {
116 audioError: '',
117 details: '',
118 languageError: '',
119 learnMore: '',
120 listening: '',
121 networkError: '',
122 noTranslation: '',
123 noVoice: '',
124 otherError: '',
125 permissionError: '',
126 ready: '',
127 tryAgain: '',
128 waiting: ''
129};
130
131/**
132 * The set of controller states.
133 * @enum {number}
134 * @private
135 */
136speech.State_ = {
137 // Initial state of the controller. It is never re-entered.
138 // The only state from which the |speech.init()| method can be called.
139 // The UI overlay is hidden, recognition is inactive.
140 UNINITIALIZED: -1,
141 // Represents a ready to be activated state. If voice search is unsuccessful
142 // for any reason, the controller will return to this state
143 // using |speech.reset_()|. The UI overlay is hidden, recognition is inactive.
144 READY: 0,
145 // Indicates that speech recognition has started, but no audio has yet
146 // been captured. The UI overlay is visible, recognition is active.
147 STARTED: 1,
148 // Indicates that audio is being captured by the Web Speech API, but no
149 // speech has yet been recognized. The UI overlay is visible and indicating
150 // that audio is being captured, recognition is active.
151 AUDIO_RECEIVED: 2,
152 // Represents a state where speech has been recognized by the Web Speech API,
153 // but no resulting transcripts have yet been received back. The UI overlay is
154 // visible and indicating that audio is being captured, recognition is active.
155 SPEECH_RECEIVED: 3,
156 // Controller state where speech has been successfully recognized and text
157 // transcripts have been reported back. The UI overlay is visible
158 // and displaying intermediate results, recognition is active.
159 // This state remains until recognition ends successfully or due to an error.
160 RESULT_RECEIVED: 4,
161 // Indicates that speech recognition has failed due to an error
162 // (or a no match error) being received from the Web Speech API.
163 // A timeout may have occurred as well. The UI overlay is visible
164 // and displaying an error message, recognition is inactive.
165 ERROR_RECEIVED: 5,
166 // Represents a state where speech recognition has been stopped
167 // (either on success or failure) and the UI has not yet reset/redirected.
168 // The UI overlay is displaying results or an error message with a timeout,
169 // after which the site will either get redirected to search results
170 // (successful) or back to the NTP by hiding the overlay (unsuccessful).
171 STOPPED: 6
172};
173
174/**
175 * Threshold for considering an interim speech transcript result as "confident
176 * enough". The more confident the API is about a transcript, the higher the
177 * confidence (number between 0 and 1).
178 * @private {number}
179 * @const
180 */
181speech.RECOGNITION_CONFIDENCE_THRESHOLD_ = 0.5;
182
183/**
184 * Time in milliseconds to wait before closing the UI after an error has
185 * occured. This is a short timeout used when no click-target is present.
186 * @private {number}
187 * @const
188 */
189speech.ERROR_TIMEOUT_SHORT_MS_ = 3000;
190
191/**
192 * Time in milliseconds to wait before closing the UI after an error has
193 * occured. This is a longer timeout used when there is a click-target is
194 * present.
195 * @private {number}
196 * @const
197 */
198speech.ERROR_TIMEOUT_LONG_MS_ = 8000;
199
200/**
201 * Time in milliseconds to wait before closing the UI if no interaction has
202 * occured.
203 * @private {number}
204 * @const
205 */
206speech.IDLE_TIMEOUT_MS_ = 8000;
207
208/**
209 * Maximum number of characters recognized before force-submitting a query.
210 * Includes characters of non-confident recognition transcripts.
211 * @private {number}
212 * @const
213 */
214speech.QUERY_LENGTH_LIMIT_ = 120;
215
216/**
217 * Specifies the current state of the controller.
218 * Note: Different than the UI state.
219 * @private {speech.State_}
220 */
221speech.currentState_ = speech.State_.UNINITIALIZED;
222
223/**
224 * The ID for the error timer.
225 * @private {number}
226 */
227speech.errorTimer_;
228
229/**
230 * The duration of the timeout for the UI elements during an error state.
231 * Depending on the error state, we have different durations for the timeout.
232 * @private {number}
233 */
234speech.errorTimeoutMs_ = 0;
235
236/**
237 * The last high confidence voice transcript received from the Web Speech API.
238 * This is the actual query that could potentially be submitted to Search.
239 * @private {string}
240 */
241speech.finalResult_;
242
243/**
244 * Base URL for sending queries to Search. Includes trailing forward slash.
245 * @private {string}
246 */
247speech.googleBaseUrl_;
248
249/**
250 * The ID for the idle timer.
251 * @private {number}
252 */
253speech.idleTimer_;
254
255/**
256 * The last low confidence voice transcript received from the Web Speech API.
257 * @private {string}
258 */
259speech.interimResult_;
260
261/**
262 * The Web Speech API object driving the speech recognition transaction.
263 * @private {!webkitSpeechRecognition}
264 */
265speech.recognition_;
266
267/**
268 * Indicates if the user is using keyboard navigation (i.e. tab).
269 * @private {boolean}
270 */
271speech.usingKeyboardNavigation_ = false;
272
273/**
274 * Log an event from Voice Search.
275 * @param {number} eventType Event from |LOG_TYPE|.
276 */
277speech.logEvent = function(eventType) {
278 window.chrome.embeddedSearch.newTabPage.logEvent(eventType);
279};
280
281/**
282 * Initialize the speech module as part of the local NTP. Adds event handlers
283 * and shows the fakebox microphone icon.
284 * @param {string} googleBaseUrl Base URL for sending queries to Search.
285 * @param {!Object} translatedStrings Dictionary of localized string messages.
286 * @param {?Element} fakeboxMicrophoneElem Fakebox microphone icon element.
287 * @param {!Object} searchboxApiHandle SearchBox API handle.
288 */
289speech.init = function(
290 googleBaseUrl, translatedStrings, fakeboxMicrophoneElem,
291 searchboxApiHandle) {
292 if (!fakeboxMicrophoneElem) {
293 throw new Error('Speech button element not found.');
294 }
295
296 if (speech.currentState_ != speech.State_.UNINITIALIZED) {
297 throw new Error(
298 'Trying to re-initialize speech when not in UNINITIALIZED state.');
299 }
300
301 // Initialize event handlers.
302 fakeboxMicrophoneElem.hidden = false;
303 fakeboxMicrophoneElem.title = translatedStrings.fakeboxMicrophoneTooltip;
304 fakeboxMicrophoneElem.onclick = function(event) {
305 // If propagated, closes the overlay (click on the background).
306 event.stopPropagation();
307 speech.logEvent(LOG_TYPE.ACTION_ACTIVATE_FAKEBOX);
308 speech.start();
309 };
310 fakeboxMicrophoneElem.onkeydown = function(event) {
311 if (!event.repeat && speech.isSpaceOrEnter_(event.code) &&
312 speech.currentState_ == speech.State_.READY) {
313 event.stopPropagation();
314 speech.start();
315 }
316 };
317 window.addEventListener('keydown', speech.onKeyDown);
318 if (searchboxApiHandle.onfocuschange) {
319 throw new Error('OnFocusChange handler already set on searchbox.');
320 }
321 searchboxApiHandle.onfocuschange = speech.onOmniboxFocused;
322
323 // Initialize speech internal state.
324 speech.googleBaseUrl_ = googleBaseUrl;
325 speech.messages = {
326 audioError: translatedStrings.audioError,
327 details: translatedStrings.details,
328 languageError: translatedStrings.languageError,
329 learnMore: translatedStrings.learnMore,
330 listening: translatedStrings.listening,
331 networkError: translatedStrings.networkError,
332 noTranslation: translatedStrings.noTranslation,
333 noVoice: translatedStrings.noVoice,
334 otherError: translatedStrings.otherError,
335 permissionError: translatedStrings.permissionError,
336 ready: translatedStrings.ready,
337 tryAgain: translatedStrings.tryAgain,
338 waiting: translatedStrings.waiting,
339 };
340 view.init(speech.onClick_);
341 view.setTitles(translatedStrings);
342 speech.initWebkitSpeech_();
343 speech.reset_();
344};
345
346/**
347 * Initializes and configures the speech recognition API.
348 * @private
349 */
350speech.initWebkitSpeech_ = function() {
351 speech.recognition_ = new webkitSpeechRecognition();
352 speech.recognition_.continuous = false;
353 speech.recognition_.interimResults = true;
354 speech.recognition_.lang = getChromeUILanguage();
355 speech.recognition_.onaudiostart = speech.handleRecognitionAudioStart_;
356 speech.recognition_.onend = speech.handleRecognitionEnd_;
357 speech.recognition_.onerror = speech.handleRecognitionError_;
358 speech.recognition_.onnomatch = speech.handleRecognitionOnNoMatch_;
359 speech.recognition_.onresult = speech.handleRecognitionResult_;
360 speech.recognition_.onspeechstart = speech.handleRecognitionSpeechStart_;
361};
362
363/**
364 * Sets up the necessary states for voice search and then starts the
365 * speech recognition interface.
366 */
367speech.start = function() {
368 view.show();
369
370 speech.resetIdleTimer_(speech.IDLE_TIMEOUT_MS_);
371
372 document.addEventListener(
373 'webkitvisibilitychange', speech.onVisibilityChange_, false);
374
375 // Initialize |speech.recognition_| if it isn't already.
376 if (!speech.recognition_) {
377 speech.initWebkitSpeech_();
378 }
379
380 // If |speech.start()| is called too soon after |speech.stop()| then the
381 // recognition interface hasn't yet reset and an error occurs. In this case
382 // we need to hard-reset it and reissue the |recognition_.start()| command.
383 try {
384 speech.recognition_.start();
385 speech.currentState_ = speech.State_.STARTED;
386 } catch (error) {
387 speech.initWebkitSpeech_();
388 try {
389 speech.recognition_.start();
390 speech.currentState_ = speech.State_.STARTED;
391 } catch (error2) {
392 speech.stop();
393 }
394 }
395};
396
397/**
398 * Hides the overlay and resets the speech state.
399 */
400speech.stop = function() {
401 speech.recognition_.abort();
402 speech.currentState_ = speech.State_.STOPPED;
403 view.hide();
404 speech.reset_();
405};
406
407/**
408 * Resets the internal state to the READY state.
409 * @private
410 */
411speech.reset_ = function() {
412 window.clearTimeout(speech.idleTimer_);
413 window.clearTimeout(speech.errorTimer_);
414
415 document.removeEventListener(
416 'webkitvisibilitychange', speech.onVisibilityChange_, false);
417
418 speech.interimResult_ = '';
419 speech.finalResult_ = '';
420 speech.currentState_ = speech.State_.READY;
421 speech.usingKeyboardNavigation_ = false;
422};
423
424/**
425 * Informs the view that the browser is receiving audio input.
426 * @param {Event=} opt_event Emitted event for audio start.
427 * @private
428 */
429speech.handleRecognitionAudioStart_ = function(opt_event) {
430 speech.resetIdleTimer_(speech.IDLE_TIMEOUT_MS_);
431 speech.currentState_ = speech.State_.AUDIO_RECEIVED;
432 view.setReadyForSpeech();
433};
434
435/**
436 * Function is called when the user starts speaking.
437 * @param {Event=} opt_event Emitted event for speech start.
438 * @private
439 */
440speech.handleRecognitionSpeechStart_ = function(opt_event) {
441 speech.resetIdleTimer_(speech.IDLE_TIMEOUT_MS_);
442 speech.currentState_ = speech.State_.SPEECH_RECEIVED;
443 view.setReceivingSpeech();
444};
445
446/**
447 * Processes the recognition results arriving from the Web Speech API.
448 * @param {SpeechRecognitionEvent} responseEvent Event coming from the API.
449 * @private
450 */
451speech.handleRecognitionResult_ = function(responseEvent) {
452 speech.resetIdleTimer_(speech.IDLE_TIMEOUT_MS_);
453
454 switch (speech.currentState_) {
455 case speech.State_.RESULT_RECEIVED:
456 case speech.State_.SPEECH_RECEIVED:
457 // Normal, expected states for processing results.
458 break;
459 case speech.State_.AUDIO_RECEIVED:
460 // Network bugginess (the onaudiostart packet was lost).
461 speech.handleRecognitionSpeechStart_();
462 break;
463 case speech.State_.STARTED:
464 // Network bugginess (the onspeechstart packet was lost).
465 speech.handleRecognitionAudioStart_();
466 speech.handleRecognitionSpeechStart_();
467 break;
468 default:
469 // Not expecting results in any other states.
470 return;
471 }
472
473 const results = responseEvent.results;
474 if (results.length == 0) {
475 return;
476 }
477 speech.currentState_ = speech.State_.RESULT_RECEIVED;
478 speech.interimResult_ = '';
479 speech.finalResult_ = '';
480
481 const finalResult = results[responseEvent.resultIndex];
482 // Process final results.
483 if (finalResult.isFinal) {
484 speech.finalResult_ = finalResult[0].transcript;
485 view.updateSpeechResult(speech.finalResult_, speech.finalResult_);
486
487 speech.submitFinalResult_();
488 return;
489 }
490
491 // Process interim results.
492 for (let j = 0; j < results.length; j++) {
493 const result = results[j][0];
494 speech.interimResult_ += result.transcript;
495 if (result.confidence > speech.RECOGNITION_CONFIDENCE_THRESHOLD_) {
496 speech.finalResult_ += result.transcript;
497 }
498 }
499 view.updateSpeechResult(speech.interimResult_, speech.finalResult_);
500
501 // Force-stop long queries.
502 if (speech.interimResult_.length > speech.QUERY_LENGTH_LIMIT_) {
503 if (speech.finalResult_) {
504 speech.submitFinalResult_();
505 } else {
506 speech.onErrorReceived_(RecognitionError.NO_MATCH);
507 }
508 }
509};
510
511/**
512 * Convert a |RecognitionError| to a |LOG_TYPE| error constant,
513 * for UMA logging.
514 * @param {RecognitionError} error The received error.
515 * @private
516 */
517speech.errorToLogType_ = function(error) {
518 switch (error) {
519 case RecognitionError.ABORTED:
520 return LOG_TYPE.ERROR_ABORTED;
521 case RecognitionError.AUDIO_CAPTURE:
522 return LOG_TYPE.ERROR_AUDIO_CAPTURE;
523 case RecognitionError.BAD_GRAMMAR:
524 return LOG_TYPE.ERROR_BAD_GRAMMAR;
525 case RecognitionError.LANGUAGE_NOT_SUPPORTED:
526 return LOG_TYPE.ERROR_LANGUAGE_NOT_SUPPORTED;
527 case RecognitionError.NETWORK:
528 return LOG_TYPE.ERROR_NETWORK;
529 case RecognitionError.NO_MATCH:
530 return LOG_TYPE.ERROR_NO_MATCH;
531 case RecognitionError.NO_SPEECH:
532 return LOG_TYPE.ERROR_NO_SPEECH;
533 case RecognitionError.NOT_ALLOWED:
534 return LOG_TYPE.ERROR_NOT_ALLOWED;
535 case RecognitionError.SERVICE_NOT_ALLOWED:
536 return LOG_TYPE.ERROR_SERVICE_NOT_ALLOWED;
537 default:
538 return LOG_TYPE.ERROR_OTHER;
539 }
540};
541
542/**
543 * Handles state transition for the controller when an error occurs
544 * during speech recognition.
545 * @param {RecognitionError} error The appropriate error state from
546 * the RecognitionError enum.
547 * @private
548 */
549speech.onErrorReceived_ = function(error) {
550 speech.logEvent(speech.errorToLogType_(error));
551 speech.resetIdleTimer_(speech.IDLE_TIMEOUT_MS_);
552 speech.errorTimeoutMs_ = speech.getRecognitionErrorTimeout_(error);
553 if (error != RecognitionError.ABORTED) {
554 speech.currentState_ = speech.State_.ERROR_RECEIVED;
555 view.showError(error);
556 window.clearTimeout(speech.idleTimer_);
557 speech.resetErrorTimer_(speech.errorTimeoutMs_);
558 }
559};
560
561/**
562 * Called when an error from Web Speech API is received.
563 * @param {SpeechRecognitionError} error The error event.
564 * @private
565 */
566speech.handleRecognitionError_ = function(error) {
567 speech.onErrorReceived_(speech.getRecognitionError_(error.error));
568};
569
570/**
571 * Stops speech recognition when no matches are found.
572 * @private
573 */
574speech.handleRecognitionOnNoMatch_ = function() {
575 speech.onErrorReceived_(RecognitionError.NO_MATCH);
576};
577
578/**
579 * Stops the UI when the Web Speech API reports that it has halted speech
580 * recognition.
581 * @private
582 */
583speech.handleRecognitionEnd_ = function() {
584 window.clearTimeout(speech.idleTimer_);
585
586 let error;
587 switch (speech.currentState_) {
588 case speech.State_.STARTED:
589 error = RecognitionError.AUDIO_CAPTURE;
590 break;
591 case speech.State_.AUDIO_RECEIVED:
592 error = RecognitionError.NO_SPEECH;
593 break;
594 case speech.State_.SPEECH_RECEIVED:
595 case speech.State_.RESULT_RECEIVED:
596 error = RecognitionError.NO_MATCH;
597 break;
598 case speech.State_.ERROR_RECEIVED:
599 error = RecognitionError.OTHER;
600 break;
601 default:
602 return;
603 }
604
605 // If error has not yet been displayed.
606 if (speech.currentState_ != speech.State_.ERROR_RECEIVED) {
607 view.showError(error);
608 speech.resetErrorTimer_(speech.ERROR_TIMEOUT_LONG_MS_);
609 }
610 speech.currentState_ = speech.State_.STOPPED;
611};
612
613/**
614 * Determines whether the user's browser is probably running on a Mac.
615 * @return {boolean} True iff the user's browser is running on a Mac.
616 * @private
617 */
618speech.isUserAgentMac_ = function() {
619 return window.navigator.userAgent.includes('Macintosh');
620};
621
622/**
623 * Determines, if the given KeyboardEvent |code| is a space or enter key.
624 * @param {string} code A KeyboardEvent's |code| property.
625 * @return True, iff the code represents a space or enter key.
626 * @private
627 */
628speech.isSpaceOrEnter_ = function(code) {
629 switch (code) {
630 case KEYCODE.ENTER:
631 case KEYCODE.NUMPAD_ENTER:
632 case KEYCODE.SPACE:
633 return true;
634 default:
635 return false;
636 }
637};
638
639/**
640 * Determines if the given event's target id is for a button or navigation link.
641 * @param {string} id An event's target id.
642 * @return True, iff the id is for a button or link.
643 * @private
644 */
645speech.isButtonOrLink_ = function(id) {
646 switch (id) {
647 case text.RETRY_LINK_ID:
648 case text.SUPPORT_LINK_ID:
649 case view.CLOSE_BUTTON_ID:
650 return true;
651 default:
652 return false;
653 }
654};
655
656/**
657 * Handles the following keyboard actions.
658 * - <CTRL> + <SHIFT> + <.> starts voice input(<CMD> + <SHIFT> + <.> on mac).
659 * - <ESC> aborts voice input when the recognition interface is active.
660 * - <ENTER> or <SPACE> interprets as a click if the target is a button or
661 * navigation link, otherwise it submits the speech query if there is one
662 * @param {!Event} event The keydown event.
663 */
664speech.onKeyDown = function(event) {
665 if (speech.isUiDefinitelyHidden_()) {
666 const ctrlKeyPressed =
667 event.ctrlKey || (speech.isUserAgentMac_() && event.metaKey);
668 if (speech.currentState_ == speech.State_.READY &&
669 event.code == KEYCODE.PERIOD && event.shiftKey && ctrlKeyPressed) {
670 speech.logEvent(LOG_TYPE.ACTION_ACTIVATE_KEYBOARD);
671 speech.start();
672 }
673 } else {
674 // Ensures that keyboard events are not propagated during voice input.
675 event.stopPropagation();
676
677 if (event.code == KEYCODE.TAB) {
678 speech.usingKeyboardNavigation_ = true;
679 } else if (speech.isSpaceOrEnter_(event.code)) {
680 if (event.target != null && speech.isButtonOrLink_(event.target.id)) {
681 view.onWindowClick_(event);
682 } else if (speech.finalResult_) {
683 speech.submitFinalResult_();
684 } else {
685 speech.logEvent(LOG_TYPE.ACTION_CLOSE_OVERLAY);
686 speech.stop();
687 }
688 } else if (event.code == KEYCODE.ESC) {
689 speech.logEvent(LOG_TYPE.ACTION_CLOSE_OVERLAY);
690 speech.stop();
691 }
692 }
693};
694
695/**
696 * Displays the no match error if no interactions occur after some time while
697 * the interface is active. This is a safety net in case the onend event
698 * doesn't fire, or the user has persistent noise in the background, and does
699 * not speak. If a high confidence transcription was received, then this submits
700 * the search query instead of displaying an error.
701 * @private
702 */
703speech.onIdleTimeout_ = function() {
704 if (speech.finalResult_) {
705 speech.submitFinalResult_();
706 return;
707 }
708
709 switch (speech.currentState_) {
710 case speech.State_.STARTED:
711 case speech.State_.AUDIO_RECEIVED:
712 case speech.State_.SPEECH_RECEIVED:
713 case speech.State_.RESULT_RECEIVED:
714 case speech.State_.ERROR_RECEIVED:
715 speech.onErrorReceived_(RecognitionError.NO_MATCH);
716 break;
717 }
718};
719
720/**
721 * Aborts the speech recognition interface when the user switches to a new
722 * tab or window.
723 * @private
724 */
725speech.onVisibilityChange_ = function() {
726 if (speech.isUiDefinitelyHidden_()) {
727 return;
728 }
729
730 if (document.webkitHidden) {
731 speech.stop();
732 }
733};
734
735/**
736 * Aborts the speech session if the UI is showing and omnibox gets focused. Does
737 * not abort if the user is using keyboard navigation (i.e. tab).
738 */
739speech.onOmniboxFocused = function() {
740 if (!speech.isUiDefinitelyHidden_() && !speech.usingKeyboardNavigation_) {
741 speech.logEvent(LOG_TYPE.ACTION_CLOSE_OVERLAY);
742 speech.stop();
743 }
744};
745
746/**
747 * Change the location of this tab to the new URL. Used for query submission.
748 * @param {!URL} url The URL to navigate to.
749 * @private
750 */
751speech.navigateToUrl_ = function(url) {
752 window.location.href = url.href;
753};
754
755/**
756 * Submits the final spoken speech query to perform a search.
757 * @private
758 */
759speech.submitFinalResult_ = function() {
760 window.clearTimeout(speech.idleTimer_);
761 if (!speech.finalResult_) {
762 throw new Error('Submitting empty query.');
763 }
764
765 const searchParams = new URLSearchParams();
766 // Add the encoded query. Getting |speech.finalResult_| needs to happen
767 // before stopping speech.
768 searchParams.append('q', speech.finalResult_);
769 // Add a parameter to indicate that this request is a voice search.
770 searchParams.append('gs_ivs', '1');
771
772 // Build the query URL.
773 const queryUrl = new URL('/search', speech.googleBaseUrl_);
774 queryUrl.search = searchParams.toString();
775
776 speech.logEvent(LOG_TYPE.ACTION_QUERY_SUBMITTED);
777 speech.stop();
778 speech.navigateToUrl_(queryUrl);
779};
780
781/**
782 * Returns the error type based on the error string received from the webkit
783 * speech recognition API.
784 * @param {string} error The error string received from the webkit speech
785 * recognition API.
786 * @return {RecognitionError} The appropriate error state from
787 * the RecognitionError enum.
788 * @private
789 */
790speech.getRecognitionError_ = function(error) {
791 switch (error) {
792 case 'aborted':
793 return RecognitionError.ABORTED;
794 case 'audio-capture':
795 return RecognitionError.AUDIO_CAPTURE;
796 case 'bad-grammar':
797 return RecognitionError.BAD_GRAMMAR;
798 case 'language-not-supported':
799 return RecognitionError.LANGUAGE_NOT_SUPPORTED;
800 case 'network':
801 return RecognitionError.NETWORK;
802 case 'no-speech':
803 return RecognitionError.NO_SPEECH;
804 case 'not-allowed':
805 return RecognitionError.NOT_ALLOWED;
806 case 'service-not-allowed':
807 return RecognitionError.SERVICE_NOT_ALLOWED;
808 default:
809 return RecognitionError.OTHER;
810 }
811};
812
813/**
814 * Returns a timeout based on the error received from the webkit speech
815 * recognition API.
816 * @param {RecognitionError} error An error from the RecognitionError enum.
817 * @return {number} The appropriate timeout duration for displaying the error.
818 * @private
819 */
820speech.getRecognitionErrorTimeout_ = function(error) {
821 switch (error) {
822 case RecognitionError.AUDIO_CAPTURE:
823 case RecognitionError.NO_SPEECH:
824 case RecognitionError.NOT_ALLOWED:
825 case RecognitionError.SERVICE_NOT_ALLOWED:
826 case RecognitionError.NO_MATCH:
827 return speech.ERROR_TIMEOUT_LONG_MS_;
828 default:
829 return speech.ERROR_TIMEOUT_SHORT_MS_;
830 }
831};
832
833/**
834 * Resets the idle state timeout.
835 * @param {number} duration The duration after which to close the UI.
836 * @private
837 */
838speech.resetIdleTimer_ = function(duration) {
839 window.clearTimeout(speech.idleTimer_);
840 speech.idleTimer_ = window.setTimeout(speech.onIdleTimeout_, duration);
841};
842
843/**
844 * Resets the idle error state timeout.
845 * @param {number} duration The duration after which to close the UI during an
846 * error state.
847 * @private
848 */
849speech.resetErrorTimer_ = function(duration) {
850 window.clearTimeout(speech.errorTimer_);
851 speech.errorTimer_ = window.setTimeout(speech.stop, duration);
852};
853
854/**
855 * Check to see if the speech recognition interface is running, and has
856 * received any results.
857 * @return {boolean} True, if the speech recognition interface is running,
858 * and has received any results.
859 */
860speech.hasReceivedResults = function() {
861 return speech.currentState_ == speech.State_.RESULT_RECEIVED;
862};
863
864/**
865 * Check to see if the speech recognition interface is running.
866 * @return {boolean} True, if the speech recognition interface is running.
867 */
868speech.isRecognizing = function() {
869 switch (speech.currentState_) {
870 case speech.State_.STARTED:
871 case speech.State_.AUDIO_RECEIVED:
872 case speech.State_.SPEECH_RECEIVED:
873 case speech.State_.RESULT_RECEIVED:
874 return true;
875 }
876 return false;
877};
878
879/**
880 * Check if the controller is in a state where the UI is definitely hidden.
881 * Since we show the UI for a few seconds after we receive an error from the
882 * API, we need a separate definition to |speech.isRecognizing()| to indicate
883 * when the UI is hidden. <strong>Note:</strong> that if this function
884 * returns false, it might not necessarily mean that the UI is visible.
885 * @return {boolean} True if the UI is hidden.
886 * @private
887 */
888speech.isUiDefinitelyHidden_ = function() {
889 switch (speech.currentState_) {
890 case speech.State_.READY:
891 case speech.State_.UNINITIALIZED:
892 return true;
893 }
894 return false;
895};
896
897/**
898 * Handles click events during speech recognition.
899 * @param {boolean} shouldSubmit True if a query should be submitted.
900 * @param {boolean} shouldRetry True if the interface should be restarted.
901 * @param {boolean} navigatingAway True if the browser is navigating away
902 * from the NTP.
903 * @private
904 */
905speech.onClick_ = function(shouldSubmit, shouldRetry, navigatingAway) {
906 if (speech.finalResult_ && shouldSubmit) {
907 speech.submitFinalResult_();
908 } else if (speech.currentState_ == speech.State_.STOPPED && shouldRetry) {
909 speech.reset_();
910 speech.start();
911 } else if (speech.currentState_ == speech.State_.STOPPED && navigatingAway) {
912 // If the user clicks on a "Learn more" or "Details" support page link
913 // from an error message, do nothing, and let Chrome navigate to that page.
914 } else {
915 speech.logEvent(LOG_TYPE.ACTION_CLOSE_OVERLAY);
916 speech.stop();
917 }
918};
919
920/* TEXT VIEW */
921
922/**
923 * Provides methods for styling and animating the text areas
924 * left of the microphone button.
925 */
926const text = {};
927
928/**
929 * ID for the "Try Again" link shown in error output.
930 * @const
931 */
932text.RETRY_LINK_ID = 'voice-retry-link';
933
934/**
935 * ID for the Voice Search support site link shown in error output.
936 * @const
937 */
938text.SUPPORT_LINK_ID = 'voice-support-link';
939
940/**
941 * Class for the links shown in error output.
942 * @const @private
943 */
944text.ERROR_LINK_CLASS_ = 'voice-text-link';
945
946/**
947 * Class name for the speech recognition result output area.
948 * @const @private
949 */
950text.TEXT_AREA_CLASS_ = 'voice-text';
951
952/**
953 * Class name for the "Listening..." text animation.
954 * @const @private
955 */
956text.LISTENING_ANIMATION_CLASS_ = 'listening-animation';
957
958/**
959 * ID of the final / high confidence speech recognition results element.
960 * @const @private
961 */
962text.FINAL_TEXT_AREA_ID_ = 'voice-text-f';
963
964/**
965 * ID of the interim / low confidence speech recognition results element.
966 * @const @private
967 */
968text.INTERIM_TEXT_AREA_ID_ = 'voice-text-i';
969
970/**
971 * The line height of the speech recognition results text.
972 * @const @private
973 */
974text.LINE_HEIGHT_ = 1.2;
975
976/**
977 * Font size in the full page view in pixels.
978 * @const @private
979 */
980text.FONT_SIZE_ = 32;
981
982/**
983 * Delay in milliseconds before showing the initializing message.
984 * @const @private
985 */
986text.INITIALIZING_TIMEOUT_MS_ = 300;
987
988/**
989 * Delay in milliseconds before showing the listening message.
990 * @const @private
991 */
992text.LISTENING_TIMEOUT_MS_ = 2000;
993
994/**
995 * Base link target for help regarding voice search. To be appended
996 * with a locale string for proper target site localization.
997 * @const @private
998 */
999text.SUPPORT_LINK_BASE_ =
1000 'https://support.google.com/chrome/?p=ui_voice_search&hl=';
1001
1002/**
1003 * The final / high confidence speech recognition result element.
1004 * @private {Element}
1005 */
1006text.final_;
1007
1008/**
1009 * The interim / low confidence speech recognition result element.
1010 * @private {Element}
1011 */
1012text.interim_;
1013
1014/**
1015 * Stores the ID of the initializing message timer.
1016 * @private {number}
1017 */
1018text.initializingTimer_;
1019
1020/**
1021 * Stores the ID of the listening message timer.
1022 * @private {number}
1023 */
1024text.listeningTimer_;
1025
1026/**
1027 * Finds the text view elements.
1028 */
1029text.init = function() {
1030 text.final_ = $(text.FINAL_TEXT_AREA_ID_);
1031 text.interim_ = $(text.INTERIM_TEXT_AREA_ID_);
1032 text.clear();
1033};
1034
1035/**
1036 * Updates the text elements with new recognition results.
1037 * @param {string} interimText Low confidence speech recognition result text.
1038 * @param {string} opt_finalText High confidence speech recognition result
1039 * text, defaults to an empty string.
1040 */
1041text.updateTextArea = function(interimText, opt_finalText = '') {
1042 window.clearTimeout(text.initializingTimer_);
1043 text.clearListeningTimeout();
1044
1045 text.interim_.textContent = interimText;
1046 text.final_.textContent = opt_finalText;
1047
1048 text.interim_.className = text.final_.className = text.getTextClassName_();
1049};
1050
1051/**
1052 * Sets the text view to the initializing state. The initializing message
1053 * shown while waiting for permission is not displayed immediately, but after
1054 * a short timeout. The reason for this is that the "Waiting..." message would
1055 * still appear ("blink") every time a user opens Voice Search, even if they
1056 * have already granted and persisted microphone permission for the NTP,
1057 * and could therefore directly proceed to the "Speak now" message.
1058 */
1059text.showInitializingMessage = function() {
1060 text.interim_.textContent = '';
1061 text.final_.textContent = '';
1062
1063 const displayMessage = function() {
1064 if (text.interim_.textContent == '') {
1065 text.updateTextArea(speech.messages.waiting);
1066 }
1067 };
1068 text.initializingTimer_ =
1069 window.setTimeout(displayMessage, text.INITIALIZING_TIMEOUT_MS_);
1070};
1071
1072/**
1073 * Sets the text view to the ready state.
1074 */
1075text.showReadyMessage = function() {
1076 window.clearTimeout(text.initializingTimer_);
1077 text.clearListeningTimeout();
1078 text.updateTextArea(speech.messages.ready);
1079 text.startListeningMessageAnimation_();
1080};
1081
1082/**
1083 * Display an error message in the text area for the given error.
1084 * @param {RecognitionError} error The error that occured.
1085 */
1086text.showErrorMessage = function(error) {
1087 text.updateTextArea(text.getErrorMessage_(error));
1088
1089 const linkElement = text.getErrorLink_(error);
1090 // Setting textContent removes all children (no need to clear link elements).
1091 if (linkElement) {
1092 text.interim_.textContent += ' ';
1093 text.interim_.appendChild(linkElement);
1094 }
1095};
1096
1097/**
1098 * Returns an error message based on the error.
1099 * @param {RecognitionError} error The error that occured.
1100 * @private
1101 */
1102text.getErrorMessage_ = function(error) {
1103 switch (error) {
1104 case RecognitionError.NO_MATCH:
1105 return speech.messages.noTranslation;
1106 case RecognitionError.NO_SPEECH:
1107 return speech.messages.noVoice;
1108 case RecognitionError.AUDIO_CAPTURE:
1109 return speech.messages.audioError;
1110 case RecognitionError.NETWORK:
1111 return speech.messages.networkError;
1112 case RecognitionError.NOT_ALLOWED:
1113 case RecognitionError.SERVICE_NOT_ALLOWED:
1114 return speech.messages.permissionError;
1115 case RecognitionError.LANGUAGE_NOT_SUPPORTED:
1116 return speech.messages.languageError;
1117 default:
1118 return speech.messages.otherError;
1119 }
1120};
1121
1122/**
1123 * Returns an error message help link based on the error.
1124 * @param {RecognitionError} error The error that occured.
1125 * @private
1126 */
1127text.getErrorLink_ = function(error) {
1128 const linkElement = document.createElement('a');
1129 linkElement.className = text.ERROR_LINK_CLASS_;
1130
1131 switch (error) {
1132 case RecognitionError.NO_MATCH:
1133 linkElement.id = text.RETRY_LINK_ID;
1134 linkElement.tabIndex = '0';
1135 linkElement.textContent = speech.messages.tryAgain;
1136 // When clicked, |view.onWindowClick_| gets called.
1137 return linkElement;
1138 case RecognitionError.NO_SPEECH:
1139 case RecognitionError.AUDIO_CAPTURE:
1140 linkElement.id = text.SUPPORT_LINK_ID;
1141 linkElement.href = text.SUPPORT_LINK_BASE_ + getChromeUILanguage();
1142 linkElement.textContent = speech.messages.learnMore;
1143 linkElement.target = '_blank';
1144 return linkElement;
1145 case RecognitionError.NOT_ALLOWED:
1146 case RecognitionError.SERVICE_NOT_ALLOWED:
1147 linkElement.id = text.SUPPORT_LINK_ID;
1148 linkElement.href = text.SUPPORT_LINK_BASE_ + getChromeUILanguage();
1149 linkElement.textContent = speech.messages.details;
1150 linkElement.target = '_blank';
1151 return linkElement;
1152 default:
1153 return null;
1154 }
1155};
1156
1157/**
1158 * Clears the text elements.
1159 */
1160text.clear = function() {
1161 text.updateTextArea('');
1162
1163 text.clearListeningTimeout();
1164 window.clearTimeout(text.initializingTimer_);
1165
1166 text.interim_.className = text.TEXT_AREA_CLASS_;
1167 text.final_.className = text.TEXT_AREA_CLASS_;
1168};
1169
1170/**
1171 * Cancels listening message display.
1172 */
1173text.clearListeningTimeout = function() {
1174 window.clearTimeout(text.listeningTimer_);
1175};
1176
1177/**
1178 * Determines the class name of the text output Elements.
1179 * @return {string} The class name.
1180 * @private
1181 */
1182text.getTextClassName_ = function() {
1183 // Shift up for every line.
1184 const oneLineHeight = text.LINE_HEIGHT_ * text.FONT_SIZE_ + 1;
1185 const twoLineHeight = text.LINE_HEIGHT_ * text.FONT_SIZE_ * 2 + 1;
1186 const threeLineHeight = text.LINE_HEIGHT_ * text.FONT_SIZE_ * 3 + 1;
1187 const fourLineHeight = text.LINE_HEIGHT_ * text.FONT_SIZE_ * 4 + 1;
1188
1189 const height = text.interim_.scrollHeight;
1190 let className = text.TEXT_AREA_CLASS_;
1191
1192 if (height > fourLineHeight) {
1193 className += ' voice-text-5l';
1194 } else if (height > threeLineHeight) {
1195 className += ' voice-text-4l';
1196 } else if (height > twoLineHeight) {
1197 className += ' voice-text-3l';
1198 } else if (height > oneLineHeight) {
1199 className += ' voice-text-2l';
1200 }
1201 return className;
1202};
1203
1204/**
1205 * Displays the listening message animation after the ready message has been
1206 * shown for |text.LISTENING_TIMEOUT_MS_| milliseconds without further user
1207 * action.
1208 * @private
1209 */
1210text.startListeningMessageAnimation_ = function() {
1211 const animateListeningText = function() {
1212 // If speech is active with no results yet, show the message and animation.
1213 if (speech.isRecognizing() && !speech.hasReceivedResults()) {
1214 text.updateTextArea(speech.messages.listening);
1215 text.interim_.classList.add(text.LISTENING_ANIMATION_CLASS_);
1216 }
1217 };
1218
1219 text.listeningTimer_ =
1220 window.setTimeout(animateListeningText, text.LISTENING_TIMEOUT_MS_);
1221};
1222
1223/* END TEXT VIEW */
1224
1225/* MICROPHONE VIEW */
1226
1227/**
1228 * Provides methods for animating the microphone button and icon
1229 * on the Voice Search full screen overlay.
1230 */
1231const microphone = {};
1232
1233/**
1234 * ID for the button Element.
1235 * @const
1236 */
1237microphone.RED_BUTTON_ID = 'voice-button';
1238
1239/**
1240 * ID for the level animations Element that indicates input volume.
1241 * @const @private
1242 */
1243microphone.LEVEL_ID_ = 'voice-level';
1244
1245/**
1246 * ID for the container of the microphone, red button and level animations.
1247 * @const @private
1248 */
1249microphone.CONTAINER_ID_ = 'voice-button-container';
1250
1251/**
1252 * The minimum transform scale for the volume rings.
1253 * @const @private
1254 */
1255microphone.LEVEL_SCALE_MINIMUM_ = 0.5;
1256
1257/**
1258 * The range of the transform scale for the volume rings.
1259 * @const @private
1260 */
1261microphone.LEVEL_SCALE_RANGE_ = 0.55;
1262
1263/**
1264 * The minimum transition time (in milliseconds) for the volume rings.
1265 * @const @private
1266 */
1267microphone.LEVEL_TIME_STEP_MINIMUM_ = 170;
1268
1269/**
1270 * The range of the transition time for the volume rings.
1271 * @const @private
1272 */
1273microphone.LEVEL_TIME_STEP_RANGE_ = 10;
1274
1275/**
1276 * The button with the microphone icon.
1277 * @private {Element}
1278 */
1279microphone.button_;
1280
1281/**
1282 * The voice level element that is displayed when the user starts speaking.
1283 * @private {Element}
1284 */
1285microphone.level_;
1286
1287/**
1288 * Variable to indicate whether level animations are underway.
1289 * @private {boolean}
1290 */
1291microphone.isLevelAnimating_ = false;
1292
1293/**
1294 * Creates/finds the output elements for the microphone rendering and animation.
1295 */
1296microphone.init = function() {
1297 // Get the button element and microphone container.
1298 microphone.button_ = $(microphone.RED_BUTTON_ID);
1299
1300 // Get the animation elements.
1301 microphone.level_ = $(microphone.LEVEL_ID_);
1302};
1303
1304/**
1305 * Starts the volume circles animations, if it has not started yet.
1306 */
1307microphone.startInputAnimation = function() {
1308 if (!microphone.isLevelAnimating_) {
1309 microphone.isLevelAnimating_ = true;
1310 microphone.runLevelAnimation_();
1311 }
1312};
1313
1314/**
1315 * Stops the volume circles animations.
1316 */
1317microphone.stopInputAnimation = function() {
1318 microphone.isLevelAnimating_ = false;
1319};
1320
1321/**
1322 * Runs the volume level animation.
1323 * @private
1324 */
1325microphone.runLevelAnimation_ = function() {
1326 if (!microphone.isLevelAnimating_) {
1327 microphone.level_.style.removeProperty('opacity');
1328 microphone.level_.style.removeProperty('transition');
1329 microphone.level_.style.removeProperty('transform');
1330 return;
1331 }
1332 const scale = microphone.LEVEL_SCALE_MINIMUM_ +
1333 Math.random() * microphone.LEVEL_SCALE_RANGE_;
1334 const timeStep = Math.round(
1335 microphone.LEVEL_TIME_STEP_MINIMUM_ +
1336 Math.random() * microphone.LEVEL_TIME_STEP_RANGE_);
1337 microphone.level_.style.setProperty(
1338 'transition', 'transform ' + timeStep + 'ms ease-in-out');
1339 microphone.level_.style.setProperty('transform', 'scale(' + scale + ')');
1340 window.setTimeout(microphone.runLevelAnimation_, timeStep);
1341};
1342
1343/* END MICROPHONE VIEW */
1344
1345/* VIEW */
1346
1347/**
1348 * Provides methods for manipulating and animating the Voice Search
1349 * full screen overlay.
1350 */
1351const view = {};
1352
1353/**
1354 * ID for the close button in the speech output container.
1355 * @const
1356 */
1357view.CLOSE_BUTTON_ID = 'voice-close-button';
1358
1359/**
1360 * Class name of the speech recognition interface on the homepage.
1361 * @const @private
1362 */
1363view.OVERLAY_CLASS_ = 'overlay';
1364
1365/**
1366 * Class name of the speech recognition interface when it is hidden on the
1367 * homepage.
1368 * @const @private
1369 */
1370view.OVERLAY_HIDDEN_CLASS_ = 'overlay-hidden';
1371
1372/**
1373 * ID for the dialog that contains the speech recognition interface.
1374 * @const @private
1375 */
1376view.DIALOG_ID_ = 'voice-overlay-dialog';
1377
1378/**
1379 * ID for the speech output background.
1380 * @const @private
1381 */
1382view.BACKGROUND_ID_ = 'voice-overlay';
1383
1384/**
1385 * ID for the speech output container.
1386 * @const @private
1387 */
1388view.CONTAINER_ID_ = 'voice-outer';
1389
1390/**
1391 * Class name used to modify the UI to the 'listening' state.
1392 * @const @private
1393 */
1394view.MICROPHONE_LISTENING_CLASS_ = 'outer voice-ml';
1395
1396/**
1397 * Class name used to modify the UI to the 'receiving speech' state.
1398 * @const @private
1399 */
1400view.RECEIVING_SPEECH_CLASS_ = 'outer voice-rs';
1401
1402/**
1403 * Class name used to modify the UI to the 'error received' state.
1404 * @const @private
1405 */
1406view.ERROR_RECEIVED_CLASS_ = 'outer voice-er';
1407
1408/**
1409 * Class name used to modify the UI to the inactive state.
1410 * @const @private
1411 */
1412view.INACTIVE_CLASS_ = 'outer';
1413
1414/**
1415 * Background element and container of all other elements.
1416 * @private {Element}
1417 */
1418view.background_;
1419
1420/**
1421 * The container used to position the microphone and text output area.
1422 * @private {Element}
1423 */
1424view.container_;
1425
1426/**
1427 * True if the the last error message shown was for the 'no-match' error.
1428 * @private {boolean}
1429 */
1430view.isNoMatchShown_ = false;
1431
1432/**
1433 * True if the UI elements are visible.
1434 * @private {boolean}
1435 */
1436view.isVisible_ = false;
1437
1438/**
1439 * The function to call when there is a click event.
1440 * @private {Function}
1441 */
1442view.onClick_;
1443
1444/**
1445 * Displays the UI.
1446 */
1447view.show = function() {
1448 if (!view.isVisible_) {
1449 text.showInitializingMessage();
1450 view.showView_();
1451 window.addEventListener('click', view.onWindowClick_, false);
1452 }
1453};
1454
1455/**
1456 * Sets the output area text to listening. This should only be called when
1457 * the Web Speech API starts receiving audio input (i.e., onaudiostart).
1458 */
1459view.setReadyForSpeech = function() {
1460 if (view.isVisible_) {
1461 view.container_.className = view.MICROPHONE_LISTENING_CLASS_;
1462 text.showReadyMessage();
1463 }
1464};
1465
1466/**
1467 * Shows the pulsing animation emanating from the microphone. This should only
1468 * be called when the Web Speech API starts receiving speech input (i.e.,
1469 * |onspeechstart|). Do note that this may also be run when the Web Speech API
1470 * is receiving speech recognition results (|onresult|), because |onspeechstart|
1471 * may not have been called.
1472 */
1473view.setReceivingSpeech = function() {
1474 if (view.isVisible_) {
1475 view.container_.className = view.RECEIVING_SPEECH_CLASS_;
1476 microphone.startInputAnimation();
1477 text.clearListeningTimeout();
1478 }
1479};
1480
1481/**
1482 * Updates the speech recognition results output with the latest results.
1483 * @param {string} interimResultText Low confidence recognition text (grey).
1484 * @param {string} finalResultText High confidence recognition text (black).
1485 */
1486view.updateSpeechResult = function(interimResultText, finalResultText) {
1487 if (view.isVisible_) {
1488 // If the Web Speech API is receiving speech recognition results
1489 // (|onresult|) and |onspeechstart| has not been called.
1490 if (view.container_.className != view.RECEIVING_SPEECH_CLASS_) {
1491 view.setReceivingSpeech();
1492 }
1493 text.updateTextArea(interimResultText, finalResultText);
1494 }
1495};
1496
1497/**
1498 * Hides the UI and stops animations.
1499 */
1500view.hide = function() {
1501 window.removeEventListener('click', view.onWindowClick_, false);
1502 view.stopMicrophoneAnimations_();
1503 view.hideView_();
1504 view.isNoMatchShown_ = false;
1505 text.clear();
1506};
1507
1508/**
1509 * Find the page elements that will be used to render the speech recognition
1510 * interface area.
1511 * @param {Function} onClick The function to call when there is a click event
1512 * in the window.
1513 */
1514view.init = function(onClick) {
1515 view.onClick_ = onClick;
1516
1517 view.dialog_ = $(view.DIALOG_ID_);
1518 view.background_ = $(view.BACKGROUND_ID_);
1519 view.container_ = $(view.CONTAINER_ID_);
1520
1521 text.init();
1522 microphone.init();
1523};
1524
1525/**
1526 * Sets accessibility titles/labels for the page elements.
1527 * @param {!Object} translatedStrings Dictionary of localized title strings.
1528 */
1529view.setTitles = function(translatedStrings) {
1530 const closeButton = $(view.CLOSE_BUTTON_ID);
1531 closeButton.title = translatedStrings.voiceCloseTooltip;
1532 closeButton.setAttribute('aria-label', translatedStrings.voiceCloseTooltip);
1533};
1534
1535/**
1536 * Displays an error message and stops animations.
1537 * @param {RecognitionError} error The error type.
1538 */
1539view.showError = function(error) {
1540 view.container_.className = view.ERROR_RECEIVED_CLASS_;
1541 text.showErrorMessage(error);
1542 view.stopMicrophoneAnimations_();
1543 view.isNoMatchShown_ = (error == RecognitionError.NO_MATCH);
1544};
1545
1546/**
1547 * Makes the view visible.
1548 * @private
1549 */
1550view.showView_ = function() {
1551 if (!view.isVisible_) {
1552 view.dialog_.showModal();
1553 view.background_.className = view.OVERLAY_HIDDEN_CLASS_;
1554 view.background_.className = view.OVERLAY_CLASS_;
1555 view.isVisible_ = true;
1556 }
1557};
1558
1559/**
1560 * Hides the view.
1561 * @private
1562 */
1563view.hideView_ = function() {
1564 view.dialog_.close();
1565 view.background_.className = view.OVERLAY_HIDDEN_CLASS_;
1566 view.container_.className = view.INACTIVE_CLASS_;
1567 view.background_.removeAttribute('style');
1568 view.isVisible_ = false;
1569};
1570
1571/**
1572 * Stops the animations in the microphone view.
1573 * @private
1574 */
1575view.stopMicrophoneAnimations_ = function() {
1576 microphone.stopInputAnimation();
1577};
1578
1579/**
1580 * Makes sure that a click anywhere closes the UI when it is active.
1581 * @param {!Event} event The click event.
1582 * @private
1583 */
1584view.onWindowClick_ = function(event) {
1585 if (!view.isVisible_) {
1586 return;
1587 }
1588 const retryLinkClicked = event.target.id === text.RETRY_LINK_ID;
1589 const supportLinkClicked = event.target.id === text.SUPPORT_LINK_ID;
1590 const micIconClicked = event.target.id === microphone.RED_BUTTON_ID;
1591
1592 const submitQuery = micIconClicked && !view.isNoMatchShown_;
1593 const shouldRetry =
1594 retryLinkClicked || (micIconClicked && view.isNoMatchShown_);
1595 const navigatingAway = supportLinkClicked;
1596
1597 speech.usingKeyboardNavigation_ = false;
1598
1599 if (shouldRetry) {
1600 if (micIconClicked) {
1601 speech.logEvent(LOG_TYPE.ACTION_TRY_AGAIN_MIC_BUTTON);
1602 } else if (retryLinkClicked) {
1603 speech.logEvent(LOG_TYPE.ACTION_TRY_AGAIN_LINK);
1604 }
1605 }
1606 if (supportLinkClicked) {
1607 speech.logEvent(LOG_TYPE.ACTION_SUPPORT_LINK_CLICKED);
1608 }
1609
1610 view.onClick_(submitQuery, shouldRetry, navigatingAway);
1611};
1612
1613/* END VIEW */