background.js revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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/** 6 * @fileoverview Script that runs on the background page. 7 * 8 */ 9 10goog.provide('cvox.ChromeVoxBackground'); 11 12goog.require('cvox.AbstractEarcons'); 13goog.require('cvox.AccessibilityApiHandler'); 14goog.require('cvox.BrailleBackground'); 15goog.require('cvox.BrailleCaptionsBackground'); 16goog.require('cvox.ChromeVox'); 17goog.require('cvox.ChromeVoxEditableTextBase'); 18goog.require('cvox.ChromeVoxPrefs'); 19goog.require('cvox.CompositeTts'); 20goog.require('cvox.ConsoleTts'); 21goog.require('cvox.EarconsBackground'); 22goog.require('cvox.ExtensionBridge'); 23goog.require('cvox.HostFactory'); 24goog.require('cvox.InjectedScriptLoader'); 25goog.require('cvox.Msgs'); 26goog.require('cvox.NavBraille'); 27// TODO(dtseng): This is required to prevent Closure from stripping our export 28// prefs on window. 29goog.require('cvox.OptionsPage'); 30goog.require('cvox.PlatformFilter'); 31goog.require('cvox.PlatformUtil'); 32goog.require('cvox.TabsApiHandler'); 33goog.require('cvox.TtsBackground'); 34 35 36/** 37 * This object manages the global and persistent state for ChromeVox. 38 * It listens for messages from the content scripts on pages and 39 * interprets them. 40 * @constructor 41 */ 42cvox.ChromeVoxBackground = function() { 43}; 44 45 46/** 47 * Initialize the background page: set up TTS and bridge listeners. 48 */ 49cvox.ChromeVoxBackground.prototype.init = function() { 50 // In the case of ChromeOS, only continue initialization if this instance of 51 // ChromeVox is as we expect. This prevents ChromeVox from the webstore from 52 // running. 53 if (cvox.ChromeVox.isChromeOS && 54 chrome.i18n.getMessage('@@extension_id') != 55 'mndnfokpggljbaajbnioimlmbfngpief') { 56 return; 57 } 58 59 cvox.ChromeVox.msgs = new cvox.Msgs(); 60 this.prefs = new cvox.ChromeVoxPrefs(); 61 this.readPrefs(); 62 63 var consoleTts = cvox.ConsoleTts.getInstance(); 64 consoleTts.setEnabled(true); 65 66 /** 67 * Chrome's actual TTS which knows and cares about pitch, volume, etc. 68 * @type {cvox.TtsBackground} 69 * @private 70 */ 71 this.backgroundTts_ = new cvox.TtsBackground(); 72 73 /** 74 * @type {cvox.TtsInterface} 75 */ 76 this.tts = new cvox.CompositeTts() 77 .add(this.backgroundTts_) 78 .add(consoleTts); 79 80 this.earcons = new cvox.EarconsBackground(); 81 this.addBridgeListener(); 82 83 /** 84 * The actual Braille service. 85 * @type {cvox.BrailleBackground} 86 * @private 87 */ 88 this.backgroundBraille_ = new cvox.BrailleBackground(); 89 90 this.accessibilityApiHandler_ = new cvox.AccessibilityApiHandler( 91 this.tts, this.backgroundBraille_, this.earcons); 92 this.tabsApiHandler_ = new cvox.TabsApiHandler( 93 this.tts, this.backgroundBraille_, this.earcons); 94 95 // Export globals on cvox.ChromeVox. 96 cvox.ChromeVox.tts = this.tts; 97 cvox.ChromeVox.braille = this.backgroundBraille_; 98 cvox.ChromeVox.earcons = this.earcons; 99 100 // TODO(dtseng): Remove the second check on or after m33. 101 if (cvox.ChromeVox.isChromeOS && 102 chrome.accessibilityPrivate.onChromeVoxLoadStateChanged) { 103 chrome.accessibilityPrivate.onChromeVoxLoadStateChanged.addListener( 104 this.onLoadStateChanged); 105 } 106 107 this.checkVersionNumber(); 108 109 // Set up a message passing system for goog.provide() calls from 110 // within the content scripts. 111 chrome.extension.onMessage.addListener( 112 function(request, sender, callback) { 113 if (request['srcFile']) { 114 var srcFile = request['srcFile']; 115 cvox.InjectedScriptLoader.fetchCode( 116 [srcFile], 117 function(code) { 118 callback({'code': code[srcFile]}); 119 }); 120 } 121 return true; 122 }); 123 124 var self = this; 125 if (chrome.commandLinePrivate) { 126 chrome.commandLinePrivate.hasSwitch('enable-chromevox-next', 127 goog.bind(function(result) { 128 if (result) { 129 return; 130 } 131 // Inject the content script into all running tabs. 132 chrome.windows.getAll({'populate': true}, function(windows) { 133 for (var i = 0; i < windows.length; i++) { 134 var tabs = windows[i].tabs; 135 self.injectChromeVoxIntoTabs(tabs); 136 } 137 }); 138 }, this)); 139 } 140 141 if (localStorage['active'] == 'false') { 142 // Warn the user when the browser first starts if ChromeVox is inactive. 143 this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_inactive'), 1); 144 } else if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) { 145 // Introductory message. 146 this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_intro'), 1); 147 cvox.ChromeVox.braille.write(cvox.NavBraille.fromText( 148 cvox.ChromeVox.msgs.getMsg('intro_brl'))); 149 } 150}; 151 152 153/** 154 * Inject ChromeVox into a tab. 155 * @param {Array.<Tab>} tabs The tab where ChromeVox scripts should be injected. 156 * @param {boolean=} opt_forceCompiled forces compiled ChromeVox to be injected; 157 * defaults to Closure's compiled flag. 158 */ 159cvox.ChromeVoxBackground.prototype.injectChromeVoxIntoTabs = 160 function(tabs, opt_forceCompiled) { 161 var listOfFiles; 162 163 // These lists of files must match the content_scripts section in 164 // the manifest files. 165 if (COMPILED || opt_forceCompiled) { 166 listOfFiles = ['chromeVoxChromePageScript.js']; 167 } else { 168 listOfFiles = [ 169 'closure/closure_preinit.js', 170 'closure/base.js', 171 'deps.js', 172 'chromevox/injected/loader.js']; 173 } 174 175 var stageTwo = function(code) { 176 for (var i = 0, tab; tab = tabs[i]; i++) { 177 window.console.log('Injecting into ' + tab.id, tab); 178 var sawError = false; 179 180 /** 181 * A helper function which executes code. 182 * @param {string} code The code to execute. 183 */ 184 var executeScript = goog.bind(function(code) { 185 chrome.tabs.executeScript( 186 tab.id, 187 {'code': code, 188 'allFrames': true}, 189 goog.bind(function() { 190 if (!chrome.extension.lastError) { 191 return; 192 } 193 if (sawError) { 194 return; 195 } 196 sawError = true; 197 console.error('Could not inject into tab', tab); 198 this.tts.speak('Error starting ChromeVox for ' + 199 tab.title + ', ' + tab.url, 1); 200 }, this)); 201 }, this); 202 203 // There is a scenario where two copies of the content script can get 204 // loaded into the same tab on browser startup - one automatically and one 205 // because the background page injects the content script into every tab 206 // on startup. To work around potential bugs resulting from this, 207 // ChromeVox exports a global function called disableChromeVox() that can 208 // be used here to disable any existing running instance before we inject 209 // a new instance of the content script into this tab. 210 // 211 // It's harmless if there wasn't a copy of ChromeVox already running. 212 // 213 // Also, set some variables so that Closure deps work correctly and so 214 // that ChromeVox knows not to announce feedback as if a page just loaded. 215 executeScript('try { window.disableChromeVox(); } catch(e) { }\n' + 216 'window.INJECTED_AFTER_LOAD = true;\n' + 217 'window.CLOSURE_NO_DEPS = true\n'); 218 219 // Now inject the ChromeVox content script code into the tab. 220 listOfFiles.forEach(function(file) { executeScript(code[file]); }); 221 } 222 }; 223 224 // We use fetchCode instead of chrome.extensions.executeFile because 225 // executeFile doesn't propagate the file name to the content script 226 // which means that script is not visible in Dev Tools. 227 cvox.InjectedScriptLoader.fetchCode(listOfFiles, stageTwo); 228}; 229 230 231/** 232 * Called when a TTS message is received from a page content script. 233 * @param {Object} msg The TTS message. 234 */ 235cvox.ChromeVoxBackground.prototype.onTtsMessage = function(msg) { 236 if (msg['action'] == 'speak') { 237 // Tell the handler for native UI (chrome of chrome) events that 238 // the last speech came from web, and not from native UI. 239 this.accessibilityApiHandler_.setWebContext(); 240 this.tts.speak(msg['text'], msg['queueMode'], msg['properties']); 241 } else if (msg['action'] == 'stop') { 242 this.tts.stop(); 243 } else if (msg['action'] == 'increaseOrDecrease') { 244 this.tts.increaseOrDecreaseProperty(msg['property'], msg['increase']); 245 var property = msg['property']; 246 var engine = this.backgroundTts_; 247 var valueAsPercent = Math.round( 248 this.backgroundTts_.propertyToPercentage(property) * 100); 249 var announcement; 250 switch (msg['property']) { 251 case cvox.AbstractTts.RATE: 252 announcement = cvox.ChromeVox.msgs.getMsg('announce_rate', 253 [valueAsPercent]); 254 break; 255 case cvox.AbstractTts.PITCH: 256 announcement = cvox.ChromeVox.msgs.getMsg('announce_pitch', 257 [valueAsPercent]); 258 break; 259 case cvox.AbstractTts.VOLUME: 260 announcement = cvox.ChromeVox.msgs.getMsg('announce_volume', 261 [valueAsPercent]); 262 break; 263 } 264 if (announcement) { 265 this.tts.speak(announcement, 266 cvox.AbstractTts.QUEUE_MODE_FLUSH, 267 cvox.AbstractTts.PERSONALITY_ANNOTATION); 268 } 269 } else if (msg['action'] == 'cyclePunctuationEcho') { 270 this.tts.speak(cvox.ChromeVox.msgs.getMsg( 271 this.backgroundTts_.cyclePunctuationEcho()), 272 cvox.AbstractTts.QUEUE_MODE_FLUSH); 273 } 274}; 275 276 277/** 278 * Called when an earcon message is received from a page content script. 279 * @param {Object} msg The earcon message. 280 */ 281cvox.ChromeVoxBackground.prototype.onEarconMessage = function(msg) { 282 if (msg.action == 'play') { 283 this.earcons.playEarcon(msg.earcon); 284 } 285}; 286 287 288/** 289 * Listen for connections from our content script bridges, and dispatch the 290 * messages to the proper destination. 291 */ 292cvox.ChromeVoxBackground.prototype.addBridgeListener = function() { 293 cvox.ExtensionBridge.addMessageListener(goog.bind(function(msg, port) { 294 var target = msg['target']; 295 var action = msg['action']; 296 297 switch (target) { 298 case 'OpenTab': 299 var destination = new Object(); 300 destination.url = msg['url']; 301 chrome.tabs.create(destination); 302 break; 303 case 'KbExplorer': 304 var explorerPage = new Object(); 305 explorerPage.url = 'chromevox/background/kbexplorer.html'; 306 chrome.tabs.create(explorerPage); 307 break; 308 case 'HelpDocs': 309 var helpPage = new Object(); 310 helpPage.url = 'http://chromevox.com/tutorial/index.html'; 311 chrome.tabs.create(helpPage); 312 break; 313 case 'Options': 314 if (action == 'open') { 315 var optionsPage = new Object(); 316 optionsPage.url = 'chromevox/background/options.html'; 317 chrome.tabs.create(optionsPage); 318 } 319 break; 320 case 'Data': 321 if (action == 'getHistory') { 322 var results = {}; 323 chrome.history.search({text: '', maxResults: 25}, function(items) { 324 items.forEach(function(item) { 325 if (item.url) { 326 results[item.url] = true; 327 } 328 }); 329 port.postMessage({ 330 'history': results 331 }); 332 }); 333 } 334 break; 335 case 'Prefs': 336 if (action == 'getPrefs') { 337 this.prefs.sendPrefsToPort(port); 338 } else if (action == 'setPref') { 339 if (msg['pref'] == 'active' && 340 msg['value'] != cvox.ChromeVox.isActive) { 341 if (cvox.ChromeVox.isActive) { 342 this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_inactive')); 343 chrome.accessibilityPrivate.setNativeAccessibilityEnabled( 344 true); 345 } else { 346 chrome.accessibilityPrivate.setNativeAccessibilityEnabled( 347 false); 348 } 349 } else if (msg['pref'] == 'earcons') { 350 this.earcons.enabled = msg['value']; 351 } else if (msg['pref'] == 'sticky' && msg['announce']) { 352 if (msg['value']) { 353 this.tts.speak(cvox.ChromeVox.msgs.getMsg('sticky_mode_enabled')); 354 } else { 355 this.tts.speak( 356 cvox.ChromeVox.msgs.getMsg('sticky_mode_disabled')); 357 } 358 } else if (msg['pref'] == 'typingEcho' && msg['announce']) { 359 var announce = ''; 360 switch (msg['value']) { 361 case cvox.TypingEcho.CHARACTER: 362 announce = cvox.ChromeVox.msgs.getMsg('character_echo'); 363 break; 364 case cvox.TypingEcho.WORD: 365 announce = cvox.ChromeVox.msgs.getMsg('word_echo'); 366 break; 367 case cvox.TypingEcho.CHARACTER_AND_WORD: 368 announce = cvox.ChromeVox.msgs.getMsg('character_and_word_echo'); 369 break; 370 case cvox.TypingEcho.NONE: 371 announce = cvox.ChromeVox.msgs.getMsg('none_echo'); 372 break; 373 default: 374 break; 375 } 376 if (announce) { 377 this.tts.speak(announce); 378 } 379 } else if (msg['pref'] == 'brailleCaptions') { 380 cvox.BrailleCaptionsBackground.setActive(msg['value']); 381 } 382 this.prefs.setPref(msg['pref'], msg['value']); 383 this.readPrefs(); 384 } 385 break; 386 case 'Math': 387 // TODO (sorge): Put the change of styles etc. here! 388 if (msg['action'] == 'getDomains') { 389 port.postMessage({'message': 'DOMAINS_STYLES', 390 'domains': this.backgroundTts_.mathmap.allDomains, 391 'styles': this.backgroundTts_.mathmap.allStyles}); 392 } 393 break; 394 case 'TTS': 395 if (msg['startCallbackId'] != undefined) { 396 msg['properties']['startCallback'] = function() { 397 port.postMessage({'message': 'TTS_CALLBACK', 398 'id': msg['startCallbackId']}); 399 }; 400 } 401 if (msg['endCallbackId'] != undefined) { 402 msg['properties']['endCallback'] = function() { 403 port.postMessage({'message': 'TTS_CALLBACK', 404 'id': msg['endCallbackId']}); 405 }; 406 } 407 try { 408 this.onTtsMessage(msg); 409 } catch (err) { 410 console.log(err); 411 } 412 break; 413 case 'EARCON': 414 this.onEarconMessage(msg); 415 break; 416 case 'BRAILLE': 417 try { 418 this.backgroundBraille_.onBrailleMessage(msg); 419 } catch (err) { 420 console.log(err); 421 } 422 break; 423 } 424 }, this)); 425}; 426 427 428/** 429 * Checks the version number. If it has changed, display release notes 430 * to the user. 431 */ 432cvox.ChromeVoxBackground.prototype.checkVersionNumber = function() { 433 // Don't update version or show release notes if the current tab is within an 434 // incognito window (which may occur on ChromeOS immediately after OOBE). 435 if (this.isIncognito_()) { 436 return; 437 } 438 this.localStorageVersion = localStorage['versionString']; 439 this.showNotesIfNewVersion(); 440}; 441 442 443/** 444 * Display release notes to the user. 445 */ 446cvox.ChromeVoxBackground.prototype.displayReleaseNotes = function() { 447 chrome.tabs.create( 448 {'url': 'http://chromevox.com/release_notes.html'}); 449}; 450 451 452/** 453 * Gets the current version number from the extension manifest. 454 */ 455cvox.ChromeVoxBackground.prototype.showNotesIfNewVersion = function() { 456 // Check version number in manifest. 457 var url = chrome.extension.getURL('manifest.json'); 458 var xhr = new XMLHttpRequest(); 459 var context = this; 460 xhr.onreadystatechange = function() { 461 if (xhr.readyState == 4) { 462 var manifest = JSON.parse(xhr.responseText); 463 console.log('Version: ' + manifest.version); 464 465 var shouldShowReleaseNotes = 466 (context.localStorageVersion != manifest.version); 467 468 // On Chrome OS, don't show the release notes the first time, only 469 // after a version upgrade. 470 if (navigator.userAgent.indexOf('CrOS') != -1 && 471 context.localStorageVersion == undefined) { 472 shouldShowReleaseNotes = false; 473 } 474 475 if (shouldShowReleaseNotes) { 476 context.displayReleaseNotes(); 477 } 478 479 // Update version number in local storage 480 localStorage['versionString'] = manifest.version; 481 this.localStorageVersion = manifest.version; 482 } 483 }; 484 xhr.open('GET', url); 485 xhr.send(); 486}; 487 488 489/** 490 * Read and apply preferences that affect the background context. 491 */ 492cvox.ChromeVoxBackground.prototype.readPrefs = function() { 493 var prefs = this.prefs.getPrefs(); 494 cvox.ChromeVoxEditableTextBase.useIBeamCursor = 495 (prefs['useIBeamCursor'] == 'true'); 496 cvox.ChromeVox.isActive = 497 (prefs['active'] == 'true' || cvox.ChromeVox.isChromeOS); 498 cvox.ChromeVox.isStickyPrefOn = (prefs['sticky'] == 'true'); 499}; 500 501/** 502 * Checks if we are currently in an incognito window. 503 * @return {boolean} True if incognito or not within a tab context, false 504 * otherwise. 505 * @private 506 */ 507cvox.ChromeVoxBackground.prototype.isIncognito_ = function() { 508 var incognito = false; 509 chrome.tabs.getCurrent(function(tab) { 510 // Tab is null if not called from a tab context. In that case, also consider 511 // it incognito. 512 incognito = tab ? tab.incognito : true; 513 }); 514 return incognito; 515}; 516 517 518// TODO(dtseng): The loading param is no longer used. Remove it once the 519// upstream Chrome API changes. 520/** 521 * Handles the onChromeVoxLoadStateChanged event. 522 * @param {boolean} loading True if ChromeVox is loading; false if it is 523 * unloading. 524 * @param {boolean} makeAnnouncements True if announcements should be made. 525 */ 526cvox.ChromeVoxBackground.prototype.onLoadStateChanged = function( 527 loading, makeAnnouncements) { 528 if (loading) { 529 if (makeAnnouncements) { 530 cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_intro'), 531 1, 532 {doNotInterrupt: true}); 533 cvox.ChromeVox.braille.write(cvox.NavBraille.fromText( 534 cvox.ChromeVox.msgs.getMsg('intro_brl'))); 535 } 536 } 537 }; 538 539 540// Create the background page object and export a function window['speak'] 541// so that other background pages can access it. Also export the prefs object 542// for access by the options page. 543(function() { 544 var background = new cvox.ChromeVoxBackground(); 545 background.init(); 546 window['speak'] = goog.bind(background.tts.speak, background.tts); 547 548 // Export the prefs object for access by the options page. 549 window['prefs'] = background.prefs; 550 551 // Export the braille object for access by the options page. 552 window['braille'] = cvox.ChromeVox.braille; 553 554 // Export this background page for ChromeVox Next to access. 555 cvox.ChromeVox.background = background; 556})(); 557