diff options
Diffstat (limited to 'chromium/chrome/browser/resources/hotword_audio_verification/flow.js')
-rw-r--r-- | chromium/chrome/browser/resources/hotword_audio_verification/flow.js | 449 |
1 files changed, 397 insertions, 52 deletions
diff --git a/chromium/chrome/browser/resources/hotword_audio_verification/flow.js b/chromium/chrome/browser/resources/hotword_audio_verification/flow.js index abf7429d037..9c0c4b01600 100644 --- a/chromium/chrome/browser/resources/hotword_audio_verification/flow.js +++ b/chromium/chrome/browser/resources/hotword_audio_verification/flow.js @@ -5,10 +5,10 @@ (function() { // Correspond to steps in the hotword opt-in flow. - /** @const */ var HOTWORD_AUDIO_HISTORY = 'hotword-audio-history-container'; - /** @const */ var HOTWORD_ONLY_START = 'hotword-only-container'; + /** @const */ var START = 'start-container'; + /** @const */ var AUDIO_HISTORY = 'audio-history-container'; /** @const */ var SPEECH_TRAINING = 'speech-training-container'; - /** @const */ var FINISHED = 'finished-container'; + /** @const */ var FINISH = 'finish-container'; /** * These flows correspond to the three LaunchModes as defined in @@ -17,9 +17,9 @@ * @const */ var FLOWS = [ - [HOTWORD_ONLY_START, FINISHED], - [HOTWORD_AUDIO_HISTORY, SPEECH_TRAINING, FINISHED], - [SPEECH_TRAINING, FINISHED] + [START, SPEECH_TRAINING, FINISH], + [START, AUDIO_HISTORY, SPEECH_TRAINING, FINISH], + [SPEECH_TRAINING, FINISH] ]; /** @@ -34,6 +34,16 @@ }; /** + * The training state. + * @enum {string} + */ + var TrainingState = { + RESET: 'reset', + TIMEOUT: 'timeout', + ERROR: 'error', + }; + + /** * Class to control the page flow of the always-on hotword and * Audio History opt-in process. * @constructor @@ -43,7 +53,7 @@ this.currentFlow_ = []; /** - * Whether this flow is currently in the process of training a voice model. + * The mode that this app was launched in. * @private {LaunchMode} */ this.launchMode_ = LaunchMode.HOTWORD_AND_AUDIO_HISTORY; @@ -55,19 +65,73 @@ this.training_ = false; /** + * The current training state. + * @private {?TrainingState} + */ + this.trainingState_ = null; + + /** + * Whether an expected hotword trigger has been received, indexed by + * training step. + * @private {boolean[]} + */ + this.hotwordTriggerReceived_ = []; + + /** * Prefix of the element ids for the page that is currently training. * @private {string} */ - this.trainingPagePrefix_ = ''; + this.trainingPagePrefix_ = 'speech-training'; + + /** + * Whether the speaker model for this flow has been finalized. + * @private {boolean} + */ + this.speakerModelFinalized_ = false; + + /** + * ID of the currently active timeout. + * @private {?number} + */ + this.timeoutId_ = null; + + /** + * Listener for the speakerModelSaved event. + * @private {Function} + */ + this.speakerModelFinalizedListener_ = + this.onSpeakerModelFinalized_.bind(this); + + /** + * Listener for the hotword trigger event. + * @private {Function} + */ + this.hotwordTriggerListener_ = + this.handleHotwordTrigger_.bind(this); + + // Listen for the user locking the screen. + chrome.idle.onStateChanged.addListener( + this.handleIdleStateChanged_.bind(this)); + + // Listen for hotword settings changes. This used to detect when the user + // switches to a different profile. + if (chrome.hotwordPrivate.onEnabledChanged) { + chrome.hotwordPrivate.onEnabledChanged.addListener( + this.handleEnabledChanged_.bind(this)); + } } /** - * Advances the current step. + * Advances the current step. Begins training if the speech-training + * page has been reached. */ Flow.prototype.advanceStep = function() { this.currentStepIndex_++; - if (this.currentStepIndex_ < this.currentFlow_.length) + if (this.currentStepIndex_ < this.currentFlow_.length) { + if (this.currentFlow_[this.currentStepIndex_] == SPEECH_TRAINING) + this.startTraining(); this.showStep_.apply(this); + } }; /** @@ -87,17 +151,15 @@ return; this.training_ = true; - if (this.launchMode_ == LaunchMode.HOTWORD_ONLY) { - this.trainingPagePrefix_ = 'hotword-only'; - } else if (this.launchMode_ == LaunchMode.HOTWORD_AND_AUDIO_HISTORY || - this.launchMode_ == LaunchMode.RETRAIN) { - this.trainingPagePrefix_ = 'speech-training'; - } - if (chrome.hotwordPrivate.onHotwordTriggered) { + if (chrome.hotwordPrivate.onHotwordTriggered && + !chrome.hotwordPrivate.onHotwordTriggered.hasListener( + this.hotwordTriggerListener_)) { chrome.hotwordPrivate.onHotwordTriggered.addListener( - this.handleHotwordTrigger_.bind(this)); + this.hotwordTriggerListener_); } + + this.waitForHotwordTrigger_(0); if (chrome.hotwordPrivate.startTraining) chrome.hotwordPrivate.startTraining(); }; @@ -112,27 +174,116 @@ this.training_ = false; if (chrome.hotwordPrivate.onHotwordTriggered) { chrome.hotwordPrivate.onHotwordTriggered. - removeListener(this.handleHotwordTrigger_); + removeListener(this.hotwordTriggerListener_); } if (chrome.hotwordPrivate.stopTraining) chrome.hotwordPrivate.stopTraining(); }; /** - * Handles the speaker model finalized event. + * Attempts to enable audio history for the signed-in account. + */ + Flow.prototype.enableAudioHistory = function() { + // Update UI + $('audio-history-agree').disabled = true; + $('audio-history-cancel').disabled = true; + + $('audio-history-error').hidden = true; + $('audio-history-wait').hidden = false; + + if (chrome.hotwordPrivate.setAudioHistoryEnabled) { + chrome.hotwordPrivate.setAudioHistoryEnabled( + true, this.onAudioHistoryRequestCompleted_.bind(this)); + } + }; + + // ---- private methods: + + /** + * Shows an error if the audio history setting was not enabled successfully. + * @private */ - Flow.prototype.onSpeakerModelFinalized = function() { + Flow.prototype.handleAudioHistoryError_ = function() { + $('audio-history-agree').disabled = false; + $('audio-history-cancel').disabled = false; + + $('audio-history-wait').hidden = true; + $('audio-history-error').hidden = false; + + // Set a timeout before focusing the Enable button so that screenreaders + // have time to announce the error first. + this.setTimeout_(function() { + $('audio-history-agree').focus(); + }.bind(this), 50); + }; + + /** + * Callback for when an audio history request completes. + * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history + * request state. + * @private + */ + Flow.prototype.onAudioHistoryRequestCompleted_ = function(state) { + if (!state.success || !state.enabled) { + this.handleAudioHistoryError_(); + return; + } + + this.advanceStep(); + }; + + /** + * Shows an error if the speaker model has not been finalized. + * @private + */ + Flow.prototype.handleSpeakerModelFinalizedError_ = function() { + if (!this.training_) + return; + + if (this.speakerModelFinalized_) + return; + + this.updateTrainingState_(TrainingState.ERROR); this.stopTraining(); + }; - if (chrome.hotwordPrivate.setAudioLoggingEnabled) - chrome.hotwordPrivate.setAudioLoggingEnabled(true, function() {}); + /** + * Handles the speaker model finalized event. + * @private + */ + Flow.prototype.onSpeakerModelFinalized_ = function() { + this.speakerModelFinalized_ = true; + if (chrome.hotwordPrivate.onSpeakerModelSaved) { + chrome.hotwordPrivate.onSpeakerModelSaved.removeListener( + this.speakerModelFinalizedListener_); + } + this.stopTraining(); + this.setTimeout_(this.finishFlow_.bind(this), 2000); + }; + /** + * Completes the training process. + * @private + */ + Flow.prototype.finishFlow_ = function() { if (chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled) { chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled(true, this.advanceStep.bind(this)); } }; + /** + * Handles a user clicking on the retry button. + */ + Flow.prototype.handleRetry = function() { + if (!(this.trainingState_ == TrainingState.TIMEOUT || + this.trainingState_ == TrainingState.ERROR)) + return; + + this.startTraining(); + this.updateTrainingState_(TrainingState.RESET); + }; + // ---- private methods: /** @@ -143,12 +294,176 @@ if (!this.training_) return; + // Listen for the success event from the NaCl module. + if (chrome.hotwordPrivate.onSpeakerModelSaved && + !chrome.hotwordPrivate.onSpeakerModelSaved.hasListener( + this.speakerModelFinalizedListener_)) { + chrome.hotwordPrivate.onSpeakerModelSaved.addListener( + this.speakerModelFinalizedListener_); + } + + this.speakerModelFinalized_ = false; + this.setTimeout_(this.handleSpeakerModelFinalizedError_.bind(this), 30000); if (chrome.hotwordPrivate.finalizeSpeakerModel) chrome.hotwordPrivate.finalizeSpeakerModel(); + }; - // TODO(kcarattini): Implement a notification that speaker model has been - // finalized instead of setting a timeout. - setTimeout(this.onSpeakerModelFinalized.bind(this), 2000); + /** + * Returns the current training step. + * @param {string} curStepClassName The name of the class of the current + * training step. + * @return {Object} The current training step, its index, and an array of + * all training steps. Any of these can be undefined. + * @private + */ + Flow.prototype.getCurrentTrainingStep_ = function(curStepClassName) { + var steps = + $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train'); + var curStep = + $(this.trainingPagePrefix_ + '-training').querySelector('.listening'); + + return {current: curStep, + index: Array.prototype.indexOf.call(steps, curStep), + steps: steps}; + }; + + /** + * Updates the training state. + * @param {TrainingState} state The training state. + * @private + */ + Flow.prototype.updateTrainingState_ = function(state) { + this.trainingState_ = state; + this.updateErrorUI_(); + }; + + /** + * Waits two minutes and then checks for a training error. + * @param {number} index The index of the training step. + * @private + */ + Flow.prototype.waitForHotwordTrigger_ = function(index) { + if (!this.training_) + return; + + this.hotwordTriggerReceived_[index] = false; + this.setTimeout_(this.handleTrainingTimeout_.bind(this, index), 120000); + }; + + /** + * Checks for and handles a training error. + * @param {number} index The index of the training step. + * @private + */ + Flow.prototype.handleTrainingTimeout_ = function(index) { + if (this.hotwordTriggerReceived_[index]) + return; + + this.timeoutTraining_(); + }; + + /** + * Times out training and updates the UI to show a "retry" message, if + * currently training. + * @private + */ + Flow.prototype.timeoutTraining_ = function() { + if (!this.training_) + return; + + this.clearTimeout_(); + this.updateTrainingState_(TrainingState.TIMEOUT); + this.stopTraining(); + }; + + /** + * Sets a timeout. If any timeout is active, clear it. + * @param {Function} func The function to invoke when the timeout occurs. + * @param {number} delay Timeout delay in milliseconds. + * @private + */ + Flow.prototype.setTimeout_ = function(func, delay) { + this.clearTimeout_(); + this.timeoutId_ = setTimeout(function() { + this.timeoutId_ = null; + func(); + }, delay); + }; + + /** + * Clears any currently active timeout. + * @private + */ + Flow.prototype.clearTimeout_ = function() { + if (this.timeoutId_ != null) { + clearTimeout(this.timeoutId_); + this.timeoutId_ = null; + } + }; + + /** + * Updates the training error UI. + * @private + */ + Flow.prototype.updateErrorUI_ = function() { + if (!this.training_) + return; + + var trainingSteps = this.getCurrentTrainingStep_('listening'); + var steps = trainingSteps.steps; + + $(this.trainingPagePrefix_ + '-toast').hidden = + this.trainingState_ != TrainingState.TIMEOUT; + if (this.trainingState_ == TrainingState.RESET) { + // We reset the training to begin at the first step. + // The first step is reset to 'listening', while the rest + // are reset to 'not-started'. + var prompt = loadTimeData.getString('trainingFirstPrompt'); + for (var i = 0; i < steps.length; ++i) { + steps[i].classList.remove('recorded'); + if (i == 0) { + steps[i].classList.remove('not-started'); + steps[i].classList.add('listening'); + } else { + steps[i].classList.add('not-started'); + if (i == steps.length - 1) + prompt = loadTimeData.getString('trainingLastPrompt'); + else + prompt = loadTimeData.getString('trainingMiddlePrompt'); + } + steps[i].querySelector('.text').textContent = prompt; + } + + // Reset the buttonbar. + $(this.trainingPagePrefix_ + '-processing').hidden = true; + $(this.trainingPagePrefix_ + '-wait').hidden = false; + $(this.trainingPagePrefix_ + '-error').hidden = true; + $(this.trainingPagePrefix_ + '-retry').hidden = true; + } else if (this.trainingState_ == TrainingState.TIMEOUT) { + var curStep = trainingSteps.current; + if (curStep) { + curStep.classList.remove('listening'); + curStep.classList.add('not-started'); + } + + // Set a timeout before focusing the Retry button so that screenreaders + // have time to announce the timeout first. + this.setTimeout_(function() { + $(this.trainingPagePrefix_ + '-toast').children[1].focus(); + }.bind(this), 50); + } else if (this.trainingState_ == TrainingState.ERROR) { + // Update the buttonbar. + $(this.trainingPagePrefix_ + '-wait').hidden = true; + $(this.trainingPagePrefix_ + '-error').hidden = false; + $(this.trainingPagePrefix_ + '-retry').hidden = false; + $(this.trainingPagePrefix_ + '-processing').hidden = false; + + // Set a timeout before focusing the Retry button so that screenreaders + // have time to announce the error first. + this.setTimeout_(function() { + $(this.trainingPagePrefix_ + '-retry').children[0].focus(); + }.bind(this), 50); + } }; /** @@ -156,33 +471,59 @@ * @private */ Flow.prototype.handleHotwordTrigger_ = function() { - var curStep = - $(this.trainingPagePrefix_ + '-training').querySelector('.listening'); - // TODO(kcarattini): Localize this string. - curStep.querySelector('.text').textContent = 'Recorded'; - curStep.classList.remove('listening'); - curStep.classList.add('recorded'); + var trainingSteps = this.getCurrentTrainingStep_('listening'); - var steps = - $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train'); - var index = Array.prototype.indexOf.call(steps, curStep); - if (steps[index + 1]) { - steps[index + 1].classList.remove('not-started'); - steps[index + 1].classList.add('listening'); + if (!trainingSteps.current) + return; + + var index = trainingSteps.index; + this.hotwordTriggerReceived_[index] = true; + + trainingSteps.current.querySelector('.text').textContent = + loadTimeData.getString('trainingRecorded'); + trainingSteps.current.classList.remove('listening'); + trainingSteps.current.classList.add('recorded'); + + if (trainingSteps.steps[index + 1]) { + trainingSteps.steps[index + 1].classList.remove('not-started'); + trainingSteps.steps[index + 1].classList.add('listening'); + this.waitForHotwordTrigger_(index + 1); return; } // Only the last step makes it here. - var buttonElem = $(this.trainingPagePrefix_ + '-cancel-button'); - // TODO(kcarattini): Localize this string. - buttonElem.textContent = 'Please wait ...'; - buttonElem.classList.add('grayed-out'); - buttonElem.classList.remove('finish-button'); - + var buttonElem = $(this.trainingPagePrefix_ + '-processing').hidden = false; this.finalizeSpeakerModel_(); }; /** + * Handles a chrome.idle.onStateChanged event and times out the training if + * the state is "locked". + * @param {!string} state State, one of "active", "idle", or "locked". + * @private + */ + Flow.prototype.handleIdleStateChanged_ = function(state) { + if (state == 'locked') + this.timeoutTraining_(); + }; + + /** + * Handles a chrome.hotwordPrivate.onEnabledChanged event and times out + * training if the user is no longer the active user (user switches profiles). + * @private + */ + Flow.prototype.handleEnabledChanged_ = function() { + if (chrome.hotwordPrivate.getStatus) { + chrome.hotwordPrivate.getStatus(function(status) { + if (status.userIsActive) + return; + + this.timeoutTraining_(); + }.bind(this)); + } + }; + + /** * Gets and starts the appropriate flow for the launch mode. * @param {chrome.hotwordPrivate.LaunchState} state Launch state of the * Hotword Audio Verification App. @@ -193,22 +534,26 @@ assert(state.launchMode >= 0 && state.launchMode < FLOWS.length, 'Invalid Launch Mode.'); this.currentFlow_ = FLOWS[state.launchMode]; - this.advanceStep(); - // If the flow begins with a a training step, then start the training flow. - if (state.launchMode == LaunchMode.HOTWORD_ONLY || - state.launchMode == LaunchMode.RETRAIN) { - this.startTraining(); + if (state.launchMode == LaunchMode.HOTWORD_ONLY) { + $('intro-description-audio-history-enabled').hidden = false; + } else if (state.launchMode == LaunchMode.HOTWORD_AND_AUDIO_HISTORY) { + $('intro-description').hidden = false; } + + this.advanceStep(); }; /** * Displays the current step. If the current step is not the first step, - * also hides the previous step. + * also hides the previous step. Focuses the current step's first button. * @private */ Flow.prototype.showStep_ = function() { - var currentStep = this.currentFlow_[this.currentStepIndex_]; - document.getElementById(currentStep).hidden = false; + var currentStepId = this.currentFlow_[this.currentStepIndex_]; + var currentStep = document.getElementById(currentStepId); + currentStep.hidden = false; + + cr.ui.setInitialFocus(currentStep); var previousStep = null; if (this.currentStepIndex_ > 0) |