OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 (function() { | 5 (function() { |
6 | 6 |
7 // Correspond to steps in the hotword opt-in flow. | 7 // Correspond to steps in the hotword opt-in flow. |
8 /** @const */ var START = 'start-container'; | 8 /** @const */ var START = 'start-container'; |
9 /** @const */ var AUDIO_HISTORY = 'audio-history-container'; | 9 /** @const */ var AUDIO_HISTORY = 'audio-history-container'; |
10 /** @const */ var SPEECH_TRAINING = 'speech-training-container'; | 10 /** @const */ var SPEECH_TRAINING = 'speech-training-container'; |
11 /** @const */ var FINISH = 'finish-container'; | 11 /** @const */ var FINISH = 'finish-container'; |
12 | 12 |
13 /** | 13 /** |
14 * These flows correspond to the three LaunchModes as defined in | 14 * These flows correspond to the three LaunchModes as defined in |
15 * chrome/browser/search/hotword_service.h and should be kept in sync | 15 * chrome/browser/search/hotword_service.h and should be kept in sync |
16 * with them. | 16 * with them. |
17 * @const | 17 * @const |
18 */ | 18 */ |
19 var FLOWS = [ | 19 var FLOWS = [ |
20 [START, SPEECH_TRAINING, FINISH], | 20 [START, SPEECH_TRAINING, FINISH], |
21 [START, AUDIO_HISTORY, SPEECH_TRAINING, FINISH], | 21 [START, AUDIO_HISTORY, SPEECH_TRAINING, FINISH], [SPEECH_TRAINING, FINISH] |
22 [SPEECH_TRAINING, FINISH] | 22 ]; |
23 ]; | 23 |
24 | 24 /** |
25 /** | 25 * The launch mode. This enum needs to be kept in sync with that of |
26 * The launch mode. This enum needs to be kept in sync with that of | 26 * the same name in hotword_service.h. |
27 * the same name in hotword_service.h. | 27 * @enum {number} |
28 * @enum {number} | 28 */ |
29 */ | 29 var LaunchMode = {HOTWORD_ONLY: 0, HOTWORD_AND_AUDIO_HISTORY: 1, RETRAIN: 2}; |
30 var LaunchMode = { | 30 |
31 HOTWORD_ONLY: 0, | 31 /** |
32 HOTWORD_AND_AUDIO_HISTORY: 1, | 32 * The training state. |
33 RETRAIN: 2 | 33 * @enum {string} |
| 34 */ |
| 35 var TrainingState = { |
| 36 RESET: 'reset', |
| 37 TIMEOUT: 'timeout', |
| 38 ERROR: 'error', |
| 39 }; |
| 40 |
| 41 /** |
| 42 * Class to control the page flow of the always-on hotword and |
| 43 * Audio History opt-in process. |
| 44 * @constructor |
| 45 */ |
| 46 function Flow() { |
| 47 this.currentStepIndex_ = -1; |
| 48 this.currentFlow_ = []; |
| 49 |
| 50 /** |
| 51 * The mode that this app was launched in. |
| 52 * @private {LaunchMode} |
| 53 */ |
| 54 this.launchMode_ = LaunchMode.HOTWORD_AND_AUDIO_HISTORY; |
| 55 |
| 56 /** |
| 57 * Whether this flow is currently in the process of training a voice model. |
| 58 * @private {boolean} |
| 59 */ |
| 60 this.training_ = false; |
| 61 |
| 62 /** |
| 63 * The current training state. |
| 64 * @private {?TrainingState} |
| 65 */ |
| 66 this.trainingState_ = null; |
| 67 |
| 68 /** |
| 69 * Whether an expected hotword trigger has been received, indexed by |
| 70 * training step. |
| 71 * @private {boolean[]} |
| 72 */ |
| 73 this.hotwordTriggerReceived_ = []; |
| 74 |
| 75 /** |
| 76 * Prefix of the element ids for the page that is currently training. |
| 77 * @private {string} |
| 78 */ |
| 79 this.trainingPagePrefix_ = 'speech-training'; |
| 80 |
| 81 /** |
| 82 * Whether the speaker model for this flow has been finalized. |
| 83 * @private {boolean} |
| 84 */ |
| 85 this.speakerModelFinalized_ = false; |
| 86 |
| 87 /** |
| 88 * ID of the currently active timeout. |
| 89 * @private {?number} |
| 90 */ |
| 91 this.timeoutId_ = null; |
| 92 |
| 93 /** |
| 94 * Listener for the speakerModelSaved event. |
| 95 * @private {Function} |
| 96 */ |
| 97 this.speakerModelFinalizedListener_ = |
| 98 this.onSpeakerModelFinalized_.bind(this); |
| 99 |
| 100 /** |
| 101 * Listener for the hotword trigger event. |
| 102 * @private {Function} |
| 103 */ |
| 104 this.hotwordTriggerListener_ = this.handleHotwordTrigger_.bind(this); |
| 105 |
| 106 // Listen for the user locking the screen. |
| 107 chrome.idle.onStateChanged.addListener( |
| 108 this.handleIdleStateChanged_.bind(this)); |
| 109 |
| 110 // Listen for hotword settings changes. This used to detect when the user |
| 111 // switches to a different profile. |
| 112 if (chrome.hotwordPrivate.onEnabledChanged) { |
| 113 chrome.hotwordPrivate.onEnabledChanged.addListener( |
| 114 this.handleEnabledChanged_.bind(this)); |
| 115 } |
| 116 } |
| 117 |
| 118 /** |
| 119 * Advances the current step. Begins training if the speech-training |
| 120 * page has been reached. |
| 121 */ |
| 122 Flow.prototype.advanceStep = function() { |
| 123 this.currentStepIndex_++; |
| 124 if (this.currentStepIndex_ < this.currentFlow_.length) { |
| 125 if (this.currentFlow_[this.currentStepIndex_] == SPEECH_TRAINING) |
| 126 this.startTraining(); |
| 127 this.showStep_.apply(this); |
| 128 } |
| 129 }; |
| 130 |
| 131 /** |
| 132 * Gets the appropriate flow and displays its first page. |
| 133 */ |
| 134 Flow.prototype.startFlow = function() { |
| 135 if (chrome.hotwordPrivate && chrome.hotwordPrivate.getLaunchState) |
| 136 chrome.hotwordPrivate.getLaunchState(this.startFlowForMode_.bind(this)); |
| 137 }; |
| 138 |
| 139 /** |
| 140 * Starts the training process. |
| 141 */ |
| 142 Flow.prototype.startTraining = function() { |
| 143 // Don't start a training session if one already exists. |
| 144 if (this.training_) |
| 145 return; |
| 146 |
| 147 this.training_ = true; |
| 148 |
| 149 if (chrome.hotwordPrivate.onHotwordTriggered && |
| 150 !chrome.hotwordPrivate.onHotwordTriggered.hasListener( |
| 151 this.hotwordTriggerListener_)) { |
| 152 chrome.hotwordPrivate.onHotwordTriggered.addListener( |
| 153 this.hotwordTriggerListener_); |
| 154 } |
| 155 |
| 156 this.waitForHotwordTrigger_(0); |
| 157 if (chrome.hotwordPrivate.startTraining) |
| 158 chrome.hotwordPrivate.startTraining(); |
| 159 }; |
| 160 |
| 161 /** |
| 162 * Stops the training process. |
| 163 */ |
| 164 Flow.prototype.stopTraining = function() { |
| 165 if (!this.training_) |
| 166 return; |
| 167 |
| 168 this.training_ = false; |
| 169 if (chrome.hotwordPrivate.onHotwordTriggered) { |
| 170 chrome.hotwordPrivate.onHotwordTriggered.removeListener( |
| 171 this.hotwordTriggerListener_); |
| 172 } |
| 173 if (chrome.hotwordPrivate.stopTraining) |
| 174 chrome.hotwordPrivate.stopTraining(); |
| 175 }; |
| 176 |
| 177 /** |
| 178 * Attempts to enable audio history for the signed-in account. |
| 179 */ |
| 180 Flow.prototype.enableAudioHistory = function() { |
| 181 // Update UI |
| 182 $('audio-history-agree').disabled = true; |
| 183 $('audio-history-cancel').disabled = true; |
| 184 |
| 185 $('audio-history-error').hidden = true; |
| 186 $('audio-history-wait').hidden = false; |
| 187 |
| 188 if (chrome.hotwordPrivate.setAudioHistoryEnabled) { |
| 189 chrome.hotwordPrivate.setAudioHistoryEnabled( |
| 190 true, this.onAudioHistoryRequestCompleted_.bind(this)); |
| 191 } |
| 192 }; |
| 193 |
| 194 // ---- private methods: |
| 195 |
| 196 /** |
| 197 * Shows an error if the audio history setting was not enabled successfully. |
| 198 * @private |
| 199 */ |
| 200 Flow.prototype.handleAudioHistoryError_ = function() { |
| 201 $('audio-history-agree').disabled = false; |
| 202 $('audio-history-cancel').disabled = false; |
| 203 |
| 204 $('audio-history-wait').hidden = true; |
| 205 $('audio-history-error').hidden = false; |
| 206 |
| 207 // Set a timeout before focusing the Enable button so that screenreaders |
| 208 // have time to announce the error first. |
| 209 this.setTimeout_(function() { |
| 210 $('audio-history-agree').focus(); |
| 211 }.bind(this), 50); |
| 212 }; |
| 213 |
| 214 /** |
| 215 * Callback for when an audio history request completes. |
| 216 * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history |
| 217 * request state. |
| 218 * @private |
| 219 */ |
| 220 Flow.prototype.onAudioHistoryRequestCompleted_ = function(state) { |
| 221 if (!state.success || !state.enabled) { |
| 222 this.handleAudioHistoryError_(); |
| 223 return; |
| 224 } |
| 225 |
| 226 this.advanceStep(); |
| 227 }; |
| 228 |
| 229 /** |
| 230 * Shows an error if the speaker model has not been finalized. |
| 231 * @private |
| 232 */ |
| 233 Flow.prototype.handleSpeakerModelFinalizedError_ = function() { |
| 234 if (!this.training_) |
| 235 return; |
| 236 |
| 237 if (this.speakerModelFinalized_) |
| 238 return; |
| 239 |
| 240 this.updateTrainingState_(TrainingState.ERROR); |
| 241 this.stopTraining(); |
| 242 }; |
| 243 |
| 244 /** |
| 245 * Handles the speaker model finalized event. |
| 246 * @private |
| 247 */ |
| 248 Flow.prototype.onSpeakerModelFinalized_ = function() { |
| 249 this.speakerModelFinalized_ = true; |
| 250 if (chrome.hotwordPrivate.onSpeakerModelSaved) { |
| 251 chrome.hotwordPrivate.onSpeakerModelSaved.removeListener( |
| 252 this.speakerModelFinalizedListener_); |
| 253 } |
| 254 this.stopTraining(); |
| 255 this.setTimeout_(this.finishFlow_.bind(this), 2000); |
| 256 }; |
| 257 |
| 258 /** |
| 259 * Completes the training process. |
| 260 * @private |
| 261 */ |
| 262 Flow.prototype.finishFlow_ = function() { |
| 263 if (chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled) { |
| 264 chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled( |
| 265 true, this.advanceStep.bind(this)); |
| 266 } |
| 267 }; |
| 268 |
| 269 /** |
| 270 * Handles a user clicking on the retry button. |
| 271 */ |
| 272 Flow.prototype.handleRetry = function() { |
| 273 if (!(this.trainingState_ == TrainingState.TIMEOUT || |
| 274 this.trainingState_ == TrainingState.ERROR)) |
| 275 return; |
| 276 |
| 277 this.startTraining(); |
| 278 this.updateTrainingState_(TrainingState.RESET); |
| 279 }; |
| 280 |
| 281 // ---- private methods: |
| 282 |
| 283 /** |
| 284 * Completes the training process. |
| 285 * @private |
| 286 */ |
| 287 Flow.prototype.finalizeSpeakerModel_ = function() { |
| 288 if (!this.training_) |
| 289 return; |
| 290 |
| 291 // Listen for the success event from the NaCl module. |
| 292 if (chrome.hotwordPrivate.onSpeakerModelSaved && |
| 293 !chrome.hotwordPrivate.onSpeakerModelSaved.hasListener( |
| 294 this.speakerModelFinalizedListener_)) { |
| 295 chrome.hotwordPrivate.onSpeakerModelSaved.addListener( |
| 296 this.speakerModelFinalizedListener_); |
| 297 } |
| 298 |
| 299 this.speakerModelFinalized_ = false; |
| 300 this.setTimeout_(this.handleSpeakerModelFinalizedError_.bind(this), 30000); |
| 301 if (chrome.hotwordPrivate.finalizeSpeakerModel) |
| 302 chrome.hotwordPrivate.finalizeSpeakerModel(); |
| 303 }; |
| 304 |
| 305 /** |
| 306 * Returns the current training step. |
| 307 * @param {string} curStepClassName The name of the class of the current |
| 308 * training step. |
| 309 * @return {Object} The current training step, its index, and an array of |
| 310 * all training steps. Any of these can be undefined. |
| 311 * @private |
| 312 */ |
| 313 Flow.prototype.getCurrentTrainingStep_ = function(curStepClassName) { |
| 314 var steps = |
| 315 $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train'); |
| 316 var curStep = |
| 317 $(this.trainingPagePrefix_ + '-training').querySelector('.listening'); |
| 318 |
| 319 return { |
| 320 current: curStep, |
| 321 index: Array.prototype.indexOf.call(steps, curStep), |
| 322 steps: steps |
34 }; | 323 }; |
35 | 324 }; |
36 /** | 325 |
37 * The training state. | 326 /** |
38 * @enum {string} | 327 * Updates the training state. |
39 */ | 328 * @param {TrainingState} state The training state. |
40 var TrainingState = { | 329 * @private |
41 RESET: 'reset', | 330 */ |
42 TIMEOUT: 'timeout', | 331 Flow.prototype.updateTrainingState_ = function(state) { |
43 ERROR: 'error', | 332 this.trainingState_ = state; |
44 }; | 333 this.updateErrorUI_(); |
45 | 334 }; |
46 /** | 335 |
47 * Class to control the page flow of the always-on hotword and | 336 /** |
48 * Audio History opt-in process. | 337 * Waits two minutes and then checks for a training error. |
49 * @constructor | 338 * @param {number} index The index of the training step. |
50 */ | 339 * @private |
51 function Flow() { | 340 */ |
52 this.currentStepIndex_ = -1; | 341 Flow.prototype.waitForHotwordTrigger_ = function(index) { |
53 this.currentFlow_ = []; | 342 if (!this.training_) |
54 | 343 return; |
55 /** | 344 |
56 * The mode that this app was launched in. | 345 this.hotwordTriggerReceived_[index] = false; |
57 * @private {LaunchMode} | 346 this.setTimeout_(this.handleTrainingTimeout_.bind(this, index), 120000); |
58 */ | 347 }; |
59 this.launchMode_ = LaunchMode.HOTWORD_AND_AUDIO_HISTORY; | 348 |
60 | 349 /** |
61 /** | 350 * Checks for and handles a training error. |
62 * Whether this flow is currently in the process of training a voice model. | 351 * @param {number} index The index of the training step. |
63 * @private {boolean} | 352 * @private |
64 */ | 353 */ |
65 this.training_ = false; | 354 Flow.prototype.handleTrainingTimeout_ = function(index) { |
66 | 355 if (this.hotwordTriggerReceived_[index]) |
67 /** | 356 return; |
68 * The current training state. | 357 |
69 * @private {?TrainingState} | 358 this.timeoutTraining_(); |
70 */ | 359 }; |
71 this.trainingState_ = null; | 360 |
72 | 361 /** |
73 /** | 362 * Times out training and updates the UI to show a "retry" message, if |
74 * Whether an expected hotword trigger has been received, indexed by | 363 * currently training. |
75 * training step. | 364 * @private |
76 * @private {boolean[]} | 365 */ |
77 */ | 366 Flow.prototype.timeoutTraining_ = function() { |
78 this.hotwordTriggerReceived_ = []; | 367 if (!this.training_) |
79 | 368 return; |
80 /** | 369 |
81 * Prefix of the element ids for the page that is currently training. | 370 this.clearTimeout_(); |
82 * @private {string} | 371 this.updateTrainingState_(TrainingState.TIMEOUT); |
83 */ | 372 this.stopTraining(); |
84 this.trainingPagePrefix_ = 'speech-training'; | 373 }; |
85 | 374 |
86 /** | 375 /** |
87 * Whether the speaker model for this flow has been finalized. | 376 * Sets a timeout. If any timeout is active, clear it. |
88 * @private {boolean} | 377 * @param {Function} func The function to invoke when the timeout occurs. |
89 */ | 378 * @param {number} delay Timeout delay in milliseconds. |
90 this.speakerModelFinalized_ = false; | 379 * @private |
91 | 380 */ |
92 /** | 381 Flow.prototype.setTimeout_ = function(func, delay) { |
93 * ID of the currently active timeout. | 382 this.clearTimeout_(); |
94 * @private {?number} | 383 this.timeoutId_ = setTimeout(function() { |
95 */ | |
96 this.timeoutId_ = null; | 384 this.timeoutId_ = null; |
97 | 385 func(); |
98 /** | 386 }, delay); |
99 * Listener for the speakerModelSaved event. | 387 }; |
100 * @private {Function} | 388 |
101 */ | 389 /** |
102 this.speakerModelFinalizedListener_ = | 390 * Clears any currently active timeout. |
103 this.onSpeakerModelFinalized_.bind(this); | 391 * @private |
104 | 392 */ |
105 /** | 393 Flow.prototype.clearTimeout_ = function() { |
106 * Listener for the hotword trigger event. | 394 if (this.timeoutId_ != null) { |
107 * @private {Function} | 395 clearTimeout(this.timeoutId_); |
108 */ | 396 this.timeoutId_ = null; |
109 this.hotwordTriggerListener_ = | 397 } |
110 this.handleHotwordTrigger_.bind(this); | 398 }; |
111 | 399 |
112 // Listen for the user locking the screen. | 400 /** |
113 chrome.idle.onStateChanged.addListener( | 401 * Updates the training error UI. |
114 this.handleIdleStateChanged_.bind(this)); | 402 * @private |
115 | 403 */ |
116 // Listen for hotword settings changes. This used to detect when the user | 404 Flow.prototype.updateErrorUI_ = function() { |
117 // switches to a different profile. | 405 if (!this.training_) |
118 if (chrome.hotwordPrivate.onEnabledChanged) { | 406 return; |
119 chrome.hotwordPrivate.onEnabledChanged.addListener( | 407 |
120 this.handleEnabledChanged_.bind(this)); | 408 var trainingSteps = this.getCurrentTrainingStep_('listening'); |
| 409 var steps = trainingSteps.steps; |
| 410 |
| 411 $(this.trainingPagePrefix_ + '-toast').hidden = |
| 412 this.trainingState_ != TrainingState.TIMEOUT; |
| 413 if (this.trainingState_ == TrainingState.RESET) { |
| 414 // We reset the training to begin at the first step. |
| 415 // The first step is reset to 'listening', while the rest |
| 416 // are reset to 'not-started'. |
| 417 var prompt = loadTimeData.getString('trainingFirstPrompt'); |
| 418 for (var i = 0; i < steps.length; ++i) { |
| 419 steps[i].classList.remove('recorded'); |
| 420 if (i == 0) { |
| 421 steps[i].classList.remove('not-started'); |
| 422 steps[i].classList.add('listening'); |
| 423 } else { |
| 424 steps[i].classList.add('not-started'); |
| 425 if (i == steps.length - 1) |
| 426 prompt = loadTimeData.getString('trainingLastPrompt'); |
| 427 else |
| 428 prompt = loadTimeData.getString('trainingMiddlePrompt'); |
| 429 } |
| 430 steps[i].querySelector('.text').textContent = prompt; |
121 } | 431 } |
122 } | 432 |
123 | 433 // Reset the buttonbar. |
124 /** | 434 $(this.trainingPagePrefix_ + '-processing').hidden = true; |
125 * Advances the current step. Begins training if the speech-training | 435 $(this.trainingPagePrefix_ + '-wait').hidden = false; |
126 * page has been reached. | 436 $(this.trainingPagePrefix_ + '-error').hidden = true; |
127 */ | 437 $(this.trainingPagePrefix_ + '-retry').hidden = true; |
128 Flow.prototype.advanceStep = function() { | 438 } else if (this.trainingState_ == TrainingState.TIMEOUT) { |
129 this.currentStepIndex_++; | 439 var curStep = trainingSteps.current; |
130 if (this.currentStepIndex_ < this.currentFlow_.length) { | 440 if (curStep) { |
131 if (this.currentFlow_[this.currentStepIndex_] == SPEECH_TRAINING) | 441 curStep.classList.remove('listening'); |
132 this.startTraining(); | 442 curStep.classList.add('not-started'); |
133 this.showStep_.apply(this); | |
134 } | 443 } |
135 }; | 444 |
136 | 445 // Set a timeout before focusing the Retry button so that screenreaders |
137 /** | 446 // have time to announce the timeout first. |
138 * Gets the appropriate flow and displays its first page. | 447 this.setTimeout_(function() { |
139 */ | 448 $(this.trainingPagePrefix_ + '-toast').children[1].focus(); |
140 Flow.prototype.startFlow = function() { | 449 }.bind(this), 50); |
141 if (chrome.hotwordPrivate && chrome.hotwordPrivate.getLaunchState) | 450 } else if (this.trainingState_ == TrainingState.ERROR) { |
142 chrome.hotwordPrivate.getLaunchState(this.startFlowForMode_.bind(this)); | 451 // Update the buttonbar. |
143 }; | 452 $(this.trainingPagePrefix_ + '-wait').hidden = true; |
144 | 453 $(this.trainingPagePrefix_ + '-error').hidden = false; |
145 /** | 454 $(this.trainingPagePrefix_ + '-retry').hidden = false; |
146 * Starts the training process. | 455 $(this.trainingPagePrefix_ + '-processing').hidden = false; |
147 */ | 456 |
148 Flow.prototype.startTraining = function() { | 457 // Set a timeout before focusing the Retry button so that screenreaders |
149 // Don't start a training session if one already exists. | |
150 if (this.training_) | |
151 return; | |
152 | |
153 this.training_ = true; | |
154 | |
155 if (chrome.hotwordPrivate.onHotwordTriggered && | |
156 !chrome.hotwordPrivate.onHotwordTriggered.hasListener( | |
157 this.hotwordTriggerListener_)) { | |
158 chrome.hotwordPrivate.onHotwordTriggered.addListener( | |
159 this.hotwordTriggerListener_); | |
160 } | |
161 | |
162 this.waitForHotwordTrigger_(0); | |
163 if (chrome.hotwordPrivate.startTraining) | |
164 chrome.hotwordPrivate.startTraining(); | |
165 }; | |
166 | |
167 /** | |
168 * Stops the training process. | |
169 */ | |
170 Flow.prototype.stopTraining = function() { | |
171 if (!this.training_) | |
172 return; | |
173 | |
174 this.training_ = false; | |
175 if (chrome.hotwordPrivate.onHotwordTriggered) { | |
176 chrome.hotwordPrivate.onHotwordTriggered. | |
177 removeListener(this.hotwordTriggerListener_); | |
178 } | |
179 if (chrome.hotwordPrivate.stopTraining) | |
180 chrome.hotwordPrivate.stopTraining(); | |
181 }; | |
182 | |
183 /** | |
184 * Attempts to enable audio history for the signed-in account. | |
185 */ | |
186 Flow.prototype.enableAudioHistory = function() { | |
187 // Update UI | |
188 $('audio-history-agree').disabled = true; | |
189 $('audio-history-cancel').disabled = true; | |
190 | |
191 $('audio-history-error').hidden = true; | |
192 $('audio-history-wait').hidden = false; | |
193 | |
194 if (chrome.hotwordPrivate.setAudioHistoryEnabled) { | |
195 chrome.hotwordPrivate.setAudioHistoryEnabled( | |
196 true, this.onAudioHistoryRequestCompleted_.bind(this)); | |
197 } | |
198 }; | |
199 | |
200 // ---- private methods: | |
201 | |
202 /** | |
203 * Shows an error if the audio history setting was not enabled successfully. | |
204 * @private | |
205 */ | |
206 Flow.prototype.handleAudioHistoryError_ = function() { | |
207 $('audio-history-agree').disabled = false; | |
208 $('audio-history-cancel').disabled = false; | |
209 | |
210 $('audio-history-wait').hidden = true; | |
211 $('audio-history-error').hidden = false; | |
212 | |
213 // Set a timeout before focusing the Enable button so that screenreaders | |
214 // have time to announce the error first. | 458 // have time to announce the error first. |
215 this.setTimeout_(function() { | 459 this.setTimeout_(function() { |
216 $('audio-history-agree').focus(); | 460 $(this.trainingPagePrefix_ + '-retry').children[0].focus(); |
217 }.bind(this), 50); | 461 }.bind(this), 50); |
218 }; | 462 } |
219 | 463 }; |
220 /** | 464 |
221 * Callback for when an audio history request completes. | 465 /** |
222 * @param {chrome.hotwordPrivate.AudioHistoryState} state The audio history | 466 * Handles a hotword trigger event and updates the training UI. |
223 * request state. | 467 * @private |
224 * @private | 468 */ |
225 */ | 469 Flow.prototype.handleHotwordTrigger_ = function() { |
226 Flow.prototype.onAudioHistoryRequestCompleted_ = function(state) { | 470 var trainingSteps = this.getCurrentTrainingStep_('listening'); |
227 if (!state.success || !state.enabled) { | 471 |
228 this.handleAudioHistoryError_(); | 472 if (!trainingSteps.current) |
229 return; | 473 return; |
230 } | 474 |
231 | 475 var index = trainingSteps.index; |
232 this.advanceStep(); | 476 this.hotwordTriggerReceived_[index] = true; |
233 }; | 477 |
234 | 478 trainingSteps.current.querySelector('.text').textContent = |
235 /** | 479 loadTimeData.getString('trainingRecorded'); |
236 * Shows an error if the speaker model has not been finalized. | 480 trainingSteps.current.classList.remove('listening'); |
237 * @private | 481 trainingSteps.current.classList.add('recorded'); |
238 */ | 482 |
239 Flow.prototype.handleSpeakerModelFinalizedError_ = function() { | 483 if (trainingSteps.steps[index + 1]) { |
240 if (!this.training_) | 484 trainingSteps.steps[index + 1].classList.remove('not-started'); |
241 return; | 485 trainingSteps.steps[index + 1].classList.add('listening'); |
242 | 486 this.waitForHotwordTrigger_(index + 1); |
243 if (this.speakerModelFinalized_) | 487 return; |
244 return; | 488 } |
245 | 489 |
246 this.updateTrainingState_(TrainingState.ERROR); | 490 // Only the last step makes it here. |
247 this.stopTraining(); | 491 var buttonElem = $(this.trainingPagePrefix_ + '-processing').hidden = false; |
248 }; | 492 this.finalizeSpeakerModel_(); |
249 | 493 }; |
250 /** | 494 |
251 * Handles the speaker model finalized event. | 495 /** |
252 * @private | 496 * Handles a chrome.idle.onStateChanged event and times out the training if |
253 */ | 497 * the state is "locked". |
254 Flow.prototype.onSpeakerModelFinalized_ = function() { | 498 * @param {!string} state State, one of "active", "idle", or "locked". |
255 this.speakerModelFinalized_ = true; | 499 * @private |
256 if (chrome.hotwordPrivate.onSpeakerModelSaved) { | 500 */ |
257 chrome.hotwordPrivate.onSpeakerModelSaved.removeListener( | 501 Flow.prototype.handleIdleStateChanged_ = function(state) { |
258 this.speakerModelFinalizedListener_); | 502 if (state == 'locked') |
259 } | |
260 this.stopTraining(); | |
261 this.setTimeout_(this.finishFlow_.bind(this), 2000); | |
262 }; | |
263 | |
264 /** | |
265 * Completes the training process. | |
266 * @private | |
267 */ | |
268 Flow.prototype.finishFlow_ = function() { | |
269 if (chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled) { | |
270 chrome.hotwordPrivate.setHotwordAlwaysOnSearchEnabled(true, | |
271 this.advanceStep.bind(this)); | |
272 } | |
273 }; | |
274 | |
275 /** | |
276 * Handles a user clicking on the retry button. | |
277 */ | |
278 Flow.prototype.handleRetry = function() { | |
279 if (!(this.trainingState_ == TrainingState.TIMEOUT || | |
280 this.trainingState_ == TrainingState.ERROR)) | |
281 return; | |
282 | |
283 this.startTraining(); | |
284 this.updateTrainingState_(TrainingState.RESET); | |
285 }; | |
286 | |
287 // ---- private methods: | |
288 | |
289 /** | |
290 * Completes the training process. | |
291 * @private | |
292 */ | |
293 Flow.prototype.finalizeSpeakerModel_ = function() { | |
294 if (!this.training_) | |
295 return; | |
296 | |
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_); | |
303 } | |
304 | |
305 this.speakerModelFinalized_ = false; | |
306 this.setTimeout_(this.handleSpeakerModelFinalizedError_.bind(this), 30000); | |
307 if (chrome.hotwordPrivate.finalizeSpeakerModel) | |
308 chrome.hotwordPrivate.finalizeSpeakerModel(); | |
309 }; | |
310 | |
311 /** | |
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 | |
318 */ | |
319 Flow.prototype.getCurrentTrainingStep_ = function(curStepClassName) { | |
320 var steps = | |
321 $(this.trainingPagePrefix_ + '-training').querySelectorAll('.train'); | |
322 var curStep = | |
323 $(this.trainingPagePrefix_ + '-training').querySelector('.listening'); | |
324 | |
325 return {current: curStep, | |
326 index: Array.prototype.indexOf.call(steps, curStep), | |
327 steps: steps}; | |
328 }; | |
329 | |
330 /** | |
331 * Updates the training state. | |
332 * @param {TrainingState} state The training state. | |
333 * @private | |
334 */ | |
335 Flow.prototype.updateTrainingState_ = function(state) { | |
336 this.trainingState_ = state; | |
337 this.updateErrorUI_(); | |
338 }; | |
339 | |
340 /** | |
341 * Waits two minutes and then checks for a training error. | |
342 * @param {number} index The index of the training step. | |
343 * @private | |
344 */ | |
345 Flow.prototype.waitForHotwordTrigger_ = function(index) { | |
346 if (!this.training_) | |
347 return; | |
348 | |
349 this.hotwordTriggerReceived_[index] = false; | |
350 this.setTimeout_(this.handleTrainingTimeout_.bind(this, index), 120000); | |
351 }; | |
352 | |
353 /** | |
354 * Checks for and handles a training error. | |
355 * @param {number} index The index of the training step. | |
356 * @private | |
357 */ | |
358 Flow.prototype.handleTrainingTimeout_ = function(index) { | |
359 if (this.hotwordTriggerReceived_[index]) | |
360 return; | |
361 | |
362 this.timeoutTraining_(); | 503 this.timeoutTraining_(); |
363 }; | 504 }; |
364 | 505 |
365 /** | 506 /** |
366 * Times out training and updates the UI to show a "retry" message, if | 507 * Handles a chrome.hotwordPrivate.onEnabledChanged event and times out |
367 * currently training. | 508 * training if the user is no longer the active user (user switches profiles). |
368 * @private | 509 * @private |
369 */ | 510 */ |
370 Flow.prototype.timeoutTraining_ = function() { | 511 Flow.prototype.handleEnabledChanged_ = function() { |
371 if (!this.training_) | 512 if (chrome.hotwordPrivate.getStatus) { |
372 return; | 513 chrome.hotwordPrivate.getStatus(function(status) { |
373 | 514 if (status.userIsActive) |
374 this.clearTimeout_(); | 515 return; |
375 this.updateTrainingState_(TrainingState.TIMEOUT); | 516 |
376 this.stopTraining(); | |
377 }; | |
378 | |
379 /** | |
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 | |
384 */ | |
385 Flow.prototype.setTimeout_ = function(func, delay) { | |
386 this.clearTimeout_(); | |
387 this.timeoutId_ = setTimeout(function() { | |
388 this.timeoutId_ = null; | |
389 func(); | |
390 }, delay); | |
391 }; | |
392 | |
393 /** | |
394 * Clears any currently active timeout. | |
395 * @private | |
396 */ | |
397 Flow.prototype.clearTimeout_ = function() { | |
398 if (this.timeoutId_ != null) { | |
399 clearTimeout(this.timeoutId_); | |
400 this.timeoutId_ = null; | |
401 } | |
402 }; | |
403 | |
404 /** | |
405 * Updates the training error UI. | |
406 * @private | |
407 */ | |
408 Flow.prototype.updateErrorUI_ = function() { | |
409 if (!this.training_) | |
410 return; | |
411 | |
412 var trainingSteps = this.getCurrentTrainingStep_('listening'); | |
413 var steps = trainingSteps.steps; | |
414 | |
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'); | |
433 } | |
434 steps[i].querySelector('.text').textContent = prompt; | |
435 } | |
436 | |
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'); | |
447 } | |
448 | |
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; | |
460 | |
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); | |
466 } | |
467 }; | |
468 | |
469 /** | |
470 * Handles a hotword trigger event and updates the training UI. | |
471 * @private | |
472 */ | |
473 Flow.prototype.handleHotwordTrigger_ = function() { | |
474 var trainingSteps = this.getCurrentTrainingStep_('listening'); | |
475 | |
476 if (!trainingSteps.current) | |
477 return; | |
478 | |
479 var index = trainingSteps.index; | |
480 this.hotwordTriggerReceived_[index] = true; | |
481 | |
482 trainingSteps.current.querySelector('.text').textContent = | |
483 loadTimeData.getString('trainingRecorded'); | |
484 trainingSteps.current.classList.remove('listening'); | |
485 trainingSteps.current.classList.add('recorded'); | |
486 | |
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; | |
492 } | |
493 | |
494 // Only the last step makes it here. | |
495 var buttonElem = $(this.trainingPagePrefix_ + '-processing').hidden = false; | |
496 this.finalizeSpeakerModel_(); | |
497 }; | |
498 | |
499 /** | |
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 | |
504 */ | |
505 Flow.prototype.handleIdleStateChanged_ = function(state) { | |
506 if (state == 'locked') | |
507 this.timeoutTraining_(); | 517 this.timeoutTraining_(); |
508 }; | 518 }.bind(this)); |
509 | 519 } |
510 /** | 520 }; |
511 * Handles a chrome.hotwordPrivate.onEnabledChanged event and times out | 521 |
512 * training if the user is no longer the active user (user switches profiles). | 522 /** |
513 * @private | 523 * Gets and starts the appropriate flow for the launch mode. |
514 */ | 524 * @param {chrome.hotwordPrivate.LaunchState} state Launch state of the |
515 Flow.prototype.handleEnabledChanged_ = function() { | 525 * Hotword Audio Verification App. |
516 if (chrome.hotwordPrivate.getStatus) { | 526 * @private |
517 chrome.hotwordPrivate.getStatus(function(status) { | 527 */ |
518 if (status.userIsActive) | 528 Flow.prototype.startFlowForMode_ = function(state) { |
519 return; | 529 this.launchMode_ = state.launchMode; |
520 | 530 assert( |
521 this.timeoutTraining_(); | 531 state.launchMode >= 0 && state.launchMode < FLOWS.length, |
522 }.bind(this)); | 532 'Invalid Launch Mode.'); |
523 } | 533 this.currentFlow_ = FLOWS[state.launchMode]; |
524 }; | 534 if (state.launchMode == LaunchMode.HOTWORD_ONLY) { |
525 | 535 $('intro-description-audio-history-enabled').hidden = false; |
526 /** | 536 } else if (state.launchMode == LaunchMode.HOTWORD_AND_AUDIO_HISTORY) { |
527 * Gets and starts the appropriate flow for the launch mode. | 537 $('intro-description').hidden = false; |
528 * @param {chrome.hotwordPrivate.LaunchState} state Launch state of the | 538 } |
529 * Hotword Audio Verification App. | 539 |
530 * @private | 540 this.advanceStep(); |
531 */ | 541 }; |
532 Flow.prototype.startFlowForMode_ = function(state) { | 542 |
533 this.launchMode_ = state.launchMode; | 543 /** |
534 assert(state.launchMode >= 0 && state.launchMode < FLOWS.length, | 544 * Displays the current step. If the current step is not the first step, |
535 'Invalid Launch Mode.'); | 545 * also hides the previous step. Focuses the current step's first button. |
536 this.currentFlow_ = FLOWS[state.launchMode]; | 546 * @private |
537 if (state.launchMode == LaunchMode.HOTWORD_ONLY) { | 547 */ |
538 $('intro-description-audio-history-enabled').hidden = false; | 548 Flow.prototype.showStep_ = function() { |
539 } else if (state.launchMode == LaunchMode.HOTWORD_AND_AUDIO_HISTORY) { | 549 var currentStepId = this.currentFlow_[this.currentStepIndex_]; |
540 $('intro-description').hidden = false; | 550 var currentStep = document.getElementById(currentStepId); |
541 } | 551 currentStep.hidden = false; |
542 | 552 |
543 this.advanceStep(); | 553 cr.ui.setInitialFocus(currentStep); |
544 }; | 554 |
545 | 555 var previousStep = null; |
546 /** | 556 if (this.currentStepIndex_ > 0) |
547 * Displays the current step. If the current step is not the first step, | 557 previousStep = this.currentFlow_[this.currentStepIndex_ - 1]; |
548 * also hides the previous step. Focuses the current step's first button. | 558 |
549 * @private | 559 if (previousStep) |
550 */ | 560 document.getElementById(previousStep).hidden = true; |
551 Flow.prototype.showStep_ = function() { | 561 |
552 var currentStepId = this.currentFlow_[this.currentStepIndex_]; | 562 chrome.app.window.current().show(); |
553 var currentStep = document.getElementById(currentStepId); | 563 }; |
554 currentStep.hidden = false; | 564 |
555 | 565 window.Flow = Flow; |
556 cr.ui.setInitialFocus(currentStep); | |
557 | |
558 var previousStep = null; | |
559 if (this.currentStepIndex_ > 0) | |
560 previousStep = this.currentFlow_[this.currentStepIndex_ - 1]; | |
561 | |
562 if (previousStep) | |
563 document.getElementById(previousStep).hidden = true; | |
564 | |
565 chrome.app.window.current().show(); | |
566 }; | |
567 | |
568 window.Flow = Flow; | |
569 })(); | 566 })(); |
OLD | NEW |