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

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

Powered by Google App Engine
This is Rietveld 408576698