KeyboardView.java revision a0537fb4c73dff8beecc328720830af9719d0277
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.os.SystemClock;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.util.TypedValue;
42import android.view.GestureDetector;
43import android.view.Gravity;
44import android.view.LayoutInflater;
45import android.view.MotionEvent;
46import android.view.View;
47import android.view.ViewGroup;
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.List;
56import java.util.WeakHashMap;
57
58/**
59 * A view that renders a virtual {@link Keyboard}. It handles rendering of keys and detecting key
60 * presses and touch movements.
61 *
62 * @attr ref R.styleable#KeyboardView_keyBackground
63 * @attr ref R.styleable#KeyboardView_keyPreviewLayout
64 * @attr ref R.styleable#KeyboardView_keyPreviewOffset
65 * @attr ref R.styleable#KeyboardView_labelTextSize
66 * @attr ref R.styleable#KeyboardView_keyTextSize
67 * @attr ref R.styleable#KeyboardView_keyTextColor
68 * @attr ref R.styleable#KeyboardView_verticalCorrection
69 * @attr ref R.styleable#KeyboardView_popupLayout
70 */
71public class KeyboardView extends View implements PointerTracker.UIProxy {
72    private static final String TAG = KeyboardView.class.getSimpleName();
73    private static final boolean DEBUG_SHOW_ALIGN = false;
74    private static final boolean DEBUG_KEYBOARD_GRID = false;
75
76    private static final boolean ENABLE_CAPSLOCK_BY_LONGPRESS = false;
77    private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
78
79    public static final int COLOR_SCHEME_WHITE = 0;
80    public static final int COLOR_SCHEME_BLACK = 1;
81
82    // Timing constants
83    private final int mKeyRepeatInterval;
84
85    // Miscellaneous constants
86    private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
87    private static final int HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL = -1;
88
89    // XML attribute
90    private int mKeyLetterSize;
91    private int mKeyTextColor;
92    private int mKeyTextColorDisabled;
93    private Typeface mKeyLetterStyle = Typeface.DEFAULT;
94    private int mLabelTextSize;
95    private int mColorScheme = COLOR_SCHEME_WHITE;
96    private int mShadowColor;
97    private float mShadowRadius;
98    private Drawable mKeyBackground;
99    private float mBackgroundDimAmount;
100    private float mKeyHysteresisDistance;
101    private float mVerticalCorrection;
102    private int mPreviewOffset;
103    private int mPreviewHeight;
104    private int mPopupLayout;
105
106    // Main keyboard
107    private Keyboard mKeyboard;
108
109    // Key preview
110    private boolean mInForeground;
111    private TextView mPreviewText;
112    private int mPreviewTextSizeLarge;
113    private final int[] mOffsetInWindow = new int[2];
114    private boolean mShowKeyPreview = true;
115    private int mKeyPreviewDisplayedY;
116    private final int mDelayBeforePreview;
117    private final int mDelayAfterPreview;
118    private ViewGroup mPreviewPlacer;
119
120    // Mini keyboard
121    private PopupWindow mMiniKeyboardWindow;
122    private KeyboardView mMiniKeyboardView;
123    private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>();
124    private int mMiniKeyboardOriginX;
125    private int mMiniKeyboardOriginY;
126    private long mMiniKeyboardDisplayedTime;
127    private int[] mWindowOffset;
128    private final float mMiniKeyboardSlideAllowance;
129    private int mMiniKeyboardTrackerId;
130    private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
131
132    /** Listener for {@link KeyboardActionListener}. */
133    private KeyboardActionListener mKeyboardActionListener;
134
135    private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
136
137    // TODO: Let the PointerTracker class manage this pointer queue
138    private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
139
140    private final boolean mHasDistinctMultitouch;
141    private int mOldPointerCount = 1;
142    private int mOldKeyIndex;
143
144    // Accessibility
145    private boolean mIsAccessibilityEnabled;
146
147    protected KeyDetector mKeyDetector = new KeyDetector();
148
149    // Swipe gesture detector
150    private GestureDetector mGestureDetector;
151    private final SwipeTracker mSwipeTracker = new SwipeTracker();
152    private final int mSwipeThreshold;
153    private final boolean mDisambiguateSwipe;
154
155    // Drawing
156    /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
157    private boolean mDrawPending;
158    /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
159    private boolean mKeyboardChanged;
160    /** The dirty region in the keyboard bitmap */
161    private final Rect mDirtyRect = new Rect();
162    /** The key to invalidate. */
163    private Key mInvalidatedKey;
164    /** The dirty region for single key drawing */
165    private final Rect mInvalidatedKeyRect = new Rect();
166    /** The keyboard bitmap for faster updates */
167    private Bitmap mBuffer;
168    /** The canvas for the above mutable keyboard bitmap */
169    private Canvas mCanvas;
170    private final Paint mPaint;
171    private final Rect mPadding;
172    // This map caches key label text height in pixel as value and key label text size as map key.
173    private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
174    // Distance from horizontal center of the key, proportional to key label text height and width.
175    private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
176    private final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
177    private final String KEY_LABEL_REFERENCE_CHAR = "H";
178    private final int KEY_LABEL_OPTION_ALIGN_LEFT = 1;
179    private final int KEY_LABEL_OPTION_ALIGN_RIGHT = 2;
180    private final int KEY_LABEL_OPTION_ALIGN_BOTTOM = 8;
181    private final int KEY_LABEL_OPTION_FONT_NORMAL = 16;
182    private final int mKeyLabelHorizontalPadding;
183
184    private final UIHandler mHandler = new UIHandler();
185
186    class UIHandler extends Handler {
187        private static final int MSG_SHOW_KEY_PREVIEW = 1;
188        private static final int MSG_DISMISS_KEY_PREVIEW = 2;
189        private static final int MSG_REPEAT_KEY = 3;
190        private static final int MSG_LONGPRESS_KEY = 4;
191        private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
192
193        private boolean mInKeyRepeat;
194
195        @Override
196        public void handleMessage(Message msg) {
197            switch (msg.what) {
198                case MSG_SHOW_KEY_PREVIEW:
199                    showKey(msg.arg1, (PointerTracker)msg.obj);
200                    break;
201                case MSG_DISMISS_KEY_PREVIEW:
202                    mPreviewText.setVisibility(View.INVISIBLE);
203                    break;
204                case MSG_REPEAT_KEY: {
205                    final PointerTracker tracker = (PointerTracker)msg.obj;
206                    tracker.onRepeatKey(msg.arg1);
207                    startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
208                    break;
209                }
210                case MSG_LONGPRESS_KEY: {
211                    final PointerTracker tracker = (PointerTracker)msg.obj;
212                    openMiniKeyboardIfRequired(msg.arg1, tracker);
213                    break;
214                }
215                case MSG_LONGPRESS_SHIFT_KEY: {
216                    final PointerTracker tracker = (PointerTracker)msg.obj;
217                    onLongPressShiftKey(tracker);
218                    break;
219                }
220            }
221        }
222
223        public void showKeyPreview(long delay, int keyIndex, PointerTracker tracker) {
224            removeMessages(MSG_SHOW_KEY_PREVIEW);
225            if (mPreviewText.getVisibility() == VISIBLE || delay == 0) {
226                // Show right away, if it's already visible and finger is moving around
227                showKey(keyIndex, tracker);
228            } else {
229                sendMessageDelayed(
230                        obtainMessage(MSG_SHOW_KEY_PREVIEW, keyIndex, 0, tracker), delay);
231            }
232        }
233
234        public void cancelShowKeyPreview(PointerTracker tracker) {
235            removeMessages(MSG_SHOW_KEY_PREVIEW, tracker);
236        }
237
238        public void cancelAllShowKeyPreviews() {
239            removeMessages(MSG_SHOW_KEY_PREVIEW);
240        }
241
242        public void dismissKeyPreview(long delay, PointerTracker tracker) {
243            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
244        }
245
246        public void cancelDismissKeyPreview(PointerTracker tracker) {
247            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
248        }
249
250        public void cancelAllDismissKeyPreviews() {
251            removeMessages(MSG_DISMISS_KEY_PREVIEW);
252        }
253
254        public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
255            mInKeyRepeat = true;
256            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
257        }
258
259        public void cancelKeyRepeatTimer() {
260            mInKeyRepeat = false;
261            removeMessages(MSG_REPEAT_KEY);
262        }
263
264        public boolean isInKeyRepeat() {
265            return mInKeyRepeat;
266        }
267
268        public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
269            cancelLongPressTimers();
270            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
271        }
272
273        public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
274            cancelLongPressTimers();
275            if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
276                sendMessageDelayed(
277                        obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
278            }
279        }
280
281        public void cancelLongPressTimers() {
282            removeMessages(MSG_LONGPRESS_KEY);
283            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
284        }
285
286        public void cancelKeyTimers() {
287            cancelKeyRepeatTimer();
288            cancelLongPressTimers();
289        }
290
291        public void cancelAllMessages() {
292            cancelKeyTimers();
293            cancelAllShowKeyPreviews();
294            cancelAllDismissKeyPreviews();
295        }
296    }
297
298    public KeyboardView(Context context, AttributeSet attrs) {
299        this(context, attrs, R.attr.keyboardViewStyle);
300    }
301
302    public KeyboardView(Context context, AttributeSet attrs, int defStyle) {
303        super(context, attrs, defStyle);
304
305        final TypedArray a = context.obtainStyledAttributes(
306                attrs, R.styleable.KeyboardView, defStyle, R.style.KeyboardView);
307        int previewLayout = 0;
308        int keyTextSize = 0;
309
310        int n = a.getIndexCount();
311
312        for (int i = 0; i < n; i++) {
313            int attr = a.getIndex(i);
314
315            switch (attr) {
316            case R.styleable.KeyboardView_keyBackground:
317                mKeyBackground = a.getDrawable(attr);
318                break;
319            case R.styleable.KeyboardView_keyHysteresisDistance:
320                mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0);
321                break;
322            case R.styleable.KeyboardView_verticalCorrection:
323                mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
324                break;
325            case R.styleable.KeyboardView_keyPreviewLayout:
326                previewLayout = a.getResourceId(attr, 0);
327                break;
328            case R.styleable.KeyboardView_keyPreviewOffset:
329                mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
330                break;
331            case R.styleable.KeyboardView_keyPreviewHeight:
332                mPreviewHeight = a.getDimensionPixelSize(attr, 80);
333                break;
334            case R.styleable.KeyboardView_keyLetterSize:
335                mKeyLetterSize = a.getDimensionPixelSize(attr, 18);
336                break;
337            case R.styleable.KeyboardView_keyTextColor:
338                mKeyTextColor = a.getColor(attr, 0xFF000000);
339                break;
340            case R.styleable.KeyboardView_keyTextColorDisabled:
341                mKeyTextColorDisabled = a.getColor(attr, 0xFF000000);
342                break;
343            case R.styleable.KeyboardView_labelTextSize:
344                mLabelTextSize = a.getDimensionPixelSize(attr, 14);
345                break;
346            case R.styleable.KeyboardView_popupLayout:
347                mPopupLayout = a.getResourceId(attr, 0);
348                break;
349            case R.styleable.KeyboardView_shadowColor:
350                mShadowColor = a.getColor(attr, 0);
351                break;
352            case R.styleable.KeyboardView_shadowRadius:
353                mShadowRadius = a.getFloat(attr, 0f);
354                break;
355            // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
356            case R.styleable.KeyboardView_backgroundDimAmount:
357                mBackgroundDimAmount = a.getFloat(attr, 0.5f);
358                break;
359            case R.styleable.KeyboardView_keyLetterStyle:
360                mKeyLetterStyle = Typeface.defaultFromStyle(a.getInt(attr, Typeface.NORMAL));
361                break;
362            case R.styleable.KeyboardView_colorScheme:
363                mColorScheme = a.getInt(attr, COLOR_SCHEME_WHITE);
364                break;
365            }
366        }
367
368        final Resources res = getResources();
369
370        if (previewLayout != 0) {
371            mPreviewText = (TextView) LayoutInflater.from(context).inflate(previewLayout, null);
372            mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large);
373        } else {
374            mShowKeyPreview = false;
375        }
376        mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
377        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
378        mKeyLabelHorizontalPadding = (int)res.getDimension(
379                R.dimen.key_label_horizontal_alignment_padding);
380
381        mMiniKeyboardWindow = new PopupWindow(context);
382        mMiniKeyboardWindow.setBackgroundDrawable(null);
383        mMiniKeyboardWindow.setAnimationStyle(R.style.MiniKeyboardAnimation);
384        // Allow popup window to be drawn off the screen.
385        mMiniKeyboardWindow.setClippingEnabled(false);
386
387        mPaint = new Paint();
388        mPaint.setAntiAlias(true);
389        mPaint.setTextSize(keyTextSize);
390        mPaint.setTextAlign(Align.CENTER);
391        mPaint.setAlpha(255);
392
393        mPadding = new Rect(0, 0, 0, 0);
394        mKeyBackground.getPadding(mPadding);
395
396        mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
397        // TODO: Refer frameworks/base/core/res/res/values/config.xml
398        mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
399        mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance);
400        mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
401                R.bool.config_show_mini_keyboard_at_touched_point);
402
403        GestureDetector.SimpleOnGestureListener listener =
404                new GestureDetector.SimpleOnGestureListener() {
405            private boolean mProcessingShiftDoubleTapEvent = false;
406
407            @Override
408            public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
409                    float velocityY) {
410                final float absX = Math.abs(velocityX);
411                final float absY = Math.abs(velocityY);
412                float deltaY = me2.getY() - me1.getY();
413                int travelY = getHeight() / 2; // Half the keyboard height
414                mSwipeTracker.computeCurrentVelocity(1000);
415                final float endingVelocityY = mSwipeTracker.getYVelocity();
416                if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
417                    if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
418                        onSwipeDown();
419                        return true;
420                    }
421                }
422                return false;
423            }
424
425            @Override
426            public boolean onDoubleTap(MotionEvent firstDown) {
427                if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
428                        && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
429                    final int pointerIndex = firstDown.getActionIndex();
430                    final int id = firstDown.getPointerId(pointerIndex);
431                    final PointerTracker tracker = getPointerTracker(id);
432                    // If the first down event is on shift key.
433                    if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
434                        mProcessingShiftDoubleTapEvent = true;
435                        return true;
436                    }
437                }
438                mProcessingShiftDoubleTapEvent = false;
439                return false;
440            }
441
442            @Override
443            public boolean onDoubleTapEvent(MotionEvent secondTap) {
444                if (mProcessingShiftDoubleTapEvent
445                        && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
446                    final MotionEvent secondDown = secondTap;
447                    final int pointerIndex = secondDown.getActionIndex();
448                    final int id = secondDown.getPointerId(pointerIndex);
449                    final PointerTracker tracker = getPointerTracker(id);
450                    // If the second down event is also on shift key.
451                    if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
452                        onDoubleTapShiftKey(tracker);
453                        return true;
454                    }
455                    // Otherwise these events should not be handled as double tap.
456                    mProcessingShiftDoubleTapEvent = false;
457                }
458                return mProcessingShiftDoubleTapEvent;
459            }
460        };
461
462        final boolean ignoreMultitouch = true;
463        mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
464        mGestureDetector.setIsLongpressEnabled(false);
465
466        mHasDistinctMultitouch = context.getPackageManager()
467                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
468        mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
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        mMiniKeyboardCache.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 (mMiniKeyboardView != 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            placer.addView(keyPreview, new FrameLayout.LayoutParams(0, 0));
929        } else {
930            placer.addView(keyPreview, new LinearLayout.LayoutParams(0, 0));
931        }
932    }
933
934    // TODO: Introduce minimum duration for displaying key previews
935    // TODO: Display up to two key previews when the user presses two keys at the same time
936    private void showKey(final int keyIndex, PointerTracker tracker) {
937        // If the key preview has no parent view yet, add it to the ViewGroup which can place
938        // key preview absolutely in SoftInputWindow.
939        if (mPreviewText.getParent() == null) {
940            addKeyPreview(mPreviewText);
941        }
942
943        final Key key = tracker.getKey(keyIndex);
944        // If keyIndex is invalid or IME is already closed, we must not show key preview.
945        // Trying to show key preview while root window is closed causes
946        // WindowManager.BadTokenException.
947        if (key == null || !mInForeground)
948            return;
949        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
950        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
951        // What we show as preview should match what we show on key top in onBufferDraw().
952        if (key.mLabel != null) {
953            // TODO Should take care of temporaryShiftLabel here.
954            mPreviewText.setCompoundDrawables(null, null, null, null);
955            mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
956            if (key.mLabel.length() > 1) {
957                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
958                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
959            } else {
960                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
961                mPreviewText.setTypeface(mKeyLetterStyle);
962            }
963        } else {
964            final Drawable previewIcon = key.getPreviewIcon();
965            mPreviewText.setCompoundDrawables(null, null, null,
966                   previewIcon != null ? previewIcon : key.getIcon());
967            mPreviewText.setText(null);
968        }
969        // Set the preview background state
970        mPreviewText.getBackground().setState(
971                key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
972
973        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
974                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
975        int previewWidth = Math.max(mPreviewText.getMeasuredWidth(), keyDrawWidth
976                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
977        final int previewHeight = mPreviewHeight;
978        final ViewGroup.LayoutParams lp = mPreviewText.getLayoutParams();
979        lp.width = previewWidth;
980        lp.height = previewHeight;
981
982        int previewX = keyDrawX - (previewWidth - keyDrawWidth) / 2;
983        int previewY = key.mY - previewHeight + mPreviewOffset;
984
985        mHandler.cancelAllDismissKeyPreviews();
986        getLocationInWindow(mOffsetInWindow);
987        previewX += mOffsetInWindow[0];
988        previewY += mOffsetInWindow[1];
989
990        // Place the key preview.
991        // TODO: Adjust position of key previews which touch screen edges
992        if (lp instanceof ViewGroup.MarginLayoutParams) {
993            ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams)lp;
994            mlp.setMargins(previewX, previewY, 0, 0);
995        }
996        // Record key preview position to display mini-keyboard later at the same position
997        mKeyPreviewDisplayedY = previewY;
998        mPreviewText.setVisibility(VISIBLE);
999    }
1000
1001    /**
1002     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1003     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1004     * draws the cached buffer.
1005     * @see #invalidateKey(Key)
1006     */
1007    public void invalidateAllKeys() {
1008        mDirtyRect.union(0, 0, getWidth(), getHeight());
1009        mDrawPending = true;
1010        invalidate();
1011    }
1012
1013    /**
1014     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1015     * one key is changing it's content. Any changes that affect the position or size of the key
1016     * may not be honored.
1017     * @param key key in the attached {@link Keyboard}.
1018     * @see #invalidateAllKeys
1019     */
1020    @Override
1021    public void invalidateKey(Key key) {
1022        if (key == null)
1023            return;
1024        mInvalidatedKey = key;
1025        mInvalidatedKeyRect.set(0, 0, key.mWidth, key.mHeight);
1026        mInvalidatedKeyRect.offset(key.mX + getPaddingLeft(), key.mY + getPaddingTop());
1027        mDirtyRect.union(mInvalidatedKeyRect);
1028        onBufferDraw();
1029        invalidate(mInvalidatedKeyRect);
1030    }
1031
1032    private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
1033        // Check if we have a popup layout specified first.
1034        if (mPopupLayout == 0) {
1035            return false;
1036        }
1037
1038        Key parentKey = tracker.getKey(keyIndex);
1039        if (parentKey == null)
1040            return false;
1041        boolean result = onLongPress(parentKey, tracker);
1042        if (result) {
1043            dismissAllKeyPreviews();
1044            mMiniKeyboardTrackerId = tracker.mPointerId;
1045            tracker.onLongPressed(mPointerQueue);
1046        }
1047        return result;
1048    }
1049
1050    private void onLongPressShiftKey(PointerTracker tracker) {
1051        tracker.onLongPressed(mPointerQueue);
1052        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1053    }
1054
1055    private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
1056        // When shift key is double tapped, the first tap is correctly processed as usual tap. And
1057        // the second tap is treated as this double tap event, so that we need not mark tracker
1058        // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
1059        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1060    }
1061
1062    private View inflateMiniKeyboardContainer(Key parentKey) {
1063        final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
1064        if (container == null)
1065            throw new NullPointerException();
1066
1067        final KeyboardView miniKeyboardView =
1068                (KeyboardView)container.findViewById(R.id.KeyboardView);
1069        miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
1070            @Override
1071            public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1072                mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
1073                dismissMiniKeyboard();
1074            }
1075
1076            @Override
1077            public void onTextInput(CharSequence text) {
1078                mKeyboardActionListener.onTextInput(text);
1079                dismissMiniKeyboard();
1080            }
1081
1082            @Override
1083            public void onCancelInput() {
1084                mKeyboardActionListener.onCancelInput();
1085                dismissMiniKeyboard();
1086            }
1087
1088            @Override
1089            public void onSwipeDown() {
1090                // Nothing to do.
1091            }
1092            @Override
1093            public void onPress(int primaryCode, boolean withSliding) {
1094                mKeyboardActionListener.onPress(primaryCode, withSliding);
1095            }
1096            @Override
1097            public void onRelease(int primaryCode, boolean withSliding) {
1098                mKeyboardActionListener.onRelease(primaryCode, withSliding);
1099            }
1100        });
1101        // Override default ProximityKeyDetector.
1102        miniKeyboardView.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance);
1103        // Remove gesture detector on mini-keyboard
1104        miniKeyboardView.mGestureDetector = null;
1105
1106        final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
1107                parentKey).build();
1108        miniKeyboardView.setKeyboard(keyboard);
1109
1110        container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1111                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1112
1113        return container;
1114    }
1115
1116    private static boolean isOneRowKeys(List<Key> keys) {
1117        if (keys.size() == 0) return false;
1118        final int edgeFlags = keys.get(0).mEdgeFlags;
1119        // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
1120        // does not have both top and bottom edge flags on at the same time.  On the other hand,
1121        // the first key of mini keyboard that was created with popupCharacters must have both top
1122        // and bottom edge flags on.
1123        // When you want to use one row mini-keyboard from xml file, make sure that the row has
1124        // both top and bottom edge flags set.
1125        return (edgeFlags & Keyboard.EDGE_TOP) != 0
1126                && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
1127    }
1128
1129    /**
1130     * Called when a key is long pressed. By default this will open any mini keyboard associated
1131     * with this key through the attributes popupLayout and popupCharacters.
1132     * @param parentKey the key that was long pressed
1133     * @return true if the long press is handled, false otherwise. Subclasses should call the
1134     * method on the base class if the subclass doesn't wish to handle the call.
1135     */
1136    protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
1137        if (parentKey.mPopupCharacters == null)
1138            return false;
1139
1140        View container = mMiniKeyboardCache.get(parentKey);
1141        if (container == null) {
1142            container = inflateMiniKeyboardContainer(parentKey);
1143            mMiniKeyboardCache.put(parentKey, container);
1144        }
1145        mMiniKeyboardView = (KeyboardView)container.findViewById(R.id.KeyboardView);
1146        final MiniKeyboard miniKeyboard = (MiniKeyboard)mMiniKeyboardView.getKeyboard();
1147
1148        if (mWindowOffset == null) {
1149            mWindowOffset = new int[2];
1150            getLocationInWindow(mWindowOffset);
1151        }
1152        final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
1153                : parentKey.mX + parentKey.mWidth / 2;
1154        final int miniKeyboardX = pointX - miniKeyboard.getDefaultCoordX()
1155                - container.getPaddingLeft()
1156                + getPaddingLeft() + mWindowOffset[0];
1157        final int miniKeyboardY = parentKey.mY - mKeyboard.getVerticalGap()
1158                - (container.getMeasuredHeight() - container.getPaddingBottom())
1159                + getPaddingTop() + mWindowOffset[1];
1160        final int x = miniKeyboardX;
1161        final int y = mShowKeyPreview && isOneRowKeys(miniKeyboard.getKeys())
1162                ? mKeyPreviewDisplayedY : miniKeyboardY;
1163
1164        mMiniKeyboardOriginX = x + container.getPaddingLeft() - mWindowOffset[0];
1165        mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1];
1166        if (miniKeyboard.setShifted(
1167                mKeyboard == null ? false : mKeyboard.isShiftedOrShiftLocked())) {
1168            mMiniKeyboardView.invalidateAllKeys();
1169        }
1170        // Mini keyboard needs no pop-up key preview displayed.
1171        mMiniKeyboardView.setKeyPreviewEnabled(false);
1172        mMiniKeyboardWindow.setContentView(container);
1173        mMiniKeyboardWindow.setWidth(container.getMeasuredWidth());
1174        mMiniKeyboardWindow.setHeight(container.getMeasuredHeight());
1175        mMiniKeyboardWindow.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1176
1177        // Inject down event on the key to mini keyboard.
1178        final long eventTime = SystemClock.uptimeMillis();
1179        mMiniKeyboardDisplayedTime = eventTime;
1180        final MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN,
1181                pointX, parentKey.mY + parentKey.mHeight / 2, eventTime);
1182        mMiniKeyboardView.onTouchEvent(downEvent);
1183        downEvent.recycle();
1184
1185        invalidateAllKeys();
1186        return true;
1187    }
1188
1189    private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) {
1190        return MotionEvent.obtain(mMiniKeyboardDisplayedTime, eventTime, action,
1191                    x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0);
1192    }
1193
1194    private PointerTracker getPointerTracker(final int id) {
1195        final ArrayList<PointerTracker> pointers = mPointerTrackers;
1196        final KeyboardActionListener listener = mKeyboardActionListener;
1197
1198        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
1199        for (int i = pointers.size(); i <= id; i++) {
1200            final PointerTracker tracker =
1201                new PointerTracker(i, this, mHandler, mKeyDetector, this);
1202            if (mKeyboard != null)
1203                tracker.setKeyboard(mKeyboard, mKeyHysteresisDistance);
1204            if (listener != null)
1205                tracker.setOnKeyboardActionListener(listener);
1206            pointers.add(tracker);
1207        }
1208
1209        return pointers.get(id);
1210    }
1211
1212    public boolean isInSlidingKeyInput() {
1213        if (mMiniKeyboardView != null) {
1214            return mMiniKeyboardView.isInSlidingKeyInput();
1215        } else {
1216            return mPointerQueue.isInSlidingKeyInput();
1217        }
1218    }
1219
1220    public int getPointerCount() {
1221        return mOldPointerCount;
1222    }
1223
1224    @Override
1225    public boolean onTouchEvent(MotionEvent me) {
1226        final int action = me.getActionMasked();
1227        final int pointerCount = me.getPointerCount();
1228        final int oldPointerCount = mOldPointerCount;
1229        mOldPointerCount = pointerCount;
1230
1231        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1232        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1233        // events except a transition from/to single-touch.
1234        if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled)
1235                && pointerCount > 1 && oldPointerCount > 1) {
1236            return true;
1237        }
1238
1239        // Track the last few movements to look for spurious swipes.
1240        mSwipeTracker.addMovement(me);
1241
1242        // Gesture detector must be enabled only when mini-keyboard is not on the screen and
1243        // accessibility is not enabled.
1244        // TODO: Reconcile gesture detection and accessibility features.
1245        if (mMiniKeyboardView == null && !mIsAccessibilityEnabled
1246                && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
1247            dismissAllKeyPreviews();
1248            mHandler.cancelKeyTimers();
1249            return true;
1250        }
1251
1252        final long eventTime = me.getEventTime();
1253        final int index = me.getActionIndex();
1254        final int id = me.getPointerId(index);
1255        final int x = (int)me.getX(index);
1256        final int y = (int)me.getY(index);
1257
1258        // Needs to be called after the gesture detector gets a turn, as it may have
1259        // displayed the mini keyboard
1260        if (mMiniKeyboardView != null) {
1261            final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId);
1262            if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) {
1263                final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex);
1264                final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex);
1265                MotionEvent translated = generateMiniKeyboardMotionEvent(action,
1266                        miniKeyboardX, miniKeyboardY, eventTime);
1267                mMiniKeyboardView.onTouchEvent(translated);
1268                translated.recycle();
1269            }
1270            return true;
1271        }
1272
1273        if (mHandler.isInKeyRepeat()) {
1274            final PointerTracker tracker = getPointerTracker(id);
1275            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1276            // event (UP or DOWN) is non-modifier key.
1277            if (pointerCount > 1 && !tracker.isModifier()) {
1278                mHandler.cancelKeyRepeatTimer();
1279            }
1280            // Up event will pass through.
1281        }
1282
1283        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1284        // Translate mutli-touch event to single-touch events on the device that has no distinct
1285        // multi-touch panel.
1286        if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) {
1287            // Use only main (id=0) pointer tracker.
1288            PointerTracker tracker = getPointerTracker(0);
1289            if (pointerCount == 1 && oldPointerCount == 2) {
1290                // Multi-touch to single touch transition.
1291                // Send a down event for the latest pointer if the key is different from the
1292                // previous key.
1293                final int newKeyIndex = tracker.getKeyIndexOn(x, y);
1294                if (mOldKeyIndex != newKeyIndex) {
1295                    tracker.onDownEvent(x, y, eventTime, null);
1296                    if (action == MotionEvent.ACTION_UP)
1297                        tracker.onUpEvent(x, y, eventTime, null);
1298                }
1299            } else if (pointerCount == 2 && oldPointerCount == 1) {
1300                // Single-touch to multi-touch transition.
1301                // Send an up event for the last pointer.
1302                final int lastX = tracker.getLastX();
1303                final int lastY = tracker.getLastY();
1304                mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
1305                tracker.onUpEvent(lastX, lastY, eventTime, null);
1306            } else if (pointerCount == 1 && oldPointerCount == 1) {
1307                tracker.onTouchEvent(action, x, y, eventTime, null);
1308            } else {
1309                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1310                        + " (old " + oldPointerCount + ")");
1311            }
1312            return true;
1313        }
1314
1315        final PointerTrackerQueue queue = mPointerQueue;
1316        if (action == MotionEvent.ACTION_MOVE) {
1317            for (int i = 0; i < pointerCount; i++) {
1318                final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
1319                tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
1320            }
1321        } else {
1322            final PointerTracker tracker = getPointerTracker(id);
1323            switch (action) {
1324            case MotionEvent.ACTION_DOWN:
1325            case MotionEvent.ACTION_POINTER_DOWN:
1326                tracker.onDownEvent(x, y, eventTime, queue);
1327                break;
1328            case MotionEvent.ACTION_UP:
1329            case MotionEvent.ACTION_POINTER_UP:
1330                tracker.onUpEvent(x, y, eventTime, queue);
1331                break;
1332            case MotionEvent.ACTION_CANCEL:
1333                tracker.onCancelEvent(x, y, eventTime, queue);
1334                break;
1335            }
1336        }
1337
1338        return true;
1339    }
1340
1341    protected void onSwipeDown() {
1342        mKeyboardActionListener.onSwipeDown();
1343    }
1344
1345    public void closing() {
1346        mPreviewText.setVisibility(View.GONE);
1347        mHandler.cancelAllMessages();
1348
1349        dismissMiniKeyboard();
1350        mDirtyRect.union(0, 0, getWidth(), getHeight());
1351        mMiniKeyboardCache.clear();
1352        requestLayout();
1353    }
1354
1355    public void purgeKeyboardAndClosing() {
1356        mKeyboard = null;
1357        closing();
1358    }
1359
1360    @Override
1361    public void onDetachedFromWindow() {
1362        super.onDetachedFromWindow();
1363        closing();
1364    }
1365
1366    private void dismissMiniKeyboard() {
1367        if (mMiniKeyboardWindow.isShowing()) {
1368            mMiniKeyboardWindow.dismiss();
1369            mMiniKeyboardView = null;
1370            mMiniKeyboardOriginX = 0;
1371            mMiniKeyboardOriginY = 0;
1372            invalidateAllKeys();
1373        }
1374    }
1375
1376    public boolean handleBack() {
1377        if (mMiniKeyboardWindow.isShowing()) {
1378            dismissMiniKeyboard();
1379            return true;
1380        }
1381        return false;
1382    }
1383}
1384