Home | History | Annotate | Download | only in accessibility
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.view.accessibility;
     18 
     19 import android.os.Build;
     20 import android.util.Log;
     21 import android.util.LongSparseArray;
     22 import android.util.SparseLongArray;
     23 
     24 import java.util.HashSet;
     25 import java.util.LinkedList;
     26 import java.util.Queue;
     27 
     28 /**
     29  * Simple cache for AccessibilityNodeInfos. The cache is mapping an
     30  * accessibility id to an info. The cache allows storing of
     31  * <code>null</code> values. It also tracks accessibility events
     32  * and invalidates accordingly.
     33  *
     34  * @hide
     35  */
     36 public class AccessibilityNodeInfoCache {
     37 
     38     private static final String LOG_TAG = AccessibilityNodeInfoCache.class.getSimpleName();
     39 
     40     private static final boolean ENABLED = true;
     41 
     42     private static final boolean DEBUG = false;
     43 
     44     private static final boolean CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD = true;
     45 
     46     private final Object mLock = new Object();
     47 
     48     private final LongSparseArray<AccessibilityNodeInfo> mCacheImpl;
     49 
     50     private int mWindowId;
     51 
     52     public AccessibilityNodeInfoCache() {
     53         if (ENABLED) {
     54             mCacheImpl = new LongSparseArray<AccessibilityNodeInfo>();
     55         } else {
     56             mCacheImpl = null;
     57         }
     58     }
     59 
     60     /**
     61      * The cache keeps track of {@link AccessibilityEvent}s and invalidates
     62      * cached nodes as appropriate.
     63      *
     64      * @param event An event.
     65      */
     66     public void onAccessibilityEvent(AccessibilityEvent event) {
     67         if (ENABLED) {
     68             final int eventType = event.getEventType();
     69             switch (eventType) {
     70                 case AccessibilityEvent.TYPE_TOUCH_EXPLORATION_GESTURE_END:
     71                 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
     72                 case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER:
     73                 case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: {
     74                     // If the active window changes, clear the cache.
     75                     final int windowId = event.getWindowId();
     76                     if (mWindowId != windowId) {
     77                         mWindowId = windowId;
     78                         clear();
     79                     }
     80                 } break;
     81                 case AccessibilityEvent.TYPE_VIEW_FOCUSED:
     82                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED:
     83                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED:
     84                 case AccessibilityEvent.TYPE_VIEW_SELECTED:
     85                 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
     86                 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
     87                     refreshCachedNode(event.getSourceNodeId());
     88                 } break;
     89                 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
     90                     synchronized (mLock) {
     91                         clearSubTreeLocked(event.getSourceNodeId());
     92                     }
     93                 } break;
     94                 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
     95                     synchronized (mLock) {
     96                         final long sourceId = event.getSourceNodeId();
     97                         if ((event.getContentChangeTypes()
     98                                 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
     99                             clearSubTreeLocked(sourceId);
    100                         } else {
    101                             refreshCachedNode(sourceId);
    102                         }
    103                     }
    104                 } break;
    105             }
    106             if (CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD && Build.IS_DEBUGGABLE) {
    107                 checkIntegrity();
    108             }
    109         }
    110     }
    111 
    112     private void refreshCachedNode(long sourceId) {
    113         if (DEBUG) {
    114             Log.i(LOG_TAG, "Refreshing cached node.");
    115         }
    116         synchronized (mLock) {
    117             AccessibilityNodeInfo cachedInfo = mCacheImpl.get(sourceId);
    118             // If the source is not in the cache - nothing to do.
    119             if (cachedInfo == null) {
    120                 return;
    121             }
    122             // The node changed so we will just refresh it right now.
    123             if (cachedInfo.refresh(true)) {
    124                 return;
    125             }
    126             // Weird, we could not refresh. Just evict the entire sub-tree.
    127             clearSubTreeLocked(sourceId);
    128         }
    129     }
    130 
    131     /**
    132      * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id.
    133      *
    134      * @param accessibilityNodeId The info accessibility node id.
    135      * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
    136      */
    137     public AccessibilityNodeInfo get(long accessibilityNodeId) {
    138         if (ENABLED) {
    139             synchronized(mLock) {
    140                 AccessibilityNodeInfo info = mCacheImpl.get(accessibilityNodeId);
    141                 if (info != null) {
    142                     // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
    143                     // will wipe the data of the cached info.
    144                     info = AccessibilityNodeInfo.obtain(info);
    145                 }
    146                 if (DEBUG) {
    147                     Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
    148                 }
    149                 return info;
    150             }
    151         } else {
    152             return null;
    153         }
    154     }
    155 
    156     /**
    157      * Caches an {@link AccessibilityNodeInfo} given its accessibility node id.
    158      *
    159      * @param info The {@link AccessibilityNodeInfo} to cache.
    160      */
    161     public void add(AccessibilityNodeInfo info) {
    162         if (ENABLED) {
    163             synchronized(mLock) {
    164                 if (DEBUG) {
    165                     Log.i(LOG_TAG, "add(" + info + ")");
    166                 }
    167 
    168                 final long sourceId = info.getSourceNodeId();
    169                 AccessibilityNodeInfo oldInfo = mCacheImpl.get(sourceId);
    170                 if (oldInfo != null) {
    171                     // If the added node is in the cache we have to be careful if
    172                     // the new one represents a source state where some of the
    173                     // children have been removed to avoid having disconnected
    174                     // subtrees in the cache.
    175                     SparseLongArray oldChildrenIds = oldInfo.getChildNodeIds();
    176                     SparseLongArray newChildrenIds = info.getChildNodeIds();
    177                     final int oldChildCount = oldChildrenIds.size();
    178                     for (int i = 0; i < oldChildCount; i++) {
    179                         final long oldChildId = oldChildrenIds.valueAt(i);
    180                         if (newChildrenIds.indexOfValue(oldChildId) < 0) {
    181                             clearSubTreeLocked(oldChildId);
    182                         }
    183                     }
    184 
    185                     // Also be careful if the parent has changed since the new
    186                     // parent may be a predecessor of the old parent which will
    187                     // make the cached tree cyclic.
    188                     final long oldParentId = oldInfo.getParentNodeId();
    189                     if (info.getParentNodeId() != oldParentId) {
    190                         clearSubTreeLocked(oldParentId);
    191                     }
    192                 }
    193 
    194                 // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
    195                 // will wipe the data of the cached info.
    196                 AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
    197                 mCacheImpl.put(sourceId, clone);
    198             }
    199         }
    200     }
    201 
    202     /**
    203      * Clears the cache.
    204      */
    205     public void clear() {
    206         if (ENABLED) {
    207             synchronized(mLock) {
    208                 if (DEBUG) {
    209                     Log.i(LOG_TAG, "clear()");
    210                 }
    211                 // Recycle the nodes before clearing the cache.
    212                 final int nodeCount = mCacheImpl.size();
    213                 for (int i = 0; i < nodeCount; i++) {
    214                     AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
    215                     info.recycle();
    216                 }
    217                 mCacheImpl.clear();
    218             }
    219         }
    220     }
    221 
    222     /**
    223      * Clears a subtree rooted at the node with the given id.
    224      *
    225      * @param rootNodeId The root id.
    226      */
    227     private void clearSubTreeLocked(long rootNodeId) {
    228         if (DEBUG) {
    229             Log.i(LOG_TAG, "Clearing cached subtree.");
    230         }
    231         clearSubTreeRecursiveLocked(rootNodeId);
    232     }
    233 
    234     private void clearSubTreeRecursiveLocked(long rootNodeId) {
    235         AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId);
    236         if (current == null) {
    237             return;
    238         }
    239         mCacheImpl.remove(rootNodeId);
    240         SparseLongArray childNodeIds = current.getChildNodeIds();
    241         final int childCount = childNodeIds.size();
    242         for (int i = 0; i < childCount; i++) {
    243             final long childNodeId = childNodeIds.valueAt(i);
    244             clearSubTreeRecursiveLocked(childNodeId);
    245         }
    246     }
    247 
    248     /**
    249      * Check the integrity of the cache which is it does not have nodes
    250      * from more than one window, there are no duplicates, all nodes are
    251      * connected, there is a single input focused node, and there is a
    252      * single accessibility focused node.
    253      */
    254     private void checkIntegrity() {
    255         synchronized (mLock) {
    256             // Get the root.
    257             if (mCacheImpl.size() <= 0) {
    258                 return;
    259             }
    260 
    261             // If the cache is a tree it does not matter from
    262             // which node we start to search for the root.
    263             AccessibilityNodeInfo root = mCacheImpl.valueAt(0);
    264             AccessibilityNodeInfo parent = root;
    265             while (parent != null) {
    266                 root = parent;
    267                 parent = mCacheImpl.get(parent.getParentNodeId());
    268             }
    269 
    270             // Traverse the tree and do some checks.
    271             final int windowId = root.getWindowId();
    272             AccessibilityNodeInfo accessFocus = null;
    273             AccessibilityNodeInfo inputFocus = null;
    274             HashSet<AccessibilityNodeInfo> seen = new HashSet<AccessibilityNodeInfo>();
    275             Queue<AccessibilityNodeInfo> fringe = new LinkedList<AccessibilityNodeInfo>();
    276             fringe.add(root);
    277 
    278             while (!fringe.isEmpty()) {
    279                 AccessibilityNodeInfo current = fringe.poll();
    280                 // Check for duplicates
    281                 if (!seen.add(current)) {
    282                     Log.e(LOG_TAG, "Duplicate node: " + current);
    283                     return;
    284                 }
    285 
    286                 // Check for one accessibility focus.
    287                 if (current.isAccessibilityFocused()) {
    288                     if (accessFocus != null) {
    289                         Log.e(LOG_TAG, "Duplicate accessibility focus:" + current);
    290                     } else {
    291                         accessFocus = current;
    292                     }
    293                 }
    294 
    295                 // Check for one input focus.
    296                 if (current.isFocused()) {
    297                     if (inputFocus != null) {
    298                         Log.e(LOG_TAG, "Duplicate input focus: " + current);
    299                     } else {
    300                         inputFocus = current;
    301                     }
    302                 }
    303 
    304                 SparseLongArray childIds = current.getChildNodeIds();
    305                 final int childCount = childIds.size();
    306                 for (int i = 0; i < childCount; i++) {
    307                     final long childId = childIds.valueAt(i);
    308                     AccessibilityNodeInfo child = mCacheImpl.get(childId);
    309                     if (child != null) {
    310                         fringe.add(child);
    311                     }
    312                 }
    313             }
    314 
    315             // Check for disconnected nodes or ones from another window.
    316             for (int i = 0; i < mCacheImpl.size(); i++) {
    317                 AccessibilityNodeInfo info = mCacheImpl.valueAt(i);
    318                 if (!seen.contains(info)) {
    319                     if (info.getWindowId() == windowId) {
    320                         Log.e(LOG_TAG, "Disconneced node: " + info);
    321                     } else {
    322                         Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:"
    323                                 + windowId + " " + info);
    324                     }
    325                 }
    326             }
    327         }
    328     }
    329 }
    330