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