content: /*
   * Copyright (C) 2006 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.text.method;
  
  import android.graphics.Paint;
  import android.icu.lang.UCharacter;
  import android.icu.lang.UProperty;
  import android.text.Editable;
  import android.text.Emoji;
  import android.text.InputType;
  import android.text.Layout;
  import android.text.NoCopySpan;
  import android.text.Selection;
  import android.text.Spanned;
  import android.text.method.TextKeyListener.Capitalize;
  import android.text.style.ReplacementSpan;
  import android.view.KeyEvent;
  import android.view.View;
  import android.widget.TextView;
  
  import com.android.internal.annotations.GuardedBy;
  
  import java.text.BreakIterator;
  
  /**
   * Abstract base class for key listeners.
   *
   * Provides a basic foundation for entering and editing text.
   * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert
   * characters as keys are pressed.
   * <p></p>
   * As for all implementations of {@link KeyListener}, this class is only concerned
   * with hardware keyboards.  Software input methods have no obligation to trigger
   * the methods in this class.
   */
  public abstract class BaseKeyListener extends MetaKeyKeyListener
          implements KeyListener {
      /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
  
      private static final int LINE_FEED = 0x0A;
      private static final int CARRIAGE_RETURN = 0x0D;
  
      private final Object mLock = new Object();
  
      @GuardedBy("mLock")
      static Paint sCachedPaint = null;
  
      /**
       * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
       * a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
       * deletes the character before the cursor, if any; ALT+DEL deletes everything on
       * the line the cursor is on.
       *
       * @return true if anything was deleted; false otherwise.
       */
      public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
          return backspaceOrForwardDelete(view, content, keyCode, event, false);
      }
  
      /**
       * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL}
       * key in a {@link TextView}.  If there is a selection, deletes the selection; otherwise,
       * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on
       * the line the cursor is on.
       *
       * @return true if anything was deleted; false otherwise.
       */
      public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) {
          return backspaceOrForwardDelete(view, content, keyCode, event, true);
      }
  
      // Returns true if the given code point is a variation selector.
      private static boolean isVariationSelector(int codepoint) {
          return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR);
      }
  
      // Returns the offset of the replacement span edge if the offset is inside of the replacement
      // span.  Otherwise, does nothing and returns the input offset value.
      private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) {
          if (!(text instanceof Spanned)) {
              return offset;
          }
  
          ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class);
          for (int i = 0; i < spans.length; i++) {
              final int start = ((Spanned) text).getSpanStart(spans[i]);
              final int end = ((Spanned) text).getSpanEnd(spans[i]);
  
              if (start < offset && end > offset) {
                  offset = moveToStart ? start : end;
              }
          }
          return offset;
      }
  
      // Returns the start offset to be deleted by a backspace key from the given offset.
      private static int getOffsetForBackspaceKey(CharSequence text, int offset) {
          if (offset <= 1) {
              return 0;
          }
  
          // Initial state
          final int STATE_START = 0;
  
          // The offset is immediately before line feed.
          final int STATE_LF = 1;
  
          // The offset is immediately before a KEYCAP.
          final int STATE_BEFORE_KEYCAP = 2;
          // The offset is immediately before a variation selector and a KEYCAP.
          final int STATE_BEFORE_VS_AND_KEYCAP = 3;
  
          // The offset is immediately before an emoji modifier.
          final int STATE_BEFORE_EMOJI_MODIFIER = 4;
          // The offset is immediately before a variation selector and an emoji modifier.
          final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5;
  
          // The offset is immediately before a variation selector.
          final int STATE_BEFORE_VS = 6;
  
          // The offset is immediately before an emoji.
          final int STATE_BEFORE_EMOJI = 7;
          // The offset is immediately before a ZWJ that were seen before a ZWJ emoji.
          final int STATE_BEFORE_ZWJ = 8;
          // The offset is immediately before a variation selector and a ZWJ that were seen before a
          // ZWJ emoji.
          final int STATE_BEFORE_VS_AND_ZWJ = 9;
  
          // The number of following RIS code points is odd.
          final int STATE_ODD_NUMBERED_RIS = 10;
          // The number of following RIS code points is even.
          final int STATE_EVEN_NUMBERED_RIS = 11;
  
          // The offset is in emoji tag sequence.
          final int STATE_IN_TAG_SEQUENCE = 12;
  
          // The state machine has been stopped.
          final int STATE_FINISHED = 13;
  
          int deleteCharCount = 0;  // Char count to be deleted by backspace.
          int lastSeenVSCharCount = 0;  // Char count of previous variation selector.
  
          int state = STATE_START;
  
          int tmpOffset = offset;
          do {
              final int codePoint = Character.codePointBefore(text, tmpOffset);
              tmpOffset -= Character.charCount(codePoint);
  
              switch (state) {
                  case STATE_START:
                      deleteCharCount = Character.charCount(codePoint);
                      if (codePoint == LINE_FEED) {
                          state = STATE_LF;
                      } else if (isVariationSelector(codePoint)) {
                          state = STATE_BEFORE_VS;
                      } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
                          state = STATE_ODD_NUMBERED_RIS;
                      } else if (Emoji.isEmojiModifier(codePoint)) {
                          state = STATE_BEFORE_EMOJI_MODIFIER;
                      } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) {
                          state = STATE_BEFORE_KEYCAP;
                      } else if (Emoji.isEmoji(codePoint)) {
                          state = STATE_BEFORE_EMOJI;
                      } else if (codePoint == Emoji.CANCEL_TAG) {
                          state = STATE_IN_TAG_SEQUENCE;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_LF:
                      if (codePoint == CARRIAGE_RETURN) {
                          ++deleteCharCount;
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_ODD_NUMBERED_RIS:
                      if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
                          deleteCharCount += 2; /* Char count of RIS */
                          state = STATE_EVEN_NUMBERED_RIS;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_EVEN_NUMBERED_RIS:
                      if (Emoji.isRegionalIndicatorSymbol(codePoint)) {
                          deleteCharCount -= 2; /* Char count of RIS */
                          state = STATE_ODD_NUMBERED_RIS;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_BEFORE_KEYCAP:
                      if (isVariationSelector(codePoint)) {
                          lastSeenVSCharCount = Character.charCount(codePoint);
                          state = STATE_BEFORE_VS_AND_KEYCAP;
                          break;
                      }
  
                      if (Emoji.isKeycapBase(codePoint)) {
                          deleteCharCount += Character.charCount(codePoint);
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_BEFORE_VS_AND_KEYCAP:
                      if (Emoji.isKeycapBase(codePoint)) {
                          deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_BEFORE_EMOJI_MODIFIER:
                      if (isVariationSelector(codePoint)) {
                          lastSeenVSCharCount = Character.charCount(codePoint);
                          state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER;
                          break;
                      } else if (Emoji.isEmojiModifierBase(codePoint)) {
                          deleteCharCount += Character.charCount(codePoint);
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_BEFORE_VS_AND_EMOJI_MODIFIER:
                      if (Emoji.isEmojiModifierBase(codePoint)) {
                          deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint);
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_BEFORE_VS:
                      if (Emoji.isEmoji(codePoint)) {
                          deleteCharCount += Character.charCount(codePoint);
                          state = STATE_BEFORE_EMOJI;
                          break;
                      }
  
                      if (!isVariationSelector(codePoint) &&
                              UCharacter.getCombiningClass(codePoint) == 0) {
                          deleteCharCount += Character.charCount(codePoint);
                      }
                      state = STATE_FINISHED;
                      break;
                  case STATE_BEFORE_EMOJI:
                      if (codePoint == Emoji.ZERO_WIDTH_JOINER) {
                          state = STATE_BEFORE_ZWJ;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_BEFORE_ZWJ:
                      if (Emoji.isEmoji(codePoint)) {
                          deleteCharCount += Character.charCount(codePoint) + 1;  // +1 for ZWJ.
                          state = Emoji.isEmojiModifier(codePoint) ?
                                  STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI;
                      } else if (isVariationSelector(codePoint)) {
                          lastSeenVSCharCount = Character.charCount(codePoint);
                          state = STATE_BEFORE_VS_AND_ZWJ;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_BEFORE_VS_AND_ZWJ:
                      if (Emoji.isEmoji(codePoint)) {
                          // +1 for ZWJ.
                          deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint);
                          lastSeenVSCharCount = 0;
                          state = STATE_BEFORE_EMOJI;
                      } else {
                          state = STATE_FINISHED;
                      }
                      break;
                  case STATE_IN_TAG_SEQUENCE:
                      if (Emoji.isTagSpecChar(codePoint)) {
                          deleteCharCount += 2; /* Char count of emoji tag spec character. */
                          // Keep the same state.
                      } else if (Emoji.isEmoji(codePoint)) {
                          deleteCharCount += Character.charCount(codePoint);
                          state = STATE_FINISHED;
                      } else {
                          // Couldn't find tag_base character. Delete the last tag_term character.
                          deleteCharCount = 2;  // for U+E007F
                          state = STATE_FINISHED;
                      }
                      // TODO: Need handle emoji variation selectors. Issue 35224297
                      break;
                  default:
                      throw new IllegalArgumentException("state " + state + " is unknown");
              }
          } while (tmpOffset > 0 && state != STATE_FINISHED);
  
          return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */);
      }
  
      // Returns the end offset to be deleted by a forward delete key from the given offset.
      private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
          final int len = text.length();
  
          if (offset >= len - 1) {
              return len;
          }
  
          offset = paint.getTextRunCursor(text, offset, len, false /* LTR, not used */,
                  offset, Paint.CURSOR_AFTER);
  
          return adjustReplacementSpan(text, offset, false /* move to the end */);
      }
  
      private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode,
              KeyEvent event, boolean isForwardDelete) {
          // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL.
          if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState()
                  & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) {
              return false;
          }
  
          // If there is a current selection, delete it.
          if (deleteSelection(view, content)) {
              return true;
          }
  
          // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead.
          boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0);
          boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1);
          boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1);
  
          if (isCtrlActive) {
              if (isAltActive || isShiftActive) {
                  // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters.
                  return false;
              }
              return deleteUntilWordBoundary(view, content, isForwardDelete);
          }
  
          // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible.
          if (isAltActive && deleteLine(view, content)) {
              return true;
          }
  
          // Delete a character.
          final int start = Selection.getSelectionEnd(content);
          final int end;
          if (isForwardDelete) {
              final Paint paint;
              if (view instanceof TextView) {
                  paint = ((TextView)view).getPaint();
              } else {
                  synchronized (mLock) {
                      if (sCachedPaint == null) {
                          sCachedPaint = new Paint();
                      }
                      paint = sCachedPaint;
                  }
              }
              end = getOffsetForForwardDeleteKey(content, start, paint);
          } else {
              end = getOffsetForBackspaceKey(content, start);
          }
          if (start != end) {
              content.delete(Math.min(start, end), Math.max(start, end));
              return true;
          }
          return false;
      }
  
      private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) {
          int currentCursorOffset = Selection.getSelectionStart(content);
  
          // If there is a selection, do nothing.
          if (currentCursorOffset != Selection.getSelectionEnd(content)) {
              return false;
          }
  
          // Early exit if there is no contents to delete.
          if ((!isForwardDelete && currentCursorOffset == 0) ||
              (isForwardDelete && currentCursorOffset == content.length())) {
              return false;
          }
  
          WordIterator wordIterator = null;
          if (view instanceof TextView) {
              wordIterator = ((TextView)view).getWordIterator();
          }
  
          if (wordIterator == null) {
              // Default locale is used for WordIterator since the appropriate locale is not clear
              // here.
              // TODO: Use appropriate locale for WordIterator.
              wordIterator = new WordIterator();
          }
  
          int deleteFrom;
          int deleteTo;
  
          if (isForwardDelete) {
              deleteFrom = currentCursorOffset;
              wordIterator.setCharSequence(content, deleteFrom, content.length());
              deleteTo = wordIterator.following(currentCursorOffset);
              if (deleteTo == BreakIterator.DONE) {
                  deleteTo = content.length();
              }
          } else {
              deleteTo = currentCursorOffset;
              wordIterator.setCharSequence(content, 0, deleteTo);
              deleteFrom = wordIterator.preceding(currentCursorOffset);
              if (deleteFrom == BreakIterator.DONE) {
                  deleteFrom = 0;
              }
          }
          content.delete(deleteFrom, deleteTo);
          return true;
      }
  
      private boolean deleteSelection(View view, Editable content) {
          int selectionStart = Selection.getSelectionStart(content);
          int selectionEnd = Selection.getSelectionEnd(content);
          if (selectionEnd < selectionStart) {
              int temp = selectionEnd;
              selectionEnd = selectionStart;
              selectionStart = temp;
          }
          if (selectionStart != selectionEnd) {
              content.delete(selectionStart, selectionEnd);
              return true;
          }
          return false;
      }
  
      private boolean deleteLine(View view, Editable content) {
          if (view instanceof TextView) {
              final Layout layout = ((TextView) view).getLayout();
              if (layout != null) {
                  final int line = layout.getLineForOffset(Selection.getSelectionStart(content));
                  final int start = layout.getLineStart(line);
                  final int end = layout.getLineEnd(line);
                  if (end != start) {
                      content.delete(start, end);
                      return true;
                  }
              }
          }
          return false;
      }
  
      static int makeTextContentType(Capitalize caps, boolean autoText) {
          int contentType = InputType.TYPE_CLASS_TEXT;
          switch (caps) {
              case CHARACTERS:
                  contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS;
                  break;
              case WORDS:
                  contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS;
                  break;
              case SENTENCES:
                  contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
                  break;
          }
          if (autoText) {
              contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
          }
          return contentType;
      }
  
      public boolean onKeyDown(View view, Editable content,
                               int keyCode, KeyEvent event) {
          boolean handled;
          switch (keyCode) {
              case KeyEvent.KEYCODE_DEL:
                  handled = backspace(view, content, keyCode, event);
                  break;
              case KeyEvent.KEYCODE_FORWARD_DEL:
                  handled = forwardDelete(view, content, keyCode, event);
                  break;
              default:
                  handled = false;
                  break;
          }
  
          if (handled) {
              adjustMetaAfterKeypress(content);
              return true;
          }
  
          return super.onKeyDown(view, content, keyCode, event);
      }
  
      /**
       * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting
       * the event's text into the content.
       */
      public boolean onKeyOther(View view, Editable content, KeyEvent event) {
          if (event.getAction() != KeyEvent.ACTION_MULTIPLE
                  || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) {
              // Not something we are interested in.
              return false;
          }
  
          int selectionStart = Selection.getSelectionStart(content);
          int selectionEnd = Selection.getSelectionEnd(content);
          if (selectionEnd < selectionStart) {
              int temp = selectionEnd;
              selectionEnd = selectionStart;
              selectionStart = temp;
          }
  
          CharSequence text = event.getCharacters();
          if (text == null) {
              return false;
          }
  
          content.replace(selectionStart, selectionEnd, text);
          return true;
      }
  }
  