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