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                final int keyCode = oldKey.codes[0];
859                sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT,
860                        keyCode);
861                // TODO: We need to implement AccessibilityNodeProvider for this view.
862                sendAccessibilityEventForUnicodeCharacter(
863                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, keyCode);
864            }
865            if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
866                Key newKey = keys[mCurrentKeyIndex];
867                newKey.onPressed();
868                invalidateKey(mCurrentKeyIndex);
869                final int keyCode = newKey.codes[0];
870                sendAccessibilityEventForUnicodeCharacter(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER,
871                        keyCode);
872                // TODO: We need to implement AccessibilityNodeProvider for this view.
873                sendAccessibilityEventForUnicodeCharacter(
874                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, keyCode);
875            }
876        }
877        // If key changed and preview is on ...
878        if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
879            mHandler.removeMessages(MSG_SHOW_PREVIEW);
880            if (previewPopup.isShowing()) {
881                if (keyIndex == NOT_A_KEY) {
882                    mHandler.sendMessageDelayed(mHandler
883                            .obtainMessage(MSG_REMOVE_PREVIEW),
884                            DELAY_AFTER_PREVIEW);
885                }
886            }
887            if (keyIndex != NOT_A_KEY) {
888                if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
889                    // Show right away, if it's already visible and finger is moving around
890                    showKey(keyIndex);
891                } else {
892                    mHandler.sendMessageDelayed(
893                            mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
894                            DELAY_BEFORE_PREVIEW);
895                }
896            }
897        }
898    }
899
900    private void showKey(final int keyIndex) {
901        final PopupWindow previewPopup = mPreviewPopup;
902        final Key[] keys = mKeys;
903        if (keyIndex < 0 || keyIndex >= mKeys.length) return;
904        Key key = keys[keyIndex];
905        if (key.icon != null) {
906            mPreviewText.setCompoundDrawables(null, null, null,
907                    key.iconPreview != null ? key.iconPreview : key.icon);
908            mPreviewText.setText(null);
909        } else {
910            mPreviewText.setCompoundDrawables(null, null, null, null);
911            mPreviewText.setText(getPreviewText(key));
912            if (key.label.length() > 1 && key.codes.length < 2) {
913                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
914                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
915            } else {
916                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
917                mPreviewText.setTypeface(Typeface.DEFAULT);
918            }
919        }
920        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
921                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
922        int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
923                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
924        final int popupHeight = mPreviewHeight;
925        LayoutParams lp = mPreviewText.getLayoutParams();
926        if (lp != null) {
927            lp.width = popupWidth;
928            lp.height = popupHeight;
929        }
930        if (!mPreviewCentered) {
931            mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
932            mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
933        } else {
934            // TODO: Fix this if centering is brought back
935            mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
936            mPopupPreviewY = - mPreviewText.getMeasuredHeight();
937        }
938        mHandler.removeMessages(MSG_REMOVE_PREVIEW);
939        getLocationInWindow(mCoordinates);
940        mCoordinates[0] += mMiniKeyboardOffsetX; // Offset may be zero
941        mCoordinates[1] += mMiniKeyboardOffsetY; // Offset may be zero
942
943        // Set the preview background state
944        mPreviewText.getBackground().setState(
945                key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
946        mPopupPreviewX += mCoordinates[0];
947        mPopupPreviewY += mCoordinates[1];
948
949        // If the popup cannot be shown above the key, put it on the side
950        getLocationOnScreen(mCoordinates);
951        if (mPopupPreviewY + mCoordinates[1] < 0) {
952            // If the key you're pressing is on the left side of the keyboard, show the popup on
953            // the right, offset by enough to see at least one key to the left/right.
954            if (key.x + key.width <= getWidth() / 2) {
955                mPopupPreviewX += (int) (key.width * 2.5);
956            } else {
957                mPopupPreviewX -= (int) (key.width * 2.5);
958            }
959            mPopupPreviewY += popupHeight;
960        }
961
962        if (previewPopup.isShowing()) {
963            previewPopup.update(mPopupPreviewX, mPopupPreviewY,
964                    popupWidth, popupHeight);
965        } else {
966            previewPopup.setWidth(popupWidth);
967            previewPopup.setHeight(popupHeight);
968            previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
969                    mPopupPreviewX, mPopupPreviewY);
970        }
971        mPreviewText.setVisibility(VISIBLE);
972    }
973
974    private void sendAccessibilityEventForUnicodeCharacter(int eventType, int code) {
975        if (mAccessibilityManager.isEnabled()) {
976            AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
977            onInitializeAccessibilityEvent(event);
978            String text = null;
979            // This is very efficient since the properties are cached.
980            final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
981                    Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
982            // Add text only if password announcement is enabled or if headset is
983            // used to avoid leaking passwords.
984            if (speakPassword || mAudioManager.isBluetoothA2dpOn()
985                    || mAudioManager.isWiredHeadsetOn()) {
986                switch (code) {
987                    case Keyboard.KEYCODE_ALT:
988                        text = mContext.getString(R.string.keyboardview_keycode_alt);
989                        break;
990                    case Keyboard.KEYCODE_CANCEL:
991                        text = mContext.getString(R.string.keyboardview_keycode_cancel);
992                        break;
993                    case Keyboard.KEYCODE_DELETE:
994                        text = mContext.getString(R.string.keyboardview_keycode_delete);
995                        break;
996                    case Keyboard.KEYCODE_DONE:
997                        text = mContext.getString(R.string.keyboardview_keycode_done);
998                        break;
999                    case Keyboard.KEYCODE_MODE_CHANGE:
1000                        text = mContext.getString(R.string.keyboardview_keycode_mode_change);
1001                        break;
1002                    case Keyboard.KEYCODE_SHIFT:
1003                        text = mContext.getString(R.string.keyboardview_keycode_shift);
1004                        break;
1005                    case '\n':
1006                        text = mContext.getString(R.string.keyboardview_keycode_enter);
1007                        break;
1008                    default:
1009                        text = String.valueOf((char) code);
1010                }
1011            } else if (!mHeadsetRequiredToHearPasswordsAnnounced) {
1012                // We want the waring for required head set to be send with both the
1013                // hover enter and hover exit event, so set the flag after the exit.
1014                if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT) {
1015                    mHeadsetRequiredToHearPasswordsAnnounced = true;
1016                }
1017                text = mContext.getString(R.string.keyboard_headset_required_to_hear_password);
1018            } else {
1019                text = mContext.getString(R.string.keyboard_password_character_no_headset);
1020            }
1021            event.getText().add(text);
1022            mAccessibilityManager.sendAccessibilityEvent(event);
1023        }
1024    }
1025
1026    /**
1027     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1028     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1029     * draws the cached buffer.
1030     * @see #invalidateKey(int)
1031     */
1032    public void invalidateAllKeys() {
1033        mDirtyRect.union(0, 0, getWidth(), getHeight());
1034        mDrawPending = true;
1035        invalidate();
1036    }
1037
1038    /**
1039     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1040     * one key is changing it's content. Any changes that affect the position or size of the key
1041     * may not be honored.
1042     * @param keyIndex the index of the key in the attached {@link Keyboard}.
1043     * @see #invalidateAllKeys
1044     */
1045    public void invalidateKey(int keyIndex) {
1046        if (mKeys == null) return;
1047        if (keyIndex < 0 || keyIndex >= mKeys.length) {
1048            return;
1049        }
1050        final Key key = mKeys[keyIndex];
1051        mInvalidatedKey = key;
1052        mDirtyRect.union(key.x + mPaddingLeft, key.y + mPaddingTop,
1053                key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
1054        onBufferDraw();
1055        invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
1056                key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
1057    }
1058
1059    private boolean openPopupIfRequired(MotionEvent me) {
1060        // Check if we have a popup layout specified first.
1061        if (mPopupLayout == 0) {
1062            return false;
1063        }
1064        if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
1065            return false;
1066        }
1067
1068        Key popupKey = mKeys[mCurrentKey];
1069        boolean result = onLongPress(popupKey);
1070        if (result) {
1071            mAbortKey = true;
1072            showPreview(NOT_A_KEY);
1073        }
1074        return result;
1075    }
1076
1077    /**
1078     * Called when a key is long pressed. By default this will open any popup keyboard associated
1079     * with this key through the attributes popupLayout and popupCharacters.
1080     * @param popupKey the key that was long pressed
1081     * @return true if the long press is handled, false otherwise. Subclasses should call the
1082     * method on the base class if the subclass doesn't wish to handle the call.
1083     */
1084    protected boolean onLongPress(Key popupKey) {
1085        int popupKeyboardId = popupKey.popupResId;
1086
1087        if (popupKeyboardId != 0) {
1088            mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
1089            if (mMiniKeyboardContainer == null) {
1090                LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
1091                        Context.LAYOUT_INFLATER_SERVICE);
1092                mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
1093                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1094                        com.android.internal.R.id.keyboardView);
1095                View closeButton = mMiniKeyboardContainer.findViewById(
1096                        com.android.internal.R.id.closeButton);
1097                if (closeButton != null) closeButton.setOnClickListener(this);
1098                mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
1099                    public void onKey(int primaryCode, int[] keyCodes) {
1100                        mKeyboardActionListener.onKey(primaryCode, keyCodes);
1101                        dismissPopupKeyboard();
1102                    }
1103
1104                    public void onText(CharSequence text) {
1105                        mKeyboardActionListener.onText(text);
1106                        dismissPopupKeyboard();
1107                    }
1108
1109                    public void swipeLeft() { }
1110                    public void swipeRight() { }
1111                    public void swipeUp() { }
1112                    public void swipeDown() { }
1113                    public void onPress(int primaryCode) {
1114                        mKeyboardActionListener.onPress(primaryCode);
1115                    }
1116                    public void onRelease(int primaryCode) {
1117                        mKeyboardActionListener.onRelease(primaryCode);
1118                    }
1119                });
1120                //mInputView.setSuggest(mSuggest);
1121                Keyboard keyboard;
1122                if (popupKey.popupCharacters != null) {
1123                    keyboard = new Keyboard(getContext(), popupKeyboardId,
1124                            popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
1125                } else {
1126                    keyboard = new Keyboard(getContext(), popupKeyboardId);
1127                }
1128                mMiniKeyboard.setKeyboard(keyboard);
1129                mMiniKeyboard.setPopupParent(this);
1130                mMiniKeyboardContainer.measure(
1131                        MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1132                        MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1133
1134                mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
1135            } else {
1136                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
1137                        com.android.internal.R.id.keyboardView);
1138            }
1139            getLocationInWindow(mCoordinates);
1140            mPopupX = popupKey.x + mPaddingLeft;
1141            mPopupY = popupKey.y + mPaddingTop;
1142            mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
1143            mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
1144            final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mCoordinates[0];
1145            final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mCoordinates[1];
1146            mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
1147            mMiniKeyboard.setShifted(isShifted());
1148            mPopupKeyboard.setContentView(mMiniKeyboardContainer);
1149            mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
1150            mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
1151            mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1152            mMiniKeyboardOnScreen = true;
1153            //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
1154            invalidateAllKeys();
1155            return true;
1156        }
1157        return false;
1158    }
1159
1160    @Override
1161    public boolean onHoverEvent(MotionEvent event) {
1162        if (mAccessibilityManager.isTouchExplorationEnabled() && event.getPointerCount() == 1) {
1163            final int action = event.getAction();
1164            switch (action) {
1165                case MotionEvent.ACTION_HOVER_ENTER: {
1166                    event.setAction(MotionEvent.ACTION_DOWN);
1167                } break;
1168                case MotionEvent.ACTION_HOVER_MOVE: {
1169                    event.setAction(MotionEvent.ACTION_MOVE);
1170                } break;
1171                case MotionEvent.ACTION_HOVER_EXIT: {
1172                    event.setAction(MotionEvent.ACTION_UP);
1173                } break;
1174            }
1175            return onTouchEvent(event);
1176        }
1177        return true;
1178    }
1179
1180    @Override
1181    public boolean onTouchEvent(MotionEvent me) {
1182        // Convert multi-pointer up/down events to single up/down events to
1183        // deal with the typical multi-pointer behavior of two-thumb typing
1184        final int pointerCount = me.getPointerCount();
1185        final int action = me.getAction();
1186        boolean result = false;
1187        final long now = me.getEventTime();
1188
1189        if (pointerCount != mOldPointerCount) {
1190            if (pointerCount == 1) {
1191                // Send a down event for the latest pointer
1192                MotionEvent down = MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN,
1193                        me.getX(), me.getY(), me.getMetaState());
1194                result = onModifiedTouchEvent(down, false);
1195                down.recycle();
1196                // If it's an up action, then deliver the up as well.
1197                if (action == MotionEvent.ACTION_UP) {
1198                    result = onModifiedTouchEvent(me, true);
1199                }
1200            } else {
1201                // Send an up event for the last pointer
1202                MotionEvent up = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP,
1203                        mOldPointerX, mOldPointerY, me.getMetaState());
1204                result = onModifiedTouchEvent(up, true);
1205                up.recycle();
1206            }
1207        } else {
1208            if (pointerCount == 1) {
1209                result = onModifiedTouchEvent(me, false);
1210                mOldPointerX = me.getX();
1211                mOldPointerY = me.getY();
1212            } else {
1213                // Don't do anything when 2 pointers are down and moving.
1214                result = true;
1215            }
1216        }
1217        mOldPointerCount = pointerCount;
1218
1219        return result;
1220    }
1221
1222    private boolean onModifiedTouchEvent(MotionEvent me, boolean possiblePoly) {
1223        int touchX = (int) me.getX() - mPaddingLeft;
1224        int touchY = (int) me.getY() - mPaddingTop;
1225        if (touchY >= -mVerticalCorrection)
1226            touchY += mVerticalCorrection;
1227        final int action = me.getAction();
1228        final long eventTime = me.getEventTime();
1229        int keyIndex = getKeyIndices(touchX, touchY, null);
1230        mPossiblePoly = possiblePoly;
1231
1232        // Track the last few movements to look for spurious swipes.
1233        if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear();
1234        mSwipeTracker.addMovement(me);
1235
1236        // Ignore all motion events until a DOWN.
1237        if (mAbortKey
1238                && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) {
1239            return true;
1240        }
1241
1242        if (mGestureDetector.onTouchEvent(me)) {
1243            showPreview(NOT_A_KEY);
1244            mHandler.removeMessages(MSG_REPEAT);
1245            mHandler.removeMessages(MSG_LONGPRESS);
1246            return true;
1247        }
1248
1249        // Needs to be called after the gesture detector gets a turn, as it may have
1250        // displayed the mini keyboard
1251        if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) {
1252            return true;
1253        }
1254
1255        switch (action) {
1256            case MotionEvent.ACTION_DOWN:
1257                mAbortKey = false;
1258                mStartX = touchX;
1259                mStartY = touchY;
1260                mLastCodeX = touchX;
1261                mLastCodeY = touchY;
1262                mLastKeyTime = 0;
1263                mCurrentKeyTime = 0;
1264                mLastKey = NOT_A_KEY;
1265                mCurrentKey = keyIndex;
1266                mDownKey = keyIndex;
1267                mDownTime = me.getEventTime();
1268                mLastMoveTime = mDownTime;
1269                checkMultiTap(eventTime, keyIndex);
1270                mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
1271                        mKeys[keyIndex].codes[0] : 0);
1272                if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
1273                    mRepeatKeyIndex = mCurrentKey;
1274                    Message msg = mHandler.obtainMessage(MSG_REPEAT);
1275                    mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
1276                    repeatKey();
1277                    // Delivering the key could have caused an abort
1278                    if (mAbortKey) {
1279                        mRepeatKeyIndex = NOT_A_KEY;
1280                        break;
1281                    }
1282                }
1283                if (mCurrentKey != NOT_A_KEY) {
1284                    Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1285                    mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1286                }
1287                showPreview(keyIndex);
1288                break;
1289
1290            case MotionEvent.ACTION_MOVE:
1291                boolean continueLongPress = false;
1292                if (keyIndex != NOT_A_KEY) {
1293                    if (mCurrentKey == NOT_A_KEY) {
1294                        mCurrentKey = keyIndex;
1295                        mCurrentKeyTime = eventTime - mDownTime;
1296                    } else {
1297                        if (keyIndex == mCurrentKey) {
1298                            mCurrentKeyTime += eventTime - mLastMoveTime;
1299                            continueLongPress = true;
1300                        } else if (mRepeatKeyIndex == NOT_A_KEY) {
1301                            resetMultiTap();
1302                            mLastKey = mCurrentKey;
1303                            mLastCodeX = mLastX;
1304                            mLastCodeY = mLastY;
1305                            mLastKeyTime =
1306                                    mCurrentKeyTime + eventTime - mLastMoveTime;
1307                            mCurrentKey = keyIndex;
1308                            mCurrentKeyTime = 0;
1309                        }
1310                    }
1311                }
1312                if (!continueLongPress) {
1313                    // Cancel old longpress
1314                    mHandler.removeMessages(MSG_LONGPRESS);
1315                    // Start new longpress if key has changed
1316                    if (keyIndex != NOT_A_KEY) {
1317                        Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1318                        mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1319                    }
1320                }
1321                showPreview(mCurrentKey);
1322                mLastMoveTime = eventTime;
1323                break;
1324
1325            case MotionEvent.ACTION_UP:
1326                removeMessages();
1327                if (keyIndex == mCurrentKey) {
1328                    mCurrentKeyTime += eventTime - mLastMoveTime;
1329                } else {
1330                    resetMultiTap();
1331                    mLastKey = mCurrentKey;
1332                    mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1333                    mCurrentKey = keyIndex;
1334                    mCurrentKeyTime = 0;
1335                }
1336                if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME
1337                        && mLastKey != NOT_A_KEY) {
1338                    mCurrentKey = mLastKey;
1339                    touchX = mLastCodeX;
1340                    touchY = mLastCodeY;
1341                }
1342                showPreview(NOT_A_KEY);
1343                Arrays.fill(mKeyIndices, NOT_A_KEY);
1344                // If we're not on a repeating key (which sends on a DOWN event)
1345                if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
1346                    detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
1347                }
1348                invalidateKey(keyIndex);
1349                mRepeatKeyIndex = NOT_A_KEY;
1350                break;
1351            case MotionEvent.ACTION_CANCEL:
1352                removeMessages();
1353                dismissPopupKeyboard();
1354                mAbortKey = true;
1355                showPreview(NOT_A_KEY);
1356                invalidateKey(mCurrentKey);
1357                break;
1358        }
1359        mLastX = touchX;
1360        mLastY = touchY;
1361        return true;
1362    }
1363
1364    private boolean repeatKey() {
1365        Key key = mKeys[mRepeatKeyIndex];
1366        detectAndSendKey(mCurrentKey, key.x, key.y, mLastTapTime);
1367        return true;
1368    }
1369
1370    protected void swipeRight() {
1371        mKeyboardActionListener.swipeRight();
1372    }
1373
1374    protected void swipeLeft() {
1375        mKeyboardActionListener.swipeLeft();
1376    }
1377
1378    protected void swipeUp() {
1379        mKeyboardActionListener.swipeUp();
1380    }
1381
1382    protected void swipeDown() {
1383        mKeyboardActionListener.swipeDown();
1384    }
1385
1386    public void closing() {
1387        if (mPreviewPopup.isShowing()) {
1388            mPreviewPopup.dismiss();
1389        }
1390        removeMessages();
1391
1392        dismissPopupKeyboard();
1393        mBuffer = null;
1394        mCanvas = null;
1395        mMiniKeyboardCache.clear();
1396    }
1397
1398    private void removeMessages() {
1399        mHandler.removeMessages(MSG_REPEAT);
1400        mHandler.removeMessages(MSG_LONGPRESS);
1401        mHandler.removeMessages(MSG_SHOW_PREVIEW);
1402    }
1403
1404    @Override
1405    public void onDetachedFromWindow() {
1406        super.onDetachedFromWindow();
1407        closing();
1408    }
1409
1410    private void dismissPopupKeyboard() {
1411        if (mPopupKeyboard.isShowing()) {
1412            mPopupKeyboard.dismiss();
1413            mMiniKeyboardOnScreen = false;
1414            invalidateAllKeys();
1415        }
1416    }
1417
1418    public boolean handleBack() {
1419        if (mPopupKeyboard.isShowing()) {
1420            dismissPopupKeyboard();
1421            return true;
1422        }
1423        return false;
1424    }
1425
1426    private void resetMultiTap() {
1427        mLastSentIndex = NOT_A_KEY;
1428        mTapCount = 0;
1429        mLastTapTime = -1;
1430        mInMultiTap = false;
1431    }
1432
1433    private void checkMultiTap(long eventTime, int keyIndex) {
1434        if (keyIndex == NOT_A_KEY) return;
1435        Key key = mKeys[keyIndex];
1436        if (key.codes.length > 1) {
1437            mInMultiTap = true;
1438            if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1439                    && keyIndex == mLastSentIndex) {
1440                mTapCount = (mTapCount + 1) % key.codes.length;
1441                return;
1442            } else {
1443                mTapCount = -1;
1444                return;
1445            }
1446        }
1447        if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1448            resetMultiTap();
1449        }
1450    }
1451
1452    private static class SwipeTracker {
1453
1454        static final int NUM_PAST = 4;
1455        static final int LONGEST_PAST_TIME = 200;
1456
1457        final float mPastX[] = new float[NUM_PAST];
1458        final float mPastY[] = new float[NUM_PAST];
1459        final long mPastTime[] = new long[NUM_PAST];
1460
1461        float mYVelocity;
1462        float mXVelocity;
1463
1464        public void clear() {
1465            mPastTime[0] = 0;
1466        }
1467
1468        public void addMovement(MotionEvent ev) {
1469            long time = ev.getEventTime();
1470            final int N = ev.getHistorySize();
1471            for (int i=0; i<N; i++) {
1472                addPoint(ev.getHistoricalX(i), ev.getHistoricalY(i),
1473                        ev.getHistoricalEventTime(i));
1474            }
1475            addPoint(ev.getX(), ev.getY(), time);
1476        }
1477
1478        private void addPoint(float x, float y, long time) {
1479            int drop = -1;
1480            int i;
1481            final long[] pastTime = mPastTime;
1482            for (i=0; i<NUM_PAST; i++) {
1483                if (pastTime[i] == 0) {
1484                    break;
1485                } else if (pastTime[i] < time-LONGEST_PAST_TIME) {
1486                    drop = i;
1487                }
1488            }
1489            if (i == NUM_PAST && drop < 0) {
1490                drop = 0;
1491            }
1492            if (drop == i) drop--;
1493            final float[] pastX = mPastX;
1494            final float[] pastY = mPastY;
1495            if (drop >= 0) {
1496                final int start = drop+1;
1497                final int count = NUM_PAST-drop-1;
1498                System.arraycopy(pastX, start, pastX, 0, count);
1499                System.arraycopy(pastY, start, pastY, 0, count);
1500                System.arraycopy(pastTime, start, pastTime, 0, count);
1501                i -= (drop+1);
1502            }
1503            pastX[i] = x;
1504            pastY[i] = y;
1505            pastTime[i] = time;
1506            i++;
1507            if (i < NUM_PAST) {
1508                pastTime[i] = 0;
1509            }
1510        }
1511
1512        public void computeCurrentVelocity(int units) {
1513            computeCurrentVelocity(units, Float.MAX_VALUE);
1514        }
1515
1516        public void computeCurrentVelocity(int units, float maxVelocity) {
1517            final float[] pastX = mPastX;
1518            final float[] pastY = mPastY;
1519            final long[] pastTime = mPastTime;
1520
1521            final float oldestX = pastX[0];
1522            final float oldestY = pastY[0];
1523            final long oldestTime = pastTime[0];
1524            float accumX = 0;
1525            float accumY = 0;
1526            int N=0;
1527            while (N < NUM_PAST) {
1528                if (pastTime[N] == 0) {
1529                    break;
1530                }
1531                N++;
1532            }
1533
1534            for (int i=1; i < N; i++) {
1535                final int dur = (int)(pastTime[i] - oldestTime);
1536                if (dur == 0) continue;
1537                float dist = pastX[i] - oldestX;
1538                float vel = (dist/dur) * units;   // pixels/frame.
1539                if (accumX == 0) accumX = vel;
1540                else accumX = (accumX + vel) * .5f;
1541
1542                dist = pastY[i] - oldestY;
1543                vel = (dist/dur) * units;   // pixels/frame.
1544                if (accumY == 0) accumY = vel;
1545                else accumY = (accumY + vel) * .5f;
1546            }
1547            mXVelocity = accumX < 0.0f ? Math.max(accumX, -maxVelocity)
1548                    : Math.min(accumX, maxVelocity);
1549            mYVelocity = accumY < 0.0f ? Math.max(accumY, -maxVelocity)
1550                    : Math.min(accumY, maxVelocity);
1551        }
1552
1553        public float getXVelocity() {
1554            return mXVelocity;
1555        }
1556
1557        public float getYVelocity() {
1558            return mYVelocity;
1559        }
1560    }
1561}
1562