Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(579)

Unified Diff: chrome/browser/resources/hotword/page_audio_manager.js

Issue 600523004: Support hotwording on google.com and the new tab page. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: More review comments. Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | chrome/browser/resources/hotword/state_manager.js » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
+ };
+});
« no previous file with comments | « chrome/browser/resources/hotword/manifest.json ('k') | chrome/browser/resources/hotword/state_manager.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698