KeyboardView.java revision 3001a035439d8134a7d70d796376d1dfbff3cdcd
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 com.android.internal.R;
20
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.Rect;
27import android.graphics.Typeface;
28import android.graphics.Paint.Align;
29import android.graphics.drawable.Drawable;
30import android.inputmethodservice.Keyboard.Key;
31import android.os.Handler;
32import android.os.Message;
33import android.os.Vibrator;
34import android.preference.PreferenceManager;
35import android.util.AttributeSet;
36import android.view.GestureDetector;
37import android.view.Gravity;
38import android.view.LayoutInflater;
39import android.view.MotionEvent;
40import android.view.View;
41import android.view.ViewConfiguration;
42import android.view.ViewGroup.LayoutParams;
43import android.widget.Button;
44import android.widget.PopupWindow;
45import android.widget.TextView;
46
47import java.util.Arrays;
48import java.util.HashMap;
49import java.util.List;
50import java.util.Map;
51
52/**
53 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and
54 * detecting key presses and touch movements.
55 *
56 * @attr ref android.R.styleable#KeyboardView_keyBackground
57 * @attr ref android.R.styleable#KeyboardView_keyPreviewLayout
58 * @attr ref android.R.styleable#KeyboardView_keyPreviewOffset
59 * @attr ref android.R.styleable#KeyboardView_labelTextSize
60 * @attr ref android.R.styleable#KeyboardView_keyTextSize
61 * @attr ref android.R.styleable#KeyboardView_keyTextColor
62 * @attr ref android.R.styleable#KeyboardView_verticalCorrection
63 * @attr ref android.R.styleable#KeyboardView_popupLayout
64 */
65public class KeyboardView extends View implements View.OnClickListener {
66
67    /**
68     * Listener for virtual keyboard events.
69     */
70    public interface OnKeyboardActionListener {
71
72        /**
73         * Called when the user presses a key. This is sent before the {@link #onKey} is called.
74         * For keys that repeat, this is only called once.
75         * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid
76         * key, the value will be zero.
77         * @hide Pending API Council approval
78         */
79        void onPress(int primaryCode);
80
81        /**
82         * Called when the user releases a key. This is sent after the {@link #onKey} is called.
83         * For keys that repeat, this is only called once.
84         * @param primaryCode the code of the key that was released
85         * @hide Pending API Council approval
86         */
87        void onRelease(int primaryCode);
88
89        /**
90         * Send a key press to the listener.
91         * @param primaryCode this is the key that was pressed
92         * @param keyCodes the codes for all the possible alternative keys
93         * with the primary code being the first. If the primary key code is
94         * a single character such as an alphabet or number or symbol, the alternatives
95         * will include other characters that may be on the same key or adjacent keys.
96         * These codes are useful to correct for accidental presses of a key adjacent to
97         * the intended key.
98         */
99        void onKey(int primaryCode, int[] keyCodes);
100
101        /**
102         * Called when the user quickly moves the finger from right to left.
103         */
104        void swipeLeft();
105
106        /**
107         * Called when the user quickly moves the finger from left to right.
108         */
109        void swipeRight();
110
111        /**
112         * Called when the user quickly moves the finger from up to down.
113         */
114        void swipeDown();
115
116        /**
117         * Called when the user quickly moves the finger from down to up.
118         */
119        void swipeUp();
120    }
121
122    private static final boolean DEBUG = false;
123    private static final int NOT_A_KEY = -1;
124    private static final int[] KEY_DELETE = { Keyboard.KEYCODE_DELETE };
125    private static final int[] LONG_PRESSABLE_STATE_SET = { R.attr.state_long_pressable };
126
127    private Keyboard mKeyboard;
128    private int mCurrentKeyIndex = NOT_A_KEY;
129    private int mLabelTextSize;
130    private int mKeyTextSize;
131    private int mKeyTextColor;
132    private float mShadowRadius;
133    private int mShadowColor;
134    private float mBackgroundDimAmount;
135
136    private TextView mPreviewText;
137    private PopupWindow mPreviewPopup;
138    private int mPreviewTextSizeLarge;
139    private int mPreviewOffset;
140    private int mPreviewHeight;
141    private int[] mOffsetInWindow;
142
143    private PopupWindow mPopupKeyboard;
144    private View mMiniKeyboardContainer;
145    private KeyboardView mMiniKeyboard;
146    private boolean mMiniKeyboardOnScreen;
147    private View mPopupParent;
148    private int mMiniKeyboardOffsetX;
149    private int mMiniKeyboardOffsetY;
150    private Map<Key,View> mMiniKeyboardCache;
151    private int[] mWindowOffset;
152    private Key[] mKeys;
153
154    /** Listener for {@link OnKeyboardActionListener}. */
155    private OnKeyboardActionListener mKeyboardActionListener;
156
157    private static final int MSG_SHOW_PREVIEW = 1;
158    private static final int MSG_REMOVE_PREVIEW = 2;
159    private static final int MSG_REPEAT = 3;
160    private static final int MSG_LONGPRESS = 4;
161
162    private static final int DELAY_BEFORE_PREVIEW = 70;
163    private static final int DELAY_AFTER_PREVIEW = 60;
164
165    private int mVerticalCorrection;
166    private int mProximityThreshold;
167
168    private boolean mPreviewCentered = false;
169    private boolean mShowPreview = true;
170    private boolean mShowTouchPoints = true;
171    private int mPopupPreviewX;
172    private int mPopupPreviewY;
173
174    private int mLastX;
175    private int mLastY;
176    private int mStartX;
177    private int mStartY;
178
179    private boolean mProximityCorrectOn;
180
181    private Paint mPaint;
182    private Rect mPadding;
183
184    private long mDownTime;
185    private long mLastMoveTime;
186    private int mLastKey;
187    private int mLastCodeX;
188    private int mLastCodeY;
189    private int mCurrentKey = NOT_A_KEY;
190    private long mLastKeyTime;
191    private long mCurrentKeyTime;
192    private int[] mKeyIndices = new int[12];
193    private GestureDetector mGestureDetector;
194    private int mPopupX;
195    private int mPopupY;
196    private int mRepeatKeyIndex = NOT_A_KEY;
197    private int mPopupLayout;
198    private boolean mAbortKey;
199    private Key mInvalidatedKey;
200    private Rect mClipRegion = new Rect(0, 0, 0, 0);
201
202    private Drawable mKeyBackground;
203
204    private static final int REPEAT_INTERVAL = 50; // ~20 keys per second
205    private static final int REPEAT_START_DELAY = 400;
206    private static final int LONGPRESS_TIMEOUT = 800;
207    // Deemed to be too short : ViewConfiguration.getLongPressTimeout();
208
209    private static int MAX_NEARBY_KEYS = 12;
210    private int[] mDistances = new int[MAX_NEARBY_KEYS];
211
212    // For multi-tap
213    private int mLastSentIndex;
214    private int mTapCount;
215    private long mLastTapTime;
216    private boolean mInMultiTap;
217    private static final int MULTITAP_INTERVAL = 800; // milliseconds
218    private StringBuilder mPreviewLabel = new StringBuilder(1);
219
220    Handler mHandler = new Handler() {
221        @Override
222        public void handleMessage(Message msg) {
223            switch (msg.what) {
224                case MSG_SHOW_PREVIEW:
225                    showKey(msg.arg1);
226                    break;
227                case MSG_REMOVE_PREVIEW:
228                    mPreviewText.setVisibility(INVISIBLE);
229                    break;
230                case MSG_REPEAT:
231                    if (repeatKey()) {
232                        Message repeat = Message.obtain(this, MSG_REPEAT);
233                        sendMessageDelayed(repeat, REPEAT_INTERVAL);
234                    }
235                    break;
236                case MSG_LONGPRESS:
237                    openPopupIfRequired((MotionEvent) msg.obj);
238                    break;
239            }
240        }
241    };
242
243    public KeyboardView(Context context, AttributeSet attrs) {
244        this(context, attrs, com.android.internal.R.attr.keyboardViewStyle);
245    }
246
247    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
248        super(context, attrs, defStyle);
249
250        TypedArray a =
251            context.obtainStyledAttributes(
252                attrs, android.R.styleable.KeyboardView, defStyle, 0);
253
254        LayoutInflater inflate =
255                (LayoutInflater) context
256                        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
257
258        int previewLayout = 0;
259        int keyTextSize = 0;
260
261        int n = a.getIndexCount();
262
263        for (int i = 0; i < n; i++) {
264            int attr = a.getIndex(i);
265
266            switch (attr) {
267            case com.android.internal.R.styleable.KeyboardView_keyBackground:
268                mKeyBackground = a.getDrawable(attr);
269                break;
270            case com.android.internal.R.styleable.KeyboardView_verticalCorrection:
271                mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
272                break;
273            case com.android.internal.R.styleable.KeyboardView_keyPreviewLayout:
274                previewLayout = a.getResourceId(attr, 0);
275                break;
276            case com.android.internal.R.styleable.KeyboardView_keyPreviewOffset:
277                mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
278                break;
279            case com.android.internal.R.styleable.KeyboardView_keyPreviewHeight:
280                mPreviewHeight = a.getDimensionPixelSize(attr, 80);
281                break;
282            case com.android.internal.R.styleable.KeyboardView_keyTextSize:
283                mKeyTextSize = a.getDimensionPixelSize(attr, 18);
284                break;
285            case com.android.internal.R.styleable.KeyboardView_keyTextColor:
286                mKeyTextColor = a.getColor(attr, 0xFF000000);
287                break;
288            case com.android.internal.R.styleable.KeyboardView_labelTextSize:
289                mLabelTextSize = a.getDimensionPixelSize(attr, 14);
290                break;
291            case com.android.internal.R.styleable.KeyboardView_popupLayout:
292                mPopupLayout = a.getResourceId(attr, 0);
293                break;
294            case com.android.internal.R.styleable.KeyboardView_shadowColor:
295                mShadowColor = a.getColor(attr, 0);
296                break;
297            case com.android.internal.R.styleable.KeyboardView_shadowRadius:
298                mShadowRadius = a.getFloat(attr, 0f);
299                break;
300            }
301        }
302
303        a = mContext.obtainStyledAttributes(
304                com.android.internal.R.styleable.Theme);
305        mBackgroundDimAmount = a.getFloat(android.R.styleable.Theme_backgroundDimAmount, 0.5f);
306
307        mPreviewPopup = new PopupWindow(context);
308        if (previewLayout != 0) {
309            mPreviewText = (TextView) inflate.inflate(previewLayout, null);
310            mPreviewTextSizeLarge = (int) mPreviewText.getTextSize();
311            mPreviewPopup.setContentView(mPreviewText);
312            mPreviewPopup.setBackgroundDrawable(null);
313        } else {
314            mShowPreview = false;
315        }
316
317        mPreviewPopup.setTouchable(false);
318
319        mPopupKeyboard = new PopupWindow(context);
320        mPopupKeyboard.setBackgroundDrawable(null);
321        //mPopupKeyboard.setClippingEnabled(false);
322
323        mPopupParent = this;
324        //mPredicting = true;
325
326        mPaint = new Paint();
327        mPaint.setAntiAlias(true);
328        mPaint.setTextSize(keyTextSize);
329        mPaint.setTextAlign(Align.CENTER);
330
331        mPadding = new Rect(0, 0, 0, 0);
332        mMiniKeyboardCache = new HashMap<Key,View>();
333        mKeyBackground.getPadding(mPadding);
334
335        resetMultiTap();
336        initGestureDetector();
337    }
338
339    private void initGestureDetector() {
340        mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
341            @Override
342            public boolean onFling(MotionEvent me1, MotionEvent me2,
343                    float velocityX, float velocityY) {
344                final float absX = Math.abs(velocityX);
345                final float absY = Math.abs(velocityY);
346                if (velocityX > 500 && absY < absX) {
347                    swipeRight();
348                    return true;
349                } else if (velocityX < -500 && absY < absX) {
350                    swipeLeft();
351                    return true;
352                } else if (velocityY < -500 && absX < absY) {
353                    swipeUp();
354                    return true;
355                } else if (velocityY > 500 && absX < 200) {
356                    swipeDown();
357                    return true;
358                } else if (absX > 800 || absY > 800) {
359                    return true;
360                }
361                return false;
362            }
363        });
364
365        mGestureDetector.setIsLongpressEnabled(false);
366    }
367
368    public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
369        mKeyboardActionListener = listener;
370    }
371
372    /**
373     * Returns the {@link OnKeyboardActionListener} object.
374     * @return the listener attached to this keyboard
375     */
376    protected OnKeyboardActionListener getOnKeyboardActionListener() {
377        return mKeyboardActionListener;
378    }
379
380    /**
381     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
382     * view will re-layout itself to accommodate the keyboard.
383     * @see Keyboard
384     * @see #getKeyboard()
385     * @param keyboard the keyboard to display in this view
386     */
387    public void setKeyboard(Keyboard keyboard) {
388        if (mKeyboard != null) {
389            showPreview(NOT_A_KEY);
390        }
391        mKeyboard = keyboard;
392        List<Key> keys = mKeyboard.getKeys();
393        mKeys = keys.toArray(new Key[keys.size()]);
394        requestLayout();
395        invalidate();
396        computeProximityThreshold(keyboard);
397    }
398
399    /**
400     * Returns the current keyboard being displayed by this view.
401     * @return the currently attached keyboard
402     * @see #setKeyboard(Keyboard)
403     */
404    public Keyboard getKeyboard() {
405        return mKeyboard;
406    }
407
408    /**
409     * Sets the state of the shift key of the keyboard, if any.
410     * @param shifted whether or not to enable the state of the shift key
411     * @return true if the shift key state changed, false if there was no change
412     * @see KeyboardView#isShifted()
413     */
414    public boolean setShifted(boolean shifted) {
415        if (mKeyboard != null) {
416            if (mKeyboard.setShifted(shifted)) {
417                // The whole keyboard probably needs to be redrawn
418                invalidate();
419                return true;
420            }
421        }
422        return false;
423    }
424
425    /**
426     * Returns the state of the shift key of the keyboard, if any.
427     * @return true if the shift is in a pressed state, false otherwise. If there is
428     * no shift key on the keyboard or there is no keyboard attached, it returns false.
429     * @see KeyboardView#setShifted(boolean)
430     */
431    public boolean isShifted() {
432        if (mKeyboard != null) {
433            return mKeyboard.isShifted();
434        }
435        return false;
436    }
437
438    /**
439     * Enables or disables the key feedback popup. This is a popup that shows a magnified
440     * version of the depressed key. By default the preview is enabled.
441     * @param previewEnabled whether or not to enable the key feedback popup
442     * @see #isPreviewEnabled()
443     */
444    public void setPreviewEnabled(boolean previewEnabled) {
445        mShowPreview = previewEnabled;
446    }
447
448    /**
449     * Returns the enabled state of the key feedback popup.
450     * @return whether or not the key feedback popup is enabled
451     * @see #setPreviewEnabled(boolean)
452     */
453    public boolean isPreviewEnabled() {
454        return mShowPreview;
455    }
456
457    public void setVerticalCorrection(int verticalOffset) {
458
459    }
460    public void setPopupParent(View v) {
461        mPopupParent = v;
462    }
463
464    public void setPopupOffset(int x, int y) {
465        mMiniKeyboardOffsetX = x;
466        mMiniKeyboardOffsetY = y;
467        if (mPreviewPopup.isShowing()) {
468            mPreviewPopup.dismiss();
469        }
470    }
471
472    /**
473     * Enables or disables proximity correction. When enabled, {@link OnKeyboardActionListener#onKey}
474     * gets called with key codes for adjacent keys. Otherwise only the primary code is returned.
475     * @param enabled whether or not the proximity correction is enabled
476     * @hide Pending API Council approval
477     */
478    public void setProximityCorrectionEnabled(boolean enabled) {
479        mProximityCorrectOn = enabled;
480    }
481
482    /**
483     * Returns the enabled state of the proximity correction.
484     * @return true if proximity correction is enabled, false otherwise
485     * @hide Pending API Council approval
486     */
487    public boolean isProximityCorrectionEnabled() {
488        return mProximityCorrectOn;
489    }
490
491    /**
492     * Popup keyboard close button clicked.
493     * @hide
494     */
495    public void onClick(View v) {
496        dismissPopupKeyboard();
497    }
498
499    private CharSequence adjustCase(CharSequence label) {
500        if (mKeyboard.isShifted() && label != null && label.length() == 1
501                && Character.isLowerCase(label.charAt(0))) {
502            label = label.toString().toUpperCase();
503        }
504        return label;
505    }
506
507    @Override
508    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
509        // Round up a little
510        if (mKeyboard == null) {
511            setMeasuredDimension(mPaddingLeft + mPaddingRight, mPaddingTop + mPaddingBottom);
512        } else {
513            int width = mKeyboard.getMinWidth() + mPaddingLeft + mPaddingRight;
514            if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
515                width = MeasureSpec.getSize(widthMeasureSpec);
516            }
517            setMeasuredDimension(width, mKeyboard.getHeight() + mPaddingTop + mPaddingBottom);
518        }
519    }
520
521    /**
522     * Compute the average distance between adjacent keys (horizontally and vertically)
523     * and square it to get the proximity threshold. We use a square here and in computing
524     * the touch distance from a key's center to avoid taking a square root.
525     * @param keyboard
526     */
527    private void computeProximityThreshold(Keyboard keyboard) {
528        if (keyboard == null) return;
529        final Key[] keys = mKeys;
530        if (keys == null) return;
531        int length = keys.length;
532        int dimensionSum = 0;
533        for (int i = 0; i < length; i++) {
534            Key key = keys[i];
535            dimensionSum += Math.min(key.width, key.height) + key.gap;
536        }
537        if (dimensionSum < 0 || length == 0) return;
538        mProximityThreshold = (int) (dimensionSum * 1.4f / length);
539        mProximityThreshold *= mProximityThreshold; // Square it
540    }
541
542    @Override
543    public void onDraw(Canvas canvas) {
544        super.onDraw(canvas);
545        if (mKeyboard == null) return;
546
547        final Paint paint = mPaint;
548        final Drawable keyBackground = mKeyBackground;
549        final Rect clipRegion = mClipRegion;
550        final Rect padding = mPadding;
551        final int kbdPaddingLeft = mPaddingLeft;
552        final int kbdPaddingTop = mPaddingTop;
553        final Key[] keys = mKeys;
554        final Key invalidKey = mInvalidatedKey;
555        //canvas.translate(0, mKeyboardPaddingTop);
556        paint.setAlpha(255);
557        paint.setColor(mKeyTextColor);
558        boolean drawSingleKey = false;
559        if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
560//            System.out.println("Key bounds = " + (invalidKey.x + mPaddingLeft) + ","
561//                    + (invalidKey.y + mPaddingTop) + ","
562//                    + (invalidKey.x + invalidKey.width + mPaddingLeft) + ","
563//                    + (invalidKey.y + invalidKey.height + mPaddingTop));
564//            System.out.println("Clip bounds =" + clipRegion.toShortString());
565            // Is clipRegion completely contained within the invalidated key?
566            if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
567                    invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
568                    invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
569                    invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
570                drawSingleKey = true;
571            }
572        }
573        final int keyCount = keys.length;
574        for (int i = 0; i < keyCount; i++) {
575            final Key key = keys[i];
576            if (drawSingleKey && invalidKey != key) {
577                continue;
578            }
579            int[] drawableState = key.getCurrentDrawableState();
580            keyBackground.setState(drawableState);
581
582            // Switch the character to uppercase if shift is pressed
583            String label = key.label == null? null : adjustCase(key.label).toString();
584
585            final Rect bounds = keyBackground.getBounds();
586            if (key.width != bounds.right ||
587                    key.height != bounds.bottom) {
588                keyBackground.setBounds(0, 0, key.width, key.height);
589            }
590            canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
591            keyBackground.draw(canvas);
592
593            if (label != null) {
594                // For characters, use large font. For labels like "Done", use small font.
595                if (label.length() > 1 && key.codes.length < 2) {
596                    paint.setTextSize(mLabelTextSize);
597                    paint.setTypeface(Typeface.DEFAULT_BOLD);
598                } else {
599                    paint.setTextSize(mKeyTextSize);
600                    paint.setTypeface(Typeface.DEFAULT);
601                }
602                // Draw a drop shadow for the text
603                paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
604                // Draw the text
605                canvas.drawText(label,
606                    (key.width - padding.left - padding.right) / 2
607                            + padding.left,
608                    (key.height - padding.top - padding.bottom) / 2
609                            + (paint.getTextSize() - paint.descent()) / 2 + padding.top,
610                    paint);
611                // Turn off drop shadow
612                paint.setShadowLayer(0, 0, 0, 0);
613            } else if (key.icon != null) {
614                final int drawableX = (key.width - padding.left - padding.right
615                                - key.icon.getIntrinsicWidth()) / 2 + padding.left;
616                final int drawableY = (key.height - padding.top - padding.bottom
617                        - key.icon.getIntrinsicHeight()) / 2 + padding.top;
618                canvas.translate(drawableX, drawableY);
619                key.icon.setBounds(0, 0,
620                        key.icon.getIntrinsicWidth(), key.icon.getIntrinsicHeight());
621                key.icon.draw(canvas);
622                canvas.translate(-drawableX, -drawableY);
623            }
624            canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
625        }
626        mInvalidatedKey = null;
627        // Overlay a dark rectangle to dim the keyboard
628        if (mMiniKeyboardOnScreen) {
629            paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
630            canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
631        }
632
633        if (DEBUG && mShowTouchPoints) {
634            paint.setAlpha(128);
635            paint.setColor(0xFFFF0000);
636            canvas.drawCircle(mStartX, mStartY, 3, paint);
637            canvas.drawLine(mStartX, mStartY, mLastX, mLastY, paint);
638            paint.setColor(0xFF0000FF);
639            canvas.drawCircle(mLastX, mLastY, 3, paint);
640            paint.setColor(0xFF00FF00);
641            canvas.drawCircle((mStartX + mLastX) / 2, (mStartY + mLastY) / 2, 2, paint);
642        }
643    }
644
645    private int getKeyIndices(int x, int y, int[] allKeys) {
646        final Key[] keys = mKeys;
647        final boolean shifted = mKeyboard.isShifted();
648        int primaryIndex = NOT_A_KEY;
649        int closestKey = NOT_A_KEY;
650        int closestKeyDist = mProximityThreshold + 1;
651        java.util.Arrays.fill(mDistances, Integer.MAX_VALUE);
652        int [] nearestKeyIndices = mKeyboard.getNearestKeys(x, y);
653        final int keyCount = nearestKeyIndices.length;
654        for (int i = 0; i < keyCount; i++) {
655            final Key key = keys[nearestKeyIndices[i]];
656            int dist = 0;
657            boolean isInside = key.isInside(x,y);
658            if (((mProximityCorrectOn
659                    && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold)
660                    || isInside)
661                    && key.codes[0] > 32) {
662                // Find insertion point
663                final int nCodes = key.codes.length;
664                if (dist < closestKeyDist) {
665                    closestKeyDist = dist;
666                    closestKey = nearestKeyIndices[i];
667                }
668
669                if (allKeys == null) continue;
670
671                for (int j = 0; j < mDistances.length; j++) {
672                    if (mDistances[j] > dist) {
673                        // Make space for nCodes codes
674                        System.arraycopy(mDistances, j, mDistances, j + nCodes,
675                                mDistances.length - j - nCodes);
676                        System.arraycopy(allKeys, j, allKeys, j + nCodes,
677                                allKeys.length - j - nCodes);
678                        for (int c = 0; c < nCodes; c++) {
679                            allKeys[j + c] = key.codes[c];
680                            mDistances[j + c] = dist;
681                        }
682                        break;
683                    }
684                }
685            }
686
687            if (isInside) {
688                primaryIndex = nearestKeyIndices[i];
689            }
690        }
691        if (primaryIndex == NOT_A_KEY) {
692            primaryIndex = closestKey;
693        }
694        return primaryIndex;
695    }
696
697    private void detectAndSendKey(int x, int y, long eventTime) {
698        int index = mCurrentKey;
699        if (index != NOT_A_KEY && index < mKeys.length) {
700            final Key key = mKeys[index];
701            if (key.text != null) {
702                for (int i = 0; i < key.text.length(); i++) {
703                    mKeyboardActionListener.onKey(key.text.charAt(i), key.codes);
704                }
705                mKeyboardActionListener.onRelease(NOT_A_KEY);
706            } else {
707                int code = key.codes[0];
708                //TextEntryState.keyPressedAt(key, x, y);
709                int[] codes = new int[MAX_NEARBY_KEYS];
710                Arrays.fill(codes, NOT_A_KEY);
711                getKeyIndices(x, y, codes);
712                // Multi-tap
713                if (mInMultiTap) {
714                    if (mTapCount != -1) {
715                        mKeyboardActionListener.onKey(Keyboard.KEYCODE_DELETE, KEY_DELETE);
716                    } else {
717                        mTapCount = 0;
718                    }
719                    code = key.codes[mTapCount];
720                }
721                mKeyboardActionListener.onKey(code, codes);
722                mKeyboardActionListener.onRelease(code);
723            }
724            mLastSentIndex = index;
725            mLastTapTime = eventTime;
726        }
727    }
728
729    /**
730     * Handle multi-tap keys by producing the key label for the current multi-tap state.
731     */
732    private CharSequence getPreviewText(Key key) {
733        if (mInMultiTap) {
734            // Multi-tap
735            mPreviewLabel.setLength(0);
736            mPreviewLabel.append((char) key.codes[mTapCount < 0 ? 0 : mTapCount]);
737            return adjustCase(mPreviewLabel);
738        } else {
739            return adjustCase(key.label);
740        }
741    }
742
743    private void showPreview(int keyIndex) {
744        int oldKeyIndex = mCurrentKeyIndex;
745        final PopupWindow previewPopup = mPreviewPopup;
746
747        mCurrentKeyIndex = keyIndex;
748        // Release the old key and press the new key
749        final Key[] keys = mKeys;
750        if (oldKeyIndex != mCurrentKeyIndex) {
751            if (oldKeyIndex != NOT_A_KEY && keys.length > oldKeyIndex) {
752                keys[oldKeyIndex].onReleased(mCurrentKeyIndex == NOT_A_KEY);
753                invalidateKey(oldKeyIndex);
754            }
755            if (mCurrentKeyIndex != NOT_A_KEY && keys.length > mCurrentKeyIndex) {
756                keys[mCurrentKeyIndex].onPressed();
757                invalidateKey(mCurrentKeyIndex);
758            }
759        }
760        // If key changed and preview is on ...
761        if (oldKeyIndex != mCurrentKeyIndex && mShowPreview) {
762            mHandler.removeMessages(MSG_SHOW_PREVIEW);
763            if (previewPopup.isShowing()) {
764                if (keyIndex == NOT_A_KEY) {
765                    mHandler.sendMessageDelayed(mHandler
766                            .obtainMessage(MSG_REMOVE_PREVIEW),
767                            DELAY_AFTER_PREVIEW);
768                }
769            }
770            if (keyIndex != NOT_A_KEY) {
771                if (previewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
772                    // Show right away, if it's already visible and finger is moving around
773                    showKey(keyIndex);
774                } else {
775                    mHandler.sendMessageDelayed(
776                            mHandler.obtainMessage(MSG_SHOW_PREVIEW, keyIndex, 0),
777                            DELAY_BEFORE_PREVIEW);
778                }
779            }
780        }
781    }
782
783    private void showKey(final int keyIndex) {
784        final PopupWindow previewPopup = mPreviewPopup;
785        final Key[] keys = mKeys;
786        Key key = keys[keyIndex];
787        if (key.icon != null) {
788            mPreviewText.setCompoundDrawables(null, null, null,
789                    key.iconPreview != null ? key.iconPreview : key.icon);
790            mPreviewText.setText(null);
791        } else {
792            mPreviewText.setCompoundDrawables(null, null, null, null);
793            mPreviewText.setText(getPreviewText(key));
794            if (key.label.length() > 1 && key.codes.length < 2) {
795                mPreviewText.setTextSize(mLabelTextSize);
796                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
797            } else {
798                mPreviewText.setTextSize(mPreviewTextSizeLarge);
799                mPreviewText.setTypeface(Typeface.DEFAULT);
800            }
801        }
802        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
803                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
804        int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
805                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
806        final int popupHeight = mPreviewHeight;
807        LayoutParams lp = mPreviewText.getLayoutParams();
808        if (lp != null) {
809            lp.width = popupWidth;
810            lp.height = popupHeight;
811        }
812        if (!mPreviewCentered) {
813            mPopupPreviewX = key.x - mPreviewText.getPaddingLeft() + mPaddingLeft;
814            mPopupPreviewY = key.y - popupHeight + mPreviewOffset;
815        } else {
816            // TODO: Fix this if centering is brought back
817            mPopupPreviewX = 160 - mPreviewText.getMeasuredWidth() / 2;
818            mPopupPreviewY = - mPreviewText.getMeasuredHeight();
819        }
820        mHandler.removeMessages(MSG_REMOVE_PREVIEW);
821        if (mOffsetInWindow == null) {
822            mOffsetInWindow = new int[2];
823            getLocationInWindow(mOffsetInWindow);
824            mOffsetInWindow[0] += mMiniKeyboardOffsetX; // Offset may be zero
825            mOffsetInWindow[1] += mMiniKeyboardOffsetY; // Offset may be zero
826        }
827        // Set the preview background state
828        mPreviewText.getBackground().setState(
829                key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
830        if (previewPopup.isShowing()) {
831            previewPopup.update(mPopupPreviewX + mOffsetInWindow[0],
832                    mPopupPreviewY + mOffsetInWindow[1],
833                    popupWidth, popupHeight);
834        } else {
835            previewPopup.setWidth(popupWidth);
836            previewPopup.setHeight(popupHeight);
837            previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY,
838                    mPopupPreviewX + mOffsetInWindow[0],
839                    mPopupPreviewY + mOffsetInWindow[1]);
840        }
841        mPreviewText.setVisibility(VISIBLE);
842    }
843
844    private void invalidateKey(int keyIndex) {
845        if (keyIndex < 0 || keyIndex >= mKeys.length) {
846            return;
847        }
848        final Key key = mKeys[keyIndex];
849        mInvalidatedKey = key;
850        invalidate(key.x + mPaddingLeft, key.y + mPaddingTop,
851                key.x + key.width + mPaddingLeft, key.y + key.height + mPaddingTop);
852    }
853
854    private boolean openPopupIfRequired(MotionEvent me) {
855        // Check if we have a popup layout specified first.
856        if (mPopupLayout == 0) {
857            return false;
858        }
859        if (mCurrentKey < 0 || mCurrentKey >= mKeys.length) {
860            return false;
861        }
862
863        Key popupKey = mKeys[mCurrentKey];
864        boolean result = onLongPress(popupKey);
865        if (result) {
866            mAbortKey = true;
867            showPreview(NOT_A_KEY);
868        }
869        return result;
870    }
871
872    /**
873     * Called when a key is long pressed. By default this will open any popup keyboard associated
874     * with this key through the attributes popupLayout and popupCharacters.
875     * @param popupKey the key that was long pressed
876     * @return true if the long press is handled, false otherwise. Subclasses should call the
877     * method on the base class if the subclass doesn't wish to handle the call.
878     */
879    protected boolean onLongPress(Key popupKey) {
880        int popupKeyboardId = popupKey.popupResId;
881
882        if (popupKeyboardId != 0) {
883            mMiniKeyboardContainer = mMiniKeyboardCache.get(popupKey);
884            if (mMiniKeyboardContainer == null) {
885                LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
886                        Context.LAYOUT_INFLATER_SERVICE);
887                mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null);
888                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
889                        com.android.internal.R.id.keyboardView);
890                View closeButton = mMiniKeyboardContainer.findViewById(
891                        com.android.internal.R.id.button_close);
892                if (closeButton != null) closeButton.setOnClickListener(this);
893                mMiniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
894                    public void onKey(int primaryCode, int[] keyCodes) {
895                        mKeyboardActionListener.onKey(primaryCode, keyCodes);
896                        dismissPopupKeyboard();
897                    }
898
899                    public void swipeLeft() { }
900                    public void swipeRight() { }
901                    public void swipeUp() { }
902                    public void swipeDown() { }
903                    public void onPress(int primaryCode) {
904                        mKeyboardActionListener.onPress(primaryCode);
905                    }
906                    public void onRelease(int primaryCode) {
907                        mKeyboardActionListener.onRelease(primaryCode);
908                    }
909                });
910                //mInputView.setSuggest(mSuggest);
911                Keyboard keyboard;
912                if (popupKey.popupCharacters != null) {
913                    keyboard = new Keyboard(getContext(), popupKeyboardId,
914                            popupKey.popupCharacters, -1, getPaddingLeft() + getPaddingRight());
915                } else {
916                    keyboard = new Keyboard(getContext(), popupKeyboardId);
917                }
918                mMiniKeyboard.setKeyboard(keyboard);
919                mMiniKeyboard.setPopupParent(this);
920                mMiniKeyboardContainer.measure(
921                        MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
922                        MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
923
924                mMiniKeyboardCache.put(popupKey, mMiniKeyboardContainer);
925            } else {
926                mMiniKeyboard = (KeyboardView) mMiniKeyboardContainer.findViewById(
927                        com.android.internal.R.id.keyboardView);
928            }
929            if (mWindowOffset == null) {
930                mWindowOffset = new int[2];
931                getLocationInWindow(mWindowOffset);
932            }
933            mPopupX = popupKey.x + mPaddingLeft;
934            mPopupY = popupKey.y + mPaddingTop;
935            mPopupX = mPopupX + popupKey.width - mMiniKeyboardContainer.getMeasuredWidth();
936            mPopupY = mPopupY - mMiniKeyboardContainer.getMeasuredHeight();
937            final int x = mPopupX + mMiniKeyboardContainer.getPaddingRight() + mWindowOffset[0];
938            final int y = mPopupY + mMiniKeyboardContainer.getPaddingBottom() + mWindowOffset[1];
939            mMiniKeyboard.setPopupOffset(x < 0 ? 0 : x, y);
940            mMiniKeyboard.setShifted(isShifted());
941            mPopupKeyboard.setContentView(mMiniKeyboardContainer);
942            mPopupKeyboard.setWidth(mMiniKeyboardContainer.getMeasuredWidth());
943            mPopupKeyboard.setHeight(mMiniKeyboardContainer.getMeasuredHeight());
944            mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
945            mMiniKeyboardOnScreen = true;
946            //mMiniKeyboard.onTouchEvent(getTranslatedEvent(me));
947            invalidate();
948            return true;
949        }
950        return false;
951    }
952
953    @Override
954    public boolean onTouchEvent(MotionEvent me) {
955        int touchX = (int) me.getX() - mPaddingLeft;
956        int touchY = (int) me.getY() + mVerticalCorrection - mPaddingTop;
957        int action = me.getAction();
958        long eventTime = me.getEventTime();
959        int keyIndex = getKeyIndices(touchX, touchY, null);
960
961        if (mGestureDetector.onTouchEvent(me)) {
962            showPreview(NOT_A_KEY);
963            mHandler.removeMessages(MSG_REPEAT);
964            mHandler.removeMessages(MSG_LONGPRESS);
965            return true;
966        }
967
968        // Needs to be called after the gesture detector gets a turn, as it may have
969        // displayed the mini keyboard
970        if (mMiniKeyboardOnScreen) {
971            return true;
972        }
973
974        switch (action) {
975            case MotionEvent.ACTION_DOWN:
976                mAbortKey = false;
977                mStartX = touchX;
978                mStartY = touchY;
979                mLastCodeX = touchX;
980                mLastCodeY = touchY;
981                mLastKeyTime = 0;
982                mCurrentKeyTime = 0;
983                mLastKey = NOT_A_KEY;
984                mCurrentKey = keyIndex;
985                mDownTime = me.getEventTime();
986                mLastMoveTime = mDownTime;
987                checkMultiTap(eventTime, keyIndex);
988                mKeyboardActionListener.onPress(keyIndex != NOT_A_KEY ?
989                        mKeys[keyIndex].codes[0] : 0);
990                if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) {
991                    mRepeatKeyIndex = mCurrentKey;
992                    repeatKey();
993                    Message msg = mHandler.obtainMessage(MSG_REPEAT);
994                    mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY);
995                }
996                if (mCurrentKey != NOT_A_KEY) {
997                    Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
998                    mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
999                }
1000                showPreview(keyIndex);
1001                break;
1002
1003            case MotionEvent.ACTION_MOVE:
1004                boolean continueLongPress = false;
1005                if (keyIndex != NOT_A_KEY) {
1006                    if (mCurrentKey == NOT_A_KEY) {
1007                        mCurrentKey = keyIndex;
1008                        mCurrentKeyTime = eventTime - mDownTime;
1009                    } else {
1010                        if (keyIndex == mCurrentKey) {
1011                            mCurrentKeyTime += eventTime - mLastMoveTime;
1012                            continueLongPress = true;
1013                        } else {
1014                            resetMultiTap();
1015                            mLastKey = mCurrentKey;
1016                            mLastCodeX = mLastX;
1017                            mLastCodeY = mLastY;
1018                            mLastKeyTime =
1019                                    mCurrentKeyTime + eventTime - mLastMoveTime;
1020                            mCurrentKey = keyIndex;
1021                            mCurrentKeyTime = 0;
1022                        }
1023                    }
1024                    if (keyIndex != mRepeatKeyIndex) {
1025                        mHandler.removeMessages(MSG_REPEAT);
1026                        mRepeatKeyIndex = NOT_A_KEY;
1027                    }
1028                }
1029                if (!continueLongPress) {
1030                    // Cancel old longpress
1031                    mHandler.removeMessages(MSG_LONGPRESS);
1032                    // Start new longpress if key has changed
1033                    if (keyIndex != NOT_A_KEY) {
1034                        Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me);
1035                        mHandler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT);
1036                    }
1037                }
1038                showPreview(keyIndex);
1039                break;
1040
1041            case MotionEvent.ACTION_UP:
1042                mHandler.removeMessages(MSG_SHOW_PREVIEW);
1043                mHandler.removeMessages(MSG_REPEAT);
1044                mHandler.removeMessages(MSG_LONGPRESS);
1045                if (keyIndex == mCurrentKey) {
1046                    mCurrentKeyTime += eventTime - mLastMoveTime;
1047                } else {
1048                    resetMultiTap();
1049                    mLastKey = mCurrentKey;
1050                    mLastKeyTime = mCurrentKeyTime + eventTime - mLastMoveTime;
1051                    mCurrentKey = keyIndex;
1052                    mCurrentKeyTime = 0;
1053                }
1054                if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) {
1055                    mCurrentKey = mLastKey;
1056                    touchX = mLastCodeX;
1057                    touchY = mLastCodeY;
1058                }
1059                showPreview(NOT_A_KEY);
1060                Arrays.fill(mKeyIndices, NOT_A_KEY);
1061                invalidateKey(keyIndex);
1062                // If we're not on a repeating key (which sends on a DOWN event)
1063                if (mRepeatKeyIndex == NOT_A_KEY && !mMiniKeyboardOnScreen && !mAbortKey) {
1064                    detectAndSendKey(touchX, touchY, eventTime);
1065                }
1066                mRepeatKeyIndex = NOT_A_KEY;
1067                break;
1068        }
1069        mLastX = touchX;
1070        mLastY = touchY;
1071        return true;
1072    }
1073
1074    private boolean repeatKey() {
1075        Key key = mKeys[mRepeatKeyIndex];
1076        detectAndSendKey(key.x, key.y, mLastTapTime);
1077        return true;
1078    }
1079
1080    protected void swipeRight() {
1081        mKeyboardActionListener.swipeRight();
1082    }
1083
1084    protected void swipeLeft() {
1085        mKeyboardActionListener.swipeLeft();
1086    }
1087
1088    protected void swipeUp() {
1089        mKeyboardActionListener.swipeUp();
1090    }
1091
1092    protected void swipeDown() {
1093        mKeyboardActionListener.swipeDown();
1094    }
1095
1096    public void closing() {
1097        if (mPreviewPopup.isShowing()) {
1098            mPreviewPopup.dismiss();
1099        }
1100        mHandler.removeMessages(MSG_REPEAT);
1101        mHandler.removeMessages(MSG_LONGPRESS);
1102        mHandler.removeMessages(MSG_SHOW_PREVIEW);
1103
1104        dismissPopupKeyboard();
1105    }
1106
1107    @Override
1108    public void onDetachedFromWindow() {
1109        super.onDetachedFromWindow();
1110        closing();
1111    }
1112
1113    private void dismissPopupKeyboard() {
1114        if (mPopupKeyboard.isShowing()) {
1115            mPopupKeyboard.dismiss();
1116            mMiniKeyboardOnScreen = false;
1117            invalidate();
1118        }
1119    }
1120
1121    public boolean handleBack() {
1122        if (mPopupKeyboard.isShowing()) {
1123            dismissPopupKeyboard();
1124            return true;
1125        }
1126        return false;
1127    }
1128
1129    private void resetMultiTap() {
1130        mLastSentIndex = NOT_A_KEY;
1131        mTapCount = 0;
1132        mLastTapTime = -1;
1133        mInMultiTap = false;
1134    }
1135
1136    private void checkMultiTap(long eventTime, int keyIndex) {
1137        if (keyIndex == NOT_A_KEY) return;
1138        Key key = mKeys[keyIndex];
1139        if (key.codes.length > 1) {
1140            mInMultiTap = true;
1141            if (eventTime < mLastTapTime + MULTITAP_INTERVAL
1142                    && keyIndex == mLastSentIndex) {
1143                mTapCount = (mTapCount + 1) % key.codes.length;
1144                return;
1145            } else {
1146                mTapCount = -1;
1147                return;
1148            }
1149        }
1150        if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
1151            resetMultiTap();
1152        }
1153    }
1154}
1155