1d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/* 2d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Copyright (C) 2016 The Android Open Source Project 3d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 4d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Licensed under the Apache License, Version 2.0 (the "License"); 5d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * you may not use this file except in compliance with the License. 6d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * You may obtain a copy of the License at 7d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 8d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * http://www.apache.org/licenses/LICENSE-2.0 9d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 10d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Unless required by applicable law or agreed to in writing, software 11d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * distributed under the License is distributed on an "AS IS" BASIS, 12d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * See the License for the specific language governing permissions and 14d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * limitations under the License. 15d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */ 16d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 17d832154e333a3a45b5faecd518b664ddd297183cMaurice Lampackage com.android.setupwizardlib.util; 18d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 19d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.graphics.Rect; 20ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.os.Build; 21d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.os.Bundle; 22b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lamimport android.support.annotation.NonNull; 23b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lamimport android.support.annotation.VisibleForTesting; 24ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.support.v4.view.AccessibilityDelegateCompat; 25d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 26ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; 27d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.widget.ExploreByTouchHelper; 28d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Layout; 29d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Spanned; 30d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.style.ClickableSpan; 31d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.util.Log; 32ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.view.MotionEvent; 33ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.view.View; 34ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lamimport android.view.ViewGroup; 35d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.view.accessibility.AccessibilityEvent; 36d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.widget.TextView; 37d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 38d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport java.util.List; 39d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 40d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/** 41d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and 42d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * clicked by accessibility services. 43d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 44b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * <p><strong>Note:</strong> This class is a no-op on Android O or above since there is native 45b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * support for ClickableSpan accessibility. 46b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * 47b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * <p>Sample usage: 48d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * <pre> 49d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * LinkAccessibilityHelper mAccessibilityHelper; 50d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 51d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * private void init() { 52d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * mAccessibilityHelper = new LinkAccessibilityHelper(myTextView); 53d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper); 54d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 55d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 56d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * {@literal @}Override 57d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) { 58d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { 59d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * return true; 60d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 61d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * return super.dispatchHoverEvent(event); 62d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * } 63d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * </pre> 64d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * 65d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see com.android.setupwizardlib.view.RichTextView 66d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see android.support.v4.widget.ExploreByTouchHelper 67d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */ 68ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lampublic class LinkAccessibilityHelper extends AccessibilityDelegateCompat { 69d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 70d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam private static final String TAG = "LinkAccessibilityHelper"; 71d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 72b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private final AccessibilityDelegateCompat mDelegate; 73d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 74d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam public LinkAccessibilityHelper(TextView view) { 75b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam this(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O 76b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Platform support was added in O. This helper will be no-op 77b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ? new AccessibilityDelegateCompat() 78b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Pre-O, we extend ExploreByTouchHelper to expose a virtual view hierarchy 79b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam : new PreOLinkAccessibilityHelper(view)); 80b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 81ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 82b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam @VisibleForTesting 83b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam LinkAccessibilityHelper(@NonNull AccessibilityDelegateCompat delegate) { 84b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate = delegate; 85d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 86d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 87d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam @Override 88ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public void sendAccessibilityEvent(View host, int eventType) { 89b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate.sendAccessibilityEvent(host, eventType); 90ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 91ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 92ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 93ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { 94b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate.sendAccessibilityEventUnchecked(host, event); 95ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 96ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 97ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 98ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 99b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return mDelegate.dispatchPopulateAccessibilityEvent(host, event); 100ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 101ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 102ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 103ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 104b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate.onPopulateAccessibilityEvent(host, event); 105ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 106ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 107ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 108ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { 109b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate.onInitializeAccessibilityEvent(host, event); 110ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 111ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 112ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 113ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { 114b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mDelegate.onInitializeAccessibilityNodeInfo(host, info); 115ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 116ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 117ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 118ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, 119ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam AccessibilityEvent event) { 120b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return mDelegate.onRequestSendAccessibilityEvent(host, child, event); 121ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 122ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 123ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 124ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { 125b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return mDelegate.getAccessibilityNodeProvider(host); 126ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 127ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 128ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam @Override 129ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public boolean performAccessibilityAction(View host, int action, Bundle args) { 130b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return mDelegate.performAccessibilityAction(host, action, args); 131ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 132ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 133ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam /** 134b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * Dispatches hover event to the virtual view hierarchy. This method should be called in 135b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * {@link View#dispatchHoverEvent(MotionEvent)}. 136b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * 137b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam * @see ExploreByTouchHelper#dispatchHoverEvent(MotionEvent) 138ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam */ 139ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam public final boolean dispatchHoverEvent(MotionEvent event) { 140b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return mDelegate instanceof ExploreByTouchHelper 141b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam && ((ExploreByTouchHelper) mDelegate).dispatchHoverEvent(event); 142ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam } 143ba44e63191bbb6bc564c18626119aebf2e717a26Maurice Lam 144b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam @VisibleForTesting 145b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam static class PreOLinkAccessibilityHelper extends ExploreByTouchHelper { 146b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam 147b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private final Rect mTempRect = new Rect(); 148b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private final TextView mView; 149b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam 150b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam PreOLinkAccessibilityHelper(TextView view) { 151b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam super(view); 152b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mView = view; 153d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 154d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 15525b95910469405efda4f481fe3c949aeb04a5ca9Maurice Lam @Override 156b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam protected int getVirtualViewAt(float x, float y) { 157b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final CharSequence text = mView.getText(); 158b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (text instanceof Spanned) { 159b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final Spanned spannedText = (Spanned) text; 160b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int offset = getOffsetForPosition(mView, x, y); 161b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan[] linkSpans = 162b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam spannedText.getSpans(offset, offset, ClickableSpan.class); 163b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (linkSpans.length == 1) { 164b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan linkSpan = linkSpans[0]; 165b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return spannedText.getSpanStart(linkSpan); 166b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 167d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 168b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return ExploreByTouchHelper.INVALID_ID; 169d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 170d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 17125b95910469405efda4f481fe3c949aeb04a5ca9Maurice Lam @Override 172b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 173b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final CharSequence text = mView.getText(); 174b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (text instanceof Spanned) { 175b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final Spanned spannedText = (Spanned) text; 176b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), 177b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan.class); 178b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam for (ClickableSpan span : linkSpans) { 179b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam virtualViewIds.add(spannedText.getSpanStart(span)); 180b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 181b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 182d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 183d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 18425b95910469405efda4f481fe3c949aeb04a5ca9Maurice Lam @Override 185b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 186b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final ClickableSpan span = getSpanForOffset(virtualViewId); 187b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (span != null) { 188b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam event.setContentDescription(getTextForSpan(span)); 189b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } else { 190b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 191b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam event.setContentDescription(mView.getText()); 192b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 193d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 194d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 19525b95910469405efda4f481fe3c949aeb04a5ca9Maurice Lam @Override 196b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam protected void onPopulateNodeForVirtualView( 197b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam int virtualViewId, 198b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam AccessibilityNodeInfoCompat info) { 199b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final ClickableSpan span = getSpanForOffset(virtualViewId); 200d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam if (span != null) { 201b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.setContentDescription(getTextForSpan(span)); 202d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } else { 203d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 204b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.setContentDescription(mView.getText()); 205d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 206b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.setFocusable(true); 207b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.setClickable(true); 208b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam getBoundsForSpan(span, mTempRect); 209b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (mTempRect.isEmpty()) { 210b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); 211b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam mTempRect.set(0, 0, 1, 1); 212b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 213b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.setBoundsInParent(mTempRect); 214b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); 215d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 216d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 21725b95910469405efda4f481fe3c949aeb04a5ca9Maurice Lam @Override 218b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam protected boolean onPerformActionForVirtualView( 219b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam int virtualViewId, 220b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam int action, 221b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Bundle arguments) { 222b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { 223b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan span = getSpanForOffset(virtualViewId); 224b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (span != null) { 225b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam span.onClick(mView); 226b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return true; 227b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } else { 228b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); 229b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 230d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 231b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return false; 232d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 233d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 234b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private ClickableSpan getSpanForOffset(int offset) { 235b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam CharSequence text = mView.getText(); 236b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (text instanceof Spanned) { 237b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Spanned spannedText = (Spanned) text; 238b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); 239b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (spans.length == 1) { 240b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return spans[0]; 241b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 242b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 243b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return null; 244d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 245d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 246b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private CharSequence getTextForSpan(ClickableSpan span) { 247b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam CharSequence text = mView.getText(); 248b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (text instanceof Spanned) { 249cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam Spanned spannedText = (Spanned) text; 250b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return spannedText.subSequence( 251b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam spannedText.getSpanStart(span), 252b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam spannedText.getSpanEnd(span)); 253b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 254b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return text; 255b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 256b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam 257b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for 258b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // the section on the first line. 259b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { 260b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam CharSequence text = mView.getText(); 261b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.setEmpty(); 262b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (text instanceof Spanned) { 263b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final Layout layout = mView.getLayout(); 264b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (layout != null) { 265b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam Spanned spannedText = (Spanned) text; 266b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int spanStart = spannedText.getSpanStart(span); 267b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int spanEnd = spannedText.getSpanEnd(span); 268b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final float xStart = layout.getPrimaryHorizontal(spanStart); 269b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final float xEnd = layout.getPrimaryHorizontal(spanEnd); 270b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int lineStart = layout.getLineForOffset(spanStart); 271b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int lineEnd = layout.getLineForOffset(spanEnd); 272b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam layout.getLineBounds(lineStart, outRect); 273b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (lineEnd == lineStart) { 274b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // If the span is on a single line, adjust both the left and right bounds 275b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // so outrect is exactly bounding the span. 276b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.left = (int) Math.min(xStart, xEnd); 277b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.right = (int) Math.max(xStart, xEnd); 27804eecdd42f0bc9751825f9f21131a59852256278Maurice Lam } else { 279b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // If the span wraps across multiple lines, only use the first line (as 280b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // returned by layout.getLineBounds above), and adjust the "start" of 281b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // outrect to where the span starts, leaving the "end" of outrect at the end 282b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // of the line. ("start" being left for LTR, and right for RTL) 283b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (layout.getParagraphDirection(lineStart) == Layout.DIR_RIGHT_TO_LEFT) { 284b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.right = (int) xStart; 285b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } else { 286b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.left = (int) xStart; 287b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 28804eecdd42f0bc9751825f9f21131a59852256278Maurice Lam } 289cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam 290b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Offset for padding 291b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); 292b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 293cf90658b5c16018c9f3db7fd1d872025cff5d1dcMaurice Lam } 294b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return outRect; 295d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 296d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 297b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Compat implementation of TextView#getOffsetForPosition(). 298d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 299b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private static int getOffsetForPosition(TextView view, float x, float y) { 300b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam if (view.getLayout() == null) return -1; 301b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam final int line = getLineAtCoordinate(view, y); 302b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return getOffsetAtCoordinate(view, line, x); 303b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 304d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 305b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private static float convertToLocalHorizontalCoordinate(TextView view, float x) { 306b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam x -= view.getTotalPaddingLeft(); 307b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Clamp the position to inside of the view. 308b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam x = Math.max(0.0f, x); 309b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x); 310b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam x += view.getScrollX(); 311b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return x; 312b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 313d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 314b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private static int getLineAtCoordinate(TextView view, float y) { 315b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam y -= view.getTotalPaddingTop(); 316b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam // Clamp the position to inside of the view. 317b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam y = Math.max(0.0f, y); 318b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y); 319b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam y += view.getScrollY(); 320b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return view.getLayout().getLineForVertical((int) y); 321b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 322d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam 323b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam private static int getOffsetAtCoordinate(TextView view, int line, float x) { 324b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam x = convertToLocalHorizontalCoordinate(view, x); 325b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam return view.getLayout().getOffsetForHorizontal(line, x); 326b72f3fb4598d2bd2560cdf5043defc80a0199e2eMaurice Lam } 327d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam } 328d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam} 329