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

Commit 23161e71 authored by Phil Weaver's avatar Phil Weaver
Browse files

Make a11y clickable span work after node recycle

Services recycle nodes when gathering possible actions
for users to take. It's very confusing for them if they
can't recycle the node without breaking their ability
to activate a clickable span inside it.

Making ClickableSpans used for accessibility independent
of their parent nodes.

Also adjusting the value used for invalid window ids to
come from AccessibilityWindowInfo. I must have missed this
in an earlier cleanup. I needed the value for the spans,
so I figured I might as well use the correct one.

Bug: 37004527
Test: Now recycling the node in the a11y cts test.
Change-Id: I6de4e98a182dd43c4fcd0430a3c082fcc8e458c7
parent 3d5f41bb
Loading
Loading
Loading
Loading
+23 −20
Original line number Original line Diff line number Diff line
@@ -16,6 +16,9 @@
package android.text.style;
package android.text.style;


import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;
import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID;
import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_NODE_ID;
import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;


import android.os.Bundle;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcel;
@@ -24,13 +27,11 @@ import android.text.ParcelableSpan;
import android.text.Spanned;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils;
import android.view.View;
import android.view.View;
import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo;


import com.android.internal.R;
import com.android.internal.R;


import java.lang.ref.WeakReference;


/**
/**
 * {@link ClickableSpan} cannot be parceled, but accessibility services need to be able to cause
 * {@link ClickableSpan} cannot be parceled, but accessibility services need to be able to cause
 * their callback handlers to be called. This class serves as a parcelable placeholder for the
 * their callback handlers to be called. This class serves as a parcelable placeholder for the
@@ -47,10 +48,9 @@ public class AccessibilityClickableSpan extends ClickableSpan
    // The id of the span this one replaces
    // The id of the span this one replaces
    private final int mOriginalClickableSpanId;
    private final int mOriginalClickableSpanId;


    // Only retain a weak reference to the node to avoid referencing cycles that could create memory
    private int mWindowId = UNDEFINED_WINDOW_ID;
    // leaks.
    private long mSourceNodeId = UNDEFINED_NODE_ID;
    private WeakReference<AccessibilityNodeInfo> mAccessibilityNodeInfoRef;
    private int mConnectionId = UNDEFINED_CONNECTION_ID;



    /**
    /**
     * @param originalClickableSpanId The id of the span this one replaces
     * @param originalClickableSpanId The id of the span this one replaces
@@ -110,13 +110,15 @@ public class AccessibilityClickableSpan extends ClickableSpan
    }
    }


    /**
    /**
     * Set the accessibilityNodeInfo that this placeholder belongs to. This node is not
     * Configure this object to perform clicks on the view that contains the original span.
     * included in the parceling logic, and must be set to allow the onClick handler to function.
     *
     *
     * @param accessibilityNodeInfo The info this span is part of
     * @param accessibilityNodeInfo The info corresponding to the view containing the original
     *                              span.
     */
     */
    public void setAccessibilityNodeInfo(AccessibilityNodeInfo accessibilityNodeInfo) {
    public void copyConnectionDataFrom(AccessibilityNodeInfo accessibilityNodeInfo) {
        mAccessibilityNodeInfoRef = new WeakReference<>(accessibilityNodeInfo);
        mConnectionId = accessibilityNodeInfo.getConnectionId();
        mWindowId = accessibilityNodeInfo.getWindowId();
        mSourceNodeId = accessibilityNodeInfo.getSourceNodeId();
    }
    }


    /**
    /**
@@ -128,17 +130,18 @@ public class AccessibilityClickableSpan extends ClickableSpan
     */
     */
    @Override
    @Override
    public void onClick(View unused) {
    public void onClick(View unused) {
        if (mAccessibilityNodeInfoRef == null) {
            return;
        }
        AccessibilityNodeInfo info = mAccessibilityNodeInfoRef.get();
        if (info == null) {
            return;
        }
        Bundle arguments = new Bundle();
        Bundle arguments = new Bundle();
        arguments.putParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN, this);
        arguments.putParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN, this);


        info.performAction(R.id.accessibilityActionClickOnClickableSpan, arguments);
        if ((mWindowId == UNDEFINED_WINDOW_ID) || (mSourceNodeId == UNDEFINED_NODE_ID)
                || (mConnectionId == UNDEFINED_CONNECTION_ID)) {
            throw new RuntimeException(
                    "ClickableSpan for accessibility service not properly initialized");
        }

        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
        client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId,
                R.id.accessibilityActionClickOnClickableSpan, arguments);
    }
    }


    public static final Parcelable.Creator<AccessibilityClickableSpan> CREATOR =
    public static final Parcelable.Creator<AccessibilityClickableSpan> CREATOR =
