From 77e9a28e2faa36f127231b842476d47f9823a83a Mon Sep 17 00:00:00 2001 From: Alan Viverette Date: Thu, 12 Sep 2013 17:16:09 -0700 Subject: Add live region politeness to View, AccessibilityNodeInfo Alters the content change API to contain a bit mask of types of changes represented by the event. Live regions send CONTENT_CHANGED events immediately. Removes unused APIs for EXPANDABLE/EXPANDED. BUG: 10527284 Change-Id: I21523e85e47df23706976dc0a8bf615f83072c04 --- core/java/android/view/View.java | 180 +++++++++++++++++++-- core/java/android/view/ViewGroup.java | 11 +- core/java/android/view/ViewParent.java | 21 ++- core/java/android/view/ViewRootImpl.java | 17 +- .../view/accessibility/AccessibilityEvent.java | 67 +++++--- .../view/accessibility/AccessibilityNodeInfo.java | 114 ++++--------- .../accessibility/AccessibilityNodeInfoCache.java | 8 +- 7 files changed, 278 insertions(+), 140 deletions(-) (limited to 'core/java/android/view') diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 8f8f9c6..a5db6ee 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2134,6 +2134,50 @@ public class View implements Drawable.Callback, KeyEvent.Callback, << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT; /** + * Shift for the bits in {@link #mPrivateFlags2} related to the + * "accessibilityLiveRegion" attribute. + */ + static final int PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT = 22; + + /** + * Live region mode specifying that accessibility services should not + * automatically announce changes to this view. This is the default live + * region mode for most views. + *

+ * Use with {@link #setAccessibilityLiveRegion(int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000; + + /** + * Live region mode specifying that accessibility services should announce + * changes to this view. + *

+ * Use with {@link #setAccessibilityLiveRegion(int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001; + + /** + * Live region mode specifying that accessibility services should interrupt + * ongoing speech to immediately announce changes to this view. + *

+ * Use with {@link #setAccessibilityLiveRegion(int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 0x00000002; + + /** + * The default whether the view is important for accessibility. + */ + static final int ACCESSIBILITY_LIVE_REGION_DEFAULT = ACCESSIBILITY_LIVE_REGION_NONE; + + /** + * Mask for obtaining the bits which specify a view's accessibility live + * region mode. + */ + static final int PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK = (ACCESSIBILITY_LIVE_REGION_NONE + | ACCESSIBILITY_LIVE_REGION_POLITE | ACCESSIBILITY_LIVE_REGION_ASSERTIVE) + << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT; + + /** * Flag indicating whether a view has accessibility focus. */ static final int PFLAG2_ACCESSIBILITY_FOCUSED = 0x04000000; @@ -3763,6 +3807,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setImportantForAccessibility(a.getInt(attr, IMPORTANT_FOR_ACCESSIBILITY_DEFAULT)); break; + case R.styleable.View_accessibilityLiveRegion: + setAccessibilityLiveRegion(a.getInt(attr, ACCESSIBILITY_LIVE_REGION_DEFAULT)); + break; } } @@ -4710,7 +4757,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (gainFocus) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); } else { - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } InputMethodManager imm = InputMethodManager.peekInstance(); @@ -5431,7 +5479,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); notifySubtreeAccessibilityStateChangedIfNeeded(); } else { - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION); } } @@ -6954,6 +7003,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the live region mode for this view. This indicates to accessibility + * services whether they should automatically notify the user about changes + * to the view's content description or text, or to the content descriptions + * or text of the view's children (where applicable). + *

