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