1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.text.style;
17
18import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;
19import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID;
20import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_NODE_ID;
21import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
22
23import android.os.Bundle;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.text.ParcelableSpan;
27import android.text.Spanned;
28import android.text.TextUtils;
29import android.view.View;
30import android.view.accessibility.AccessibilityInteractionClient;
31import android.view.accessibility.AccessibilityNodeInfo;
32
33import com.android.internal.R;
34
35/**
36 * {@link ClickableSpan} cannot be parceled, but accessibility services need to be able to cause
37 * their callback handlers to be called. This class serves as a parcelable placeholder for the
38 * real spans.
39 *
40 * This span is also passed back to an app's process when an accessibility service tries to click
41 * it. It contains enough information to track down the original clickable span so it can be
42 * called.
43 *
44 * @hide
45 */
46public class AccessibilityClickableSpan extends ClickableSpan
47        implements ParcelableSpan {
48    // The id of the span this one replaces
49    private final int mOriginalClickableSpanId;
50
51    private int mWindowId = UNDEFINED_WINDOW_ID;
52    private long mSourceNodeId = UNDEFINED_NODE_ID;
53    private int mConnectionId = UNDEFINED_CONNECTION_ID;
54
55    /**
56     * @param originalClickableSpanId The id of the span this one replaces
57     */
58    public AccessibilityClickableSpan(int originalClickableSpanId) {
59        mOriginalClickableSpanId = originalClickableSpanId;
60    }
61
62    public AccessibilityClickableSpan(Parcel p) {
63        mOriginalClickableSpanId = p.readInt();
64    }
65
66    @Override
67    public int getSpanTypeId() {
68        return getSpanTypeIdInternal();
69    }
70
71    @Override
72    public int getSpanTypeIdInternal() {
73        return TextUtils.ACCESSIBILITY_CLICKABLE_SPAN;
74    }
75
76    @Override
77    public int describeContents() {
78        return 0;
79    }
80
81    @Override
82    public void writeToParcel(Parcel dest, int flags) {
83        writeToParcelInternal(dest, flags);
84    }
85
86    @Override
87    public void writeToParcelInternal(Parcel dest, int flags) {
88        dest.writeInt(mOriginalClickableSpanId);
89    }
90
91    /**
92     * Find the ClickableSpan that matches the one used to create this object.
93     *
94     * @param text The text that contains the original ClickableSpan.
95     * @return The ClickableSpan that matches this object, or {@code null} if no such object
96     * can be found.
97     */
98    public ClickableSpan findClickableSpan(CharSequence text) {
99        if (!(text instanceof Spanned)) {
100            return null;
101        }
102        Spanned sp = (Spanned) text;
103        ClickableSpan[] os = sp.getSpans(0, text.length(), ClickableSpan.class);
104        for (int i = 0; i < os.length; i++) {
105            if (os[i].getId() == mOriginalClickableSpanId) {
106                return os[i];
107            }
108        }
109        return null;
110    }
111
112    /**
113     * Configure this object to perform clicks on the view that contains the original span.
114     *
115     * @param accessibilityNodeInfo The info corresponding to the view containing the original
116     *                              span.
117     */
118    public void copyConnectionDataFrom(AccessibilityNodeInfo accessibilityNodeInfo) {
119        mConnectionId = accessibilityNodeInfo.getConnectionId();
120        mWindowId = accessibilityNodeInfo.getWindowId();
121        mSourceNodeId = accessibilityNodeInfo.getSourceNodeId();
122    }
123
124    /**
125     * Perform the click from an accessibility service. Will not work unless
126     * setAccessibilityNodeInfo is called with a properly initialized node.
127     *
128     * @param unused This argument is required by the superclass but is unused. The real view will
129     * be determined by the AccessibilityNodeInfo.
130     */
131    @Override
132    public void onClick(View unused) {
133        Bundle arguments = new Bundle();
134        arguments.putParcelable(ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN, this);
135
136        if ((mWindowId == UNDEFINED_WINDOW_ID) || (mSourceNodeId == UNDEFINED_NODE_ID)
137                || (mConnectionId == UNDEFINED_CONNECTION_ID)) {
138            throw new RuntimeException(
139                    "ClickableSpan for accessibility service not properly initialized");
140        }
141
142        AccessibilityInteractionClient client = AccessibilityInteractionClient.getInstance();
143        client.performAccessibilityAction(mConnectionId, mWindowId, mSourceNodeId,
144                R.id.accessibilityActionClickOnClickableSpan, arguments);
145    }
146
147    public static final Parcelable.Creator<AccessibilityClickableSpan> CREATOR =
148            new Parcelable.Creator<AccessibilityClickableSpan>() {
149                @Override
150                public AccessibilityClickableSpan createFromParcel(Parcel parcel) {
151                    return new AccessibilityClickableSpan(parcel);
152                }
153
154                @Override
155                public AccessibilityClickableSpan[] newArray(int size) {
156                    return new AccessibilityClickableSpan[size];
157                }
158            };
159}
160