automationInternal.onTreeChange.addListener(function(treeID,
                                                     nodeID,
                                                     changeType) {
  var tree = AutomationRootNode.getOrCreate(treeID);
  if (!tree)
    return;

  var node = privates(tree).impl.get(nodeID);
  if (!node)
    return;

  if (node.role == 'webView' || node.role == 'embeddedObject') {
    // 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.
    var childTreeID = GetIntAttribute(treeID, nodeID, 'childTreeId');
    if (!childTreeID)
      return;

    var subroot = AutomationRootNode.get(childTreeID);
    if (!subroot) {
      automationUtil.storeTreeCallback(childTreeID, function(root) {
        privates(root).impl.setHostNode(node);

        if (root.docLoaded)
          privates(root).impl.dispatchEvent(schema.EventType.loadComplete);

        privates(node).impl.dispatchEvent(schema.EventType.childrenChanged);
      });

      automationInternal.enableFrame(childTreeID);
    } else {
      privates(subroot).impl.setHostNode(node);
    }
  }

  var treeChange = {target: node, type: changeType};

  // Make a copy of the observers in case one of these callbacks tries
  // to change the list of observers.
  var observers = automationUtil.treeChangeObservers.slice();
  for (var i = 0; i < observers.length; i++) {
    try {
      observers[i](treeChange);
    } catch (e) {
      exceptionHandler.handle('Error in tree change observer for ' +
          treeChange.type, e);
    }
  }

  if (changeType == schema.TreeChangeType.nodeRemoved) {
    privates(tree).impl.remove(nodeID);
  }
});
automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) {
  // Destroy the AutomationRootNode.
  var 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.enableDesktop(function(treeId) {
        if (hasLastError()) {
          AutomationRootNode.destroy(treeId);
          desktopId = undefined;
          callback();
          return;
        }
        desktopId = treeId;
        desktopTree = AutomationRootNode.getOrCreate(desktopId);
        callback(desktopTree);

        // TODO(dtseng): Disable desktop tree once desktop object goes out of
        // scope.
      });
 automationInternal.enableDesktop(routingID, function() {
   if (lastError.hasError(chrome)) {
     AutomationRootNode.destroy(DESKTOP_TREE_ID);
     callback();
     return;
   }
 });
automationInternal.onGetTextLocationResult.addListener(function(
    textLocationParams) {
  var targetTree = AutomationRootNode.get(textLocationParams.treeID);
  if (!targetTree)
    return;
  privates(targetTree).impl.onGetTextLocationResult(textLocationParams);
});
automationInternal.onAccessibilityEvent.addListener(function(data) {
  var id = data.treeID;
  var targetTree = AutomationRootNode.getOrCreate(id);

  if (!privates(targetTree).impl.onAccessibilityEvent(data))
    return;

  // If we're not waiting on a callback to getTree(), 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 != DESKTOP_TREE_ID && !targetTree.url &&
      targetTree.children.length == 0)
    return;

  // If the tree wasn't available when getTree() was called, the callback will
  // have been cached in idToCallback, so call and delete it now that we
  // have the complete tree.
  for (var i = 0; i < idToCallback[id].length; i++) {
    var callback = idToCallback[id][i];
    callback(targetTree);
  }
  delete idToCallback[id];
});
automationInternal.onActionResult.addListener(function(
    treeID, requestID, result) {
  var targetTree = AutomationRootNode.get(treeID);
  if (!targetTree)
    return;

  privates(targetTree).impl.onActionResult(requestID, result);
});
automationInternal.onNodesRemoved.addListener(function(treeID, nodeIDs) {
  var tree = AutomationRootNode.getOrCreate(treeID);
  if (!tree)
    return;

  for (var i = 0; i < nodeIDs.length; i++) {
    privates(tree).impl.remove(nodeIDs[i]);
  }
});
automationUtil.updateFocusedNode = function() {
  automationUtil.focusedNode = null;
  var focusedNodeInfo = GetFocus(DESKTOP_TREE_ID);
  if (!focusedNodeInfo)
    return;
  var tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
  if (tree) {
    automationUtil.focusedNode =
        privates(tree).impl.get(focusedNodeInfo.nodeId);
  }
};
automationUtil.getFocus = function() {
  if (desktopId === undefined)
    return;

  var focusedNodeInfo = GetFocusNative(desktopId);
  if (!focusedNodeInfo)
    return null;
  var tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
  if (tree)
    return privates(tree).impl.get(focusedNodeInfo.nodeId);
};
automationInternal.onChildTreeID.addListener(function(treeID,
                                                      nodeID) {
  var tree = AutomationRootNode.getOrCreate(treeID);
  if (!tree)
    return;

  var node = privates(tree).impl.get(nodeID);
  if (!node)
    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.
  var childTreeID = GetIntAttribute(treeID, nodeID, 'childTreeId');
  if (!childTreeID)
    return;

  var subroot = AutomationRootNode.get(childTreeID);
  if (!subroot) {
    automationUtil.storeTreeCallback(childTreeID, function(root) {
      // Return early if the root has already been attached.
      if (root.parent)
        return;

      privates(root).impl.setHostNode(node);

      if (root.docLoaded)
        privates(root).impl.dispatchEvent(schema.EventType.loadComplete);

      privates(node).impl.dispatchEvent(schema.EventType.childrenChanged);
    });

    automationInternal.enableFrame(childTreeID);
  } else {
    privates(subroot).impl.setHostNode(node);
  }
});
automationUtil.storeTreeCallback = function(id, callback) {
  if (!callback)
    return;

  var 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);
  }
};
automationInternal.onAccessibilityEvent.addListener(function(eventParams) {
  var id = eventParams.treeID;
  var targetTree = AutomationRootNode.getOrCreate(id);

  // When we get a focus event, ignore the actual event target, and instead
  // check what node has focus globally. If that represents a focus change,
  // fire a focus event on the correct target.
  if (eventParams.eventType == schema.EventType.focus) {
    var previousFocusedNode = automationUtil.focusedNode;
    automationUtil.updateFocusedNode();
    if (automationUtil.focusedNode &&
        automationUtil.focusedNode == previousFocusedNode) {
      return;
    }

    if (automationUtil.focusedNode) {
      targetTree = automationUtil.focusedNode.root;
      eventParams.treeID = privates(targetTree).impl.treeID;
      eventParams.targetID = privates(automationUtil.focusedNode).impl.id;
    }
  }

  if (!privates(targetTree).impl.onAccessibilityEvent(eventParams))
    return;

  // If we're not waiting on a callback to getTree(), 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 != DESKTOP_TREE_ID && !targetTree.url &&
      targetTree.children.length == 0)
    return;

  // If the tree wasn't available when getTree() was called, the callback will
  // have been cached in idToCallback, so call and delete it now that we
  // have the complete tree.
  for (var i = 0; i < idToCallback[id].length; i++) {
    var callback = idToCallback[id][i];
    callback(targetTree);
  }
  delete idToCallback[id];
});
automationInternal.onChildTreeID.addListener(function(childTreeId) {
  var 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) {
    privates(root).impl.dispatchEvent('loadComplete', 'page');
  }, true);

  automationInternal.enableFrame(childTreeId);
});
  apiFunctions.setHandleRequest('getDesktop', function(callback) {
    StartCachingAccessibilityTrees();
    if (desktopId !== undefined)
      desktopTree = AutomationRootNode.get(desktopId);
    if (!desktopTree) {
      automationInternal.enableDesktop(function(treeId) {
        if (hasLastError()) {
          AutomationRootNode.destroy(treeId);
          desktopId = undefined;
          callback();
          return;
        }
        desktopId = treeId;
        desktopTree = AutomationRootNode.getOrCreate(desktopId);
        callback(desktopTree);

        // TODO(dtseng): Disable desktop tree once desktop object goes out of
        // scope.
      });
    } else {
      callback(desktopTree);
    }
  });
