Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6d17a936 authored by Svetoslav Ganov's avatar Svetoslav Ganov
Browse files

Text traversal at various granularities.

1. Implementing text content navigation at various granularities.
   For views that have content description but no text the
   content description is the traversed at character and word
   granularities. For views that inherit from TextView the
   supported granularities are character, word, line, and page.

bug:5932640

Conflicts:

	core/java/android/view/View.java

Conflicts:

	core/java/android/view/View.java

Change-Id: I66d1e16ce9ac5d6b49f036b17c087b2a7075e4c0
parent 2551e5a1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -25090,6 +25090,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();
@@ -25100,6 +25101,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);
+352 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+162 −6
Original line number Diff line number Diff line
@@ -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;
@@ -1524,7 +1527,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
@@ -1589,6 +1593,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}
@@ -4714,11 +4723,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);
        }
    }
@@ -5949,7 +5959,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)) {
@@ -6050,6 +6061,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) {
@@ -6447,9 +6462,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()) {
@@ -6498,9 +6514,149 @@ 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
+35 −1
Original line number Diff line number Diff line
@@ -236,12 +236,19 @@ import java.util.List;
 *   <li>{@link #getClassName()} - The class name of the source.</li>
 *   <li>{@link #getPackageName()} - The package name of the source.</li>
 *   <li>{@link #getEventTime()}  - The event time.</li>
 *   <li>{@link #getText()} - The text of the current text at the movement granularity.</li>
 *   <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
 *       was traversed.</li>
 *   <li>{@link #getText()} -  The text of the source's sub-tree.</li>
 *   <li>{@link #getFromIndex()} - The start of the next/previous text at the specified granularity
 *           - inclusive.</li>
 *   <li>{@link #getToIndex()} - The end of the next/previous text at the specified granularity
 *           - exclusive.</li>
 *   <li>{@link #isPassword()} - Whether the source is password.</li>
 *   <li>{@link #isEnabled()} - Whether the source is enabled.</li>
 *   <li>{@link #getContentDescription()} - The content description of the source.</li>
 *   <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
 *       was traversed.</li>
 *   <li>{@link #getAction()} - Gets traversal action which specifies the direction.</li>
 * </ul>
 * </p>
 * <p>
@@ -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<AccessibilityRecord> mRecords = new ArrayList<AccessibilityRecord>();

@@ -653,6 +661,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        super.init(event);
        mEventType = event.mEventType;
        mMovementGranularity = event.mMovementGranularity;
        mAction = event.mAction;
        mEventTime = event.mEventTime;
        mPackageName = event.mPackageName;
    }
@@ -790,6 +799,27 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        return mMovementGranularity;
    }

    /**
     * Sets the performed action that triggered this event.
     *
     * @param action The action.
     *
     * @throws IllegalStateException If called from an AccessibilityService.
     */
    public void setAction(int action) {
        enforceNotSealed();
        mAction = action;
    }

    /**
     * Gets the performed action that triggered this event.
     *
     * @return The action.
     */
    public int getAction() {
        return mAction;
    }

    /**
     * Returns a cached instance if such is available or a new one is
     * instantiated with its type property set.
@@ -879,6 +909,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        super.clear();
        mEventType = 0;
        mMovementGranularity = 0;
        mAction = 0;
        mPackageName = null;
        mEventTime = 0;
        while (!mRecords.isEmpty()) {
@@ -896,6 +927,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        mSealed = (parcel.readInt() == 1);
        mEventType = parcel.readInt();
        mMovementGranularity = parcel.readInt();
        mAction = parcel.readInt();
        mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
        mEventTime = parcel.readLong();
        mConnectionId = parcel.readInt();
@@ -947,6 +979,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        parcel.writeInt(isSealed() ? 1 : 0);
        parcel.writeInt(mEventType);
        parcel.writeInt(mMovementGranularity);
        parcel.writeInt(mAction);
        TextUtils.writeToParcel(mPackageName, parcel, 0);
        parcel.writeLong(mEventTime);
        parcel.writeInt(mConnectionId);
@@ -1004,6 +1037,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
        builder.append("; EventTime: ").append(mEventTime);
        builder.append("; PackageName: ").append(mPackageName);
        builder.append("; MovementGranularity: ").append(mMovementGranularity);
        builder.append("; Action: ").append(mAction);
        builder.append(super.toString());
        if (DEBUG) {
            builder.append("\n");
+2 −2
Original line number Diff line number Diff line
@@ -102,12 +102,12 @@ public class AccessibilityNodeInfo implements Parcelable {
    public static final int ACTION_CLEAR_SELECTION = 0x00000008;

    /**
     * Action that long clicks on the node info.
     * Action that clicks on the node info.
     */
    public static final int ACTION_CLICK = 0x00000010;

    /**
     * Action that clicks on the node.
     * Action that long clicks on the node.
     */
    public static final int ACTION_LONG_CLICK = 0x00000020;

Loading