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