automationInternal.onTreeChange.addListener(function(observerID,
                                                     treeID,
                                                     nodeID,
                                                     changeType) {
  var tree = AutomationRootNode.getOrCreate(treeID);
  if (!tree)
    return;

  var node = privates(tree).impl.get(nodeID);
  if (!node)
    return;

  var 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);
  }
});
  apiFunctions.setHandleRequest('getDesktop', function(callback) {
    StartCachingAccessibilityTrees();
    desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID);
    if (!desktopTree) {
      if (DESKTOP_TREE_ID in idToCallback)
        idToCallback[DESKTOP_TREE_ID].push(callback);
      else
        idToCallback[DESKTOP_TREE_ID] = [callback];

      var routingID = GetRoutingID();

      // TODO(dtseng): Disable desktop tree once desktop object goes out of
      // scope.
      automationInternal.enableDesktop(routingID, function() {
        if (lastError.hasError(chrome)) {
          AutomationRootNode.destroy(DESKTOP_TREE_ID);
          callback();
          return;
        }
      });
    } else {
      callback(desktopTree);
    }
  });
automationInternal.onAccessibilityEvent.addListener(function(eventParams) {
  var id = eventParams.treeID;
  var targetTree = AutomationRootNode.getOrCreate(id);
  if (eventParams.eventType == 'blur') {
    // Work around an issue where Chrome sends us 'blur' events on the
    // root node when nothing has focus, we need to treat those as focus
    // events but otherwise not handle blur events specially.
    var node = privates(targetTree).impl.get(eventParams.targetID);
    if (!node)
      return;

    if (node == node.root)
      automationUtil.updateFocusedNodeOnBlur();
  } else if (eventParams.eventType == 'mediaStartedPlaying' ||
      eventParams.eventType == 'mediaStoppedPlaying') {
    // These events are global to the tree.
    eventParams.targetID = privates(targetTree).impl.id;
  } else {
    var previousFocusedNode = automationUtil.focusedNode;
    automationUtil.updateFocusedNode();

    // Fire focus events if necessary.
    if (automationUtil.focusedNode &&
        automationUtil.focusedNode != previousFocusedNode) {
      var eventParamsCopy = {};
      for (var key in eventParams)
        eventParamsCopy[key] = eventParams[key];
      eventParamsCopy['eventType'] = 'focus';
      eventParamsCopy['treeID'] =
          privates(automationUtil.focusedNode.root).impl.treeID;
      eventParamsCopy['targetID'] =
          privates(automationUtil.focusedNode).impl.id;
      privates(automationUtil.focusedNode.root)
          .impl.onAccessibilityEvent(eventParamsCopy);
    }
  }

  // Note that focus type events have already been handled above if there was a
  // focused node. All other events, even non-focus events that triggered a
  // focus dispatch, still need to have their original event fired.
  if ((!automationUtil.focusedNode || eventParams.eventType != 'focus') &&
      !privates(targetTree).impl.onAccessibilityEvent(eventParams))
    return;

  // If we're not waiting on a callback to getTree(), 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 when getTree() was called, the callback will
  // have been cached in idToCallback, so call and delete it now that we
  // have the complete tree.
  for (var i = 0; i < idToCallback[id].length; i++) {
    var callback = idToCallback[id][i];
    callback(targetTree);
  }
  delete idToCallback[id];
});