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