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