1/*
2 * Copyright (C) 2006 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
17package android.text.method;
18
19import android.text.Layout;
20import android.text.NoCopySpan;
21import android.text.Selection;
22import android.text.Spannable;
23import android.text.style.ClickableSpan;
24import android.view.KeyEvent;
25import android.view.MotionEvent;
26import android.view.View;
27import android.widget.TextView;
28
29/**
30 * A movement method that traverses links in the text buffer and scrolls if necessary.
31 * Supports clicking on links with DPad Center or Enter.
32 */
33public class LinkMovementMethod extends ScrollingMovementMethod {
34    private static final int CLICK = 1;
35    private static final int UP = 2;
36    private static final int DOWN = 3;
37
38    @Override
39    protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
40            int movementMetaState, KeyEvent event) {
41        switch (keyCode) {
42            case KeyEvent.KEYCODE_DPAD_CENTER:
43            case KeyEvent.KEYCODE_ENTER:
44                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
45                    if (event.getAction() == KeyEvent.ACTION_DOWN &&
46                            event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
47                        return true;
48                    }
49                }
50                break;
51        }
52        return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
53    }
54
55    @Override
56    protected boolean up(TextView widget, Spannable buffer) {
57        if (action(UP, widget, buffer)) {
58            return true;
59        }
60
61        return super.up(widget, buffer);
62    }
63
64    @Override
65    protected boolean down(TextView widget, Spannable buffer) {
66        if (action(DOWN, widget, buffer)) {
67            return true;
68        }
69
70        return super.down(widget, buffer);
71    }
72
73    @Override
74    protected boolean left(TextView widget, Spannable buffer) {
75        if (action(UP, widget, buffer)) {
76            return true;
77        }
78
79        return super.left(widget, buffer);
80    }
81
82    @Override
83    protected boolean right(TextView widget, Spannable buffer) {
84        if (action(DOWN, widget, buffer)) {
85            return true;
86        }
87
88        return super.right(widget, buffer);
89    }
90
91    private boolean action(int what, TextView widget, Spannable buffer) {
92        Layout layout = widget.getLayout();
93
94        int padding = widget.getTotalPaddingTop() +
95                      widget.getTotalPaddingBottom();
96        int areatop = widget.getScrollY();
97        int areabot = areatop + widget.getHeight() - padding;
98
99        int linetop = layout.getLineForVertical(areatop);
100        int linebot = layout.getLineForVertical(areabot);
101
102        int first = layout.getLineStart(linetop);
103        int last = layout.getLineEnd(linebot);
104
105        ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
106
107        int a = Selection.getSelectionStart(buffer);
108        int b = Selection.getSelectionEnd(buffer);
109
110        int selStart = Math.min(a, b);
111        int selEnd = Math.max(a, b);
112
113        if (selStart < 0) {
114            if (buffer.getSpanStart(FROM_BELOW) >= 0) {
115                selStart = selEnd = buffer.length();
116            }
117        }
118
119        if (selStart > last)
120            selStart = selEnd = Integer.MAX_VALUE;
121        if (selEnd < first)
122            selStart = selEnd = -1;
123
124        switch (what) {
125        case CLICK:
126            if (selStart == selEnd) {
127                return false;
128            }
129
130            ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
131
132            if (link.length != 1)
133                return false;
134
135            link[0].onClick(widget);
136            break;
137
138        case UP:
139            int beststart, bestend;
140
141            beststart = -1;
142            bestend = -1;
143
144            for (int i = 0; i < candidates.length; i++) {
145                int end = buffer.getSpanEnd(candidates[i]);
146
147                if (end < selEnd || selStart == selEnd) {
148                    if (end > bestend) {
149                        beststart = buffer.getSpanStart(candidates[i]);
150                        bestend = end;
151                    }
152                }
153            }
154
155            if (beststart >= 0) {
156                Selection.setSelection(buffer, bestend, beststart);
157                return true;
158            }
159
160            break;
161
162        case DOWN:
163            beststart = Integer.MAX_VALUE;
164            bestend = Integer.MAX_VALUE;
165
166            for (int i = 0; i < candidates.length; i++) {
167                int start = buffer.getSpanStart(candidates[i]);
168
169                if (start > selStart || selStart == selEnd) {
170                    if (start < beststart) {
171                        beststart = start;
172                        bestend = buffer.getSpanEnd(candidates[i]);
173                    }
174                }
175            }
176
177            if (bestend < Integer.MAX_VALUE) {
178                Selection.setSelection(buffer, beststart, bestend);
179                return true;
180            }
181
182            break;
183        }
184
185        return false;
186    }
187
188    @Override
189    public boolean onTouchEvent(TextView widget, Spannable buffer,
190                                MotionEvent event) {
191        int action = event.getAction();
192
193        if (action == MotionEvent.ACTION_UP ||
194            action == MotionEvent.ACTION_DOWN) {
195            int x = (int) event.getX();
196            int y = (int) event.getY();
197
198            x -= widget.getTotalPaddingLeft();
199            y -= widget.getTotalPaddingTop();
200
201            x += widget.getScrollX();
202            y += widget.getScrollY();
203
204            Layout layout = widget.getLayout();
205            int line = layout.getLineForVertical(y);
206            int off = layout.getOffsetForHorizontal(line, x);
207
208            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
209
210            if (link.length != 0) {
211                if (action == MotionEvent.ACTION_UP) {
212                    link[0].onClick(widget);
213                } else if (action == MotionEvent.ACTION_DOWN) {
214                    Selection.setSelection(buffer,
215                                           buffer.getSpanStart(link[0]),
216                                           buffer.getSpanEnd(link[0]));
217                }
218
219                return true;
220            } else {
221                Selection.removeSelection(buffer);
222            }
223        }
224
225        return super.onTouchEvent(widget, buffer, event);
226    }
227
228    @Override
229    public void initialize(TextView widget, Spannable text) {
230        Selection.removeSelection(text);
231        text.removeSpan(FROM_BELOW);
232    }
233
234    @Override
235    public void onTakeFocus(TextView view, Spannable text, int dir) {
236        Selection.removeSelection(text);
237
238        if ((dir & View.FOCUS_BACKWARD) != 0) {
239            text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
240        } else {
241            text.removeSpan(FROM_BELOW);
242        }
243    }
244
245    public static MovementMethod getInstance() {
246        if (sInstance == null)
247            sInstance = new LinkMovementMethod();
248
249        return sInstance;
250    }
251
252    private static LinkMovementMethod sInstance;
253    private static Object FROM_BELOW = new NoCopySpan.Concrete();
254}
255