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