diff --git a/api/current.txt b/api/current.txt index 9a349a5c61369..7a5d687ffff27 100644 --- a/api/current.txt +++ b/api/current.txt @@ -25168,6 +25168,7 @@ package android.view.accessibility { method public void appendRecord(android.view.accessibility.AccessibilityRecord); method public int describeContents(); method public static java.lang.String eventTypeToString(int); + method public int getAction(); method public long getEventTime(); method public int getEventType(); method public int getMovementGranularity(); @@ -25178,6 +25179,7 @@ package android.view.accessibility { method public static android.view.accessibility.AccessibilityEvent obtain(int); method public static android.view.accessibility.AccessibilityEvent obtain(android.view.accessibility.AccessibilityEvent); method public static android.view.accessibility.AccessibilityEvent obtain(); + method public void setAction(int); method public void setEventTime(long); method public void setEventType(int); method public void setMovementGranularity(int); diff --git a/core/java/android/view/AccessibilityIterators.java b/core/java/android/view/AccessibilityIterators.java new file mode 100644 index 0000000000000..386c866d68479 --- /dev/null +++ b/core/java/android/view/AccessibilityIterators.java @@ -0,0 +1,352 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; + +import java.text.BreakIterator; +import java.util.Locale; + +/** + * This class contains the implementation of text segment iterators + * for accessibility support. + * + * Note: Such iterators are needed in the view package since we want + * to be able to iterator over content description of any view. + * + * @hide + */ +public final class AccessibilityIterators { + + /** + * @hide + */ + public static interface TextSegmentIterator { + public int[] following(int current); + public int[] preceding(int current); + } + + /** + * @hide + */ + public static abstract class AbstractTextSegmentIterator implements TextSegmentIterator { + protected static final int DONE = -1; + + protected String mText; + + private final int[] mSegment = new int[2]; + + public void initialize(String text) { + mText = text; + } + + protected int[] getRange(int start, int end) { + if (start < 0 || end < 0 || start == end) { + return null; + } + mSegment[0] = start; + mSegment[1] = end; + return mSegment; + } + } + + static class CharacterTextSegmentIterator extends AbstractTextSegmentIterator + implements ComponentCallbacks { + private static CharacterTextSegmentIterator sInstance; + + private final Context mAppContext; + + protected BreakIterator mImpl; + + public static CharacterTextSegmentIterator getInstance(Context context) { + if (sInstance == null) { + sInstance = new CharacterTextSegmentIterator(context); + } + return sInstance; + } + + private CharacterTextSegmentIterator(Context context) { + mAppContext = context.getApplicationContext(); + Locale locale = mAppContext.getResources().getConfiguration().locale; + onLocaleChanged(locale); + ViewRootImpl.addConfigCallback(this); + } + + @Override + public void initialize(String text) { + super.initialize(text); + mImpl.setText(text); + } + + @Override + public int[] following(int offset) { + final int textLegth = mText.length(); + if (textLegth <= 0) { + return null; + } + if (offset >= textLegth) { + return null; + } + int start = -1; + if (offset < 0) { + offset = 0; + if (mImpl.isBoundary(offset)) { + start = offset; + } + } + if (start < 0) { + start = mImpl.following(offset); + } + if (start < 0) { + return null; + } + final int end = mImpl.following(start); + return getRange(start, end); + } + + @Override + public int[] preceding(int offset) { + final int textLegth = mText.length(); + if (textLegth <= 0) { + return null; + } + if (offset <= 0) { + return null; + } + int end = -1; + if (offset > mText.length()) { + offset = mText.length(); + if (mImpl.isBoundary(offset)) { + end = offset; + } + } + if (end < 0) { + end = mImpl.preceding(offset); + } + if (end < 0) { + return null; + } + final int start = mImpl.preceding(end); + return getRange(start, end); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Configuration oldConfig = mAppContext.getResources().getConfiguration(); + final int changed = oldConfig.diff(newConfig); + if ((changed & ActivityInfo.CONFIG_LOCALE) != 0) { + Locale locale = newConfig.locale; + onLocaleChanged(locale); + } + } + + @Override + public void onLowMemory() { + /* ignore */ + } + + protected void onLocaleChanged(Locale locale) { + mImpl = BreakIterator.getCharacterInstance(locale); + } + } + + static class WordTextSegmentIterator extends CharacterTextSegmentIterator { + private static WordTextSegmentIterator sInstance; + + public static WordTextSegmentIterator getInstance(Context context) { + if (sInstance == null) { + sInstance = new WordTextSegmentIterator(context); + } + return sInstance; + } + + private WordTextSegmentIterator(Context context) { + super(context); + } + + @Override + protected void onLocaleChanged(Locale locale) { + mImpl = BreakIterator.getWordInstance(locale); + } + + @Override + public int[] following(int offset) { + final int textLegth = mText.length(); + if (textLegth <= 0) { + return null; + } + if (offset >= mText.length()) { + return null; + } + int start = -1; + if (offset < 0) { + offset = 0; + if (mImpl.isBoundary(offset) && isLetterOrDigit(offset)) { + start = offset; + } + } + if (start < 0) { + while ((offset = mImpl.following(offset)) != DONE) { + if (isLetterOrDigit(offset)) { + start = offset; + break; + } + } + } + if (start < 0) { + return null; + } + final int end = mImpl.following(start); + return getRange(start, end); + } + + @Override + public int[] preceding(int offset) { + final int textLegth = mText.length(); + if (textLegth <= 0) { + return null; + } + if (offset <= 0) { + return null; + } + int end = -1; + if (offset > mText.length()) { + offset = mText.length(); + if (mImpl.isBoundary(offset) && offset > 0 && isLetterOrDigit(offset - 1)) { + end = offset; + } + } + if (end < 0) { + while ((offset = mImpl.preceding(offset)) != DONE) { + if (offset > 0 && isLetterOrDigit(offset - 1)) { + end = offset; + break; + } + } + } + if (end < 0) { + return null; + } + final int start = mImpl.preceding(end); + return getRange(start, end); + } + + private boolean isLetterOrDigit(int index) { + if (index >= 0 && index < mText.length()) { + final int codePoint = mText.codePointAt(index); + return Character.isLetterOrDigit(codePoint); + } + return false; + } + } + + static class ParagraphTextSegmentIterator extends AbstractTextSegmentIterator { + private static ParagraphTextSegmentIterator sInstance; + + public static ParagraphTextSegmentIterator getInstance() { + if (sInstance == null) { + sInstance = new ParagraphTextSegmentIterator(); + } + return sInstance; + } + + @Override + public int[] following(int offset) { + final int textLength = mText.length(); + if (textLength <= 0) { + return null; + } + if (offset >= textLength) { + return null; + } + int start = -1; + if (offset < 0) { + start = 0; + } else { + for (int i = offset + 1; i < textLength; i++) { + if (mText.charAt(i) == '\n') { + start = i; + break; + } + } + } + while (start < textLength && mText.charAt(start) == '\n') { + start++; + } + if (start < 0) { + return null; + } + int end = start; + for (int i = end + 1; i < textLength; i++) { + end = i; + if (mText.charAt(i) == '\n') { + break; + } + } + while (end < textLength && mText.charAt(end) == '\n') { + end++; + } + return getRange(start, end); + } + + @Override + public int[] preceding(int offset) { + final int textLength = mText.length(); + if (textLength <= 0) { + return null; + } + if (offset <= 0) { + return null; + } + int end = -1; + if (offset > mText.length()) { + end = mText.length(); + } else { + if (offset > 0 && mText.charAt(offset - 1) == '\n') { + offset--; + } + for (int i = offset - 1; i >= 0; i--) { + if (i > 0 && mText.charAt(i - 1) == '\n') { + end = i; + break; + } + } + } + if (end <= 0) { + return null; + } + int start = end; + while (start > 0 && mText.charAt(start - 1) == '\n') { + start--; + } + if (start == 0 && mText.charAt(start) == '\n') { + return null; + } + for (int i = start - 1; i >= 0; i--) { + start = i; + if (start > 0 && mText.charAt(i - 1) == '\n') { + break; + } + } + start = Math.max(0, start); + return getRange(start, end); + } + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c054d387d402e..5220f2d49ef21 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -47,7 +47,6 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; -import android.text.TextUtils; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.LocaleUtil; @@ -60,6 +59,10 @@ import android.util.Property; import android.util.SparseArray; import android.util.TypedValue; import android.view.ContextMenu.ContextMenuInfo; +import android.view.AccessibilityIterators.TextSegmentIterator; +import android.view.AccessibilityIterators.CharacterTextSegmentIterator; +import android.view.AccessibilityIterators.WordTextSegmentIterator; +import android.view.AccessibilityIterators.ParagraphTextSegmentIterator; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEventSource; import android.view.accessibility.AccessibilityManager; @@ -1528,7 +1531,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED - | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; + | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED + | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY; /** * Temporary Rect currently for use in setBackground(). This will probably @@ -1593,6 +1597,11 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal */ int mAccessibilityViewId = NO_ID; + /** + * @hide + */ + private int mAccessibilityCursorPosition = -1; + /** * The view's tag. * {@hide} @@ -4699,11 +4708,12 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK); } - if (getContentDescription() != null) { + if (mContentDescription != null && mContentDescription.length() > 0) { info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER - | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD); + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH); } } @@ -5929,7 +5939,8 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal outViews.add(this); } } else if ((flags & FIND_VIEWS_WITH_CONTENT_DESCRIPTION) != 0 - && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mContentDescription)) { + && (searched != null && searched.length() > 0) + && (mContentDescription != null && mContentDescription.length() > 0)) { String searchedLowerCase = searched.toString().toLowerCase(); String contentDescriptionLowerCase = mContentDescription.toString().toLowerCase(); if (contentDescriptionLowerCase.contains(searchedLowerCase)) { @@ -6030,6 +6041,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal invalidate(); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); notifyAccessibilityStateChanged(); + + // Clear the text navigation state. + setAccessibilityCursorPosition(-1); + // Try to move accessibility focus to the input focus. View rootView = getRootView(); if (rootView != null) { @@ -6427,9 +6442,10 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * possible accessibility actions look at {@link AccessibilityNodeInfo}. * * @param action The action to perform. + * @param arguments Optional action arguments. * @return Whether the action was performed. */ - public boolean performAccessibilityAction(int action, Bundle args) { + public boolean performAccessibilityAction(int action, Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (isClickable()) { @@ -6478,10 +6494,150 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal return true; } } break; + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: { + if (arguments != null) { + final int granularity = arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + return nextAtGranularity(granularity); + } + } break; + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: { + if (arguments != null) { + final int granularity = arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + return previousAtGranularity(granularity); + } + } break; } return false; } + private boolean nextAtGranularity(int granularity) { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + return false; + } + TextSegmentIterator iterator = getIteratorForGranularity(granularity); + if (iterator == null) { + return false; + } + final int current = getAccessibilityCursorPosition(); + final int[] range = iterator.following(current); + if (range == null) { + setAccessibilityCursorPosition(-1); + return false; + } + final int start = range[0]; + final int end = range[1]; + setAccessibilityCursorPosition(start); + sendViewTextTraversedAtGranularityEvent( + AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, + granularity, start, end); + return true; + } + + private boolean previousAtGranularity(int granularity) { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + return false; + } + TextSegmentIterator iterator = getIteratorForGranularity(granularity); + if (iterator == null) { + return false; + } + final int selectionStart = getAccessibilityCursorPosition(); + final int current = selectionStart >= 0 ? selectionStart : text.length() + 1; + final int[] range = iterator.preceding(current); + if (range == null) { + setAccessibilityCursorPosition(-1); + return false; + } + final int start = range[0]; + final int end = range[1]; + setAccessibilityCursorPosition(end); + sendViewTextTraversedAtGranularityEvent( + AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, + granularity, start, end); + return true; + } + + /** + * Gets the text reported for accessibility purposes. + * + * @return The accessibility text. + * + * @hide + */ + public CharSequence getIterableTextForAccessibility() { + return mContentDescription; + } + + /** + * @hide + */ + public int getAccessibilityCursorPosition() { + return mAccessibilityCursorPosition; + } + + /** + * @hide + */ + public void setAccessibilityCursorPosition(int position) { + mAccessibilityCursorPosition = position; + } + + private void sendViewTextTraversedAtGranularityEvent(int action, int granularity, + int fromIndex, int toIndex) { + if (mParent == null) { + return; + } + AccessibilityEvent event = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); + onInitializeAccessibilityEvent(event); + onPopulateAccessibilityEvent(event); + event.setFromIndex(fromIndex); + event.setToIndex(toIndex); + event.setAction(action); + event.setMovementGranularity(granularity); + mParent.requestSendAccessibilityEvent(this, event); + } + + /** + * @hide + */ + public TextSegmentIterator getIteratorForGranularity(int granularity) { + switch (granularity) { + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + CharacterTextSegmentIterator iterator = + CharacterTextSegmentIterator.getInstance(mContext); + iterator.initialize(text.toString()); + return iterator; + } + } break; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + WordTextSegmentIterator iterator = + WordTextSegmentIterator.getInstance(mContext); + iterator.initialize(text.toString()); + return iterator; + } + } break; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH: { + CharSequence text = getIterableTextForAccessibility(); + if (text != null && text.length() > 0) { + ParagraphTextSegmentIterator iterator = + ParagraphTextSegmentIterator.getInstance(); + iterator.initialize(text.toString()); + return iterator; + } + } break; + } + return null; + } + /** * @hide */ diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index f70ffa95493aa..1a2a194f8211d 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -236,12 +236,19 @@ import java.util.List; *
@@ -635,6 +642,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
private CharSequence mPackageName;
private long mEventTime;
int mMovementGranularity;
+ int mAction;
private final ArrayList