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

Commit 193520e3 authored by Phil Weaver's avatar Phil Weaver
Browse files

Accessibility support for ClickableSpan

Bug: 17726921
Test: Adding CTS tests for new behavior in linked CL.
Change-Id: Ifa85c309106d5ef29bb130edff9e2e0b88547a8f
parent c9facc0a
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -29,6 +29,8 @@ import android.os.Parcelable;
import android.os.SystemProperties;
import android.provider.Settings;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AccessibilityClickableSpan;
import android.text.style.AccessibilityURLSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
@@ -621,7 +623,11 @@ public class TextUtils {
    /** @hide */
    public static final int TTS_SPAN = 24;
    /** @hide */
    public static final int LAST_SPAN = TTS_SPAN;
    public static final int ACCESSIBILITY_CLICKABLE_SPAN = 25;
    /** @hide */
    public static final int ACCESSIBILITY_URL_SPAN = 26;
    /** @hide */
    public static final int LAST_SPAN = ACCESSIBILITY_URL_SPAN;

    /**
     * Flatten a CharSequence and whatever styles can be copied across processes
@@ -803,6 +809,14 @@ public class TextUtils {
                    readSpan(p, sp, new TtsSpan(p));
                    break;

                case ACCESSIBILITY_CLICKABLE_SPAN:
                    readSpan(p, sp, new AccessibilityClickableSpan(p));
                    break;

                case ACCESSIBILITY_URL_SPAN:
                    readSpan(p, sp, new AccessibilityURLSpan(p));
                    break;

                default:
                    throw new RuntimeException("bogus span encoding " + kind);
                }
+156 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.text.style;

import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;

import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.ParcelableSpan;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

import com.android.internal.R;

import java.lang.ref.WeakReference;


/**
 * {@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
 * real spans.
 *
 * This span is also passed back to an app's process when an accessibility service tries to click
 * it. It contains enough information to track down the original clickable span so it can be
 * called.
 *
 * @hide
 */
public class AccessibilityClickableSpan extends ClickableSpan
        implements ParcelableSpan {
    // The id of the span this one replaces
    private final int mOriginalClickableSpanId;

    // Only retain a weak reference to the node to avoid referencing cycles that could create memory
    // leaks.
    private WeakReference<AccessibilityNodeInfo> mAccessibilityNodeInfoRef;


    /**
     * @param originalClickableSpanId The id of the span this one replaces
     */
    public AccessibilityClickableSpan(int originalClickableSpanId) {
        mOriginalClickableSpanId = originalClickableSpanId;
    }

    public AccessibilityClickableSpan(Parcel p) {
        mOriginalClickableSpanId = p.readInt();
    }

    @Override
    public int getSpanTypeId() {
        return getSpanTypeIdInternal();
    }

    @Override
    public int getSpanTypeIdInternal() {
        return TextUtils.ACCESSIBILITY_CLICKABLE_SPAN;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        writeToParcelInternal(dest, flags);
    }

    @Override
    public void writeToParcelInternal(Parcel dest, int flags) {
        dest.writeInt(mOriginalClickableSpanId);
    }

    /**
     * Find the ClickableSpan that matches the one used to create this object.
     *
     * @param text The text that contains the original ClickableSpan.
     * @return The ClickableSpan that matches this object, or {@code null} if no such object
     * can be found.
     */
    public ClickableSpan findClickableSpan(CharSequence text) {
        if (!(text instanceof Spanned)) {
            return null;
        }
        Spanned sp = (Spanned) text;
        ClickableSpan[] os = sp.getSpans(0, text.length(), ClickableSpan.class);
        for (int i = 0; i < os.length; i++) {
            if (os[i].getId() == mOriginalClickableSpanId) {
                return os[i];
            }
        }
        return null;
    }

    /**
     * Set the accessibilityNodeInfo that this placeholder belongs to. This node is not
     * 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
     */
    public void setAccessibilityNodeInfo(AccessibilityNodeInfo accessibilityNodeInfo) {
        mAccessibilityNodeInfoRef = new WeakReference<>(accessibilityNodeInfo);
    }

    /**
     * Perform the click from an accessibility service. Will not work unless
     * setAccessibilityNodeInfo is called with a properly initialized node.
     *
     * @param unused This argument is required by the superclass but is unused. The real view will
     * be determined by the AccessibilityNodeInfo.
     */
    @Override
    public void onClick(View unused) {
        if (mAccessibilityNodeInfoRef == null) {
            return;
        }
        AccessibilityNodeInfo info = mAccessibilityNodeInfoRef.get();
        if (info == null) {
            return;
        }
        Bundle arguments = new Bundle();
        arguments.putParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN, this);

        info.performAction(R.id.accessibilityActionClickOnClickableSpan, arguments);
    }

    public static final Parcelable.Creator<AccessibilityClickableSpan> CREATOR =
            new Parcelable.Creator<AccessibilityClickableSpan>() {
                @Override
                public AccessibilityClickableSpan createFromParcel(Parcel parcel) {
                    return new AccessibilityClickableSpan(parcel);
                }

                @Override
                public AccessibilityClickableSpan[] newArray(int size) {
                    return new AccessibilityClickableSpan[size];
                }
            };
}
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.text.style;

import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;

