KeyboardView.java revision ff082d081f3ea18ff0b9b22126ee4a86504cf83c
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of 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,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard;
18
19import com.android.inputmethod.latin.LatinImeLogger;
20import com.android.inputmethod.latin.R;
21
22import android.content.Context;
23import android.content.pm.PackageManager;
24import android.content.res.Resources;
25import android.content.res.TypedArray;
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Color;
29import android.graphics.Paint;
30import android.graphics.Paint.Align;
31import android.graphics.PorterDuff;
32import android.graphics.Rect;
33import android.graphics.Region.Op;
34import android.graphics.Typeface;
35import android.graphics.drawable.Drawable;
36import android.os.Handler;
37import android.os.Message;
38import android.util.AttributeSet;
39import android.util.Log;
40import android.util.TypedValue;
41import android.view.GestureDetector;
42import android.view.LayoutInflater;
43import android.view.MotionEvent;
44import android.view.View;
45import android.view.ViewConfiguration;
46import android.view.ViewGroup;
47import android.view.ViewGroup.MarginLayoutParams;
48import android.widget.FrameLayout;
49import android.widget.LinearLayout;
50import android.widget.PopupWindow;
51import android.widget.TextView;
52
53import java.util.ArrayList;
54import java.util.HashMap;
55import java.util.WeakHashMap;
56
57/**
58 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key
59 * presses and touch movements.
60 *
61 * @attr ref R.styleable#KeyboardView_keyBackground
62 * @attr ref R.styleable#KeyboardView_keyPreviewLayout
63 * @attr ref R.styleable#KeyboardView_keyPreviewOffset
64 * @attr ref R.styleable#KeyboardView_labelTextSize
65 * @attr ref R.styleable#KeyboardView_keyTextSize
66 * @attr ref R.styleable#KeyboardView_keyTextColor
67 * @attr ref R.styleable#KeyboardView_verticalCorrection
68 * @attr ref R.styleable#KeyboardView_popupLayout
69 */
70public class KeyboardView extends View implements PointerTracker.UIProxy {
71    private static final String TAG = KeyboardView.class.getSimpleName();
72    private static final boolean DEBUG_SHOW_ALIGN = false;
73    private static final boolean DEBUG_KEYBOARD_GRID = false;
74
75    private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = true;
76    private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
77
78    public static final int COLOR_SCHEME_WHITE = 0;
79    public static final int COLOR_SCHEME_BLACK = 1;
80
81    // Timing constants
82    private final int mKeyRepeatInterval;
83
84    // Miscellaneous constants
85    private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
86    private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1;
87
88    // XML attribute
89    private int mKeyLetterSize;
90    private int mKeyTextColor;
91    private int mKeyTextColorDisabled;
92    private Typeface mKeyLetterStyle = Typeface.DEFAULT;
93    private int mLabelTextSize;
94    private int mColorScheme = COLOR_SCHEME_WHITE;
95    private int mShadowColor;
96    private float mShadowRadius;
97    private Drawable mKeyBackground;
98    private float mBackgroundDimAmount;
99    private float mKeyHysteresisDistance;
100    private float mVerticalCorrection;
101    private int mPreviewOffset;
102    private int mPreviewHeight;
103    private int mPopupLayout;
104
105    // Main keyboard
106    private Keyboard mKeyboard;
107
108    // Key preview
109    private boolean mInForeground;
110    private TextView mPreviewText;
111    private int mPreviewTextSizeLarge;
112    private boolean mShowKeyPreview = true;
113    private int mKeyPreviewDisplayedY;
114    private final int mDelayBeforePreview;
115    private final int mDelayAfterPreview;
116    private ViewGroup mPreviewPlacer;
117    private final int[] mCoordinates = new int[2];
118
119    // Mini keyboard
120    private PopupWindow mPopupWindow;
121    private PopupPanel mPopupMiniKeyboardPanel;
122    private final WeakHashMap<Key, PopupPanel> mPopupPanelCache =
123            new WeakHashMap<Key, PopupPanel>();
124
125    /** Listener for {@link KeyboardActionListener}. */
126    private KeyboardActionListener mKeyboardActionListener;
127
128    private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
129
130    // TODO: Let the PointerTracker class manage this pointer queue
131    private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
132
133    private final boolean mHasDistinctMultitouch;
134    private int mOldPointerCount = 1;
135    private int mOldKeyIndex;
136
137    // Accessibility
138    private boolean mIsAccessibilityEnabled;
139
140    protected KeyDetector mKeyDetector = new KeyDetector();
141
142    // Swipe gesture detector
143    protected GestureDetector mGestureDetector;
144    private final SwipeTracker mSwipeTracker = new SwipeTracker();
145    private final int mSwipeThreshold;
146    private final boolean mDisambiguateSwipe;
147
148    // Drawing
149    /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
150    private boolean mDrawPending;
151    /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
152    private boolean mKeyboardChanged;
153    /** The dirty region in the keyboard bitmap */
154    private final Rect mDirtyRect = new Rect();
155    /** The key to invalidate. */
156    private Key mInvalidatedKey;
157    /** The dirty region for single key drawing */
158    private final Rect mInvalidatedKeyRect = new Rect();
159    /** The keyboard bitmap for faster updates */
160    private Bitmap mBuffer;
161    /** The canvas for the above mutable keyboard bitmap */
162    private Canvas mCanvas;
163    private final Paint mPaint;
164    private final Rect mPadding;
165    // This map caches key label text height in pixel as value and key label text size as map key.
166    private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
167    // Distance from horizontal center of the key, proportional to key label text height and width.
168    private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
169    private final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
170    private final String KEY_LABEL_REFERENCE_CHAR = "H";
171    private final int KEY_LABEL_OPTION_ALIGN_LEFT = 1;
172    private final int KEY_LABEL_OPTION_ALIGN_RIGHT = 2;
173    private final int KEY_LABEL_OPTION_ALIGN_BOTTOM = 8;
174    private final int KEY_LABEL_OPTION_FONT_NORMAL = 16;
175    private final int mKeyLabelHorizontalPadding;
176
177    private final UIHandler mHandler = new UIHandler();
178
179    class UIHandler extends Handler {
180        private static final int MSG_SHOW_KEY_PREVIEW = 1;
181        private static final int MSG_DISMISS_KEY_PREVIEW = 2;
182        private static final int MSG_REPEAT_KEY = 3;
183        private static final int MSG_LONGPRESS_KEY = 4;
184        private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
185        private static final int MSG_IGNORE_DOUBLE_TAP = 6;
186
187        private boolean mInKeyRepeat;
188
189        @Override
190        public void handleMessage(Message msg) {
191            final PointerTracker tracker = (PointerTracker) msg.obj;
192            switch (msg.what) {
193            case MSG_SHOW_KEY_PREVIEW:
194                showKey(msg.arg1, tracker);
195                break;
196            case MSG_DISMISS_KEY_PREVIEW:
197                mPreviewText.setVisibility(View.INVISIBLE);
198                break;
199            case MSG_REPEAT_KEY:
200                tracker.onRepeatKey(msg.arg1);
201                startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
202                break;
203            case MSG_LONGPRESS_KEY:
204                openMiniKeyboardIfRequired(msg.arg1, tracker);
205                break;
206            case MSG_LONGPRESS_SHIFT_KEY:
207                onLongPressShiftKey(tracker);
208                break;
209            }
210        }
211
212        public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
213            removeMessages(MSG_SHOW_KEY_PREVIEW);
214            if (mPreviewText.getVisibility() == VISIBLE || delay == 0) {
215                // Show right away, if it's already visible and finger is moving around
216                showKey(keyIndex, tracker);
217            } else {
218                sendMessageDelayed(
219                        obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
220            }
221        }
222
223        public void cancelShowKeyPreview(PointerTracker tracker) {
224            removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
225        }
226
227        public void cancelAllShowKeyPreviews() {
228            removeMessages(MSG_SHOW_KEY_PREVIEW);
229        }
230
231        public void dismissKeyPreview(long delay, PointerTracker tracker) {
232            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
233        }
234
235        public void cancelDismissKeyPreview(PointerTracker tracker) {
236            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
237        }
238
239        public void cancelAllDismissKeyPreviews() {
240            removeMessages(MSG_DISMISS_KEY_PREVIEW);
241        }
242
243        public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
244            mInKeyRepeat = true;
245            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
246        }
247
248        public void cancelKeyRepeatTimer() {
249            mInKeyRepeat = false;
250            removeMessages(MSG_REPEAT_KEY);
251        }
252
253        public boolean isInKeyRepeat() {
254            return mInKeyRepeat;
255        }
256
257        public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
258            cancelLongPressTimers();
259            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
260        }
261
262        public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
263            cancelLongPressTimers();
264            if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
265                sendMessageDelayed(
266                        obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
267            }
268        }
269
270        public void cancelLongPressTimers() {
271            removeMessages(MSG_LONGPRESS_KEY);
272            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
273        }
274
275        public void cancelKeyTimers() {
276            cancelKeyRepeatTimer();
277            cancelLongPressTimers();
278            removeMessages(MSG_IGNORE_DOUBLE_TAP);
279        }
280
281        public void startIgnoringDoubleTap() {
282            sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP),
283                    ViewConfiguration.getDoubleTapTimeout());
284        }
285
286        public boolean isIgnoringDoubleTap() {
287            return hasMessages(MSG_IGNORE_DOUBLE_TAP);
288        }
289
290        public void cancelAllMessages() {
291            cancelKeyTimers();
292            cancelAllShowKeyPreviews();
293            cancelAllDismissKeyPreviews();
294        }
295    }
296
297    public KeyboardView(Context context, AttributeSet attrs) {
298        this(context, attrs, R.attr.keyboardViewStyle);
299    }
300
301    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
302        super(context, attrs, defStyle);
303
304        final TypedArray a = context.obtainStyledAttributes(
305                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
306        int previewLayout = 0;
307        int keyTextSize = 0;
308
309        int n = a.getIndexCount();
310
311        for (int i = 0; i < n; i++) {
312            int attr = a.getIndex(i);
313
314            switch (attr) {
315            case R.styleable.KeyboardView_keyBackground:
316                mKeyBackground = a.getDrawable(attr);
317                break;
318            case R.styleable.KeyboardView_keyHysteresisDistance:
319                mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0);
320                break;
321            case R.styleable.KeyboardView_verticalCorrection:
322                mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
323                break;
324            case R.styleable.KeyboardView_keyPreviewLayout:
325                previewLayout = a.getResourceId(attr, 0);
326                break;
327            case R.styleable.KeyboardView_keyPreviewOffset:
328                mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
329                break;
330            case R.styleable.KeyboardView_keyPreviewHeight:
331                mPreviewHeight = a.getDimensionPixelSize(attr, 80);
332                break;
333            case R.styleable.KeyboardView_keyLetterSize:
334                mKeyLetterSize = a.getDimensionPixelSize(attr, 18);
335                break;
336            case R.styleable.KeyboardView_keyTextColor:
337                mKeyTextColor = a.getColor(attr, 0xFF000000);
338                break;
339            case R.styleable.KeyboardView_keyTextColorDisabled:
340                mKeyTextColorDisabled = a.getColor(attr, 0xFF000000);
341                break;
342            case R.styleable.KeyboardView_labelTextSize:
343                mLabelTextSize = a.getDimensionPixelSize(attr, 14);
344                break;
345            case R.styleable.KeyboardView_popupLayout:
346                mPopupLayout = a.getResourceId(attr, 0);
347                break;
348            case R.styleable.KeyboardView_shadowColor:
349                mShadowColor = a.getColor(attr, 0);
350                break;
351            case R.styleable.KeyboardView_shadowRadius:
352                mShadowRadius = a.getFloat(attr, 0f);
353                break;
354            // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
355            case R.styleable.KeyboardView_backgroundDimAmount:
356                mBackgroundDimAmount = a.getFloat(attr, 0.5f);
357                break;
358            case R.styleable.KeyboardView_keyLetterStyle:
359                mKeyLetterStyle = Typeface.defaultFromStyle(a.getInt(attr, Typeface.NORMAL));
360                break;
361            case R.styleable.KeyboardView_colorScheme:
362                mColorScheme = a.getInt(attr, COLOR_SCHEME_WHITE);
363                break;
364            }
365        }
366
367        final Resources res = getResources();
368
369        if (previewLayout != 0) {
370            mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null);
371            mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large);
372        } else {
373            mShowKeyPreview = false;
374        }
375        mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
376        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
377        mKeyLabelHorizontalPadding = (int)res.getDimension(
378                R.dimen.key_label_horizontal_alignment_padding);
379
380        mPaint = new Paint();
381        mPaint.setAntiAlias(true);
382        mPaint.setTextSize(keyTextSize);
383        mPaint.setTextAlign(Align.CENTER);
384        mPaint.setAlpha(255);
385
386        mPadding = new Rect(0, 0, 0, 0);
387        mKeyBackground.getPadding(mPadding);
388
389        mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
390        // TODO: Refer to frameworks/base/core/res/res/values/config.xml
391        mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
392
393        GestureDetector.SimpleOnGestureListener listener =
394                new GestureDetector.SimpleOnGestureListener() {
395            private boolean mProcessingShiftDoubleTapEvent = false;
396
397            @Override
398            public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
399                    float velocityY) {
400                final float absX = Math.abs(velocityX);
401                final float absY = Math.abs(velocityY);
402                float deltaY = me2.getY() - me1.getY();
403                int travelY = getHeight() / 2; // Half the keyboard height
404                mSwipeTracker.computeCurrentVelocity(1000);
405                final float endingVelocityY = mSwipeTracker.getYVelocity();
406                if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
407                    if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
408                        onSwipeDown();
409                        return true;
410                    }
411                }
412                return false;
413            }
414
415            @Override
416            public boolean onDoubleTap(MotionEvent firstDown) {
417                if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
418                        && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
419                    final int pointerIndex = firstDown.getActionIndex();
420                    final int id = firstDown.getPointerId(pointerIndex);
421                    final PointerTracker tracker = getPointerTracker(id);
422                    // If the first down event is on shift key.
423                    if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
424                        mProcessingShiftDoubleTapEvent = true;
425                        return true;
426                    }
427                }
428                mProcessingShiftDoubleTapEvent = false;
429                return false;
430            }
431
432            @Override
433            public boolean onDoubleTapEvent(MotionEvent secondTap) {
434                if (mProcessingShiftDoubleTapEvent
435                        && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
436                    final MotionEvent secondDown = secondTap;
437                    final int pointerIndex = secondDown.getActionIndex();
438                    final int id = secondDown.getPointerId(pointerIndex);
439                    final PointerTracker tracker = getPointerTracker(id);
440                    // If the second down event is also on shift key.
441                    if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
442                        // Detected a double tap on shift key. If we are in the ignoring double tap
443                        // mode, it means we have already turned off caps lock in
444                        // {@link KeyboardSwitcher#onReleaseShift} .
445                        final boolean ignoringDoubleTap = mHandler.isIgnoringDoubleTap();
446                        if (!ignoringDoubleTap)
447                            onDoubleTapShiftKey(tracker);
448                        return true;
449                    }
450                    // Otherwise these events should not be handled as double tap.
451                    mProcessingShiftDoubleTapEvent = false;
452                }
453                return mProcessingShiftDoubleTapEvent;
454            }
455        };
456
457        final boolean ignoreMultitouch = true;
458        mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
459        mGestureDetector.setIsLongpressEnabled(false);
460
461        mHasDistinctMultitouch = context.getPackageManager()
462                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
463        mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
464    }
465
466    public void startIgnoringDoubleTap() {
467        if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
468            mHandler.startIgnoringDoubleTap();
469    }
470
471    public void setOnKeyboardActionListener(KeyboardActionListener listener) {
472        mKeyboardActionListener = listener;
473        for (PointerTracker tracker : mPointerTrackers) {
474            tracker.setOnKeyboardActionListener(listener);
475        }
476    }
477
478    /**
479     * Returns the {@link KeyboardActionListener} object.
480     * @return the listener attached to this keyboard
481     */
482    protected KeyboardActionListener getOnKeyboardActionListener() {
483        return mKeyboardActionListener;
484    }
485
486    /**
487     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
488     * view will re-layout itself to accommodate the keyboard.
489     * @see Keyboard
490     * @see #getKeyboard()
491     * @param keyboard the keyboard to display in this view
492     */
493    public void setKeyboard(Keyboard keyboard) {
494        if (mKeyboard != null) {
495            dismissAllKeyPreviews();
496        }
497        // Remove any pending messages, except dismissing preview
498        mHandler.cancelKeyTimers();
499        mHandler.cancelAllShowKeyPreviews();
500        mKeyboard = keyboard;
501        LatinImeLogger.onSetKeyboard(keyboard);
502        mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
503                -getPaddingTop() + mVerticalCorrection);
504        for (PointerTracker tracker : mPointerTrackers) {
505            tracker.setKeyboard(keyboard, mKeyHysteresisDistance);
506        }
507        requestLayout();
508        mKeyboardChanged = true;
509        invalidateAllKeys();
510        mKeyDetector.setProximityThreshold(keyboard.getMostCommonKeyWidth());
511        mPopupPanelCache.clear();
512    }
513
514    /**
515     * Returns the current keyboard being displayed by this view.
516     * @return the currently attached keyboard
517     * @see #setKeyboard(Keyboard)
518     */
519    public Keyboard getKeyboard() {
520        return mKeyboard;
521    }
522
523    /**
524     * Returns whether the device has distinct multi-touch panel.
525     * @return true if the device has distinct multi-touch panel.
526     */
527    @Override
528    public boolean hasDistinctMultitouch() {
529        return mHasDistinctMultitouch;
530    }
531
532    /**
533     * Enables or disables accessibility.
534     * @param accessibilityEnabled whether or not to enable accessibility
535     */
536    public void setAccessibilityEnabled(boolean accessibilityEnabled) {
537        mIsAccessibilityEnabled = accessibilityEnabled;
538
539        // Propagate this change to all existing pointer trackers.
540        for (PointerTracker tracker : mPointerTrackers) {
541            tracker.setAccessibilityEnabled(accessibilityEnabled);
542        }
543    }
544
545    /**
546     * Returns whether the device has accessibility enabled.
547     * @return true if the device has accessibility enabled.
548     */
549    @Override
550    public boolean isAccessibilityEnabled() {
551        return mIsAccessibilityEnabled;
552    }
553
554    /**
555     * Enables or disables the key feedback preview. This is a preview that shows a magnified
556     * version of the depressed key. By default the preview is enabled.
557     * @param previewEnabled whether or not to enable the key feedback preview
558     * @see #isKeyPreviewEnabled()
559     */
560    public void setKeyPreviewEnabled(boolean previewEnabled) {
561        mShowKeyPreview = previewEnabled;
562    }
563
564    /**
565     * Returns the enabled state of the key feedback preview
566     * @return whether or not the key feedback preview is enabled
567     * @see #setKeyPreviewEnabled(boolean)
568     */
569    public boolean isKeyPreviewEnabled() {
570        return mShowKeyPreview;
571    }
572
573    public int getColorScheme() {
574        return mColorScheme;
575    }
576
577    /**
578     * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
579     * codes for adjacent keys.  When disabled, only the primary key code will be
580     * reported.
581     * @param enabled whether or not the proximity correction is enabled
582     */
583    public void setProximityCorrectionEnabled(boolean enabled) {
584        mKeyDetector.setProximityCorrectionEnabled(enabled);
585    }
586
587    /**
588     * Returns true if proximity correction is enabled.
589     */
590    public boolean isProximityCorrectionEnabled() {
591        return mKeyDetector.isProximityCorrectionEnabled();
592    }
593
594    protected CharSequence adjustCase(CharSequence label) {
595        if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
596                && Character.isLowerCase(label.charAt(0))) {
597            return label.toString().toUpperCase();
598        }
599        return label;
600    }
601
602    @Override
603    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
604        // Round up a little
605        if (mKeyboard == null) {
606            setMeasuredDimension(
607                    getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
608        } else {
609            int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
610            if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
611                width = MeasureSpec.getSize(widthMeasureSpec);
612            }
613            setMeasuredDimension(
614                    width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
615        }
616    }
617
618    @Override
619    public void onDraw(Canvas canvas) {
620        super.onDraw(canvas);
621        if (mDrawPending || mBuffer == null || mKeyboardChanged) {
622            onBufferDraw();
623        }
624        canvas.drawBitmap(mBuffer, 0, 0, null);
625    }
626
627    private void onBufferDraw() {
628        final int width = getWidth();
629        final int height = getHeight();
630        if (width == 0 || height == 0)
631            return;
632        if (mBuffer == null || mKeyboardChanged) {
633            mKeyboardChanged = false;
634            mDirtyRect.union(0, 0, width, height);
635        }
636        if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
637            mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
638            mCanvas = new Canvas(mBuffer);
639        }
640        final Canvas canvas = mCanvas;
641        canvas.clipRect(mDirtyRect, Op.REPLACE);
642        canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR);
643
644        if (mKeyboard == null) return;
645
646        if (mInvalidatedKey != null && mInvalidatedKeyRect.contains(mDirtyRect)) {
647            // Draw a single key.
648            onBufferDrawKey(canvas, mInvalidatedKey);
649        } else {
650            // Draw all keys.
651            for (final Key key : mKeyboard.getKeys()) {
652                onBufferDrawKey(canvas, key);
653            }
654        }
655
656        // TODO: Move this function to ProximityInfo for getting rid of
657        // public declarations for
658        // GRID_WIDTH and GRID_HEIGHT
659        if (DEBUG_KEYBOARD_GRID) {
660            Paint p = new Paint();
661            p.setStyle(Paint.Style.STROKE);
662            p.setStrokeWidth(1.0f);
663            p.setColor(0x800000c0);
664            int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1)
665                    / mKeyboard.GRID_WIDTH;
666            int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1)
667                    / mKeyboard.GRID_HEIGHT;
668            for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++)
669                canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p);
670            for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++)
671                canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p);
672        }
673
674        // Overlay a dark rectangle to dim the keyboard
675        if (mPopupMiniKeyboardPanel != null) {
676            mPaint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
677            canvas.drawRect(0, 0, width, height, mPaint);
678        }
679
680        mInvalidatedKey = null;
681        mDrawPending = false;
682        mDirtyRect.setEmpty();
683    }
684
685    private void onBufferDrawKey(final Canvas canvas, final Key key) {
686        final Paint paint = mPaint;
687        final Drawable keyBackground = mKeyBackground;
688        final Rect padding = mPadding;
689        final int kbdPaddingLeft = getPaddingLeft();
690        final int kbdPaddingTop = getPaddingTop();
691        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
692        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
693        final int rowHeight = padding.top + key.mHeight;
694        final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
695
696        canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop);
697
698        // Draw key background.
699        final int[] drawableState = key.getCurrentDrawableState();
700        keyBackground.setState(drawableState);
701        final Rect bounds = keyBackground.getBounds();
702        if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) {
703            keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight);
704        }
705        keyBackground.draw(canvas);
706
707        // Draw key label.
708        if (key.mLabel != null) {
709            // Switch the character to uppercase if shift is pressed
710            final String label = key.mLabel == null ? null : adjustCase(key.mLabel).toString();
711            // For characters, use large font. For labels like "Done", use small font.
712            final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint);
713            final int labelCharHeight = getLabelCharHeight(labelSize, paint);
714
715            // Vertical label text alignment.
716            final float baseline;
717            if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) {
718                baseline = key.mHeight - labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR;
719                if (DEBUG_SHOW_ALIGN)
720                    drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
721                            new Paint());
722            } else { // Align center
723                final float centerY = (key.mHeight + padding.top - padding.bottom) / 2;
724                baseline = centerY + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER;
725                if (DEBUG_SHOW_ALIGN)
726                    drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
727                            new Paint());
728            }
729            // Horizontal label text alignment
730            final int positionX;
731            if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) {
732                positionX = mKeyLabelHorizontalPadding + padding.left;
733                paint.setTextAlign(Align.LEFT);
734                if (DEBUG_SHOW_ALIGN)
735                    drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint());
736            } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) {
737                positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right;
738                paint.setTextAlign(Align.RIGHT);
739                if (DEBUG_SHOW_ALIGN)
740                    drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint());
741            } else {
742                positionX = (keyDrawWidth + padding.left - padding.right) / 2;
743                paint.setTextAlign(Align.CENTER);
744                if (DEBUG_SHOW_ALIGN) {
745                    if (label.length() > 1)
746                        drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint());
747                }
748            }
749            if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) {
750                paint.setColor(mKeyTextColorDisabled);
751            } else {
752                paint.setColor(mKeyTextColor);
753            }
754            if (key.mEnabled) {
755                // Set a drop shadow for the text
756                paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
757            } else {
758                // Make label invisible
759                paint.setColor(Color.TRANSPARENT);
760            }
761            canvas.drawText(label, positionX, baseline, paint);
762            // Turn off drop shadow
763            paint.setShadowLayer(0, 0, 0, 0);
764        }
765
766        // Draw key icon.
767        final Drawable icon = key.getIcon();
768        if (key.mLabel == null && icon != null) {
769            final int drawableWidth = icon.getIntrinsicWidth();
770            final int drawableHeight = icon.getIntrinsicHeight();
771            final int drawableX;
772            final int drawableY = (key.mHeight + padding.top - padding.bottom - drawableHeight) / 2;
773            if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) {
774                drawableX = padding.left + mKeyLabelHorizontalPadding;
775                if (DEBUG_SHOW_ALIGN)
776                    drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint());
777            } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) {
778                drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding
779                        - drawableWidth;
780                if (DEBUG_SHOW_ALIGN)
781                    drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight,
782                            0xc0808000, new Paint());
783            } else { // Align center
784                drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2;
785                if (DEBUG_SHOW_ALIGN)
786                    drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight,
787                            0xc0008080, new Paint());
788            }
789            drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight);
790            if (DEBUG_SHOW_ALIGN)
791                drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
792                        0x80c00000, new Paint());
793        }
794
795        // Draw hint icon.
796        if (key.mHintIcon != null) {
797            final int drawableWidth = keyDrawWidth;
798            final int drawableHeight = key.mHeight;
799            final int drawableX = 0;
800            final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
801            Drawable hintIcon = (isManualTemporaryUpperCase
802                    && key.mManualTemporaryUpperCaseHintIcon != null)
803                    ? key.mManualTemporaryUpperCaseHintIcon : key.mHintIcon;
804            drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight);
805            if (DEBUG_SHOW_ALIGN)
806                drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
807                        0x80c0c000, new Paint());
808        }
809
810        canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop);
811    }
812
813    public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) {
814        // For characters, use large font. For labels like "Done", use small font.
815        final int labelSize;
816        final Typeface labelStyle;
817        if (label.length() > 1) {
818            labelSize = mLabelTextSize;
819            if ((keyLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) {
820                labelStyle = Typeface.DEFAULT;
821            } else {
822                labelStyle = Typeface.DEFAULT_BOLD;
823            }
824        } else {
825            labelSize = mKeyLetterSize;
826            labelStyle = mKeyLetterStyle;
827        }
828        paint.setTextSize(labelSize);
829        paint.setTypeface(labelStyle);
830        return labelSize;
831    }
832
833    private int getLabelCharHeight(int labelSize, Paint paint) {
834        Integer labelHeightValue = mTextHeightCache.get(labelSize);
835        final int labelCharHeight;
836        if (labelHeightValue != null) {
837            labelCharHeight = labelHeightValue;
838        } else {
839            Rect textBounds = new Rect();
840            paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds);
841            labelCharHeight = textBounds.height();
842            mTextHeightCache.put(labelSize, labelCharHeight);
843        }
844        return labelCharHeight;
845    }
846
847    private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
848            int height) {
849        canvas.translate(x, y);
850        icon.setBounds(0, 0, width, height);
851        icon.draw(canvas);
852        canvas.translate(-x, -y);
853    }
854
855    private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) {
856        paint.setStyle(Paint.Style.STROKE);
857        paint.setStrokeWidth(1.0f);
858        paint.setColor(color);
859        canvas.drawLine(0, y, w, y, paint);
860    }
861
862    private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) {
863        paint.setStyle(Paint.Style.STROKE);
864        paint.setStrokeWidth(1.0f);
865        paint.setColor(color);
866        canvas.drawLine(x, 0, x, h, paint);
867    }
868
869    private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color,
870            Paint paint) {
871        paint.setStyle(Paint.Style.STROKE);
872        paint.setStrokeWidth(1.0f);
873        paint.setColor(color);
874        canvas.translate(x, y);
875        canvas.drawRect(0, 0, w, h, paint);
876        canvas.translate(-x, -y);
877    }
878
879    public void setForeground(boolean foreground) {
880        mInForeground = foreground;
881    }
882
883    // TODO: clean up this method.
884    private void dismissAllKeyPreviews() {
885        for (PointerTracker tracker : mPointerTrackers) {
886            tracker.setReleasedKeyGraphics();
887            dismissKeyPreview(tracker);
888        }
889    }
890
891    @Override
892    public void showKeyPreview(int keyIndex, PointerTracker tracker) {
893        if (mShowKeyPreview) {
894            mHandler.showKeyPreview(mDelayBeforePreview, keyIndex, tracker);
895        } else if (mKeyboard.needSpacebarPreview(keyIndex)) {
896            // Show key preview (in this case, slide language switcher) without any delay.
897            showKey(keyIndex, tracker);
898        }
899    }
900
901    @Override
902    public void dismissKeyPreview(PointerTracker tracker) {
903        if (mShowKeyPreview) {
904            mHandler.cancelShowKeyPreview(tracker);
905            mHandler.dismissKeyPreview(mDelayAfterPreview, tracker);
906        } else if (mKeyboard.needSpacebarPreview(KeyDetector.NOT_A_KEY)) {
907            // Dismiss key preview (in this case, slide language switcher) without any delay.
908            mPreviewText.setVisibility(View.INVISIBLE);
909        }
910    }
911
912    private void addKeyPreview(TextView keyPreview) {
913        ViewGroup placer = mPreviewPlacer;
914        if (placer == null) {
915            final FrameLayout screenContent = (FrameLayout) getRootView().findViewById(
916                    android.R.id.content);
917            if (android.os.Build.VERSION.SDK_INT >= /* HONEYCOMB */11) {
918                placer = screenContent;
919            } else {
920                // Insert LinearLayout to be able to setMargin because pre-Honeycomb FrameLayout
921                // could not handle setMargin properly.
922                placer = new LinearLayout(getContext());
923                screenContent.addView(placer);
924            }
925            mPreviewPlacer = placer;
926        }
927        if (placer instanceof FrameLayout) {
928            // Honeycomb or later.
929            placer.addView(keyPreview, new FrameLayout.LayoutParams(0, 0));
930        } else {
931            // Gingerbread or ealier.
932            placer.addView(keyPreview, new LinearLayout.LayoutParams(0, 0));
933        }
934    }
935
936    // TODO: Introduce minimum duration for displaying key previews
937    // TODO: Display up to two key previews when the user presses two keys at the same time
938    private void showKey(final int keyIndex, PointerTracker tracker) {
939        final TextView previewText = mPreviewText;
940        // If the key preview has no parent view yet, add it to the ViewGroup which can place
941        // key preview absolutely in SoftInputWindow.
942        if (previewText.getParent() == null) {
943            addKeyPreview(previewText);
944        }
945
946        final Key key = tracker.getKey(keyIndex);
947        // If keyIndex is invalid or IME is already closed, we must not show key preview.
948        // Trying to show key preview while root window is closed causes
949        // WindowManager.BadTokenException.
950        if (key == null || !mInForeground)
951            return;
952
953        mHandler.cancelAllDismissKeyPreviews();
954
955        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
956        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
957        // What we show as preview should match what we show on key top in onBufferDraw().
958        if (key.mLabel != null) {
959            // TODO Should take care of temporaryShiftLabel here.
960            previewText.setCompoundDrawables(null, null, null, null);
961            previewText.setText(adjustCase(tracker.getPreviewText(key)));
962            if (key.mLabel.length() > 1) {
963                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
964                previewText.setTypeface(Typeface.DEFAULT_BOLD);
965            } else {
966                previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
967                previewText.setTypeface(mKeyLetterStyle);
968            }
969        } else {
970            final Drawable previewIcon = key.getPreviewIcon();
971            previewText.setCompoundDrawables(null, null, null,
972                   previewIcon != null ? previewIcon : key.getIcon());
973            previewText.setText(null);
974        }
975        // Set the preview background state
976        previewText.getBackground().setState(
977                key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
978
979        previewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
980                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
981        final int previewWidth = Math.max(previewText.getMeasuredWidth(), keyDrawWidth
982                + previewText.getPaddingLeft() + previewText.getPaddingRight());
983        final int previewHeight = mPreviewHeight;
984        getLocationInWindow(mCoordinates);
985        final int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2 + mCoordinates[0];
986        final int previewY = key.mY - previewHeight + mCoordinates[1] + mPreviewOffset;
987        // Record key preview position to display mini-keyboard later at the same position
988        mKeyPreviewDisplayedY = previewY;
989
990        // Place the key preview.
991        // TODO: Adjust position of key previews which touch screen edges
992        final MarginLayoutParams lp = (MarginLayoutParams)previewText.getLayoutParams();
993        lp.width = previewWidth;
994        lp.height = previewHeight;
995        lp.setMargins(previewX, previewY, 0, 0);
996        previewText.setVisibility(VISIBLE);
997    }
998
999    /**
1000     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1001     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1002     * draws the cached buffer.
1003     * @see #invalidateKey(Key)
1004     */
1005    public void invalidateAllKeys() {
1006        mDirtyRect.union(0, 0, getWidth(), getHeight());
1007        mDrawPending = true;
1008        invalidate();
1009    }
1010
1011    /**
1012     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1013     * one key is changing it's content. Any changes that affect the position or size of the key
1014     * may not be honored.
1015     * @param key key in the attached {@link Keyboard}.
1016     * @see #invalidateAllKeys
1017     */
1018    @Override
1019    public void invalidateKey(Key key) {
1020        if (key == null)
1021            return;
1022        mInvalidatedKey = key;
1023        final int x = key.mX + getPaddingLeft();
1024        final int y = key.mY + getPaddingTop();
1025        mInvalidatedKeyRect.set(x, y, x + key.mWidth, y + key.mHeight);
1026        mDirtyRect.union(mInvalidatedKeyRect);
1027        onBufferDraw();
1028        invalidate(mInvalidatedKeyRect);
1029    }
1030
1031    private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
1032        // Check if we have a popup layout specified first.
1033        if (mPopupLayout == 0) {
1034            return false;
1035        }
1036
1037        final Key parentKey = tracker.getKey(keyIndex);
1038        if (parentKey == null)
1039            return false;
1040        boolean result = onLongPress(parentKey, tracker);
1041        if (result) {
1042            dismissAllKeyPreviews();
1043            tracker.onLongPressed(mPointerQueue);
1044        }
1045        return result;
1046    }
1047
1048    private void onLongPressShiftKey(PointerTracker tracker) {
1049        tracker.onLongPressed(mPointerQueue);
1050        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1051    }
1052
1053    private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
1054        // When shift key is double tapped, the first tap is correctly processed as usual tap. And
1055        // the second tap is treated as this double tap event, so that we need not mark tracker
1056        // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
1057        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1058    }
1059
1060    // This default implementation returns a popup mini keyboard panel.
1061    // A derived class may return a language switcher popup panel, for instance.
1062    protected PopupPanel onCreatePopupPanel(Key parentKey) {
1063        if (parentKey.mPopupCharacters == null)
1064            return null;
1065
1066        final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
1067        if (container == null)
1068            throw new NullPointerException();
1069
1070        final PopupMiniKeyboardView miniKeyboardView =
1071                (PopupMiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
1072        miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
1073            @Override
1074            public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1075                mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
1076                dismissMiniKeyboard();
1077            }
1078
1079            @Override
1080            public void onTextInput(CharSequence text) {
1081                mKeyboardActionListener.onTextInput(text);
1082                dismissMiniKeyboard();
1083            }
1084
1085            @Override
1086            public void onCancelInput() {
1087                mKeyboardActionListener.onCancelInput();
1088                dismissMiniKeyboard();
1089            }
1090
1091            @Override
1092            public void onSwipeDown() {
1093                // Nothing to do.
1094            }
1095            @Override
1096            public void onPress(int primaryCode, boolean withSliding) {
1097                mKeyboardActionListener.onPress(primaryCode, withSliding);
1098            }
1099            @Override
1100            public void onRelease(int primaryCode, boolean withSliding) {
1101                mKeyboardActionListener.onRelease(primaryCode, withSliding);
1102            }
1103        });
1104
1105        final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
1106                parentKey).build();
1107        miniKeyboardView.setKeyboard(keyboard);
1108
1109        container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1110                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1111
1112        return miniKeyboardView;
1113    }
1114
1115    /**
1116     * Called when a key is long pressed. By default this will open mini keyboard associated
1117     * with this key.
1118     * @param parentKey the key that was long pressed
1119     * @param tracker the pointer tracker which pressed the parent key
1120     * @return true if the long press is handled, false otherwise. Subclasses should call the
1121     * method on the base class if the subclass doesn't wish to handle the call.
1122     */
1123    protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
1124        PopupPanel popupPanel = mPopupPanelCache.get(parentKey);
1125        if (popupPanel == null) {
1126            popupPanel = onCreatePopupPanel(parentKey);
1127            if (popupPanel == null)
1128                return false;
1129            mPopupPanelCache.put(parentKey, popupPanel);
1130        }
1131        if (mPopupWindow == null) {
1132            mPopupWindow = new PopupWindow(getContext());
1133            mPopupWindow.setBackgroundDrawable(null);
1134            mPopupWindow.setAnimationStyle(R.style.PopupMiniKeyboardAnimation);
1135            // Allow popup window to be drawn off the screen.
1136            mPopupWindow.setClippingEnabled(false);
1137        }
1138        mPopupMiniKeyboardPanel = popupPanel;
1139        popupPanel.showPanel(this, parentKey, tracker, mKeyPreviewDisplayedY, mPopupWindow);
1140
1141        invalidateAllKeys();
1142        return true;
1143    }
1144
1145    private PointerTracker getPointerTracker(final int id) {
1146        final ArrayList<PointerTracker> pointers = mPointerTrackers;
1147        final KeyboardActionListener listener = mKeyboardActionListener;
1148
1149        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
1150        for (int i = pointers.size(); i <= id; i++) {
1151            final PointerTracker tracker =
1152                new PointerTracker(i, this, mHandler, mKeyDetector, this);
1153            if (mKeyboard != null)
1154                tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance);
1155            if (listener != null)
1156                tracker.setOnKeyboardActionListener(listener);
1157            pointers.add(tracker);
1158        }
1159
1160        return pointers.get(id);
1161    }
1162
1163    public boolean isInSlidingKeyInput() {
1164        if (mPopupMiniKeyboardPanel != null) {
1165            return mPopupMiniKeyboardPanel.isInSlidingKeyInput();
1166        } else {
1167            return mPointerQueue.isInSlidingKeyInput();
1168        }
1169    }
1170
1171    public int getPointerCount() {
1172        return mOldPointerCount;
1173    }
1174
1175    @Override
1176    public boolean onTouchEvent(MotionEvent me) {
1177        final int action = me.getActionMasked();
1178        final int pointerCount = me.getPointerCount();
1179        final int oldPointerCount = mOldPointerCount;
1180        mOldPointerCount = pointerCount;
1181
1182        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1183        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1184        // events except a transition from/to single-touch.
1185        if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled)
1186                && pointerCount > 1 && oldPointerCount > 1) {
1187            return true;
1188        }
1189
1190        // Track the last few movements to look for spurious swipes.
1191        mSwipeTracker.addMovement(me);
1192
1193        // Gesture detector must be enabled only when mini-keyboard is not on the screen and
1194        // accessibility is not enabled.
1195        // TODO: Reconcile gesture detection and accessibility features.
1196        if (mPopupMiniKeyboardPanel == null && !mIsAccessibilityEnabled
1197                && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
1198            dismissAllKeyPreviews();
1199            mHandler.cancelKeyTimers();
1200            return true;
1201        }
1202
1203        final long eventTime = me.getEventTime();
1204        final int index = me.getActionIndex();
1205        final int id = me.getPointerId(index);
1206        final int x = (int)me.getX(index);
1207        final int y = (int)me.getY(index);
1208
1209        // Needs to be called after the gesture detector gets a turn, as it may have displayed the
1210        // mini keyboard
1211        if (mPopupMiniKeyboardPanel != null) {
1212            return mPopupMiniKeyboardPanel.onTouchEvent(me);
1213        }
1214
1215        if (mHandler.isInKeyRepeat()) {
1216            final PointerTracker tracker = getPointerTracker(id);
1217            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1218            // event (UP or DOWN) is non-modifier key.
1219            if (pointerCount > 1 && !tracker.isModifier()) {
1220                mHandler.cancelKeyRepeatTimer();
1221            }
1222            // Up event will pass through.
1223        }
1224
1225        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1226        // Translate mutli-touch event to single-touch events on the device that has no distinct
1227        // multi-touch panel.
1228        if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) {
1229            // Use only main (id=0) pointer tracker.
1230            PointerTracker tracker = getPointerTracker(0);
1231            if (pointerCount == 1 && oldPointerCount == 2) {
1232                // Multi-touch to single touch transition.
1233                // Send a down event for the latest pointer if the key is different from the
1234                // previous key.
1235                final int newKeyIndex = tracker.getKeyIndexOn(x, y);
1236                if (mOldKeyIndex != newKeyIndex) {
1237                    tracker.onDownEvent(x, y, eventTime, null);
1238                    if (action == MotionEvent.ACTION_UP)
1239                        tracker.onUpEvent(x, y, eventTime, null);
1240                }
1241            } else if (pointerCount == 2 && oldPointerCount == 1) {
1242                // Single-touch to multi-touch transition.
1243                // Send an up event for the last pointer.
1244                final int lastX = tracker.getLastX();
1245                final int lastY = tracker.getLastY();
1246                mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
1247                tracker.onUpEvent(lastX, lastY, eventTime, null);
1248            } else if (pointerCount == 1 && oldPointerCount == 1) {
1249                tracker.onTouchEvent(action, x, y, eventTime, null);
1250            } else {
1251                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1252                        + " (old " + oldPointerCount + ")");
1253            }
1254            return true;
1255        }
1256
1257        final PointerTrackerQueue queue = mPointerQueue;
1258        if (action == MotionEvent.ACTION_MOVE) {
1259            for (int i = 0; i < pointerCount; i++) {
1260                final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
1261                tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
1262            }
1263        } else {
1264            final PointerTracker tracker = getPointerTracker(id);
1265            switch (action) {
1266            case MotionEvent.ACTION_DOWN:
1267            case MotionEvent.ACTION_POINTER_DOWN:
1268                tracker.onDownEvent(x, y, eventTime, queue);
1269                break;
1270            case MotionEvent.ACTION_UP:
1271            case MotionEvent.ACTION_POINTER_UP:
1272                tracker.onUpEvent(x, y, eventTime, queue);
1273                break;
1274            case MotionEvent.ACTION_CANCEL:
1275                tracker.onCancelEvent(x, y, eventTime, queue);
1276                break;
1277            }
1278        }
1279
1280        return true;
1281    }
1282
1283    protected void onSwipeDown() {
1284        mKeyboardActionListener.onSwipeDown();
1285    }
1286
1287    public void closing() {
1288        mPreviewText.setVisibility(View.GONE);
1289        mHandler.cancelAllMessages();
1290
1291        dismissMiniKeyboard();
1292        mDirtyRect.union(0, 0, getWidth(), getHeight());
1293        mPopupPanelCache.clear();
1294        requestLayout();
1295    }
1296
1297    public void purgeKeyboardAndClosing() {
1298        mKeyboard = null;
1299        closing();
1300    }
1301
1302    @Override
1303    public void onDetachedFromWindow() {
1304        super.onDetachedFromWindow();
1305        closing();
1306    }
1307
1308    private boolean dismissMiniKeyboard() {
1309        if (mPopupWindow != null && mPopupWindow.isShowing()) {
1310            mPopupWindow.dismiss();
1311            mPopupMiniKeyboardPanel = null;
1312            invalidateAllKeys();
1313            return true;
1314        }
1315        return false;
1316    }
1317
1318    public boolean handleBack() {
1319        return dismissMiniKeyboard();
1320    }
1321}
1322