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.ArraySet;
     21 import android.util.Log;
     22 import android.util.LongArray;
     23 import android.util.LongSparseArray;
     24 import android.util.SparseArray;
     25 
     26 import java.util.ArrayList;
     27 import java.util.List;
     28 
     29 /**
     30  * Cache for AccessibilityWindowInfos and AccessibilityNodeInfos.
     31  * It is updated when windows change or nodes change.
     32  * @hide
     33  */
     34 public class AccessibilityCache {
     35 
     36     private static final String LOG_TAG = "AccessibilityCache";
     37 
     38     private static final boolean DEBUG = false;
     39 
     40     private static final boolean CHECK_INTEGRITY = Build.IS_ENG;
     41 
     42     /**
     43      * {@link AccessibilityEvent} types that are critical for the cache to stay up to date
     44      *
     45      * When adding new event types in {@link #onAccessibilityEvent}, please add it here also, to
     46      * make sure that the events are delivered to cache regardless of
     47      * {@link android.accessibilityservice.AccessibilityServiceInfo#eventTypes}
     48      */
     49     public static final int CACHE_CRITICAL_EVENTS_MASK =
     50             AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
     51                     | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
     52                     | AccessibilityEvent.TYPE_VIEW_FOCUSED
     53                     | AccessibilityEvent.TYPE_VIEW_SELECTED
     54                     | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
     55                     | AccessibilityEvent.TYPE_VIEW_CLICKED
     56                     | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
     57                     | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
     58                     | AccessibilityEvent.TYPE_VIEW_SCROLLED
     59                     | AccessibilityEvent.TYPE_WINDOWS_CHANGED
     60                     | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
     61 
     62     private final Object mLock = new Object();
     63 
     64     private final AccessibilityNodeRefresher mAccessibilityNodeRefresher;
     65 
     66     private long mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
     67     private long mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
     68 
     69     private boolean mIsAllWindowsCached;
     70 
     71     private final SparseArray<AccessibilityWindowInfo> mWindowCache =
     72             new SparseArray<>();
     73 
     74     private final SparseArray<LongSparseArray<AccessibilityNodeInfo>> mNodeCache =
     75             new SparseArray<>();
     76 
     77     private final SparseArray<AccessibilityWindowInfo> mTempWindowArray =
     78             new SparseArray<>();
     79 
     80     public AccessibilityCache(AccessibilityNodeRefresher nodeRefresher) {
     81         mAccessibilityNodeRefresher = nodeRefresher;
     82     }
     83 
     84     public void setWindows(List<AccessibilityWindowInfo> windows) {
     85         synchronized (mLock) {
     86             if (DEBUG) {
     87                 Log.i(LOG_TAG, "Set windows");
     88             }
     89             clearWindowCache();
     90             if (windows == null) {
     91                 return;
     92             }
     93             final int windowCount = windows.size();
     94             for (int i = 0; i < windowCount; i++) {
     95                 final AccessibilityWindowInfo window = windows.get(i);
     96                 addWindow(window);
     97             }
     98             mIsAllWindowsCached = true;
     99         }
    100     }
    101 
    102     public void addWindow(AccessibilityWindowInfo window) {
    103         synchronized (mLock) {
    104             if (DEBUG) {
    105                 Log.i(LOG_TAG, "Caching window: " + window.getId());
    106             }
    107             final int windowId = window.getId();
    108             AccessibilityWindowInfo oldWindow = mWindowCache.get(windowId);
    109             if (oldWindow != null) {
    110                 oldWindow.recycle();
    111             }
    112             mWindowCache.put(windowId, AccessibilityWindowInfo.obtain(window));
    113         }
    114     }
    115 
    116     /**
    117      * Notifies the cache that the something in the UI changed. As a result
    118      * the cache will either refresh some nodes or evict some nodes.
    119      *
    120      * Note: any event that ends up affecting the cache should also be present in
    121      * {@link #CACHE_CRITICAL_EVENTS_MASK}
    122      *
    123      * @param event An event.
    124      */
    125     public void onAccessibilityEvent(AccessibilityEvent event) {
    126         synchronized (mLock) {
    127             final int eventType = event.getEventType();
    128             switch (eventType) {
    129                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: {
    130                     if (mAccessibilityFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
    131                         refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
    132                     }
    133                     mAccessibilityFocus = event.getSourceNodeId();
    134                     refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
    135                 } break;
    136 
    137                 case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED: {
    138                     if (mAccessibilityFocus == event.getSourceNodeId()) {
    139                         refreshCachedNodeLocked(event.getWindowId(), mAccessibilityFocus);
    140                         mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
    141                     }
    142                 } break;
    143 
    144                 case AccessibilityEvent.TYPE_VIEW_FOCUSED: {
    145                     if (mInputFocus != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
    146                         refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
    147                     }
    148                     mInputFocus = event.getSourceNodeId();
    149                     refreshCachedNodeLocked(event.getWindowId(), mInputFocus);
    150                 } break;
    151 
    152                 case AccessibilityEvent.TYPE_VIEW_SELECTED:
    153                 case AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED:
    154                 case AccessibilityEvent.TYPE_VIEW_CLICKED:
    155                 case AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED: {
    156                     refreshCachedNodeLocked(event.getWindowId(), event.getSourceNodeId());
    157                 } break;
    158 
    159                 case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: {
    160                     synchronized (mLock) {
    161                         final int windowId = event.getWindowId();
    162                         final long sourceId = event.getSourceNodeId();
    163                         if ((event.getContentChangeTypes()
    164                                 & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) {
    165                             clearSubTreeLocked(windowId, sourceId);
    166                         } else {
    167                             refreshCachedNodeLocked(windowId, sourceId);
    168                         }
    169                     }
    170                 } break;
    171 
    172                 case AccessibilityEvent.TYPE_VIEW_SCROLLED: {
    173                     clearSubTreeLocked(event.getWindowId(), event.getSourceNodeId());
    174                 } break;
    175 
    176                 case AccessibilityEvent.TYPE_WINDOWS_CHANGED:
    177                 case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED: {
    178                     clear();
    179                 } break;
    180             }
    181         }
    182 
    183         if (CHECK_INTEGRITY) {
    184             checkIntegrity();
    185         }
    186     }
    187 
    188     private void refreshCachedNodeLocked(int windowId, long sourceId) {
    189         if (DEBUG) {
    190             Log.i(LOG_TAG, "Refreshing cached node.");
    191         }
    192 
    193         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
    194         if (nodes == null) {
    195             return;
    196         }
    197         AccessibilityNodeInfo cachedInfo = nodes.get(sourceId);
    198         // If the source is not in the cache - nothing to do.
    199         if (cachedInfo == null) {
    200             return;
    201         }
    202         // The node changed so we will just refresh it right now.
    203         if (mAccessibilityNodeRefresher.refreshNode(cachedInfo, true)) {
    204             return;
    205         }
    206         // Weird, we could not refresh. Just evict the entire sub-tree.
    207         clearSubTreeLocked(windowId, sourceId);
    208     }
    209 
    210     /**
    211      * Gets a cached {@link AccessibilityNodeInfo} given the id of the hosting
    212      * window and the accessibility id of the node.
    213      *
    214      * @param windowId The id of the window hosting the node.
    215      * @param accessibilityNodeId The info accessibility node id.
    216      * @return The cached {@link AccessibilityNodeInfo} or null if such not found.
    217      */
    218     public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) {
    219         synchronized(mLock) {
    220             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
    221             if (nodes == null) {
    222                 return null;
    223             }
    224             AccessibilityNodeInfo info = nodes.get(accessibilityNodeId);
    225             if (info != null) {
    226                 // Return a copy since the client calls to AccessibilityNodeInfo#recycle()
    227                 // will wipe the data of the cached info.
    228                 info = AccessibilityNodeInfo.obtain(info);
    229             }
    230             if (DEBUG) {
    231                 Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info);
    232             }
    233             return info;
    234         }
    235     }
    236 
    237     public List<AccessibilityWindowInfo> getWindows() {
    238         synchronized (mLock) {
    239             if (!mIsAllWindowsCached) {
    240                 return null;
    241             }
    242 
    243             final int windowCount = mWindowCache.size();
    244             if (windowCount > 0) {
    245                 // Careful to return the windows in a decreasing layer order.
    246                 SparseArray<AccessibilityWindowInfo> sortedWindows = mTempWindowArray;
    247                 sortedWindows.clear();
    248 
    249                 for (int i = 0; i < windowCount; i++) {
    250                     AccessibilityWindowInfo window = mWindowCache.valueAt(i);
    251                     sortedWindows.put(window.getLayer(), window);
    252                 }
    253 
    254                 // It's possible in transient conditions for two windows to share the same
    255                 // layer, which results in sortedWindows being smaller than mWindowCache
    256                 final int sortedWindowCount = sortedWindows.size();
    257                 List<AccessibilityWindowInfo> windows = new ArrayList<>(sortedWindowCount);
    258                 for (int i = sortedWindowCount - 1; i >= 0; i--) {
    259                     AccessibilityWindowInfo window = sortedWindows.valueAt(i);
    260                     windows.add(AccessibilityWindowInfo.obtain(window));
    261                     sortedWindows.removeAt(i);
    262                 }
    263 
    264                 return windows;
    265             }
    266             return null;
    267         }
    268     }
    269 
    270     public AccessibilityWindowInfo getWindow(int windowId) {
    271         synchronized (mLock) {
    272             AccessibilityWindowInfo window = mWindowCache.get(windowId);
    273             if (window != null) {
    274                return AccessibilityWindowInfo.obtain(window);
    275             }
    276             return null;
    277         }
    278     }
    279 
    280     /**
    281      * Caches an {@link AccessibilityNodeInfo}.
    282      *
    283      * @param info The node to cache.
    284      */
    285     public void add(AccessibilityNodeInfo info) {
    286         synchronized(mLock) {
    287             if (DEBUG) {
    288                 Log.i(LOG_TAG, "add(" + info + ")");
    289             }
    290 
    291             final int windowId = info.getWindowId();
    292             LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
    293             if (nodes == null) {
    294                 nodes = new LongSparseArray<>();
    295                 mNodeCache.put(windowId, nodes);
    296             }
    297 
    298             final long sourceId = info.getSourceNodeId();
    299             AccessibilityNodeInfo oldInfo = nodes.get(sourceId);
    300             if (oldInfo != null) {
    301                 // If the added node is in the cache we have to be careful if
    302                 // the new one represents a source state where some of the
    303                 // children have been removed to remove the descendants that
    304                 // are no longer present.
    305                 final LongArray newChildrenIds = info.getChildNodeIds();
    306 
    307                 final int oldChildCount = oldInfo.getChildCount();
    308                 for (int i = 0; i < oldChildCount; i++) {
    309                     if (nodes.get(sourceId) == null) {
    310                         // We've removed (and thus recycled) this node because it was its own
    311                         // ancestor (the app gave us bad data), we can't continue using it.
    312                         // Clear the cache for this window and give up on adding the node.
    313                         clearNodesForWindowLocked(windowId);
    314                         return;
    315                     }
    316                     final long oldChildId = oldInfo.getChildId(i);
    317                     // If the child is no longer present, remove the sub-tree.
    318                     if (newChildrenIds == null || newChildrenIds.indexOf(oldChildId) < 0) {
    319                         clearSubTreeLocked(windowId, oldChildId);
    320                     }
    321                 }
    322 
    323                 // Also be careful if the parent has changed since the new
    324                 // parent may be a predecessor of the old parent which will
    325                 // add cycles to the cache.
    326                 final long oldParentId = oldInfo.getParentNodeId();
    327                 if (info.getParentNodeId() != oldParentId) {
    328                     clearSubTreeLocked(windowId, oldParentId);
    329                 } else {
    330                     oldInfo.recycle();
    331                 }
    332            }
    333 
    334             // Cache a copy since the client calls to AccessibilityNodeInfo#recycle()
    335             // will wipe the data of the cached info.
    336             AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(info);
    337             nodes.put(sourceId, clone);
    338             if (clone.isAccessibilityFocused()) {
    339                 mAccessibilityFocus = sourceId;
    340             }
    341             if (clone.isFocused()) {
    342                 mInputFocus = sourceId;
    343             }
    344         }
    345     }
    346 
    347     /**
    348      * Clears the cache.
    349      */
    350     public void clear() {
    351         synchronized(mLock) {
    352             if (DEBUG) {
    353                 Log.i(LOG_TAG, "clear()");
    354             }
    355             clearWindowCache();
    356             final int nodesForWindowCount = mNodeCache.size();
    357             for (int i = 0; i < nodesForWindowCount; i++) {
    358                 final int windowId = mNodeCache.keyAt(i);
    359                 clearNodesForWindowLocked(windowId);
    360             }
    361 
    362             mAccessibilityFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
    363             mInputFocus = AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
    364         }
    365     }
    366 
    367     private void clearWindowCache() {
    368         final int windowCount = mWindowCache.size();
    369         for (int i = windowCount - 1; i >= 0; i--) {
    370             AccessibilityWindowInfo window = mWindowCache.valueAt(i);
    371             window.recycle();
    372             mWindowCache.removeAt(i);
    373         }
    374         mIsAllWindowsCached = false;
    375     }
    376 
    377     /**
    378      * Clears nodes for the window with the given id
    379      */
    380     private void clearNodesForWindowLocked(int windowId) {
    381         if (DEBUG) {
    382             Log.i(LOG_TAG, "clearNodesForWindowLocked(" + windowId + ")");
    383         }
    384         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
    385         if (nodes == null) {
    386             return;
    387         }
    388         // Recycle the nodes before clearing the cache.
    389         final int nodeCount = nodes.size();
    390         for (int i = nodeCount - 1; i >= 0; i--) {
    391             AccessibilityNodeInfo info = nodes.valueAt(i);
    392             nodes.removeAt(i);
    393             info.recycle();
    394         }
    395         mNodeCache.remove(windowId);
    396     }
    397 
    398     /**
    399      * Clears a subtree rooted at the node with the given id that is
    400      * hosted in a given window.
    401      *
    402      * @param windowId The id of the hosting window.
    403      * @param rootNodeId The root id.
    404      */
    405     private void clearSubTreeLocked(int windowId, long rootNodeId) {
    406         if (DEBUG) {
    407             Log.i(LOG_TAG, "Clearing cached subtree.");
    408         }
    409         LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.get(windowId);
    410         if (nodes != null) {
    411             clearSubTreeRecursiveLocked(nodes, rootNodeId);
    412         }
    413     }
    414 
    415     /**
    416      * Clears a subtree given a pointer to the root id and the nodes
    417      * in the hosting window.
    418      *
    419      * @param nodes The nodes in the hosting window.
    420      * @param rootNodeId The id of the root to evict.
    421      */
    422     private void clearSubTreeRecursiveLocked(LongSparseArray<AccessibilityNodeInfo> nodes,
    423             long rootNodeId) {
    424         AccessibilityNodeInfo current = nodes.get(rootNodeId);
    425         if (current == null) {
    426             return;
    427         }
    428         nodes.remove(rootNodeId);
    429         final int childCount = current.getChildCount();
    430         for (int i = 0; i < childCount; i++) {
    431             final long childNodeId = current.getChildId(i);
    432             clearSubTreeRecursiveLocked(nodes, childNodeId);
    433         }
    434         current.recycle();
    435     }
    436 
    437     /**
    438      * Check the integrity of the cache which is nodes from different windows
    439      * are not mixed, there is a single active window, there is a single focused
    440      * window, for every window there are no duplicates nodes, all nodes for a
    441      * window are connected, for every window there is a single input focused
    442      * node, and for every window there is a single accessibility focused node.
    443      */
    444     public void checkIntegrity() {
    445         synchronized (mLock) {
    446             // Get the root.
    447             if (mWindowCache.size() <= 0 && mNodeCache.size() == 0) {
    448                 return;
    449             }
    450 
    451             AccessibilityWindowInfo focusedWindow = null;
    452             AccessibilityWindowInfo activeWindow = null;
    453 
    454             final int windowCount = mWindowCache.size();
    455             for (int i = 0; i < windowCount; i++) {
    456                 AccessibilityWindowInfo window = mWindowCache.valueAt(i);
    457 
    458                 // Check for one active window.
    459                 if (window.isActive()) {
    460                     if (activeWindow != null) {
    461                         Log.e(LOG_TAG, "Duplicate active window:" + window);
    462                     } else {
    463                         activeWindow = window;
    464                     }
    465                 }
    466 
    467                 // Check for one focused window.
    468                 if (window.isFocused()) {
    469                     if (focusedWindow != null) {
    470                         Log.e(LOG_TAG, "Duplicate focused window:" + window);
    471                     } else {
    472                         focusedWindow = window;
    473                     }
    474                 }
    475             }
    476 
    477             // Traverse the tree and do some checks.
    478             AccessibilityNodeInfo accessFocus = null;
    479             AccessibilityNodeInfo inputFocus = null;
    480 
    481             final int nodesForWindowCount = mNodeCache.size();
    482             for (int i = 0; i < nodesForWindowCount; i++) {
    483                 LongSparseArray<AccessibilityNodeInfo> nodes = mNodeCache.valueAt(i);
    484                 if (nodes.size() <= 0) {
    485                     continue;
    486                 }
    487 
    488                 ArraySet<AccessibilityNodeInfo> seen = new ArraySet<>();
    489                 final int windowId = mNodeCache.keyAt(i);
    490 
    491                 final int nodeCount = nodes.size();
    492                 for (int j = 0; j < nodeCount; j++) {
    493                     AccessibilityNodeInfo node = nodes.valueAt(j);
    494 
    495                     // Check for duplicates
    496                     if (!seen.add(node)) {
    497                         Log.e(LOG_TAG, "Duplicate node: " + node
    498                                 + " in window:" + windowId);
    499                         // Stop now as we potentially found a loop.
    500                         continue;
    501                     }
    502 
    503                     // Check for one accessibility focus.
    504                     if (node.isAccessibilityFocused()) {
    505                         if (accessFocus != null) {
    506                             Log.e(LOG_TAG, "Duplicate accessibility focus:" + node
    507                                     + " in window:" + windowId);
    508                         } else {
    509                             accessFocus = node;
    510                         }
    511                     }
    512 
    513                     // Check for one input focus.
    514                     if (node.isFocused()) {
    515                         if (inputFocus != null) {
    516                             Log.e(LOG_TAG, "Duplicate input focus: " + node
    517                                     + " in window:" + windowId);
    518                         } else {
    519                             inputFocus = node;
    520                         }
    521                     }
    522 
    523                     // The node should be a child of its parent if we have the parent.
    524                     AccessibilityNodeInfo nodeParent = nodes.get(node.getParentNodeId());
    525                     if (nodeParent != null) {
    526                         boolean childOfItsParent = false;
    527                         final int childCount = nodeParent.getChildCount();
    528                         for (int k = 0; k < childCount; k++) {
    529                             AccessibilityNodeInfo child = nodes.get(nodeParent.getChildId(k));
    530                             if (child == node) {
    531                                 childOfItsParent = true;
    532                                 break;
    533                             }
    534                         }
    535                         if (!childOfItsParent) {
    536                             Log.e(LOG_TAG, "Invalid parent-child relation between parent: "
    537                                     + nodeParent + " and child: " + node);
    538                         }
    539                     }
    540 
    541                     // The node should be the parent of its child if we have the child.
    542                     final int childCount = node.getChildCount();
    543                     for (int k = 0; k < childCount; k++) {
    544                         AccessibilityNodeInfo child = nodes.get(node.getChildId(k));
    545                         if (child != null) {
    546                             AccessibilityNodeInfo parent = nodes.get(child.getParentNodeId());
    547                             if (parent != node) {
    548                                 Log.e(LOG_TAG, "Invalid child-parent relation between child: "
    549                                         + node + " and parent: " + nodeParent);
    550                             }
    551                         }
    552                     }
    553                 }
    554             }
    555         }
    556     }
    557 
    558     // Layer of indirection included to break dependency chain for testing
    559     public static class AccessibilityNodeRefresher {
    560         public boolean refreshNode(AccessibilityNodeInfo info, boolean bypassCache) {
    561             return info.refresh(null, bypassCache);
    562         }
    563     }
    564 }
    565