diff --git a/core/api/current.txt b/core/api/current.txt index 17beece326df6..c05f39b25e349 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -3014,6 +3014,8 @@ package android.accessibilityservice { public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); + method public boolean clearCache(); + method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo); method public final void disableSelf(); method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); @@ -3028,6 +3030,8 @@ package android.accessibilityservice { method @NonNull public final android.accessibilityservice.TouchInteractionController getTouchInteractionController(int); method public java.util.List getWindows(); method @NonNull public final android.util.SparseArray> getWindowsOnAllDisplays(); + method public boolean isCacheEnabled(); + method public boolean isNodeInCache(@NonNull android.view.accessibility.AccessibilityNodeInfo); method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent); method public final android.os.IBinder onBind(android.content.Intent); method @Deprecated protected boolean onGesture(int); @@ -3038,6 +3042,7 @@ package android.accessibilityservice { method public void onSystemActionsChanged(); method public final boolean performGlobalAction(int); method public void setAccessibilityFocusAppearance(int, @ColorInt int); + method public boolean setCacheEnabled(boolean); method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region); method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region); diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 3d38551642742..9a5586747d0eb 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -59,6 +59,7 @@ import android.view.MotionEvent; import android.view.SurfaceView; import android.view.WindowManager; import android.view.WindowManagerImpl; +import android.view.accessibility.AccessibilityCache; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; @@ -748,7 +749,6 @@ public abstract class AccessibilityService extends Service { private FingerprintGestureController mFingerprintGestureController; - /** * Callback for {@link android.view.accessibility.AccessibilityEvent}s. * @@ -2076,6 +2076,85 @@ public abstract class AccessibilityService extends Service { available); } + /** Sets the cache status. + * + *

If {@code enabled}, enable the cache and prefetching. Otherwise, disable the cache + * and prefetching. + * Note: By default the cache is enabled. + * @param enabled whether to enable or disable the cache. + * @return {@code true} if the cache and connection are not null, so the cache status is set. + */ + public boolean setCacheEnabled(boolean enabled) { + AccessibilityCache cache = + AccessibilityInteractionClient.getCache(mConnectionId); + if (cache == null) { + return false; + } + final IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getConnection(mConnectionId); + if (connection == null) { + return false; + } + try { + connection.setCacheEnabled(enabled); + cache.setEnabled(enabled); + return true; + } catch (RemoteException re) { + Log.w(LOG_TAG, "Error while setting status of cache", re); + re.rethrowFromSystemServer(); + } + return false; + } + + /** Invalidates {@code node} and its subtree in the cache. + * @param node the node to invalidate. + * @return {@code true} if the subtree rooted at {@code node} was invalidated. + */ + public boolean clearCachedSubtree(@NonNull AccessibilityNodeInfo node) { + AccessibilityCache cache = + AccessibilityInteractionClient.getCache(mConnectionId); + if (cache == null) { + return false; + } + return cache.clearSubTree(node); + } + + /** Clears the cache. + * @return {@code true} if the cache was cleared + */ + public boolean clearCache() { + AccessibilityCache cache = + AccessibilityInteractionClient.getCache(mConnectionId); + if (cache == null) { + return false; + } + cache.clear(); + return true; + } + + /** Checks if {@code node} is in the cache. + * @param node the node to check. + * @return {@code true} if {@code node} is in the cache. + */ + public boolean isNodeInCache(@NonNull AccessibilityNodeInfo node) { + AccessibilityCache cache = + AccessibilityInteractionClient.getCache(mConnectionId); + if (cache == null) { + return false; + } + return cache.isNodeInCache(node); + } + + /** Returns {@code true} if the cache is enabled. */ + public boolean isCacheEnabled() { + AccessibilityCache cache = + AccessibilityInteractionClient.getCache(mConnectionId); + if (cache == null) { + return false; + } + return cache.isEnabled(); + } + /** This is called when the system action list is changed. */ public void onSystemActionsChanged() { } @@ -2613,11 +2692,11 @@ public abstract class AccessibilityService extends Service { mCallback.init(mConnectionId, windowToken); mCallback.onServiceConnected(); } else { + AccessibilityInteractionClient.getInstance(mContext) + .clearCache(mConnectionId); AccessibilityInteractionClient.getInstance(mContext).removeConnection( mConnectionId); mConnectionId = AccessibilityInteractionClient.NO_ID; - AccessibilityInteractionClient.getInstance(mContext) - .clearCache(mConnectionId); mCallback.init(AccessibilityInteractionClient.NO_ID, null); } return; diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl index 93e69145f90c3..7d76bbf350814 100644 --- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl +++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl @@ -124,6 +124,8 @@ interface IAccessibilityServiceConnection { void setFocusAppearance(int strokeWidth, int color); + void setCacheEnabled(boolean enabled); + oneway void logTrace(long timestamp, String where, long loggingTypes, String callingParams, int processId, long threadId, int callingUid, in Bundle serializedCallingStackInBundle); diff --git a/core/java/android/view/accessibility/AccessibilityCache.java b/core/java/android/view/accessibility/AccessibilityCache.java index 91ef8a5600130..e6385a5ecbd59 100644 --- a/core/java/android/view/accessibility/AccessibilityCache.java +++ b/core/java/android/view/accessibility/AccessibilityCache.java @@ -46,6 +46,8 @@ public class AccessibilityCache { private static final boolean CHECK_INTEGRITY = Build.IS_ENG; + private boolean mEnabled = true; + /** * {@link AccessibilityEvent} types that are critical for the cache to stay up to date * @@ -98,6 +100,21 @@ public class AccessibilityCache { mAccessibilityNodeRefresher = nodeRefresher; } + /** Returns if the cache is enabled. */ + public boolean isEnabled() { + synchronized (mLock) { + return mEnabled; + } + } + + /** Sets enabled status. */ + public void setEnabled(boolean enabled) { + synchronized (mLock) { + mEnabled = enabled; + clear(); + } + } + /** * Sets all {@link AccessibilityWindowInfo}s of all displays into the cache. * The key of SparseArray is display ID. @@ -110,6 +127,12 @@ public class AccessibilityCache { SparseArray> windowsOnAllDisplays, long populationTimeStamp) { synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return; + } if (DEBUG) { Log.i(LOG_TAG, "Set windows"); } @@ -148,6 +171,12 @@ public class AccessibilityCache { */ public void addWindow(AccessibilityWindowInfo window) { synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return; + } if (DEBUG) { Log.i(LOG_TAG, "Caching window: " + window.getId() + " at display Id [ " + window.getDisplayId() + " ]"); @@ -177,6 +206,12 @@ public class AccessibilityCache { public void onAccessibilityEvent(AccessibilityEvent event) { AccessibilityNodeInfo nodeToRefresh = null; synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return; + } if (DEBUG) { Log.i(LOG_TAG, "onAccessibilityEvent(" + event + ")"); } @@ -292,6 +327,12 @@ public class AccessibilityCache { */ public AccessibilityNodeInfo getNode(int windowId, long accessibilityNodeId) { synchronized(mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return null; + } LongSparseArray nodes = mNodeCache.get(windowId); if (nodes == null) { return null; @@ -309,6 +350,28 @@ public class AccessibilityCache { } } + /** Returns {@code true} if {@code info} is in the cache. */ + public boolean isNodeInCache(AccessibilityNodeInfo info) { + if (info == null) { + return false; + } + int windowId = info.getWindowId(); + long accessibilityNodeId = info.getSourceNodeId(); + synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return false; + } + LongSparseArray nodes = mNodeCache.get(windowId); + if (nodes == null) { + return false; + } + return nodes.get(accessibilityNodeId) != null; + } + } + /** * Gets all {@link AccessibilityWindowInfo}s of all displays from the cache. * @@ -317,6 +380,12 @@ public class AccessibilityCache { */ public SparseArray> getWindowsOnAllDisplays() { synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return null; + } if (!mIsAllWindowsCached) { return null; } @@ -373,6 +442,12 @@ public class AccessibilityCache { */ public AccessibilityWindowInfo getWindow(int windowId) { synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return null; + } final int displayCounts = mWindowCacheByDisplay.size(); for (int i = 0; i < displayCounts; i++) { final SparseArray windowsOfDisplay = @@ -397,6 +472,12 @@ public class AccessibilityCache { */ public void add(AccessibilityNodeInfo info) { synchronized(mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return; + } if (VERBOSE) { Log.i(LOG_TAG, "add(" + info + ")"); } @@ -522,6 +603,12 @@ public class AccessibilityCache { */ public AccessibilityNodeInfo getFocus(int focusType, long initialNodeId, int windowId) { synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return null; + } int currentFocusWindowId; long currentFocusId; if (focusType == FOCUS_ACCESSIBILITY) { @@ -602,6 +689,23 @@ public class AccessibilityCache { mNodeCache.remove(windowId); } + /** Clears a subtree rooted at {@code info}. */ + public boolean clearSubTree(AccessibilityNodeInfo info) { + if (info == null) { + return false; + } + synchronized (mLock) { + if (!mEnabled) { + if (DEBUG) { + Log.i(LOG_TAG, "Cache is disabled"); + } + return false; + } + clearSubTreeLocked(info.getWindowId(), info.getSourceNodeId()); + return true; + } + } + /** * Clears a subtree rooted at the node with the given id that is * hosted in a given window. diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index dc4c59a1e1ec6..6f4bc719dc84e 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -18,6 +18,7 @@ package android.view.accessibility; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CLIENT; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_INTERACTION_CONNECTION_CALLBACK; +import static android.os.Build.VERSION_CODES.S; import android.accessibilityservice.IAccessibilityServiceConnection; import android.annotation.NonNull; @@ -225,6 +226,9 @@ public final class AccessibilityInteractionClient */ public static void addConnection(int connectionId, IAccessibilityServiceConnection connection, boolean initializeCache) { + if (connectionId == NO_ID) { + return; + } synchronized (sConnectionCache) { sConnectionCache.put(connectionId, connection); if (!initializeCache) { @@ -554,6 +558,10 @@ public final class AccessibilityInteractionClient } return cachedInfo; } + if (!cache.isEnabled()) { + // Skip prefetching if cache is disabled. + prefetchFlags &= ~AccessibilityNodeInfo.FLAG_PREFETCH_MASK; + } if (DEBUG) { Log.i(LOG_TAG, "Node cache miss for " + idToString(accessibilityWindowId, accessibilityNodeId)); @@ -970,9 +978,9 @@ public final class AccessibilityInteractionClient /** * Clears the cache associated with {@code connectionId} * @param connectionId the connection id - * TODO(207417185): Modify UnsupportedAppUsage */ - @UnsupportedAppUsage() + @UnsupportedAppUsage(maxTargetSdk = S, publicAlternatives = + "{@link android.accessibilityservice.AccessibilityService#clearCache()}") public void clearCache(int connectionId) { AccessibilityCache cache = getCache(connectionId); if (cache == null) { diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java index e689b5d331075..dd8cc6e0dd03e 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityCacheTest.java @@ -26,6 +26,8 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyObject; @@ -997,6 +999,60 @@ public class AccessibilityCacheTest { } } + @Test + public void enable_cacheEnabled() { + mAccessibilityCache.setEnabled(false); + assertFalse(mAccessibilityCache.isEnabled()); + + mAccessibilityCache.setEnabled(true); + assertTrue(mAccessibilityCache.isEnabled()); + } + + @Test + public void disable_cacheDisabled() { + mAccessibilityCache.setEnabled(false); + assertFalse(mAccessibilityCache.isEnabled()); + } + + @Test + public void queryNode_nodeIsInCache() { + AccessibilityNodeInfo info = new AccessibilityNodeInfo(); + mAccessibilityCache.add(info); + + assertTrue(mAccessibilityCache.isNodeInCache(info)); + } + + @Test + public void clearSubtreeWithNode_nodeInCacheInvalidated() { + AccessibilityNodeInfo info = new AccessibilityNodeInfo(); + info.setSource(getMockViewWithA11yAndWindowIds(1, 1)); + mAccessibilityCache.add(info); + + mAccessibilityCache.clearSubTree(info); + assertFalse(mAccessibilityCache.isNodeInCache(info)); + } + + @Test + public void clearSubtreeWithNode_subtreeInCacheInvalidated() { + AccessibilityNodeInfo info = new AccessibilityNodeInfo(); + View parentView = getMockViewWithA11yAndWindowIds(1, 1); + info.setSource(parentView); + + AccessibilityNodeInfo childInfo = new AccessibilityNodeInfo(); + View childView = getMockViewWithA11yAndWindowIds(2, 1); + childInfo.setSource(childView); + + childInfo.setParent(parentView); + info.addChild(childView); + mAccessibilityCache.add(info); + mAccessibilityCache.add(childInfo); + + mAccessibilityCache.clearSubTree(info); + + assertFalse(mAccessibilityCache.isNodeInCache(info)); + assertFalse(mAccessibilityCache.isNodeInCache(childInfo)); + } + private AccessibilityWindowInfo obtainAccessibilityWindowInfo(int windowId, int layer) { AccessibilityWindowInfo windowInfo = AccessibilityWindowInfo.obtain(); windowInfo.setId(windowId); diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java index ad1f298e57a17..62d0b2e0b52fd 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java @@ -173,6 +173,8 @@ public class AccessibilityServiceConnectionImpl extends IAccessibilityServiceCon public void setFocusAppearance(int strokeWidth, int color) {} + public void setCacheEnabled(boolean enabled) {} + public void logTrace(long timestamp, String where, String callingParams, int processId, long threadId, int callingUid, Bundle callingStack) {} diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index f050b6622a5d4..ad3e1d56c51bf 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -2032,6 +2032,22 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } } + @Override + public void setCacheEnabled(boolean enabled) { + if (svcConnTracingEnabled()) { + logTraceSvcConn("setCacheEnabled", "enabled=" + enabled); + } + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + mUsesAccessibilityCache = enabled; + mSystemSupport.onClientChangeLocked(true); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public void logTrace(long timestamp, String where, long loggingTypes, String callingParams, int processId, long threadId, int callingUid, Bundle callingStack) {