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// Custom bindings for the automation API. 6var AutomationNode = require('automationNode').AutomationNode; 7var AutomationRootNode = require('automationNode').AutomationRootNode; 8var automation = require('binding').Binding.create('automation'); 9var automationInternal = 10 require('binding').Binding.create('automationInternal').generate(); 11var eventBindings = require('event_bindings'); 12var Event = eventBindings.Event; 13var forEach = require('utils').forEach; 14var lastError = require('lastError'); 15var schema = requireNative('automationInternal').GetSchemaAdditions(); 16 17// TODO(aboxhall): Look into using WeakMap 18var idToAutomationRootNode = {}; 19var idToCallback = {}; 20 21// TODO(dtseng): Move out to automation/automation_util.js or as a static member 22// of AutomationRootNode to keep this file clean. 23/* 24 * Creates an id associated with a particular AutomationRootNode based upon a 25 * renderer/renderer host pair's process and routing id. 26 */ 27var createAutomationRootNodeID = function(pid, rid) { 28 return pid + '_' + rid; 29}; 30 31var DESKTOP_TREE_ID = createAutomationRootNodeID(0, 0); 32 33automation.registerCustomHook(function(bindingsAPI) { 34 var apiFunctions = bindingsAPI.apiFunctions; 35 36 // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj. 37 apiFunctions.setHandleRequest('getTree', function getTree(tabId, callback) { 38 // enableTab() ensures the renderer for the active or specified tab has 39 // accessibility enabled, and fetches its process and routing ids to use as 40 // a key in the idToAutomationRootNode map. The callback to enableTab is is 41 // bound to the callback passed in to getTree(), so that once the tree is 42 // available (either due to having been cached earlier, or after an 43 // accessibility event occurs which causes the tree to be populated), the 44 // callback can be called. 45 automationInternal.enableTab(tabId, function onEnable(pid, rid) { 46 if (lastError.hasError(chrome)) { 47 callback(); 48 return; 49 } 50 var id = createAutomationRootNodeID(pid, rid); 51 var targetTree = idToAutomationRootNode[id]; 52 if (!targetTree) { 53 // If we haven't cached the tree, hold the callback until the tree is 54 // populated by the initial onAccessibilityEvent call. 55 if (id in idToCallback) 56 idToCallback[id].push(callback); 57 else 58 idToCallback[id] = [callback]; 59 } else { 60 callback(targetTree); 61 } 62 }); 63 }); 64 65 var desktopTree = null; 66 apiFunctions.setHandleRequest('getDesktop', function(callback) { 67 desktopTree = idToAutomationRootNode[DESKTOP_TREE_ID]; 68 if (!desktopTree) { 69 if (DESKTOP_TREE_ID in idToCallback) 70 idToCallback[DESKTOP_TREE_ID].push(callback); 71 else 72 idToCallback[DESKTOP_TREE_ID] = [callback]; 73 74 // TODO(dtseng): Disable desktop tree once desktop object goes out of 75 // scope. 76 automationInternal.enableDesktop(function() { 77 if (lastError.hasError(chrome)) { 78 delete idToAutomationRootNode[DESKTOP_TREE_ID]; 79 callback(); 80 return; 81 } 82 }); 83 } else { 84 callback(desktopTree); 85 } 86 }); 87}); 88 89// Listen to the automationInternal.onAccessibilityEvent event, which is 90// essentially a proxy for the AccessibilityHostMsg_Events IPC from the 91// renderer. 92automationInternal.onAccessibilityEvent.addListener(function(data) { 93 var pid = data.processID; 94 var rid = data.routingID; 95 var id = createAutomationRootNodeID(pid, rid); 96 var targetTree = idToAutomationRootNode[id]; 97 if (!targetTree) { 98 // If this is the first time we've gotten data for this tree, it will 99 // contain all of the tree's data, so create a new tree which will be 100 // bootstrapped from |data|. 101 targetTree = new AutomationRootNode(pid, rid); 102 idToAutomationRootNode[id] = targetTree; 103 } 104 if (!privates(targetTree).impl.onAccessibilityEvent(data)) 105 return; 106 107 // If we're not waiting on a callback to getTree(), we can early out here. 108 if (!(id in idToCallback)) 109 return; 110 111 // We usually get a 'placeholder' tree first, which doesn't have any url 112 // attribute or child nodes. If we've got that, wait for the full tree before 113 // calling the callback. 114 // TODO(dmazzoni): Don't send down placeholder (crbug.com/397553) 115 if (id != DESKTOP_TREE_ID && !targetTree.attributes.url && 116 targetTree.children.length == 0) { 117 return; 118 } 119 120 // If the tree wasn't available when getTree() was called, the callback will 121 // have been cached in idToCallback, so call and delete it now that we 122 // have the complete tree. 123 for (var i = 0; i < idToCallback[id].length; i++) { 124 console.log('calling getTree() callback'); 125 var callback = idToCallback[id][i]; 126 callback(targetTree); 127 } 128 delete idToCallback[id]; 129}); 130 131automationInternal.onAccessibilityTreeDestroyed.addListener(function(pid, rid) { 132 var id = createAutomationRootNodeID(pid, rid); 133 var targetTree = idToAutomationRootNode[id]; 134 if (targetTree) { 135 privates(targetTree).impl.destroy(); 136 delete idToAutomationRootNode[id]; 137 } else { 138 logging.WARNING('no targetTree to destroy'); 139 } 140 delete idToAutomationRootNode[id]; 141}); 142 143exports.binding = automation.generate(); 144 145// Add additional accessibility bindings not specified in the automation IDL. 146// Accessibility and automation share some APIs (see 147// ui/accessibility/ax_enums.idl). 148forEach(schema, function(k, v) { 149 exports.binding[k] = v; 150}); 151