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