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