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