/**
 * URLSpan's onClick method does not work from an accessibility service. This version of it does.
 * It is used to replace URLSpans in {@link AccessibilityNodeInfo#setText(CharSequence)}
 * @hide
 */
public class AccessibilityURLSpan extends URLSpan implements Parcelable {
    final AccessibilityClickableSpan mAccessibilityClickableSpan;

    /**
     * @param spanToReplace The original span
     */
    public AccessibilityURLSpan(URLSpan spanToReplace) {
        super(spanToReplace.getURL());
        mAccessibilityClickableSpan =
                new AccessibilityClickableSpan(spanToReplace.getId());
    }

    public AccessibilityURLSpan(Parcel p) {
        super(p);
        mAccessibilityClickableSpan = new AccessibilityClickableSpan(p);
    }

    @Override
    public int getSpanTypeId() {
        return getSpanTypeIdInternal();
    }

    @Override
    public int getSpanTypeIdInternal() {
        return TextUtils.ACCESSIBILITY_URL_SPAN;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        writeToParcelInternal(dest, flags);
    }

    @Override
    public void writeToParcelInternal(Parcel dest, int flags) {
        super.writeToParcelInternal(dest, flags);
        mAccessibilityClickableSpan.writeToParcel(dest, flags);
    }

    @Override
    public void onClick(View unused) {
        mAccessibilityClickableSpan.onClick(unused);
    }

    /**
     * Delegated to AccessibilityClickableSpan
     * @param accessibilityNodeInfo
     */
    public void setAccessibilityNodeInfo(AccessibilityNodeInfo accessibilityNodeInfo) {
        mAccessibilityClickableSpan.setAccessibilityNodeInfo(accessibilityNodeInfo);
    }
}
+14 −1
Original line number Diff line number Diff line
@@ -26,6 +26,9 @@ import android.view.View;
 * be called.
 */
public abstract class ClickableSpan extends CharacterStyle implements UpdateAppearance {
    private static int sIdCounter = 0;

    private int mId = sIdCounter++;

    /**
     * Performs the click action associated with this span.
@@ -40,4 +43,14 @@ public abstract class ClickableSpan extends CharacterStyle implements UpdateAppe
        ds.setColor(ds.linkColor);
        ds.setUnderlineText(true);
    }

    /**
     * Get the unique ID for this span.
     *
     * @return The unique ID.
     * @hide
     */
    public int getId() {
        return mId;
    }
}
+54 −10
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.view;

import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;

import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -24,8 +26,12 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.text.TextUtils;
import android.text.style.AccessibilityClickableSpan;
import android.text.style.ClickableSpan;
import android.util.LongSparseArray;
import android.view.View.AttachInfo;
import android.view.accessibility.AccessibilityInteractionClient;
@@ -33,6 +39,7 @@ import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;

import com.android.internal.R;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.Predicate;

@@ -655,19 +662,25 @@ final class AccessibilityInteractionController {
                target = mViewRootImpl.mView;
            }
            if (target != null && isShown(target)) {
                if (action == R.id.accessibilityActionClickOnClickableSpan) {
                    // Handle this hidden action separately
                    succeeded = handleClickableSpanActionUiThread(
                            target, virtualDescendantId, arguments);
                } else {
                    AccessibilityNodeProvider provider = target.getAccessibilityNodeProvider();
                    if (provider != null) {
                        if (virtualDescendantId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
                            succeeded = provider.performAction(virtualDescendantId, action,
                                    arguments);
                        } else {
                        succeeded = provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
                                action, arguments);
                            succeeded = provider.performAction(
                                    AccessibilityNodeProvider.HOST_VIEW_ID, action, arguments);
                        }
                    } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
                        succeeded = target.performAccessibilityAction(action, arguments);
                    }
                }
            }
        } finally {
            try {
                mViewRootImpl.mAttachInfo.mAccessibilityFetchFlags = 0;
@@ -816,6 +829,37 @@ final class AccessibilityInteractionController {
        return (appScale != 1.0f || (spec != null && !spec.isNop()));
    }

    private boolean handleClickableSpanActionUiThread(
            View view, int virtualDescendantId, Bundle arguments) {
        Parcelable span = arguments.getParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN);
        if (!(span instanceof AccessibilityClickableSpan)) {
            return false;
        }

        // Find the original ClickableSpan if it's still on the screen
        AccessibilityNodeInfo infoWithSpan = null;
        AccessibilityNodeProvider provider = view.getAccessibilityNodeProvider();
        if (provider != null) {
            int idForNode = (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID)
                    ? AccessibilityNodeProvider.HOST_VIEW_ID : virtualDescendantId;
            infoWithSpan = provider.createAccessibilityNodeInfo(idForNode);
        } else if (virtualDescendantId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) {
            infoWithSpan = view.createAccessibilityNodeInfo();
        }
        if (infoWithSpan == null) {
            return false;
        }

        // Click on the corresponding span
        ClickableSpan clickableSpan = ((AccessibilityClickableSpan) span).findClickableSpan(
                infoWithSpan.getOriginalText());
        if (clickableSpan != null) {
            clickableSpan.onClick(view);
            return true;
        }
        return false;
    }

    /**
     * This class encapsulates a prefetching strategy for the accessibility APIs for
     * querying window content. It is responsible to prefetch a batch of
Loading