1 /*
2  * Copyright (C) 2023 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 
17 package androidx.core.text.method;
18 
19 import android.os.Build;
20 import android.text.Layout;
21 import android.text.Selection;
22 import android.text.Spannable;
23 import android.text.method.LinkMovementMethod;
24 import android.text.method.Touch;
25 import android.view.MotionEvent;
26 import android.widget.TextView;
27 
28 import org.jspecify.annotations.NonNull;
29 import org.jspecify.annotations.Nullable;
30 
31 /**
32  * Backwards compatible version of {@link LinkMovementMethod} which fixes the issue that links can
33  * be triggered for touches outside of line bounds before Android V.
34  */
35 public class LinkMovementMethodCompat extends LinkMovementMethod {
36     private static LinkMovementMethodCompat sInstance;
37 
LinkMovementMethodCompat()38     private LinkMovementMethodCompat() {}
39 
40     @Override
onTouchEvent(@ullable TextView widget, @Nullable Spannable buffer, @Nullable MotionEvent event)41     public boolean onTouchEvent(@Nullable TextView widget, @Nullable Spannable buffer,
42             @Nullable MotionEvent event) {
43         if (Build.VERSION.SDK_INT < 35) {
44             int action = event.getAction();
45 
46             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
47                 int x = (int) event.getX();
48                 int y = (int) event.getY();
49 
50                 x -= widget.getTotalPaddingLeft();
51                 y -= widget.getTotalPaddingTop();
52 
53                 x += widget.getScrollX();
54                 y += widget.getScrollY();
55 
56                 Layout layout = widget.getLayout();
57                 boolean isOutOfLineBounds;
58                 if (y < 0 || y > layout.getHeight()) {
59                     isOutOfLineBounds = true;
60                 } else {
61                     int line = layout.getLineForVertical(y);
62                     isOutOfLineBounds = x < layout.getLineLeft(line)
63                             || x > layout.getLineRight(line);
64                 }
65 
66                 if (isOutOfLineBounds) {
67                     Selection.removeSelection(buffer);
68 
69                     // The same as super.onTouchEvent() in LinkMovementMethod.onTouchEvent(), i.e.
70                     // ScrollingMovementMethod.onTouchEvent().
71                     return Touch.onTouchEvent(widget, buffer, event);
72                 }
73             }
74         }
75 
76         return super.onTouchEvent(widget, buffer, event);
77     }
78 
79     /**
80      * Retrieves the singleton instance of {@link LinkMovementMethodCompat}.
81      *
82      * @return the singleton instance of {@link LinkMovementMethodCompat}
83      */
getInstance()84     public static @NonNull LinkMovementMethodCompat getInstance() {
85         if (sInstance == null) {
86             sInstance = new LinkMovementMethodCompat();
87         }
88 
89         return sInstance;
90     }
91 }
92