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