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.
7 // Correspond to steps in the hotword opt-in flow.
8 /** @const */ var START
= 'start-container';
9 /** @const */ var AUDIO_HISTORY
= 'audio-history-container';
10 /** @const */ var SPEECH_TRAINING
= 'speech-training-container';
11 /** @const */ var FINISH
= 'finish-container';
14 * These flows correspond to the three LaunchModes as defined in
15 * chrome/browser/search/hotword_service.h and should be kept in sync
20 [START
, SPEECH_TRAINING
, FINISH
],
21 [START
, AUDIO_HISTORY
, SPEECH_TRAINING
, FINISH
],
22 [SPEECH_TRAINING
, FINISH
]
26 * The launch mode. This enum needs to be kept in sync with that of
27 * the same name in hotword_service.h.
32 HOTWORD_AND_AUDIO_HISTORY
: 1,
47 * Class to control the page flow of the always-on hotword and
48 * Audio History opt-in process.
52 this.currentStepIndex_
= -1;
53 this.currentFlow_
= [];
56 * The mode that this app was launched in.
57 * @private {LaunchMode}
59 this.launchMode_
= LaunchMode
.HOTWORD_AND_AUDIO_HISTORY
;
62 * Whether this flow is currently in the process of training a voice model.
65 this.training_
= false;
68 * The current training state.
69 * @private {?TrainingState}
71 this.trainingState_
= null;
74 * Whether an expected hotword trigger has been received, indexed by
76 * @private {boolean[]}
78 this.hotwordTriggerReceived_
= [];
81 * Prefix of the element ids for the page that is currently training.
84 this.trainingPagePrefix_
= 'speech-training';
87 * Whether the speaker model for this flow has been finalized.
90 this.speakerModelFinalized_
= false;
93 * ID of the currently active timeout.
96 this.timeoutId_
= null;
99 * Listener for the speakerModelSaved event.
100 * @private {Function}
102 this.speakerModelFinalizedListener_
=
103 this.onSpeakerModelFinalized_
.bind(this);
106 * Listener for the hotword trigger event.
107 * @private {Function}
109 this.hotwordTriggerListener_
=
110 this.handleHotwordTrigger_
.bind(this);
112 // Listen for the user locking the screen.
113 chrome
.idle
.onStateChanged
.addListener(
114 this.handleIdleStateChanged_
.bind(this));
116 // Listen for hotword settings changes. This used to detect when the user
117 // switches to a different profile.
118 if (chrome
.hotwordPrivate
.onEnabledChanged
) {
119 chrome
.hotwordPrivate
.onEnabledChanged
.addListener(
120 this.handleEnabledChanged_
.bind(this));
125 * Advances the current step. Begins training if the speech-training
126 * page has been reached.
128 Flow
.prototype.advanceStep = function() {
129 this.currentStepIndex_
++;
130 if (this.currentStepIndex_
< this.currentFlow_
.length
) {
131 if (this.currentFlow_
[this.currentStepIndex_
] == SPEECH_TRAINING
)
132 this.startTraining();
133 this.showStep_
.apply(this);
138 * Gets the appropriate flow and displays its first page.
140 Flow
.prototype.startFlow = function() {
141 if (chrome
.hotwordPrivate
&& chrome
.hotwordPrivate
.getLaunchState
)
142 chrome
.hotwordPrivate
.getLaunchState(this.startFlowForMode_
.bind(this));
146 * Starts the training process.
148 Flow
.prototype.startTraining = function() {
149 // Don't start a training session if one already exists.
153 this.training_
= true;
155 if (chrome
.hotwordPrivate
.onHotwordTriggered
&&
156 !chrome
.hotwordPrivate
.onHotwordTriggered
.hasListener(
157 this.hotwordTriggerListener_
)) {
158 chrome
.hotwordPrivate
.onHotwordTriggered
.addListener(
159 this.hotwordTriggerListener_
);
162 this.waitForHotwordTrigger_(0);
163 if (chrome
.hotwordPrivate
.startTraining
)
164 chrome
.hotwordPrivate
.startTraining();
168 * Stops the training process.
170 Flow
.prototype.stopTraining = function() {
174 this.training_
= false;
175 if (chrome
.hotwordPrivate
.onHotwordTriggered
) {
176 chrome
.hotwordPrivate
.onHotwordTriggered
.
177 removeListener(this.hotwordTriggerListener_
);
179 if (chrome
.hotwordPrivate
.stopTraining
)
180 chrome
.hotwordPrivate
.stopTraining();
184 * Attempts to enable audio history for the signed-in account.
186 Flow
.prototype.enableAudioHistory = function() {
188 $('audio-history-agree').disabled
= true;
189 $('audio-history-cancel').disabled
= true;
191 $('audio-history-error').hidden
= true;
192 $('audio-history-wait').hidden
= false;
194 if (chrome
.hotwordPrivate
.setAudioHistoryEnabled
) {
195 chrome
.hotwordPrivate
.setAudioHistoryEnabled(
196 true, this.onAudioHistoryRequestCompleted_
.bind(this));
200 // ---- private methods:
203 * Shows an error if the audio history setting was not enabled successfully.
206 Flow
.prototype.handleAudioHistoryError_ = function() {
207 $('audio-history-agree').disabled
= false;
208 $('audio-history-cancel').disabled
= false;
210 $('audio-history-wait').hidden
= true;
211 $('audio-history-error').hidden
= false;
213 // Set a timeout before focusing the Enable button so that screenreaders
214 // have time to announce the error first.
215 this.setTimeout_(function() {
216 $('audio-history-agree').focus();
221 * Callback for when an audio history request completes.
222 * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history
226 Flow
.prototype.onAudioHistoryRequestCompleted_ = function(state
) {
227 if (!state
.success
|| !state
.enabled
) {
228 this.handleAudioHistoryError_();
236 * Shows an error if the speaker model has not been finalized.
239 Flow
.prototype.handleSpeakerModelFinalizedError_ = function() {
243 if (this.speakerModelFinalized_
)
246 this.updateTrainingState_(TrainingState
.ERROR
);
251 * Handles the speaker model finalized event.
254 Flow
.prototype.onSpeakerModelFinalized_ = function() {
255 this.speakerModelFinalized_
= true;
256 if (chrome
.hotwordPrivate
.onSpeakerModelSaved
) {
257 chrome
.hotwordPrivate
.onSpeakerModelSaved
.removeListener(
258 this.speakerModelFinalizedListener_
);
261 this.setTimeout_(this.finishFlow_
.bind(this), 2000);
265 * Completes the training process.
268 Flow
.prototype.finishFlow_ = function() {
269 if (chrome
.hotwordPrivate
.setHotwordAlwaysOnSearchEnabled
) {
270 chrome
.hotwordPrivate
.setHotwordAlwaysOnSearchEnabled(true,
271 this.advanceStep
.bind(this));
276 * Handles a user clicking on the retry button.
278 Flow
.prototype.handleRetry = function() {
279 if (!(this.trainingState_
== TrainingState
.TIMEOUT
||
280 this.trainingState_
== TrainingState
.ERROR
))
283 this.startTraining();
284 this.updateTrainingState_(TrainingState
.RESET
);
287 // ---- private methods:
290 * Completes the training process.
293 Flow
.prototype.finalizeSpeakerModel_ = function() {
297 // Listen for the success event from the NaCl module.
298 if (chrome
.hotwordPrivate
.onSpeakerModelSaved
&&
299 !chrome
.hotwordPrivate
.onSpeakerModelSaved
.hasListener(
300 this.speakerModelFinalizedListener_
)) {
301 chrome
.hotwordPrivate
.onSpeakerModelSaved
.addListener(
302 this.speakerModelFinalizedListener_
);
305 this.speakerModelFinalized_
= false;
306 this.setTimeout_(this.handleSpeakerModelFinalizedError_
.bind(this), 30000);
307 if (chrome
.hotwordPrivate
.finalizeSpeakerModel
)
308 chrome
.hotwordPrivate
.finalizeSpeakerModel();
312 * Returns the current training step.
313 * @param {string} curStepClassName The name of the class of the current
315 * @return {Object} The current training step, its index, and an array of
316 * all training steps. Any of these can be undefined.
319 Flow
.prototype.getCurrentTrainingStep_ = function(curStepClassName
) {
321 $(this.trainingPagePrefix_
+ '-training').querySelectorAll('.train');
323 $(this.trainingPagePrefix_
+ '-training').querySelector('.listening');
325 return {current
: curStep
,
326 index
: Array
.prototype.indexOf
.call(steps
, curStep
),
331 * Updates the training state.
332 * @param {TrainingState} state The training state.
335 Flow
.prototype.updateTrainingState_ = function(state
) {
336 this.trainingState_
= state
;
337 this.updateErrorUI_();
341 * Waits two minutes and then checks for a training error.
342 * @param {number} index The index of the training step.
345 Flow
.prototype.waitForHotwordTrigger_ = function(index
) {
349 this.hotwordTriggerReceived_
[index
] = false;
350 this.setTimeout_(this.handleTrainingTimeout_
.bind(this, index
), 120000);
354 * Checks for and handles a training error.
355 * @param {number} index The index of the training step.
358 Flow
.prototype.handleTrainingTimeout_ = function(index
) {
359 if (this.hotwordTriggerReceived_
[index
])
362 this.timeoutTraining_();
366 * Times out training and updates the UI to show a "retry" message, if
367 * currently training.
370 Flow
.prototype.timeoutTraining_ = function() {
374 this.clearTimeout_();
375 this.updateTrainingState_(TrainingState
.TIMEOUT
);
380 * Sets a timeout. If any timeout is active, clear it.
381 * @param {Function} func The function to invoke when the timeout occurs.
382 * @param {number} delay Timeout delay in milliseconds.
385 Flow
.prototype.setTimeout_ = function(func
, delay
) {
386 this.clearTimeout_();
387 this.timeoutId_
= setTimeout(function() {
388 this.timeoutId_
= null;
394 * Clears any currently active timeout.
397 Flow
.prototype.clearTimeout_ = function() {
398 if (this.timeoutId_
!= null) {
399 clearTimeout(this.timeoutId_
);
400 this.timeoutId_
= null;
405 * Updates the training error UI.
408 Flow
.prototype.updateErrorUI_ = function() {
412 var trainingSteps
= this.getCurrentTrainingStep_('listening');
413 var steps
= trainingSteps
.steps
;
415 $(this.trainingPagePrefix_
+ '-toast').hidden
=
416 this.trainingState_
!= TrainingState
.TIMEOUT
;
417 if (this.trainingState_
== TrainingState
.RESET
) {
418 // We reset the training to begin at the first step.
419 // The first step is reset to 'listening', while the rest
420 // are reset to 'not-started'.
421 var prompt
= loadTimeData
.getString('trainingFirstPrompt');
422 for (var i
= 0; i
< steps
.length
; ++i
) {
423 steps
[i
].classList
.remove('recorded');
425 steps
[i
].classList
.remove('not-started');
426 steps
[i
].classList
.add('listening');
428 steps
[i
].classList
.add('not-started');
429 if (i
== steps
.length
- 1)
430 prompt
= loadTimeData
.getString('trainingLastPrompt');
432 prompt
= loadTimeData
.getString('trainingMiddlePrompt');
434 steps
[i
].querySelector('.text').textContent
= prompt
;
437 // Reset the buttonbar.
438 $(this.trainingPagePrefix_
+ '-processing').hidden
= true;
439 $(this.trainingPagePrefix_
+ '-wait').hidden
= false;
440 $(this.trainingPagePrefix_
+ '-error').hidden
= true;
441 $(this.trainingPagePrefix_
+ '-retry').hidden
= true;
442 } else if (this.trainingState_
== TrainingState
.TIMEOUT
) {
443 var curStep
= trainingSteps
.current
;
445 curStep
.classList
.remove('listening');
446 curStep
.classList
.add('not-started');
449 // Set a timeout before focusing the Retry button so that screenreaders
450 // have time to announce the timeout first.
451 this.setTimeout_(function() {
452 $(this.trainingPagePrefix_
+ '-toast').children
[1].focus();
454 } else if (this.trainingState_
== TrainingState
.ERROR
) {
455 // Update the buttonbar.
456 $(this.trainingPagePrefix_
+ '-wait').hidden
= true;
457 $(this.trainingPagePrefix_
+ '-error').hidden
= false;
458 $(this.trainingPagePrefix_
+ '-retry').hidden
= false;
459 $(this.trainingPagePrefix_
+ '-processing').hidden
= false;
461 // Set a timeout before focusing the Retry button so that screenreaders
462 // have time to announce the error first.
463 this.setTimeout_(function() {
464 $(this.trainingPagePrefix_
+ '-retry').children
[0].focus();
470 * Handles a hotword trigger event and updates the training UI.
473 Flow
.prototype.handleHotwordTrigger_ = function() {
474 var trainingSteps
= this.getCurrentTrainingStep_('listening');
476 if (!trainingSteps
.current
)
479 var index
= trainingSteps
.index
;
480 this.hotwordTriggerReceived_
[index
] = true;
482 trainingSteps
.current
.querySelector('.text').textContent
=
483 loadTimeData
.getString('trainingRecorded');
484 trainingSteps
.current
.classList
.remove('listening');
485 trainingSteps
.current
.classList
.add('recorded');
487 if (trainingSteps
.steps
[index
+ 1]) {
488 trainingSteps
.steps
[index
+ 1].classList
.remove('not-started');
489 trainingSteps
.steps
[index
+ 1].classList
.add('listening');
490 this.waitForHotwordTrigger_(index
+ 1);
494 // Only the last step makes it here.
495 var buttonElem
= $(this.trainingPagePrefix_
+ '-processing').hidden
= false;
496 this.finalizeSpeakerModel_();
500 * Handles a chrome.idle.onStateChanged event and times out the training if
501 * the state is "locked".
502 * @param {!string} state State, one of "active", "idle", or "locked".
505 Flow
.prototype.handleIdleStateChanged_ = function(state
) {
506 if (state
== 'locked')
507 this.timeoutTraining_();
511 * Handles a chrome.hotwordPrivate.onEnabledChanged event and times out
512 * training if the user is no longer the active user (user switches profiles).
515 Flow
.prototype.handleEnabledChanged_ = function() {
516 if (chrome
.hotwordPrivate
.getStatus
) {
517 chrome
.hotwordPrivate
.getStatus(function(status
) {
518 if (status
.userIsActive
)
521 this.timeoutTraining_();
527 * Gets and starts the appropriate flow for the launch mode.
528 * @param {chrome.hotwordPrivate.LaunchState} state Launch state of the
529 * Hotword Audio Verification App.
532 Flow
.prototype.startFlowForMode_ = function(state
) {
533 this.launchMode_
= state
.launchMode
;
534 assert(state
.launchMode
>= 0 && state
.launchMode
< FLOWS
.length
,
535 'Invalid Launch Mode.');
536 this.currentFlow_
= FLOWS
[state
.launchMode
];
537 if (state
.launchMode
== LaunchMode
.HOTWORD_ONLY
) {
538 $('intro-description-audio-history-enabled').hidden
= false;
539 } else if (state
.launchMode
== LaunchMode
.HOTWORD_AND_AUDIO_HISTORY
) {
540 $('intro-description').hidden
= false;
547 * Displays the current step. If the current step is not the first step,
548 * also hides the previous step. Focuses the current step's first button.
551 Flow
.prototype.showStep_ = function() {
552 var currentStepId
= this.currentFlow_
[this.currentStepIndex_
];
553 var currentStep
= document
.getElementById(currentStepId
);
554 currentStep
.hidden
= false;
556 cr
.ui
.setInitialFocus(currentStep
);
558 var previousStep
= null;
559 if (this.currentStepIndex_
> 0)
560 previousStep
= this.currentFlow_
[this.currentStepIndex_
- 1];
563 document
.getElementById(previousStep
).hidden
= true;
565 chrome
.app
.window
.current().show();