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

Commit b2ee0d57 authored by Svetoslav Ganov's avatar Svetoslav Ganov Committed by Android (Google) Code Review
Browse files

Merge "Text traversal at various granularities." into jb-dev

parents f4c77df0 6d17a936
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -25088,6 +25088,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();
@@ -25098,6 +25099,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