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