content: /*
   * Copyright (C) 2008-2009 Google Inc.
   *
   * 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.inputmethodservice;
  
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.content.res.TypedArray;
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Paint.Align;
  import android.graphics.PorterDuff;
  import android.graphics.Rect;
  import android.graphics.Typeface;
  import android.graphics.drawable.Drawable;
  import android.inputmethodservice.Keyboard.Key;
  import android.media.AudioManager;
  import android.os.Handler;
  import android.os.Message;
  import android.util.AttributeSet;
  import android.util.TypedValue;
  import android.view.GestureDetector;
  import android.view.Gravity;
  import android.view.LayoutInflater;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewConfiguration;
  import android.view.ViewGroup.LayoutParams;
  import android.view.accessibility.AccessibilityEvent;
  import android.view.accessibility.AccessibilityManager;
  import android.widget.PopupWindow;
  import android.widget.TextView;
  
  import com.android.internal.R;
  
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and
   * detecting key presses and touch movements.
   *
   * @attr ref android.R.styleable#KeyboardView_keyBackground
   * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
   * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
   * @attr ref android.R.styleable#KeyboardView_keyPreviewHeight
   * @attr ref android.R.styleable#KeyboardView_labelTextSize
   * @attr ref android.R.styleable#KeyboardView_keyTextSize
   * @attr ref android.R.styleable#KeyboardView_keyTextColor
   * @attr ref android.R.styleable#KeyboardView_verticalCorrection
   * @attr ref android.R.styleable#KeyboardView_popupLayout
   *
   * @deprecated This class is deprecated because this is just a convenient UI widget class that
   *             application developers can re-implement on top of existing public APIs.  If you have
   *             already depended on this class, consider copying the implementation from AOSP into
   *             your project or re-implementing a similar widget by yourselves
   */
  @Deprecated
  public class KeyboardView extends View implements View.OnClickListener {
  
      /**
       * Listener for virtual keyboard events.
       */
      public interface OnKeyboardActionListener {
  
          /**
           * Called when the user presses a key. This is sent before the {@link #onKey} is called.
           * For keys that repeat, this is only called once.
           * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
           * key, the value will be zero.
           */
          void onPress(int primaryCode);
  
          /**
           * Called when the user releases a key. This is sent after the {@link #onKey} is called.
           * For keys that repeat, this is only called once.
           * @param primaryCode the code of the key that was released
           */
          void onRelease(int primaryCode);
  
          /**
           * Send a key press to the listener.
           * @param primaryCode this is the key that was pressed
           * @param keyCodes the codes for all the possible alternative keys
           * with the primary code being the first. If the primary key code is
           * a single character such as an alphabet or number or symbol, the alternatives
           * will include other characters that may be on the same key or adjacent keys.
           * These codes are useful to correct for accidental presses of a key adjacent to
           * the intended key.
           */
          void onKey(int primaryCode, int[] keyCodes);
  
          /**
           * Sends a sequence of characters to the listener.
           * @param text the sequence of characters to be displayed.
           */
          void onText(CharSequence text);
  
          /**
           * Called when the user quickly moves the finger from right to left.
           */
          void swipeLeft();
  
          /**
           * Called when the user quickly moves the finger from left to right.
           */
          void swipeRight();
  
          /**
           * Called when the user quickly moves the finger from up to down.
           */
          void swipeDown();
  
          /**
           * Called when the user quickly moves the finger from down to up.
           */
          void swipeUp();
      }
  
      private static final boolean DEBUG = false;
      private static final int NOT_A_KEY = -1;
      private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
      private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
  
      private Keyboard mKeyboard;
      private int mCurrentKeyIndex = NOT_A_KEY;
      @UnsupportedAppUsage
      private int mLabelTextSize;
      private int mKeyTextSize;
      private int mKeyTextColor;
      private float mShadowRadius;
      private int mShadowColor;
      private float mBackgroundDimAmount;
  
      @UnsupportedAppUsage
      private TextView mPreviewText;
      private PopupWindow mPreviewPopup;
      private int mPreviewTextSizeLarge;
      private int mPreviewOffset;
      private int mPreviewHeight;
      // Working variable
      private final int[] mCoordinates = new int[2];
  
      private PopupWindow mPopupKeyboard;
      private View mMiniKeyboardContainer;
      private KeyboardView mMiniKeyboard;
      private boolean mMiniKeyboardOnScreen;
      private View mPopupParent;
      private int mMiniKeyboardOffsetX;
      private int mMiniKeyboardOffsetY;
      private Map<Key,View> mMiniKeyboardCache;
      private Key[] mKeys;
  
      /** Listener for {@link OnKeyboardActionListener}. */
      private OnKeyboardActionListener mKeyboardActionListener;
  
      private static final int MSG_SHOW_PREVIEW = 1;
      private static final int MSG_REMOVE_PREVIEW = 2;
      private static final int MSG_REPEAT = 3;
      private static final int MSG_LONGPRESS = 4;
  
      private static final int DELAY_BEFORE_PREVIEW = 0;
      private static final int DELAY_AFTER_PREVIEW = 70;
      private static final int DEBOUNCE_TIME = 70;
  
      private int mVerticalCorrection;
      private int mProximityThreshold;
  
      private boolean mPreviewCentered = false;
      private boolean mShowPreview = true;
      private boolean mShowTouchPoints = true;
      private int mPopupPreviewX;
      private int mPopupPreviewY;
  
      private int mLastX;
      private int mLastY;
      private int mStartX;
      private int mStartY;
  
      private boolean mProximityCorrectOn;
  
      private Paint mPaint;
      private Rect mPadding;
  
      private long mDownTime;
      private long mLastMoveTime;
      private int mLastKey;
      private int mLastCodeX;
      private int mLastCodeY;
      private int mCurrentKey = NOT_A_KEY;
      private int mDownKey = NOT_A_KEY;
      private long mLastKeyTime;
      private long mCurrentKeyTime;
      private int[] mKeyIndices = new int[12];
      private GestureDetector mGestureDetector;
      private int mPopupX;
      private int mPopupY;
      private int mRepeatKeyIndex = NOT_A_KEY;
      private int mPopupLayout;
      private boolean mAbortKey;
      private Key mInvalidatedKey;
      private Rect mClipRegion = new Rect(0, 0, 0, 0);
      private boolean mPossiblePoly;
      private SwipeTracker mSwipeTracker = new SwipeTracker();
      private int mSwipeThreshold;
      private boolean mDisambiguateSwipe;
  
      // Variables for dealing with multiple pointers
      private int mOldPointerCount = 1;
      private float mOldPointerX;
      private float mOldPointerY;
  
      @UnsupportedAppUsage
      private Drawable mKeyBackground;
  
      private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
      private static final int REPEAT_START_DELAY = 400;
      private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
  
      private static int MAX_NEARBY_KEYS = 12;
      private int[] mDistances = new int[MAX_NEARBY_KEYS];
  
      // For multi-tap
      private int mLastSentIndex;
      private int mTapCount;
      private long mLastTapTime;
      private boolean mInMultiTap;
      private static final int MULTITAP_INTERVAL = 800; // milliseconds
      private StringBuilder mPreviewLabel = new StringBuilder(1);
  
      /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
      private boolean mDrawPending;
      /** The dirty region in the keyboard bitmap */
      private Rect mDirtyRect = new Rect();
      /** The keyboard bitmap for faster updates */
      private Bitmap mBuffer;
      /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
      private boolean mKeyboardChanged;
      /** The canvas for the above mutable keyboard bitmap */
      private Canvas mCanvas;
      /** The accessibility manager for accessibility support */
      private AccessibilityManager mAccessibilityManager;
      /** The audio manager for accessibility support */
      private AudioManager mAudioManager;
      /** Whether the requirement of a headset to hear passwords if accessibility is enabled is announced. */
      private boolean mHeadsetRequiredToHearPasswordsAnnounced;
  
      Handler mHandler;
  
      public KeyboardView(Context context, AttributeSet attrs) {
          this(context, attrs, com.android.internal.R.attr.keyboardViewStyle);
      }
  
      public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public KeyboardView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
          super(context, attrs, defStyleAttr, defStyleRes);
  
          TypedArray a = context.obtainStyledAttributes(
                  attrs, android.R.styleable.KeyboardView, defStyleAttr, defStyleRes);
  
          LayoutInflater inflate =
                  (LayoutInflater) context
                          .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  
          int previewLayout = 0;
          int keyTextSize = 0;
  
          int n = a.getIndexCount();
  
          for (int i = 0; i < n; i++) {
              int attr = a.getIndex(i);
  
              switch (attr) {
              case com.android.internal.R.styleable.KeyboardView_keyBackground:
                  mKeyBackground = a.getDrawable(attr);
                  break;
              case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
                  mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
                  break;
              case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
                  previewLayout = a.getResourceId(attr, 0);
                  break;
              case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
                  mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
                  break;
              case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
                  mPreviewHeight = a.getDimensionPixelSize(attr, 80);
                  break;
              case com.android.internal.R.styleable.KeyboardView_keyTextSize:
                  mKeyTextSize = a.getDimensionPixelSize(attr, 18);
                  break;
              case com.android.internal.R.styleable.KeyboardView_keyTextColor:
                  mKeyTextColor = a.getColor(attr, 0xFF000000);
                  break;
              case com.android.internal.R.styleable.KeyboardView_labelTextSize:
                  mLabelTextSize = a.getDimensionPixelSize(attr, 14);
                  break;
              case com.android.internal.R.styleable.KeyboardView_popupLayout:
                  mPopupLayout = a.getResourceId(attr, 0);
                  break;
              case com.android.internal.R.styleable.KeyboardView_shadowColor:
                  mShadowColor = a.getColor(attr, 0);
                  break;
              case com.android.internal.R.styleable.KeyboardView_shadowRadius:
                  mShadowRadius = a.getFloat(attr, 0f);
                  break;
              }
          }
  
          a = mContext.obtainStyledAttributes(
                  com.android.internal.R.styleable.Theme);
          mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
  
          mPreviewPopup = new PopupWindow(context);
          if (previewLayout != 0) {
              mPreviewText = (TextView) inflate.inflate(previewLayout, null);
              mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
              mPreviewPopup.setContentView(mPreviewText);
              mPreviewPopup.setBackgroundDrawable(null);
          } else {
              mShowPreview = false;
          }
  
          mPreviewPopup.setTouchable(false);
  
          mPopupKeyboard = new PopupWindow(context);
          mPopupKeyboard.setBackgroundDrawable(null);
          //mPopupKeyboard.setClippingEnabled(false);
  
          mPopupParent = this;
          //mPredicting = true;
  
          mPaint = new Paint();
          mPaint.setAntiAlias(true);
          mPaint.setTextSize(keyTextSize);
          mPaint.setTextAlign(Align.CENTER);
          mPaint.setAlpha(255);
  
          mPadding = new Rect(0, 0, 0, 0);
          mMiniKeyboardCache = new HashMap<Key,View>();
          mKeyBackground.getPadding(mPadding);
  
          mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
          mDisambiguateSwipe = getResources().getBoolean(
                  com.android.internal.R.bool.config_swipeDisambiguation);
  
          mAccessibilityManager = AccessibilityManager.getInstance(context);
          mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
  
          resetMultiTap();
      }
  
      @Override
      protected void onAttachedToWindow() {
          super.onAttachedToWindow();
          initGestureDetector();
          if (mHandler == null) {
              mHandler = new Handler() {
                  @Override
                  public void handleMessage(Message msg) {
                      switch (msg.what) {
                          case MSG_SHOW_PREVIEW:
                              showKey(msg.arg1);
                              break;
                          case MSG_REMOVE_PREVIEW:
                              mPreviewText.setVisibility(INVISIBLE);
                              break;
                          case MSG_REPEAT:
                              if (repeatKey()) {
                                  Message repeat = Message.obtain(this, MSG_REPEAT);
                                  sendMessageDelayed(repeat, REPEAT_INTERVAL);
                              }
                              break;
                          case MSG_LONGPRESS:
                              openPopupIfRequired((MotionEvent) msg.obj);
                              break;
                      }
                  }
              };
          }
      }
  
      private void initGestureDetector() {
          if (mGestureDetector == null) {
              mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
                  @Override
                  public boolean onFling(MotionEvent me1, MotionEvent me2,
                          float velocityX, float velocityY) {
                      if (mPossiblePoly) return false;
                      final float absX = Math.abs(velocityX);
                      final float absY = Math.abs(velocityY);
                      float deltaX = me2.getX() - me1.getX();
                      float deltaY = me2.getY() - me1.getY();
                      int travelX = getWidth() / 2; // Half the keyboard width
                      int travelY = getHeight() / 2; // Half the keyboard height
                      mSwipeTracker.computeCurrentVelocity(1000);
                      final float endingVelocityX = mSwipeTracker.getXVelocity();
                      final float endingVelocityY = mSwipeTracker.getYVelocity();
                      boolean sendDownKey = false;
                      if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
                          if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) {
                              sendDownKey = true;
                          } else {
                              swipeRight();
                              return true;
                          }
                      } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
                          if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) {
                              sendDownKey = true;
                          } else {
                              swipeLeft();
                              return true;
                          }
                      } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
                          if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) {
                              sendDownKey = true;
                          } else {
                              swipeUp();
                              return true;
                          }
                      } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
                          if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) {
                              sendDownKey = true;
                          } else {
                              swipeDown();
                              return true;
                          }
                      }
  
                      if (sendDownKey) {
                          detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime());
                      }
                      return false;
                  }
              });
  
              mGestureDetector.setIsLongpressEnabled(false);
          }
      }
  
      public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
          mKeyboardActionListener = listener;
      }
  
      /**
       * Returns the {@link OnKeyboardActionListener} object.
       * @return the listener attached to this keyboard
       */
      protected OnKeyboardActionListener getOnKeyboardActionListener() {
          return mKeyboardActionListener;
      }
  
      /**
       * Attaches a keyboard to this view. The keyboard can be switched at any time and the
       * view will re-layout itself to accommodate the keyboard.
       * @see Keyboard
       * @see #getKeyboard()
       * @param keyboard the keyboard to display in this view
       */
      public void setKeyboard(Keyboard keyboard) {
          if (mKeyboard != null) {
              showPreview(NOT_A_KEY);
          }
          // Remove any pending messages
          removeMessages();
          mKeyboard = keyboard;
          List<Key> keys = mKeyboard.getKeys();
          mKeys = keys.toArray(new Key[keys.size()]);
          requestLayout();
          // Hint to reallocate the buffer if the size changed
          mKeyboardChanged = true;
          invalidateAllKeys();
          computeProximityThreshold(keyboard);
          mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
          // Switching to a different keyboard should abort any pending keys so that the key up
          // doesn't get delivered to the old or new keyboard
          mAbortKey = true; // Until the next ACTION_DOWN
      }
  
      /**
       * Returns the current keyboard being displayed by this view.
       * @return the currently attached keyboard
       * @see #setKeyboard(Keyboard)
       */
      public Keyboard getKeyboard() {
          return mKeyboard;
      }
  
      /**
       * Sets the state of the shift key of the keyboard, if any.
       * @param shifted whether or not to enable the state of the shift key
       * @return true if the shift key state changed, false if there was no change
       * @see KeyboardView#isShifted()
       */
      public boolean setShifted(boolean shifted) {
          if (mKeyboard != null) {
              if (mKeyboard.setShifted(shifted)) {
                  // The whole keyboard probably needs to be redrawn
                  invalidateAllKeys();
                  return true;
              }
          }
          return false;
      }
  
      /**
       * Returns the state of the shift key of the keyboard, if any.
       * @return true if the shift is in a pressed state, false otherwise. If there is
       * no shift key on the keyboard or there is no keyboard attached, it returns false.
       * @see KeyboardView#setShifted(boolean)
       */
      public boolean isShifted() {
          if (mKeyboard != null) {
              return mKeyboard.isShifted();
          }
          return false;
      }
  
      /**
       * Enables or disables the key feedback popup. This is a popup that shows a magnified
       * version of the depressed key. By default the preview is enabled.
       * @param previewEnabled whether or not to enable the key feedback popup
       * @see #isPreviewEnabled()
       */
      public void setPreviewEnabled(boolean previewEnabled) {
          mShowPreview = previewEnabled;
      }
  
      /**
       * Returns the enabled state of the key feedback popup.
       * @return whether or not the key feedback popup is enabled
       * @see #setPreviewEnabled(boolean)
       */
      public boolean isPreviewEnabled() {
          return mShowPreview;
      }
  
      public void setVerticalCorrection(int verticalOffset) {
  
      }
      public void setPopupParent(View v) {
          mPopupParent = v;
      }
  
      public void setPopupOffset(int x, int y) {
          mMiniKeyboardOffsetX = x;
          mMiniKeyboardOffsetY = y;
          if (mPreviewPopup.isShowing()) {
              mPreviewPopup.dismiss();
          }
      }
  
      /**
       * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
       * codes for adjacent keys.  When disabled, only the primary key code will be
       * reported.
       * @param enabled whether or not the proximity correction is enabled
       */
      public void setProximityCorrectionEnabled(boolean enabled) {
          mProximityCorrectOn = enabled;
      }
  
      /**
       * Returns true if proximity correction is enabled.
       */
      public boolean isProximityCorrectionEnabled() {
          return mProximityCorrectOn;
      }
  
      /**
       * Popup keyboard close button clicked.
       * @hide
       */
      public void onClick(View v) {
          dismissPopupKeyboard();
      }
  
      private CharSequence adjustCase(CharSequence label) {
          if (mKeyboard.isShifted() && label != null && label.length() < 3
                  && Character.isLowerCase(label.charAt(0))) {
              label = label.toString().toUpperCase();
          }
          return label;
      }
  
      @Override
      public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
          // Round up a little
          if (mKeyboard == null) {
              setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom);
          } else {
              int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight;
              if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
                  width = MeasureSpec.getSize(widthMeasureSpec);
              }
              setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom);
          }
      }
  
      /**
       * Compute the average distance between adjacent keys (horizontally and vertically)
       * and square it to get the proximity threshold. We use a square here and in computing
       * the touch distance from a key's center to avoid taking a square root.
       * @param keyboard
       */
      private void computeProximityThreshold(Keyboard keyboard) {
          if (keyboard == null) return;
          final Key[] keys = mKeys;
          if (keys == null) return;
          int length = keys.length;
          int dimensionSum = 0;
          for (int i = 0; i < length; i++) {
              Key key = keys[i];
              dimensionSum += Math.min(key.width, key.height) + key.gap;
          }
          if (dimensionSum < 0 || length == 0) return;
          mProximityThreshold = (int) (dimensionSum * 1.4f / length);
          mProximityThreshold *= mProximityThreshold; // Square it
      }
  
      @Override
      public void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
          if (mKeyboard != null) {
              mKeyboard.resize(w, h);
          }
          // Release the buffer, if any and it will be reallocated on the next draw
          mBuffer = null;
      }
  
      @Override
      public void onDraw(Canvas canvas) {
          super.onDraw(canvas);
          if (mDrawPending || mBuffer == null || mKeyboardChanged) {
              onBufferDraw();
          }
          canvas.drawBitmap(mBuffer, 0, 0, null);
      }
  
      private void onBufferDraw() {
          if (mBuffer == null || mKeyboardChanged) {
              if (mBuffer == null || mKeyboardChanged &&
                      (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
                  // Make sure our bitmap is at least 1x1
                  final int width = Math.max(1, getWidth());
                  final int height = Math.max(1, getHeight());
                  mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                  mCanvas = new Canvas(mBuffer);
              }
              invalidateAllKeys();
              mKeyboardChanged = false;
          }
  
          if (mKeyboard == null) return;
  
          mCanvas.save();
          final Canvas canvas = mCanvas;
          canvas.clipRect(mDirtyRect);
  
          final Paint paint = mPaint;
          final Drawable keyBackground = mKeyBackground;
          final Rect clipRegion = mClipRegion;
          final Rect padding = mPadding;
          final int kbdPaddingLeft = mPaddingLeft;
          final int kbdPaddingTop = mPaddingTop;
          final Key[] keys = mKeys;
          final Key invalidKey = mInvalidatedKey;
  
          paint.setColor(mKeyTextColor);
          boolean drawSingleKey = false;
          if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
            // Is clipRegion completely contained within the invalidated key?
            if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
                    invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
                    invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
                    invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
                drawSingleKey = true;
            }
          }
          canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
          final int keyCount = keys.length;
          for (int i = 0; i < keyCount; i++) {
              final Key key = keys[i];
              if (drawSingleKey && invalidKey != key) {
                  continue;
              }
              int[] drawableState = key.getCurrentDrawableState();
              keyBackground.setState(drawableState);
  
              // Switch the character to uppercase if shift is pressed
              String label = key.label == null? null : adjustCase(key.label).toString();
  
              final Rect bounds = keyBackground.getBounds();
              if (key.width != bounds.right ||
                      key.height != bounds.bottom) {
                  keyBackground.setBounds(0, 0, key.width, key.height);
              }
              canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
              keyBackground.draw(canvas);
  
              if (label != null) {
                  // For characters, use large font. For labels like "Done", use small font.
                  if (label.length() > 1 && key.codes.length < 2) {
                      paint.setTextSize(mLabelTextSize);
                      paint.setTypeface(Typeface.DEFAULT_BOLD);
                  } else {
                      paint.setTextSize(mKeyTextSize);
                      paint.setTypeface(Typeface.DEFAULT);
                  }
                  // Draw a drop shadow for the text
                  paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
                  // Draw the text
                  canvas.drawText(label,
                      (key.width - padding.left - padding.right) / 2
                              + padding.left,
                      (key.height - padding.top - padding.bottom) / 2
                              + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
                      paint);
                  // Turn off drop shadow
                  paint.setShadowLayer(0, 0, 0, 0);
              } else if (key.icon != null) {
                  final int drawableX = (key.width - padding.left - padding.right
                                  - key.icon.getIntrinsicWidth()) / 2 + padding.left;
                  final int drawableY = (key.height - padding.top - padding.bottom
                          - key.icon.getIntrinsicHeight()) / 2 + padding.top;
                  canvas.translate(drawableX, drawableY);
                  key.icon.setBounds(0, 0,
                          key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
                  key.icon.draw(canvas);
                  canvas.translate(-drawableX, -drawableY);
              }
              canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
          }
          mInvalidatedKey = null;
          // Overlay a dark rectangle to dim the keyboard
          if (mMiniKeyboardOnScreen) {
              paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
              canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
          }
  
          if (DEBUG && mShowTouchPoints) {
              paint.setAlpha(128);
              paint.setColor(0xFFFF0000);
              canvas.drawCircle(mStartX, mStartY, 3, paint);
              canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
              paint.setColor(0xFF0000FF);
              canvas.drawCircle(mLastX, mLastY, 3, paint);
              paint.setColor(0xFF00FF00);
              canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
          }
          mCanvas.restore();
          mDrawPending = false;
          mDirtyRect.setEmpty();
      }
  
      private int getKeyIndices(int x, int y, int[] allKeys) {
          final Key[] keys = mKeys;
          int primaryIndex = NOT_A_KEY;
          int closestKey = NOT_A_KEY;
          int closestKeyDist = mProximityThreshold + 1;
          java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
          int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
          final int keyCount = nearestKeyIndices.length;
          for (int i = 0; i < keyCount; i++) {
              final Key key = keys[nearestKeyIndices[i]];
              int dist = 0;
              boolean isInside = key.isInside(x,y);
              if (isInside) {
                  primaryIndex = nearestKeyIndices[i];
              }
  
              if (((mProximityCorrectOn
                      && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
                      || isInside)
                      && key.codes[0] > 32) {
                  // Find insertion point
                  final int nCodes = key.codes.length;
                  if (dist < closestKeyDist) {
                      closestKeyDist = dist;
                      closestKey = nearestKeyIndices[i];
                  }
  
                  if (allKeys == null) continue;
  
                  for (int j = 0; j < mDistances.length; j++) {
                      if (mDistances[j] > dist) {
                          // Make space for nCodes codes
                          System.arraycopy(mDistances, j, mDistances, j + nCodes,
                                  mDistances.length - j - nCodes);
                          System.arraycopy(allKeys, j, allKeys, j + nCodes,
                                  allKeys.length - j - nCodes);
                          for (int c = 0; c < nCodes; c++) {
                              allKeys[j + c] = key.codes[c];
                              mDistances[j + c] = dist;
                          }
                          break;
                      }
                  }
              }
          }
          if (primaryIndex == NOT_A_KEY) {
              primaryIndex = closestKey;
          }
          return primaryIndex;
      }
  
      private void detectAndSendKey(int index, int x, int y, long eventTime) {
          if (index != NOT_A_KEY && index < mKeys.length) {
              final Key key = mKeys[index];
              if (key.text != null) {
                  mKeyboardActionListener.onText(key.text);
                  mKeyboardActionListener.onRelease(NOT_A_KEY);
              } else {
                  int code = key.codes[0];
                  //TextEntryState.keyPressedAt(key, x, y);
                  int[] codes = new int[MAX_NEARBY_KEYS];
                  Arrays.fill(codes, NOT_A_KEY);
                  getKeyIndices(x, y, codes);
                  // Multi-tap
                  if (mInMultiTap) {
                      if (mTapCount != -1) {
                          mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
                      } else {
                          mTapCount = 0;
                      }
                      code = key.codes[mTapCount];
                  }
                  mKeyboardActionListener.onKey(code, codes);
                  mKeyboardActionListener.onRelease(code);
              }
              mLastSentIndex = index;
              mLastTapTime = eventTime;
          }
      }
  
      /**
       * Handle multi-tap keys by producing the key label for the current multi-tap state.
       */
      private CharSequence getPreviewText(Key key) {
          if (mInMultiTap) {
              // Multi-tap
              mPreviewLabel.setLength(0);
              mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
              return adjustCase(mPreviewLabel);
          } else {
              return adjustCase(key.label);
          }
      }
  
      private void showPreview(int keyIndex) {
          int oldKeyIndex = mCurrentKeyIndex;
          final PopupWindow previewPopup = mPreviewPopup;
  
          mCurrentKeyIndex = keyIndex;
          // Release the old key and press the new key
          final Key[] keys = mKeys;
          if (oldKeyIndex != mCurrentKeyIndex) {
              if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
                  Key oldKey = keys[oldKeyIndex];
                  oldKey.onReleased(mCurrentKeyIndex == NOT_A_KEY);
                  invalidateKey(oldKeyIndex);
                  final int keyCode = oldKey.codes[0];
                  sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT,
                          keyCode);
                  // TODO: We need to implement AccessibilityNodeProvider for this view.
                  sendAccessibilityEventForUnicodeCharacter(
                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, keyCode);
              }
              if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
                  Key newKey = keys[mCurrentKeyIndex];
                  newKey.onPressed();
                  invalidateKey(mCurrentKeyIndex);
                  final int keyCode = newKey.codes[0];
                  sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
                          keyCode);
                  // TODO: We need to implement AccessibilityNodeProvider for this view.
                  sendAccessibilityEventForUnicodeCharacter(
                          AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, keyCode);
              }
          }
          // If key changed and preview is on ...
          if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
              mHandler.removeMessages(MSG_SHOW_PREVIEW);
              if (previewPopup.isShowing()) {
                  if (keyIndex == NOT_A_KEY) {
                      mHandler.sendMessageDelayed(mHandler
                              .obtainMessage(MSG_REMOVE_PREVIEW),
                              DELAY_AFTER_PREVIEW);
                  }
              }
              if (keyIndex != NOT_A_KEY) {
                  if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
                      // Show right away, if it's already visible and finger is moving around
                      showKey(keyIndex);
                  } else {
                      mHandler.sendMessageDelayed(
                              mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
                              DELAY_BEFORE_PREVIEW);
                  }
              }
          }
      }
  
      @UnsupportedAppUsage
      private void showKey(final int keyIndex) {
          final PopupWindow previewPopup = mPreviewPopup;
          final Key[] keys = mKeys;
          if (keyIndex < 0 || keyIndex >= mKeys.length) return;
          Key key = keys[keyIndex];
          if (key.icon != null) {
              mPreviewText.setCompoundDrawables(null, null, null,
                      key.iconPreview != null ? key.iconPreview : key.icon);
              mPreviewText.setText(null);
          } else {
              mPreviewText.setCompoundDrawables(null, null, null, null);
              mPreviewText.setText(getPreviewText(key));
              if (key.label.length() > 1 && key.codes.length < 2) {
                  mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
                  mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
              } else {
                  mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
                  mPreviewText.setTypeface(Typeface.DEFAULT);
              }
          }
          mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                  MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
          int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
                  + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
          final int popupHeight = mPreviewHeight;
          LayoutParams lp = mPreviewText.getLayoutParams();
          if (lp != null) {
              lp.width = popupWidth;
              lp.height = popupHeight;
          }
          if (!mPreviewCentered) {
              mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
              mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
          } else {
              // TODO: Fix this if centering is brought back
              mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
              mPopupPreviewY = - mPreviewText.getMeasuredHeight();
          }
          mHandler.removeMessages(MSG_REMOVE_PREVIEW);
          getLocationInWindow(mCoordinates);
          mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
          mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
  
          // Set the preview background state
          mPreviewText.getBackground().setState(
                  key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
          mPopupPreviewX += mCoordinates[0];
          mPopupPreviewY += mCoordinates[1];
  
          // If the popup cannot be shown above the key, put it on the side
          getLocationOnScreen(mCoordinates);
          if (mPopupPreviewY + mCoordinates[1] < 0) {
              // If the key you're pressing is on the left side of the keyboard, show the popup on
              // the right, offset by enough to see at least one key to the left/right.
              if (key.x + key.width <= getWidth() / 2) {
                  mPopupPreviewX += (int) (key.width * 2.5);
              } else {
                  mPopupPreviewX -= (int) (key.width * 2.5);
              }
              mPopupPreviewY += popupHeight;
          }
  
          if (previewPopup.isShowing()) {
              previewPopup.update(mPopupPreviewX, mPopupPreviewY,
                      popupWidth, popupHeight);
          } else {
              previewPopup.setWidth(popupWidth);
              previewPopup.setHeight(popupHeight);
              previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
                      mPopupPreviewX, mPopupPreviewY);
          }
          mPreviewText.setVisibility(VISIBLE);
      }
  
      private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) {
          if (mAccessibilityManager.isEnabled()) {
              AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
              onInitializeAccessibilityEvent(event);
              final String text;
              switch (code) {
                  case Keyboard.KEYCODE_ALT:
                      text = mContext.getString(R.string.keyboardview_keycode_alt);
                      break;
                  case Keyboard.KEYCODE_CANCEL:
                      text = mContext.getString(R.string.keyboardview_keycode_cancel);
                      break;
                  case Keyboard.KEYCODE_DELETE:
                      text = mContext.getString(R.string.keyboardview_keycode_delete);
                      break;
                  case Keyboard.KEYCODE_DONE:
                      text = mContext.getString(R.string.keyboardview_keycode_done);
                      break;
                  case Keyboard.KEYCODE_MODE_CHANGE:
                      text = mContext.getString(R.string.keyboardview_keycode_mode_change);
                      break;
                  case Keyboard.KEYCODE_SHIFT:
                      text = mContext.getString(R.string.keyboardview_keycode_shift);
                      break;
                  case '\n':
                      text = mContext.getString(R.string.keyboardview_keycode_enter);
                      break;
                  default:
                      text = String.valueOf((char) code);
              }
              event.getText().add(text);
              mAccessibilityManager.sendAccessibilityEvent(event);
          }
      }
  
      /**
       * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
       * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
       * draws the cached buffer.
       * @see #invalidateKey(int)
       */
      public void invalidateAllKeys() {
          mDirtyRect.union(0, 0, getWidth(), getHeight());
          mDrawPending = true;
          invalidate();
      }
  
      /**
       * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
       * one key is changing it's content. Any changes that affect the position or size of the key
       * may not be honored.
       * @param keyIndex the index of the key in the attached {@link Keyboard}.
       * @see #invalidateAllKeys
       */
      public void invalidateKey(int keyIndex) {
          if (mKeys == null) return;
          if (keyIndex < 0 || keyIndex >= mKeys.length) {
              return;
          }
          final Key key = mKeys[keyIndex];
          mInvalidatedKey = key;
          mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop,
                  key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
          onBufferDraw();
          invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
                  key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
      }
  
      @UnsupportedAppUsage
      private boolean openPopupIfRequired(MotionEvent me) {
          // Check if we have a popup layout specified first.
          if (mPopupLayout == 0) {
              return false;
          }
          if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
              return false;
          }
  
          Key popupKey = mKeys[mCurrentKey];
          boolean result = onLongPress(popupKey);
          if (result) {
              mAbortKey = true;
              showPreview(NOT_A_KEY);
          }
          return result;
      }
  
      /**
       * Called when a key is long pressed. By default this will open any popup keyboard associated
       * with this key through the attributes popupLayout and popupCharacters.
       * @param popupKey the key that was long pressed
       * @return true if the long press is handled, false otherwise. Subclasses should call the
       * method on the base class if the subclass doesn't wish to handle the call.
       */
      protected boolean onLongPress(Key popupKey) {
          int popupKeyboardId = popupKey.popupResId;
  
          if (popupKeyboardId != 0) {
              mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
              if (mMiniKeyboardContainer == null) {
                  LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
                          Context.LAYOUT_INFLATER_SERVICE);
                  mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
                  mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
                          com.android.internal.R.id.keyboardView);
                  View closeButton = mMiniKeyboardContainer.findViewById(
                          com.android.internal.R.id.closeButton);
                  if (closeButton != null) closeButton.setOnClickListener(this);
                  mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
                      public void onKey(int primaryCode, int[] keyCodes) {
                          mKeyboardActionListener.onKey(primaryCode, keyCodes);
                          dismissPopupKeyboard();
                      }
  
                      public void onText(CharSequence text) {
                          mKeyboardActionListener.onText(text);
                          dismissPopupKeyboard();
                      }
  
                      public void swipeLeft() { }
                      public void swipeRight() { }
                      public void swipeUp() { }
                      public void swipeDown() { }
                      public void onPress(int primaryCode) {
                          mKeyboardActionListener.onPress(primaryCode);
                      }
                      public void onRelease(int primaryCode) {
                          mKeyboardActionListener.onRelease(primaryCode);
                      }
                  });
                  //mInputView.setSuggest(mSuggest);
                  Keyboard keyboard;
                  if (popupKey.popupCharacters != null) {
                      keyboard = new Keyboard(getContext(), popupKeyboardId,
                              popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
                  } else {
                      keyboard = new Keyboard(getContext(), popupKeyboardId);
                  }
                  mMiniKeyboard.setKeyboard(keyboard);
                  mMiniKeyboard.setPopupParent(this);
                  mMiniKeyboardContainer.measure(
                          MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
                          MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
  
                  mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
              } else {
                  mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
                          com.android.internal.R.id.keyboardView);
              }
              getLocationInWindow(mCoordinates);
              mPopupX = popupKey.x + mPaddingLeft;
              mPopupY = popupKey.y + mPaddingTop;
              mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
              mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
              final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0];
              final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1];
              mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
              mMiniKeyboard.setShifted(isShifted());
              mPopupKeyboard.setContentView(mMiniKeyboardContainer);
              mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
              mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
              mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
              mMiniKeyboardOnScreen = true;
              //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
              invalidateAllKeys();
              return true;
          }
          return false;
      }
  
      @Override
      public boolean onHoverEvent(MotionEvent event) {
          if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
              final int action = event.getAction();
              switch (action) {
                  case MotionEvent.ACTION_HOVER_ENTER: {
                      event.setAction(MotionEvent.ACTION_DOWN);
                  } break;
                  case MotionEvent.ACTION_HOVER_MOVE: {
                      event.setAction(MotionEvent.ACTION_MOVE);
                  } break;
                  case MotionEvent.ACTION_HOVER_EXIT: {
                      event.setAction(MotionEvent.ACTION_UP);
                  } break;
              }
              return onTouchEvent(event);
          }
          return true;
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent me) {
          // Convert multi-pointer up/down events to single up/down events to
          // deal with the typical multi-pointer behavior of two-thumb typing
          final int pointerCount = me.getPointerCount();
          final int action = me.getAction();
          boolean result = false;
          final long now = me.getEventTime();
  
          if (pointerCount != mOldPointerCount) {
              if (pointerCount == 1) {
                  // Send a down event for the latest pointer
                  MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
                          me.getX(), me.getY(), me.getMetaState());
                  result = onModifiedTouchEvent(down, false);
                  down.recycle();
                  // If it's an up action, then deliver the up as well.
                  if (action == MotionEvent.ACTION_UP) {
                      result = onModifiedTouchEvent(me, true);
                  }
              } else {
                  // Send an up event for the last pointer
                  MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
                          mOldPointerX, mOldPointerY, me.getMetaState());
                  result = onModifiedTouchEvent(up, true);
                  up.recycle();
              }
          } else {
              if (pointerCount == 1) {
                  result = onModifiedTouchEvent(me, false);
                  mOldPointerX = me.getX();
                  mOldPointerY = me.getY();
              } else {
                  // Don't do anything when 2 pointers are down and moving.
                  result = true;
              }
          }
          mOldPointerCount = pointerCount;
  
          return result;
      }
  
      private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
          int touchX = (int) me.getX() - mPaddingLeft;
          int touchY = (int) me.getY() - mPaddingTop;
          if (touchY >= -mVerticalCorrection)
              touchY += mVerticalCorrection;
          final int action = me.getAction();
          final long eventTime = me.getEventTime();
          int keyIndex = getKeyIndices(touchX, touchY, null);
          mPossiblePoly = possiblePoly;
  
          // Track the last few movements to look for spurious swipes.
          if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
          mSwipeTracker.addMovement(me);
  
          // Ignore all motion events until a DOWN.
          if (mAbortKey
                  && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
              return true;
          }
  
          if (mGestureDetector.onTouchEvent(me)) {
              showPreview(NOT_A_KEY);
              mHandler.removeMessages(MSG_REPEAT);
              mHandler.removeMessages(MSG_LONGPRESS);
              return true;
          }
  
          // Needs to be called after the gesture detector gets a turn, as it may have
          // displayed the mini keyboard
          if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) {
              return true;
          }
  
          switch (action) {
              case MotionEvent.ACTION_DOWN:
                  mAbortKey = false;
                  mStartX = touchX;
                  mStartY = touchY;
                  mLastCodeX = touchX;
                  mLastCodeY = touchY;
                  mLastKeyTime = 0;
                  mCurrentKeyTime = 0;
                  mLastKey = NOT_A_KEY;
                  mCurrentKey = keyIndex;
                  mDownKey = keyIndex;
                  mDownTime = me.getEventTime();
                  mLastMoveTime = mDownTime;
                  checkMultiTap(eventTime, keyIndex);
                  mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
                          mKeys[keyIndex].codes[0] : 0);
                  if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
                      mRepeatKeyIndex = mCurrentKey;
                      Message msg = mHandler.obtainMessage(MSG_REPEAT);
                      mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
                      repeatKey();
                      // Delivering the key could have caused an abort
                      if (mAbortKey) {
                          mRepeatKeyIndex = NOT_A_KEY;
                          break;
                      }
                  }
                  if (mCurrentKey != NOT_A_KEY) {
                      Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
                      mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
                  }
                  showPreview(keyIndex);
                  break;
  
              case MotionEvent.ACTION_MOVE:
                  boolean continueLongPress = false;
                  if (keyIndex != NOT_A_KEY) {
                      if (mCurrentKey == NOT_A_KEY) {
                          mCurrentKey = keyIndex;
                          mCurrentKeyTime = eventTime - mDownTime;
                      } else {
                          if (keyIndex == mCurrentKey) {
                              mCurrentKeyTime += eventTime - mLastMoveTime;
                              continueLongPress = true;
                          } else if (mRepeatKeyIndex == NOT_A_KEY) {
                              resetMultiTap();
                              mLastKey = mCurrentKey;
                              mLastCodeX = mLastX;
                              mLastCodeY = mLastY;
                              mLastKeyTime =
                                      mCurrentKeyTime + eventTime - mLastMoveTime;
                              mCurrentKey = keyIndex;
                              mCurrentKeyTime = 0;
                          }
                      }
                  }
                  if (!continueLongPress) {
                      // Cancel old longpress
                      mHandler.removeMessages(MSG_LONGPRESS);
                      // Start new longpress if key has changed
                      if (keyIndex != NOT_A_KEY) {
                          Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
                          mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
                      }
                  }
                  showPreview(mCurrentKey);
                  mLastMoveTime = eventTime;
                  break;
  
              case MotionEvent.ACTION_UP:
                  removeMessages();
                  if (keyIndex == mCurrentKey) {
                      mCurrentKeyTime += eventTime - mLastMoveTime;
                  } else {
                      resetMultiTap();
                      mLastKey = mCurrentKey;
                      mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
                      mCurrentKey = keyIndex;
                      mCurrentKeyTime = 0;
                  }
                  if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
                          && mLastKey != NOT_A_KEY) {
                      mCurrentKey = mLastKey;
                      touchX = mLastCodeX;
                      touchY = mLastCodeY;
                  }
                  showPreview(NOT_A_KEY);
                  Arrays.fill(mKeyIndices, NOT_A_KEY);
                  // If we're not on a repeating key (which sends on a DOWN event)
                  if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
                      detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
                  }
                  invalidateKey(keyIndex);
                  mRepeatKeyIndex = NOT_A_KEY;
                  break;
              case MotionEvent.ACTION_CANCEL:
                  removeMessages();
                  dismissPopupKeyboard();
                  mAbortKey = true;
                  showPreview(NOT_A_KEY);
                  invalidateKey(mCurrentKey);
                  break;
          }
          mLastX = touchX;
          mLastY = touchY;
          return true;
      }
  
      @UnsupportedAppUsage
      private boolean repeatKey() {
          Key key = mKeys[mRepeatKeyIndex];
          detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
          return true;
      }
  
      protected void swipeRight() {
          mKeyboardActionListener.swipeRight();
      }
  
      protected void swipeLeft() {
          mKeyboardActionListener.swipeLeft();
      }
  
      protected void swipeUp() {
          mKeyboardActionListener.swipeUp();
      }
  
      protected void swipeDown() {
          mKeyboardActionListener.swipeDown();
      }
  
      public void closing() {
          if (mPreviewPopup.isShowing()) {
              mPreviewPopup.dismiss();
          }
          removeMessages();
  
          dismissPopupKeyboard();
          mBuffer = null;
          mCanvas = null;
          mMiniKeyboardCache.clear();
      }
  
      private void removeMessages() {
          if (mHandler != null) {
              mHandler.removeMessages(MSG_REPEAT);
              mHandler.removeMessages(MSG_LONGPRESS);
              mHandler.removeMessages(MSG_SHOW_PREVIEW);
          }
      }
  
      @Override
      public void onDetachedFromWindow() {
          super.onDetachedFromWindow();
          closing();
      }
  
      private void dismissPopupKeyboard() {
          if (mPopupKeyboard.isShowing()) {
              mPopupKeyboard.dismiss();
              mMiniKeyboardOnScreen = false;
              invalidateAllKeys();
          }
      }
  
      public boolean handleBack() {
          if (mPopupKeyboard.isShowing()) {
              dismissPopupKeyboard();
              return true;
          }
          return false;
      }
  
      private void resetMultiTap() {
          mLastSentIndex = NOT_A_KEY;
          mTapCount = 0;
          mLastTapTime = -1;
          mInMultiTap = false;
      }
  
      private void checkMultiTap(long eventTime, int keyIndex) {
          if (keyIndex == NOT_A_KEY) return;
          Key key = mKeys[keyIndex];
          if (key.codes.length > 1) {
              mInMultiTap = true;
              if (eventTime < mLastTapTime + MULTITAP_INTERVAL
                      && keyIndex == mLastSentIndex) {
                  mTapCount = (mTapCount + 1) % key.codes.length;
                  return;
              } else {
                  mTapCount = -1;
                  return;
              }
          }
          if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
              resetMultiTap();
          }
      }
  
      private static class SwipeTracker {
  
          static final int NUM_PAST = 4;
          static final int LONGEST_PAST_TIME = 200;
  
          final float mPastX[] = new float[NUM_PAST];
          final float mPastY[] = new float[NUM_PAST];
          final long mPastTime[] = new long[NUM_PAST];
  
          float mYVelocity;
          float mXVelocity;
  
          public void clear() {
              mPastTime[0] = 0;
          }
  
          public void addMovement(MotionEvent ev) {
              long time = ev.getEventTime();
              final int N = ev.getHistorySize();
              for (int i=0; i<N; i++) {
                  addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
                          ev.getHistoricalEventTime(i));
              }
              addPoint(ev.getX(), ev.getY(), time);
          }
  
          private void addPoint(float x, float y, long time) {
              int drop = -1;
              int i;
              final long[] pastTime = mPastTime;
              for (i=0; i<NUM_PAST; i++) {
                  if (pastTime[i] == 0) {
                      break;
                  } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
                      drop = i;
                  }
              }
              if (i == NUM_PAST && drop < 0) {
                  drop = 0;
              }
              if (drop == i) drop--;
              final float[] pastX = mPastX;
              final float[] pastY = mPastY;
              if (drop >= 0) {
                  final int start = drop+1;
                  final int count = NUM_PAST-drop-1;
                  System.arraycopy(pastX, start, pastX, 0, count);
                  System.arraycopy(pastY, start, pastY, 0, count);
                  System.arraycopy(pastTime, start, pastTime, 0, count);
                  i -= (drop+1);
              }
              pastX[i] = x;
              pastY[i] = y;
              pastTime[i] = time;
              i++;
              if (i < NUM_PAST) {
                  pastTime[i] = 0;
              }
          }
  
          public void computeCurrentVelocity(int units) {
              computeCurrentVelocity(units, Float.MAX_VALUE);
          }
  
          public void computeCurrentVelocity(int units, float maxVelocity) {
              final float[] pastX = mPastX;
              final float[] pastY = mPastY;
              final long[] pastTime = mPastTime;
  
              final float oldestX = pastX[0];
              final float oldestY = pastY[0];
              final long oldestTime = pastTime[0];
              float accumX = 0;
              float accumY = 0;
              int N=0;
              while (N < NUM_PAST) {
                  if (pastTime[N] == 0) {
                      break;
                  }
                  N++;
              }
  
              for (int i=1; i < N; i++) {
                  final int dur = (int)(pastTime[i] - oldestTime);
                  if (dur == 0) continue;
                  float dist = pastX[i] - oldestX;
                  float vel = (dist/dur) * units;   // pixels/frame.
                  if (accumX == 0) accumX = vel;
                  else accumX = (accumX + vel) * .5f;
  
                  dist = pastY[i] - oldestY;
                  vel = (dist/dur) * units;   // pixels/frame.
                  if (accumY == 0) accumY = vel;
                  else accumY = (accumY + vel) * .5f;
              }
              mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
                      : Math.min(accumX, maxVelocity);
              mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
                      : Math.min(accumY, maxVelocity);
          }
  
          public float getXVelocity() {
              return mXVelocity;
          }
  
          public float getYVelocity() {
              return mYVelocity;
          }
      }
  }
  