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