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