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 a ZWJ emoji.
133         final int STATE_BEFORE_ZWJ_EMOJI = 7;
134         // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
135         final int STATE_BEFORE_ZWJ = 8;
136         // The offset is immediately before a variation selector and a ZWJ that were seen before a
137         // ZWJ emoji.
138         final int STATE_BEFORE_VS_AND_ZWJ = 9;
139 
140         // The number of following RIS code points is odd.
141         final int STATE_ODD_NUMBERED_RIS = 10;
142         // The number of following RIS code points is even.
143         final int STATE_EVEN_NUMBERED_RIS = 11;
144 
145         // The state machine has been stopped.
146         final int STATE_FINISHED = 12;
147 
148         int deleteCharCount = 0;  // Char count to be deleted by backspace.
149         int lastSeenVSCharCount = 0;  // Char count of previous variation selector.
150 
151         int state = STATE_START;
152 
153         int tmpOffset = offset;
154         do {
155             final int codePoint = Character.codePointBefore(text, tmpOffset);
156             tmpOffset -= Character.charCount(codePoint);
157 
158             switch (state) {
159                 case STATE_START:
160                     deleteCharCount = Character.charCount(codePoint);
161                     if (codePoint == LINE_FEED) {
162                         state = STATE_LF;
163                     } else if (isVariationSelector(codePoint)) {
164                         state = STATE_BEFORE_VS;
165                     } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
166                         state = STATE_ODD_NUMBERED_RIS;
167                     } else if (Emoji.isEmojiModifier(codePoint)) {
168                         state = STATE_BEFORE_EMOJI_MODIFIER;
169                     } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
170                         state = STATE_BEFORE_KEYCAP;
171                     } else if (Emoji.isEmoji(codePoint)) {
172                         state = STATE_BEFORE_ZWJ_EMOJI;
173                     } else {
174                         state = STATE_FINISHED;
175                     }
176                     break;
177                 case STATE_LF:
178                     if (codePoint == CARRIAGE_RETURN) {
179                         ++deleteCharCount;
180                     }
181                     state = STATE_FINISHED;
182                 case STATE_ODD_NUMBERED_RIS:
183                     if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
184                         deleteCharCount += 2; /* Char count of RIS */
185                         state = STATE_EVEN_NUMBERED_RIS;
186                     } else {
187                         state = STATE_FINISHED;
188                     }
189                     break;
190                 case STATE_EVEN_NUMBERED_RIS:
191                     if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
192                         deleteCharCount -= 2; /* Char count of RIS */
193                         state = STATE_ODD_NUMBERED_RIS;
194                     } else {
195                         state = STATE_FINISHED;
196                     }
197                     break;
198                 case STATE_BEFORE_KEYCAP:
199                     if (isVariationSelector(codePoint)) {
200                         lastSeenVSCharCount = Character.charCount(codePoint);
201                         state = STATE_BEFORE_VS_AND_KEYCAP;
202                         break;
203                     }
204 
205                     if (Emoji.isKeycapBase(codePoint)) {
206                         deleteCharCount += Character.charCount(codePoint);
207                     }
208                     state = STATE_FINISHED;
209                     break;
210                 case STATE_BEFORE_VS_AND_KEYCAP:
211                     if (Emoji.isKeycapBase(codePoint)) {
212                         deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
213                     }
214                     state = STATE_FINISHED;
215                     break;
216                 case STATE_BEFORE_EMOJI_MODIFIER:
217                     if (isVariationSelector(codePoint)) {
218                         lastSeenVSCharCount = Character.charCount(codePoint);
219                         state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
220                         break;
221                     } else if (Emoji.isEmojiModifierBase(codePoint)) {
222                         deleteCharCount += Character.charCount(codePoint);
223                     }
224                     state = STATE_FINISHED;
225                     break;
226                 case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
227                     if (Emoji.isEmojiModifierBase(codePoint)) {
228                         deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
229                     }
230                     state = STATE_FINISHED;
231                     break;
232                 case STATE_BEFORE_VS:
233                     if (Emoji.isEmoji(codePoint)) {
234                         deleteCharCount += Character.charCount(codePoint);
235                         state = STATE_BEFORE_ZWJ_EMOJI;
236                         break;
237                     }
238 
239                     if (!isVariationSelector(codePoint) &&
240                             UCharacter.getCombiningClass(codePoint) == 0) {
241                         deleteCharCount += Character.charCount(codePoint);
242                     }
243                     state = STATE_FINISHED;
244                     break;
245                 case STATE_BEFORE_ZWJ_EMOJI:
246                     if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
247                         state = STATE_BEFORE_ZWJ;
248                     } else {
249                         state = STATE_FINISHED;
250                     }
251                     break;
252                 case STATE_BEFORE_ZWJ:
253                     if (Emoji.isEmoji(codePoint)) {
254                         deleteCharCount += Character.charCount(codePoint) + 1;  // +1 for ZWJ.
255                         state = STATE_BEFORE_ZWJ_EMOJI;
256                     } else if (isVariationSelector(codePoint)) {
257                         lastSeenVSCharCount = Character.charCount(codePoint);
258                         state = STATE_BEFORE_VS_AND_ZWJ;
259                     } else {
260                         state = STATE_FINISHED;
261                     }
262                     break;
263                 case STATE_BEFORE_VS_AND_ZWJ:
264                     if (Emoji.isEmoji(codePoint)) {
265                         // +1 for ZWJ.
266                         deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
267                         lastSeenVSCharCount = 0;
268                         state = STATE_BEFORE_ZWJ_EMOJI;
269                     } else {
270                         state = STATE_FINISHED;
271                     }
272                     break;
273                 default:
274                     throw new IllegalArgumentException("state " + state + " is unknown");
275             }
276         } while (tmpOffset > 0 && state != STATE_FINISHED);
277 
278         return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
279     }
280 
281     // Returns the end offset to be deleted by a forward delete key from the given offset.
getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint)282     private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
283         final int len = text.length();
284 
285         if (offset >= len - 1) {
286             return len;
287         }
288 
289         offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
290                 offset, Paint.CURSOR_AFTER);
291 
292         return adjustReplacementSpan(text, offset, false /* move to the end */);
293     }
294 
backspaceOrForwardDelete(View view, Editable content, int keyCode, KeyEvent event, boolean isForwardDelete)295     private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
296             KeyEvent event, boolean isForwardDelete) {
297         // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
298         if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
299                 & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
300             return false;
301         }
302 
303         // If there is a current selection, delete it.
304         if (deleteSelection(view, content)) {
305             return true;
306         }
307 
308         // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
309         boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
310         boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
311         boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
312 
313         if (isCtrlActive) {
314             if (isAltActive || isShiftActive) {
315                 // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
316                 return false;
317             }
318             return deleteUntilWordBoundary(view, content, isForwardDelete);
319         }
320 
321         // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
322         if (isAltActive && deleteLine(view, content)) {
323             return true;
324         }
325 
326         // Delete a character.
327         final int start = Selection.getSelectionEnd(content);
328         final int end;
329         if (isForwardDelete) {
330             final Paint paint;
331             if (view instanceof TextView) {
332                 paint = ((TextView)view).getPaint();
333             } else {
334                 synchronized (mLock) {
335                     if (sCachedPaint == null) {
336                         sCachedPaint = new Paint();
337                     }
338                     paint = sCachedPaint;
339                 }
340             }
341             end = getOffsetForForwardDeleteKey(content, start, paint);
342         } else {
343             end = getOffsetForBackspaceKey(content, start);
344         }
345         if (start != end) {
346             content.delete(Math.min(start, end), Math.max(start, end));
347             return true;
348         }
349         return false;
350     }
351 
deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete)352     private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
353         int currentCursorOffset = Selection.getSelectionStart(content);
354 
355         // If there is a selection, do nothing.
356         if (currentCursorOffset != Selection.getSelectionEnd(content)) {
357             return false;
358         }
359 
360         // Early exit if there is no contents to delete.
361         if ((!isForwardDelete && currentCursorOffset == 0) ||
362             (isForwardDelete && currentCursorOffset == content.length())) {
363             return false;
364         }
365 
366         WordIterator wordIterator = null;
367         if (view instanceof TextView) {
368             wordIterator = ((TextView)view).getWordIterator();
369         }
370 
371         if (wordIterator == null) {
372             // Default locale is used for WordIterator since the appropriate locale is not clear
373             // here.
374             // TODO: Use appropriate locale for WordIterator.
375             wordIterator = new WordIterator();
376         }
377 
378         int deleteFrom;
379         int deleteTo;
380 
381         if (isForwardDelete) {
382             deleteFrom = currentCursorOffset;
383             wordIterator.setCharSequence(content, deleteFrom, content.length());
384             deleteTo = wordIterator.following(currentCursorOffset);
385             if (deleteTo == BreakIterator.DONE) {
386                 deleteTo = content.length();
387             }
388         } else {
389             deleteTo = currentCursorOffset;
390             wordIterator.setCharSequence(content, 0, deleteTo);
391             deleteFrom = wordIterator.preceding(currentCursorOffset);
392             if (deleteFrom == BreakIterator.DONE) {
393                 deleteFrom = 0;
394             }
395         }
396         content.delete(deleteFrom, deleteTo);
397         return true;
398     }
399 
deleteSelection(View view, Editable content)400     private boolean deleteSelection(View view, Editable content) {
401         int selectionStart = Selection.getSelectionStart(content);
402         int selectionEnd = Selection.getSelectionEnd(content);
403         if (selectionEnd < selectionStart) {
404             int temp = selectionEnd;
405             selectionEnd = selectionStart;
406             selectionStart = temp;
407         }
408         if (selectionStart != selectionEnd) {
409             content.delete(selectionStart, selectionEnd);
410             return true;
411         }
412         return false;
413     }
414 
deleteLine(View view, Editable content)415     private boolean deleteLine(View view, Editable content) {
416         if (view instanceof TextView) {
417             final Layout layout = ((TextView) view).getLayout();
418             if (layout != null) {
419                 final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
420                 final int start = layout.getLineStart(line);
421                 final int end = layout.getLineEnd(line);
422                 if (end != start) {
423                     content.delete(start, end);
424                     return true;
425                 }
426             }
427         }
428         return false;
429     }
430 
makeTextContentType(Capitalize caps, boolean autoText)431     static int makeTextContentType(Capitalize caps, boolean autoText) {
432         int contentType = InputType.TYPE_CLASS_TEXT;
433         switch (caps) {
434             case CHARACTERS:
435                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
436                 break;
437             case WORDS:
438                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
439                 break;
440             case SENTENCES:
441                 contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
442                 break;
443         }
444         if (autoText) {
445             contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
446         }
447         return contentType;
448     }
449 
onKeyDown(View view, Editable content, int keyCode, KeyEvent event)450     public boolean onKeyDown(View view, Editable content,
451                              int keyCode, KeyEvent event) {
452         boolean handled;
453         switch (keyCode) {
454             case KeyEvent.KEYCODE_DEL:
455                 handled = backspace(view, content, keyCode, event);
456                 break;
457             case KeyEvent.KEYCODE_FORWARD_DEL:
458                 handled = forwardDelete(view, content, keyCode, event);
459                 break;
460             default:
461                 handled = false;
462                 break;
463         }
464 
465         if (handled) {
466             adjustMetaAfterKeypress(content);
467             return true;
468         }
469 
470         return super.onKeyDown(view, content, keyCode, event);
471     }
472 
473     /**
474      * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
475      * the event's text into the content.
476      */
onKeyOther(View view, Editable content, KeyEvent event)477     public boolean onKeyOther(View view, Editable content, KeyEvent event) {
478         if (event.getAction() != KeyEvent.ACTION_MULTIPLE
479                 || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
480             // Not something we are interested in.
481             return false;
482         }
483 
484         int selectionStart = Selection.getSelectionStart(content);
485         int selectionEnd = Selection.getSelectionEnd(content);
486         if (selectionEnd < selectionStart) {
487             int temp = selectionEnd;
488             selectionEnd = selectionStart;
489             selectionStart = temp;
490         }
491 
492         CharSequence text = event.getCharacters();
493         if (text == null) {
494             return false;
495         }
496 
497         content.replace(selectionStart, selectionEnd, text);
498         return true;
499     }
500 }
501