diff options
author | Svetoslav <svetoslavganov@google.com> | 2013-06-04 17:22:14 -0700 |
---|---|---|
committer | Svetoslav <svetoslavganov@google.com> | 2013-06-05 15:16:05 -0700 |
commit | 6254f4806dd3db53b7380e77fbb183065685573e (patch) | |
tree | 554ff88119ae63465cbd4bd51a7e26ef0869cddb /core/java/android/view/accessibility | |
parent | 85de5f420efaaf20c9dcaf09035b0fb3980b76ab (diff) | |
download | frameworks_base-6254f4806dd3db53b7380e77fbb183065685573e.zip frameworks_base-6254f4806dd3db53b7380e77fbb183065685573e.tar.gz frameworks_base-6254f4806dd3db53b7380e77fbb183065685573e.tar.bz2 |
Optimizing AccessibilityNodeInfo caching.
1. Before we were firing an accessibility event from the common
predecessor of views with accessibility related state changes
every X amount of time. These events designate that the tree
rooted at the source is invalid and should not be cached.
However, some of the state changes do not affect the view tree
structure and we can just refresh the node instead of evicting
and recaching nodes infos for views that did not change. Hence,
we need a way to distinguish between a subtree changed over a
node changed.
Adding a new event type will not work since if say two siblings
have local changes and their predecessor fires a window state
change event, the client will drop the subtree rooted at the
parent including the two views with changes. Subsequent, more
specialized events emitted from the two changed siblings will
be useless since the parent which did not changed is already
evicted from the cache. Conversely, if the specialized events
are fired from the two siblings with local changes and they
are refreshed in the cache the subsequent window state change
event from the common predecessor will force the refreshed
nodes to be evicted.
Hence, to enable distinction between node being changed and
a subtree baing changed while not changing existing behavior,
we will fire only window content change event with an additional
argument specifying what changed - node or a subtree for now.
Also if the changes are local to a view we fire the window
content changed event from the view. So, the two siblings will
fire such an event independently and the client will know that
these are local changes and can just refresh the node. If the
changes are structural, then we fire the window state change
event from the common predecessor.
2. Added the input type of a text view as one of the properties
reported by an AccessibilityNodeInfo. It is nice to prompt the
user what input is expected.
3. Added a bundle for optional information to AccessiiblityNodeInfo.
For example, it will be used for putting web specific properties
that do not map cleanly to Android specific ones in WebView.
4. AccessibilityInteractionController was not taking into account
whether the current accessibility focused node is shown before
returing it. Hence, a disconnected node would be returned and
caching it puts our cahche in an inconsistent state.
Change-Id: I8ed19cfb4a70bdd7597c3f105487f1651cffd9e0
Diffstat (limited to 'core/java/android/view/accessibility')
5 files changed, 201 insertions, 79 deletions
diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index dbeca1f..82c8163 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -326,6 +326,7 @@ import java.util.List; * <em>Properties:</em></br> * <ul> * <li>{@link #getEventType()} - The type of the event.</li> + * <li>{@link #getContentChangeType()} - The type of content change.</li> * <li>{@link #getSource()} - The source info (for registered clients).</li> * <li>{@link #getClassName()} - The class name of the source.</li> * <li>{@link #getPackageName()} - The package name of the source.</li> @@ -661,6 +662,18 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The subtree rooted at the source node changed. + */ + public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * Only the source node changed. + */ + public static final int CONTENT_CHANGE_TYPE_NODE = 1; + + /** * Mask for {@link AccessibilityEvent} all types. * * @see #TYPE_VIEW_CLICKED @@ -695,6 +708,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par private long mEventTime; int mMovementGranularity; int mAction; + int mContentChangeType; private final ArrayList<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>(); @@ -714,6 +728,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mEventType = event.mEventType; mMovementGranularity = event.mMovementGranularity; mAction = event.mAction; + mContentChangeType = event.mContentChangeType; mEventTime = event.mEventTime; mPackageName = event.mPackageName; } @@ -777,6 +792,33 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par } /** + * Gets the type of node tree change signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. + * + * @see #CONTENT_CHANGE_TYPE_NODE + * @see #CONTENT_CHANGE_TYPE_SUBTREE + * + * @return The change type. + */ + public int getContentChangeType() { + return mContentChangeType; + } + + /** + * Sets the type of node tree change signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. + * + * @see #CONTENT_CHANGE_TYPE_NODE + * @see #CONTENT_CHANGE_TYPE_SUBTREE + * + * @param changeType The change type. + */ + public void setContentChangeType(int changeType) { + enforceNotSealed(); + mContentChangeType = changeType; + } + + /** * Sets the event type. * * @param eventType The event type. @@ -943,6 +985,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mEventType = 0; mMovementGranularity = 0; mAction = 0; + mContentChangeType = 0; mPackageName = null; mEventTime = 0; while (!mRecords.isEmpty()) { @@ -961,6 +1004,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par mEventType = parcel.readInt(); mMovementGranularity = parcel.readInt(); mAction = parcel.readInt(); + mContentChangeType = parcel.readInt(); mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel); mEventTime = parcel.readLong(); mConnectionId = parcel.readInt(); @@ -1013,6 +1057,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par parcel.writeInt(mEventType); parcel.writeInt(mMovementGranularity); parcel.writeInt(mAction); + parcel.writeInt(mContentChangeType); TextUtils.writeToParcel(mPackageName, parcel, 0); parcel.writeLong(mEventTime); parcel.writeInt(mConnectionId); @@ -1074,6 +1119,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par builder.append(super.toString()); if (DEBUG) { builder.append("\n"); + builder.append("; ContentChangeType: ").append(mContentChangeType); builder.append("; sourceWindowId: ").append(mSourceWindowId); builder.append("; mSourceNodeId: ").append(mSourceNodeId); for (int i = 0; i < mRecords.size(); i++) { diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 84d7e72..139df3e 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -163,7 +163,7 @@ public final class AccessibilityInteractionClient public AccessibilityNodeInfo getRootInActiveWindow(int connectionId) { return findAccessibilityNodeInfoByAccessibilityId(connectionId, AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, - AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + false, AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); } /** @@ -177,18 +177,22 @@ public final class AccessibilityInteractionClient * where to start the search. Use * {@link android.view.accessibility.AccessibilityNodeInfo#ROOT_NODE_ID} * to start from the root. + * @param bypassCache Whether to bypass the cache while looking for the node. * @param prefetchFlags flags to guide prefetching. * @return An {@link AccessibilityNodeInfo} if found, null otherwise. */ public AccessibilityNodeInfo findAccessibilityNodeInfoByAccessibilityId(int connectionId, - int accessibilityWindowId, long accessibilityNodeId, int prefetchFlags) { + int accessibilityWindowId, long accessibilityNodeId, boolean bypassCache, + int prefetchFlags) { try { IAccessibilityServiceConnection connection = getConnection(connectionId); if (connection != null) { - AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( - accessibilityNodeId); - if (cachedInfo != null) { - return cachedInfo; + if (!bypassCache) { + AccessibilityNodeInfo cachedInfo = sAccessibilityNodeInfoCache.get( + accessibilityNodeId); + if (cachedInfo != null) { + return cachedInfo; + } } final int interactionId = mInteractionIdCounter.getAndIncrement(); final boolean success = connection.findAccessibilityNodeInfoByAccessibilityId( @@ -350,7 +354,7 @@ public final class AccessibilityInteractionClient } } catch (RemoteException re) { if (DEBUG) { - Log.w(LOG_TAG, "Error while calling remote findAccessibilityFocus", re); + Log.w(LOG_TAG, "Error while calling remote findFocus", re); } } return null; diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index d9c9b69..750e022 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -21,6 +21,7 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.text.InputType; import android.util.Pools.SynchronizedPool; import android.util.SparseLongArray; import android.view.View; @@ -482,6 +483,9 @@ public class AccessibilityNodeInfo implements Parcelable { private int mTextSelectionStart = UNDEFINED; private int mTextSelectionEnd = UNDEFINED; + private int mInputType = InputType.TYPE_NULL; + + private Bundle mBundle; private int mConnectionId = UNDEFINED; @@ -594,16 +598,20 @@ public class AccessibilityNodeInfo implements Parcelable { * since it represents a view that is no longer in the view tree and should * be recycled. * </p> + * + * @param bypassCache Whether to bypass the cache. * @return Whether the refresh succeeded. + * + * @hide */ - public boolean refresh() { + public boolean refresh(boolean bypassCache) { enforceSealed(); if (!canPerformRequestOverConnection(mSourceNodeId)) { return false; } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); AccessibilityNodeInfo refreshedInfo = client.findAccessibilityNodeInfoByAccessibilityId( - mConnectionId, mWindowId, mSourceNodeId, 0); + mConnectionId, mWindowId, mSourceNodeId, bypassCache, 0); if (refreshedInfo == null) { return false; } @@ -613,6 +621,19 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Refreshes this info with the latest state of the view it represents. + * <p> + * <strong>Note:</strong> If this method returns false this info is obsolete + * since it represents a view that is no longer in the view tree and should + * be recycled. + * </p> + * @return Whether the refresh succeeded. + */ + public boolean refresh() { + return refresh(false); + } + + /** * @return The ids of the children. * * @hide @@ -652,7 +673,7 @@ public class AccessibilityNodeInfo implements Parcelable { final long childId = mChildNodeIds.get(index); AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mWindowId, - childId, FLAG_PREFETCH_DESCENDANTS); + childId, false, FLAG_PREFETCH_DESCENDANTS); } /** @@ -878,7 +899,7 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mParentNodeId, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mParentNodeId, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -1470,7 +1491,7 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabelForId, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mLabelForId, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -1527,7 +1548,7 @@ public class AccessibilityNodeInfo implements Parcelable { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, - mWindowId, mLabeledById, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); + mWindowId, mLabeledById, false, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS); } /** @@ -1600,6 +1621,52 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets the input type of the source as defined by {@link InputType}. + * + * @return The input type. + */ + public int getInputType() { + return mInputType; + } + + /** + * Sets the input type of the source as defined by {@link InputType}. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an + * AccessibilityService. + * </p> + * + * @param inputType The input type. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setInputType(int inputType) { + mInputType = inputType; + } + + /** + * Gets an optional bundle with additional data. The bundle + * is lazily created and never <code>null</code>. + * <p> + * <strong>Note:</strong> It is recommended to use the package + * name of your application as a prefix for the keys to avoid + * collisions which may confuse an accessibility service if the + * same key has different meaning when emitted from different + * applications. + * </p> + * + * @return The bundle. + */ + public Bundle getBundle() { + if (mBundle == null) { + mBundle = new Bundle(); + } + return mBundle; + } + + /** * Gets the value of a boolean property. * * @param property The property. @@ -1845,6 +1912,14 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeInt(mTextSelectionStart); parcel.writeInt(mTextSelectionEnd); + parcel.writeInt(mInputType); + + if (mBundle != null) { + parcel.writeInt(1); + parcel.writeBundle(mBundle); + } else { + parcel.writeInt(0); + } // Since instances of this class are fetched via synchronous i.e. blocking // calls in IPCs we always recycle as soon as the instance is marshaled. @@ -1880,6 +1955,10 @@ public class AccessibilityNodeInfo implements Parcelable { } mTextSelectionStart = other.mTextSelectionStart; mTextSelectionEnd = other.mTextSelectionEnd; + mInputType = other.mInputType; + if (other.mBundle != null && !other.mBundle.isEmpty()) { + getBundle().putAll(other.mBundle); + } } /** @@ -1927,6 +2006,11 @@ public class AccessibilityNodeInfo implements Parcelable { mTextSelectionStart = parcel.readInt(); mTextSelectionEnd = parcel.readInt(); + mInputType = parcel.readInt(); + + if (parcel.readInt() == 1) { + getBundle().putAll(parcel.readBundle()); + } } /** @@ -1953,6 +2037,10 @@ public class AccessibilityNodeInfo implements Parcelable { mActions = 0; mTextSelectionStart = UNDEFINED; mTextSelectionEnd = UNDEFINED; + mInputType = InputType.TYPE_NULL; + if (mBundle != null) { + mBundle.clear(); + } } /** diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java index 28518aa..dded74c 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfoCache.java @@ -39,9 +39,9 @@ public class AccessibilityNodeInfoCache { private static final boolean ENABLED = true; - private static final boolean DEBUG = false; + private static final boolean DEBUG = true; - private static final boolean CHECK_INTEGRITY = true; + private static final boolean CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD = true; private final Object mLock = new Object(); @@ -67,16 +67,12 @@ public class AccessibilityNodeInfoCache { 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_WINDOW_STATE_CHANGED: case AccessibilityEvent.TYPE_VIEW_HOVER_ENTER: case AccessibilityEvent.TYPE_VIEW_HOVER_EXIT: { final int windowId = event.getWindowId(); + // If a new window, we clear the cache. if (mWindowId != windowId) { - // New window so we clear the cache. mWindowId = windowId; clear(); } @@ -87,34 +83,48 @@ public class AccessibilityNodeInfoCache { 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); - } - } + refreshCachedNode(event.getSourceNodeId()); } break; - case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: case AccessibilityEvent.TYPE_VIEW_SCROLLED: { + clearSubTreeLocked(event.getSourceNodeId()); + } break; + case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { synchronized (mLock) { - final long accessibilityNodeId = event.getSourceNodeId(); - clearSubTreeLocked(accessibilityNodeId); + final long sourceId = event.getSourceNodeId(); + if (event.getContentChangeType() + == AccessibilityEvent.CONTENT_CHANGE_TYPE_NODE) { + refreshCachedNode(sourceId); + } else { + clearSubTreeLocked(sourceId); + } } } break; } - if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY) { + if (Build.IS_DEBUGGABLE && CHECK_INTEGRITY_IF_DEBUGGABLE_BUILD) { checkIntegrity(); } } } + private void refreshCachedNode(long sourceId) { + if (DEBUG) { + Log.i(LOG_TAG, "Refresing cached node."); + } + synchronized (mLock) { + AccessibilityNodeInfo cachedInfo = mCacheImpl.get(sourceId); + // If the source is not in the cache - nothing to do. + if (cachedInfo == null) { + return; + } + // The node changed so we will just refresh it right now. + if (cachedInfo.refresh(false)) { + return; + } + // Weird, we could not refresh. Just evict the entire sub-tree. + clearSubTreeLocked(sourceId); + } + } + /** * Gets a cached {@link AccessibilityNodeInfo} given its accessibility node id. * @@ -131,7 +141,7 @@ public class AccessibilityNodeInfoCache { info = AccessibilityNodeInfo.obtain(info); } if (DEBUG) { - Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); +// Log.i(LOG_TAG, "get(" + accessibilityNodeId + ") = " + info); } return info; } @@ -149,7 +159,7 @@ public class AccessibilityNodeInfoCache { if (ENABLED) { synchronized(mLock) { if (DEBUG) { - Log.i(LOG_TAG, "add(" + info + ")"); +// Log.i(LOG_TAG, "add(" + info + ")"); } final long sourceId = info.getSourceNodeId(); @@ -212,6 +222,13 @@ public class AccessibilityNodeInfoCache { * @param rootNodeId The root id. */ private void clearSubTreeLocked(long rootNodeId) { + if (DEBUG) { + Log.i(LOG_TAG, "Clearing cached subtree."); + } + clearSubTreeRecursiveLocked(rootNodeId); + } + + private void clearSubTreeRecursiveLocked(long rootNodeId) { AccessibilityNodeInfo current = mCacheImpl.get(rootNodeId); if (current == null) { return; @@ -221,41 +238,7 @@ public class AccessibilityNodeInfoCache { 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; - } + clearSubTreeRecursiveLocked(childNodeId); } } @@ -327,16 +310,17 @@ public class AccessibilityNodeInfoCache { } // Check for disconnected nodes or ones from another window. - final int cacheSize = mCacheImpl.size(); - for (int i = 0; i < cacheSize; i++) { + for (int i = 0; i < mCacheImpl.size(); i++) { AccessibilityNodeInfo info = mCacheImpl.valueAt(i); if (!seen.contains(info)) { if (info.getWindowId() == windowId) { - Log.e(LOG_TAG, "Disconneced node: "); + Log.e(LOG_TAG, "Disconneced node: " + info); } else { Log.e(LOG_TAG, "Node from: " + info.getWindowId() + " not from:" + windowId + " " + info); } + mCacheImpl.removeAt(i); + i--; } } } diff --git a/core/java/android/view/accessibility/AccessibilityRecord.java b/core/java/android/view/accessibility/AccessibilityRecord.java index 7147c57..3fcd218 100644 --- a/core/java/android/view/accessibility/AccessibilityRecord.java +++ b/core/java/android/view/accessibility/AccessibilityRecord.java @@ -164,7 +164,7 @@ public class AccessibilityRecord { } AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance(); return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId, mSourceWindowId, - mSourceNodeId, GET_SOURCE_PREFETCH_FLAGS); + mSourceNodeId, false, GET_SOURCE_PREFETCH_FLAGS); } /** |