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