KeyboardView.java revision ebe3d518ffaeb14f3f2e0e74cdbc15fd2ddf4453
1/*
2 * Copyright (C) 2008-2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package android.inputmethodservice;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Bitmap;
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.PorterDuff;
25import android.graphics.Rect;
26import android.graphics.Typeface;
27import android.graphics.Paint.Align;
28import android.graphics.Region.Op;
29import android.graphics.drawable.Drawable;
30import android.inputmethodservice.Keyboard.Key;
31import android.os.Handler;
32import android.os.Message;
33import android.util.AttributeSet;
34import android.util.TypedValue;
35import android.view.GestureDetector;
36import android.view.Gravity;
37import android.view.LayoutInflater;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewConfiguration;
41import android.view.ViewGroup.LayoutParams;
42import android.widget.PopupWindow;
43import android.widget.TextView;
44
45import com.android.internal.R;
46
47import java.util.Arrays;
48import java.util.HashMap;
49import java.util.List;
50import java.util.Map;
51
52/**
53 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and
54 * detecting key presses and touch movements.
55 *
56 * @attr ref android.R.styleable#KeyboardView_keyBackground
57 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
58 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
59 * @attr ref android.R.styleable#KeyboardView_labelTextSize
60 * @attr ref android.R.styleable#KeyboardView_keyTextSize
61 * @attr ref android.R.styleable#KeyboardView_keyTextColor
62 * @attr ref android.R.styleable#KeyboardView_verticalCorrection
63 * @attr ref android.R.styleable#KeyboardView_popupLayout
64 */
65public class KeyboardView extends View implements View.OnClickListener {
66
67    /**
68     * Listener for virtual keyboard events.
69     */
70    public interface OnKeyboardActionListener {
71
72        /**
73         * Called when the user presses a key. This is sent before the {@link #onKey} is called.
74         * For keys that repeat, this is only called once.
75         * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
76         * key, the value will be zero.
77         */
78        void onPress(int primaryCode);
79
80        /**
81         * Called when the user releases a key. This is sent after the {@link #onKey} is called.
82         * For keys that repeat, this is only called once.
83         * @param primaryCode the code of the key that was released
84         */
85        void onRelease(int primaryCode);
86
87        /**
88         * Send a key press to the listener.
89         * @param primaryCode this is the key that was pressed
90         * @param keyCodes the codes for all the possible alternative keys
91         * with the primary code being the first. If the primary key code is
92         * a single character such as an alphabet or number or symbol, the alternatives
93         * will include other characters that may be on the same key or adjacent keys.
94         * These codes are useful to correct for accidental presses of a key adjacent to
95         * the intended key.
96         */
97        void onKey(int primaryCode, int[] keyCodes);
98
99        /**
100         * Sends a sequence of characters to the listener.
101         * @param text the sequence of characters to be displayed.
102         */
103        void onText(CharSequence text);
104
105        /**
106         * Called when the user quickly moves the finger from right to left.
107         */
108        void swipeLeft();
109
110        /**
111         * Called when the user quickly moves the finger from left to right.
112         */
113        void swipeRight();
114
115        /**
116         * Called when the user quickly moves the finger from up to down.
117         */
118        void swipeDown();
119
120        /**
121         * Called when the user quickly moves the finger from down to up.
122         */
123        void swipeUp();
124    }
125
126    private static final boolean DEBUG = false;
127    private static final int NOT_A_KEY = -1;
128    private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
129    private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
130
131    private Keyboard mKeyboard;
132    private int mCurrentKeyIndex = NOT_A_KEY;
133    private int mLabelTextSize;
134    private int mKeyTextSize;
135    private int mKeyTextColor;
136    private float mShadowRadius;
137    private int mShadowColor;
138    private float mBackgroundDimAmount;
139
140    private TextView mPreviewText;
141    private PopupWindow mPreviewPopup;
142    private int mPreviewTextSizeLarge;
143    private int mPreviewOffset;
144    private int mPreviewHeight;
145    private int[] mOffsetInWindow;
146
147    private PopupWindow mPopupKeyboard;
148    private View mMiniKeyboardContainer;
149    private KeyboardView mMiniKeyboard;
150    private boolean mMiniKeyboardOnScreen;
151    private View mPopupParent;
152    private int mMiniKeyboardOffsetX;
153    private int mMiniKeyboardOffsetY;
154    private Map<Key,View> mMiniKeyboardCache;
155    private int[] mWindowOffset;
156    private Key[] mKeys;
157
158    /** Listener for {@link OnKeyboardActionListener}. */
159    private OnKeyboardActionListener mKeyboardActionListener;
160
161    private static final int MSG_SHOW_PREVIEW = 1;
162    private static final int MSG_REMOVE_PREVIEW = 2;
163    private static final int MSG_REPEAT = 3;
164    private static final int MSG_LONGPRESS = 4;
165
166    private static final int DELAY_BEFORE_PREVIEW = 0;
167    private static final int DELAY_AFTER_PREVIEW = 70;
168
169    private int mVerticalCorrection;
170    private int mProximityThreshold;
171
172    private boolean mPreviewCentered = false;
173    private boolean mShowPreview = true;
174    private boolean mShowTouchPoints = true;
175    private int mPopupPreviewX;
176    private int mPopupPreviewY;
177
178    private int mLastX;
179    private int mLastY;
180    private int mStartX;
181    private int mStartY;
182
183    private boolean mProximityCorrectOn;
184
185    private Paint mPaint;
186    private Rect mPadding;
187
188    private long mDownTime;
189    private long mLastMoveTime;
190    private int mLastKey;
191    private int mLastCodeX;
192    private int mLastCodeY;
193    private int mCurrentKey = NOT_A_KEY;
194    private long mLastKeyTime;
195    private long mCurrentKeyTime;
196    private int[] mKeyIndices = new int[12];
197    private GestureDetector mGestureDetector;
198    private int mPopupX;
199    private int mPopupY;
200    private int mRepeatKeyIndex = NOT_A_KEY;
201    private int mPopupLayout;
202    private boolean mAbortKey;
203    private Key mInvalidatedKey;
204    private Rect mClipRegion = new Rect(0, 0, 0, 0);
205
206    // Variables for dealing with multiple pointers
207    private int mOldPointerCount = 1;
208    private float mOldPointerX;
209    private float mOldPointerY;
210
211    private Drawable mKeyBackground;
212
213    private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
214    private static final int REPEAT_START_DELAY = 400;
215    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
216
217    private static int MAX_NEARBY_KEYS = 12;
218    private int[] mDistances = new int[MAX_NEARBY_KEYS];
219
220    // For multi-tap
221    private int mLastSentIndex;
222    private int mTapCount;
223    private long mLastTapTime;
224    private boolean mInMultiTap;
225    private static final int MULTITAP_INTERVAL = 800; // milliseconds
226    private StringBuilder mPreviewLabel = new StringBuilder(1);
227
228    /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
229    private boolean mDrawPending;
230    /** The dirty region in the keyboard bitmap */
231    private Rect mDirtyRect = new Rect();
232    /** The keyboard bitmap for faster updates */
233    private Bitmap mBuffer;
234    /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
235    private boolean mKeyboardChanged;
236    /** The canvas for the above mutable keyboard bitmap */
237    private Canvas mCanvas;
238
239    Handler mHandler = new Handler() {
240        @Override
241        public void handleMessage(Message msg) {
242            switch (msg.what) {
243                case MSG_SHOW_PREVIEW:
244                    showKey(msg.arg1);
245                    break;
246                case MSG_REMOVE_PREVIEW:
247                    mPreviewText.setVisibility(INVISIBLE);
248                    break;
249                case MSG_REPEAT:
250                    if (repeatKey()) {
251                        Message repeat = Message.obtain(this, MSG_REPEAT);
252                        sendMessageDelayed(repeat, REPEAT_INTERVAL);
253                    }
254                    break;
255                case MSG_LONGPRESS:
256                    openPopupIfRequired((MotionEvent) msg.obj);
257                    break;
258            }
259        }
260    };
261
262    public KeyboardView(Context context, AttributeSet attrs) {
263        this(context, attrs, com.android.internal.R.attr.keyboardViewStyle);
264    }
265
266    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
267        super(context, attrs, defStyle);
268
269        TypedArray a =
270            context.obtainStyledAttributes(
271                attrs, android.R.styleable.KeyboardView, defStyle, 0);
272
273        LayoutInflater inflate =
274                (LayoutInflater) context
275                        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
276
277        int previewLayout = 0;
278        int keyTextSize = 0;
279
280        int n = a.getIndexCount();
281
282        for (int i = 0; i < n; i++) {
283            int attr = a.getIndex(i);
284
285            switch (attr) {
286            case com.android.internal.R.styleable.KeyboardView_keyBackground:
287                mKeyBackground = a.getDrawable(attr);
288                break;
289            case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
290                mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
291                break;
292            case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
293                previewLayout = a.getResourceId(attr, 0);
294                break;
295            case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
296                mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
297                break;
298            case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
299                mPreviewHeight = a.getDimensionPixelSize(attr, 80);
300                break;
301            case com.android.internal.R.styleable.KeyboardView_keyTextSize:
302                mKeyTextSize = a.getDimensionPixelSize(attr, 18);
303                break;
304            case com.android.internal.R.styleable.KeyboardView_keyTextColor:
305                mKeyTextColor = a.getColor(attr, 0xFF000000);
306                break;
307            case com.android.internal.R.styleable.KeyboardView_labelTextSize:
308                mLabelTextSize = a.getDimensionPixelSize(attr, 14);
309                break;
310            case com.android.internal.R.styleable.KeyboardView_popupLayout:
311                mPopupLayout = a.getResourceId(attr, 0);
312                break;
313            case com.android.internal.R.styleable.KeyboardView_shadowColor:
314                mShadowColor = a.getColor(attr, 0);
315                break;
316            case com.android.internal.R.styleable.KeyboardView_shadowRadius:
317                mShadowRadius = a.getFloat(attr, 0f);
318                break;
319            }
320        }
321
322        a = mContext.obtainStyledAttributes(
323                com.android.internal.R.styleable.Theme);
324        mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
325
326        mPreviewPopup = new PopupWindow(context);
327        if (previewLayout != 0) {
328            mPreviewText = (TextView) inflate.inflate(previewLayout, null);
329            mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
330            mPreviewPopup.setContentView(mPreviewText);
331            mPreviewPopup.setBackgroundDrawable(null);
332        } else {
333            mShowPreview = false;
334        }
335
336        mPreviewPopup.setTouchable(false);
337
338        mPopupKeyboard = new PopupWindow(context);
339        mPopupKeyboard.setBackgroundDrawable(null);
340        //mPopupKeyboard.setClippingEnabled(false);
341
342        mPopupParent = this;
343        //mPredicting = true;
344
345        mPaint = new Paint();
346        mPaint.setAntiAlias(true);
347        mPaint.setTextSize(keyTextSize);
348        mPaint.setTextAlign(Align.CENTER);
349        mPaint.setAlpha(255);
350
351        mPadding = new Rect(0, 0, 0, 0);
352        mMiniKeyboardCache = new HashMap<Key,View>();
353        mKeyBackground.getPadding(mPadding);
354
355        resetMultiTap();
356        initGestureDetector();
357    }
358
359    private void initGestureDetector() {
360        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
361            @Override
362            public boolean onFling(MotionEvent me1, MotionEvent me2,
363                    float velocityX, float velocityY) {
364                final float absX = Math.abs(velocityX);
365                final float absY = Math.abs(velocityY);
366                if (velocityX > 500 && absY < absX) {
367                    swipeRight();
368                    return true;
369                } else if (velocityX < -500 && absY < absX) {
370                    swipeLeft();
371                    return true;
372                } else if (velocityY < -500 && absX < absY) {
373                    swipeUp();
374                    return true;
375                } else if (velocityY > 500 && absX < 200) {
376                    swipeDown();
377                    return true;
378                } else if (absX > 800 || absY > 800) {
379                    return true;
380                }
381                return false;
382            }
383        });
384
385        mGestureDetector.setIsLongpressEnabled(false);
386    }
387
388    public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
389        mKeyboardActionListener = listener;
390    }
391
392    /**
393     * Returns the {@link OnKeyboardActionListener} object.
394     * @return the listener attached to this keyboard
395     */
396    protected OnKeyboardActionListener getOnKeyboardActionListener() {
397        return mKeyboardActionListener;
398    }
399
400    /**
401     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
402     * view will re-layout itself to accommodate the keyboard.
403     * @see Keyboard
404     * @see #getKeyboard()
405     * @param keyboard the keyboard to display in this view
406     */
407    public void setKeyboard(Keyboard keyboard) {
408        if (mKeyboard != null) {
409            showPreview(NOT_A_KEY);
410        }
411        // Remove any pending messages
412        removeMessages();
413        mKeyboard = keyboard;
414        List<Key> keys = mKeyboard.getKeys();
415        mKeys = keys.toArray(new Key[keys.size()]);
416        requestLayout();
417        // Hint to reallocate the buffer if the size changed
418        mKeyboardChanged = true;
419        invalidateAllKeys();
420        computeProximityThreshold(keyboard);
421        mMiniKeyboardCache.clear(); // Not really necessary to do every time, but will free up views
422        // Switching to a different keyboard should abort any pending keys so that the key up
423        // doesn't get delivered to the old or new keyboard
424        mAbortKey = true; // Until the next ACTION_DOWN
425    }
426
427    /**
428     * Returns the current keyboard being displayed by this view.
429     * @return the currently attached keyboard
430     * @see #setKeyboard(Keyboard)
431     */
432    public Keyboard getKeyboard() {
433        return mKeyboard;
434    }
435
436    /**
437     * Sets the state of the shift key of the keyboard, if any.
438     * @param shifted whether or not to enable the state of the shift key
439     * @return true if the shift key state changed, false if there was no change
440     * @see KeyboardView#isShifted()
441     */
442    public boolean setShifted(boolean shifted) {
443        if (mKeyboard != null) {
444            if (mKeyboard.setShifted(shifted)) {
445                // The whole keyboard probably needs to be redrawn
446                invalidateAllKeys();
447                return true;
448            }
449        }
450        return false;
451    }
452
453    /**
454     * Returns the state of the shift key of the keyboard, if any.
455     * @return true if the shift is in a pressed state, false otherwise. If there is
456     * no shift key on the keyboard or there is no keyboard attached, it returns false.
457     * @see KeyboardView#setShifted(boolean)
458     */
459    public boolean isShifted() {
460        if (mKeyboard != null) {
461            return mKeyboard.isShifted();
462        }
463        return false;
464    }
465
466    /**
467     * Enables or disables the key feedback popup. This is a popup that shows a magnified
468     * version of the depressed key. By default the preview is enabled.
469     * @param previewEnabled whether or not to enable the key feedback popup
470     * @see #isPreviewEnabled()
471     */
472    public void setPreviewEnabled(boolean previewEnabled) {
473        mShowPreview = previewEnabled;
474    }
475
476    /**
477     * Returns the enabled state of the key feedback popup.
478     * @return whether or not the key feedback popup is enabled
479     * @see #setPreviewEnabled(boolean)
480     */
481    public boolean isPreviewEnabled() {
482        return mShowPreview;
483    }
484
485    public void setVerticalCorrection(int verticalOffset) {
486
487    }
488    public void setPopupParent(View v) {
489        mPopupParent = v;
490    }
491
492    public void setPopupOffset(int x, int y) {
493        mMiniKeyboardOffsetX = x;
494        mMiniKeyboardOffsetY = y;
495        if (mPreviewPopup.isShowing()) {
496            mPreviewPopup.dismiss();
497        }
498    }
499
500    /**
501     * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
502     * codes for adjacent keys.  When disabled, only the primary key code will be
503     * reported.
504     * @param enabled whether or not the proximity correction is enabled
505     */
506    public void setProximityCorrectionEnabled(boolean enabled) {
507        mProximityCorrectOn = enabled;
508    }
509
510    /**
511     * Returns true if proximity correction is enabled.
512     */
513    public boolean isProximityCorrectionEnabled() {
514        return mProximityCorrectOn;
515    }
516
517    /**
518     * Popup keyboard close button clicked.
519     * @hide
520     */
521    public void onClick(View v) {
522        dismissPopupKeyboard();
523    }
524
525    private CharSequence adjustCase(CharSequence label) {
526        if (mKeyboard.isShifted() && label != null && label.length() < 3
527                && Character.isLowerCase(label.charAt(0))) {
528            label = label.toString().toUpperCase();
529        }
530        return label;
531    }
532
533    @Override
534    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
535        // Round up a little
536        if (mKeyboard == null) {
537            setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom);
538        } else {
539            int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight;
540            if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
541                width = MeasureSpec.getSize(widthMeasureSpec);
542            }
543            setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom);
544        }
545    }
546
547    /**
548     * Compute the average distance between adjacent keys (horizontally and vertically)
549     * and square it to get the proximity threshold. We use a square here and in computing
550     * the touch distance from a key's center to avoid taking a square root.
551     * @param keyboard
552     */
553    private void computeProximityThreshold(Keyboard keyboard) {
554        if (keyboard == null) return;
555        final Key[] keys = mKeys;
556        if (keys == null) return;
557        int length = keys.length;
558        int dimensionSum = 0;
559        for (int i = 0; i < length; i++) {
560            Key key = keys[i];
561            dimensionSum += Math.min(key.width, key.height) + key.gap;
562        }
563        if (dimensionSum < 0 || length == 0) return;
564        mProximityThreshold = (int) (dimensionSum * 1.4f / length);
565        mProximityThreshold *= mProximityThreshold; // Square it
566    }
567
568    @Override
569    public void onSizeChanged(int w, int h, int oldw, int oldh) {
570        super.onSizeChanged(w, h, oldw, oldh);
571        // Release the buffer, if any and it will be reallocated on the next draw
572        mBuffer = null;
573    }
574
575    @Override
576    public void onDraw(Canvas canvas) {
577        super.onDraw(canvas);
578        if (mDrawPending || mBuffer == null || mKeyboardChanged) {
579            onBufferDraw();
580        }
581        canvas.drawBitmap(mBuffer, 0, 0, null);
582    }
583
584    private void onBufferDraw() {
585        if (mBuffer == null || mKeyboardChanged) {
586            if (mBuffer == null || mKeyboardChanged &&
587                    (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
588                mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
589                mCanvas = new Canvas(mBuffer);
590            }
591            invalidateAllKeys();
592            mKeyboardChanged = false;
593        }
594        final Canvas canvas = mCanvas;
595        canvas.clipRect(mDirtyRect, Op.REPLACE);
596
597        if (mKeyboard == null) return;
598
599        final Paint paint = mPaint;
600        final Drawable keyBackground = mKeyBackground;
601        final Rect clipRegion = mClipRegion;
602        final Rect padding = mPadding;
603        final int kbdPaddingLeft = mPaddingLeft;
604        final int kbdPaddingTop = mPaddingTop;
605        final Key[] keys = mKeys;
606        final Key invalidKey = mInvalidatedKey;
607
608        paint.setColor(mKeyTextColor);
609        boolean drawSingleKey = false;
610        if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
611          // Is clipRegion completely contained within the invalidated key?
612          if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
613                  invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
614                  invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
615                  invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
616              drawSingleKey = true;
617          }
618        }
619        canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
620        final int keyCount = keys.length;
621        for (int i = 0; i < keyCount; i++) {
622            final Key key = keys[i];
623            if (drawSingleKey && invalidKey != key) {
624                continue;
625            }
626            int[] drawableState = key.getCurrentDrawableState();
627            keyBackground.setState(drawableState);
628
629            // Switch the character to uppercase if shift is pressed
630            String label = key.label == null? null : adjustCase(key.label).toString();
631
632            final Rect bounds = keyBackground.getBounds();
633            if (key.width != bounds.right ||
634                    key.height != bounds.bottom) {
635                keyBackground.setBounds(0, 0, key.width, key.height);
636            }
637            canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
638            keyBackground.draw(canvas);
639
640            if (label != null) {
641                // For characters, use large font. For labels like "Done", use small font.
642                if (label.length() > 1 && key.codes.length < 2) {
643                    paint.setTextSize(mLabelTextSize);
644                    paint.setTypeface(Typeface.DEFAULT_BOLD);
645                } else {
646                    paint.setTextSize(mKeyTextSize);
647                    paint.setTypeface(Typeface.DEFAULT);
648                }
649                // Draw a drop shadow for the text
650                paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
651                // Draw the text
652                canvas.drawText(label,
653                    (key.width - padding.left - padding.right) / 2
654                            + padding.left,
655                    (key.height - padding.top - padding.bottom) / 2
656                            + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
657                    paint);
658                // Turn off drop shadow
659                paint.setShadowLayer(0, 0, 0, 0);
660            } else if (key.icon != null) {
661                final int drawableX = (key.width - padding.left - padding.right
662                                - key.icon.getIntrinsicWidth()) / 2 + padding.left;
663                final int drawableY = (key.height - padding.top - padding.bottom
664                        - key.icon.getIntrinsicHeight()) / 2 + padding.top;
665                canvas.translate(drawableX, drawableY);
666                key.icon.setBounds(0, 0,
667                        key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
668                key.icon.draw(canvas);
669                canvas.translate(-drawableX, -drawableY);
670            }
671            canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
672        }
673        mInvalidatedKey = null;
674        // Overlay a dark rectangle to dim the keyboard
675        if (mMiniKeyboardOnScreen) {
676            paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
677            canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
678        }
679
680        if (DEBUG && mShowTouchPoints) {
681            paint.setAlpha(128);
682            paint.setColor(0xFFFF0000);
683            canvas.drawCircle(mStartX, mStartY, 3, paint);
684            canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
685            paint.setColor(0xFF0000FF);
686            canvas.drawCircle(mLastX, mLastY, 3, paint);
687            paint.setColor(0xFF00FF00);
688            canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
689        }
690
691        mDrawPending = false;
692        mDirtyRect.setEmpty();
693    }
694
695    private int getKeyIndices(int x, int y, int[] allKeys) {
696        final Key[] keys = mKeys;
697        int primaryIndex = NOT_A_KEY;
698        int closestKey = NOT_A_KEY;
699        int closestKeyDist = mProximityThreshold + 1;
700        java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
701        int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
702        final int keyCount = nearestKeyIndices.length;
703        for (int i = 0; i < keyCount; i++) {
704            final Key key = keys[nearestKeyIndices[i]];
705            int dist = 0;
706            boolean isInside = key.isInside(x,y);
707            if (((mProximityCorrectOn
708                    && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
709                    || isInside)
710                    && key.codes[0] > 32) {
711                // Find insertion point
712                final int nCodes = key.codes.length;
713                if (dist < closestKeyDist) {
714                    closestKeyDist = dist;
715                    closestKey = nearestKeyIndices[i];
716                }
717
718                if (allKeys == null) continue;
719
720                for (int j = 0; j < mDistances.length; j++) {
721                    if (mDistances[j] > dist) {
722                        // Make space for nCodes codes
723                        System.arraycopy(mDistances, j, mDistances, j + nCodes,
724                                mDistances.length - j - nCodes);
725                        System.arraycopy(allKeys, j, allKeys, j + nCodes,
726                                allKeys.length - j - nCodes);
727                        for (int c = 0; c < nCodes; c++) {
728                            allKeys[j + c] = key.codes[c];
729                            mDistances[j + c] = dist;
730                        }
731                        break;
732                    }
733                }
734            }
735
736            if (isInside) {
737                primaryIndex = nearestKeyIndices[i];
738            }
739        }
740        if (primaryIndex == NOT_A_KEY) {
741            primaryIndex = closestKey;
742        }
743        return primaryIndex;
744    }
745
746    private void detectAndSendKey(int x, int y, long eventTime) {
747        int index = mCurrentKey;
748        if (index != NOT_A_KEY && index < mKeys.length) {
749            final Key key = mKeys[index];
750            if (key.text != null) {
751                mKeyboardActionListener.onText(key.text);
752                mKeyboardActionListener.onRelease(NOT_A_KEY);
753            } else {
754                int code = key.codes[0];
755                //TextEntryState.keyPressedAt(key, x, y);
756                int[] codes = new int[MAX_NEARBY_KEYS];
757                Arrays.fill(codes, NOT_A_KEY);
758                getKeyIndices(x, y, codes);
759                // Multi-tap
760                if (mInMultiTap) {
761                    if (mTapCount != -1) {
762                        mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
763                    } else {
764                        mTapCount = 0;
765                    }
766                    code = key.codes[mTapCount];
767                }
768                mKeyboardActionListener.onKey(code, codes);
769                mKeyboardActionListener.onRelease(code);
770            }
771            mLastSentIndex = index;
772            mLastTapTime = eventTime;
773        }
774    }
775
776    /**
777     * Handle multi-tap keys by producing the key label for the current multi-tap state.
778     */
779    private CharSequence getPreviewText(Key key) {
780        if (mInMultiTap) {
781            // Multi-tap
782            mPreviewLabel.setLength(0);
783            mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
784            return adjustCase(mPreviewLabel);
785        } else {
786            return adjustCase(key.label);
787        }
788    }
789
790    private void showPreview(int keyIndex) {
791        int oldKeyIndex = mCurrentKeyIndex;
792        final PopupWindow previewPopup = mPreviewPopup;
793
794        mCurrentKeyIndex = keyIndex;
795        // Release the old key and press the new key
796        final Key[] keys = mKeys;
797        if (oldKeyIndex != mCurrentKeyIndex) {
798            if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
799                keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY);
800                invalidateKey(oldKeyIndex);
801            }
802            if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
803                keys[mCurrentKeyIndex].onPressed();
804                invalidateKey(mCurrentKeyIndex);
805            }
806        }
807        // If key changed and preview is on ...
808        if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
809            mHandler.removeMessages(MSG_SHOW_PREVIEW);
810            if (previewPopup.isShowing()) {
811                if (keyIndex == NOT_A_KEY) {
812                    mHandler.sendMessageDelayed(mHandler
813                            .obtainMessage(MSG_REMOVE_PREVIEW),
814                            DELAY_AFTER_PREVIEW);
815                }
816            }
817            if (keyIndex != NOT_A_KEY) {
818                if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
819                    // Show right away, if it's already visible and finger is moving around
820                    showKey(keyIndex);
821                } else {
822                    mHandler.sendMessageDelayed(
823                            mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
824                            DELAY_BEFORE_PREVIEW);
825                }
826            }
827        }
828    }
829
830    private void showKey(final int keyIndex) {
831        final PopupWindow previewPopup = mPreviewPopup;
832        final Key[] keys = mKeys;
833        if (keyIndex < 0 || keyIndex >= mKeys.length) return;
834        Key key = keys[keyIndex];
835        if (key.icon != null) {
836            mPreviewText.setCompoundDrawables(null, null, null,
837                    key.iconPreview != null ? key.iconPreview : key.icon);
838            mPreviewText.setText(null);
839        } else {
840            mPreviewText.setCompoundDrawables(null, null, null, null);
841            mPreviewText.setText(getPreviewText(key));
842            if (key.label.length() > 1 && key.codes.length < 2) {
843                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
844                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
845            } else {
846                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
847                mPreviewText.setTypeface(Typeface.DEFAULT);
848            }
849        }
850        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
851                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
852        int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
853                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
854        final int popupHeight = mPreviewHeight;
855        LayoutParams lp = mPreviewText.getLayoutParams();
856        if (lp != null) {
857            lp.width = popupWidth;
858            lp.height = popupHeight;
859        }
860        if (!mPreviewCentered) {
861            mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
862            mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
863        } else {
864            // TODO: Fix this if centering is brought back
865            mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
866            mPopupPreviewY = - mPreviewText.getMeasuredHeight();
867        }
868        mHandler.removeMessages(MSG_REMOVE_PREVIEW);
869        if (mOffsetInWindow == null) {
870            mOffsetInWindow = new int[2];
871            getLocationInWindow(mOffsetInWindow);
872            mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero
873            mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero
874        }
875        // Set the preview background state
876        mPreviewText.getBackground().setState(
877                key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
878        if (previewPopup.isShowing()) {
879            previewPopup.update(mPopupPreviewX + mOffsetInWindow[0],
880                    mPopupPreviewY + mOffsetInWindow[1],
881                    popupWidth, popupHeight);
882        } else {
883            previewPopup.setWidth(popupWidth);
884            previewPopup.setHeight(popupHeight);
885            previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
886                    mPopupPreviewX + mOffsetInWindow[0],
887                    mPopupPreviewY + mOffsetInWindow[1]);
888        }
889        mPreviewText.setVisibility(VISIBLE);
890    }
891
892    /**
893     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
894     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
895     * draws the cached buffer.
896     * @see #invalidateKey(int)
897     */
898    public void invalidateAllKeys() {
899        mDirtyRect.union(0, 0, getWidth(), getHeight());
900        mDrawPending = true;
901        invalidate();
902    }
903
904    /**
905     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
906     * one key is changing it's content. Any changes that affect the position or size of the key
907     * may not be honored.
908     * @param keyIndex the index of the key in the attached {@link Keyboard}.
909     * @see #invalidateAllKeys
910     */
911    public void invalidateKey(int keyIndex) {
912        if (mKeys == null) return;
913        if (keyIndex < 0 || keyIndex >= mKeys.length) {
914            return;
915        }
916        final Key key = mKeys[keyIndex];
917        mInvalidatedKey = key;
918        mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop,
919                key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
920        onBufferDraw();
921        invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
922                key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
923    }
924
925    private boolean openPopupIfRequired(MotionEvent me) {
926        // Check if we have a popup layout specified first.
927        if (mPopupLayout == 0) {
928            return false;
929        }
930        if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
931            return false;
932        }
933
934        Key popupKey = mKeys[mCurrentKey];
935        boolean result = onLongPress(popupKey);
936        if (result) {
937            mAbortKey = true;
938            showPreview(NOT_A_KEY);
939        }
940        return result;
941    }
942
943    /**
944     * Called when a key is long pressed. By default this will open any popup keyboard associated
945     * with this key through the attributes popupLayout and popupCharacters.
946     * @param popupKey the key that was long pressed
947     * @return true if the long press is handled, false otherwise. Subclasses should call the
948     * method on the base class if the subclass doesn't wish to handle the call.
949     */
950    protected boolean onLongPress(Key popupKey) {
951        int popupKeyboardId = popupKey.popupResId;
952
953        if (popupKeyboardId != 0) {
954            mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
955            if (mMiniKeyboardContainer == null) {
956                LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
957                        Context.LAYOUT_INFLATER_SERVICE);
958                mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
959                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
960                        com.android.internal.R.id.keyboardView);
961                View closeButton = mMiniKeyboardContainer.findViewById(
962                        com.android.internal.R.id.closeButton);
963                if (closeButton != null) closeButton.setOnClickListener(this);
964                mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
965                    public void onKey(int primaryCode, int[] keyCodes) {
966                        mKeyboardActionListener.onKey(primaryCode, keyCodes);
967                        dismissPopupKeyboard();
968                    }
969
970                    public void onText(CharSequence text) {
971                        mKeyboardActionListener.onText(text);
972                        dismissPopupKeyboard();
973                    }
974
975                    public void swipeLeft() { }
976                    public void swipeRight() { }
977                    public void swipeUp() { }
978                    public void swipeDown() { }
979                    public void onPress(int primaryCode) {
980                        mKeyboardActionListener.onPress(primaryCode);
981                    }
982                    public void onRelease(int primaryCode) {
983                        mKeyboardActionListener.onRelease(primaryCode);
984                    }
985                });
986                //mInputView.setSuggest(mSuggest);
987                Keyboard keyboard;
988                if (popupKey.popupCharacters != null) {
989                    keyboard = new Keyboard(getContext(), popupKeyboardId,
990                            popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
991                } else {
992                    keyboard = new Keyboard(getContext(), popupKeyboardId);
993                }
994                mMiniKeyboard.setKeyboard(keyboard);
995                mMiniKeyboard.setPopupParent(this);
996                mMiniKeyboardContainer.measure(
997                        MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
998                        MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
999
1000                mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
1001            } else {
1002                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1003                        com.android.internal.R.id.keyboardView);
1004            }
1005            if (mWindowOffset == null) {
1006                mWindowOffset = new int[2];
1007                getLocationInWindow(mWindowOffset);
1008            }
1009            mPopupX = popupKey.x + mPaddingLeft;
1010            mPopupY = popupKey.y + mPaddingTop;
1011            mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
1012            mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
1013            final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0];
1014            final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1];
1015            mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
1016            mMiniKeyboard.setShifted(isShifted());
1017            mPopupKeyboard.setContentView(mMiniKeyboardContainer);
1018            mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
1019            mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
1020            mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1021            mMiniKeyboardOnScreen = true;
1022            //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
1023            invalidateAllKeys();
1024            return true;
1025        }
1026        return false;
1027    }
1028
1029    @Override
1030    public boolean onTouchEvent(MotionEvent me) {
1031        // Convert multi-pointer up/down events to single up/down events to
1032        // deal with the typical multi-pointer behavior of two-thumb typing
1033        int pointerCount = me.getPointerCount();
1034        boolean result = false;
1035        if (pointerCount != mOldPointerCount) {
1036            long now = me.getEventTime();
1037            if (pointerCount == 1) {
1038                // Send a down event for the latest pointer
1039                MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1040                        me.getX(), me.getY(), me.getMetaState());
1041                result = onModifiedTouchEvent(down);
1042                down.recycle();
1043                // If it's an up action, then deliver the up as well.
1044                if (me.getAction() == MotionEvent.ACTION_UP) {
1045                    result = onModifiedTouchEvent(me);
1046                }
1047            } else {
1048                // Send an up event for the last pointer
1049                MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1050                        mOldPointerX, mOldPointerY, me.getMetaState());
1051                result = onModifiedTouchEvent(up);
1052                up.recycle();
1053            }
1054        } else {
1055            if (pointerCount == 1) {
1056                mOldPointerX = me.getX();
1057                mOldPointerY = me.getY();
1058                result = onModifiedTouchEvent(me);
1059            } else {
1060                // Don't do anything when 2 pointers are down and moving.
1061                result = true;
1062            }
1063        }
1064        mOldPointerCount = pointerCount;
1065        return result;
1066    }
1067
1068    private boolean onModifiedTouchEvent(MotionEvent me) {
1069        int touchX = (int) me.getX() - mPaddingLeft;
1070        int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop;
1071        int action = me.getAction();
1072        long eventTime = me.getEventTime();
1073        int keyIndex = getKeyIndices(touchX, touchY, null);
1074        if (mGestureDetector.onTouchEvent(me)) {
1075            showPreview(NOT_A_KEY);
1076            mHandler.removeMessages(MSG_REPEAT);
1077            mHandler.removeMessages(MSG_LONGPRESS);
1078            return true;
1079        }
1080
1081        // Needs to be called after the gesture detector gets a turn, as it may have
1082        // displayed the mini keyboard
1083        if (mMiniKeyboardOnScreen) {
1084            return true;
1085        }
1086
1087        switch (action) {
1088            case MotionEvent.ACTION_DOWN:
1089                mAbortKey = false;
1090                mStartX = touchX;
1091                mStartY = touchY;
1092                mLastCodeX = touchX;
1093                mLastCodeY = touchY;
1094                mLastKeyTime = 0;
1095                mCurrentKeyTime = 0;
1096                mLastKey = NOT_A_KEY;
1097                mCurrentKey = keyIndex;
1098                mDownTime = me.getEventTime();
1099                mLastMoveTime = mDownTime;
1100                checkMultiTap(eventTime, keyIndex);
1101                mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
1102                        mKeys[keyIndex].codes[0] : 0);
1103                if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1104                    mRepeatKeyIndex = mCurrentKey;
1105                    repeatKey();
1106                    Message msg = mHandler.obtainMessage(MSG_REPEAT);
1107                    mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1108                }
1109                if (mCurrentKey != NOT_A_KEY) {
1110                    Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1111                    mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1112                }
1113                showPreview(keyIndex);
1114                break;
1115
1116            case MotionEvent.ACTION_MOVE:
1117                boolean continueLongPress = false;
1118                if (keyIndex != NOT_A_KEY) {
1119                    if (mCurrentKey == NOT_A_KEY) {
1120                        mCurrentKey = keyIndex;
1121                        mCurrentKeyTime = eventTime - mDownTime;
1122                    } else {
1123                        if (keyIndex == mCurrentKey) {
1124                            mCurrentKeyTime += eventTime - mLastMoveTime;
1125                            continueLongPress = true;
1126                        } else if (mRepeatKeyIndex == NOT_A_KEY) {
1127                            resetMultiTap();
1128                            mLastKey = mCurrentKey;
1129                            mLastCodeX = mLastX;
1130                            mLastCodeY = mLastY;
1131                            mLastKeyTime =
1132                                    mCurrentKeyTime + eventTime - mLastMoveTime;
1133                            mCurrentKey = keyIndex;
1134                            mCurrentKeyTime = 0;
1135                        }
1136                    }
1137                }
1138                if (!continueLongPress) {
1139                    // Cancel old longpress
1140                    mHandler.removeMessages(MSG_LONGPRESS);
1141                    // Start new longpress if key has changed
1142                    if (keyIndex != NOT_A_KEY) {
1143                        Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1144                        mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1145                    }
1146                }
1147                showPreview(mCurrentKey);
1148                break;
1149
1150            case MotionEvent.ACTION_UP:
1151                removeMessages();
1152                if (keyIndex == mCurrentKey) {
1153                    mCurrentKeyTime += eventTime - mLastMoveTime;
1154                } else {
1155                    resetMultiTap();
1156                    mLastKey = mCurrentKey;
1157                    mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1158                    mCurrentKey = keyIndex;
1159                    mCurrentKeyTime = 0;
1160                }
1161                if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) {
1162                    mCurrentKey = mLastKey;
1163                    touchX = mLastCodeX;
1164                    touchY = mLastCodeY;
1165                }
1166                showPreview(NOT_A_KEY);
1167                Arrays.fill(mKeyIndices, NOT_A_KEY);
1168                // If we're not on a repeating key (which sends on a DOWN event)
1169                if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
1170                    detectAndSendKey(touchX, touchY, eventTime);
1171                }
1172                invalidateKey(keyIndex);
1173                mRepeatKeyIndex = NOT_A_KEY;
1174                break;
1175        }
1176        mLastX = touchX;
1177        mLastY = touchY;
1178        return true;
1179    }
1180
1181    private boolean repeatKey() {
1182        Key key = mKeys[mRepeatKeyIndex];
1183        detectAndSendKey(key.x, key.y, mLastTapTime);
1184        return true;
1185    }
1186
1187    protected void swipeRight() {
1188        mKeyboardActionListener.swipeRight();
1189    }
1190
1191    protected void swipeLeft() {
1192        mKeyboardActionListener.swipeLeft();
1193    }
1194
1195    protected void swipeUp() {
1196        mKeyboardActionListener.swipeUp();
1197    }
1198
1199    protected void swipeDown() {
1200        mKeyboardActionListener.swipeDown();
1201    }
1202
1203    public void closing() {
1204        if (mPreviewPopup.isShowing()) {
1205            mPreviewPopup.dismiss();
1206        }
1207        removeMessages();
1208
1209        dismissPopupKeyboard();
1210        mBuffer = null;
1211        mCanvas = null;
1212        mMiniKeyboardCache.clear();
1213    }
1214
1215    private void removeMessages() {
1216        mHandler.removeMessages(MSG_REPEAT);
1217        mHandler.removeMessages(MSG_LONGPRESS);
1218        mHandler.removeMessages(MSG_SHOW_PREVIEW);
1219    }
1220
1221    @Override
1222    public void onDetachedFromWindow() {
1223        super.onDetachedFromWindow();
1224        closing();
1225    }
1226
1227    private void dismissPopupKeyboard() {
1228        if (mPopupKeyboard.isShowing()) {
1229            mPopupKeyboard.dismiss();
1230            mMiniKeyboardOnScreen = false;
1231            invalidateAllKeys();
1232        }
1233    }
1234
1235    public boolean handleBack() {
1236        if (mPopupKeyboard.isShowing()) {
1237            dismissPopupKeyboard();
1238            return true;
1239        }
1240        return false;
1241    }
1242
1243    private void resetMultiTap() {
1244        mLastSentIndex = NOT_A_KEY;
1245        mTapCount = 0;
1246        mLastTapTime = -1;
1247        mInMultiTap = false;
1248    }
1249
1250    private void checkMultiTap(long eventTime, int keyIndex) {
1251        if (keyIndex == NOT_A_KEY) return;
1252        Key key = mKeys[keyIndex];
1253        if (key.codes.length > 1) {
1254            mInMultiTap = true;
1255            if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1256                    && keyIndex == mLastSentIndex) {
1257                mTapCount = (mTapCount + 1) % key.codes.length;
1258                return;
1259            } else {
1260                mTapCount = -1;
1261                return;
1262            }
1263        }
1264        if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1265            resetMultiTap();
1266        }
1267    }
1268}
1269