• 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.view.KeyEvent;
23 import android.view.View;
24 import android.text.*;
25 import android.text.method.TextKeyListener.Capitalize;
26 import android.text.style.ReplacementSpan;
27 import android.widget.TextView;
28 
29 import com.android.internal.annotations.GuardedBy;
30 
31 import java.text.BreakIterator;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import 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  */
47 public 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      */
backspace(View view, Editable content, int keyCode, KeyEvent event)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      */
forwardDelete(View view, Editable content, int keyCode, KeyEvent event)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.
isVariationSelector(int codepoint)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.
adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart)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.
getOffsetForBackspaceKey(CharSequence text, int 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 an emoji.
133         final int STATE_BEFORE_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_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_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_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 = Emoji.isEmojiModifier(codePoint) ?
256                                 STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI;
257                     } else if (isVariationSelector(codePoint)) {
258                         lastSeenVSCharCount = Character.charCount(codePoint);
259                         state = STATE_BEFORE_VS_AND_ZWJ;
260                     } else {
261                         state = STATE_FINISHED;
262                     }
263                     break;
264                 case STATE_BEFORE_VS_AND_ZWJ:
265                     if (Emoji.isEmoji(codePoint)) {
266                         // +1 for ZWJ.
267                         deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
268                         lastSeenVSCharCount = 0;
269                         state = STATE_BEFORE_EMOJI;
270                     } else {
271                         state = STATE_FINISHED;
272                     }
273                     break;
274                 default:
275                     throw new IllegalArgumentException("state " + state + " is unknown");
276             }
277         } while (tmpOffset > 0 && state != STATE_FINISHED);
278 
279         return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
280     }
281 
282     // Returns the end offset to be deleted by a forward delete key from the given offset.
getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint)283     private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
284         final int len = text.length();
285 
286         if (offset >= len - 1) {
287             return len;
288         }
289 
290         offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
291                 offset, Paint.CURSOR_AFTER);
292 
293         return adjustReplacementSpan(text, offset, false /* move to the end */);
294     }
295 
backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete)296     private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
297             KeyEvent event, boolean isForwardDelete) {
298         // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
299         if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
300                 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
301             return false;
302         }
303 
304         // If there is a current selection, delete it.
305         if (deleteSelection(view, content)) {
306             return true;
307         }
308 
309         // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
310         boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
311         boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
312         boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
313 
314         if (isCtrlActive) {
315             if (isAltActive || isShiftActive) {
316                 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
317                 return false;
318             }
319             return deleteUntilWordBoundary(view, content, isForwardDelete);
320         }
321 
322         // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
323         if (isAltActive && deleteLine(view, content)) {
324             return true;
325         }
326 
327         // Delete a character.
328         final int start = Selection.getSelectionEnd(content);
329         final int end;
330         if (isForwardDelete) {
331             final Paint paint;
332             if (view instanceof TextView) {
333                 paint = ((TextView)view).getPaint();
334             } else {
335                 synchronized (mLock) {
336                     if (sCachedPaint == null) {
337                         sCachedPaint = new Paint();
338                     }
339                     paint = sCachedPaint;
340                 }
341             }
342             end = getOffsetForForwardDeleteKey(content, start, paint);
343         } else {
344             end = getOffsetForBackspaceKey(content, start);
345         }
346         if (start != end) {
347             content.delete(Math.min(start, end), Math.max(start, end));
348             return true;
349         }
350         return false;
351     }
352 
deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete)353     private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
354         int currentCursorOffset = Selection.getSelectionStart(content);
355 
356         // If there is a selection, do nothing.
357         if (currentCursorOffset != Selection.getSelectionEnd(content)) {
358             return false;
359         }
360 
361         // Early exit if there is no contents to delete.
362         if ((!isForwardDelete && currentCursorOffset == 0) ||
363             (isForwardDelete && currentCursorOffset == content.length())) {
364             return false;
365         }
366 
367         WordIterator wordIterator = null;
368         if (view instanceof TextView) {
369             wordIterator = ((TextView)view).getWordIterator();
370         }
371 
372         if (wordIterator == null) {
373             // Default locale is used for WordIterator since the appropriate locale is not clear
374             // here.
375             // TODO: Use appropriate locale for WordIterator.
376             wordIterator = new WordIterator();
377         }
378 
379         int deleteFrom;
380         int deleteTo;
381 
382         if (isForwardDelete) {
383             deleteFrom = currentCursorOffset;
384             wordIterator.setCharSequence(content, deleteFrom, content.length());
385             deleteTo = wordIterator.following(currentCursorOffset);
386             if (deleteTo == BreakIterator.DONE) {
387                 deleteTo = content.length();
388             }
389         } else {
390             deleteTo = currentCursorOffset;
391             wordIterator.setCharSequence(content, 0, deleteTo);
392             deleteFrom = wordIterator.preceding(currentCursorOffset);
393             if (deleteFrom == BreakIterator.DONE) {
394                 deleteFrom = 0;
395             }
396         }
397         content.delete(deleteFrom, deleteTo);
398         return true;
399     }
400 
deleteSelection(View view, Editable content)401     private boolean deleteSelection(View view, Editable content) {
402         int selectionStart = Selection.getSelectionStart(content);
403         int selectionEnd = Selection.getSelectionEnd(content);
404         if (selectionEnd < selectionStart) {
405             int temp = selectionEnd;
406             selectionEnd = selectionStart;
407             selectionStart = temp;
408         }
409         if (selectionStart != selectionEnd) {
410             content.delete(selectionStart, selectionEnd);
411             return true;
412         }
413         return false;
414     }
415 
deleteLine(View view, Editable content)416     private boolean deleteLine(View view, Editable content) {
417         if (view instanceof TextView) {
418             final Layout layout = ((TextView) view).getLayout();
419             if (layout != null) {
420                 final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
421                 final int start = layout.getLineStart(line);
422                 final int end = layout.getLineEnd(line);
423                 if (end != start) {
424                     content.delete(start, end);
425                     return true;
426                 }
427             }
428         }
429         return false;
430     }
431 
makeTextContentType(Capitalize caps, boolean autoText)432     static int makeTextContentType(Capitalize caps, boolean autoText) {
433         int contentType = InputType.TYPE_CLASS_TEXT;
434         switch (caps) {
435             case CHARACTERS:
436                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
437                 break;
438             case WORDS:
439                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
440                 break;
441             case SENTENCES:
442                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
443                 break;
444         }
445         if (autoText) {
446             contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
447         }
448         return contentType;
449     }
450 
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)451     public boolean onKeyDown(View view, Editable content,
452                              int keyCode, KeyEvent event) {
453         boolean handled;
454         switch (keyCode) {
455             case KeyEvent.KEYCODE_DEL:
456                 handled = backspace(view, content, keyCode, event);
457                 break;
458             case KeyEvent.KEYCODE_FORWARD_DEL:
459                 handled = forwardDelete(view, content, keyCode, event);
460                 break;
461             default:
462                 handled = false;
463                 break;
464         }
465 
466         if (handled) {
467             adjustMetaAfterKeypress(content);
468             return true;
469         }
470 
471         return super.onKeyDown(view, content, keyCode, event);
472     }
473 
474     /**
475      * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
476      * the event's text into the content.
477      */
onKeyOther(View view, Editable content, KeyEvent event)478     public boolean onKeyOther(View view, Editable content, KeyEvent event) {
479         if (event.getAction() != KeyEvent.ACTION_MULTIPLE
480                 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
481             // Not something we are interested in.
482             return false;
483         }
484 
485         int selectionStart = Selection.getSelectionStart(content);
486         int selectionEnd = Selection.getSelectionEnd(content);
487         if (selectionEnd < selectionStart) {
488             int temp = selectionEnd;
489             selectionEnd = selectionStart;
490             selectionStart = temp;
491         }
492 
493         CharSequence text = event.getCharacters();
494         if (text == null) {
495             return false;
496         }
497 
498         content.replace(selectionStart, selectionEnd, text);
499         return true;
500     }
501 }
502