blob: daad1a489330bc8409ca584dbbbf5abb65dbaea3 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Script that runs on the background page.
*/
goog.provide('cvox.ChromeVoxBackground');
goog.require('cvox.AbstractEarcons');
goog.require('cvox.BrailleBackground');
goog.require('cvox.BrailleCaptionsBackground');
goog.require('cvox.ChromeVox');
goog.require('cvox.ChromeVoxEditableTextBase');
goog.require('cvox.ChromeVoxPrefs');
goog.require('cvox.CompositeTts');
goog.require('cvox.ConsoleTts');
goog.require('cvox.EarconsBackground');
goog.require('cvox.ExtensionBridge');
goog.require('cvox.HostFactory');
goog.require('cvox.InjectedScriptLoader');
goog.require('cvox.Msgs');
goog.require('cvox.NavBraille');
// TODO(dtseng): This is required to prevent Closure from stripping our export
// prefs on window.
goog.require('cvox.OptionsPage');
goog.require('cvox.PlatformFilter');
goog.require('cvox.PlatformUtil');
goog.require('cvox.QueueMode');
goog.require('cvox.TabsApiHandler');
goog.require('cvox.TtsBackground');
/**
* This object manages the global and persistent state for ChromeVox.
* It listens for messages from the content scripts on pages and
* interprets them.
* @constructor
*/
cvox.ChromeVoxBackground = function() {
};
/**
* Initialize the background page: set up TTS and bridge listeners.
*/
cvox.ChromeVoxBackground.prototype.init = function() {
// In the case of ChromeOS, only continue initialization if this instance of
// ChromeVox is as we expect. This prevents ChromeVox from the webstore from
// running.
if (cvox.ChromeVox.isChromeOS &&
chrome.i18n.getMessage('@@extension_id') !=
'mndnfokpggljbaajbnioimlmbfngpief') {
return;
}
cvox.ChromeVox.msgs = new cvox.Msgs();
this.prefs = new cvox.ChromeVoxPrefs();
this.readPrefs();
var consoleTts = cvox.ConsoleTts.getInstance();
consoleTts.setEnabled(true);
/**
* Chrome's actual TTS which knows and cares about pitch, volume, etc.
* @type {cvox.TtsBackground}
* @private
*/
this.backgroundTts_ = new cvox.TtsBackground();
/**
* @type {cvox.TtsInterface}
*/
this.tts = new cvox.CompositeTts()
.add(this.backgroundTts_)
.add(consoleTts);
this.earcons = new cvox.EarconsBackground();
this.addBridgeListener();
/**
* The actual Braille service.
* @type {cvox.BrailleBackground}
* @private
*/
this.backgroundBraille_ = new cvox.BrailleBackground();
this.tabsApiHandler_ = new cvox.TabsApiHandler(
this.tts, this.backgroundBraille_, this.earcons);
// Export globals on cvox.ChromeVox.
cvox.ChromeVox.tts = this.tts;
cvox.ChromeVox.braille = this.backgroundBraille_;
cvox.ChromeVox.earcons = this.earcons;
if (cvox.ChromeVox.isChromeOS &&
chrome.accessibilityPrivate.onIntroduceChromeVox) {
chrome.accessibilityPrivate.onIntroduceChromeVox.addListener(
this.onIntroduceChromeVox);
}
// Set up a message passing system for goog.provide() calls from
// within the content scripts.
chrome.extension.onMessage.addListener(
function(request, sender, callback) {
if (request['srcFile']) {
var srcFile = request['srcFile'];
cvox.InjectedScriptLoader.fetchCode(
[srcFile],
function(code) {
callback({'code': code[srcFile]});
});
}
return true;
});
var self = this;
// Inject the content script into all running tabs.
chrome.windows.getAll({'populate': true}, function(windows) {
for (var i = 0; i < windows.length; i++) {
var tabs = windows[i].tabs;
self.injectChromeVoxIntoTabs(tabs);
}
});
if (localStorage['active'] == 'false') {
// Warn the user when the browser first starts if ChromeVox is inactive.
this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_inactive'),
cvox.QueueMode.QUEUE);
} else if (cvox.PlatformUtil.matchesPlatform(cvox.PlatformFilter.WML)) {
// Introductory message.
this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_intro'),
cvox.QueueMode.QUEUE);
cvox.ChromeVox.braille.write(cvox.NavBraille.fromText(
cvox.ChromeVox.msgs.getMsg('intro_brl')));
}
};
/**
* Inject ChromeVox into a tab.
* @param {Array<Tab>} tabs The tab where ChromeVox scripts should be injected.
*/
cvox.ChromeVoxBackground.prototype.injectChromeVoxIntoTabs = function(tabs) {
var listOfFiles;
// These lists of files must match the content_scripts section in
// the manifest files.
if (COMPILED) {
listOfFiles = ['chromeVoxChromePageScript.js'];
} else {
listOfFiles = [
'closure/closure_preinit.js',
'closure/base.js',
'deps.js',
'chromevox/injected/loader.js'];
}
var stageTwo = function(code) {
for (var i = 0, tab; tab = tabs[i]; i++) {
window.console.log('Injecting into ' + tab.id, tab);
var sawError = false;
/**
* A helper function which executes code.
* @param {string} code The code to execute.
*/
var executeScript = goog.bind(function(code) {
chrome.tabs.executeScript(
tab.id,
{'code': code,
'allFrames': true},
goog.bind(function() {
if (!chrome.extension.lastError) {
return;
}
if (sawError) {
return;
}
sawError = true;
console.error('Could not inject into tab', tab);
this.tts.speak('Error starting ChromeVox for ' +
tab.title + ', ' + tab.url, cvox.QueueMode.QUEUE);
}, this));
}, this);
// There is a scenario where two copies of the content script can get
// loaded into the same tab on browser startup - one automatically and one
// because the background page injects the content script into every tab
// on startup. To work around potential bugs resulting from this,
// ChromeVox exports a global function called disableChromeVox() that can
// be used here to disable any existing running instance before we inject
// a new instance of the content script into this tab.
//
// It's harmless if there wasn't a copy of ChromeVox already running.
//
// Also, set some variables so that Closure deps work correctly and so
// that ChromeVox knows not to announce feedback as if a page just loaded.
executeScript('try { window.disableChromeVox(); } catch(e) { }\n' +
'window.INJECTED_AFTER_LOAD = true;\n' +
'window.CLOSURE_NO_DEPS = true\n');
// Now inject the ChromeVox content script code into the tab.
listOfFiles.forEach(function(file) { executeScript(code[file]); });
}
};
// We use fetchCode instead of chrome.extensions.executeFile because
// executeFile doesn't propagate the file name to the content script
// which means that script is not visible in Dev Tools.
cvox.InjectedScriptLoader.fetchCode(listOfFiles, stageTwo);
};
/**
* Called when a TTS message is received from a page content script.
* @param {Object} msg The TTS message.
*/
cvox.ChromeVoxBackground.prototype.onTtsMessage = function(msg) {
if (msg['action'] == 'speak') {
this.tts.speak(msg['text'],
/** cvox.QueueMode */msg['queueMode'],
msg['properties']);
} else if (msg['action'] == 'stop') {
this.tts.stop();
} else if (msg['action'] == 'increaseOrDecrease') {
this.tts.increaseOrDecreaseProperty(msg['property'], msg['increase']);
var property = msg['property'];
var engine = this.backgroundTts_;
var valueAsPercent = Math.round(
this.backgroundTts_.propertyToPercentage(property) * 100);
var announcement;
switch (msg['property']) {
case cvox.AbstractTts.RATE:
announcement = cvox.ChromeVox.msgs.getMsg('announce_rate',
[valueAsPercent]);
break;
case cvox.AbstractTts.PITCH:
announcement = cvox.ChromeVox.msgs.getMsg('announce_pitch',
[valueAsPercent]);
break;
case cvox.AbstractTts.VOLUME:
announcement = cvox.ChromeVox.msgs.getMsg('announce_volume',
[valueAsPercent]);
break;
}
if (announcement) {
this.tts.speak(announcement,
cvox.QueueMode.FLUSH,
cvox.AbstractTts.PERSONALITY_ANNOTATION);
}
} else if (msg['action'] == 'cyclePunctuationEcho') {
this.tts.speak(cvox.ChromeVox.msgs.getMsg(
this.backgroundTts_.cyclePunctuationEcho()),
cvox.QueueMode.FLUSH);
}
};
/**
* Called when an earcon message is received from a page content script.
* @param {Object} msg The earcon message.
*/
cvox.ChromeVoxBackground.prototype.onEarconMessage = function(msg) {
if (msg.action == 'play') {
this.earcons.playEarcon(msg.earcon);
}
};
/**
* Listen for connections from our content script bridges, and dispatch the
* messages to the proper destination.
*/
cvox.ChromeVoxBackground.prototype.addBridgeListener = function() {
cvox.ExtensionBridge.addMessageListener(goog.bind(function(msg, port) {
var target = msg['target'];
var action = msg['action'];
switch (target) {
case 'OpenTab':
var destination = new Object();
destination.url = msg['url'];
chrome.tabs.create(destination);
break;
case 'KbExplorer':
var explorerPage = new Object();
explorerPage.url = 'chromevox/background/kbexplorer.html';
chrome.tabs.create(explorerPage);
break;
case 'HelpDocs':
var helpPage = new Object();
helpPage.url = 'http://chromevox.com/tutorial/index.html';
chrome.tabs.create(helpPage);
break;
case 'Options':
if (action == 'open') {
var optionsPage = new Object();
optionsPage.url = 'chromevox/background/options.html';
chrome.tabs.create(optionsPage);
}
break;
case 'Data':
if (action == 'getHistory') {
var results = {};
chrome.history.search({text: '', maxResults: 25}, function(items) {
items.forEach(function(item) {
if (item.url) {
results[item.url] = true;
}
});
port.postMessage({
'history': results
});
});
}
break;
case 'Prefs':
if (action == 'getPrefs') {
this.prefs.sendPrefsToPort(port);
} else if (action == 'setPref') {
if (msg['pref'] == 'active' &&
msg['value'] != cvox.ChromeVox.isActive) {
if (cvox.ChromeVox.isActive) {
this.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_inactive'),
cvox.QueueMode.FLUSH);
chrome.accessibilityPrivate.setNativeAccessibilityEnabled(
true);
} else {
chrome.accessibilityPrivate.setNativeAccessibilityEnabled(
false);
}
} else if (msg['pref'] == 'earcons') {
this.earcons.enabled = msg['value'];
} else if (msg['pref'] == 'sticky' && msg['announce']) {
if (msg['value']) {
this.tts.speak(cvox.ChromeVox.msgs.getMsg('sticky_mode_enabled'),
cvox.QueueMode.QUEUE);
} else {
this.tts.speak(
cvox.ChromeVox.msgs.getMsg('sticky_mode_disabled'),
cvox.QueueMode.QUEUE);
}
} else if (msg['pref'] == 'typingEcho' && msg['announce']) {
var announce = '';
switch (msg['value']) {
case cvox.TypingEcho.CHARACTER:
announce = cvox.ChromeVox.msgs.getMsg('character_echo');
break;
case cvox.TypingEcho.WORD:
announce = cvox.ChromeVox.msgs.getMsg('word_echo');
break;
case cvox.TypingEcho.CHARACTER_AND_WORD:
announce = cvox.ChromeVox.msgs.getMsg('character_and_word_echo');
break;
case cvox.TypingEcho.NONE:
announce = cvox.ChromeVox.msgs.getMsg('none_echo');
break;
default:
break;
}
if (announce) {
this.tts.speak(announce, cvox.QueueMode.QUEUE);
}
} else if (msg['pref'] == 'brailleCaptions') {
cvox.BrailleCaptionsBackground.setActive(msg['value']);
}
this.prefs.setPref(msg['pref'], msg['value']);
this.readPrefs();
}
break;
case 'Math':
// TODO (sorge): Put the change of styles etc. here!
if (msg['action'] == 'getDomains') {
port.postMessage({'message': 'DOMAINS_STYLES',
'domains': this.backgroundTts_.mathmap.allDomains,
'styles': this.backgroundTts_.mathmap.allStyles});
}
break;
case 'TTS':
if (msg['startCallbackId'] != undefined) {
msg['properties']['startCallback'] = function(opt_cleanupOnly) {
port.postMessage({'message': 'TTS_CALLBACK',
'cleanupOnly': opt_cleanupOnly,
'id': msg['startCallbackId']});
};
}
if (msg['endCallbackId'] != undefined) {
msg['properties']['endCallback'] = function(opt_cleanupOnly) {
port.postMessage({'message': 'TTS_CALLBACK',
'cleanupOnly': opt_cleanupOnly,
'id': msg['endCallbackId']});
};
}
try {
this.onTtsMessage(msg);
} catch (err) {
console.log(err);
}
break;
case 'EARCON':
this.onEarconMessage(msg);
break;
case 'BRAILLE':
try {
this.backgroundBraille_.onBrailleMessage(msg);
} catch (err) {
console.log(err);
}
break;
}
}, this));
};
/**
* Read and apply preferences that affect the background context.
*/
cvox.ChromeVoxBackground.prototype.readPrefs = function() {
var prefs = this.prefs.getPrefs();
cvox.ChromeVoxEditableTextBase.useIBeamCursor =
(prefs['useIBeamCursor'] == 'true');
cvox.ChromeVox.isActive =
(prefs['active'] == 'true' || cvox.ChromeVox.isChromeOS);
cvox.ChromeVox.isStickyPrefOn = (prefs['sticky'] == 'true');
};
/**
* Checks if we are currently in an incognito window.
* @return {boolean} True if incognito or not within a tab context, false
* otherwise.
* @private
*/
cvox.ChromeVoxBackground.prototype.isIncognito_ = function() {
var incognito = false;
chrome.tabs.getCurrent(function(tab) {
// Tab is null if not called from a tab context. In that case, also consider
// it incognito.
incognito = tab ? tab.incognito : true;
});
return incognito;
};
/**
* Handles the onIntroduceChromeVox event.
*/
cvox.ChromeVoxBackground.prototype.onIntroduceChromeVox = function() {
cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('chromevox_intro'),
cvox.QueueMode.QUEUE,
{doNotInterrupt: true});
cvox.ChromeVox.braille.write(cvox.NavBraille.fromText(
cvox.ChromeVox.msgs.getMsg('intro_brl')));
};
// Create the background page object and export a function window['speak']
// so that other background pages can access it. Also export the prefs object
// for access by the options page.
(function() {
var background = new cvox.ChromeVoxBackground();
background.init();
window['speak'] = goog.bind(background.tts.speak, background.tts);
// Export the prefs object for access by the options page.
window['prefs'] = background.prefs;
// Export the braille translator manager for access by the options page.
window['braille_translator_manager'] =
background.backgroundBraille_.getTranslatorManager();
// Export injection for ChromeVox Next.
cvox.ChromeVox.injectChromeVoxIntoTabs =
background.injectChromeVoxIntoTabs.bind(background);
})();