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 
17 package android.text.method;
18 
19 import android.graphics.Paint;
20 import android.icu.lang.UCharacter;
21 import android.icu.lang.UProperty;
22 import android.text.Editable;
23 import android.text.Emoji;
24 import android.text.InputType;
25 import android.text.Layout;
26 import android.text.NoCopySpan;
27 import android.text.Selection;
28 import android.text.Spanned;
29 import android.text.method.TextKeyListener.Capitalize;
30 import android.text.style.ReplacementSpan;
31 import android.view.KeyEvent;
32 import android.view.View;
33 import android.widget.TextView;
34 
35 import com.android.internal.annotations.GuardedBy;
36 
37 import 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  */
50 public 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      */
backspace(View view, Editable content, int keyCode, KeyEvent event)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      */
forwardDelete(View view, Editable content, int keyCode, KeyEvent event)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.
isVariationSelector(int codepoint)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.
adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart)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.
getOffsetForBackspaceKey(CharSequence text, int 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                         state = STATE_BEFORE_EMOJI;
233                         break;
234                     }
235                     state = STATE_FINISHED;
236                     break;
237                 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
238                     if (Emoji.isEmojiModifierBase(codePoint)) {
239                         deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
240                     }
241                     state = STATE_FINISHED;
242                     break;
243                 case STATE_BEFORE_VS:
244                     if (Emoji.isEmoji(codePoint)) {
245                         deleteCharCount += Character.charCount(codePoint);
246                         state = STATE_BEFORE_EMOJI;
247                         break;
248                     }
249 
250                     if (!isVariationSelector(codePoint) &&
251                             UCharacter.getCombiningClass(codePoint) == 0) {
252                         deleteCharCount += Character.charCount(codePoint);
253                     }
254                     state = STATE_FINISHED;
255                     break;
256                 case STATE_BEFORE_EMOJI:
257                     if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
258                         state = STATE_BEFORE_ZWJ;
259                     } else {
260                         state = STATE_FINISHED;
261                     }
262                     break;
263                 case STATE_BEFORE_ZWJ:
264                     if (Emoji.isEmoji(codePoint)) {
265                         deleteCharCount += Character.charCount(codePoint) + 1;  // +1 for ZWJ.
266                         state = Emoji.isEmojiModifier(codePoint) ?
267                                 STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI;
268                     } else if (isVariationSelector(codePoint)) {
269                         lastSeenVSCharCount = Character.charCount(codePoint);
270                         state = STATE_BEFORE_VS_AND_ZWJ;
271                     } else {
272                         state = STATE_FINISHED;
273                     }
274                     break;
275                 case STATE_BEFORE_VS_AND_ZWJ:
276                     if (Emoji.isEmoji(codePoint)) {
277                         // +1 for ZWJ.
278                         deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
279                         lastSeenVSCharCount = 0;
280                         state = STATE_BEFORE_EMOJI;
281                     } else {
282                         state = STATE_FINISHED;
283                     }
284                     break;
285                 case STATE_IN_TAG_SEQUENCE:
286                     if (Emoji.isTagSpecChar(codePoint)) {
287                         deleteCharCount += 2; /* Char count of emoji tag spec character. */
288                         // Keep the same state.
289                     } else if (Emoji.isEmoji(codePoint)) {
290                         deleteCharCount += Character.charCount(codePoint);
291                         state = STATE_FINISHED;
292                     } else {
293                         // Couldn't find tag_base character. Delete the last tag_term character.
294                         deleteCharCount = 2;  // for U+E007F
295                         state = STATE_FINISHED;
296                     }
297                     // TODO: Need handle emoji variation selectors. Issue 35224297
298                     break;
299                 default:
300                     throw new IllegalArgumentException("state " + state + " is unknown");
301             }
302         } while (tmpOffset > 0 && state != STATE_FINISHED);
303 
304         return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
305     }
306 
307     // Returns the end offset to be deleted by a forward delete key from the given offset.
getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint)308     private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
309         final int len = text.length();
310 
311         if (offset >= len - 1) {
312             return len;
313         }
314 
315         offset = paint.getTextRunCursor(text, offset, len, false /* LTR, not used */,
316                 offset, Paint.CURSOR_AFTER);
317 
318         return adjustReplacementSpan(text, offset, false /* move to the end */);
319     }
320 
backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete)321     private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
322             KeyEvent event, boolean isForwardDelete) {
323         // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
324         if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
325                 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
326             return false;
327         }
328 
329         // If there is a current selection, delete it.
330         if (deleteSelection(view, content)) {
331             return true;
332         }
333 
334         // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
335         boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
336         boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
337         boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
338 
339         if (isCtrlActive) {
340             if (isAltActive || isShiftActive) {
341                 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
342                 return false;
343             }
344             return deleteUntilWordBoundary(view, content, isForwardDelete);
345         }
346 
347         // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
348         if (isAltActive && deleteLineFromCursor(view, content, isForwardDelete)) {
349             return true;
350         }
351 
352         // Delete a character.
353         final int start = Selection.getSelectionEnd(content);
354         final int end;
355         if (isForwardDelete) {
356             final Paint paint;
357             if (view instanceof TextView) {
358                 paint = ((TextView)view).getPaint();
359             } else {
360                 synchronized (mLock) {
361                     if (sCachedPaint == null) {
362                         sCachedPaint = new Paint();
363                     }
364                     paint = sCachedPaint;
365                 }
366             }
367             end = getOffsetForForwardDeleteKey(content, start, paint);
368         } else {
369             end = getOffsetForBackspaceKey(content, start);
370         }
371         if (start != end) {
372             content.delete(Math.min(start, end), Math.max(start, end));
373             return true;
374         }
375         return false;
376     }
377 
deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete)378     private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
379         int currentCursorOffset = Selection.getSelectionStart(content);
380 
381         // If there is a selection, do nothing.
382         if (currentCursorOffset != Selection.getSelectionEnd(content)) {
383             return false;
384         }
385 
386         // Early exit if there is no contents to delete.
387         if ((!isForwardDelete && currentCursorOffset == 0) ||
388             (isForwardDelete && currentCursorOffset == content.length())) {
389             return false;
390         }
391 
392         WordIterator wordIterator = null;
393         if (view instanceof TextView) {
394             wordIterator = ((TextView)view).getWordIterator();
395         }
396 
397         if (wordIterator == null) {
398             // Default locale is used for WordIterator since the appropriate locale is not clear
399             // here.
400             // TODO: Use appropriate locale for WordIterator.
401             wordIterator = new WordIterator();
402         }
403 
404         int deleteFrom;
405         int deleteTo;
406 
407         if (isForwardDelete) {
408             deleteFrom = currentCursorOffset;
409             wordIterator.setCharSequence(content, deleteFrom, content.length());
410             deleteTo = wordIterator.following(currentCursorOffset);
411             if (deleteTo == BreakIterator.DONE) {
412                 deleteTo = content.length();
413             }
414         } else {
415             deleteTo = currentCursorOffset;
416             wordIterator.setCharSequence(content, 0, deleteTo);
417             deleteFrom = wordIterator.preceding(currentCursorOffset);
418             if (deleteFrom == BreakIterator.DONE) {
419                 deleteFrom = 0;
420             }
421         }
422         content.delete(deleteFrom, deleteTo);
423         return true;
424     }
425 
deleteSelection(View view, Editable content)426     private boolean deleteSelection(View view, Editable content) {
427         int selectionStart = Selection.getSelectionStart(content);
428         int selectionEnd = Selection.getSelectionEnd(content);
429         if (selectionEnd < selectionStart) {
430             int temp = selectionEnd;
431             selectionEnd = selectionStart;
432             selectionStart = temp;
433         }
434         if (selectionStart != selectionEnd) {
435             content.delete(selectionStart, selectionEnd);
436             return true;
437         }
438         return false;
439     }
440 
deleteLineFromCursor(View view, Editable content, boolean forward)441     private boolean deleteLineFromCursor(View view, Editable content, boolean forward) {
442         if (view instanceof TextView) {
443             final int selectionStart = Selection.getSelectionStart(content);
444             final int selectionEnd = Selection.getSelectionEnd(content);
445             final int selectionMin;
446             final int selectionMax;
447             if (selectionStart < selectionEnd) {
448                 selectionMin = selectionStart;
449                 selectionMax = selectionEnd;
450             } else {
451                 selectionMin = selectionEnd;
452                 selectionMax = selectionStart;
453             }
454 
455             final TextView textView = (TextView) view;
456             final Layout layout = textView.getLayout();
457             if (layout != null && !textView.isOffsetMappingAvailable()) {
458                 final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
459                 final int start = layout.getLineStart(line);
460                 final int end = layout.getLineEnd(line);
461 
462                 if (forward) {
463                     content.delete(selectionMin, end);
464                 } else {
465                     content.delete(start, selectionMax);
466                 }
467 
468                 return true;
469             }
470         }
471         return false;
472     }
473 
makeTextContentType(Capitalize caps, boolean autoText)474     static int makeTextContentType(Capitalize caps, boolean autoText) {
475         int contentType = InputType.TYPE_CLASS_TEXT;
476         switch (caps) {
477             case CHARACTERS:
478                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
479                 break;
480             case WORDS:
481                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
482                 break;
483             case SENTENCES:
484                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
485                 break;
486         }
487         if (autoText) {
488             contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
489         }
490         return contentType;
491     }
492 
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)493     public boolean onKeyDown(View view, Editable content,
494                              int keyCode, KeyEvent event) {
495         boolean handled;
496         switch (keyCode) {
497             case KeyEvent.KEYCODE_DEL:
498                 handled = backspace(view, content, keyCode, event);
499                 break;
500             case KeyEvent.KEYCODE_FORWARD_DEL:
501                 handled = forwardDelete(view, content, keyCode, event);
502                 break;
503             default:
504                 handled = false;
505                 break;
506         }
507 
508         if (handled) {
509             adjustMetaAfterKeypress(content);
510             return true;
511         }
512 
513         return super.onKeyDown(view, content, keyCode, event);
514     }
515 
516     /**
517      * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
518      * the event's text into the content.
519      */
onKeyOther(View view, Editable content, KeyEvent event)520     public boolean onKeyOther(View view, Editable content, KeyEvent event) {
521         if (event.getAction() != KeyEvent.ACTION_MULTIPLE
522                 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
523             // Not something we are interested in.
524             return false;
525         }
526 
527         int selectionStart = Selection.getSelectionStart(content);
528         int selectionEnd = Selection.getSelectionEnd(content);
529         if (selectionEnd < selectionStart) {
530             int temp = selectionEnd;
531             selectionEnd = selectionStart;
532             selectionStart = temp;
533         }
534 
535         CharSequence text = event.getCharacters();
536         if (text == null) {
537             return false;
538         }
539 
540         content.replace(selectionStart, selectionEnd, text);
541         return true;
542     }
543 }
544