| 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..d26190e59b63b6949a8dbc0ba74f4980d7bfe62c
|
| --- /dev/null
|
| +++ b/chrome/browser/resources/hotword/page_audio_manager.js
|
| @@ -0,0 +1,435 @@
|
| +// 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 eligible 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.call(this, 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) {
|
| + if (port.name != hotword.constants.CLIENT_PORT_NAME)
|
| + return;
|
| +
|
| + var tab = /** @type {!Tab} */(port.sender.tab);
|
| + // 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() {
|
| + for (var id in this.portMap_) {
|
| + 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) {
|
| + switch (request[hotword.constants.COMMAND_FIELD_NAME]) {
|
| + // 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() {
|
| + 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();
|
| + if (enabled) {
|
| + this.setupListeners_();
|
| + } else {
|
| + this.removeListeners_();
|
| + this.stopHotwording_();
|
| + this.disconnectAllClients_();
|
| + }
|
| + }
|
| + };
|
| +
|
| + return {
|
| + PageAudioManager: PageAudioManager
|
| + };
|
| +});
|
|
|