+2 −2
Original line number Original line Diff line number Diff line
@@ -73,7 +73,7 @@ public class AccessibilityURLSpan extends URLSpan implements Parcelable {
     * Delegated to AccessibilityClickableSpan
     * Delegated to AccessibilityClickableSpan
     * @param accessibilityNodeInfo
     * @param accessibilityNodeInfo
     */
     */
    public void setAccessibilityNodeInfo(AccessibilityNodeInfo accessibilityNodeInfo) {
    public void copyConnectionDataFrom(AccessibilityNodeInfo accessibilityNodeInfo) {
        mAccessibilityClickableSpan.setAccessibilityNodeInfo(accessibilityNodeInfo);
        mAccessibilityClickableSpan.copyConnectionDataFrom(accessibilityNodeInfo);
    }
    }
}
}
+18 −7
Original line number Original line Diff line number Diff line
@@ -695,7 +695,7 @@ public class AccessibilityNodeInfo implements Parcelable {
    private boolean mSealed;
    private boolean mSealed;


    // Data.
    // Data.
    private int mWindowId = UNDEFINED_ITEM_ID;
    private int mWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
    private long mSourceNodeId = UNDEFINED_NODE_ID;
    private long mSourceNodeId = UNDEFINED_NODE_ID;
    private long mParentNodeId = UNDEFINED_NODE_ID;
    private long mParentNodeId = UNDEFINED_NODE_ID;
    private long mLabelForId = UNDEFINED_NODE_ID;
    private long mLabelForId = UNDEFINED_NODE_ID;
@@ -2417,12 +2417,12 @@ public class AccessibilityNodeInfo implements Parcelable {
            AccessibilityClickableSpan[] clickableSpans =
            AccessibilityClickableSpan[] clickableSpans =
                    spanned.getSpans(0, mText.length(), AccessibilityClickableSpan.class);
                    spanned.getSpans(0, mText.length(), AccessibilityClickableSpan.class);
            for (int i = 0; i < clickableSpans.length; i++) {
            for (int i = 0; i < clickableSpans.length; i++) {
                clickableSpans[i].setAccessibilityNodeInfo(this);
                clickableSpans[i].copyConnectionDataFrom(this);
            }
            }
            AccessibilityURLSpan[] urlSpans =
            AccessibilityURLSpan[] urlSpans =
                    spanned.getSpans(0, mText.length(), AccessibilityURLSpan.class);
                    spanned.getSpans(0, mText.length(), AccessibilityURLSpan.class);
            for (int i = 0; i < urlSpans.length; i++) {
            for (int i = 0; i < urlSpans.length; i++) {
                urlSpans[i].setAccessibilityNodeInfo(this);
                urlSpans[i].copyConnectionDataFrom(this);
            }
            }
        }
        }
        return mText;
        return mText;
@@ -2840,6 +2840,17 @@ public class AccessibilityNodeInfo implements Parcelable {
        mConnectionId = connectionId;
        mConnectionId = connectionId;
    }
    }


    /**
     * Get the connection ID.
     *
     * @return The connection id
     *
     * @hide
     */
    public int getConnectionId() {
        return mConnectionId;
    }

    /**
    /**
     * {@inheritDoc}
     * {@inheritDoc}
     */
     */
@@ -3354,7 +3365,7 @@ public class AccessibilityNodeInfo implements Parcelable {
        mLabeledById = UNDEFINED_NODE_ID;
        mLabeledById = UNDEFINED_NODE_ID;
        mTraversalBefore = UNDEFINED_NODE_ID;
        mTraversalBefore = UNDEFINED_NODE_ID;
        mTraversalAfter = UNDEFINED_NODE_ID;
        mTraversalAfter = UNDEFINED_NODE_ID;
        mWindowId = UNDEFINED_ITEM_ID;
        mWindowId = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
        mConnectionId = UNDEFINED_CONNECTION_ID;
        mConnectionId = UNDEFINED_CONNECTION_ID;
        mMaxTextLength = -1;
        mMaxTextLength = -1;
        mMovementGranularities = 0;
        mMovementGranularities = 0;
@@ -3517,9 +3528,9 @@ public class AccessibilityNodeInfo implements Parcelable {
    }
    }


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


    @Override
    @Override