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