OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 // Custom bindings for the automation API. | 5 // Custom bindings for the automation API. |
6 var AutomationNode = require('automationNode').AutomationNode; | 6 var AutomationNode = require('automationNode').AutomationNode; |
7 var AutomationRootNode = require('automationNode').AutomationRootNode; | 7 var AutomationRootNode = require('automationNode').AutomationRootNode; |
8 var automation = require('binding').Binding.create('automation'); | 8 var automation = require('binding').Binding.create('automation'); |
9 var automationInternal = | 9 var automationInternal = |
10 require('binding').Binding.create('automationInternal').generate(); | 10 require('binding').Binding.create('automationInternal').generate(); |
11 var eventBindings = require('event_bindings'); | 11 var eventBindings = require('event_bindings'); |
12 var Event = eventBindings.Event; | 12 var Event = eventBindings.Event; |
13 var forEach = require('utils').forEach; | 13 var forEach = require('utils').forEach; |
14 var lastError = require('lastError'); | 14 var lastError = require('lastError'); |
15 var logging = requireNative('logging'); | 15 var logging = requireNative('logging'); |
16 var nativeAutomationInternal = requireNative('automationInternal'); | 16 var nativeAutomationInternal = requireNative('automationInternal'); |
17 var GetRoutingID = nativeAutomationInternal.GetRoutingID; | 17 var GetRoutingID = nativeAutomationInternal.GetRoutingID; |
18 var GetSchemaAdditions = nativeAutomationInternal.GetSchemaAdditions; | 18 var GetSchemaAdditions = nativeAutomationInternal.GetSchemaAdditions; |
19 var DestroyAccessibilityTree = | |
20 nativeAutomationInternal.DestroyAccessibilityTree; | |
21 var GetIntAttribute = nativeAutomationInternal.GetIntAttribute; | |
22 var StartCachingAccessibilityTrees = | |
23 nativeAutomationInternal.StartCachingAccessibilityTrees; | |
24 var schema = GetSchemaAdditions(); | 19 var schema = GetSchemaAdditions(); |
25 | 20 |
26 /** | 21 /** |
27 * A namespace to export utility functions to other files in automation. | 22 * A namespace to export utility functions to other files in automation. |
28 */ | 23 */ |
29 window.automationUtil = function() {}; | 24 window.automationUtil = function() {}; |
30 | 25 |
31 // TODO(aboxhall): Look into using WeakMap | 26 // TODO(aboxhall): Look into using WeakMap |
| 27 var idToAutomationRootNode = {}; |
32 var idToCallback = {}; | 28 var idToCallback = {}; |
33 | 29 |
34 var DESKTOP_TREE_ID = 0; | 30 var DESKTOP_TREE_ID = 0; |
35 | 31 |
36 automationUtil.storeTreeCallback = function(id, callback) { | 32 automationUtil.storeTreeCallback = function(id, callback) { |
37 if (!callback) | 33 if (!callback) |
38 return; | 34 return; |
39 | 35 |
40 var targetTree = AutomationRootNode.get(id); | 36 var targetTree = idToAutomationRootNode[id]; |
41 if (!targetTree) { | 37 if (!targetTree) { |
42 // If we haven't cached the tree, hold the callback until the tree is | 38 // If we haven't cached the tree, hold the callback until the tree is |
43 // populated by the initial onAccessibilityEvent call. | 39 // populated by the initial onAccessibilityEvent call. |
44 if (id in idToCallback) | 40 if (id in idToCallback) |
45 idToCallback[id].push(callback); | 41 idToCallback[id].push(callback); |
46 else | 42 else |
47 idToCallback[id] = [callback]; | 43 idToCallback[id] = [callback]; |
48 } else { | 44 } else { |
49 callback(targetTree); | 45 callback(targetTree); |
50 } | 46 } |
51 }; | 47 }; |
52 | 48 |
53 /** | 49 /** |
54 * Global list of tree change observers. | 50 * Global list of tree change observers. |
55 * @type {Array<TreeChangeObserver>} | 51 * @type {Array<TreeChangeObserver>} |
56 */ | 52 */ |
57 automationUtil.treeChangeObservers = []; | 53 automationUtil.treeChangeObservers = []; |
58 | 54 |
59 automation.registerCustomHook(function(bindingsAPI) { | 55 automation.registerCustomHook(function(bindingsAPI) { |
60 var apiFunctions = bindingsAPI.apiFunctions; | 56 var apiFunctions = bindingsAPI.apiFunctions; |
61 | 57 |
62 // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj. | 58 // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj. |
63 apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) { | 59 apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) { |
64 var routingID = GetRoutingID(); | 60 var routingID = GetRoutingID(); |
65 StartCachingAccessibilityTrees(); | |
66 | 61 |
67 // enableTab() ensures the renderer for the active or specified tab has | 62 // enableTab() ensures the renderer for the active or specified tab has |
68 // accessibility enabled, and fetches its ax tree id to use as | 63 // accessibility enabled, and fetches its ax tree id to use as |
69 // a key in the idToAutomationRootNode map. The callback to | 64 // a key in the idToAutomationRootNode map. The callback to |
70 // enableTab is bound to the callback passed in to getTree(), so that once | 65 // enableTab is bound to the callback passed in to getTree(), so that once |
71 // the tree is available (either due to having been cached earlier, or after | 66 // the tree is available (either due to having been cached earlier, or after |
72 // an accessibility event occurs which causes the tree to be populated), the | 67 // an accessibility event occurs which causes the tree to be populated), the |
73 // callback can be called. | 68 // callback can be called. |
74 var params = { routingID: routingID, tabID: tabID }; | 69 var params = { routingID: routingID, tabID: tabID }; |
75 automationInternal.enableTab(params, | 70 automationInternal.enableTab(params, |
76 function onEnable(id) { | 71 function onEnable(id) { |
77 if (lastError.hasError(chrome)) { | 72 if (lastError.hasError(chrome)) { |
78 callback(); | 73 callback(); |
79 return; | 74 return; |
80 } | 75 } |
81 automationUtil.storeTreeCallback(id, callback); | 76 automationUtil.storeTreeCallback(id, callback); |
82 }); | 77 }); |
83 }); | 78 }); |
84 | 79 |
85 var desktopTree = null; | 80 var desktopTree = null; |
86 apiFunctions.setHandleRequest('getDesktop', function(callback) { | 81 apiFunctions.setHandleRequest('getDesktop', function(callback) { |
87 StartCachingAccessibilityTrees(); | 82 desktopTree = |
88 desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID); | 83 idToAutomationRootNode[DESKTOP_TREE_ID]; |
89 if (!desktopTree) { | 84 if (!desktopTree) { |
90 if (DESKTOP_TREE_ID in idToCallback) | 85 if (DESKTOP_TREE_ID in idToCallback) |
91 idToCallback[DESKTOP_TREE_ID].push(callback); | 86 idToCallback[DESKTOP_TREE_ID].push(callback); |
92 else | 87 else |
93 idToCallback[DESKTOP_TREE_ID] = [callback]; | 88 idToCallback[DESKTOP_TREE_ID] = [callback]; |
94 | 89 |
95 var routingID = GetRoutingID(); | 90 var routingID = GetRoutingID(); |
96 | 91 |
97 // TODO(dtseng): Disable desktop tree once desktop object goes out of | 92 // TODO(dtseng): Disable desktop tree once desktop object goes out of |
98 // scope. | 93 // scope. |
99 automationInternal.enableDesktop(routingID, function() { | 94 automationInternal.enableDesktop(routingID, function() { |
100 if (lastError.hasError(chrome)) { | 95 if (lastError.hasError(chrome)) { |
101 AutomationRootNode.destroy(DESKTOP_TREE_ID); | 96 delete idToAutomationRootNode[ |
| 97 DESKTOP_TREE_ID]; |
102 callback(); | 98 callback(); |
103 return; | 99 return; |
104 } | 100 } |
105 }); | 101 }); |
106 } else { | 102 } else { |
107 callback(desktopTree); | 103 callback(desktopTree); |
108 } | 104 } |
109 }); | 105 }); |
110 | 106 |
111 function removeTreeChangeObserver(observer) { | 107 function removeTreeChangeObserver(observer) { |
(...skipping 10 matching lines...) Expand all Loading... |
122 function addTreeChangeObserver(observer) { | 118 function addTreeChangeObserver(observer) { |
123 removeTreeChangeObserver(observer); | 119 removeTreeChangeObserver(observer); |
124 automationUtil.treeChangeObservers.push(observer); | 120 automationUtil.treeChangeObservers.push(observer); |
125 } | 121 } |
126 apiFunctions.setHandleRequest('addTreeChangeObserver', function(observer) { | 122 apiFunctions.setHandleRequest('addTreeChangeObserver', function(observer) { |
127 addTreeChangeObserver(observer); | 123 addTreeChangeObserver(observer); |
128 }); | 124 }); |
129 | 125 |
130 }); | 126 }); |
131 | 127 |
132 automationInternal.onTreeChange.addListener(function(treeID, | |
133 nodeID, | |
134 changeType) { | |
135 var tree = AutomationRootNode.get(treeID); | |
136 if (!tree) | |
137 return; | |
138 | |
139 var node = privates(tree).impl.get(nodeID); | |
140 if (!node) | |
141 return; | |
142 | |
143 if (node.role == 'webView') { | |
144 // A WebView in the desktop tree has a different AX tree as its child. | |
145 // When we encounter a WebView with a child AX tree id that we don't | |
146 // currently have cached, explicitly request that AX tree from the | |
147 // browser process and set up a callback when it loads to attach that | |
148 // tree as a child of this node and fire appropriate events. | |
149 var childTreeID = GetIntAttribute(treeID, nodeID, 'childTreeId'); | |
150 if (!AutomationRootNode.get(childTreeID)) { | |
151 automationUtil.storeTreeCallback(childTreeID, function(root) { | |
152 privates(root).impl.setHostNode(node); | |
153 | |
154 if (root.docLoaded) | |
155 privates(root).impl.dispatchEvent(schema.EventType.loadComplete); | |
156 | |
157 privates(node).impl.dispatchEvent(schema.EventType.childrenChanged); | |
158 }); | |
159 | |
160 automationInternal.enableFrame(childTreeID); | |
161 } | |
162 } | |
163 | |
164 var treeChange = {target: node, type: changeType}; | |
165 | |
166 // Make a copy of the observers in case one of these callbacks tries | |
167 // to change the list of observers. | |
168 var observers = automationUtil.treeChangeObservers.slice(); | |
169 for (var i = 0; i < observers.length; i++) { | |
170 try { | |
171 observers[i](treeChange); | |
172 } catch (e) { | |
173 logging.WARNING('Error in tree change observer for ' + | |
174 treeChange.type + ': ' + e.message + | |
175 '\nStack trace: ' + e.stack); | |
176 } | |
177 } | |
178 | |
179 if (changeType == schema.TreeChangeType.nodeRemoved) { | |
180 privates(tree).impl.remove(nodeID); | |
181 } | |
182 }); | |
183 | |
184 // Listen to the automationInternal.onAccessibilityEvent event, which is | 128 // Listen to the automationInternal.onAccessibilityEvent event, which is |
185 // essentially a proxy for the AccessibilityHostMsg_Events IPC from the | 129 // essentially a proxy for the AccessibilityHostMsg_Events IPC from the |
186 // renderer. | 130 // renderer. |
187 automationInternal.onAccessibilityEvent.addListener(function(data) { | 131 automationInternal.onAccessibilityEvent.addListener(function(data) { |
188 var id = data.treeID; | 132 var id = data.treeID; |
189 var targetTree = AutomationRootNode.getOrCreate(id); | 133 var targetTree = idToAutomationRootNode[id]; |
190 | 134 if (!targetTree) { |
| 135 // If this is the first time we've gotten data for this tree, it will |
| 136 // contain all of the tree's data, so create a new tree which will be |
| 137 // bootstrapped from |data|. |
| 138 targetTree = new AutomationRootNode(id); |
| 139 idToAutomationRootNode[id] = targetTree; |
| 140 } |
191 if (!privates(targetTree).impl.onAccessibilityEvent(data)) | 141 if (!privates(targetTree).impl.onAccessibilityEvent(data)) |
192 return; | 142 return; |
193 | 143 |
194 // If we're not waiting on a callback to getTree(), we can early out here. | 144 // If we're not waiting on a callback to getTree(), we can early out here. |
195 if (!(id in idToCallback)) | 145 if (!(id in idToCallback)) |
196 return; | 146 return; |
197 | 147 |
198 // We usually get a 'placeholder' tree first, which doesn't have any url | 148 // We usually get a 'placeholder' tree first, which doesn't have any url |
199 // attribute or child nodes. If we've got that, wait for the full tree before | 149 // attribute or child nodes. If we've got that, wait for the full tree before |
200 // calling the callback. | 150 // calling the callback. |
201 // TODO(dmazzoni): Don't send down placeholder (crbug.com/397553) | 151 // TODO(dmazzoni): Don't send down placeholder (crbug.com/397553) |
202 if (id != DESKTOP_TREE_ID && !targetTree.url && | 152 if (id != DESKTOP_TREE_ID && !targetTree.attributes.url && |
203 targetTree.children.length == 0) { | 153 targetTree.children.length == 0) { |
204 return; | 154 return; |
205 } | 155 } |
206 | 156 |
207 // If the tree wasn't available when getTree() was called, the callback will | 157 // If the tree wasn't available when getTree() was called, the callback will |
208 // have been cached in idToCallback, so call and delete it now that we | 158 // have been cached in idToCallback, so call and delete it now that we |
209 // have the complete tree. | 159 // have the complete tree. |
210 for (var i = 0; i < idToCallback[id].length; i++) { | 160 for (var i = 0; i < idToCallback[id].length; i++) { |
| 161 console.log('calling getTree() callback'); |
211 var callback = idToCallback[id][i]; | 162 var callback = idToCallback[id][i]; |
212 callback(targetTree); | 163 callback(targetTree); |
213 } | 164 } |
214 delete idToCallback[id]; | 165 delete idToCallback[id]; |
215 }); | 166 }); |
216 | 167 |
217 automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) { | 168 automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) { |
218 // Destroy the AutomationRootNode. | 169 var targetTree = idToAutomationRootNode[id]; |
219 var targetTree = AutomationRootNode.get(id); | |
220 if (targetTree) { | 170 if (targetTree) { |
221 privates(targetTree).impl.destroy(); | 171 privates(targetTree).impl.destroy(); |
222 AutomationRootNode.destroy(id); | 172 delete idToAutomationRootNode[id]; |
223 } else { | 173 } else { |
224 logging.WARNING('no targetTree to destroy'); | 174 logging.WARNING('no targetTree to destroy'); |
225 } | 175 } |
226 | 176 delete idToAutomationRootNode[id]; |
227 // Destroy the native cache of the accessibility tree. | |
228 DestroyAccessibilityTree(id); | |
229 }); | 177 }); |
230 | 178 |
231 exports.binding = automation.generate(); | 179 exports.binding = automation.generate(); |
232 | 180 |
233 // Add additional accessibility bindings not specified in the automation IDL. | 181 // Add additional accessibility bindings not specified in the automation IDL. |
234 // Accessibility and automation share some APIs (see | 182 // Accessibility and automation share some APIs (see |
235 // ui/accessibility/ax_enums.idl). | 183 // ui/accessibility/ax_enums.idl). |
236 forEach(schema, function(k, v) { | 184 forEach(schema, function(k, v) { |
237 exports.binding[k] = v; | 185 exports.binding[k] = v; |
238 }); | 186 }); |
OLD | NEW |