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