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

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

Powered by Google App Engine
This is Rietveld 408576698