OLD | NEW |
---|---|
(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 }); | |
OLD | NEW |