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  */
16 package android.text.style;
17 
18 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_ACCESSIBLE_CLICKABLE_SPAN;
19 import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_CONNECTION_ID;
20 import static android.view.accessibility.AccessibilityNodeInfo.UNDEFINED_NODE_ID;
21 import static android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID;
22 
23 import android.os.Bundle;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.text.ParcelableSpan;
27 import android.text.Spanned;
28 import android.text.TextUtils;
29 import android.view.View;
30 import android.view.accessibility.AccessibilityInteractionClient;
31 import android.view.accessibility.AccessibilityNodeInfo;
32 
33 import 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  */
46 public 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      */
AccessibilityClickableSpan(int originalClickableSpanId)58     public AccessibilityClickableSpan(int originalClickableSpanId) {
59         mOriginalClickableSpanId = originalClickableSpanId;
60     }
61 
AccessibilityClickableSpan(Parcel p)62     public AccessibilityClickableSpan(Parcel p) {
63         mOriginalClickableSpanId = p.readInt();
64     }
65 
66     @Override
getSpanTypeId()67     public int getSpanTypeId() {
68         return getSpanTypeIdInternal();
69     }
70 
71     @Override
getSpanTypeIdInternal()72     public int getSpanTypeIdInternal() {
73         return TextUtils.ACCESSIBILITY_CLICKABLE_SPAN;
74     }
75 
76     @Override
describeContents()77     public int describeContents() {
78         return 0;
79     }
80 
81     @Override
writeToParcel(Parcel dest, int flags)82     public void writeToParcel(Parcel dest, int flags) {
83         writeToParcelInternal(dest, flags);
84     }
85 
86     @Override
writeToParcelInternal(Parcel dest, int flags)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      */
findClickableSpan(CharSequence text)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      */
copyConnectionDataFrom(AccessibilityNodeInfo accessibilityNodeInfo)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
onClick(View unused)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 @android.annotation.NonNull 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