+ * For example, in a login screen with a TextView that displays an "incorrect + * password" notification, that view should be marked as a live region with + * mode {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

+ * To disable change notifications for this view, use + * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region + * mode for most views. + *

+ * To indicate that the user should be notified of changes, use + * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

+ * If the view's changes should interrupt ongoing speech and notify the user + * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. + * + * @param mode The live region mode for this view, one of: + *

+ * @attr ref android.R.styleable#View_accessibilityLiveRegion + */ + public void setAccessibilityLiveRegion(int mode) { + if (mode != getAccessibilityLiveRegion()) { + mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK; + mPrivateFlags2 |= (mode << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT) + & PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK; + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); + } + } + + /** + * Gets the live region mode for this View. + * + * @return The live region mode for the view. + * + * @attr ref android.R.styleable#View_accessibilityLiveRegion + * + * @see #setAccessibilityLiveRegion(int) + */ + public int getAccessibilityLiveRegion() { + return (mPrivateFlags2 & PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK) + >> PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT; + } + + /** * Sets how to determine whether this view is important for accessibility * which is if it fires accessibility events and if it is reported to * accessibility services that query the screen. @@ -6975,7 +7076,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (oldIncludeForAccessibility != includeForAccessibility()) { notifySubtreeAccessibilityStateChangedIfNeeded(); } else { - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } } @@ -6997,7 +7099,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; case IMPORTANT_FOR_ACCESSIBILITY_AUTO: return isActionableForAccessibility() || hasListenersForAccessibility() - || getAccessibilityNodeProvider() != null; + || getAccessibilityNodeProvider() != null + || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE; default: throw new IllegalArgumentException("Unknow important for accessibility mode: " + mode); @@ -7094,7 +7197,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @hide */ - public void notifyViewAccessibilityStateChangedIfNeeded() { + public void notifyViewAccessibilityStateChangedIfNeeded(int changeType) { if (!AccessibilityManager.getInstance(mContext).isEnabled()) { return; } @@ -7102,7 +7205,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mSendViewStateChangedAccessibilityEvent = new SendViewStateChangedAccessibilityEvent(); } - mSendViewStateChangedAccessibilityEvent.runOrPost(); + mSendViewStateChangedAccessibilityEvent.runOrPost(changeType); } /** @@ -7124,7 +7227,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED; if (mParent != null) { try { - mParent.childAccessibilityStateChanged(this); + mParent.notifySubtreeAccessibilityStateChanged( + this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); } catch (AbstractMethodError e) { Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() + " does not fully implement ViewParent", e); @@ -7251,7 +7355,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, || getAccessibilitySelectionEnd() != end) && (start == end)) { setAccessibilitySelection(start, end); - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); return true; } } break; @@ -8825,11 +8930,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (oldIncludeForAccessibility != includeForAccessibility()) { notifySubtreeAccessibilityStateChangedIfNeeded(); } else { - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } - } - if ((changed & ENABLED_MASK) != 0) { - notifyViewAccessibilityStateChangedIfNeeded(); + } else if ((changed & ENABLED_MASK) != 0) { + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } } @@ -15430,7 +15536,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); refreshDrawableState(); dispatchSetSelected(selected); - notifyViewAccessibilityStateChangedIfNeeded(); + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } } @@ -19172,21 +19279,44 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } private class SendViewStateChangedAccessibilityEvent implements Runnable { + private int mChangeTypes = 0; private boolean mPosted; + private boolean mPostedWithDelay; private long mLastEventTimeMillis; + @Override public void run() { mPosted = false; + mPostedWithDelay = false; mLastEventTimeMillis = SystemClock.uptimeMillis(); if (AccessibilityManager.getInstance(mContext).isEnabled()) { - AccessibilityEvent event = AccessibilityEvent.obtain(); + final AccessibilityEvent event = AccessibilityEvent.obtain(); event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - event.setContentChangeType(AccessibilityEvent.CONTENT_CHANGE_TYPE_NODE); + event.setContentChangeTypes(mChangeTypes); sendAccessibilityEventUnchecked(event); } + mChangeTypes = 0; } - public void runOrPost() { + public void runOrPost(int changeType) { + mChangeTypes |= changeType; + + // If this is a live region or the child of a live region, collect + // all events from this frame and send them on the next frame. + if (inLiveRegion()) { + // If we're already posted with a delay, remove that. + if (mPostedWithDelay) { + removeCallbacks(this); + mPostedWithDelay = false; + } + // Only post if we're not already posted. + if (!mPosted) { + post(this); + mPosted = true; + } + return; + } + if (mPosted) { return; } @@ -19199,10 +19329,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } else { postDelayed(this, minEventIntevalMillis - timeSinceLastMillis); mPosted = true; + mPostedWithDelay = true; } } } + private boolean inLiveRegion() { + if (getAccessibilityLiveRegion() != View.ACCESSIBILITY_LIVE_REGION_NONE) { + return true; + } + + ViewParent parent = getParent(); + while (parent instanceof View) { + if (((View) parent).getAccessibilityLiveRegion() + != View.ACCESSIBILITY_LIVE_REGION_NONE) { + return true; + } + parent = parent.getParent(); + } + + return false; + } + /** * Dump all private flags in readable format, useful for documentation and * sanity checking. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index faeee3f..9414237 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -38,6 +38,7 @@ import android.util.Log; import android.util.Pools.SynchronizedPool; import android.util.SparseArray; import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.Animation; import android.view.animation.AnimationUtils; @@ -2528,10 +2529,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } @Override - public void childAccessibilityStateChanged(View root) { - if (mParent != null) { + public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) { + // If this is a live region, we should send a subtree change event + // from this view. Otherwise, we can let it propagate up. + if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) { + notifyViewAccessibilityStateChangedIfNeeded(changeType); + } else if (mParent != null) { try { - mParent.childAccessibilityStateChanged(root); + mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType); } catch (AbstractMethodError e) { Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() + " does not fully implement ViewParent", e); diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index 35113db..0137693 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -309,12 +309,21 @@ public interface ViewParent { public ViewParent getParentForAccessibility(); /** - * A child notifies its parent that the accessibility state of a subtree rooted - * at a given node changed. That is the structure of the subtree is different. - * - * @param root The root of the changed subtree. - */ - public void childAccessibilityStateChanged(View root); + * Notifies a view parent that the accessibility state of one of its + * descendants has changed and that the structure of the subtree is + * different. + * @param child The direct child whose subtree has changed. + * @param source The descendant view that changed. + * @param changeType A bit mask of the types of changes that occurred. One + * or more of: + * + */ + public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType); /** * Tells if this view parent can resolve the layout direction. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 50d5d45..38f28ae 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5807,12 +5807,12 @@ public final class ViewRootImpl implements ViewParent, * This event is send at most once every * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}. */ - private void postSendWindowContentChangedCallback(View source) { + private void postSendWindowContentChangedCallback(View source, int changeType) { if (mSendWindowContentChangedAccessibilityEvent == null) { mSendWindowContentChangedAccessibilityEvent = new SendWindowContentChangedAccessibilityEvent(); } - mSendWindowContentChangedAccessibilityEvent.runOrPost(source); + mSendWindowContentChangedAccessibilityEvent.runOrPost(source, changeType); } /** @@ -5884,8 +5884,8 @@ public final class ViewRootImpl implements ViewParent, } @Override - public void childAccessibilityStateChanged(View child) { - postSendWindowContentChangedCallback(child); + public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) { + postSendWindowContentChangedCallback(source, changeType); } @Override @@ -6538,6 +6538,8 @@ public final class ViewRootImpl implements ViewParent, } private class SendWindowContentChangedAccessibilityEvent implements Runnable { + private int mChangeTypes = 0; + public View mSource; public long mLastEventTimeMillis; @@ -6548,7 +6550,7 @@ public final class ViewRootImpl implements ViewParent, mLastEventTimeMillis = SystemClock.uptimeMillis(); AccessibilityEvent event = AccessibilityEvent.obtain(); event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - event.setContentChangeType(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); + event.setContentChangeTypes(mChangeTypes); mSource.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -6556,17 +6558,20 @@ public final class ViewRootImpl implements ViewParent, // In any case reset to initial state. mSource.resetSubtreeAccessibilityStateChanged(); mSource = null; + mChangeTypes = 0; } - public void runOrPost(View source) { + public void runOrPost(View source, int changeType) { if (mSource != null) { // If there is no common predecessor, then mSource points to // a removed view, hence in this case always prefer the source. View predecessor = getCommonPredecessor(mSource, source); mSource = (predecessor != null) ? predecessor : source; + mChangeTypes |= changeType; return; } mSource = source; + mChangeTypes = changeType; final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis; final long minEventIntevalMillis = ViewConfiguration.getSendRecurringAccessibilityEventsInterval(); diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 82c8163..7e2bffa 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -326,7 +326,7 @@ import java.util.List; * Properties:
*