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