| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.accessibility; |
| |
| import android.os.Build; |
| import android.util.Log; |
| import android.util.LongSparseArray; |
| import android.util.SparseLongArray; |
| |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.Queue; |
| |
| /** |
| * Simple cache for AccessibilityNodeInfos. The cache is mapping an |
| * accessibility id to an info. The cache allows storing of |
| * <code>null</code> values. It also tracks accessibility events |
| * and invalidates accordingly. |
| * |
| * @hide |
| */ |
| public class AccessibilityNodeInfoCache { |
| |
| private static final String LOG_TAG = AccessibilityNodeInfoCache.class.getSimpleName(); |
| |
| private static final boolean ENABLED = true; |
| |
| private static final boolean DEBUG = false; |
| |
| private static final boolean CHECK_INTEGRITY = true; |
| |
| private final Object mLock = new Object(); |
| |
| private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl; |
| |
| private int mWindowId; |
| |
| public AccessibilityNodeInfoCache() { |
| if (ENABLED) { |
| mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>(); |
| } else { |
| mCacheImpl = null; |
| } |
| } |
| |
| /** |
| * The cache keeps track of {@link AccessibilityEvent}s and invalidates |
| * cached nodes as appropriate. |
| * |
| * @param event An event. |
| */ |
| public void onAccessibilityEvent(AccessibilityEvent event) { |
| if (ENABLED) { |
| final int eventType = event.getEventType(); |
| switch (eventType) { |
| case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: { |
| // New window so we clear the cache. |
| mWindowId = event.getWindowId(); |
| clear(); |
| } break; |
| case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: |
| case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { |
| final int windowId = event.getWindowId(); |
| if (mWindowId != windowId) { |
| // New window so we clear the cache. |
| mWindowId = windowId; |
| clear(); |
| } |
| } break; |
| case AccessibilityEvent.TYPE_VIEW_FOCUSED: |
| case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: |
| case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: |
| case AccessibilityEvent.TYPE_VIEW_SELECTED: |
| case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED: |
| case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: { |
| // Since we prefetch the descendants of a node we |
| // just remove the entire subtree since when the node |
| // is fetched we will gets its descendant anyway. |
| synchronized (mLock) { |
| final long sourceId = event.getSourceNodeId(); |
| clearSubTreeLocked(sourceId); |
| if (eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED) { |
| clearSubtreeWithOldInputFocusLocked(sourceId); |
| } |
| if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { |
| clearSubtreeWithOldAccessibilityFocusLocked(sourceId); |
| } |
| } |
| } break; |
| case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: |
| case AccessibilityEvent.TYPE_VIEW_SCROLLED: { |
| synchronized (mLock) { |
| final long accessibilityNodeId = event.getSourceNodeId(); |
| clearSubTreeLocked(accessibilityNodeId); |
| } |
| } break; |
| } |
| if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) { |
| checkIntegrity(); |
| } |
| } |
| } |
| |
| /** |
| * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id. |
| * |
| * @param accessibilityNodeId The info accessibility node id. |
| * @return The cached {@link AccessibilityNodeInfo} or null if such not found. |
| */ |
| public AccessibilityNodeInfo get(long accessibilityNodeId) { |
| if (ENABLED) { |
| synchronized(mLock) { |
| AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId); |
| if (info != null) { |
| // Return a copy since the client calls to AccessibilityNodeInfo#recycle() |
| // will wipe the data of the cached info. |
| info = AccessibilityNodeInfo.obtain(info); |
| } |
| if (DEBUG) { |
| Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); |
| } |
| return info; |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Caches an {@link AccessibilityNodeInfo} given its accessibility node id. |
| * |
| * @param info The {@link AccessibilityNodeInfo} to cache. |
| */ |
| public void add(AccessibilityNodeInfo info) { |
| if (ENABLED) { |
| synchronized(mLock) { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "add(" + info + ")"); |
| } |
| |
| final long sourceId = info.getSourceNodeId(); |
| AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId); |
| if (oldInfo != null) { |
| // If the added node is in the cache we have to be careful if |
| // the new one represents a source state where some of the |
| // children have been removed to avoid having disconnected |
| // subtrees in the cache. |
| SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds(); |
| SparseLongArray newChildrenIds = info.getChildNodeIds(); |
| final int oldChildCount = oldChildrenIds.size(); |
| for (int i = 0; i < oldChildCount; i++) { |
| final long oldChildId = oldChildrenIds.valueAt(i); |
| if (newChildrenIds.indexOfValue(oldChildId) < 0) { |
| clearSubTreeLocked(oldChildId); |
| } |
| } |
| |
| // Also be careful if the parent has changed since the new |
| // parent may be a predecessor of the old parent which will |
| // make the cached tree cyclic. |
| final long oldParentId = oldInfo.getParentNodeId(); |
| if (info.getParentNodeId() != oldParentId) { |
| clearSubTreeLocked(oldParentId); |
| } |
| } |
| |
| // Cache a copy since the client calls to AccessibilityNodeInfo#recycle() |
| // will wipe the data of the cached info. |
| AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info); |
| mCacheImpl.put(sourceId, clone); |
| } |
| } |
| } |
| |
| /** |
| * Clears the cache. |
| */ |
| public void clear() { |
| if (ENABLED) { |
| synchronized(mLock) { |
| if (DEBUG) { |
| Log.i(LOG_TAG, "clear()"); |
| } |
| // Recycle the nodes before clearing the cache. |
| final int nodeCount = mCacheImpl.size(); |
| for (int i = 0; i < nodeCount; i++) { |
| AccessibilityNodeInfo info = mCacheImpl.valueAt(i); |
| info.recycle(); |
| } |
| mCacheImpl.clear(); |
| } |
| } |
| } |
| |
| /** |
| * Clears a subtree rooted at the node with the given id. |
| * |
| * @param rootNodeId The root id. |
| */ |
| private void clearSubTreeLocked(long rootNodeId) { |
| AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId); |
| if (current == null) { |
| return; |
| } |
| mCacheImpl.remove(rootNodeId); |
| SparseLongArray childNodeIds = current.getChildNodeIds(); |
| final int childCount = childNodeIds.size(); |
| for (int i = 0; i < childCount; i++) { |
| final long childNodeId = childNodeIds.valueAt(i); |
| clearSubTreeLocked(childNodeId); |
| } |
| } |
| |
| /** |
| * We are enforcing the invariant for a single input focus. |
| * |
| * @param currentInputFocusId The current input focused node. |
| */ |
| private void clearSubtreeWithOldInputFocusLocked(long currentInputFocusId) { |
| final int cacheSize = mCacheImpl.size(); |
| for (int i = 0; i < cacheSize; i++) { |
| AccessibilityNodeInfo info = mCacheImpl.valueAt(i); |
| final long infoSourceId = info.getSourceNodeId(); |
| if (infoSourceId != currentInputFocusId && info.isFocused()) { |
| clearSubTreeLocked(infoSourceId); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * We are enforcing the invariant for a single accessibility focus. |
| * |
| * @param currentAccessibilityFocusId The current input focused node. |
| */ |
| private void clearSubtreeWithOldAccessibilityFocusLocked(long currentAccessibilityFocusId) { |
| final int cacheSize = mCacheImpl.size(); |
| for (int i = 0; i < cacheSize; i++) { |
| AccessibilityNodeInfo info = mCacheImpl.valueAt(i); |
| final long infoSourceId = info.getSourceNodeId(); |
| if (infoSourceId != currentAccessibilityFocusId && info.isAccessibilityFocused()) { |
| clearSubTreeLocked(infoSourceId); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Check the integrity of the cache which is it does not have nodes |
| * from more than one window, there are no duplicates, all nodes are |
| * connected, there is a single input focused node, and there is a |
| * single accessibility focused node. |
| */ |
| private void checkIntegrity() { |
| synchronized (mLock) { |
| // Get the root. |
| if (mCacheImpl.size() <= 0) { |
| return; |
| } |
| |
| // If the cache is a tree it does not matter from |
| // which node we start to search for the root. |
| AccessibilityNodeInfo root = mCacheImpl.valueAt(0); |
| AccessibilityNodeInfo parent = root; |
| while (parent != null) { |
| root = parent; |
| parent = mCacheImpl.get(parent.getParentNodeId()); |
| } |
| |
| // Traverse the tree and do some checks. |
| final int windowId = root.getWindowId(); |
| AccessibilityNodeInfo accessFocus = null; |
| AccessibilityNodeInfo inputFocus = null; |
| HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>(); |
| Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>(); |
| fringe.add(root); |
| |
| while (!fringe.isEmpty()) { |
| AccessibilityNodeInfo current = fringe.poll(); |
| // Check for duplicates |
| if (!seen.add(current)) { |
| Log.e(LOG_TAG, "Duplicate node: " + current); |
| return; |
| } |
| |
| // Check for one accessibility focus. |
| if (current.isAccessibilityFocused()) { |
| if (accessFocus != null) { |
| Log.e(LOG_TAG, "Duplicate accessibility focus:" + current); |
| } else { |
| accessFocus = current; |
| } |
| } |
| |
| // Check for one input focus. |
| if (current.isFocused()) { |
| if (inputFocus != null) { |
| Log.e(LOG_TAG, "Duplicate input focus: " + current); |
| } else { |
| inputFocus = current; |
| } |
| } |
| |
| SparseLongArray childIds = current.getChildNodeIds(); |
| final int childCount = childIds.size(); |
| for (int i = 0; i < childCount; i++) { |
| final long childId = childIds.valueAt(i); |
| AccessibilityNodeInfo child = mCacheImpl.get(childId); |
| if (child != null) { |
| fringe.add(child); |
| } |
| } |
| } |
| |
| // Check for disconnected nodes or ones from another window. |
| final int cacheSize = mCacheImpl.size(); |
| for (int i = 0; i < cacheSize; i++) { |
| AccessibilityNodeInfo info = mCacheImpl.valueAt(i); |
| if (!seen.contains(info)) { |
| if (info.getWindowId() == windowId) { |
| Log.e(LOG_TAG, "Disconneced node: "); |
| } else { |
| Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:" |
| + windowId + " " + info); |
| } |
| } |
| } |
| } |
| } |
| } |