blob: 722c7e0b150a2fea4ec12a79c1dd9688f7bfbad0 [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 Bridge to aid in communication between a Chrome
* background page and content script.
*
* It automatically figures out where it's being run and initializes itself
* appropriately. Then just call send() to send a message from the background
* to the page or vice versa, and addMessageListener() to provide a message
* listener. Messages can be any object that can be serialized using JSON.
*
*/
goog.provide('cvox.ExtensionBridge');
goog.require('cvox.ChromeVoxJSON');
/**
* @constructor
*/
cvox.ExtensionBridge = function() {};
/**
* Initialize the extension bridge. Dynamically figure out whether we're in
* the background page, content script, or in a page, and call the
* corresponding function for more specific initialization.
*/
cvox.ExtensionBridge.init = function() {
var self = cvox.ExtensionBridge;
self.messageListeners = [];
self.disconnectListeners = [];
if (/^chrome-extension:\/\/.*background\.html$/.test(window.location.href)) {
// This depends on the fact that the background page has a specific url. We
// should never be loaded into another extension's background page, so this
// is a safe check.
self.context = self.BACKGROUND;
self.initBackground();
return;
}
if (chrome && chrome.extension) {
self.context = self.CONTENT_SCRIPT;
self.initContentScript();
}
};
/**
* Constant indicating we're in a background page.
* @type {number}
* @const
*/
cvox.ExtensionBridge.BACKGROUND = 0;
/**
* Constant indicating we're in a content script.
* @type {number}
* @const
*/
cvox.ExtensionBridge.CONTENT_SCRIPT = 1;
/**
* The name of the port between the content script and background page.
* @type {string}
* @const
*/
cvox.ExtensionBridge.PORT_NAME = 'cvox.ExtensionBridge.Port';
/**
* The name of the message between the content script and background to
* see if they're connected.
* @type {string}
* @const
*/
cvox.ExtensionBridge.PING_MSG = 'cvox.ExtensionBridge.Ping';
/**
* The name of the message between the background and content script to
* confirm that they're connected.
* @type {string}
* @const
*/
cvox.ExtensionBridge.PONG_MSG = 'cvox.ExtensionBridge.Pong';
/**
* Send a message. If the context is a page, sends a message to the
* extension background page. If the context is a background page, sends
* a message to the current active tab (not all tabs).
*
* @param {Object} message The message to be sent.
*/
cvox.ExtensionBridge.send = function(message) {
var self = cvox.ExtensionBridge;
switch (self.context) {
case self.BACKGROUND:
self.sendBackgroundToContentScript(message);
break;
case self.CONTENT_SCRIPT:
self.sendContentScriptToBackground(message);
break;
}
};
/**
* Provide a function to listen to messages. In page context, this
* listens to messages from the background. In background context,
* this listens to messages from all pages.
*
* The function gets called with two parameters: the message, and a
* port that can be used to send replies.
*
* @param {function(Object, Port)} listener The message listener.
*/
cvox.ExtensionBridge.addMessageListener = function(listener) {
cvox.ExtensionBridge.messageListeners.push(listener);
};
/**
* Provide a function to be called when the connection is
* disconnected.
*
* @param {function()} listener The listener.
*/
cvox.ExtensionBridge.addDisconnectListener = function(listener) {
cvox.ExtensionBridge.disconnectListeners.push(listener);
};
/**
* Removes all message listeners from the extension bridge.
*/
cvox.ExtensionBridge.removeMessageListeners = function() {
cvox.ExtensionBridge.messageListeners.length = 0;
};
/**
* Returns a unique id for this instance of the script.
*
* @return {number}
*/
cvox.ExtensionBridge.uniqueId = function() {
return cvox.ExtensionBridge.id_;
};
/**
* Initialize the extension bridge in a background page context by registering
* a listener for connections from the content script.
*/
cvox.ExtensionBridge.initBackground = function() {
var self = cvox.ExtensionBridge;
/** @type {!Array.<Port>} @private */
self.portCache_ = [];
/** @type {number} */
self.nextPongId_ = 1;
/** @type {number} */
self.id_ = 0;
var onConnectHandler = function(port) {
if (port.name != self.PORT_NAME) {
return;
}
self.portCache_.push(port);
port.onMessage.addListener(
function(message) {
if (message[cvox.ExtensionBridge.PING_MSG]) {
var pongMessage = {};
pongMessage[cvox.ExtensionBridge.PONG_MSG] = self.nextPongId_++;
port.postMessage(pongMessage);
return;
}
for (var i = 0; i < self.messageListeners.length; i++) {
self.messageListeners[i](message, port);
}
});
port.onDisconnect.addListener(function(message) {
for (var i = 0; i < self.portCache_.length; i++) {
if (self.portCache_[i] == port) {
self.portCache_.splice(i, 1);
break;
}
}
});
};
chrome.extension.onConnect.addListener(onConnectHandler);
};
/**
* Initialize the extension bridge in a content script context, listening
* for messages from the background page.
*/
cvox.ExtensionBridge.initContentScript = function() {
var self = cvox.ExtensionBridge;
self.connected = false;
self.pingAttempts = 0;
self.queuedMessages = [];
/** @type {number} */
self.id_ = -1;
var onMessageHandler = function(request, sender, sendResponse) {
if (request && request['srcFile']) {
// TODO (clchen, deboer): Investigate this further and come up with a
// cleaner solution. The root issue is that this should never be run on
// the background page, but it is in the Chrome OS case.
return;
}
if (request[cvox.ExtensionBridge.PONG_MSG]) {
self.gotPongFromBackgroundPage(request[cvox.ExtensionBridge.PONG_MSG]);
} else {
for (var i = 0; i < self.messageListeners.length; i++) {
self.messageListeners[i](request, cvox.ExtensionBridge.backgroundPort);
}
}
sendResponse({});
};
// Listen to requests from the background that don't come from
// our connection port.
chrome.extension.onMessage.addListener(onMessageHandler);
self.setupBackgroundPort();
self.tryToPingBackgroundPage();
};
/**
* Set up the connection to the background page.
*/
cvox.ExtensionBridge.setupBackgroundPort = function() {
// Set up the connection to the background page.
var self = cvox.ExtensionBridge;
self.backgroundPort = chrome.extension.connect({name: self.PORT_NAME});
self.backgroundPort.onMessage.addListener(
function(message) {
if (message[cvox.ExtensionBridge.PONG_MSG]) {
self.gotPongFromBackgroundPage(
message[cvox.ExtensionBridge.PONG_MSG]);
} else {
for (var i = 0; i < self.messageListeners.length; i++) {
self.messageListeners[i](message, self.backgroundPort);
}
}
});
self.backgroundPort.onDisconnect.addListener(
function(event) {
// If we're not connected yet, don't give up - try again.
if (!self.connected) {
self.backgroundPort = null;
return;
}
for (var i = 0; i < self.disconnectListeners.length; i++) {
self.disconnectListeners[i]();
}
});
};
/**
* Try to ping the background page.
*/
cvox.ExtensionBridge.tryToPingBackgroundPage = function() {
var self = cvox.ExtensionBridge;
// If we already got a pong, great - we're done.
if (self.connected) {
return;
}
self.pingAttempts++;
if (self.pingAttempts > 5) {
// Could not connect after 5 ping attempts. Call the disconnect
// handlers, which will disable ChromeVox.
for (var i = 0; i < self.disconnectListeners.length; i++) {
self.disconnectListeners[i]();
}
return;
}
// Send the ping.
var msg = {};
msg[cvox.ExtensionBridge.PING_MSG] = 1;
if (!self.backgroundPort) {
self.setupBackgroundPort();
}
self.backgroundPort.postMessage(msg);
// Check again in 500 ms in case we get no response.
window.setTimeout(cvox.ExtensionBridge.tryToPingBackgroundPage, 500);
};
/**
* Got pong from the background page, now we know the connection was
* successful.
* @param {number} pongId unique id assigned to us by the background page
*/
cvox.ExtensionBridge.gotPongFromBackgroundPage = function(pongId) {
var self = cvox.ExtensionBridge;
self.connected = true;
self.id_ = pongId;
while (self.queuedMessages.length > 0) {
self.sendContentScriptToBackground(self.queuedMessages.shift());
}
};
/**
* Send a message from the content script to the background page.
*
* @param {Object} message The message to send.
*/
cvox.ExtensionBridge.sendContentScriptToBackground = function(message) {
var self = cvox.ExtensionBridge;
if (!self.connected) {
// We're not connected to the background page, so queue this message
// until we're connected.
self.queuedMessages.push(message);
return;
}
if (cvox.ExtensionBridge.backgroundPort) {
cvox.ExtensionBridge.backgroundPort.postMessage(message);
} else {
chrome.extension.sendMessage(message);
}
};
/**
* Send a message from the background page to the content script of the
* current selected tab.
*
* @param {Object} message The message to send.
*/
cvox.ExtensionBridge.sendBackgroundToContentScript = function(message) {
chrome.tabs.query(
{'active': true, 'lastFocusedWindow': true},
function(tabs) {
if (tabs && tabs.length > 0) {
chrome.tabs.sendMessage(tabs[0].id, message);
} else {
cvox.ExtensionBridge.portCache_.forEach(function(port) {
port.postMessage(message);
});
}
});
};
cvox.ExtensionBridge.init();