Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / hotword_audio_verification / flow.js
blob9c0c4b016000bd674cc6e4203b8a2cd741e25817
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 (function() {
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';
13 /**
14 * These flows correspond to the three LaunchModes as defined in
15 * chrome/browser/search/hotword_service.h and should be kept in sync
16 * with them.
17 * @const
19 var FLOWS = [
20 [START, SPEECH_TRAINING, FINISH],
21 [START, AUDIO_HISTORY, SPEECH_TRAINING, FINISH],
22 [SPEECH_TRAINING, FINISH]
25 /**
26 * The launch mode. This enum needs to be kept in sync with that of
27 * the same name in hotword_service.h.
28 * @enum {number}
30 var LaunchMode = {
31 HOTWORD_ONLY: 0,
32 HOTWORD_AND_AUDIO_HISTORY: 1,
33 RETRAIN: 2
36 /**
37 * The training state.
38 * @enum {string}
40 var TrainingState = {
41 RESET: 'reset',
42 TIMEOUT: 'timeout',
43 ERROR: 'error',
46 /**
47 * Class to control the page flow of the always-on hotword and
48 * Audio History opt-in process.
49 * @constructor
51 function Flow() {
52 this.currentStepIndex_ = -1;
53 this.currentFlow_ = [];
55 /**
56 * The mode that this app was launched in.
57 * @private {LaunchMode}
59 this.launchMode_ = LaunchMode.HOTWORD_AND_AUDIO_HISTORY;
61 /**
62 * Whether this flow is currently in the process of training a voice model.
63 * @private {boolean}
65 this.training_ = false;
67 /**
68 * The current training state.
69 * @private {?TrainingState}
71 this.trainingState_ = null;
73 /**
74 * Whether an expected hotword trigger has been received, indexed by
75 * training step.
76 * @private {boolean[]}
78 this.hotwordTriggerReceived_ = [];
80 /**
81 * Prefix of the element ids for the page that is currently training.
82 * @private {string}
84 this.trainingPagePrefix_ = 'speech-training';
86 /**
87 * Whether the speaker model for this flow has been finalized.
88 * @private {boolean}
90 this.speakerModelFinalized_ = false;
92 /**
93 * ID of the currently active timeout.
94 * @private {?number}
96 this.timeoutId_ = null;
98 /**
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.
150 if (this.training_)
151 return;
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() {
171 if (!this.training_)
172 return;
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() {
187 // Update UI
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.
204 * @private
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();
217 }.bind(this), 50);
221 * Callback for when an audio history request completes.
222 * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history
223 * request state.
224 * @private
226 Flow.prototype.onAudioHistoryRequestCompleted_ = function(state) {
227 if (!state.success || !state.enabled) {
228 this.handleAudioHistoryError_();
229 return;
232 this.advanceStep();
236 * Shows an error if the speaker model has not been finalized.
237 * @private
239 Flow.prototype.handleSpeakerModelFinalizedError_ = function() {
240 if (!this.training_)
241 return;
243 if (this.speakerModelFinalized_)
244 return;
246 this.updateTrainingState_(TrainingState.ERROR);
247 this.stopTraining();
251 * Handles the speaker model finalized event.
252 * @private
254 Flow.prototype.onSpeakerModelFinalized_ = function() {
255 this.speakerModelFinalized_ = true;
256 if (chrome.hotwordPrivate.onSpeakerModelSaved) {
257 chrome.hotwordPrivate.onSpeakerModelSaved.removeListener(
258 this.speakerModelFinalizedListener_);
260 this.stopTraining();
261 this.setTimeout_(this.finishFlow_.bind(this), 2000);
265 * Completes the training process.
266 * @private
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))
281 return;
283 this.startTraining();
284 this.updateTrainingState_(TrainingState.RESET);
287 // ---- private methods:
290 * Completes the training process.
291 * @private
293 Flow.prototype.finalizeSpeakerModel_ = function() {
294 if (!this.training_)
295 return;
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
314 * training step.
315 * @return {Object} The current training step, its index, and an array of
316 * all training steps. Any of these can be undefined.
317 * @private
319 Flow.prototype.getCurrentTrainingStep_ = function(curStepClassName) {
320 var steps =
321 $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train');
322 var curStep =
323 $(this.trainingPagePrefix_ + '-training').querySelector('.listening');
325 return {current: curStep,
326 index: Array.prototype.indexOf.call(steps, curStep),
327 steps: steps};
331 * Updates the training state.
332 * @param {TrainingState} state The training state.
333 * @private
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.
343 * @private
345 Flow.prototype.waitForHotwordTrigger_ = function(index) {
346 if (!this.training_)
347 return;
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.
356 * @private
358 Flow.prototype.handleTrainingTimeout_ = function(index) {
359 if (this.hotwordTriggerReceived_[index])
360 return;
362 this.timeoutTraining_();
366 * Times out training and updates the UI to show a "retry" message, if
367 * currently training.
368 * @private
370 Flow.prototype.timeoutTraining_ = function() {
371 if (!this.training_)
372 return;
374 this.clearTimeout_();
375 this.updateTrainingState_(TrainingState.TIMEOUT);
376 this.stopTraining();
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.
383 * @private
385 Flow.prototype.setTimeout_ = function(func, delay) {
386 this.clearTimeout_();
387 this.timeoutId_ = setTimeout(function() {
388 this.timeoutId_ = null;
389 func();
390 }, delay);
394 * Clears any currently active timeout.
395 * @private
397 Flow.prototype.clearTimeout_ = function() {
398 if (this.timeoutId_ != null) {
399 clearTimeout(this.timeoutId_);
400 this.timeoutId_ = null;
405 * Updates the training error UI.
406 * @private
408 Flow.prototype.updateErrorUI_ = function() {
409 if (!this.training_)
410 return;
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');
424 if (i == 0) {
425 steps[i].classList.remove('not-started');
426 steps[i].classList.add('listening');
427 } else {
428 steps[i].classList.add('not-started');
429 if (i == steps.length - 1)
430 prompt = loadTimeData.getString('trainingLastPrompt');
431 else
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;
444 if (curStep) {
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();
453 }.bind(this), 50);
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();
465 }.bind(this), 50);
470 * Handles a hotword trigger event and updates the training UI.
471 * @private
473 Flow.prototype.handleHotwordTrigger_ = function() {
474 var trainingSteps = this.getCurrentTrainingStep_('listening');
476 if (!trainingSteps.current)
477 return;
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);
491 return;
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".
503 * @private
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).
513 * @private
515 Flow.prototype.handleEnabledChanged_ = function() {
516 if (chrome.hotwordPrivate.getStatus) {
517 chrome.hotwordPrivate.getStatus(function(status) {
518 if (status.userIsActive)
519 return;
521 this.timeoutTraining_();
522 }.bind(this));
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.
530 * @private
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;
543 this.advanceStep();
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.
549 * @private
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];
562 if (previousStep)
563 document.getElementById(previousStep).hidden = true;
565 chrome.app.window.current().show();
568 window.Flow = Flow;
569 })();