Chromium Code Reviews| Index: chrome/browser/resources/hotword/page_audio_manager.js |
| diff --git a/chrome/browser/resources/hotword/page_audio_manager.js b/chrome/browser/resources/hotword/page_audio_manager.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..e23e2d2cce298195a8cf2bcc2897fd139717ecf1 |
| --- /dev/null |
| +++ b/chrome/browser/resources/hotword/page_audio_manager.js |
| @@ -0,0 +1,441 @@ |
| +// Copyright 2014 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +cr.define('hotword', function() { |
| + 'use strict'; |
| + |
| + /** |
| + * Class used to manage the interaction between hotwording and the |
| + * NTP/google.com. Injects a content script to interact with NTP/google.com |
| + * and updates the global hotwording state based on interaction with those |
| + * pages. |
| + * @param {!hotword.StateManager} stateManager |
| + * @constructor |
| + * @struct |
| + */ |
| + function PageAudioManager(stateManager) { |
| + /** |
| + * Manager of global hotwording state. |
| + * @private {!hotword.StateManager} |
| + */ |
| + this.stateManager_ = stateManager; |
| + |
| + /** |
| + * Mapping between tab ID and port that is connected from the injected |
| + * content script. |
| + * @private {!Object.<number, chrome.runtime.Port>} |
| + */ |
| + this.portMap_ = {}; |
| + |
| + /** |
| + * Chrome event listeners. Saved so that they can be de-registered when |
| + * hotwording is disabled. |
| + */ |
| + this.connectListener_ = this.handleConnect_.bind(this); |
| + this.tabCreatedListener_ = this.handleCreatedTab_.bind(this); |
| + this.tabUpdatedListener_ = this.handleUpdatedTab_.bind(this); |
| + this.tabActivatedListener_ = this.handleActivatedTab_.bind(this); |
| + this.windowFocusChangedListener_ = this.handleChangedWindow_.bind(this); |
| + |
| + // Need to setup listeners on startup, otherwise events that caused the |
| + // event page to start up, will be lost. |
| + this.setupListeners_(); |
| + |
| + this.stateManager_.onStatusChanged.addListener(function() { |
| + this.updateListeners_(); |
| + }.bind(this)); |
| + }; |
| + |
| + var CommandToPage = hotword.constants.CommandToPage; |
| + var CommandFromPage = hotword.constants.CommandFromPage; |
| + |
| + PageAudioManager.prototype = { |
| + /** |
| + * Helper function to test if a URL path is elibible for hotwording. |
| + * @param {!string} url URL to check. |
| + * @param {!string} base Base URL to compare against.. |
| + * @return {boolean} True if url is an eligible hotword URL. |
| + * @private |
| + */ |
| + checkUrlPathIsEligible_: function(url, base) { |
| + if (url == base || |
| + url == base + '/' || |
| + url.indexOf(base + '/_/chrome/newtab?') == 0 || // Appcache NTP. |
| + url.indexOf(base + '/?') == 0 || |
| + url.indexOf(base + '/#') == 0 || |
| + url.indexOf(base + '/webhp') == 0 || |
| + url.indexOf(base + '/search') == 0) { |
| + return true; |
| + } |
| + return false; |
| + }, |
| + |
| + /** |
| + * Determines if a URL is eligible for hotwording. For now, the valid pages |
| + * are the Google HP and SERP (this will include the NTP). |
| + * @param {!string} url URL to check. |
| + * @return {boolean} True if url is an eligible hotword URL. |
| + * @private |
| + */ |
| + isEligibleUrl_: function(url) { |
| + if (!url) |
| + return false; |
| + |
| + var baseGoogleUrls = [ |
| + 'https://www.google.', |
| + 'https://encrypted.google.' |
| + ]; |
| + // TODO(amistry): Get this list from a file in the shared module instead. |
| + var tlds = [ |
| + 'com', |
| + 'co.uk', |
| + 'de', |
| + 'fr', |
| + 'ru' |
| + ]; |
| + |
| + // Check for the new tab page first. |
| + if (this.checkUrlPathIsEligible_(url, 'chrome://newtab')) |
| + return true; |
| + |
| + // Check URLs with each type of local-based TLD. |
| + for (var i = 0; i < baseGoogleUrls.length; i++) { |
| + for (var j = 0; j < tlds.length; j++) { |
| + var base = baseGoogleUrls[i] + tlds[j]; |
| + if (this.checkUrlPathIsEligible_(url, base)) |
| + return true; |
| + } |
| + } |
| + return false; |
| + }, |
| + |
| + /** |
| + * Locates the current active tab in the current focused window and |
| + * performs a callback with the tab as the parameter. |
| + * @param {function(?Tab)} callback Function to call with the |
| + * active tab or null if not found. The function's |this| will be set to |
| + * this object. |
| + * @private |
| + */ |
| + findCurrentTab_: function(callback) { |
| + chrome.windows.getAll( |
| + {'populate': true}, |
| + function(windows) { |
| + for (var i = 0; i < windows.length; ++i) { |
| + if (!windows[i].focused) |
| + continue; |
| + |
| + for (var j = 0; j < windows[i].tabs.length; ++j) { |
| + var tab = windows[i].tabs[j]; |
| + if (tab.active) { |
| + callback.call(this, tab); |
| + return; |
| + } |
| + } |
| + } |
| + callback(null); |
| + }.bind(this)); |
| + }, |
| + |
| + /** |
| + * This function is called when a tab is activated (comes into focus). |
| + * @param {Tab} tab Current active tab. |
| + * @private |
| + */ |
| + activateTab_: function(tab) { |
| + if (!tab) { |
| + this.stopHotwording_(); |
| + return; |
| + } |
| + if (tab.id in this.portMap_) { |
| + this.startHotwordingIfEligible_(); |
| + return; |
| + } |
| + this.stopHotwording_(); |
| + this.prepareTab_(tab); |
| + }, |
| + |
| + /** |
| + * Prepare a new or updated tab by injecting the content script. |
| + * @param {!Tab} tab Newly updated or created tab. |
| + * @private |
| + */ |
| + prepareTab_: function(tab) { |
| + if (!this.isEligibleUrl_(tab.url)) |
| + return; |
| + |
| + chrome.tabs.executeScript(tab.id, {'file': 'audio_client.js'}); |
| + }, |
| + |
| + /** |
| + * Updates hotwording state based on the state of current tabs/windows. |
| + * @private |
| + */ |
| + updateTabState_: function() { |
| + this.findCurrentTab_(this.activateTab_); |
| + }, |
| + |
| + /** |
| + * Handles a newly created tab. |
| + * @param {!Tab} tab Newly created tab. |
| + * @private |
| + */ |
| + handleCreatedTab_: function(tab) { |
| + this.prepareTab_(tab); |
| + }, |
| + |
| + /** |
| + * Handles an updated tab. |
| + * @param {number} tabId Id of the updated tab. |
| + * @param {{status: string}} info Change info of the tab. |
| + * @param {!Tab} tab Updated tab. |
| + * @private |
| + */ |
| + handleUpdatedTab_: function(tabId, info, tab) { |
| + // Chrome fires multiple update events: undefined, loading and completed. |
| + // We perform content injection on loading state. |
| + if (info['status'] != 'loading') |
| + return; |
| + |
| + this.prepareTab_(tab); |
| + }, |
| + |
| + /** |
| + * Handles a tab that was just became active. |
| + * @param {{tabId: number}} info Information about the activated tab. |
| + * @private |
| + */ |
| + handleActivatedTab_: function(info) { |
| + this.updateTabState_(); |
| + }, |
| + |
| + |
| + /** |
| + * Handles a change in Chrome windows. |
| + * Note: this does not always trigger in Linux. |
| + * @param {number} windowId Id of newly focused window. |
| + * @private |
| + */ |
| + handleChangedWindow_: function(windowId) { |
| + this.updateTabState_(); |
| + }, |
| + |
| + /** |
| + * Handles a content script attempting to connect. |
| + * @param {!Port} port Communications port from the client. |
| + * @private |
| + */ |
| + handleConnect_: function(port) { |
| + var tab = /** @type {!Tab} */(port.sender.tab); |
|
Dan Beam
2014/10/11 00:54:35
nit: lower |tab| right above where it's used
Anand Mistry (off Chromium)
2014/10/13 19:30:20
Done.
|
| + if (port.name === hotword.constants.CLIENT_PORT_NAME) { |
|
Dan Beam
2014/10/11 00:54:35
==
Dan Beam
2014/10/11 00:54:35
nit:
if (port.name != hotword.constants.CLIENT_PO
Anand Mistry (off Chromium)
2014/10/13 19:30:21
Done.
Anand Mistry (off Chromium)
2014/10/13 19:30:21
Done.
|
| + // An existing port from the same tab might already exist. But that port |
| + // may be from the previous page, so just overwrite the port. |
| + this.portMap_[tab.id] = port; |
| + port.onDisconnect.addListener(function() { |
| + this.handleClientDisconnect_(port); |
| + }.bind(this)); |
| + port.onMessage.addListener(function(msg) { |
| + this.handleMessage_(msg, port.sender, port.postMessage); |
| + }.bind(this)); |
| + } |
| + }, |
| + |
| + /** |
| + * Handles a client content script disconnect. |
| + * @param {Port} port Disconnected port. |
| + * @private |
| + */ |
| + handleClientDisconnect_: function(port) { |
| + var tabId = port.sender.tab.id; |
| + if (tabId in this.portMap_ && this.portMap_[tabId] == port) { |
| + // Due to a race between port disconnection and tabs.onUpdated messages, |
| + // the port could have changed. |
| + delete this.portMap_[port.sender.tab.id]; |
| + } |
| + this.stopHotwordingIfIneligibleTab_(); |
| + }, |
| + |
| + /** |
| + * Disconnect all connected clients. |
| + * @private |
| + */ |
| + disconnectAllClients_: function() { |
| + var tabIds = Object.keys(this.portMap_); |
| + for (var id in tabIds.portMap_) { |
|
Dan Beam
2014/10/11 00:54:35
shouldn't this be
for (var id in this.portMap_)
Anand Mistry (off Chromium)
2014/10/13 19:30:21
Oops.
|
| + var port = this.portMap_[id]; |
| + port.disconnect(); |
| + delete this.portMap_[id]; |
| + } |
| + }, |
| + |
| + /** |
| + * Sends a command to the client content script on an eligible tab. |
| + * @param {hotword.constants.CommandToPage} command Command to send. |
| + * @param {number} tabId Id of the target tab. |
| + * @private |
| + */ |
| + sendClient_: function(command, tabId) { |
| + if (tabId in this.portMap_) { |
| + var message = {}; |
| + message[hotword.constants.COMMAND_FIELD_NAME] = command; |
| + this.portMap_[tabId].postMessage(message); |
| + } |
| + }, |
| + |
| + /** |
| + * Sends a command to all connected clients. |
| + * @param {hotword.constants.CommandToPage} command Command to send. |
| + * @private |
| + */ |
| + sendAllClients_: function(command) { |
| + for (var idStr in this.portMap_) { |
| + var id = parseInt(idStr, 10); |
| + assert(!isNaN(id), 'Tab ID is not a number: ' + idStr); |
| + this.sendClient_(command, id); |
| + } |
| + }, |
| + |
| + /** |
| + * Handles a hotword trigger. Sends a trigger message to the currently |
| + * active tab. |
| + * @private |
| + */ |
| + hotwordTriggered_: function() { |
| + this.findCurrentTab_(function(tab) { |
| + if (tab) |
| + this.sendClient_(CommandToPage.HOTWORD_VOICE_TRIGGER, tab.id); |
| + }); |
| + }, |
| + |
| + /* |
| + * Starts hotwording. |
| + * @private |
| + */ |
| + startHotwording_: function() { |
| + this.stateManager_.startSession( |
| + hotword.constants.SessionSource.NTP, |
| + function() { |
| + this.sendAllClients_(CommandToPage.HOTWORD_STARTED); |
| + }.bind(this), |
| + this.hotwordTriggered_.bind(this)); |
| + }, |
| + |
| + /* |
| + * Starts hotwording if the currently active tab is eligible for hotwording |
| + * (i.e. google.com). |
| + * @private |
| + */ |
| + startHotwordingIfEligible_: function() { |
| + this.findCurrentTab_(function(tab) { |
| + if (!tab) { |
| + this.stopHotwording_(); |
| + return; |
| + } |
| + if (this.isEligibleUrl_(tab.url)) |
| + this.startHotwording_(); |
| + }); |
| + }, |
| + |
| + /* |
| + * Stops hotwording. |
| + * @private |
| + */ |
| + stopHotwording_: function() { |
| + this.stateManager_.stopSession(hotword.constants.SessionSource.NTP); |
| + this.sendAllClients_(CommandToPage.HOTWORD_ENDED); |
| + }, |
| + |
| + /* |
| + * Stops hotwording if the currently active tab is not eligible for |
| + * hotwording (i.e. google.com). |
| + * @private |
| + */ |
| + stopHotwordingIfIneligibleTab_: function() { |
| + this.findCurrentTab_(function(tab) { |
| + if (!tab) { |
| + this.stopHotwording_(); |
| + return; |
| + } |
| + if (!this.isEligibleUrl_(tab.url)) |
| + this.stopHotwording_(); |
| + }); |
| + }, |
| + |
| + /** |
| + * Handles a message from the content script injected into the page. |
| + * @param {!Object} request Request from the content script. |
| + * @param {!MessageSender} sender Message sender. |
| + * @param {!function(Object)} sendResponse Function for sending a response. |
| + * @private |
| + */ |
| + handleMessage_: function(request, sender, sendResponse) { |
| + if (request[hotword.constants.COMMAND_FIELD_NAME]) { |
| + var command = request[hotword.constants.COMMAND_FIELD_NAME]; |
|
Dan Beam
2014/10/11 00:54:35
nit: this should work fine
switch (request[hotw
Anand Mistry (off Chromium)
2014/10/13 19:30:21
Done.
|
| + switch (command) { |
| + // TODO(amistry): Handle other messages such as CLICKED_RESUME and |
| + // CLICKED_RESTART, if necessary. |
| + case CommandFromPage.SPEECH_START: |
| + this.stopHotwording_(); |
| + break; |
| + case CommandFromPage.SPEECH_END: |
| + case CommandFromPage.SPEECH_RESET: |
| + this.startHotwording_(); |
| + break; |
| + } |
| + } |
| + }, |
| + |
| + /** |
| + * Set up event listeners. |
| + * @private |
| + */ |
| + setupListeners_: function() { |
| + if (chrome.runtime.onConnect.hasListener(this.connectListener_)) |
| + return; |
| + |
| + chrome.runtime.onConnect.addListener(this.connectListener_); |
| + chrome.tabs.onCreated.addListener(this.tabCreatedListener_); |
| + chrome.tabs.onUpdated.addListener(this.tabUpdatedListener_); |
| + chrome.tabs.onActivated.addListener(this.tabActivatedListener_); |
| + chrome.windows.onFocusChanged.addListener( |
| + this.windowFocusChangedListener_); |
| + }, |
| + |
| + /** |
| + * Remove event listeners. |
| + * @private |
| + */ |
| + removeListeners_: function() { |
| + if (!chrome.runtime.onConnect.hasListener(this.connectListener_)) |
| + return; |
|
Dan Beam
2014/10/11 00:54:35
is this needed?
Anand Mistry (off Chromium)
2014/10/13 19:30:21
Unless I'm misreading the event implementation, ad
Dan Beam
2014/10/13 19:33:09
this is removeListener()
Anand Mistry (off Chromium)
2014/10/13 19:39:44
Oops. Sorry. Yeah, it is unnecessary.
|
| + |
| + chrome.runtime.onConnect.removeListener(this.connectListener_); |
| + chrome.tabs.onCreated.removeListener(this.tabCreatedListener_); |
| + chrome.tabs.onUpdated.removeListener(this.tabUpdatedListener_); |
| + chrome.tabs.onActivated.removeListener(this.tabActivatedListener_); |
| + chrome.windows.onFocusChanged.removeListener( |
| + this.windowFocusChangedListener_); |
| + }, |
| + |
| + /** |
| + * Update event listeners based on the current hotwording state. |
| + * @private |
| + */ |
| + updateListeners_: function() { |
| + var enabled = this.stateManager_.isEnabled() && |
| + !this.stateManager_.isAlwaysOnEnabled(); |
|
Dan Beam
2014/10/11 00:54:35
just isAlwaysOnEnabled() (which checks isEnabled()
Anand Mistry (off Chromium)
2014/10/13 19:30:21
That's not enough. isAlwaysOnEnabled() also return
Dan Beam
2014/10/13 19:33:09
ah, yeah, sorry -- missed a !
|
| + if (enabled) { |
| + this.setupListeners_(); |
| + } else { |
| + this.removeListeners_(); |
| + this.stopHotwording_(); |
| + this.disconnectAllClients_(); |
| + } |
| + } |
| + }; |
| + |
| + return { |
| + PageAudioManager: PageAudioManager |
| + }; |
| +}); |