| 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 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * @fileoverview This is the audio client content script injected into eligible | |
| 9 * Google.com and New tab pages for interaction between the Webpage and the | |
| 10 * Hotword extension. | |
| 11 */ | |
| 12 | |
| 13 | |
| 14 | |
| 15 (function() { | |
| 16 /** | |
| 17 * @constructor | |
| 18 */ | |
| 19 var AudioClient = function() { | |
| 20 /** @private {Element} */ | |
| 21 this.speechOverlay_ = null; | |
| 22 | |
| 23 /** @private {number} */ | |
| 24 this.checkSpeechUiRetries_ = 0; | |
| 25 | |
| 26 /** | |
| 27 * Port used to communicate with the audio manager. | |
| 28 * @private {?Port} | |
| 29 */ | |
| 30 this.port_ = null; | |
| 31 | |
| 32 /** | |
| 33 * Keeps track of the effects of different commands. Used to verify that | |
| 34 * proper UIs are shown to the user. | |
| 35 * @private {Object<AudioClient.CommandToPage, Object>} | |
| 36 */ | |
| 37 this.uiStatus_ = null; | |
| 38 | |
| 39 /** | |
| 40 * Bound function used to handle commands sent from the page to this script. | |
| 41 * @private {Function} | |
| 42 */ | |
| 43 this.handleCommandFromPageFunc_ = null; | |
| 44 }; | |
| 45 | |
| 46 | |
| 47 /** | |
| 48 * Messages sent to the page to control the voice search UI. | |
| 49 * @enum {string} | |
| 50 */ | |
| 51 AudioClient.CommandToPage = { | |
| 52 HOTWORD_VOICE_TRIGGER: 'vt', | |
| 53 HOTWORD_STARTED: 'hs', | |
| 54 HOTWORD_ENDED: 'hd', | |
| 55 HOTWORD_TIMEOUT: 'ht', | |
| 56 HOTWORD_ERROR: 'he' | |
| 57 }; | |
| 58 | |
| 59 | |
| 60 /** | |
| 61 * Messages received from the page used to indicate voice search state. | |
| 62 * @enum {string} | |
| 63 */ | |
| 64 AudioClient.CommandFromPage = { | |
| 65 SPEECH_START: 'ss', | |
| 66 SPEECH_END: 'se', | |
| 67 SPEECH_RESET: 'sr', | |
| 68 SHOWING_HOTWORD_START: 'shs', | |
| 69 SHOWING_ERROR_MESSAGE: 'sem', | |
| 70 SHOWING_TIMEOUT_MESSAGE: 'stm', | |
| 71 CLICKED_RESUME: 'hcc', | |
| 72 CLICKED_RESTART: 'hcr', | |
| 73 CLICKED_DEBUG: 'hcd' | |
| 74 }; | |
| 75 | |
| 76 | |
| 77 /** | |
| 78 * Errors that are sent to the hotword extension. | |
| 79 * @enum {string} | |
| 80 */ | |
| 81 AudioClient.Error = { | |
| 82 NO_SPEECH_UI: 'ac1', | |
| 83 NO_HOTWORD_STARTED_UI: 'ac2', | |
| 84 NO_HOTWORD_TIMEOUT_UI: 'ac3', | |
| 85 NO_HOTWORD_ERROR_UI: 'ac4' | |
| 86 }; | |
| 87 | |
| 88 | |
| 89 /** | |
| 90 * @const {string} | |
| 91 * @private | |
| 92 */ | |
| 93 AudioClient.HOTWORD_EXTENSION_ID_ = 'bepbmhgboaologfdajaanbcjmnhjmhfn'; | |
| 94 | |
| 95 | |
| 96 /** | |
| 97 * Number of times to retry checking a transient error. | |
| 98 * @const {number} | |
| 99 * @private | |
| 100 */ | |
| 101 AudioClient.MAX_RETRIES = 3; | |
| 102 | |
| 103 | |
| 104 /** | |
| 105 * Delay to wait in milliseconds before rechecking for any transient errors. | |
| 106 * @const {number} | |
| 107 * @private | |
| 108 */ | |
| 109 AudioClient.RETRY_TIME_MS_ = 2000; | |
| 110 | |
| 111 | |
| 112 /** | |
| 113 * DOM ID for the speech UI overlay. | |
| 114 * @const {string} | |
| 115 * @private | |
| 116 */ | |
| 117 AudioClient.SPEECH_UI_OVERLAY_ID_ = 'spch'; | |
| 118 | |
| 119 | |
| 120 /** | |
| 121 * @const {string} | |
| 122 * @private | |
| 123 */ | |
| 124 AudioClient.HELP_CENTER_URL_ = | |
| 125 'https://support.google.com/chrome/?p=ui_hotword_search'; | |
| 126 | |
| 127 | |
| 128 /** | |
| 129 * @const {string} | |
| 130 * @private | |
| 131 */ | |
| 132 AudioClient.CLIENT_PORT_NAME_ = 'chwcpn'; | |
| 133 | |
| 134 /** | |
| 135 * Existence of the Audio Client. | |
| 136 * @const {string} | |
| 137 * @private | |
| 138 */ | |
| 139 AudioClient.EXISTS_ = 'chwace'; | |
| 140 | |
| 141 | |
| 142 /** | |
| 143 * Checks for the presence of speech overlay UI DOM elements. | |
| 144 * @private | |
| 145 */ | |
| 146 AudioClient.prototype.checkSpeechOverlayUi_ = function() { | |
| 147 if (!this.speechOverlay_) { | |
| 148 window.setTimeout(this.delayedCheckSpeechOverlayUi_.bind(this), | |
| 149 AudioClient.RETRY_TIME_MS_); | |
| 150 } else { | |
| 151 this.checkSpeechUiRetries_ = 0; | |
| 152 } | |
| 153 }; | |
| 154 | |
| 155 | |
| 156 /** | |
| 157 * Function called to check for the speech UI overlay after some time has | |
| 158 * passed since an initial check. Will either retry triggering the speech | |
| 159 * or sends an error message depending on the number of retries. | |
| 160 * @private | |
| 161 */ | |
| 162 AudioClient.prototype.delayedCheckSpeechOverlayUi_ = function() { | |
| 163 this.speechOverlay_ = document.getElementById( | |
| 164 AudioClient.SPEECH_UI_OVERLAY_ID_); | |
| 165 if (!this.speechOverlay_) { | |
| 166 if (this.checkSpeechUiRetries_++ < AudioClient.MAX_RETRIES) { | |
| 167 this.sendCommandToPage_(AudioClient.CommandToPage.VOICE_TRIGGER); | |
| 168 this.checkSpeechOverlayUi_(); | |
| 169 } else { | |
| 170 this.sendCommandToExtension_(AudioClient.Error.NO_SPEECH_UI); | |
| 171 } | |
| 172 } else { | |
| 173 this.checkSpeechUiRetries_ = 0; | |
| 174 } | |
| 175 }; | |
| 176 | |
| 177 | |
| 178 /** | |
| 179 * Checks that the triggered UI is actually displayed. | |
| 180 * @param {AudioClient.CommandToPage} command Command that was send. | |
| 181 * @private | |
| 182 */ | |
| 183 AudioClient.prototype.checkUi_ = function(command) { | |
| 184 this.uiStatus_[command].timeoutId = | |
| 185 window.setTimeout(this.failedCheckUi_.bind(this, command), | |
| 186 AudioClient.RETRY_TIME_MS_); | |
| 187 }; | |
| 188 | |
| 189 | |
| 190 /** | |
| 191 * Function called when the UI verification is not called in time. Will either | |
| 192 * retry the command or sends an error message, depending on the number of | |
| 193 * retries for the command. | |
| 194 * @param {AudioClient.CommandToPage} command Command that was sent. | |
| 195 * @private | |
| 196 */ | |
| 197 AudioClient.prototype.failedCheckUi_ = function(command) { | |
| 198 if (this.uiStatus_[command].tries++ < AudioClient.MAX_RETRIES) { | |
| 199 this.sendCommandToPage_(command); | |
| 200 this.checkUi_(command); | |
| 201 } else { | |
| 202 this.sendCommandToExtension_(this.uiStatus_[command].error); | |
| 203 } | |
| 204 }; | |
| 205 | |
| 206 | |
| 207 /** | |
| 208 * Confirm that an UI element has been shown. | |
| 209 * @param {AudioClient.CommandToPage} command UI to confirm. | |
| 210 * @private | |
| 211 */ | |
| 212 AudioClient.prototype.verifyUi_ = function(command) { | |
| 213 if (this.uiStatus_[command].timeoutId) { | |
| 214 window.clearTimeout(this.uiStatus_[command].timeoutId); | |
| 215 this.uiStatus_[command].timeoutId = null; | |
| 216 this.uiStatus_[command].tries = 0; | |
| 217 } | |
| 218 }; | |
| 219 | |
| 220 | |
| 221 /** | |
| 222 * Sends a command to the audio manager. | |
| 223 * @param {string} commandStr command to send to plugin. | |
| 224 * @private | |
| 225 */ | |
| 226 AudioClient.prototype.sendCommandToExtension_ = function(commandStr) { | |
| 227 if (this.port_) | |
| 228 this.port_.postMessage({'cmd': commandStr}); | |
| 229 }; | |
| 230 | |
| 231 | |
| 232 /** | |
| 233 * Handles a message from the audio manager. | |
| 234 * @param {{cmd: string}} commandObj Command from the audio manager. | |
| 235 * @private | |
| 236 */ | |
| 237 AudioClient.prototype.handleCommandFromExtension_ = function(commandObj) { | |
| 238 var command = commandObj['cmd']; | |
| 239 if (command) { | |
| 240 switch (command) { | |
| 241 case AudioClient.CommandToPage.HOTWORD_VOICE_TRIGGER: | |
| 242 this.sendCommandToPage_(command); | |
| 243 this.checkSpeechOverlayUi_(); | |
| 244 break; | |
| 245 case AudioClient.CommandToPage.HOTWORD_STARTED: | |
| 246 this.sendCommandToPage_(command); | |
| 247 this.checkUi_(command); | |
| 248 break; | |
| 249 case AudioClient.CommandToPage.HOTWORD_ENDED: | |
| 250 this.sendCommandToPage_(command); | |
| 251 break; | |
| 252 case AudioClient.CommandToPage.HOTWORD_TIMEOUT: | |
| 253 this.sendCommandToPage_(command); | |
| 254 this.checkUi_(command); | |
| 255 break; | |
| 256 case AudioClient.CommandToPage.HOTWORD_ERROR: | |
| 257 this.sendCommandToPage_(command); | |
| 258 this.checkUi_(command); | |
| 259 break; | |
| 260 } | |
| 261 } | |
| 262 }; | |
| 263 | |
| 264 | |
| 265 /** | |
| 266 * @param {AudioClient.CommandToPage} commandStr Command to send. | |
| 267 * @private | |
| 268 */ | |
| 269 AudioClient.prototype.sendCommandToPage_ = function(commandStr) { | |
| 270 window.postMessage({'type': commandStr}, '*'); | |
| 271 }; | |
| 272 | |
| 273 | |
| 274 /** | |
| 275 * Handles a message from the html window. | |
| 276 * @param {!MessageEvent} messageEvent Message event from the window. | |
| 277 * @private | |
| 278 */ | |
| 279 AudioClient.prototype.handleCommandFromPage_ = function(messageEvent) { | |
| 280 if (messageEvent.source == window && messageEvent.data.type) { | |
| 281 var command = messageEvent.data.type; | |
| 282 switch (command) { | |
| 283 case AudioClient.CommandFromPage.SPEECH_START: | |
| 284 this.speechActive_ = true; | |
| 285 this.sendCommandToExtension_(command); | |
| 286 break; | |
| 287 case AudioClient.CommandFromPage.SPEECH_END: | |
| 288 this.speechActive_ = false; | |
| 289 this.sendCommandToExtension_(command); | |
| 290 break; | |
| 291 case AudioClient.CommandFromPage.SPEECH_RESET: | |
| 292 this.speechActive_ = false; | |
| 293 this.sendCommandToExtension_(command); | |
| 294 break; | |
| 295 case 'SPEECH_RESET': // Legacy, for embedded NTP. | |
| 296 this.speechActive_ = false; | |
| 297 this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_END); | |
| 298 break; | |
| 299 case AudioClient.CommandFromPage.CLICKED_RESUME: | |
| 300 this.sendCommandToExtension_(command); | |
| 301 break; | |
| 302 case AudioClient.CommandFromPage.CLICKED_RESTART: | |
| 303 this.sendCommandToExtension_(command); | |
| 304 break; | |
| 305 case AudioClient.CommandFromPage.CLICKED_DEBUG: | |
| 306 window.open(AudioClient.HELP_CENTER_URL_, '_blank'); | |
| 307 break; | |
| 308 case AudioClient.CommandFromPage.SHOWING_HOTWORD_START: | |
| 309 this.verifyUi_(AudioClient.CommandToPage.HOTWORD_STARTED); | |
| 310 break; | |
| 311 case AudioClient.CommandFromPage.SHOWING_ERROR_MESSAGE: | |
| 312 this.verifyUi_(AudioClient.CommandToPage.HOTWORD_ERROR); | |
| 313 break; | |
| 314 case AudioClient.CommandFromPage.SHOWING_TIMEOUT_MESSAGE: | |
| 315 this.verifyUi_(AudioClient.CommandToPage.HOTWORD_TIMEOUT); | |
| 316 break; | |
| 317 } | |
| 318 } | |
| 319 }; | |
| 320 | |
| 321 | |
| 322 /** | |
| 323 * Initialize the content script. | |
| 324 */ | |
| 325 AudioClient.prototype.initialize = function() { | |
| 326 if (AudioClient.EXISTS_ in window) | |
| 327 return; | |
| 328 window[AudioClient.EXISTS_] = true; | |
| 329 | |
| 330 // UI verification object. | |
| 331 this.uiStatus_ = {}; | |
| 332 this.uiStatus_[AudioClient.CommandToPage.HOTWORD_STARTED] = { | |
| 333 timeoutId: null, | |
| 334 tries: 0, | |
| 335 error: AudioClient.Error.NO_HOTWORD_STARTED_UI | |
| 336 }; | |
| 337 this.uiStatus_[AudioClient.CommandToPage.HOTWORD_TIMEOUT] = { | |
| 338 timeoutId: null, | |
| 339 tries: 0, | |
| 340 error: AudioClient.Error.NO_HOTWORD_TIMEOUT_UI | |
| 341 }; | |
| 342 this.uiStatus_[AudioClient.CommandToPage.HOTWORD_ERROR] = { | |
| 343 timeoutId: null, | |
| 344 tries: 0, | |
| 345 error: AudioClient.Error.NO_HOTWORD_ERROR_UI | |
| 346 }; | |
| 347 | |
| 348 this.handleCommandFromPageFunc_ = this.handleCommandFromPage_.bind(this); | |
| 349 window.addEventListener('message', this.handleCommandFromPageFunc_, false); | |
| 350 this.initPort_(); | |
| 351 }; | |
| 352 | |
| 353 | |
| 354 /** | |
| 355 * Initialize the communications port with the audio manager. This | |
| 356 * function will be also be called again if the audio-manager | |
| 357 * disconnects for some reason (such as the extension | |
| 358 * background.html page being reloaded). | |
| 359 * @private | |
| 360 */ | |
| 361 AudioClient.prototype.initPort_ = function() { | |
| 362 this.port_ = chrome.runtime.connect( | |
| 363 AudioClient.HOTWORD_EXTENSION_ID_, | |
| 364 {'name': AudioClient.CLIENT_PORT_NAME_}); | |
| 365 // Note that this listen may have to be destroyed manually if AudioClient | |
| 366 // is ever destroyed on this tab. | |
| 367 this.port_.onDisconnect.addListener( | |
| 368 (function(e) { | |
| 369 if (this.handleCommandFromPageFunc_) { | |
| 370 window.removeEventListener( | |
| 371 'message', this.handleCommandFromPageFunc_, false); | |
| 372 } | |
| 373 delete window[AudioClient.EXISTS_]; | |
| 374 }).bind(this)); | |
| 375 | |
| 376 // See note above. | |
| 377 this.port_.onMessage.addListener( | |
| 378 this.handleCommandFromExtension_.bind(this)); | |
| 379 | |
| 380 if (this.speechActive_) | |
| 381 this.sendCommandToExtension_(AudioClient.CommandFromPage.SPEECH_START); | |
| 382 }; | |
| 383 | |
| 384 | |
| 385 // Initializes as soon as the code is ready, do not wait for the page. | |
| 386 new AudioClient().initialize(); | |
| 387 })(); | |
| OLD | NEW |