Home | History | Annotate | Download | only in car
      1 /*
      2  * Copyright (C) 2016 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 com.android.inputmethod.latin.car;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ValueAnimator;
     21 import android.annotation.SuppressLint;
     22 import android.content.Context;
     23 import android.content.res.TypedArray;
     24 import android.graphics.Bitmap;
     25 import android.graphics.Canvas;
     26 import android.graphics.Color;
     27 import android.graphics.Paint;
     28 import android.graphics.Paint.Align;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.Rect;
     31 import android.graphics.Region.Op;
     32 import android.graphics.Typeface;
     33 import android.graphics.drawable.Drawable;
     34 import android.inputmethodservice.Keyboard;
     35 import android.inputmethodservice.Keyboard.Key;
     36 import android.os.Handler;
     37 import android.os.Message;
     38 import android.util.AttributeSet;
     39 import android.util.Log;
     40 import android.util.TypedValue;
     41 import android.view.GestureDetector;
     42 import android.view.Gravity;
     43 import android.view.LayoutInflater;
     44 import android.view.MotionEvent;
     45 import android.view.View;
     46 import android.view.ViewConfiguration;
     47 import android.view.ViewGroup.LayoutParams;
     48 import android.view.accessibility.AccessibilityManager;
     49 import android.view.animation.AccelerateDecelerateInterpolator;
     50 import android.view.animation.LinearInterpolator;
     51 import android.widget.PopupWindow;
     52 import android.widget.TextView;
     53 
     54 import com.android.inputmethod.latin.R;
     55 
     56 import java.lang.ref.WeakReference;
     57 import java.util.Arrays;
     58 import java.util.HashMap;
     59 import java.util.List;
     60 import java.util.Locale;
     61 import java.util.Map;
     62 import java.util.regex.Pattern;
     63 
     64 /**
     65  * A view that renders a virtual {@link android.inputmethodservice.Keyboard}. It handles rendering of keys and
     66  * detecting key presses and touch movements.
     67  *
     68  * @attr ref android.R.styleable#KeyboardView_keyBackground
     69  * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
     70  * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
     71  * @attr ref android.R.styleable#KeyboardView_labelTextSize
     72  * @attr ref android.R.styleable#KeyboardView_keyTextSize
     73  * @attr ref android.R.styleable#KeyboardView_keyTextColor
     74  * @attr ref android.R.styleable#KeyboardView_verticalCorrection
     75  * @attr ref android.R.styleable#KeyboardView_popupLayout
     76  */
     77 public class KeyboardView extends View implements View.OnClickListener {
     78 
     79     /**
     80      * Listener for virtual keyboard events.
     81      */
     82     public interface OnKeyboardActionListener {
     83 
     84         /**
     85          * Called when the user presses a key. This is sent before the {@link #onKey} is called.
     86          * For keys that repeat, this is only called once.
     87          * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
     88          * key, the value will be zero.
     89          */
     90         void onPress(int primaryCode);
     91 
     92         /**
     93          * Called when the user releases a key. This is sent after the {@link #onKey} is called.
     94          * For keys that repeat, this is only called once.
     95          * @param primaryCode the code of the key that was released
     96          */
     97         void onRelease(int primaryCode);
     98 
     99         /**
    100          * Send a key press to the listener.
    101          * @param primaryCode this is the key that was pressed
    102          * @param keyCodes the codes for all the possible alternative keys
    103          * with the primary code being the first. If the primary key code is
    104          * a single character such as an alphabet or number or symbol, the alternatives
    105          * will include other characters that may be on the same key or adjacent keys.
    106          * These codes are useful to correct for accidental presses of a key adjacent to
    107          * the intended key.
    108          */
    109         void onKey(int primaryCode, int[] keyCodes);
    110 
    111         /**
    112          * Sends a sequence of characters to the listener.
    113          * @param text the sequence of characters to be displayed.
    114          */
    115         void onText(CharSequence text);
    116 
    117         /**
    118          * Called when the user quickly moves the finger from right to left.
    119          */
    120         void swipeLeft();
    121 
    122         /**
    123          * Called when the user quickly moves the finger from left to right.
    124          */
    125         void swipeRight();
    126 
    127         /**
    128          * Called when the user quickly moves the finger from up to down.
    129          */
    130         void swipeDown();
    131 
    132         /**
    133          * Called when the user quickly moves the finger from down to up.
    134          */
    135         void swipeUp();
    136 
    137         /**
    138          * Called when we want to stop keyboard input.
    139          */
    140         void stopInput();
    141     }
    142 
    143     private static final boolean DEBUG = false;
    144     private static final int NOT_A_KEY = -1;
    145     private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
    146     private static final int SCRIM_ALPHA = 242;  // 95% opacity.
    147     private static final int MAX_ALPHA = 255;
    148     private static final float MIN_ANIMATION_VALUE = 0.0f;
    149     private static final float MAX_ANIMATION_VALUE = 1.0f;
    150     private static final Pattern PUNCTUATION_PATTERN = Pattern.compile("_|-|,|\\.");
    151     //private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
    152     private final Context mContext;
    153 
    154     private Keyboard mKeyboard;
    155     private int mCurrentKeyIndex = NOT_A_KEY;
    156     private int mLabelTextSize;
    157     private int mKeyTextSize;
    158     private int mKeyPunctuationSize;
    159     private int mKeyTextColorPrimary;
    160     private int mKeyTextColorSecondary;
    161     private String mFontFamily;
    162     private int mTextStyle = Typeface.BOLD;
    163 
    164     private TextView mPreviewText;
    165     private final PopupWindow mPreviewPopup;
    166     private int mPreviewTextSizeLarge;
    167     private int mPreviewOffset;
    168     private int mPreviewHeight;
    169     // Working variable
    170     private final int[] mCoordinates = new int[2];
    171 
    172     private KeyboardView mPopupKeyboardView;
    173     private boolean mPopupKeyboardOnScreen;
    174     private View mPopupParent;
    175     private int mMiniKeyboardOffsetX;
    176     private int mMiniKeyboardOffsetY;
    177     private final Map<Key,View> mMiniKeyboardCache;
    178     private Key[] mKeys;
    179 
    180     /** Listener for {@link OnKeyboardActionListener}. */
    181     private OnKeyboardActionListener mKeyboardActionListener;
    182 
    183     private static final int MSG_SHOW_PREVIEW = 1;
    184     private static final int MSG_REMOVE_PREVIEW = 2;
    185     private static final int MSG_REPEAT = 3;
    186     private static final int MSG_LONGPRESS = 4;
    187 
    188     private static final int DELAY_BEFORE_PREVIEW = 0;
    189     private static final int DELAY_AFTER_PREVIEW = 70;
    190     private static final int DEBOUNCE_TIME = 70;
    191 
    192     private static final int ANIMATE_IN_OUT_DURATION_MS = 300;
    193     private static final int ANIMATE_SCRIM_DURATION_MS = 150;
    194     private static final int ANIMATE_SCRIM_DELAY_MS = 225;
    195 
    196     private int mVerticalCorrection;
    197     private int mProximityThreshold;
    198 
    199     private final boolean mPreviewCentered = false;
    200     private boolean mShowPreview = true;
    201     private final boolean mShowTouchPoints = true;
    202     private int mPopupPreviewX;
    203     private int mPopupPreviewY;
    204     private int mPopupScrimColor;
    205     private int mBackgroundColor;
    206 
    207     private int mLastX;
    208     private int mLastY;
    209     private int mStartX;
    210     private int mStartY;
    211 
    212     private boolean mProximityCorrectOn;
    213 
    214     private final Paint mPaint;
    215     private final Rect mPadding;
    216 
    217     private long mDownTime;
    218     private long mLastMoveTime;
    219     private int mLastKey;
    220     private int mLastCodeX;
    221     private int mLastCodeY;
    222     private int mCurrentKey = NOT_A_KEY;
    223     private int mDownKey = NOT_A_KEY;
    224     private long mLastKeyTime;
    225     private long mCurrentKeyTime;
    226     private final int[] mKeyIndices = new int[12];
    227     private GestureDetector mGestureDetector;
    228     private int mRepeatKeyIndex = NOT_A_KEY;
    229     private boolean mAbortKey;
    230     private Key mInvalidatedKey;
    231     private final Rect mClipRegion = new Rect(0, 0, 0, 0);
    232     private boolean mPossiblePoly;
    233     private final SwipeTracker mSwipeTracker = new SwipeTracker();
    234     private final int mSwipeThreshold;
    235     private final boolean mDisambiguateSwipe;
    236 
    237     // Variables for dealing with multiple pointers
    238     private int mOldPointerCount = 1;
    239     private float mOldPointerX;
    240     private float mOldPointerY;
    241 
    242     private Drawable mKeyBackground;
    243 
    244     private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
    245     private static final int REPEAT_START_DELAY = 400;
    246     private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    247 
    248     private static int MAX_NEARBY_KEYS = 12;
    249     private final int[] mDistances = new int[MAX_NEARBY_KEYS];
    250 
    251     // For multi-tap
    252     private int mLastSentIndex;
    253     private int mTapCount;
    254     private long mLastTapTime;
    255     private boolean mInMultiTap;
    256     private static final int MULTITAP_INTERVAL = 800; // milliseconds
    257     private final StringBuilder mPreviewLabel = new StringBuilder(1);
    258 
    259     /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
    260     private boolean mDrawPending;
    261     /** The dirty region in the keyboard bitmap */
    262     private final Rect mDirtyRect = new Rect();
    263     /** The keyboard bitmap for faster updates */
    264     private Bitmap mBuffer;
    265     /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
    266     private boolean mKeyboardChanged;
    267     /** The canvas for the above mutable keyboard bitmap */
    268     private Canvas mCanvas;
    269     /** The accessibility manager for accessibility support */
    270     private AccessibilityManager mAccessibilityManager;
    271 
    272     private boolean mUseSecondaryColor = true;
    273     private Locale mLocale;
    274 
    275     Handler mHandler = new KeyboardHander(this);
    276 
    277     private float mAnimateInValue;
    278     private ValueAnimator mAnimateInAnimator;
    279     private float mAnimateOutValue;
    280     private ValueAnimator mAnimateOutAnimator;
    281     private int mScrimAlpha;
    282     private ValueAnimator mScrimAlphaAnimator;
    283     private int mAnimateCenter;
    284 
    285     public KeyboardView(Context context, AttributeSet attrs) {
    286         this(context, attrs, 0);
    287     }
    288 
    289     public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
    290         super(context, attrs, defStyle);
    291         mContext = context;
    292 
    293         TypedArray a =
    294                 context.obtainStyledAttributes(
    295                         attrs, R.styleable.KeyboardView, defStyle, 0);
    296 
    297         LayoutInflater inflate =
    298                 (LayoutInflater) context
    299                         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    300 
    301         int previewLayout = 0;
    302 
    303         int n = a.getIndexCount();
    304 
    305         for (int i = 0; i < n; i++) {
    306             int attr = a.getIndex(i);
    307 
    308             switch (attr) {
    309                 case R.styleable.KeyboardView_keyBackground:
    310                     mKeyBackground = a.getDrawable(attr);
    311                     break;
    312 /*                case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
    313                     mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
    314                     break;
    315                 case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
    316                     previewLayout = a.getResourceId(attr, 0);
    317                     break;
    318                 case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
    319                     mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
    320                     break;
    321                 case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
    322                     mPreviewHeight = a.getDimensionPixelSize(attr, 80);
    323                     break; */
    324                 case R.styleable.KeyboardView_keyTextSize:
    325                     mKeyTextSize = a.getDimensionPixelSize(attr, 18);
    326                     break;
    327                 case R.styleable.KeyboardView_keyTextColorPrimary:
    328                     mKeyTextColorPrimary = a.getColor(attr, 0xFF000000);
    329                     break;
    330                 case R.styleable.KeyboardView_keyTextColorSecondary:
    331                     mKeyTextColorSecondary = a.getColor(attr, 0x99000000);
    332                     break;
    333                 case R.styleable.KeyboardView_labelTextSize:
    334                     mLabelTextSize = a.getDimensionPixelSize(attr, 14);
    335                     break;
    336                 case R.styleable.KeyboardView_fontFamily:
    337                     mFontFamily = a.getString(attr);
    338                     break;
    339                 case R.styleable.KeyboardView_textStyle:
    340                     mTextStyle = a.getInt(attr, 0);
    341                     break;
    342             }
    343         }
    344 
    345         a.recycle();
    346         a = mContext.obtainStyledAttributes(R.styleable.KeyboardView);
    347         a.recycle();
    348 
    349         mPreviewPopup = new PopupWindow(context);
    350         if (previewLayout != 0) {
    351             mPreviewText = (TextView) inflate.inflate(previewLayout, null);
    352             mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
    353             mPreviewPopup.setContentView(mPreviewText);
    354             mPreviewPopup.setBackgroundDrawable(null);
    355         } else {
    356             mShowPreview = false;
    357         }
    358 
    359         mPreviewPopup.setTouchable(false);
    360 
    361         mPopupParent = this;
    362         //mPredicting = true;
    363 
    364         mPaint = new Paint();
    365         mPaint.setAntiAlias(true);
    366         mPaint.setTextSize(mKeyTextSize);
    367         mPaint.setTextAlign(Align.CENTER);
    368         mPaint.setAlpha(MAX_ALPHA);
    369 
    370         if (mFontFamily != null) {
    371             Typeface typeface = Typeface.create(mFontFamily, mTextStyle);
    372             mPaint.setTypeface(typeface);
    373         }
    374         else {
    375             mPaint.setTypeface(Typeface.create(Typeface.DEFAULT, mTextStyle));
    376         }
    377 
    378         mPadding = new Rect(0, 0, 0, 0);
    379         mMiniKeyboardCache = new HashMap<Key,View>();
    380         mKeyBackground.getPadding(mPadding);
    381 
    382         mSwipeThreshold = (int) (500 * getResources().getDisplayMetrics().density);
    383         mDisambiguateSwipe = true;
    384 
    385         int color = getResources().getColor(R.color.car_dark_blue_grey_700);
    386         mPopupScrimColor = Color.argb(
    387                 SCRIM_ALPHA, Color.red(color), Color.green(color), Color.blue(color));
    388         mBackgroundColor = Color.TRANSPARENT;
    389 
    390         mKeyPunctuationSize =
    391                 (int) getResources().getDimension(R.dimen.keyboard_key_punctuation_height);
    392 
    393         resetMultiTap();
    394         initGestureDetector();
    395 
    396         mAnimateInValue = MAX_ANIMATION_VALUE;
    397         mAnimateInAnimator = ValueAnimator.ofFloat(MIN_ANIMATION_VALUE, MAX_ANIMATION_VALUE);
    398         AccelerateDecelerateInterpolator accInterpolator = new AccelerateDecelerateInterpolator();
    399         mAnimateInAnimator.setInterpolator(accInterpolator);
    400         mAnimateInAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    401             @Override
    402             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    403                 mAnimateInValue = (float) valueAnimator.getAnimatedValue();
    404                 invalidateAllKeys();
    405             }
    406         });
    407         mAnimateInAnimator.setDuration(ANIMATE_IN_OUT_DURATION_MS);
    408 
    409         mScrimAlpha = 0;
    410         mScrimAlphaAnimator = ValueAnimator.ofInt(0, SCRIM_ALPHA);
    411         LinearInterpolator linearInterpolator = new LinearInterpolator();
    412         mScrimAlphaAnimator.setInterpolator(linearInterpolator);
    413         mScrimAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    414             @Override
    415             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    416                 mScrimAlpha = (int) valueAnimator.getAnimatedValue();
    417                 invalidateAllKeys();
    418             }
    419         });
    420         mScrimAlphaAnimator.setDuration(ANIMATE_SCRIM_DURATION_MS);
    421 
    422         mAnimateOutValue = MIN_ANIMATION_VALUE;
    423         mAnimateOutAnimator = ValueAnimator.ofFloat(MIN_ANIMATION_VALUE, MAX_ANIMATION_VALUE);
    424         mAnimateOutAnimator.setInterpolator(accInterpolator);
    425         mAnimateOutAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    426             @Override
    427             public void onAnimationUpdate(ValueAnimator valueAnimator) {
    428                 mAnimateOutValue = (float) valueAnimator.getAnimatedValue();
    429                 invalidateAllKeys();
    430             }
    431         });
    432         mAnimateOutAnimator.setDuration(ANIMATE_IN_OUT_DURATION_MS);
    433         mAnimateOutAnimator.addListener(new Animator.AnimatorListener() {
    434             @Override
    435             public void onAnimationEnd(Animator animation) {
    436                 setVisibility(View.GONE);
    437                 mAnimateOutValue = MIN_ANIMATION_VALUE;
    438             }
    439 
    440             @Override
    441             public void onAnimationCancel(Animator animator) {
    442             }
    443 
    444             @Override
    445             public void onAnimationRepeat(Animator animator) {
    446             }
    447 
    448             @Override
    449             public void onAnimationStart(Animator animator) {
    450             }
    451         });
    452     }
    453 
    454 
    455     private void initGestureDetector() {
    456         mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
    457             @Override
    458             public boolean onFling(MotionEvent me1, MotionEvent me2,
    459                     float velocityX, float velocityY) {
    460                 if (mPossiblePoly) return false;
    461                 final float absX = Math.abs(velocityX);
    462                 final float absY = Math.abs(velocityY);
    463                 float deltaX = me2.getX() - me1.getX();
    464                 float deltaY = me2.getY() - me1.getY();
    465                 int travelX = getWidth() / 2; // Half the keyboard width
    466                 int travelY = getHeight() / 2; // Half the keyboard height
    467                 mSwipeTracker.computeCurrentVelocity(1000);
    468                 final float endingVelocityX = mSwipeTracker.getXVelocity();
    469                 final float endingVelocityY = mSwipeTracker.getYVelocity();
    470                 boolean sendDownKey = false;
    471                 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
    472                     if (mDisambiguateSwipe && endingVelocityX < velocityX / 4) {
    473                         sendDownKey = true;
    474                     } else {
    475                         swipeRight();
    476                         return true;
    477                     }
    478                 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
    479                     if (mDisambiguateSwipe && endingVelocityX > velocityX / 4) {
    480                         sendDownKey = true;
    481                     } else {
    482                         swipeLeft();
    483                         return true;
    484                     }
    485                 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
    486                     if (mDisambiguateSwipe && endingVelocityY > velocityY / 4) {
    487                         sendDownKey = true;
    488                     } else {
    489                         swipeUp();
    490                         return true;
    491                     }
    492                 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
    493                     if (mDisambiguateSwipe && endingVelocityY < velocityY / 4) {
    494                         sendDownKey = true;
    495                     } else {
    496                         swipeDown();
    497                         return true;
    498                     }
    499                 }
    500 
    501                 if (sendDownKey) {
    502                     detectAndSendKey(mDownKey, mStartX, mStartY, me1.getEventTime());
    503                 }
    504                 return false;
    505             }
    506         });
    507 
    508         mGestureDetector.setIsLongpressEnabled(false);
    509     }
    510 
    511     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
    512         mKeyboardActionListener = listener;
    513     }
    514 
    515     /**
    516      * Returns the {@link OnKeyboardActionListener} object.
    517      * @return the listener attached to this keyboard
    518      */
    519     protected OnKeyboardActionListener getOnKeyboardActionListener() {
    520         return mKeyboardActionListener;
    521     }
    522 
    523     /**
    524      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
    525      * view will re-layout itself to accommodate the keyboard.
    526      * @see android.inputmethodservice.Keyboard
    527      * @see #getKeyboard()
    528      * @param keyboard the keyboard to display in this view
    529      */
    530     public void setKeyboard(Keyboard keyboard, Locale locale) {
    531         if (mKeyboard != null) {
    532             showPreview(NOT_A_KEY);
    533         }
    534         // Remove any pending messages
    535         removeMessages();
    536         mKeyboard = keyboard;
    537         List<Key> keys = mKeyboard.getKeys();
    538         mKeys = keys.toArray(new Key[keys.size()]);
    539         requestLayout();
    540         // Hint to reallocate the buffer if the size changed
    541         mKeyboardChanged = true;
    542         invalidateAllKeys();
    543         computeProximityThreshold(keyboard);
    544         mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
    545         // Switching to a different keyboard should abort any pending keys so that the key up
    546         // doesn't get delivered to the old or new keyboard
    547         mAbortKey = true; // Until the next ACTION_DOWN
    548         mLocale = locale;
    549     }
    550 
    551     /**
    552      * Returns the current keyboard being displayed by this view.
    553      * @return the currently attached keyboard
    554      * @see #setKeyboard(android.inputmethodservice.Keyboard, java.util.Locale)
    555      */
    556     public Keyboard getKeyboard() {
    557         return mKeyboard;
    558     }
    559 
    560     /**
    561      * Sets the state of the shift key of the keyboard, if any.
    562      * @param shifted whether or not to enable the state of the shift key
    563      * @return true if the shift key state changed, false if there was no change
    564      * @see KeyboardView#isShifted()
    565      */
    566     public boolean setShifted(boolean shifted) {
    567         if (mKeyboard != null) {
    568             if (mKeyboard.setShifted(shifted)) {
    569                 // The whole keyboard probably needs to be redrawn
    570                 invalidateAllKeys();
    571                 return true;
    572             }
    573         }
    574         return false;
    575     }
    576 
    577     /**
    578      * Returns the state of the shift key of the keyboard, if any.
    579      * @return true if the shift is in a pressed state, false otherwise. If there is
    580      * no shift key on the keyboard or there is no keyboard attached, it returns false.
    581      * @see KeyboardView#setShifted(boolean)
    582      */
    583     public boolean isShifted() {
    584         if (mKeyboard != null) {
    585             return mKeyboard.isShifted();
    586         }
    587         return false;
    588     }
    589 
    590     /**
    591      * Enables or disables the key feedback popup. This is a popup that shows a magnified
    592      * version of the depressed key. By default the preview is enabled.
    593      * @param previewEnabled whether or not to enable the key feedback popup
    594      * @see #isPreviewEnabled()
    595      */
    596     public void setPreviewEnabled(boolean previewEnabled) {
    597         mShowPreview = previewEnabled;
    598     }
    599 
    600     /**
    601      * Returns the enabled state of the key feedback popup.
    602      * @return whether or not the key feedback popup is enabled
    603      * @see #setPreviewEnabled(boolean)
    604      */
    605     public boolean isPreviewEnabled() {
    606         return mShowPreview;
    607     }
    608 
    609     public void setPopupParent(View v) {
    610         mPopupParent = v;
    611     }
    612 
    613     public void setPopupOffset(int x, int y) {
    614         mMiniKeyboardOffsetX = x;
    615         mMiniKeyboardOffsetY = y;
    616         if (mPreviewPopup.isShowing()) {
    617             mPreviewPopup.dismiss();
    618         }
    619     }
    620 
    621     /**
    622      * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
    623      * codes for adjacent keys.  When disabled, only the primary key code will be
    624      * reported.
    625      * @param enabled whether or not the proximity correction is enabled
    626      */
    627     public void setProximityCorrectionEnabled(boolean enabled) {
    628         mProximityCorrectOn = enabled;
    629     }
    630 
    631     /**
    632      * Returns true if proximity correction is enabled.
    633      */
    634     public boolean isProximityCorrectionEnabled() {
    635         return mProximityCorrectOn;
    636     }
    637 
    638     public boolean getUseSecondaryColor() {
    639         return mUseSecondaryColor;
    640     }
    641 
    642     public void setUseSecondaryColor(boolean useSecondaryColor) {
    643         mUseSecondaryColor = useSecondaryColor;
    644     }
    645 
    646     /**
    647      * Popup keyboard close button clicked.
    648      * @hide
    649      */
    650     @Override
    651     public void onClick(View v) {
    652         dismissPopupKeyboard();
    653     }
    654 
    655     private CharSequence adjustCase(CharSequence label) {
    656         if (mKeyboard.isShifted() && label != null && label.length() < 3
    657                 && Character.isLowerCase(label.charAt(0))) {
    658             label = label.toString().toUpperCase(mLocale);
    659         }
    660         return label;
    661     }
    662 
    663     @Override
    664     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    665         // Round up a little
    666         if (mKeyboard == null) {
    667             setMeasuredDimension(getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
    668         } else {
    669             int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
    670             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
    671                 width = MeasureSpec.getSize(widthMeasureSpec);
    672             }
    673             setMeasuredDimension(width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
    674         }
    675     }
    676 
    677     /**
    678      * Compute the average distance between adjacent keys (horizontally and vertically)
    679      * and square it to get the proximity threshold. We use a square here and in computing
    680      * the touch distance from a key's center to avoid taking a square root.
    681      * @param keyboard
    682      */
    683     private void computeProximityThreshold(Keyboard keyboard) {
    684         if (keyboard == null) return;
    685         final Key[] keys = mKeys;
    686         if (keys == null) return;
    687         int length = keys.length;
    688         int dimensionSum = 0;
    689         for (int i = 0; i < length; i++) {
    690             Key key = keys[i];
    691             dimensionSum += Math.min(key.width, key.height) + key.gap;
    692         }
    693         if (dimensionSum < 0 || length == 0) return;
    694         mProximityThreshold = (int) (dimensionSum * 1.4f / length);
    695         mProximityThreshold *= mProximityThreshold; // Square it
    696     }
    697 
    698     @Override
    699     public void onSizeChanged(int w, int h, int oldw, int oldh) {
    700         super.onSizeChanged(w, h, oldw, oldh);
    701         if (mKeyboard != null) {
    702             //TODO(ftamp): mKeyboard.resize(w, h);
    703         }
    704         // Release the buffer, if any and it will be reallocated on the next draw
    705         mBuffer = null;
    706     }
    707 
    708     @Override
    709     public void onDraw(Canvas canvas) {
    710         super.onDraw(canvas);
    711         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
    712             onBufferDraw();
    713         }
    714         canvas.drawBitmap(mBuffer, 0, 0, null);
    715     }
    716 
    717     @SuppressWarnings("unused")
    718     private void onBufferDraw() {
    719         if (mBuffer == null || mKeyboardChanged) {
    720             if (mBuffer == null || mKeyboardChanged &&
    721                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
    722                 // Make sure our bitmap is at least 1x1
    723                 final int width = Math.max(1, getWidth());
    724                 final int height = Math.max(1, getHeight());
    725                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    726                 mCanvas = new Canvas(mBuffer);
    727             }
    728             invalidateAllKeys();
    729             mKeyboardChanged = false;
    730         }
    731         final Canvas canvas = mCanvas;
    732         canvas.clipRect(mDirtyRect, Op.REPLACE);
    733 
    734         if (mKeyboard == null) return;
    735 
    736         final Paint paint = mPaint;
    737         final Drawable keyBackground = mKeyBackground;
    738         final Rect clipRegion = mClipRegion;
    739         final Rect padding = mPadding;
    740         final int kbdPaddingLeft = getPaddingLeft();
    741         final int kbdPaddingTop = getPaddingTop();
    742         final Key[] keys = mKeys;
    743         final Key invalidKey = mInvalidatedKey;
    744 
    745         paint.setColor(mKeyTextColorPrimary);
    746         boolean drawSingleKey = false;
    747         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
    748             // Is clipRegion completely contained within the invalidated key?
    749             if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
    750                     invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
    751                     invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
    752                     invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
    753                 drawSingleKey = true;
    754             }
    755         }
    756         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
    757 
    758         // Animate in translation. No overall translation for animating out.
    759         float offset = getWidth()/2 * (1 - mAnimateInValue);
    760         canvas.translate(offset, 0);
    761 
    762         // Draw background.
    763         float backgroundWidth = getWidth() * (mAnimateInValue - mAnimateOutValue);
    764         paint.setColor(mBackgroundColor);
    765         int center;
    766         if (mLastSentIndex != NOT_A_KEY && mLastSentIndex < keys.length) {
    767             center = keys[mLastSentIndex].x + keys[mLastSentIndex].width / 2;
    768         } else {
    769             center = getWidth()/2;
    770         }
    771         canvas.drawRect(mAnimateOutValue * center, 0, mAnimateOutValue * center + backgroundWidth,
    772                 getHeight(), paint);
    773         final int keyCount = keys.length;
    774         for (int i = 0; i < keyCount; i++) {
    775             final Key key = keys[i];
    776             if (drawSingleKey && invalidKey != key) {
    777                 continue;
    778             }
    779             int[] drawableState = key.getCurrentDrawableState();
    780             keyBackground.setState(drawableState);
    781             if (key.icon != null) {
    782                 key.icon.setState(drawableState);
    783             }
    784 
    785 
    786             // Switch the character to uppercase if shift is pressed
    787             String label = key.label == null? null : adjustCase(key.label).toString();
    788 
    789             final Rect bounds = keyBackground.getBounds();
    790             if (key.width != bounds.right ||
    791                     key.height != bounds.bottom) {
    792                 keyBackground.setBounds(0, 0, key.width, key.height);
    793             }
    794             float animateOutOffset = 0;
    795             if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
    796                 animateOutOffset = (center - key.x - key.width/2) * mAnimateOutValue;
    797             }
    798             canvas.translate(key.x + kbdPaddingLeft + animateOutOffset, key.y + kbdPaddingTop);
    799             keyBackground.draw(canvas);
    800 
    801             if (label != null && label.length() > 0) {
    802                 // Use primary color for letters and digits, secondary color for everything else
    803                 if (Character.isLetterOrDigit(label.charAt(0))) {
    804                     paint.setColor(mKeyTextColorPrimary);
    805                 } else if (mUseSecondaryColor) {
    806                     paint.setColor(mKeyTextColorSecondary);
    807                 }
    808                 // For characters, use large font. For labels like "Done", use small font.
    809                 if (label.length() > 1 && key.codes.length < 2) {
    810                     paint.setTextSize(mLabelTextSize);
    811                 } else if (isPunctuation(label)) {
    812                     paint.setTextSize(mKeyPunctuationSize);
    813                 } else {
    814                     paint.setTextSize(mKeyTextSize);
    815                 }
    816                 if (mAnimateInValue != MAX_ANIMATION_VALUE) {
    817                     int alpha =
    818                             (int) ((backgroundWidth - key.x - key.width) / key.width * MAX_ALPHA);
    819                     if (alpha > MAX_ALPHA) {
    820                         alpha = MAX_ALPHA;
    821                     } else if (alpha < 0) {
    822                         alpha = 0;
    823                     }
    824                     paint.setAlpha(alpha);
    825                 } else if (mAnimateOutValue != MIN_ANIMATION_VALUE) {
    826                     int alpha;
    827                     if (i == mLastSentIndex) {
    828                         // Fade out selected character from 1/2 to 3/4 of animate out.
    829                         alpha = (int) ((3 - mAnimateOutValue * 4) * MAX_ALPHA);
    830                     } else {
    831                         // Fade out the rest from start to 1/2 of animate out.
    832                         alpha = (int) ((1 - mAnimateOutValue * 2) * MAX_ALPHA);
    833                     }
    834                     if (alpha > MAX_ALPHA) {
    835                         alpha = MAX_ALPHA;
    836                     } else if (alpha < 0) {
    837                         alpha = 0;
    838                     }
    839                     paint.setAlpha(alpha);
    840                 }
    841                 // Draw the text
    842                 canvas.drawText(label,
    843                         (key.width - padding.left - padding.right) / 2
    844                                 + padding.left,
    845                         (key.height - padding.top - padding.bottom) / 2
    846                                 + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
    847                         paint);
    848                 // Turn off drop shadow
    849                 paint.setShadowLayer(0, 0, 0, 0);
    850             } else if (key.icon != null) {
    851                 final int drawableX = (key.width - padding.left - padding.right
    852                         - key.icon.getIntrinsicWidth()) / 2 + padding.left;
    853                 final int drawableY = (key.height - padding.top - padding.bottom
    854                         - key.icon.getIntrinsicHeight()) / 2 + padding.top;
    855                 canvas.translate(drawableX, drawableY);
    856                 key.icon.setBounds(0, 0,
    857                         key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
    858                 key.icon.draw(canvas);
    859                 canvas.translate(-drawableX, -drawableY);
    860             }
    861             canvas.translate(-key.x - kbdPaddingLeft - animateOutOffset, -key.y - kbdPaddingTop);
    862         }
    863         mInvalidatedKey = null;
    864         // Overlay a dark rectangle to dim the keyboard
    865         paint.setColor(mPopupScrimColor);
    866         paint.setAlpha(mScrimAlpha);
    867         canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
    868         canvas.translate(-offset, 0);
    869 
    870         if (DEBUG && mShowTouchPoints) {
    871             paint.setAlpha(128);
    872             paint.setColor(0xFFFF0000);
    873             canvas.drawCircle(mStartX, mStartY, 3, paint);
    874             canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
    875             paint.setColor(0xFF0000FF);
    876             canvas.drawCircle(mLastX, mLastY, 3, paint);
    877             paint.setColor(0xFF00FF00);
    878             canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
    879         }
    880 
    881         mDrawPending = false;
    882         mDirtyRect.setEmpty();
    883     }
    884 
    885     private boolean isPunctuation(String label) {
    886         return PUNCTUATION_PATTERN.matcher(label).matches();
    887     }
    888 
    889     private int getKeyIndices(int x, int y, int[] allKeys) {
    890         final Key[] keys = mKeys;
    891         int primaryIndex = NOT_A_KEY;
    892         int closestKey = NOT_A_KEY;
    893         int closestKeyDist = mProximityThreshold + 1;
    894         Arrays.fill(mDistances, Integer.MAX_VALUE);
    895         int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
    896         final int keyCount = nearestKeyIndices.length;
    897         for (int i = 0; i < keyCount; i++) {
    898             final Key key = keys[nearestKeyIndices[i]];
    899             int dist = 0;
    900             boolean isInside = key.isInside(x,y);
    901             if (isInside) {
    902                 primaryIndex = nearestKeyIndices[i];
    903             }
    904 
    905             if (((mProximityCorrectOn
    906                     && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
    907                     || isInside)
    908                     && key.codes[0] > 32) {
    909                 // Find insertion point
    910                 final int nCodes = key.codes.length;
    911                 if (dist < closestKeyDist) {
    912                     closestKeyDist = dist;
    913                     closestKey = nearestKeyIndices[i];
    914                 }
    915 
    916                 if (allKeys == null) continue;
    917 
    918                 for (int j = 0; j < mDistances.length; j++) {
    919                     if (mDistances[j] > dist) {
    920                         // Make space for nCodes codes
    921                         System.arraycopy(mDistances, j, mDistances, j + nCodes,
    922                                 mDistances.length - j - nCodes);
    923                         System.arraycopy(allKeys, j, allKeys, j + nCodes,
    924                                 allKeys.length - j - nCodes);
    925                         for (int c = 0; c < nCodes; c++) {
    926                             allKeys[j + c] = key.codes[c];
    927                             mDistances[j + c] = dist;
    928                         }
    929                         break;
    930                     }
    931                 }
    932             }
    933         }
    934         if (primaryIndex == NOT_A_KEY) {
    935             primaryIndex = closestKey;
    936         }
    937         return primaryIndex;
    938     }
    939 
    940     private void detectAndSendKey(int index, int x, int y, long eventTime) {
    941         if (index != NOT_A_KEY && index < mKeys.length) {
    942             final Key key = mKeys[index];
    943             if (key.text != null) {
    944                 mKeyboardActionListener.onText(key.text);
    945                 mKeyboardActionListener.onRelease(NOT_A_KEY);
    946             } else {
    947                 int code = key.codes[0];
    948                 //TextEntryState.keyPressedAt(key, x, y);
    949                 int[] codes = new int[MAX_NEARBY_KEYS];
    950                 Arrays.fill(codes, NOT_A_KEY);
    951                 getKeyIndices(x, y, codes);
    952                 // Multi-tap
    953                 if (mInMultiTap) {
    954                     if (mTapCount != -1) {
    955                         mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
    956                     } else {
    957                         mTapCount = 0;
    958                     }
    959                     code = key.codes[mTapCount];
    960                 }
    961                 mKeyboardActionListener.onKey(code, codes);
    962                 mKeyboardActionListener.onRelease(code);
    963             }
    964             mLastSentIndex = index;
    965             mLastTapTime = eventTime;
    966         }
    967     }
    968 
    969     /**
    970      * Handle multi-tap keys by producing the key label for the current multi-tap state.
    971      */
    972     private CharSequence getPreviewText(Key key) {
    973         if (mInMultiTap) {
    974             // Multi-tap
    975             mPreviewLabel.setLength(0);
    976             mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
    977             return adjustCase(mPreviewLabel);
    978         } else {
    979             return adjustCase(key.label);
    980         }
    981     }
    982 
    983     private void showPreview(int keyIndex) {
    984         int oldKeyIndex = mCurrentKeyIndex;
    985         final PopupWindow previewPopup = mPreviewPopup;
    986 
    987         mCurrentKeyIndex = keyIndex;
    988         // Release the old key and press the new key
    989         final Key[] keys = mKeys;
    990         if (oldKeyIndex != mCurrentKeyIndex) {
    991             if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
    992                 Key oldKey = keys[oldKeyIndex];
    993                 // Keyboard.Key.onReleased(boolean) should be called here, but due to b/21446448,
    994                 // onReleased doesn't check the input param and it always toggles the on state.
    995                 // Therefore we need to just set the pressed state to false here.
    996                 oldKey.pressed = false;
    997                 invalidateKey(oldKeyIndex);
    998             }
    999             if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
   1000                 Key newKey = keys[mCurrentKeyIndex];
   1001                 newKey.onPressed();
   1002                 invalidateKey(mCurrentKeyIndex);
   1003             }
   1004         }
   1005         // If key changed and preview is on ...
   1006         if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
   1007             mHandler.removeMessages(MSG_SHOW_PREVIEW);
   1008             if (previewPopup.isShowing()) {
   1009                 if (keyIndex == NOT_A_KEY) {
   1010                     mHandler.sendMessageDelayed(mHandler
   1011                                     .obtainMessage(MSG_REMOVE_PREVIEW),
   1012                             DELAY_AFTER_PREVIEW);
   1013                 }
   1014             }
   1015             if (keyIndex != NOT_A_KEY) {
   1016                 if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
   1017                     // Show right away, if it's already visible and finger is moving around
   1018                     showKey(keyIndex);
   1019                 } else {
   1020                     mHandler.sendMessageDelayed(
   1021                             mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
   1022                             DELAY_BEFORE_PREVIEW);
   1023                 }
   1024             }
   1025         }
   1026     }
   1027 
   1028     private void showKey(final int keyIndex) {
   1029         final PopupWindow previewPopup = mPreviewPopup;
   1030         final Key[] keys = mKeys;
   1031         if (keyIndex < 0 || keyIndex >= mKeys.length) return;
   1032         Key key = keys[keyIndex];
   1033         if (key.icon != null) {
   1034             mPreviewText.setCompoundDrawables(null, null, null,
   1035                     key.iconPreview != null ? key.iconPreview : key.icon);
   1036             mPreviewText.setText(null);
   1037         } else {
   1038             mPreviewText.setCompoundDrawables(null, null, null, null);
   1039             mPreviewText.setText(getPreviewText(key));
   1040             if (key.label.length() > 1 && key.codes.length < 2) {
   1041                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
   1042                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
   1043             } else {
   1044                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
   1045                 mPreviewText.setTypeface(Typeface.DEFAULT);
   1046             }
   1047         }
   1048         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
   1049                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
   1050         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
   1051                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
   1052         final int popupHeight = mPreviewHeight;
   1053         LayoutParams lp = mPreviewText.getLayoutParams();
   1054         if (lp != null) {
   1055             lp.width = popupWidth;
   1056             lp.height = popupHeight;
   1057         }
   1058         if (!mPreviewCentered) {
   1059             mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + getPaddingLeft();
   1060             mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
   1061         } else {
   1062             // TODO: Fix this if centering is brought back
   1063             mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
   1064             mPopupPreviewY = - mPreviewText.getMeasuredHeight();
   1065         }
   1066         mHandler.removeMessages(MSG_REMOVE_PREVIEW);
   1067         getLocationInWindow(mCoordinates);
   1068         mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
   1069         mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
   1070 
   1071         // Set the preview background state
   1072         /*
   1073         mPreviewText.getBackground().setState(
   1074                  key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
   1075         */
   1076         mPopupPreviewX += mCoordinates[0];
   1077         mPopupPreviewY += mCoordinates[1];
   1078 
   1079         // If the popup cannot be shown above the key, put it on the side
   1080         getLocationOnScreen(mCoordinates);
   1081         if (mPopupPreviewY + mCoordinates[1] < 0) {
   1082             // If the key you're pressing is on the left side of the keyboard, show the popup on
   1083             // the right, offset by enough to see at least one key to the left/right.
   1084             if (key.x + key.width <= getWidth() / 2) {
   1085                 mPopupPreviewX += (int) (key.width * 2.5);
   1086             } else {
   1087                 mPopupPreviewX -= (int) (key.width * 2.5);
   1088             }
   1089             mPopupPreviewY += popupHeight;
   1090         }
   1091 
   1092         if (previewPopup.isShowing()) {
   1093             previewPopup.update(mPopupPreviewX, mPopupPreviewY,
   1094                     popupWidth, popupHeight);
   1095         } else {
   1096             previewPopup.setWidth(popupWidth);
   1097             previewPopup.setHeight(popupHeight);
   1098             previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
   1099                     mPopupPreviewX, mPopupPreviewY);
   1100         }
   1101         mPreviewText.setVisibility(VISIBLE);
   1102     }
   1103 
   1104     /**
   1105      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
   1106      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
   1107      * draws the cached buffer.
   1108      * @see #invalidateKey(int)
   1109      */
   1110     public void invalidateAllKeys() {
   1111         mDirtyRect.union(0, 0, getWidth(), getHeight());
   1112         mDrawPending = true;
   1113         invalidate();
   1114     }
   1115 
   1116     /**
   1117      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
   1118      * one key is changing it's content. Any changes that affect the position or size of the key
   1119      * may not be honored.
   1120      * @param keyIndex the index of the key in the attached {@link android.inputmethodservice.Keyboard}.
   1121      * @see #invalidateAllKeys
   1122      */
   1123     public void invalidateKey(int keyIndex) {
   1124         if (mKeys == null) return;
   1125         if (keyIndex < 0 || keyIndex >= mKeys.length) {
   1126             return;
   1127         }
   1128         final Key key = mKeys[keyIndex];
   1129         mInvalidatedKey = key;
   1130         mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
   1131                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
   1132         onBufferDraw();
   1133         invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
   1134                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
   1135     }
   1136 
   1137     private void openPopupIfRequired(MotionEvent unused) {
   1138         // Check if we have a popup keyboard first.
   1139         if (mPopupKeyboardView == null || mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
   1140             return;
   1141         }
   1142 
   1143         Key popupKey = mKeys[mCurrentKey];
   1144         boolean result = onLongPress(popupKey);
   1145         if (result) {
   1146             mAbortKey = true;
   1147             showPreview(NOT_A_KEY);
   1148         }
   1149     }
   1150 
   1151     /**
   1152      * Called when a key is long pressed. By default this will open any popup keyboard associated
   1153      * with this key through the attributes popupLayout and popupCharacters.
   1154      *
   1155      * @param popupKey the key that was long pressed
   1156      * @return true if the long press is handled, false otherwise. Subclasses should call the
   1157      * method on the base class if the subclass doesn't wish to handle the call.
   1158      */
   1159     protected boolean onLongPress(Key popupKey) {
   1160         int popupKeyboardId = popupKey.popupResId;
   1161 
   1162         if (popupKeyboardId != 0) {
   1163             Keyboard keyboard;
   1164             if (popupKey.popupCharacters != null) {
   1165                 keyboard = new Keyboard(getContext(), popupKeyboardId,
   1166                         popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
   1167             } else {
   1168                 keyboard = new Keyboard(getContext(), popupKeyboardId);
   1169             }
   1170             mPopupKeyboardView.setKeyboard(keyboard, mLocale);
   1171             mPopupKeyboardView.setVisibility(VISIBLE);
   1172             mPopupKeyboardView.setShifted(isShifted());
   1173             mPopupKeyboardView.mAnimateInAnimator.start();
   1174             mPopupKeyboardView.mLastSentIndex = NOT_A_KEY;
   1175             mScrimAlphaAnimator.setStartDelay(0);
   1176             mScrimAlphaAnimator.start();
   1177             mPopupKeyboardView.invalidate();
   1178             mPopupKeyboardOnScreen = true;
   1179             invalidateAllKeys();
   1180             return true;
   1181         }
   1182         return false;
   1183     }
   1184 
   1185     @Override
   1186     public boolean onHoverEvent(MotionEvent event) {
   1187         if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
   1188             final int action = event.getAction();
   1189             switch (action) {
   1190                 case MotionEvent.ACTION_HOVER_ENTER: {
   1191                     event.setAction(MotionEvent.ACTION_DOWN);
   1192                 } break;
   1193                 case MotionEvent.ACTION_HOVER_MOVE: {
   1194                     event.setAction(MotionEvent.ACTION_MOVE);
   1195                 } break;
   1196                 case MotionEvent.ACTION_HOVER_EXIT: {
   1197                     event.setAction(MotionEvent.ACTION_UP);
   1198                 } break;
   1199             }
   1200             return onTouchEvent(event);
   1201         }
   1202         return true;
   1203     }
   1204 
   1205     @SuppressLint("ClickableViewAccessibility")
   1206     @Override
   1207     public boolean onTouchEvent(MotionEvent me) {
   1208         // Don't process touches while animating.
   1209         if (mAnimateInValue != MAX_ANIMATION_VALUE || mAnimateOutValue != MIN_ANIMATION_VALUE) {
   1210             return false;
   1211         }
   1212 
   1213         // Convert multi-pointer up/down events to single up/down events to
   1214         // deal with the typical multi-pointer behavior of two-thumb typing
   1215         final int pointerCount = me.getPointerCount();
   1216         final int action = me.getAction();
   1217         boolean result = false;
   1218         final long now = me.getEventTime();
   1219 
   1220         if (pointerCount != mOldPointerCount) {
   1221             if (pointerCount == 1) {
   1222                 // Send a down event for the latest pointer
   1223                 MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
   1224                         me.getX(), me.getY(), me.getMetaState());
   1225                 result = onModifiedTouchEvent(down, false);
   1226                 down.recycle();
   1227                 // If it's an up action, then deliver the up as well.
   1228                 if (action == MotionEvent.ACTION_UP) {
   1229                     result = onModifiedTouchEvent(me, true);
   1230                 }
   1231             } else {
   1232                 // Send an up event for the last pointer
   1233                 MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
   1234                         mOldPointerX, mOldPointerY, me.getMetaState());
   1235                 result = onModifiedTouchEvent(up, true);
   1236                 up.recycle();
   1237             }
   1238         } else {
   1239             if (pointerCount == 1) {
   1240                 result = onModifiedTouchEvent(me, false);
   1241                 mOldPointerX = me.getX();
   1242                 mOldPointerY = me.getY();
   1243             } else {
   1244                 // Don't do anything when 2 pointers are down and moving.
   1245                 result = true;
   1246             }
   1247         }
   1248         mOldPointerCount = pointerCount;
   1249 
   1250         return result;
   1251     }
   1252 
   1253     private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
   1254         int touchX = (int) me.getX() - getPaddingLeft();
   1255         int touchY = (int) me.getY() - getPaddingTop();
   1256         if (touchY >= -mVerticalCorrection)
   1257             touchY += mVerticalCorrection;
   1258         final int action = me.getAction();
   1259         final long eventTime = me.getEventTime();
   1260         int keyIndex = getKeyIndices(touchX, touchY, null);
   1261         mPossiblePoly = possiblePoly;
   1262 
   1263         // Track the last few movements to look for spurious swipes.
   1264         if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
   1265         mSwipeTracker.addMovement(me);
   1266 
   1267         // Ignore all motion events until a DOWN.
   1268         if (mAbortKey
   1269                 && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
   1270             return true;
   1271         }
   1272 
   1273         if (mGestureDetector.onTouchEvent(me)) {
   1274             showPreview(NOT_A_KEY);
   1275             mHandler.removeMessages(MSG_REPEAT);
   1276             mHandler.removeMessages(MSG_LONGPRESS);
   1277             return true;
   1278         }
   1279 
   1280         if (mPopupKeyboardOnScreen) {
   1281             dismissPopupKeyboard();
   1282             return true;
   1283         }
   1284 
   1285         switch (action) {
   1286             case MotionEvent.ACTION_DOWN:
   1287                 mAbortKey = false;
   1288                 mStartX = touchX;
   1289                 mStartY = touchY;
   1290                 mLastCodeX = touchX;
   1291                 mLastCodeY = touchY;
   1292                 mLastKeyTime = 0;
   1293                 mCurrentKeyTime = 0;
   1294                 mLastKey = NOT_A_KEY;
   1295                 mCurrentKey = keyIndex;
   1296                 mDownKey = keyIndex;
   1297                 mDownTime = me.getEventTime();
   1298                 mLastMoveTime = mDownTime;
   1299                 checkMultiTap(eventTime, keyIndex);
   1300                 if (keyIndex != NOT_A_KEY) {
   1301                     mKeyboardActionListener.onPress(mKeys[keyIndex].codes[0]);
   1302                 }
   1303                 if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
   1304                     mRepeatKeyIndex = mCurrentKey;
   1305                     Message msg = mHandler.obtainMessage(MSG_REPEAT);
   1306                     mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
   1307                     repeatKey();
   1308                     // Delivering the key could have caused an abort
   1309                     if (mAbortKey) {
   1310                         mRepeatKeyIndex = NOT_A_KEY;
   1311                         break;
   1312                     }
   1313                 }
   1314                 if (mCurrentKey != NOT_A_KEY) {
   1315                     Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
   1316                     mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
   1317                 }
   1318                 showPreview(keyIndex);
   1319                 break;
   1320 
   1321             case MotionEvent.ACTION_MOVE:
   1322                 boolean continueLongPress = false;
   1323                 if (keyIndex != NOT_A_KEY) {
   1324                     if (mCurrentKey == NOT_A_KEY) {
   1325                         mCurrentKey = keyIndex;
   1326                         mCurrentKeyTime = eventTime - mDownTime;
   1327                     } else {
   1328                         if (keyIndex == mCurrentKey) {
   1329                             mCurrentKeyTime += eventTime - mLastMoveTime;
   1330                             continueLongPress = true;
   1331                         } else if (mRepeatKeyIndex == NOT_A_KEY) {
   1332                             resetMultiTap();
   1333                             mLastKey = mCurrentKey;
   1334                             mLastCodeX = mLastX;
   1335                             mLastCodeY = mLastY;
   1336                             mLastKeyTime =
   1337                                     mCurrentKeyTime + eventTime - mLastMoveTime;
   1338                             mCurrentKey = keyIndex;
   1339                             mCurrentKeyTime = 0;
   1340                         }
   1341                     }
   1342                 }
   1343                 if (!continueLongPress) {
   1344                     // Cancel old longpress
   1345                     mHandler.removeMessages(MSG_LONGPRESS);
   1346                     // Start new longpress if key has changed
   1347                     if (keyIndex != NOT_A_KEY) {
   1348                         Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
   1349                         mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
   1350                     }
   1351                 }
   1352                 showPreview(mCurrentKey);
   1353                 mLastMoveTime = eventTime;
   1354                 break;
   1355 
   1356             case MotionEvent.ACTION_UP:
   1357                 removeMessages();
   1358                 if (keyIndex == mCurrentKey) {
   1359                     mCurrentKeyTime += eventTime - mLastMoveTime;
   1360                 } else {
   1361                     resetMultiTap();
   1362                     mLastKey = mCurrentKey;
   1363                     mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
   1364                     mCurrentKey = keyIndex;
   1365                     mCurrentKeyTime = 0;
   1366                 }
   1367                 if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
   1368                         && mLastKey != NOT_A_KEY) {
   1369                     mCurrentKey = mLastKey;
   1370                     touchX = mLastCodeX;
   1371                     touchY = mLastCodeY;
   1372                 }
   1373                 showPreview(NOT_A_KEY);
   1374                 Arrays.fill(mKeyIndices, NOT_A_KEY);
   1375                 // If we're not on a repeating key (which sends on a DOWN event)
   1376                 if (mRepeatKeyIndex == NOT_A_KEY && !mPopupKeyboardOnScreen && !mAbortKey) {
   1377                     detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
   1378                 }
   1379                 invalidateKey(keyIndex);
   1380                 mRepeatKeyIndex = NOT_A_KEY;
   1381                 break;
   1382             case MotionEvent.ACTION_CANCEL:
   1383                 removeMessages();
   1384                 dismissPopupKeyboard();
   1385                 mAbortKey = true;
   1386                 showPreview(NOT_A_KEY);
   1387                 invalidateKey(mCurrentKey);
   1388                 break;
   1389         }
   1390         mLastX = touchX;
   1391         mLastY = touchY;
   1392         return true;
   1393     }
   1394 
   1395     private boolean repeatKey() {
   1396         Key key = mKeys[mRepeatKeyIndex];
   1397         detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
   1398         return true;
   1399     }
   1400 
   1401     protected void swipeRight() {
   1402         mKeyboardActionListener.swipeRight();
   1403     }
   1404 
   1405     protected void swipeLeft() {
   1406         mKeyboardActionListener.swipeLeft();
   1407     }
   1408 
   1409     protected void swipeUp() {
   1410         mKeyboardActionListener.swipeUp();
   1411     }
   1412 
   1413     protected void swipeDown() {
   1414         mKeyboardActionListener.swipeDown();
   1415     }
   1416 
   1417     public void closing() {
   1418         if (mPreviewPopup.isShowing()) {
   1419             mPreviewPopup.dismiss();
   1420         }
   1421         removeMessages();
   1422 
   1423         dismissPopupKeyboard();
   1424         mBuffer = null;
   1425         mCanvas = null;
   1426         mMiniKeyboardCache.clear();
   1427     }
   1428 
   1429     private void removeMessages() {
   1430         mHandler.removeMessages(MSG_REPEAT);
   1431         mHandler.removeMessages(MSG_LONGPRESS);
   1432         mHandler.removeMessages(MSG_SHOW_PREVIEW);
   1433     }
   1434 
   1435     @Override
   1436     public void onDetachedFromWindow() {
   1437         super.onDetachedFromWindow();
   1438         closing();
   1439     }
   1440 
   1441     public void dismissPopupKeyboard() {
   1442         mPopupKeyboardOnScreen = false;
   1443         if (mPopupKeyboardView != null && mPopupKeyboardView.getVisibility() == View.VISIBLE) {
   1444             if (mPopupKeyboardView.mAnimateInValue == MAX_ANIMATION_VALUE) {
   1445                 mPopupKeyboardView.mAnimateOutAnimator.start();
   1446                 mScrimAlphaAnimator.setStartDelay(ANIMATE_SCRIM_DELAY_MS);
   1447                 mScrimAlphaAnimator.reverse();
   1448             } else {
   1449                 mPopupKeyboardView.mAnimateInAnimator.reverse();
   1450                 mScrimAlphaAnimator.setStartDelay(0);
   1451                 mScrimAlphaAnimator.reverse();
   1452             }
   1453         }
   1454 
   1455         invalidateAllKeys();
   1456     }
   1457 
   1458     public void setPopupKeyboardView(KeyboardView popupKeyboardView) {
   1459         mPopupKeyboardView = popupKeyboardView;
   1460         mPopupKeyboardView.mBackgroundColor = getResources().getColor(R.color.car_teal_700);
   1461     }
   1462 
   1463     private void resetMultiTap() {
   1464         mLastSentIndex = NOT_A_KEY;
   1465         mTapCount = 0;
   1466         mLastTapTime = -1;
   1467         mInMultiTap = false;
   1468     }
   1469 
   1470     private void checkMultiTap(long eventTime, int keyIndex) {
   1471         if (keyIndex == NOT_A_KEY) return;
   1472         Key key = mKeys[keyIndex];
   1473         if (key.codes.length > 1) {
   1474             mInMultiTap = true;
   1475             if (eventTime < mLastTapTime + MULTITAP_INTERVAL
   1476                     && keyIndex == mLastSentIndex) {
   1477                 mTapCount = (mTapCount + 1) % key.codes.length;
   1478                 return;
   1479             } else {
   1480                 mTapCount = -1;
   1481                 return;
   1482             }
   1483         }
   1484         if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
   1485             resetMultiTap();
   1486         }
   1487     }
   1488 
   1489     @Override
   1490     public boolean onGenericMotionEvent(MotionEvent event) {
   1491         // Close the touch keyboard when the user scrolls.
   1492         if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
   1493             mKeyboardActionListener.stopInput();
   1494             return true;
   1495         }
   1496         return false;
   1497     }
   1498 
   1499     private static class SwipeTracker {
   1500 
   1501         static final int NUM_PAST = 4;
   1502         static final int LONGEST_PAST_TIME = 200;
   1503 
   1504         final float mPastX[] = new float[NUM_PAST];
   1505         final float mPastY[] = new float[NUM_PAST];
   1506         final long mPastTime[] = new long[NUM_PAST];
   1507 
   1508         float mYVelocity;
   1509         float mXVelocity;
   1510 
   1511         public void clear() {
   1512             mPastTime[0] = 0;
   1513         }
   1514 
   1515         public void addMovement(MotionEvent ev) {
   1516             long time = ev.getEventTime();
   1517             final int N = ev.getHistorySize();
   1518             for (int i=0; i<N; i++) {
   1519                 addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
   1520                         ev.getHistoricalEventTime(i));
   1521             }
   1522             addPoint(ev.getX(), ev.getY(), time);
   1523         }
   1524 
   1525         private void addPoint(float x, float y, long time) {
   1526             int drop = -1;
   1527             int i;
   1528             final long[] pastTime = mPastTime;
   1529             for (i=0; i<NUM_PAST; i++) {
   1530                 if (pastTime[i] == 0) {
   1531                     break;
   1532                 } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
   1533                     drop = i;
   1534                 }
   1535             }
   1536             if (i == NUM_PAST && drop < 0) {
   1537                 drop = 0;
   1538             }
   1539             if (drop == i) drop--;
   1540             final float[] pastX = mPastX;
   1541             final float[] pastY = mPastY;
   1542             if (drop >= 0) {
   1543                 final int start = drop+1;
   1544                 final int count = NUM_PAST-drop-1;
   1545                 System.arraycopy(pastX, start, pastX, 0, count);
   1546                 System.arraycopy(pastY, start, pastY, 0, count);
   1547                 System.arraycopy(pastTime, start, pastTime, 0, count);
   1548                 i -= (drop+1);
   1549             }
   1550             pastX[i] = x;
   1551             pastY[i] = y;
   1552             pastTime[i] = time;
   1553             i++;
   1554             if (i < NUM_PAST) {
   1555                 pastTime[i] = 0;
   1556             }
   1557         }
   1558 
   1559         public void computeCurrentVelocity(int units) {
   1560             computeCurrentVelocity(units, Float.MAX_VALUE);
   1561         }
   1562 
   1563         public void computeCurrentVelocity(int units, float maxVelocity) {
   1564             final float[] pastX = mPastX;
   1565             final float[] pastY = mPastY;
   1566             final long[] pastTime = mPastTime;
   1567 
   1568             final float oldestX = pastX[0];
   1569             final float oldestY = pastY[0];
   1570             final long oldestTime = pastTime[0];
   1571             float accumX = 0;
   1572             float accumY = 0;
   1573             int N=0;
   1574             while (N < NUM_PAST) {
   1575                 if (pastTime[N] == 0) {
   1576                     break;
   1577                 }
   1578                 N++;
   1579             }
   1580 
   1581             for (int i=1; i < N; i++) {
   1582                 final int dur = (int)(pastTime[i] - oldestTime);
   1583                 if (dur == 0) continue;
   1584                 float dist = pastX[i] - oldestX;
   1585                 float vel = (dist/dur) * units;   // pixels/frame.
   1586                 if (accumX == 0) accumX = vel;
   1587                 else accumX = (accumX + vel) * .5f;
   1588 
   1589                 dist = pastY[i] - oldestY;
   1590                 vel = (dist/dur) * units;   // pixels/frame.
   1591                 if (accumY == 0) accumY = vel;
   1592                 else accumY = (accumY + vel) * .5f;
   1593             }
   1594             mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
   1595                     : Math.min(accumX, maxVelocity);
   1596             mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
   1597                     : Math.min(accumY, maxVelocity);
   1598         }
   1599 
   1600         public float getXVelocity() {
   1601             return mXVelocity;
   1602         }
   1603 
   1604         public float getYVelocity() {
   1605             return mYVelocity;
   1606         }
   1607     }
   1608 
   1609     private static class KeyboardHander extends Handler {
   1610         private final WeakReference<KeyboardView> mKeyboardView;
   1611 
   1612         public KeyboardHander(KeyboardView keyboardView) {
   1613             mKeyboardView = new WeakReference<KeyboardView>(keyboardView);
   1614         }
   1615 
   1616         @Override
   1617         public void handleMessage(Message msg) {
   1618             KeyboardView keyboardView = mKeyboardView.get();
   1619             if (keyboardView != null) {
   1620                 switch (msg.what) {
   1621                     case MSG_SHOW_PREVIEW:
   1622                         keyboardView.showKey(msg.arg1);
   1623                         break;
   1624                     case MSG_REMOVE_PREVIEW:
   1625                         keyboardView.mPreviewText.setVisibility(INVISIBLE);
   1626                         break;
   1627                     case MSG_REPEAT:
   1628                         if (keyboardView.repeatKey()) {
   1629                             Message repeat = Message.obtain(this, MSG_REPEAT);
   1630                             sendMessageDelayed(repeat, REPEAT_INTERVAL);
   1631                         }
   1632                         break;
   1633                     case MSG_LONGPRESS:
   1634                         keyboardView.openPopupIfRequired((MotionEvent) msg.obj);
   1635                         break;
   1636                 }
   1637             }
   1638         }
   1639     }
   1640 }
   1641