ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / chrome / browser / resources / hotword / page_audio_manager.js
blob43b763b8fd226e9ea03a97ff0d53d452b6442051
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 cr.define('hotword', function() {
6   'use strict';
8   /**
9    * Class used to manage the interaction between hotwording and the
10    * NTP/google.com. Injects a content script to interact with NTP/google.com
11    * and updates the global hotwording state based on interaction with those
12    * pages.
13    * @param {!hotword.StateManager} stateManager
14    * @constructor
15    */
16   function PageAudioManager(stateManager) {
17     /**
18      * Manager of global hotwording state.
19      * @private {!hotword.StateManager}
20      */
21     this.stateManager_ = stateManager;
23     /**
24      * Mapping between tab ID and port that is connected from the injected
25      * content script.
26      * @private {!Object<number, Port>}
27      */
28     this.portMap_ = {};
30     /**
31      * Chrome event listeners. Saved so that they can be de-registered when
32      * hotwording is disabled.
33      */
34     this.connectListener_ = this.handleConnect_.bind(this);
35     this.tabCreatedListener_ = this.handleCreatedTab_.bind(this);
36     this.tabUpdatedListener_ = this.handleUpdatedTab_.bind(this);
37     this.tabActivatedListener_ = this.handleActivatedTab_.bind(this);
38     this.windowFocusChangedListener_ = this.handleChangedWindow_.bind(this);
40     // Need to setup listeners on startup, otherwise events that caused the
41     // event page to start up, will be lost.
42     this.setupListeners_();
44     this.stateManager_.onStatusChanged.addListener(function() {
45       this.updateListeners_();
46     }.bind(this));
47   };
49   var CommandToPage = hotword.constants.CommandToPage;
50   var CommandFromPage = hotword.constants.CommandFromPage;
52   PageAudioManager.prototype = {
53     /**
54      * Helper function to test if a URL path is eligible for hotwording.
55      * @param {!string} url URL to check.
56      * @param {!string} base Base URL to compare against..
57      * @return {boolean} True if url is an eligible hotword URL.
58      * @private
59      */
60     checkUrlPathIsEligible_: function(url, base) {
61       if (url == base ||
62           url == base + '/' ||
63           url.indexOf(base + '/_/chrome/newtab?') == 0 ||  // Appcache NTP.
64           url.indexOf(base + '/?') == 0 ||
65           url.indexOf(base + '/#') == 0 ||
66           url.indexOf(base + '/webhp') == 0 ||
67           url.indexOf(base + '/search') == 0) {
68         return true;
69       }
70       return false;
71     },
73     /**
74      * Determines if a URL is eligible for hotwording. For now, the valid pages
75      * are the Google HP and SERP (this will include the NTP).
76      * @param {!string} url URL to check.
77      * @return {boolean} True if url is an eligible hotword URL.
78      * @private
79      */
80     isEligibleUrl_: function(url) {
81       if (!url)
82         return false;
84       var baseGoogleUrls = [
85         'https://www.google.',
86         'https://encrypted.google.'
87       ];
88       // TODO(amistry): Get this list from a file in the shared module instead.
89       var tlds = [
90         'at',
91         'ca',
92         'com',
93         'com.au',
94         'com.mx',
95         'com.br',
96         'co.jp',
97         'co.kr',
98         'co.nz',
99         'co.uk',
100         'co.za',
101         'de',
102         'es',
103         'fr',
104         'it',
105         'ru'
106       ];
108       // Check for the new tab page first.
109       if (this.checkUrlPathIsEligible_(url, 'chrome://newtab'))
110         return true;
112       // Check URLs with each type of local-based TLD.
113       for (var i = 0; i < baseGoogleUrls.length; i++) {
114         for (var j = 0; j < tlds.length; j++) {
115           var base = baseGoogleUrls[i] + tlds[j];
116           if (this.checkUrlPathIsEligible_(url, base))
117             return true;
118         }
119       }
120       return false;
121     },
123     /**
124      * Locates the current active tab in the current focused window and
125      * performs a callback with the tab as the parameter.
126      * @param {function(?Tab)} callback Function to call with the
127      *     active tab or null if not found. The function's |this| will be set to
128      *     this object.
129      * @private
130      */
131     findCurrentTab_: function(callback) {
132       chrome.windows.getAll(
133           {'populate': true},
134           function(windows) {
135             for (var i = 0; i < windows.length; ++i) {
136               if (!windows[i].focused)
137                 continue;
139               for (var j = 0; j < windows[i].tabs.length; ++j) {
140                 var tab = windows[i].tabs[j];
141                 if (tab.active) {
142                   callback.call(this, tab);
143                   return;
144                 }
145               }
146             }
147             callback.call(this, null);
148           }.bind(this));
149     },
151     /**
152      * This function is called when a tab is activated (comes into focus).
153      * @param {Tab} tab Current active tab.
154      * @private
155      */
156     activateTab_: function(tab) {
157       if (!tab) {
158         this.stopHotwording_();
159         return;
160       }
161       if (tab.id in this.portMap_) {
162         this.startHotwordingIfEligible_();
163         return;
164       }
165       this.stopHotwording_();
166       this.prepareTab_(tab);
167     },
169     /**
170      * Prepare a new or updated tab by injecting the content script.
171      * @param {!Tab} tab Newly updated or created tab.
172      * @private
173      */
174     prepareTab_: function(tab) {
175       if (!this.isEligibleUrl_(tab.url))
176         return;
178       chrome.tabs.executeScript(
179           tab.id,
180           {'file': 'audio_client.js'},
181           function(results) {
182             if (chrome.runtime.lastError) {
183               // Ignore this error. For new tab pages, even though the URL is
184               // reported to be chrome://newtab/, the actual URL is a
185               // country-specific google domain. Since we don't have permission
186               // to inject on every page, an error will happen when the user is
187               // in an unsupported country.
188               //
189               // The property still needs to be accessed so that the error
190               // condition is cleared. If it isn't, exectureScript will log an
191               // error the next time it is called.
192             }
193           });
194     },
196     /**
197      * Updates hotwording state based on the state of current tabs/windows.
198      * @private
199      */
200     updateTabState_: function() {
201       this.findCurrentTab_(this.activateTab_);
202     },
204     /**
205      * Handles a newly created tab.
206      * @param {!Tab} tab Newly created tab.
207      * @private
208      */
209     handleCreatedTab_: function(tab) {
210       this.prepareTab_(tab);
211     },
213     /**
214      * Handles an updated tab.
215      * @param {number} tabId Id of the updated tab.
216      * @param {{status: string}} info Change info of the tab.
217      * @param {!Tab} tab Updated tab.
218      * @private
219      */
220     handleUpdatedTab_: function(tabId, info, tab) {
221       // Chrome fires multiple update events: undefined, loading and completed.
222       // We perform content injection on loading state.
223       if (info['status'] != 'loading')
224         return;
226       this.prepareTab_(tab);
227     },
229     /**
230      * Handles a tab that was just became active.
231      * @param {{tabId: number}} info Information about the activated tab.
232      * @private
233      */
234     handleActivatedTab_: function(info) {
235       this.updateTabState_();
236     },
239     /**
240      * Handles a change in Chrome windows.
241      * Note: this does not always trigger in Linux.
242      * @param {number} windowId Id of newly focused window.
243      * @private
244      */
245     handleChangedWindow_: function(windowId) {
246       this.updateTabState_();
247     },
249     /**
250      * Handles a content script attempting to connect.
251      * @param {!Port} port Communications port from the client.
252      * @private
253      */
254     handleConnect_: function(port) {
255       if (port.name != hotword.constants.CLIENT_PORT_NAME)
256         return;
258       var tab = /** @type {!Tab} */(port.sender.tab);
259       // An existing port from the same tab might already exist. But that port
260       // may be from the previous page, so just overwrite the port.
261       this.portMap_[tab.id] = port;
262       port.onDisconnect.addListener(function() {
263         this.handleClientDisconnect_(port);
264       }.bind(this));
265       port.onMessage.addListener(function(msg) {
266         this.handleMessage_(msg, port.sender, port.postMessage);
267       }.bind(this));
268     },
270     /**
271      * Handles a client content script disconnect.
272      * @param {Port} port Disconnected port.
273      * @private
274      */
275     handleClientDisconnect_: function(port) {
276       var tabId = port.sender.tab.id;
277       if (tabId in this.portMap_ && this.portMap_[tabId] == port) {
278         // Due to a race between port disconnection and tabs.onUpdated messages,
279         // the port could have changed.
280         delete this.portMap_[port.sender.tab.id];
281       }
282       this.stopHotwordingIfIneligibleTab_();
283     },
285     /**
286      * Disconnect all connected clients.
287      * @private
288      */
289     disconnectAllClients_: function() {
290       for (var id in this.portMap_) {
291         var port = this.portMap_[id];
292         port.disconnect();
293         delete this.portMap_[id];
294       }
295     },
297     /**
298      * Sends a command to the client content script on an eligible tab.
299      * @param {hotword.constants.CommandToPage} command Command to send.
300      * @param {number} tabId Id of the target tab.
301      * @private
302      */
303     sendClient_: function(command, tabId) {
304       if (tabId in this.portMap_) {
305         var message = {};
306         message[hotword.constants.COMMAND_FIELD_NAME] = command;
307         this.portMap_[tabId].postMessage(message);
308       }
309     },
311     /**
312      * Sends a command to all connected clients.
313      * @param {hotword.constants.CommandToPage} command Command to send.
314      * @private
315      */
316     sendAllClients_: function(command) {
317       for (var idStr in this.portMap_) {
318         var id = parseInt(idStr, 10);
319         assert(!isNaN(id), 'Tab ID is not a number: ' + idStr);
320         this.sendClient_(command, id);
321       }
322     },
324     /**
325      * Handles a hotword trigger. Sends a trigger message to the currently
326      * active tab.
327      * @private
328      */
329     hotwordTriggered_: function() {
330       this.findCurrentTab_(function(tab) {
331         if (tab)
332           this.sendClient_(CommandToPage.HOTWORD_VOICE_TRIGGER, tab.id);
333       });
334     },
336     /**
337      * Starts hotwording.
338      * @private
339      */
340     startHotwording_: function() {
341       this.stateManager_.startSession(
342           hotword.constants.SessionSource.NTP,
343           function() {
344             this.sendAllClients_(CommandToPage.HOTWORD_STARTED);
345           }.bind(this),
346           this.hotwordTriggered_.bind(this));
347     },
349     /**
350      * Starts hotwording if the currently active tab is eligible for hotwording
351      * (i.e. google.com).
352      * @private
353      */
354     startHotwordingIfEligible_: function() {
355       this.findCurrentTab_(function(tab) {
356         if (!tab) {
357           this.stopHotwording_();
358           return;
359         }
360         if (this.isEligibleUrl_(tab.url))
361           this.startHotwording_();
362       });
363     },
365     /**
366      * Stops hotwording.
367      * @private
368      */
369     stopHotwording_: function() {
370       this.stateManager_.stopSession(hotword.constants.SessionSource.NTP);
371       this.sendAllClients_(CommandToPage.HOTWORD_ENDED);
372     },
374     /**
375      * Stops hotwording if the currently active tab is not eligible for
376      * hotwording (i.e. google.com).
377      * @private
378      */
379     stopHotwordingIfIneligibleTab_: function() {
380       this.findCurrentTab_(function(tab) {
381         if (!tab) {
382           this.stopHotwording_();
383           return;
384         }
385         if (!this.isEligibleUrl_(tab.url))
386           this.stopHotwording_();
387       });
388     },
390     /**
391      * Handles a message from the content script injected into the page.
392      * @param {!Object} request Request from the content script.
393      * @param {!MessageSender} sender Message sender.
394      * @param {!function(Object)} sendResponse Function for sending a response.
395      * @private
396      */
397     handleMessage_: function(request, sender, sendResponse) {
398       switch (request[hotword.constants.COMMAND_FIELD_NAME]) {
399         // TODO(amistry): Handle other messages such as CLICKED_RESUME and
400         // CLICKED_RESTART, if necessary.
401         case CommandFromPage.SPEECH_START:
402           this.stopHotwording_();
403           break;
404         case CommandFromPage.SPEECH_END:
405         case CommandFromPage.SPEECH_RESET:
406           this.startHotwording_();
407           break;
408       }
409     },
411     /**
412      * Set up event listeners.
413      * @private
414      */
415     setupListeners_: function() {
416       if (chrome.runtime.onConnect.hasListener(this.connectListener_))
417         return;
419       chrome.runtime.onConnect.addListener(this.connectListener_);
420       chrome.tabs.onCreated.addListener(this.tabCreatedListener_);
421       chrome.tabs.onUpdated.addListener(this.tabUpdatedListener_);
422       chrome.tabs.onActivated.addListener(this.tabActivatedListener_);
423       chrome.windows.onFocusChanged.addListener(
424           this.windowFocusChangedListener_);
425     },
427     /**
428      * Remove event listeners.
429      * @private
430      */
431     removeListeners_: function() {
432       chrome.runtime.onConnect.removeListener(this.connectListener_);
433       chrome.tabs.onCreated.removeListener(this.tabCreatedListener_);
434       chrome.tabs.onUpdated.removeListener(this.tabUpdatedListener_);
435       chrome.tabs.onActivated.removeListener(this.tabActivatedListener_);
436       chrome.windows.onFocusChanged.removeListener(
437           this.windowFocusChangedListener_);
438     },
440     /**
441      * Update event listeners based on the current hotwording state.
442      * @private
443      */
444     updateListeners_: function() {
445       if (this.stateManager_.isSometimesOnEnabled()) {
446         this.setupListeners_();
447       } else {
448         this.removeListeners_();
449         this.stopHotwording_();
450         this.disconnectAllClients_();
451       }
452     }
453   };
455   return {
456     PageAudioManager: PageAudioManager
457   };