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

Commit ae63875a authored by Rhed Jao's avatar Rhed Jao
Browse files

Accessibility: Improve TouchDelegate Accessibility

Adding api to get touch delegate behavior for the
represented view of AccessibilityNodeInfo.

Bug: 80061718
Test: atest AccessibilityNodeInfoTest
Test: atest AccessibilityEndToEndTest
Change-Id: I2ae65d7d44fceaf16609e512c3384f766266ecbd
parent e1825ba9
Loading
Loading
Loading
Loading
+13 −0
Original line number Diff line number Diff line
@@ -47982,6 +47982,7 @@ package android.view {
  public class TouchDelegate {
    ctor public TouchDelegate(android.graphics.Rect, android.view.View);
    method public android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo();
    method public boolean onTouchEvent(android.view.MotionEvent);
    field public static final int ABOVE = 1; // 0x1
    field public static final int BELOW = 2; // 0x2
@@ -49964,6 +49965,7 @@ package android.view.accessibility {
    method public int getTextSelectionEnd();
    method public int getTextSelectionStart();
    method public java.lang.CharSequence getTooltipText();
    method public android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo();
    method public android.view.accessibility.AccessibilityNodeInfo getTraversalAfter();
    method public android.view.accessibility.AccessibilityNodeInfo getTraversalBefore();
    method public java.lang.String getViewIdResourceName();
@@ -50054,6 +50056,7 @@ package android.view.accessibility {
    method public void setTextEntryKey(boolean);
    method public void setTextSelection(int, int);
    method public void setTooltipText(java.lang.CharSequence);
    method public void setTouchDelegateInfo(android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo);
    method public void setTraversalAfter(android.view.View);
    method public void setTraversalAfter(android.view.View, int);
    method public void setTraversalBefore(android.view.View);
@@ -50180,6 +50183,16 @@ package android.view.accessibility {
    field public static final int RANGE_TYPE_PERCENT = 2; // 0x2
  }
  public static final class AccessibilityNodeInfo.TouchDelegateInfo implements android.os.Parcelable {
    ctor public AccessibilityNodeInfo.TouchDelegateInfo(java.util.Map<android.graphics.Region, android.view.View>);
    method public int describeContents();
    method public android.graphics.Region getRegionAt(int);
    method public int getRegionCount();
    method public android.view.accessibility.AccessibilityNodeInfo getTargetForRegion(android.graphics.Region);
    method public void writeToParcel(android.os.Parcel, int);
    field public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo> CREATOR;
  }
  public abstract class AccessibilityNodeProvider {
    ctor public AccessibilityNodeProvider();
    method public void addExtraDataToAccessibilityNodeInfo(int, android.view.accessibility.AccessibilityNodeInfo, java.lang.String, android.os.Bundle);
+4 −0
Original line number Diff line number Diff line
@@ -1607,6 +1607,10 @@ package android.view.accessibility {
    method public void writeToParcelNoRecycle(android.os.Parcel, int);
  }

  public static final class AccessibilityNodeInfo.TouchDelegateInfo implements android.os.Parcelable {
    method public long getAccessibilityIdForRegion(android.graphics.Region);
  }

  public final class AccessibilityWindowInfo implements android.os.Parcelable {
    method public static void setNumInstancesInUseCounter(java.util.concurrent.atomic.AtomicInteger);
  }
+25 −0
Original line number Diff line number Diff line
@@ -16,8 +16,12 @@

package android.view;

import android.annotation.NonNull;
import android.annotation.UnsupportedAppUsage;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.ArrayMap;
import android.view.accessibility.AccessibilityNodeInfo.TouchDelegateInfo;

/**
 * Helper class to handle situations where you want a view to have a larger touch area than its
@@ -77,6 +81,11 @@ public class TouchDelegate {

    private int mSlop;

    /**
     * Touch delegate information for accessibility
     */
    private TouchDelegateInfo mTouchDelegateInfo;

    /**
     * Constructor
     *
@@ -145,4 +154,20 @@ public class TouchDelegate {
        }
        return handled;
    }

    /**
     * Return a {@link TouchDelegateInfo} mapping from regions (in view coordinates) to
     * delegated views for accessibility usage.
     *
     * @return A TouchDelegateInfo.
     */
    @NonNull
    public TouchDelegateInfo getTouchDelegateInfo() {
        if (mTouchDelegateInfo == null) {
            final ArrayMap<Region, View> targetMap = new ArrayMap<>(1);
            targetMap.put(new Region(mBounds), mDelegateView);
            mTouchDelegateInfo = new TouchDelegateInfo(targetMap);
        }
        return mTouchDelegateInfo;
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -8881,6 +8881,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        populateAccessibilityNodeInfoDrawingOrderInParent(info);
        info.setPaneTitle(mAccessibilityPaneTitle);
        info.setHeading(isAccessibilityHeading());
        if (mTouchDelegate != null) {
            info.setTouchDelegateInfo(mTouchDelegate.getTouchDelegateInfo());
        }
    }
    /**
+254 −21
Original line number Diff line number Diff line
@@ -23,10 +23,12 @@ import static java.util.Collections.EMPTY_LIST;

import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.TestApi;
import android.annotation.UnsupportedAppUsage;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
@@ -39,17 +41,21 @@ import android.text.style.AccessibilityClickableSpan;
import android.text.style.AccessibilityURLSpan;
import android.text.style.ClickableSpan;
import android.text.style.URLSpan;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LongArray;
import android.util.Pools.SynchronizedPool;
import android.view.TouchDelegate;
import android.view.View;

import com.android.internal.R;
import com.android.internal.util.CollectionUtils;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;

@@ -748,6 +754,8 @@ public class AccessibilityNodeInfo implements Parcelable {
    private CollectionInfo mCollectionInfo;
    private CollectionItemInfo mCollectionItemInfo;

    private TouchDelegateInfo mTouchDelegateInfo;

    /**
     * Hide constructor from clients.
     */
@@ -810,7 +818,7 @@ public class AccessibilityNodeInfo implements Parcelable {
    public AccessibilityNodeInfo findFocus(int focus) {
        enforceSealed();
        enforceValidFocusType(focus);
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return null;
        }
        return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, mWindowId,
@@ -834,7 +842,7 @@ public class AccessibilityNodeInfo implements Parcelable {
    public AccessibilityNodeInfo focusSearch(int direction) {
        enforceSealed();
        enforceValidFocusDirection(direction);
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return null;
        }
        return AccessibilityInteractionClient.getInstance().focusSearch(mConnectionId, mWindowId,
@@ -866,7 +874,7 @@ public class AccessibilityNodeInfo implements Parcelable {
    @UnsupportedAppUsage
    public boolean refresh(Bundle arguments, boolean bypassCache) {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return false;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -967,7 +975,7 @@ public class AccessibilityNodeInfo implements Parcelable {
        if (mChildNodeIds == null) {
            return null;
        }
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return null;
        }
        final long childId = mChildNodeIds.get(index);
@@ -1271,7 +1279,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityNodeInfo getTraversalBefore() {
        enforceSealed();
        return getNodeForAccessibilityId(mTraversalBefore);
        return getNodeForAccessibilityId(mConnectionId, mWindowId, mTraversalBefore);
    }

    /**
@@ -1332,7 +1340,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityNodeInfo getTraversalAfter() {
        enforceSealed();
        return getNodeForAccessibilityId(mTraversalAfter);
        return getNodeForAccessibilityId(mConnectionId, mWindowId, mTraversalAfter);
    }

    /**
@@ -1489,7 +1497,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public boolean performAction(int action) {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return false;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -1512,7 +1520,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public boolean performAction(int action, Bundle arguments) {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return false;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -1536,7 +1544,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return Collections.emptyList();
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -1567,7 +1575,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId) {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return Collections.emptyList();
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -1584,7 +1592,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityWindowInfo getWindow() {
        enforceSealed();
        if (!canPerformRequestOverConnection(mSourceNodeId)) {
        if (!canPerformRequestOverConnection(mConnectionId, mWindowId, mSourceNodeId)) {
            return null;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
@@ -1603,7 +1611,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityNodeInfo getParent() {
        enforceSealed();
        return getNodeForAccessibilityId(mParentNodeId);
        return getNodeForAccessibilityId(mConnectionId, mWindowId, mParentNodeId);
    }

    /**
@@ -2783,7 +2791,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityNodeInfo getLabelFor() {
        enforceSealed();
        return getNodeForAccessibilityId(mLabelForId);
        return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabelForId);
    }

    /**
@@ -2835,7 +2843,7 @@ public class AccessibilityNodeInfo implements Parcelable {
     */
    public AccessibilityNodeInfo getLabeledBy() {
        enforceSealed();
        return getNodeForAccessibilityId(mLabeledById);
        return getNodeForAccessibilityId(mConnectionId, mWindowId, mLabeledById);
    }

    /**
@@ -2974,6 +2982,43 @@ public class AccessibilityNodeInfo implements Parcelable {
        return mExtras != null;
    }

    /**
     * Get the {@link TouchDelegateInfo} for touch delegate behavior with the represented view.
     * It is possible for the same node to be pointed to by several regions. Use
     * {@link TouchDelegateInfo#getRegionAt(int)} to get touch delegate target {@link Region}, and
     * {@link TouchDelegateInfo#getTargetForRegion(Region)} for {@link AccessibilityNodeInfo} from
     * the given region.
     *
     * @return {@link TouchDelegateInfo} or {@code null} if there are no touch delegates.
     */
    @Nullable
    public TouchDelegateInfo getTouchDelegateInfo() {
        if (mTouchDelegateInfo != null) {
            mTouchDelegateInfo.setConnectionId(mConnectionId);
            mTouchDelegateInfo.setWindowId(mWindowId);
        }
        return mTouchDelegateInfo;
    }

    /**
     * Set touch delegate info if the represented view has a {@link TouchDelegate}.
     * <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 delegatedInfo {@link TouchDelegateInfo} returned from
     *         {@link TouchDelegate#getTouchDelegateInfo()}.
     *
     * @throws IllegalStateException If called from an AccessibilityService.
     */
    public void setTouchDelegateInfo(@NonNull TouchDelegateInfo delegatedInfo) {
        enforceNotSealed();
        mTouchDelegateInfo = delegatedInfo;
    }

    /**
     * Gets the value of a boolean property.
     *
@@ -3340,6 +3385,10 @@ public class AccessibilityNodeInfo implements Parcelable {
        if (!Objects.equals(mCollectionItemInfo, DEFAULT.mCollectionItemInfo)) {
            nonDefaultFields |= bitAt(fieldIndex);
        }
        fieldIndex++;
        if (!Objects.equals(mTouchDelegateInfo, DEFAULT.mTouchDelegateInfo)) {
            nonDefaultFields |= bitAt(fieldIndex);
        }
        int totalFields = fieldIndex;
        parcel.writeLong(nonDefaultFields);

@@ -3462,6 +3511,10 @@ public class AccessibilityNodeInfo implements Parcelable {
            parcel.writeInt(mCollectionItemInfo.isSelected() ? 1 : 0);
        }

        if (isBitSet(nonDefaultFields, fieldIndex++)) {
            mTouchDelegateInfo.writeToParcel(parcel, flags);
        }

        if (DEBUG) {
            fieldIndex--;
            if (totalFields != fieldIndex) {
@@ -3543,6 +3596,10 @@ public class AccessibilityNodeInfo implements Parcelable {
        if (mCollectionItemInfo != null) mCollectionItemInfo.recycle();
        mCollectionItemInfo =  (other.mCollectionItemInfo != null)
                ? CollectionItemInfo.obtain(other.mCollectionItemInfo) : null;

        final TouchDelegateInfo otherInfo = other.mTouchDelegateInfo;
        mTouchDelegateInfo = (otherInfo != null)
                ? new TouchDelegateInfo(otherInfo.mTargetMap, true) : null;
    }

    /**
@@ -3665,6 +3722,10 @@ public class AccessibilityNodeInfo implements Parcelable {
                        parcel.readInt() == 1)
                : null;

        if (isBitSet(nonDefaultFields, fieldIndex++)) {
            mTouchDelegateInfo = TouchDelegateInfo.CREATOR.createFromParcel(parcel);
        }

        mSealed = sealed;
    }

@@ -3813,10 +3874,11 @@ public class AccessibilityNodeInfo implements Parcelable {
        }
    }

    private boolean canPerformRequestOverConnection(long accessibilityNodeId) {
        return ((mWindowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
    private static boolean canPerformRequestOverConnection(int connectionId,
            int windowId, long accessibilityNodeId) {
        return ((windowId != AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)
                && (getAccessibilityViewId(accessibilityNodeId) != UNDEFINED_ITEM_ID)
                && (mConnectionId != UNDEFINED_CONNECTION_ID));
                && (connectionId != UNDEFINED_CONNECTION_ID));
    }

    @Override
@@ -3919,13 +3981,14 @@ public class AccessibilityNodeInfo implements Parcelable {
        return builder.toString();
    }

    private AccessibilityNodeInfo getNodeForAccessibilityId(long accessibilityId) {
        if (!canPerformRequestOverConnection(accessibilityId)) {
    private static AccessibilityNodeInfo getNodeForAccessibilityId(int connectionId,
            int windowId, long accessibilityId) {
        if (!canPerformRequestOverConnection(connectionId, windowId, accessibilityId)) {
            return null;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
        return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
                mWindowId, accessibilityId, false, FLAG_PREFETCH_PREDECESSORS
        return client.findAccessibilityNodeInfoByAccessibilityId(connectionId,
                windowId, accessibilityId, false, FLAG_PREFETCH_PREDECESSORS
                        | FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS, null);
    }

@@ -4895,6 +4958,176 @@ public class AccessibilityNodeInfo implements Parcelable {
        }
    }

    /**
     * Class with information of touch delegated views and regions from {@link TouchDelegate} for
     * the {@link AccessibilityNodeInfo}.
     *
     * @see AccessibilityNodeInfo#setTouchDelegateInfo(TouchDelegateInfo)
     */
    public static final class TouchDelegateInfo implements Parcelable {
        private ArrayMap<Region, Long> mTargetMap;
        // Two ids are initialized lazily in AccessibilityNodeInfo#getTouchDelegateInfo
        private int mConnectionId;
        private int mWindowId;

        /**
         * Create a new instance of {@link TouchDelegateInfo}.
         *
         * @param targetMap A map from regions (in view coordinates) to delegated views.
         * @throws IllegalArgumentException if targetMap is empty or {@code null} in
         * Regions or Views.
         */
        public TouchDelegateInfo(@NonNull Map<Region, View> targetMap) {
            Preconditions.checkArgument(!targetMap.isEmpty()
                    && !targetMap.containsKey(null) && !targetMap.containsValue(null));
            mTargetMap = new ArrayMap<>(targetMap.size());
            for (final Region region : targetMap.keySet()) {
                final View view = targetMap.get(region);
                mTargetMap.put(region, (long) view.getAccessibilityViewId());
            }
        }

        /**
         * Create a new instance from target map.
         *
         * @param targetMap A map from regions (in view coordinates) to delegated views'
         *                  accessibility id.
         * @param doCopy True if shallow copy targetMap.
         * @throws IllegalArgumentException if targetMap is empty or {@code null} in
         * Regions or Views.
         */
        TouchDelegateInfo(@NonNull ArrayMap<Region, Long> targetMap, boolean doCopy) {
            Preconditions.checkArgument(!targetMap.isEmpty()
                    && !targetMap.containsKey(null) && !targetMap.containsValue(null));
            if (doCopy) {
                mTargetMap = new ArrayMap<>(targetMap.size());
                mTargetMap.putAll(targetMap);
            } else {
                mTargetMap = targetMap;
            }
        }

        /**
         * Set the connection ID.
         *
         * @param connectionId The connection id.
         */
        private void setConnectionId(int connectionId) {
            mConnectionId = connectionId;
        }

        /**
         * Set the window ID.
         *
         * @param windowId The window id.
         */
        private void setWindowId(int windowId) {
            mWindowId = windowId;
        }

        /**
         * Returns the number of touch delegate target region.
         *
         * @return Number of touch delegate target region.
         */
        public int getRegionCount() {
            return mTargetMap.size();
        }

        /**
         * Return the {@link Region} at the given index in the {@link TouchDelegateInfo}.
         *
         * @param index The desired index, must be between 0 and {@link #getRegionCount()}-1.
         * @return Returns the {@link Region} stored at the given index.
         */
        @NonNull
        public Region getRegionAt(int index) {
            return mTargetMap.keyAt(index);
        }

        /**
         * Return the target {@link AccessibilityNodeInfo} for the given {@link Region}.
         * <p>
         *   <strong>Note:</strong> This api can only be called from {@link AccessibilityService}.
         * </p>
         * <p>
         *   <strong>Note:</strong> It is a client responsibility to recycle the
         *     received info by calling {@link AccessibilityNodeInfo#recycle()}
         *     to avoid creating of multiple instances.
         * </p>
         *
         * @param region The region retrieved from {@link #getRegionAt(int)}.
         * @return The target node associates with the given region.
         */
        @Nullable
        public AccessibilityNodeInfo getTargetForRegion(@NonNull Region region) {
            return getNodeForAccessibilityId(mConnectionId, mWindowId, mTargetMap.get(region));
        }

        /**
         * Return the accessibility id of target node.
         *
         * @param region The region retrieved from {@link #getRegionAt(int)}.
         * @return The accessibility id of target node.
         *
         * @hide
         */
        @TestApi
        public long getAccessibilityIdForRegion(@NonNull Region region) {
            return mTargetMap.get(region);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int describeContents() {
            return 0;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mTargetMap.size());
            for (int i = 0; i < mTargetMap.size(); i++) {
                final Region region = mTargetMap.keyAt(i);
                final Long accessibilityId = mTargetMap.valueAt(i);
                region.writeToParcel(dest, flags);
                dest.writeLong(accessibilityId);
            }
        }

        /**
         * @see android.os.Parcelable.Creator
         */
        public static final Parcelable.Creator<TouchDelegateInfo> CREATOR =
                new Parcelable.Creator<TouchDelegateInfo>() {
            @Override
            public TouchDelegateInfo createFromParcel(Parcel parcel) {
                final int size = parcel.readInt();
                if (size == 0) {
                    return null;
                }
                final ArrayMap<Region, Long> targetMap = new ArrayMap<>(size);
                for (int i = 0; i < size; i++) {
                    final Region region = Region.CREATOR.createFromParcel(parcel);
                    final long accessibilityId = parcel.readLong();
                    targetMap.put(region, accessibilityId);
                }
                final TouchDelegateInfo touchDelegateInfo = new TouchDelegateInfo(
                        targetMap, false);
                return touchDelegateInfo;
            }

            @Override
            public TouchDelegateInfo[] newArray(int size) {
                return new TouchDelegateInfo[size];
            }
        };
    }

    /**
     * @see android.os.Parcelable.Creator
     */
Loading