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

Commit 33aef98f authored by Svetoslav Ganov's avatar Svetoslav Ganov
Browse files

Allowing association between a view and its label for accessibility.

1. For accessibility purposes it is important to be able to associate
   a view with content with a view that labels it. For example, if
   an accessibility service knows that a TextView is associated with
   an EditText, it can provide much richer feedback.

   This change adds APIs for setting a view to be the label for another
   one and setting the label for a view, i.e. the reverse association.

bug:5016937

Change-Id: I7b837265c5ed9302e3ce352396dc6e88413038b5
parent 0f755134
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -619,6 +619,7 @@ package android {
    field public static final int keycode = 16842949; // 0x10100c5
    field public static final int killAfterRestore = 16843420; // 0x101029c
    field public static final int label = 16842753; // 0x1010001
    field public static final int labelFor = 16843717; // 0x10103c5
    field public static final int labelTextSize = 16843317; // 0x1010235
    field public static final int largeHeap = 16843610; // 0x101035a
    field public static final int largeScreens = 16843398; // 0x1010286
@@ -24853,6 +24854,7 @@ package android.view {
    method public int getImportantForAccessibility();
    method public boolean getKeepScreenOn();
    method public android.view.KeyEvent.DispatcherState getKeyDispatcherState();
    method public int getLabelFor();
    method public int getLayerType();
    method public int getLayoutDirection();
    method public android.view.ViewGroup.LayoutParams getLayoutParams();
@@ -25115,6 +25117,7 @@ package android.view {
    method public void setId(int);
    method public void setImportantForAccessibility(int);
    method public void setKeepScreenOn(boolean);
    method public void setLabelFor(int);
    method public void setLayerPaint(android.graphics.Paint);
    method public void setLayerType(int, android.graphics.Paint);
    method public void setLayoutDirection(int);
@@ -26106,6 +26109,8 @@ package android.view.accessibility {
    method public int getChildCount();
    method public java.lang.CharSequence getClassName();
    method public java.lang.CharSequence getContentDescription();
    method public android.view.accessibility.AccessibilityNodeInfo getLabelFor();
    method public android.view.accessibility.AccessibilityNodeInfo getLabeledBy();
    method public int getMovementGranularities();
    method public java.lang.CharSequence getPackageName();
    method public android.view.accessibility.AccessibilityNodeInfo getParent();
@@ -26141,6 +26146,10 @@ package android.view.accessibility {
    method public void setEnabled(boolean);
    method public void setFocusable(boolean);
    method public void setFocused(boolean);
    method public void setLabelFor(android.view.View);
    method public void setLabelFor(android.view.View, int);
    method public void setLabeledBy(android.view.View);
    method public void setLabeledBy(android.view.View, int);
    method public void setLongClickable(boolean);
    method public void setMovementGranularities(int);
    method public void setPackageName(java.lang.CharSequence);
@@ -28997,6 +29006,7 @@ package android.widget {
    method public void setImageViewUri(int, android.net.Uri);
    method public void setInt(int, java.lang.String, int);
    method public void setIntent(int, java.lang.String, android.content.Intent);
    method public void setLabelFor(int, int);
    method public void setLong(int, java.lang.String, long);
    method public void setOnClickFillInIntent(int, android.content.Intent);
    method public void setOnClickPendingIntent(int, android.app.PendingIntent);
+104 −10
Original line number Diff line number Diff line
@@ -2758,6 +2758,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
    private CharSequence mContentDescription;
    /**
     * Specifies the id of a view for which this view serves as a label for
     * accessibility purposes.
     */
    private int mLabelForId = View.NO_ID;
    /**
     * Predicate for matching labeled view id with its label for
     * accessibility purposes.
     */
    private MatchLabelForPredicate mMatchLabelForPredicate;
    /**
     * Predicate for matching a view by its id.
     */
    private MatchIdPredicate mMatchIdPredicate;
    /**
     * Cache the paddingRight set by the user to append to the scrollbar's size.
     *
@@ -3370,6 +3387,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                case com.android.internal.R.styleable.View_contentDescription:
                    setContentDescription(a.getString(attr));
                    break;
                case com.android.internal.R.styleable.View_labelFor:
                    setLabelFor(a.getResourceId(attr, NO_ID));
                    break;
                case com.android.internal.R.styleable.View_soundEffectsEnabled:
                    if (!a.getBoolean(attr, true)) {
                        viewFlagValues &= ~SOUND_EFFECTS_ENABLED;
@@ -4837,6 +4857,28 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            info.setParent((View) parent);
        }
        if (mID != View.NO_ID) {
            View rootView = getRootView();
            if (rootView == null) {
                rootView = this;
            }
            View label = rootView.findLabelForView(this, mID);
            if (label != null) {
                info.setLabeledBy(label);
            }
        }
        if (mLabelForId != View.NO_ID) {
            View rootView = getRootView();
            if (rootView == null) {
                rootView = this;
            }
            View labeled = rootView.findViewInsideOutShouldExist(this, mLabelForId);
            if (labeled != null) {
                info.setLabelFor(labeled);
            }
        }
        info.setVisibleToUser(isVisibleToUser());
        info.setPackageName(mContext.getPackageName());
@@ -4888,6 +4930,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        }
    }
    private View findLabelForView(View view, int labeledId) {
        if (mMatchLabelForPredicate == null) {
            mMatchLabelForPredicate = new MatchLabelForPredicate();
        }
        mMatchLabelForPredicate.mLabeledId = labeledId;
        return findViewByPredicateInsideOut(view, mMatchLabelForPredicate);
    }
    /**
     * Computes whether this view is visible to the user. Such a view is
     * attached, visible, all its predecessors are visible, it is not clipped
@@ -5058,6 +5108,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        }
    }
    /**
     * Gets the id of a view for which this view serves as a label for
     * accessibility purposes.
     *
     * @return The labeled view id.
     */
    @ViewDebug.ExportedProperty(category = "accessibility")
    public int getLabelFor() {
        return mLabelForId;
    }
    /**
     * Sets the id of a view for which this view serves as a label for
     * accessibility purposes.
     *
     * @param id The labeled view id.
     */
    @RemotableViewMethod
    public void setLabelFor(int id) {
        mLabelForId = id;
        if (mLabelForId != View.NO_ID
                && mID == View.NO_ID) {
            mID = generateViewId();
        }
    }
    /**
     * Invoked whenever this view loses focus, either by losing window focus or by losing
     * focus within its window. This method can be used to clear any state tied to the
@@ -6110,17 +6186,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
        return null;
    }
    private View findViewInsideOutShouldExist(View root, final int childViewId) {
        View result = root.findViewByPredicateInsideOut(this, new Predicate<View>() {
            @Override
            public boolean apply(View t) {
                return t.mID == childViewId;
    private View findViewInsideOutShouldExist(View root, int id) {
        if (mMatchIdPredicate == null) {
            mMatchIdPredicate = new MatchIdPredicate();
        }
        });
        mMatchIdPredicate.mId = id;
        View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
        if (result == null) {
            Log.w(VIEW_LOG_TAG, "couldn't find next focus view specified "
                    + "by user for id " + childViewId);
            Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
        }
        return result;
    }
@@ -14922,6 +14995,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     */
    public void setId(int id) {
        mID = id;
        if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
            mID = generateViewId();
        }
    }
    /**
@@ -18008,4 +18084,22 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
            return null;
        }
    }
    private class MatchIdPredicate implements Predicate<View> {
        public int mId;
        @Override
        public boolean apply(View view) {
            return (view.mID == mId);
        }
    }
    private class MatchLabelForPredicate implements Predicate<View> {
        private int mLabeledId;
        @Override
        public boolean apply(View view) {
            return (view.mLabelForId == mLabeledId);
        }
    }
}
+124 −0
Original line number Diff line number Diff line
@@ -365,6 +365,8 @@ public class AccessibilityNodeInfo implements Parcelable {
    private int mWindowId = UNDEFINED;
    private long mSourceNodeId = ROOT_NODE_ID;
    private long mParentNodeId = ROOT_NODE_ID;
    private long mLabelForId = ROOT_NODE_ID;
    private long mLabeledById = ROOT_NODE_ID;

    private int mBooleanProperties;
    private final Rect mBoundsInParent = new Rect();
@@ -1232,6 +1234,120 @@ public class AccessibilityNodeInfo implements Parcelable {
        mContentDescription = contentDescription;
    }

    /**
     * Sets the view for which the view represented by this info serves as a
     * label for accessibility purposes.
     *
     * @param labeled The view for which this info serves as a label.
     */
    public void setLabelFor(View labeled) {
        setLabelFor(labeled, UNDEFINED);
    }

    /**
     * Sets the view for which the view represented by this info serves as a
     * label for accessibility purposes. If <code>virtualDescendantId</code>
     * is {@link View#NO_ID} the root is set as the labeled.
     * <p>
     * A virtual descendant is an imaginary View that is reported as a part of the view
     * hierarchy for accessibility purposes. This enables custom views that draw complex
     * content to report themselves as a tree of virtual views, thus conveying their
     * logical structure.
     * </p>
     * <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 root The root whose virtual descendant serves as a label.
     * @param virtualDescendantId The id of the virtual descendant.
     */
    public void setLabelFor(View root, int virtualDescendantId) {
        enforceNotSealed();
        final int rootAccessibilityViewId = (root != null)
                ? root.getAccessibilityViewId() : UNDEFINED;
        mLabelForId = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
    }

    /**
     * Gets the node info for which the view represented by this info serves as
     * a label for accessibility purposes.
     * <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>
     *
     * @return The labeled info.
     */
    public AccessibilityNodeInfo getLabelFor() {
        enforceSealed();
        if (!canPerformRequestOverConnection(mLabelForId)) {
            return null;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
        return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
                mWindowId, mLabelForId, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS);
    }

    /**
     * Sets the view which serves as the label of the view represented by
     * this info for accessibility purposes.
     *
     * @param label The view that labels this node's source.
     */
    public void setLabeledBy(View label) {
        setLabeledBy(label, UNDEFINED);
    }

    /**
     * Sets the view which serves as the label of the view represented by
     * this info for accessibility purposes. If <code>virtualDescendantId</code>
     * is {@link View#NO_ID} the root is set as the label.
     * <p>
     * A virtual descendant is an imaginary View that is reported as a part of the view
     * hierarchy for accessibility purposes. This enables custom views that draw complex
     * content to report themselves as a tree of virtual views, thus conveying their
     * logical structure.
     * </p>
     * <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 root The root whose virtual descendant labels this node's source.
     * @param virtualDescendantId The id of the virtual descendant.
     */
    public void setLabeledBy(View root, int virtualDescendantId) {
        enforceNotSealed();
        final int rootAccessibilityViewId = (root != null)
                ? root.getAccessibilityViewId() : UNDEFINED;
        mLabeledById = makeNodeId(rootAccessibilityViewId, virtualDescendantId);
    }

    /**
     * Gets the node info which serves as the label of the view represented by
     * this info for accessibility purposes.
     * <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>
     *
     * @return The label.
     */
    public AccessibilityNodeInfo getLabeledBy() {
        enforceSealed();
        if (!canPerformRequestOverConnection(mLabeledById)) {
            return null;
        }
        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
        return client.findAccessibilityNodeInfoByAccessibilityId(mConnectionId,
                mWindowId, mLabeledById, FLAG_PREFETCH_DESCENDANTS | FLAG_PREFETCH_SIBLINGS);
    }

    /**
     * Gets the value of a boolean property.
     *
@@ -1462,6 +1578,8 @@ public class AccessibilityNodeInfo implements Parcelable {
        parcel.writeLong(mSourceNodeId);
        parcel.writeInt(mWindowId);
        parcel.writeLong(mParentNodeId);
        parcel.writeLong(mLabelForId);
        parcel.writeLong(mLabeledById);
        parcel.writeInt(mConnectionId);

        SparseLongArray childIds = mChildNodeIds;
@@ -1507,6 +1625,8 @@ public class AccessibilityNodeInfo implements Parcelable {
        mSealed = other.mSealed;
        mSourceNodeId = other.mSourceNodeId;
        mParentNodeId = other.mParentNodeId;
        mLabelForId = other.mLabelForId;
        mLabeledById = other.mLabeledById;
        mWindowId = other.mWindowId;
        mConnectionId = other.mConnectionId;
        mBoundsInParent.set(other.mBoundsInParent);
@@ -1534,6 +1654,8 @@ public class AccessibilityNodeInfo implements Parcelable {
        mSourceNodeId = parcel.readLong();
        mWindowId = parcel.readInt();
        mParentNodeId = parcel.readLong();
        mLabelForId = parcel.readLong();
        mLabeledById = parcel.readLong();
        mConnectionId = parcel.readInt();

        SparseLongArray childIds = mChildNodeIds;
@@ -1572,6 +1694,8 @@ public class AccessibilityNodeInfo implements Parcelable {
        mSealed = false;
        mSourceNodeId = ROOT_NODE_ID;
        mParentNodeId = ROOT_NODE_ID;
        mLabelForId = ROOT_NODE_ID;
        mLabeledById = ROOT_NODE_ID;
        mWindowId = UNDEFINED;
        mConnectionId = UNDEFINED;
        mMovementGranularities = 0;
+13 −3
Original line number Diff line number Diff line
@@ -2077,15 +2077,25 @@ public class RemoteViews implements Parcelable, Filter {
    }

    /**
     * Equivalent to calling View.setContentDescription
     * Equivalent to calling View.setContentDescription(CharSequence).
     *
     * @param viewId The id of the view whose content description should change
     * @param contentDescription The new content description for the view
     * @param viewId The id of the view whose content description should change.
     * @param contentDescription The new content description for the view.
     */
    public void setContentDescription(int viewId, CharSequence contentDescription) {
        setCharSequence(viewId, "setContentDescription", contentDescription);
    }

    /**
     * Equivalent to calling View.setLabelFor(int).
     *
     * @param viewId The id of the view whose property to set.
     * @param labeledId The id of a view for which this view serves as a label.
     */
    public void setLabelFor(int viewId, int labeledId) {
        setInt(viewId, "setLabelFor", labeledId);
    }

    private RemoteViews getRemoteViewsToApply(Context context) {
        if (hasLandscapeAndPortraitLayouts()) {
            int orientation = context.getResources().getConfiguration().orientation;
+6 −0
Original line number Diff line number Diff line
@@ -2163,6 +2163,12 @@
            <enum name="no" value="2" />
        </attr>

        <!-- Specifies the id of a view for which this view serves as a label for
             accessibility purposes. For example, a TextView before an EditText in
             the UI usually specifies what infomation is contained in the EditText.
             Hence, the TextView is a label for the EditText. -->
        <attr name="labelFor" format="integer" />

    </declare-styleable>

    <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any
Loading