OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
Dan Beam
2014/10/09 22:43:58
nit: no (c)
| |
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); | |
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 }); | |
OLD | NEW |