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() {
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
13 * @param {!hotword.StateManager} stateManager
16 function PageAudioManager(stateManager) {
18 * Manager of global hotwording state.
19 * @private {!hotword.StateManager}
21 this.stateManager_ = stateManager;
24 * Mapping between tab ID and port that is connected from the injected
26 * @private {!Object<number, Port>}
31 * Chrome event listeners. Saved so that they can be de-registered when
32 * hotwording is disabled.
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_();
49 var CommandToPage = hotword.constants.CommandToPage;
50 var CommandFromPage = hotword.constants.CommandFromPage;
52 PageAudioManager.prototype = {
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.
60 checkUrlPathIsEligible_: function(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) {
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.
80 isEligibleUrl_: function(url) {
84 var baseGoogleUrls = [
85 'https://www.google.',
86 'https://encrypted.google.'
88 // TODO(amistry): Get this list from a file in the shared module instead.
108 // Check for the new tab page first.
109 if (this.checkUrlPathIsEligible_(url, 'chrome://newtab'))
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))
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
131 findCurrentTab_: function(callback) {
132 chrome.windows.getAll(
135 for (var i = 0; i < windows.length; ++i) {
136 if (!windows[i].focused)
139 for (var j = 0; j < windows[i].tabs.length; ++j) {
140 var tab = windows[i].tabs[j];
142 callback.call(this, tab);
147 callback.call(this, null);
152 * This function is called when a tab is activated (comes into focus).
153 * @param {Tab} tab Current active tab.
156 activateTab_: function(tab) {
158 this.stopHotwording_();
161 if (tab.id in this.portMap_) {
162 this.startHotwordingIfEligible_();
165 this.stopHotwording_();
166 this.prepareTab_(tab);
170 * Prepare a new or updated tab by injecting the content script.
171 * @param {!Tab} tab Newly updated or created tab.
174 prepareTab_: function(tab) {
175 if (!this.isEligibleUrl_(tab.url))
178 chrome.tabs.executeScript(
180 {'file': 'audio_client.js'},
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.
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.
197 * Updates hotwording state based on the state of current tabs/windows.
200 updateTabState_: function() {
201 this.findCurrentTab_(this.activateTab_);
205 * Handles a newly created tab.
206 * @param {!Tab} tab Newly created tab.
209 handleCreatedTab_: function(tab) {
210 this.prepareTab_(tab);
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.
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')
226 this.prepareTab_(tab);
230 * Handles a tab that was just became active.
231 * @param {{tabId: number}} info Information about the activated tab.
234 handleActivatedTab_: function(info) {
235 this.updateTabState_();
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.
245 handleChangedWindow_: function(windowId) {
246 this.updateTabState_();
250 * Handles a content script attempting to connect.
251 * @param {!Port} port Communications port from the client.
254 handleConnect_: function(port) {
255 if (port.name != hotword.constants.CLIENT_PORT_NAME)
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);
265 port.onMessage.addListener(function(msg) {
266 this.handleMessage_(msg, port.sender, port.postMessage);
271 * Handles a client content script disconnect.
272 * @param {Port} port Disconnected port.
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];
282 this.stopHotwordingIfIneligibleTab_();
286 * Disconnect all connected clients.
289 disconnectAllClients_: function() {
290 for (var id in this.portMap_) {
291 var port = this.portMap_[id];
293 delete this.portMap_[id];
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.
303 sendClient_: function(command, tabId) {
304 if (tabId in this.portMap_) {
306 message[hotword.constants.COMMAND_FIELD_NAME] = command;
307 this.portMap_[tabId].postMessage(message);
312 * Sends a command to all connected clients.
313 * @param {hotword.constants.CommandToPage} command Command to send.
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);
325 * Handles a hotword trigger. Sends a trigger message to the currently
329 hotwordTriggered_: function() {
330 this.findCurrentTab_(function(tab) {
332 this.sendClient_(CommandToPage.HOTWORD_VOICE_TRIGGER, tab.id);
340 startHotwording_: function() {
341 this.stateManager_.startSession(
342 hotword.constants.SessionSource.NTP,
344 this.sendAllClients_(CommandToPage.HOTWORD_STARTED);
346 this.hotwordTriggered_.bind(this));
350 * Starts hotwording if the currently active tab is eligible for hotwording
354 startHotwordingIfEligible_: function() {
355 this.findCurrentTab_(function(tab) {
357 this.stopHotwording_();
360 if (this.isEligibleUrl_(tab.url))
361 this.startHotwording_();
369 stopHotwording_: function() {
370 this.stateManager_.stopSession(hotword.constants.SessionSource.NTP);
371 this.sendAllClients_(CommandToPage.HOTWORD_ENDED);
375 * Stops hotwording if the currently active tab is not eligible for
376 * hotwording (i.e. google.com).
379 stopHotwordingIfIneligibleTab_: function() {
380 this.findCurrentTab_(function(tab) {
382 this.stopHotwording_();
385 if (!this.isEligibleUrl_(tab.url))
386 this.stopHotwording_();
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.
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_();
404 case CommandFromPage.SPEECH_END:
405 case CommandFromPage.SPEECH_RESET:
406 this.startHotwording_();
412 * Set up event listeners.
415 setupListeners_: function() {
416 if (chrome.runtime.onConnect.hasListener(this.connectListener_))
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_);
428 * Remove event listeners.
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_);
441 * Update event listeners based on the current hotwording state.
444 updateListeners_: function() {
445 if (this.stateManager_.isSometimesOnEnabled()) {
446 this.setupListeners_();
448 this.removeListeners_();
449 this.stopHotwording_();
450 this.disconnectAllClients_();
456 PageAudioManager: PageAudioManager