// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Custom bindings for the automation API.
const AutomationRootNode = require('automationNode').AutomationRootNode;
const automationInternal = getInternalApi('automationInternal');
const exceptionHandler = require('uncaught_exception_handler');
const logging = requireNative('logging');
const nativeAutomationInternal = requireNative('automationInternal');
const DestroyAccessibilityTree =
nativeAutomationInternal.DestroyAccessibilityTree;
const StartCachingAccessibilityTrees =
nativeAutomationInternal.StartCachingAccessibilityTrees;
const StopCachingAccessibilityTrees =
nativeAutomationInternal.StopCachingAccessibilityTrees;
const AddTreeChangeObserver = nativeAutomationInternal.AddTreeChangeObserver;
const RemoveTreeChangeObserver =
nativeAutomationInternal.RemoveTreeChangeObserver;
const GetFocusNative = nativeAutomationInternal.GetFocus;
const GetAccessibilityFocusNative =
nativeAutomationInternal.GetAccessibilityFocus;
const SetDesktopID = nativeAutomationInternal.SetDesktopID;
// A namespace to export utility functions to other files in automation.
const automationUtil = function() {};
// TODO(aboxhall): Look into using WeakMap
let idToCallback = {};
let desktopId;
let desktopTree;
automationUtil.storeTreeCallback = function(id, callback) {
if (!callback) {
return;
}
const targetTree = AutomationRootNode.get(id);
if (!targetTree) {
// If we haven't cached the tree, hold the callback until the tree is
// populated by the initial onAccessibilityEvent call.
if (id in idToCallback) {
idToCallback[id].push(callback);
} else {
idToCallback[id] = [callback];
}
} else {
callback(targetTree);
}
};
/**
* Global list of tree change observers.
* @type {Object<number, TreeChangeObserver>}
*/
automationUtil.treeChangeObserverMap = {};
/**
* The id of the next tree change observer.
* @type {number}
*/
automationUtil.nextTreeChangeObserverId = 1;
apiBridge.registerCustomHook(function(bindingsAPI) {
const apiFunctions = bindingsAPI.apiFunctions;
apiFunctions.setHandleRequest('getDesktop', function(callback) {
StartCachingAccessibilityTrees();
if (desktopId !== undefined) {
desktopTree = AutomationRootNode.get(desktopId);
}
if (!desktopTree) {
automationInternal.enableDesktop(function(treeId) {
if (bindingUtil.hasLastError()) {
AutomationRootNode.destroy(treeId);
desktopId = undefined;
SetDesktopID('');
callback();
return;
}
desktopId = treeId;
SetDesktopID(desktopId);
desktopTree = AutomationRootNode.getOrCreate(desktopId);
callback(desktopTree);
// TODO(dtseng): Disable desktop tree once desktop object goes out of
// scope.
});
} else {
callback(desktopTree);
}
});
apiFunctions.setHandleRequest('getFocus', function(callback) {
const focusedNodeInfo = GetFocusNative();
if (!focusedNodeInfo) {
callback(null);
return;
}
const tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
if (tree) {
callback(privates(tree).impl.get(focusedNodeInfo.nodeId));
return;
}
});
apiFunctions.setHandleRequest('getAccessibilityFocus', function(callback) {
const focusedNodeInfo = GetAccessibilityFocusNative();
if (!focusedNodeInfo) {
callback(null);
return;
}
const tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
if (tree) {
callback(privates(tree).impl.get(focusedNodeInfo.nodeId));
}
});
function removeTreeChangeObserver(observer) {
for (const id in automationUtil.treeChangeObserverMap) {
if (automationUtil.treeChangeObserverMap[id] == observer) {
RemoveTreeChangeObserver(id);
delete automationUtil.treeChangeObserverMap[id];
return;
}
}
}
apiFunctions.setHandleRequest('removeTreeChangeObserver', function(observer) {
removeTreeChangeObserver(observer);
});
function addTreeChangeObserver(filter, observer) {
removeTreeChangeObserver(observer);
const id = automationUtil.nextTreeChangeObserverId++;
AddTreeChangeObserver(id, filter);
automationUtil.treeChangeObserverMap[id] = observer;
}
apiFunctions.setHandleRequest('addTreeChangeObserver',
function(filter, observer) {
addTreeChangeObserver(filter, observer);
});
apiFunctions.setHandleRequest('setDocumentSelection', function(params) {
const anchorNodeImpl = privates(params.anchorObject).impl;
const focusNodeImpl = privates(params.focusObject).impl;
if (anchorNodeImpl.treeID !== focusNodeImpl.treeID) {
throw new Error('Selection anchor and focus must be in the same tree.');
}
if (anchorNodeImpl.treeID === desktopId) {
throw new Error('Use AutomationNode.setSelection to set the selection ' +
'in the desktop tree.');
}
automationInternal.performAction(
{
treeID: anchorNodeImpl.treeID,
automationNodeID: anchorNodeImpl.id,
actionType: 'setSelection',
},
{
focusNodeID: focusNodeImpl.id,
anchorOffset: params.anchorOffset,
focusOffset: params.focusOffset,
});
});
});
automationInternal.onChildTreeID.addListener(function(childTreeId) {
const targetTree = AutomationRootNode.get(childTreeId);
// If the tree is already loded, or if we previously requested it be loaded
// (i.e. have a callback for it), don't try to do so again.
if (targetTree || idToCallback[childTreeId]) {
return;
}
// A WebView in the desktop tree has a different AX tree as its child.
// When we encounter a WebView with a child AX tree id that we don't
// currently have cached, explicitly request that AX tree from the
// browser process and set up a callback when it loads to attach that
// tree as a child of this node and fire appropriate events.
automationUtil.storeTreeCallback(childTreeId, function(root) {
const rootImpl = privates(root).impl;
rootImpl.dispatchEvent('loadComplete', 'page');
if (rootImpl.parent) {
privates(rootImpl.parent).impl.dispatchEvent('childrenChanged');
}
}, true);
automationInternal.enableTree(childTreeId);
});
automationInternal.onTreeChange.addListener(function(
observerID, treeID, nodeID, changeType) {
const tree = AutomationRootNode.getOrCreate(treeID);
if (!tree) {
return;
}
const node = privates(tree).impl.get(nodeID);
if (!node) {
return;
}
const observer = automationUtil.treeChangeObserverMap[observerID];
if (!observer) {
return;
}
try {
observer({target: node, type: changeType});
} catch (e) {
exceptionHandler.handle('Error in tree change observer for ' +
changeType, e);
}
});
automationInternal.onNodesRemoved.addListener(function(treeID, nodeIDs) {
const tree = AutomationRootNode.getOrCreate(treeID);
if (!tree) {
return;
}
for (let i = 0; i < nodeIDs.length; i++) {
privates(tree).impl.remove(nodeIDs[i]);
}
});
automationInternal.onAllAutomationEventListenersRemoved.addListener(() => {
if (!desktopId) {
return;
}
automationInternal.disableDesktop(() => {
desktopId = undefined;
desktopTree = undefined;
idToCallback = {};
AutomationRootNode.destroyAll();
StopCachingAccessibilityTrees();
});
});
/**
* Dispatch accessibility events fired on individual nodes to its
* corresponding AutomationNode.
*/
automationInternal.onAccessibilityEvent.addListener(function(eventParams) {
const id = eventParams.treeID;
const targetTree = AutomationRootNode.getOrCreate(id);
if (eventParams.eventType == 'mediaStartedPlaying' ||
eventParams.eventType == 'mediaStoppedPlaying') {
// These events are global to the tree.
eventParams.targetID = privates(targetTree).impl.id;
}
privates(targetTree).impl.onAccessibilityEvent(eventParams);
// If we're not waiting on a callback, we can early out here.
if (!(id in idToCallback)) {
return;
}
// We usually get a 'placeholder' tree first, which doesn't have any url
// attribute or child nodes. If we've got that, wait for the full tree before
// calling the callback.
// TODO(dmazzoni): Don't send down placeholder (crbug.com/397553)
if (id != desktopId && !targetTree.url && targetTree.children.length == 0) {
return;
}
// If the tree wasn't available, the callback will have been cached in
// idToCallback, so call and delete it now that we have the complete tree.
for (let i = 0; i < idToCallback[id].length; i++) {
const callback = idToCallback[id][i];
callback(targetTree);
}
delete idToCallback[id];
});
automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) {
// Destroy the AutomationRootNode.
const targetTree = AutomationRootNode.get(id);
if (targetTree) {
privates(targetTree).impl.destroy();
AutomationRootNode.destroy(id);
} else {
logging.WARNING('no targetTree to destroy');
}
// Destroy the native cache of the accessibility tree.
DestroyAccessibilityTree(id);
});
automationInternal.onAccessibilityTreeSerializationError.addListener(
function(id) {
automationInternal.enableTree(id);
});
automationInternal.onActionResult.addListener(function(
treeID, requestID, result) {
const targetTree = AutomationRootNode.get(treeID);
if (!targetTree) {
return;
}
privates(targetTree).impl.onActionResult(requestID, result);
});
automationInternal.onGetTextLocationResult.addListener(function(
textLocationParams) {
const targetTree = AutomationRootNode.get(textLocationParams.treeID);
if (!targetTree) {
return;
}
privates(targetTree).impl.onGetTextLocationResult(textLocationParams);
});