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.graphics.Paint;
20import android.icu.lang.UCharacter;
21import android.icu.lang.UProperty;
22import android.view.KeyEvent;
23import android.view.View;
24import android.text.*;
25import android.text.method.TextKeyListener.Capitalize;
26import android.text.style.ReplacementSpan;
27import android.widget.TextView;
28
29import com.android.internal.annotations.GuardedBy;
30
31import java.text.BreakIterator;
32import java.util.Arrays;
33import java.util.Collections;
34import java.util.HashSet;
35
36/**
37 * Abstract base class for key listeners.
38 *
39 * Provides a basic foundation for entering and editing text.
40 * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
41 * characters as keys are pressed.
42 * <p></p>
43 * As for all implementations of {@link KeyListener}, this class is only concerned
44 * with hardware keyboards.  Software input methods have no obligation to trigger
45 * the methods in this class.
46 */
47public abstract class BaseKeyListener extends MetaKeyKeyListener
48        implements KeyListener {
49    /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
50
51    private static final int LINE_FEED = 0x0A;
52    private static final int CARRIAGE_RETURN = 0x0D;
53
54    private final Object mLock = new Object();
55
56    @GuardedBy("mLock")
57    static Paint sCachedPaint = null;
58
59    /**
60     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
61     * a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
62     * deletes the character before the cursor, if any; ALT+DEL deletes everything on
63     * the line the cursor is on.
64     *
65     * @return true if anything was deleted; false otherwise.
66     */
67    public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
68        return backspaceOrForwardDelete(view, content, keyCode, event, false);
69    }
70
71    /**
72     * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
73     * key in a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
74     * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
75     * the line the cursor is on.
76     *
77     * @return true if anything was deleted; false otherwise.
78     */
79    public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
80        return backspaceOrForwardDelete(view, content, keyCode, event, true);
81    }
82
83    // Returns true if the given code point is a variation selector.
84    private static boolean isVariationSelector(int codepoint) {
85        return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR);
86    }
87
88    // Returns the offset of the replacement span edge if the offset is inside of the replacement
89    // span.  Otherwise, does nothing and returns the input offset value.
90    private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) {
91        if (!(text instanceof Spanned)) {
92            return offset;
93        }
94
95        ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class);
96        for (int i = 0; i < spans.length; i++) {
97            final int start = ((Spanned) text).getSpanStart(spans[i]);
98            final int end = ((Spanned) text).getSpanEnd(spans[i]);
99
100            if (start < offset && end > offset) {
101                offset = moveToStart ? start : end;
102            }
103        }
104        return offset;
105    }
106
107    // Returns the start offset to be deleted by a backspace key from the given offset.
108    private static int getOffsetForBackspaceKey(CharSequence text, int offset) {
109        if (offset <= 1) {
110            return 0;
111        }
112
113        // Initial state
114        final int STATE_START = 0;
115
116        // The offset is immediately before line feed.
117        final int STATE_LF = 1;
118
119        // The offset is immediately before a KEYCAP.
120        final int STATE_BEFORE_KEYCAP = 2;
121        // The offset is immediately before a variation selector and a KEYCAP.
122        final int STATE_BEFORE_VS_AND_KEYCAP = 3;
123
124        // The offset is immediately before an emoji modifier.
125        final int STATE_BEFORE_EMOJI_MODIFIER = 4;
126        // The offset is immediately before a variation selector and an emoji modifier.
127        final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5;
128
129        // The offset is immediately before a variation selector.
130        final int STATE_BEFORE_VS = 6;
131
132        // The offset is immediately before a ZWJ emoji.
133        final int STATE_BEFORE_ZWJ_EMOJI = 7;
134        // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
135        final int STATE_BEFORE_ZWJ = 8;
136        // The offset is immediately before a variation selector and a ZWJ that were seen before a
137        // ZWJ emoji.
138        final int STATE_BEFORE_VS_AND_ZWJ = 9;
139
140        // The number of following RIS code points is odd.
141        final int STATE_ODD_NUMBERED_RIS = 10;
142        // The number of following RIS code points is even.
143        final int STATE_EVEN_NUMBERED_RIS = 11;
144
145        // The state machine has been stopped.
146        final int STATE_FINISHED = 12;
147
148        int deleteCharCount = 0;  // Char count to be deleted by backspace.
149        int lastSeenVSCharCount = 0;  // Char count of previous variation selector.
150
151        int state = STATE_START;
152
153        int tmpOffset = offset;
154        do {
155            final int codePoint = Character.codePointBefore(text, tmpOffset);
156            tmpOffset -= Character.charCount(codePoint);
157
158            switch (state) {
159                case STATE_START:
160                    deleteCharCount = Character.charCount(codePoint);
161                    if (codePoint == LINE_FEED) {
162                        state = STATE_LF;
163                    } else if (isVariationSelector(codePoint)) {
164                        state = STATE_BEFORE_VS;
165                    } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
166                        state = STATE_ODD_NUMBERED_RIS;
167                    } else if (Emoji.isEmojiModifier(codePoint)) {
168                        state = STATE_BEFORE_EMOJI_MODIFIER;
169                    } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
170                        state = STATE_BEFORE_KEYCAP;
171                    } else if (Emoji.isEmoji(codePoint)) {
172                        state = STATE_BEFORE_ZWJ_EMOJI;
173                    } else {
174                        state = STATE_FINISHED;
175                    }
176                    break;
177                case STATE_LF:
178                    if (codePoint == CARRIAGE_RETURN) {
179                        ++deleteCharCount;
180                    }
181                    state = STATE_FINISHED;
182                case STATE_ODD_NUMBERED_RIS:
183                    if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
184                        deleteCharCount += 2; /* Char count of RIS */
185                        state = STATE_EVEN_NUMBERED_RIS;
186                    } else {
187                        state = STATE_FINISHED;
188                    }
189                    break;
190                case STATE_EVEN_NUMBERED_RIS:
191                    if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
192                        deleteCharCount -= 2; /* Char count of RIS */
193                        state = STATE_ODD_NUMBERED_RIS;
194                    } else {
195                        state = STATE_FINISHED;
196                    }
197                    break;
198                case STATE_BEFORE_KEYCAP:
199                    if (isVariationSelector(codePoint)) {
200                        lastSeenVSCharCount = Character.charCount(codePoint);
201                        state = STATE_BEFORE_VS_AND_KEYCAP;
202                        break;
203                    }
204
205                    if (Emoji.isKeycapBase(codePoint)) {
206                        deleteCharCount += Character.charCount(codePoint);
207                    }
208                    state = STATE_FINISHED;
209                    break;
210                case STATE_BEFORE_VS_AND_KEYCAP:
211                    if (Emoji.isKeycapBase(codePoint)) {
212                        deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
213                    }
214                    state = STATE_FINISHED;
215                    break;
216                case STATE_BEFORE_EMOJI_MODIFIER:
217                    if (isVariationSelector(codePoint)) {
218                        lastSeenVSCharCount = Character.charCount(codePoint);
219                        state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
220                        break;
221                    } else if (Emoji.isEmojiModifierBase(codePoint)) {
222                        deleteCharCount += Character.charCount(codePoint);
223                    }
224                    state = STATE_FINISHED;
225                    break;
226                case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
227                    if (Emoji.isEmojiModifierBase(codePoint)) {
228                        deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
229                    }
230                    state = STATE_FINISHED;
231                    break;
232                case STATE_BEFORE_VS:
233                    if (Emoji.isEmoji(codePoint)) {
234                        deleteCharCount += Character.charCount(codePoint);
235                        state = STATE_BEFORE_ZWJ_EMOJI;
236                        break;
237                    }
238
239                    if (!isVariationSelector(codePoint) &&
240                            UCharacter.getCombiningClass(codePoint) == 0) {
241                        deleteCharCount += Character.charCount(codePoint);
242                    }
243                    state = STATE_FINISHED;
244                    break;
245                case STATE_BEFORE_ZWJ_EMOJI:
246                    if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
247                        state = STATE_BEFORE_ZWJ;
248                    } else {
249                        state = STATE_FINISHED;
250                    }
251                    break;
252                case STATE_BEFORE_ZWJ:
253                    if (Emoji.isEmoji(codePoint)) {
254                        deleteCharCount += Character.charCount(codePoint) + 1;  // +1 for ZWJ.
255                        state = STATE_BEFORE_ZWJ_EMOJI;
256                    } else if (isVariationSelector(codePoint)) {
257                        lastSeenVSCharCount = Character.charCount(codePoint);
258                        state = STATE_BEFORE_VS_AND_ZWJ;
259                    } else {
260                        state = STATE_FINISHED;
261                    }
262                    break;
263                case STATE_BEFORE_VS_AND_ZWJ:
264                    if (Emoji.isEmoji(codePoint)) {
265                        // +1 for ZWJ.
266                        deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
267                        lastSeenVSCharCount = 0;
268                        state = STATE_BEFORE_ZWJ_EMOJI;
269                    } else {
270                        state = STATE_FINISHED;
271                    }
272                    break;
273                default:
274                    throw new IllegalArgumentException("state " + state + " is unknown");
275            }
276        } while (tmpOffset > 0 && state != STATE_FINISHED);
277
278        return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
279    }
280
281    // Returns the end offset to be deleted by a forward delete key from the given offset.
282    private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
283        final int len = text.length();
284
285        if (offset >= len - 1) {
286            return len;
287        }
288
289        offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
290                offset, Paint.CURSOR_AFTER);
291
292        return adjustReplacementSpan(text, offset, false /* move to the end */);
293    }
294
295    private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
296            KeyEvent event, boolean isForwardDelete) {
297        // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
298        if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
299                & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
300            return false;
301        }
302
303        // If there is a current selection, delete it.
304        if (deleteSelection(view, content)) {
305            return true;
306        }
307
308        // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
309        boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
310        boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
311        boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
312
313        if (isCtrlActive) {
314            if (isAltActive || isShiftActive) {
315                // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
316                return false;
317            }
318            return deleteUntilWordBoundary(view, content, isForwardDelete);
319        }
320
321        // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
322        if (isAltActive && deleteLine(view, content)) {
323            return true;
324        }
325
326        // Delete a character.
327        final int start = Selection.getSelectionEnd(content);
328        final int end;
329        if (isForwardDelete) {
330            final Paint paint;
331            if (view instanceof TextView) {
332                paint = ((TextView)view).getPaint();
333            } else {
334                synchronized (mLock) {
335                    if (sCachedPaint == null) {
336                        sCachedPaint = new Paint();
337                    }
338                    paint = sCachedPaint;
339                }
340            }
341            end = getOffsetForForwardDeleteKey(content, start, paint);
342        } else {
343            end = getOffsetForBackspaceKey(content, start);
344        }
345        if (start != end) {
346            content.delete(Math.min(start, end), Math.max(start, end));
347            return true;
348        }
349        return false;
350    }
351
352    private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
353        int currentCursorOffset = Selection.getSelectionStart(content);
354
355        // If there is a selection, do nothing.
356        if (currentCursorOffset != Selection.getSelectionEnd(content)) {
357            return false;
358        }
359
360        // Early exit if there is no contents to delete.
361        if ((!isForwardDelete && currentCursorOffset == 0) ||
362            (isForwardDelete && currentCursorOffset == content.length())) {
363            return false;
364        }
365
366        WordIterator wordIterator = null;
367        if (view instanceof TextView) {
368            wordIterator = ((TextView)view).getWordIterator();
369        }
370
371        if (wordIterator == null) {
372            // Default locale is used for WordIterator since the appropriate locale is not clear
373            // here.
374            // TODO: Use appropriate locale for WordIterator.
375            wordIterator = new WordIterator();
376        }
377
378        int deleteFrom;
379        int deleteTo;
380
381        if (isForwardDelete) {
382            deleteFrom = currentCursorOffset;
383            wordIterator.setCharSequence(content, deleteFrom, content.length());
384            deleteTo = wordIterator.following(currentCursorOffset);
385            if (deleteTo == BreakIterator.DONE) {
386                deleteTo = content.length();
387            }
388        } else {
389            deleteTo = currentCursorOffset;
390            wordIterator.setCharSequence(content, 0, deleteTo);
391            deleteFrom = wordIterator.preceding(currentCursorOffset);
392            if (deleteFrom == BreakIterator.DONE) {
393                deleteFrom = 0;
394            }
395        }
396        content.delete(deleteFrom, deleteTo);
397        return true;
398    }
399
400    private boolean deleteSelection(View view, Editable content) {
401        int selectionStart = Selection.getSelectionStart(content);
402        int selectionEnd = Selection.getSelectionEnd(content);
403        if (selectionEnd < selectionStart) {
404            int temp = selectionEnd;
405            selectionEnd = selectionStart;
406            selectionStart = temp;
407        }
408        if (selectionStart != selectionEnd) {
409            content.delete(selectionStart, selectionEnd);
410            return true;
411        }
412        return false;
413    }
414
415    private boolean deleteLine(View view, Editable content) {
416        if (view instanceof TextView) {
417            final Layout layout = ((TextView) view).getLayout();
418            if (layout != null) {
419                final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
420                final int start = layout.getLineStart(line);
421                final int end = layout.getLineEnd(line);
422                if (end != start) {
423                    content.delete(start, end);
424                    return true;
425                }
426            }
427        }
428        return false;
429    }
430
431    static int makeTextContentType(Capitalize caps, boolean autoText) {
432        int contentType = InputType.TYPE_CLASS_TEXT;
433        switch (caps) {
434            case CHARACTERS:
435                contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
436                break;
437            case WORDS:
438                contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
439                break;
440            case SENTENCES:
441                contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
442                break;
443        }
444        if (autoText) {
445            contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
446        }
447        return contentType;
448    }
449
450    public boolean onKeyDown(View view, Editable content,
451                             int keyCode, KeyEvent event) {
452        boolean handled;
453        switch (keyCode) {
454            case KeyEvent.KEYCODE_DEL:
455                handled = backspace(view, content, keyCode, event);
456                break;
457            case KeyEvent.KEYCODE_FORWARD_DEL:
458                handled = forwardDelete(view, content, keyCode, event);
459                break;
460            default:
461                handled = false;
462                break;
463        }
464
465        if (handled) {
466            adjustMetaAfterKeypress(content);
467            return true;
468        }
469
470        return super.onKeyDown(view, content, keyCode, event);
471    }
472
473    /**
474     * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
475     * the event's text into the content.
476     */
477    public boolean onKeyOther(View view, Editable content, KeyEvent event) {
478        if (event.getAction() != KeyEvent.ACTION_MULTIPLE
479                || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
480            // Not something we are interested in.
481            return false;
482        }
483
484        int selectionStart = Selection.getSelectionStart(content);
485        int selectionEnd = Selection.getSelectionEnd(content);
486        if (selectionEnd < selectionStart) {
487            int temp = selectionEnd;
488            selectionEnd = selectionStart;
489            selectionStart = temp;
490        }
491
492        CharSequence text = event.getCharacters();
493        if (text == null) {
494            return false;
495        }
496
497        content.replace(selectionStart, selectionEnd, text);
498        return true;
499    }
500}
501