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

Commit 74bf3501 authored by Phil Weaver's avatar Phil Weaver Committed by Android (Google) Code Review
Browse files

Merge "Accessibility support for ClickableSpan"

parents 505d8262 193520e3
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