764d2c099dd202f5b81f9d123d2a4195da6d1373
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / hotword / state_manager.js
1 // Copyright (c) 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.
4
5 cr.define('hotword', function() {
6   'use strict';
7
8   /**
9    * Class to manage hotwording state. Starts/stops the hotword detector based
10    * on user settings, session requests, and any other factors that play into
11    * whether or not hotwording should be running.
12    * @constructor
13    * @struct
14    */
15   function StateManager() {
16     /**
17      * Current state.
18      * @private {hotword.StateManager.State_}
19      */
20     this.state_ = State_.STOPPED;
21
22     /**
23      * Current hotwording status.
24      * @private {?chrome.hotwordPrivate.StatusDetails}
25      */
26     this.hotwordStatus_ = null;
27
28     /**
29      * NaCl plugin manager.
30      * @private {?hotword.NaClManager}
31      */
32     this.pluginManager_ = null;
33
34     /**
35      * Source of the current hotword session.
36      * @private {?hotword.constants.SessionSource}
37      */
38     this.sessionSource_ = null;
39
40     /**
41      * Callback to run when the hotword detector has successfully started.
42      * @private {!function()}
43      */
44     this.sessionStartedCb_ = null;
45
46     /**
47      * Hotword trigger audio notification... a.k.a The Chime (tm).
48      * @private {!Audio}
49      */
50     this.chime_ = document.createElement('audio');
51
52     // Get the initial status.
53     chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
54
55     // Setup the chime and insert into the page.
56     this.chime_.src = chrome.extension.getURL(
57         hotword.constants.SHARED_MODULE_ROOT + '/audio/chime.wav');
58     document.body.appendChild(this.chime_);
59   }
60
61   /**
62    * @enum {number}
63    * @private
64    */
65   StateManager.State_ = {
66     STOPPED: 0,
67     STARTING: 1,
68     RUNNING: 2,
69     ERROR: 3,
70   };
71   var State_ = StateManager.State_;
72
73   StateManager.prototype = {
74     /**
75      * Request status details update. Intended to be called from the
76      * hotwordPrivate.onEnabledChanged() event.
77      */
78     updateStatus: function() {
79       chrome.hotwordPrivate.getStatus(this.handleStatus_.bind(this));
80     },
81
82     /**
83      * Callback for hotwordPrivate.getStatus() function.
84      * @param {chrome.hotwordPrivate.StatusDetails} status Current hotword
85      *     status.
86      * @private
87      */
88     handleStatus_: function(status) {
89       this.hotwordStatus_ = status;
90       this.updateStateFromStatus_();
91     },
92
93     /**
94      * Updates state based on the current status.
95      * @private
96      */
97     updateStateFromStatus_: function() {
98       if (!this.hotwordStatus_)
99         return;
100
101       if (this.hotwordStatus_.enabled) {
102         // Start the detector if there's a session, and shut it down if there
103         // isn't.
104         // TODO(amistry): Support stacking sessions. This can happen when the
105         // user opens google.com or the NTP, then opens the launcher. Opening
106         // google.com will create one session, and opening the launcher will
107         // create the second. Closing the launcher should re-activate the
108         // google.com session.
109         // NOTE(amistry): With always-on, we want a different behaviour with
110         // sessions since the detector should always be running. The exception
111         // being when the user triggers by saying 'Ok Google'. In that case, the
112         // detector stops, so starting/stopping the launcher session should
113         // restart the detector.
114         if (this.sessionSource_)
115           this.startDetector_();
116         else
117           this.shutdownDetector_();
118       } else {
119         // Not enabled. Shut down if running.
120         this.shutdownDetector_();
121       }
122     },
123
124     /**
125      * Starts the hotword detector.
126      * @private
127      */
128     startDetector_: function() {
129       // Last attempt to start detector resulted in an error.
130       if (this.state_ == State_.ERROR) {
131         // TODO(amistry): Do some error rate tracking here and disable the
132         // extension if we error too often.
133       }
134
135       if (!this.pluginManager_) {
136         this.state_ = State_.STARTING;
137         this.pluginManager_ = new hotword.NaClManager();
138         this.pluginManager_.addEventListener(hotword.constants.Event.READY,
139                                              this.onReady_.bind(this));
140         this.pluginManager_.addEventListener(hotword.constants.Event.ERROR,
141                                              this.onError_.bind(this));
142         this.pluginManager_.addEventListener(hotword.constants.Event.TRIGGER,
143                                              this.onTrigger_.bind(this));
144         chrome.runtime.getPlatformInfo(function(platform) {
145           var naclArch = platform.nacl_arch;
146
147           // googDucking set to false so that audio output level from other tabs
148           // is not affected when hotword is enabled. https://crbug.com/357773
149           // content/common/media/media_stream_options.cc
150           var constraints = /** @type {googMediaStreamConstraints} */
151               ({audio: {optional: [{googDucking: false}]}});
152           navigator.webkitGetUserMedia(
153               /** @type {MediaStreamConstraints} */ (constraints),
154               function(stream) {
155                 if (!this.pluginManager_.initialize(naclArch, stream)) {
156                   this.state_ = State_.ERROR;
157                   this.shutdownPluginManager_();
158                 }
159               }.bind(this),
160               function(error) {
161                 this.state_ = State_.ERROR;
162                 this.pluginManager_ = null;
163               }.bind(this));
164         }.bind(this));
165       } else if (this.state_ != State_.STARTING) {
166         // Don't try to start a starting detector.
167         this.startRecognizer_();
168       }
169     },
170
171     /**
172      * Start the recognizer plugin. Assumes the plugin has been loaded and is
173      * ready to start.
174      * @private
175      */
176     startRecognizer_: function() {
177       assert(this.pluginManager_);
178       if (this.state_ != State_.RUNNING) {
179         this.state_ = State_.RUNNING;
180         this.pluginManager_.startRecognizer();
181       }
182       if (this.sessionStartedCb_) {
183         this.sessionStartedCb_();
184         this.sessionStartedCb_ = null;
185       }
186     },
187
188     /**
189      * Shuts down and removes the plugin manager, if it exists.
190      * @private
191      */
192     shutdownPluginManager_: function() {
193       if (this.pluginManager_) {
194         this.pluginManager_.shutdown();
195         this.pluginManager_ = null;
196       }
197     },
198
199     /**
200      * Shuts down the hotword detector.
201      * @private
202      */
203     shutdownDetector_: function() {
204       this.state_ = State_.STOPPED;
205       this.shutdownPluginManager_();
206     },
207
208     /**
209      * Handle the hotword plugin being ready to start.
210      * @private
211      */
212     onReady_: function() {
213       if (this.state_ != State_.STARTING) {
214         // At this point, we should not be in the RUNNING state. Doing so would
215         // imply the hotword detector was started without being ready.
216         assert(this.state_ != State_.RUNNING);
217         this.shutdownPluginManager_();
218         return;
219       }
220       this.startRecognizer_();
221     },
222
223     /**
224      * Handle an error from the hotword plugin.
225      * @private
226      */
227     onError_: function() {
228       this.state_ = State_.ERROR;
229       this.shutdownPluginManager_();
230     },
231
232     /**
233      * Handle hotword triggering.
234      * @private
235      */
236     onTrigger_: function() {
237       assert(this.pluginManager_);
238       // Detector implicitly stops when the hotword is detected.
239       this.state_ = State_.STOPPED;
240
241       // Play the chime.
242       this.chime_.play();
243
244       chrome.hotwordPrivate.notifyHotwordRecognition('search', function() {});
245
246       // Implicitly clear the session. A session needs to be started in order to
247       // restart the detector.
248       this.sessionSource_ = null;
249       this.sessionStartedCb_ = null;
250     },
251
252     /**
253      * Start a hotwording session.
254      * @param {!hotword.constants.SessionSource} source Source of the hotword
255      *     session request.
256      * @param {!function()} startedCb Callback invoked when the session has
257      *     been started successfully.
258      */
259     startSession: function(source, startedCb) {
260       this.sessionSource_ = source;
261       this.sessionStartedCb_ = startedCb;
262       this.updateStateFromStatus_();
263     },
264
265     /**
266      * Stops a hotwording session.
267      * @param {!hotword.constants.SessionSource} source Source of the hotword
268      *     session request.
269      */
270     stopSession: function(source) {
271       this.sessionSource_ = null;
272       this.sessionStartedCb_ = null;
273       this.updateStateFromStatus_();
274     }
275   };
276
277   return {
278     StateManager: StateManager
279   };
280 });