KeyboardView.java revision 56abeabcedf68f4640bb4ebd4af3ead1e3916c3c
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;
21import com.android.inputmethod.latin.SubtypeSwitcher;
22
23import android.content.Context;
24import android.content.pm.PackageManager;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Bitmap;
28import android.graphics.Canvas;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.Paint.Align;
32import android.graphics.PorterDuff;
33import android.graphics.Rect;
34import android.graphics.Region.Op;
35import android.graphics.Typeface;
36import android.graphics.drawable.Drawable;
37import android.os.Handler;
38import android.os.Message;
39import android.os.SystemClock;
40import android.util.AttributeSet;
41import android.util.Log;
42import android.util.TypedValue;
43import android.view.GestureDetector;
44import android.view.Gravity;
45import android.view.LayoutInflater;
46import android.view.MotionEvent;
47import android.view.View;
48import android.view.ViewGroup.LayoutParams;
49import android.view.WindowManager;
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    private Key[] mKeys;
109
110    // Key preview popup
111    private boolean mInForeground;
112    private TextView mPreviewText;
113    private PopupWindow mPreviewPopup;
114    private int mPreviewTextSizeLarge;
115    private int[] mOffsetInWindow;
116    private int mOldPreviewKeyIndex = KeyDetector.NOT_A_KEY;
117    private boolean mShowPreview = true;
118    private int mPopupPreviewOffsetX;
119    private int mPopupPreviewOffsetY;
120    private int mWindowY;
121    private int mPopupPreviewDisplayedY;
122    private final int mDelayBeforePreview;
123    private final int mDelayAfterPreview;
124
125    // Popup mini keyboard
126    private PopupWindow mMiniKeyboardPopup;
127    private KeyboardView mMiniKeyboardView;
128    private View mMiniKeyboardParent;
129    private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>();
130    private int mMiniKeyboardOriginX;
131    private int mMiniKeyboardOriginY;
132    private long mMiniKeyboardPopupTime;
133    private int[] mWindowOffset;
134    private final float mMiniKeyboardSlideAllowance;
135    private int mMiniKeyboardTrackerId;
136    private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
137
138    /** Listener for {@link KeyboardActionListener}. */
139    private KeyboardActionListener mKeyboardActionListener;
140
141    private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
142
143    // TODO: Let the PointerTracker class manage this pointer queue
144    private final PointerTrackerQueue mPointerQueue = new PointerTrackerQueue();
145
146    private final boolean mHasDistinctMultitouch;
147    private int mOldPointerCount = 1;
148
149    // Accessibility
150    private boolean mIsAccessibilityEnabled;
151
152    protected KeyDetector mKeyDetector = new ProximityKeyDetector();
153
154    // Swipe gesture detector
155    private GestureDetector mGestureDetector;
156    private final SwipeTracker mSwipeTracker = new SwipeTracker();
157    private final int mSwipeThreshold;
158    private final boolean mDisambiguateSwipe;
159
160    // Drawing
161    /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
162    private boolean mDrawPending;
163    /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
164    private boolean mKeyboardChanged;
165    /** The dirty region in the keyboard bitmap */
166    private final Rect mDirtyRect = new Rect();
167    /** The key to invalidate. */
168    private Key mInvalidatedKey;
169    /** The dirty region for single key drawing */
170    private final Rect mInvalidatedKeyRect = new Rect();
171    /** The keyboard bitmap for faster updates */
172    private Bitmap mBuffer;
173    /** The canvas for the above mutable keyboard bitmap */
174    private Canvas mCanvas;
175    private final Paint mPaint;
176    private final Rect mPadding;
177    // This map caches key label text height in pixel as value and key label text size as map key.
178    private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
179    // Distance from horizontal center of the key, proportional to key label text height and width.
180    private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER = 0.45f;
181    private final float KEY_LABEL_VERTICAL_PADDING_FACTOR = 1.60f;
182    private final String KEY_LABEL_REFERENCE_CHAR = "H";
183    private final int KEY_LABEL_OPTION_ALIGN_LEFT = 1;
184    private final int KEY_LABEL_OPTION_ALIGN_RIGHT = 2;
185    private final int KEY_LABEL_OPTION_ALIGN_BOTTOM = 8;
186    private final int KEY_LABEL_OPTION_FONT_NORMAL = 16;
187    private final int mKeyLabelHorizontalPadding;
188
189    private final UIHandler mHandler = new UIHandler();
190
191    class UIHandler extends Handler {
192        private static final int MSG_POPUP_PREVIEW = 1;
193        private static final int MSG_DISMISS_PREVIEW = 2;
194        private static final int MSG_REPEAT_KEY = 3;
195        private static final int MSG_LONGPRESS_KEY = 4;
196        private static final int MSG_LONGPRESS_SHIFT_KEY = 5;
197
198        private boolean mInKeyRepeat;
199
200        @Override
201        public void handleMessage(Message msg) {
202            switch (msg.what) {
203                case MSG_POPUP_PREVIEW:
204                    showKey(msg.arg1, (PointerTracker)msg.obj);
205                    break;
206                case MSG_DISMISS_PREVIEW:
207                    mPreviewPopup.dismiss();
208                    break;
209                case MSG_REPEAT_KEY: {
210                    final PointerTracker tracker = (PointerTracker)msg.obj;
211                    tracker.repeatKey(msg.arg1);
212                    startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
213                    break;
214                }
215                case MSG_LONGPRESS_KEY: {
216                    final PointerTracker tracker = (PointerTracker)msg.obj;
217                    openPopupIfRequired(msg.arg1, tracker);
218                    break;
219                }
220                case MSG_LONGPRESS_SHIFT_KEY: {
221                    final PointerTracker tracker = (PointerTracker)msg.obj;
222                    onLongPressShiftKey(tracker);
223                    break;
224                }
225            }
226        }
227
228        public void popupPreview(long delay, int keyIndex, PointerTracker tracker) {
229            removeMessages(MSG_POPUP_PREVIEW);
230            if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
231                // Show right away, if it's already visible and finger is moving around
232                showKey(keyIndex, tracker);
233            } else {
234                sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker),
235                        delay);
236            }
237        }
238
239        public void cancelPopupPreview() {
240            removeMessages(MSG_POPUP_PREVIEW);
241        }
242
243        public void dismissPreview(long delay) {
244            if (mPreviewPopup.isShowing()) {
245                sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay);
246            }
247        }
248
249        public void cancelDismissPreview() {
250            removeMessages(MSG_DISMISS_PREVIEW);
251        }
252
253        public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
254            mInKeyRepeat = true;
255            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
256        }
257
258        public void cancelKeyRepeatTimer() {
259            mInKeyRepeat = false;
260            removeMessages(MSG_REPEAT_KEY);
261        }
262
263        public boolean isInKeyRepeat() {
264            return mInKeyRepeat;
265        }
266
267        public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
268            cancelLongPressTimers();
269            sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
270        }
271
272        public void startLongPressShiftTimer(long delay, int keyIndex, PointerTracker tracker) {
273            cancelLongPressTimers();
274            if (ENABLE_CAPSLOCK_BY_LONGPRESS) {
275                sendMessageDelayed(
276                        obtainMessage(MSG_LONGPRESS_SHIFT_KEY, keyIndex, 0, tracker), delay);
277            }
278        }
279
280        public void cancelLongPressTimers() {
281            removeMessages(MSG_LONGPRESS_KEY);
282            removeMessages(MSG_LONGPRESS_SHIFT_KEY);
283        }
284
285        public void cancelKeyTimers() {
286            cancelKeyRepeatTimer();
287            cancelLongPressTimers();
288        }
289
290        public void cancelAllMessages() {
291            cancelKeyTimers();
292            cancelPopupPreview();
293            cancelDismissPreview();
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        mPreviewPopup = new PopupWindow(context);
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            mPreviewPopup.setContentView(mPreviewText);
374            mPreviewPopup.setBackgroundDrawable(null);
375        } else {
376            mShowPreview = false;
377        }
378        mPreviewPopup.setTouchable(false);
379        mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
380        mPreviewPopup.setClippingEnabled(false);
381        mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
382        mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
383        mKeyLabelHorizontalPadding = (int)res.getDimension(
384                R.dimen.key_label_horizontal_alignment_padding);
385
386        mMiniKeyboardParent = this;
387        mMiniKeyboardPopup = new PopupWindow(context);
388        mMiniKeyboardPopup.setBackgroundDrawable(null);
389        mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation);
390        // Allow popup window to be drawn off the screen.
391        mMiniKeyboardPopup.setClippingEnabled(false);
392
393        mPaint = new Paint();
394        mPaint.setAntiAlias(true);
395        mPaint.setTextSize(keyTextSize);
396        mPaint.setTextAlign(Align.CENTER);
397        mPaint.setAlpha(255);
398
399        mPadding = new Rect(0, 0, 0, 0);
400        mKeyBackground.getPadding(mPadding);
401
402        mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
403        // TODO: Refer frameworks/base/core/res/res/values/config.xml
404        mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
405        mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance);
406        mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
407                R.bool.config_show_mini_keyboard_at_touched_point);
408
409        GestureDetector.SimpleOnGestureListener listener =
410                new GestureDetector.SimpleOnGestureListener() {
411            private boolean mProcessingShiftDoubleTapEvent = false;
412
413            @Override
414            public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
415                    float velocityY) {
416                final float absX = Math.abs(velocityX);
417                final float absY = Math.abs(velocityY);
418                float deltaY = me2.getY() - me1.getY();
419                int travelY = getHeight() / 2; // Half the keyboard height
420                mSwipeTracker.computeCurrentVelocity(1000);
421                final float endingVelocityY = mSwipeTracker.getYVelocity();
422                if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
423                    if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
424                        onSwipeDown();
425                        return true;
426                    }
427                }
428                return false;
429            }
430
431            @Override
432            public boolean onDoubleTap(MotionEvent firstDown) {
433                if (ENABLE_CAPSLOCK_BY_DOUBLETAP && mKeyboard instanceof LatinKeyboard
434                        && ((LatinKeyboard) mKeyboard).isAlphaKeyboard()) {
435                    final int pointerIndex = firstDown.getActionIndex();
436                    final int id = firstDown.getPointerId(pointerIndex);
437                    final PointerTracker tracker = getPointerTracker(id);
438                    // If the first down event is on shift key.
439                    if (tracker.isOnShiftKey((int)firstDown.getX(), (int)firstDown.getY())) {
440                        mProcessingShiftDoubleTapEvent = true;
441                        return true;
442                    }
443                }
444                mProcessingShiftDoubleTapEvent = false;
445                return false;
446            }
447
448            @Override
449            public boolean onDoubleTapEvent(MotionEvent secondTap) {
450                if (mProcessingShiftDoubleTapEvent
451                        && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
452                    final MotionEvent secondDown = secondTap;
453                    final int pointerIndex = secondDown.getActionIndex();
454                    final int id = secondDown.getPointerId(pointerIndex);
455                    final PointerTracker tracker = getPointerTracker(id);
456                    // If the second down event is also on shift key.
457                    if (tracker.isOnShiftKey((int)secondDown.getX(), (int)secondDown.getY())) {
458                        onDoubleTapShiftKey(tracker);
459                        return true;
460                    }
461                    // Otherwise these events should not be handled as double tap.
462                    mProcessingShiftDoubleTapEvent = false;
463                }
464                return mProcessingShiftDoubleTapEvent;
465            }
466        };
467
468        final boolean ignoreMultitouch = true;
469        mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
470        mGestureDetector.setIsLongpressEnabled(false);
471
472        mHasDistinctMultitouch = context.getPackageManager()
473                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
474        mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
475    }
476
477    public void setOnKeyboardActionListener(KeyboardActionListener listener) {
478        mKeyboardActionListener = listener;
479        for (PointerTracker tracker : mPointerTrackers) {
480            tracker.setOnKeyboardActionListener(listener);
481        }
482    }
483
484    /**
485     * Returns the {@link KeyboardActionListener} object.
486     * @return the listener attached to this keyboard
487     */
488    protected KeyboardActionListener getOnKeyboardActionListener() {
489        return mKeyboardActionListener;
490    }
491
492    /**
493     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
494     * view will re-layout itself to accommodate the keyboard.
495     * @see Keyboard
496     * @see #getKeyboard()
497     * @param keyboard the keyboard to display in this view
498     */
499    public void setKeyboard(Keyboard keyboard) {
500        if (mKeyboard != null) {
501            dismissKeyPreview();
502        }
503        // Remove any pending messages, except dismissing preview
504        mHandler.cancelKeyTimers();
505        mHandler.cancelPopupPreview();
506        mKeyboard = keyboard;
507        LatinImeLogger.onSetKeyboard(keyboard);
508        mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
509                -getPaddingTop() + mVerticalCorrection);
510        for (PointerTracker tracker : mPointerTrackers) {
511            tracker.setKeyboard(keyboard, mKeys, mKeyHysteresisDistance);
512        }
513        requestLayout();
514        mKeyboardChanged = true;
515        invalidateAllKeys();
516        mKeyDetector.setProximityThreshold(KeyDetector.getMostCommonKeyWidth(keyboard));
517        mMiniKeyboardCache.clear();
518    }
519
520    /**
521     * Returns the current keyboard being displayed by this view.
522     * @return the currently attached keyboard
523     * @see #setKeyboard(Keyboard)
524     */
525    public Keyboard getKeyboard() {
526        return mKeyboard;
527    }
528
529    /**
530     * Returns whether the device has distinct multi-touch panel.
531     * @return true if the device has distinct multi-touch panel.
532     */
533    @Override
534    public boolean hasDistinctMultitouch() {
535        return mHasDistinctMultitouch;
536    }
537
538    /**
539     * Enables or disables accessibility.
540     * @param accessibilityEnabled whether or not to enable accessibility
541     */
542    public void setAccessibilityEnabled(boolean accessibilityEnabled) {
543        mIsAccessibilityEnabled = accessibilityEnabled;
544
545        // Propagate this change to all existing pointer trackers.
546        for (PointerTracker tracker : mPointerTrackers) {
547            tracker.setAccessibilityEnabled(accessibilityEnabled);
548        }
549    }
550
551    /**
552     * Returns whether the device has accessibility enabled.
553     * @return true if the device has accessibility enabled.
554     */
555    @Override
556    public boolean isAccessibilityEnabled() {
557        return mIsAccessibilityEnabled;
558    }
559
560    /**
561     * Enables or disables the key feedback popup. This is a popup that shows a magnified
562     * version of the depressed key. By default the preview is enabled.
563     * @param previewEnabled whether or not to enable the key feedback popup
564     * @see #isPreviewEnabled()
565     */
566    public void setPreviewEnabled(boolean previewEnabled) {
567        mShowPreview = previewEnabled;
568    }
569
570    /**
571     * Returns the enabled state of the key feedback popup.
572     * @return whether or not the key feedback popup is enabled
573     * @see #setPreviewEnabled(boolean)
574     */
575    public boolean isPreviewEnabled() {
576        return mShowPreview;
577    }
578
579    public int getColorScheme() {
580        return mColorScheme;
581    }
582
583    public void setPopupOffset(int x, int y) {
584        mPopupPreviewOffsetX = x;
585        mPopupPreviewOffsetY = y;
586        mPreviewPopup.dismiss();
587    }
588
589    /**
590     * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
591     * codes for adjacent keys.  When disabled, only the primary key code will be
592     * reported.
593     * @param enabled whether or not the proximity correction is enabled
594     */
595    public void setProximityCorrectionEnabled(boolean enabled) {
596        mKeyDetector.setProximityCorrectionEnabled(enabled);
597    }
598
599    /**
600     * Returns true if proximity correction is enabled.
601     */
602    public boolean isProximityCorrectionEnabled() {
603        return mKeyDetector.isProximityCorrectionEnabled();
604    }
605
606    protected CharSequence adjustCase(CharSequence label) {
607        if (mKeyboard.isShiftedOrShiftLocked() && label != null && label.length() < 3
608                && Character.isLowerCase(label.charAt(0))) {
609            return label.toString().toUpperCase();
610        }
611        return label;
612    }
613
614    @Override
615    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
616        // Round up a little
617        if (mKeyboard == null) {
618            setMeasuredDimension(
619                    getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
620        } else {
621            int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
622            if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
623                width = MeasureSpec.getSize(widthMeasureSpec);
624            }
625            setMeasuredDimension(
626                    width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
627        }
628    }
629
630    @Override
631    public void onDraw(Canvas canvas) {
632        super.onDraw(canvas);
633        if (mDrawPending || mBuffer == null || mKeyboardChanged) {
634            onBufferDraw();
635        }
636        canvas.drawBitmap(mBuffer, 0, 0, null);
637    }
638
639    private void onBufferDraw() {
640        final int width = getWidth();
641        final int height = getHeight();
642        if (width == 0 || height == 0)
643            return;
644        if (mBuffer == null || mKeyboardChanged) {
645            mKeyboardChanged = false;
646            mDirtyRect.union(0, 0, width, height);
647        }
648        if (mBuffer == null || mBuffer.getWidth() != width || mBuffer.getHeight() != height) {
649            mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
650            mCanvas = new Canvas(mBuffer);
651        }
652        final Canvas canvas = mCanvas;
653        canvas.clipRect(mDirtyRect, Op.REPLACE);
654
655        if (mKeyboard == null) return;
656
657        final Paint paint = mPaint;
658        final Drawable keyBackground = mKeyBackground;
659        final Rect padding = mPadding;
660        final int kbdPaddingLeft = getPaddingLeft();
661        final int kbdPaddingTop = getPaddingTop();
662        final Key[] keys = mKeys;
663        final boolean isManualTemporaryUpperCase = mKeyboard.isManualTemporaryUpperCase();
664        final boolean drawSingleKey = (mInvalidatedKey != null
665                && mInvalidatedKeyRect.contains(mDirtyRect));
666
667        canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
668        final int keyCount = keys.length;
669        for (int i = 0; i < keyCount; i++) {
670            final Key key = keys[i];
671            if (drawSingleKey && key != mInvalidatedKey) {
672                continue;
673            }
674            int[] drawableState = key.getCurrentDrawableState();
675            keyBackground.setState(drawableState);
676
677            // Switch the character to uppercase if shift is pressed
678            String label = key.mLabel == null? null : adjustCase(key.mLabel).toString();
679
680            final int keyDrawX = key.mX + key.mVisualInsetsLeft;
681            final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
682            final Rect bounds = keyBackground.getBounds();
683            if (keyDrawWidth != bounds.right || key.mHeight != bounds.bottom) {
684                keyBackground.setBounds(0, 0, keyDrawWidth, key.mHeight);
685            }
686            canvas.translate(keyDrawX + kbdPaddingLeft, key.mY + kbdPaddingTop);
687            keyBackground.draw(canvas);
688
689            final int rowHeight = padding.top + key.mHeight;
690            // Draw key label
691            if (label != null) {
692                // For characters, use large font. For labels like "Done", use small font.
693                final int labelSize = getLabelSizeAndSetPaint(label, key.mLabelOption, paint);
694                final int labelCharHeight = getLabelCharHeight(labelSize, paint);
695
696                // Vertical label text alignment.
697                final float baseline;
698                if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_BOTTOM) != 0) {
699                    baseline = key.mHeight -
700                            + labelCharHeight * KEY_LABEL_VERTICAL_PADDING_FACTOR;
701                    if (DEBUG_SHOW_ALIGN)
702                        drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
703                                new Paint());
704                } else { // Align center
705                    final float centerY = (key.mHeight + padding.top - padding.bottom) / 2;
706                    baseline = centerY
707                            + labelCharHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR_CENTER;
708                    if (DEBUG_SHOW_ALIGN)
709                        drawHorizontalLine(canvas, (int)baseline, keyDrawWidth, 0xc0008000,
710                                new Paint());
711                }
712                // Horizontal label text alignment
713                final int positionX;
714                if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) {
715                    positionX = mKeyLabelHorizontalPadding + padding.left;
716                    paint.setTextAlign(Align.LEFT);
717                    if (DEBUG_SHOW_ALIGN)
718                        drawVerticalLine(canvas, positionX, rowHeight, 0xc0800080, new Paint());
719                } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) {
720                    positionX = keyDrawWidth - mKeyLabelHorizontalPadding - padding.right;
721                    paint.setTextAlign(Align.RIGHT);
722                    if (DEBUG_SHOW_ALIGN)
723                        drawVerticalLine(canvas, positionX, rowHeight, 0xc0808000, new Paint());
724                } else {
725                    positionX = (keyDrawWidth + padding.left - padding.right) / 2;
726                    paint.setTextAlign(Align.CENTER);
727                    if (DEBUG_SHOW_ALIGN) {
728                        if (label.length() > 1)
729                            drawVerticalLine(canvas, positionX, rowHeight, 0xc0008080, new Paint());
730                    }
731                }
732                if (key.mManualTemporaryUpperCaseHintIcon != null && isManualTemporaryUpperCase) {
733                    paint.setColor(mKeyTextColorDisabled);
734                } else {
735                    paint.setColor(mKeyTextColor);
736                }
737                if (key.mEnabled) {
738                    // Set a drop shadow for the text
739                    paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
740                } else {
741                    // Make label invisible
742                    paint.setColor(Color.TRANSPARENT);
743                }
744                canvas.drawText(label, positionX, baseline, paint);
745                // Turn off drop shadow
746                paint.setShadowLayer(0, 0, 0, 0);
747            }
748            // Draw key icon
749            final Drawable icon = key.getIcon();
750            if (key.mLabel == null && icon != null) {
751                final int drawableWidth = icon.getIntrinsicWidth();
752                final int drawableHeight = icon.getIntrinsicHeight();
753                final int drawableX;
754                final int drawableY = (
755                        key.mHeight + padding.top - padding.bottom - drawableHeight) / 2;
756                if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_LEFT) != 0) {
757                    drawableX = padding.left + mKeyLabelHorizontalPadding;
758                    if (DEBUG_SHOW_ALIGN)
759                        drawVerticalLine(canvas, drawableX, rowHeight, 0xc0800080, new Paint());
760                } else if ((key.mLabelOption & KEY_LABEL_OPTION_ALIGN_RIGHT) != 0) {
761                    drawableX = keyDrawWidth - padding.right - mKeyLabelHorizontalPadding
762                            - drawableWidth;
763                    if (DEBUG_SHOW_ALIGN)
764                        drawVerticalLine(canvas, drawableX + drawableWidth, rowHeight,
765                                0xc0808000, new Paint());
766                } else { // Align center
767                    drawableX = (keyDrawWidth + padding.left - padding.right - drawableWidth) / 2;
768                    if (DEBUG_SHOW_ALIGN)
769                        drawVerticalLine(canvas, drawableX + drawableWidth / 2, rowHeight,
770                                0xc0008080, new Paint());
771                }
772                drawIcon(canvas, icon, drawableX, drawableY, drawableWidth, drawableHeight);
773                if (DEBUG_SHOW_ALIGN)
774                    drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
775                            0x80c00000, new Paint());
776            }
777            if (key.mHintIcon != null) {
778                final int drawableWidth = keyDrawWidth;
779                final int drawableHeight = key.mHeight;
780                final int drawableX = 0;
781                final int drawableY = HINT_ICON_VERTICAL_ADJUSTMENT_PIXEL;
782                Drawable hintIcon = (isManualTemporaryUpperCase
783                        && key.mManualTemporaryUpperCaseHintIcon != null)
784                        ? key.mManualTemporaryUpperCaseHintIcon : key.mHintIcon;
785                drawIcon(canvas, hintIcon, drawableX, drawableY, drawableWidth, drawableHeight);
786                if (DEBUG_SHOW_ALIGN)
787                    drawRectangle(canvas, drawableX, drawableY, drawableWidth, drawableHeight,
788                            0x80c0c000, new Paint());
789            }
790            canvas.translate(-keyDrawX - kbdPaddingLeft, -key.mY - kbdPaddingTop);
791        }
792
793        // TODO: Move this function to ProximityInfo for getting rid of public declarations for
794        // GRID_WIDTH and GRID_HEIGHT
795        if (DEBUG_KEYBOARD_GRID) {
796            Paint p = new Paint();
797            p.setStyle(Paint.Style.STROKE);
798            p.setStrokeWidth(1.0f);
799            p.setColor(0x800000c0);
800            int cw = (mKeyboard.getMinWidth() + mKeyboard.GRID_WIDTH - 1) / mKeyboard.GRID_WIDTH;
801            int ch = (mKeyboard.getHeight() + mKeyboard.GRID_HEIGHT - 1) / mKeyboard.GRID_HEIGHT;
802            for (int i = 0; i <= mKeyboard.GRID_WIDTH; i++)
803                canvas.drawLine(i * cw, 0, i * cw, ch * mKeyboard.GRID_HEIGHT, p);
804            for (int i = 0; i <= mKeyboard.GRID_HEIGHT; i++)
805                canvas.drawLine(0, i * ch, cw * mKeyboard.GRID_WIDTH, i * ch, p);
806        }
807
808        // Overlay a dark rectangle to dim the keyboard
809        if (mMiniKeyboardView != null) {
810            paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
811            canvas.drawRect(0, 0, width, height, paint);
812        }
813
814        mInvalidatedKey = null;
815        mDrawPending = false;
816        mDirtyRect.setEmpty();
817    }
818
819    public int getLabelSizeAndSetPaint(CharSequence label, int keyLabelOption, Paint paint) {
820        // For characters, use large font. For labels like "Done", use small font.
821        final int labelSize;
822        final Typeface labelStyle;
823        if (label.length() > 1) {
824            labelSize = mLabelTextSize;
825            if ((keyLabelOption & KEY_LABEL_OPTION_FONT_NORMAL) != 0) {
826                labelStyle = Typeface.DEFAULT;
827            } else {
828                labelStyle = Typeface.DEFAULT_BOLD;
829            }
830        } else {
831            labelSize = mKeyLetterSize;
832            labelStyle = mKeyLetterStyle;
833        }
834        paint.setTextSize(labelSize);
835        paint.setTypeface(labelStyle);
836        return labelSize;
837    }
838
839    private int getLabelCharHeight(int labelSize, Paint paint) {
840        Integer labelHeightValue = mTextHeightCache.get(labelSize);
841        final int labelCharHeight;
842        if (labelHeightValue != null) {
843            labelCharHeight = labelHeightValue;
844        } else {
845            Rect textBounds = new Rect();
846            paint.getTextBounds(KEY_LABEL_REFERENCE_CHAR, 0, 1, textBounds);
847            labelCharHeight = textBounds.height();
848            mTextHeightCache.put(labelSize, labelCharHeight);
849        }
850        return labelCharHeight;
851    }
852
853    private static void drawIcon(Canvas canvas, Drawable icon, int x, int y, int width,
854            int height) {
855        canvas.translate(x, y);
856        icon.setBounds(0, 0, width, height);
857        icon.draw(canvas);
858        canvas.translate(-x, -y);
859    }
860
861    private static void drawHorizontalLine(Canvas canvas, int y, int w, int color, Paint paint) {
862        paint.setStyle(Paint.Style.STROKE);
863        paint.setStrokeWidth(1.0f);
864        paint.setColor(color);
865        canvas.drawLine(0, y, w, y, paint);
866    }
867
868    private static void drawVerticalLine(Canvas canvas, int x, int h, int color, Paint paint) {
869        paint.setStyle(Paint.Style.STROKE);
870        paint.setStrokeWidth(1.0f);
871        paint.setColor(color);
872        canvas.drawLine(x, 0, x, h, paint);
873    }
874
875    private static void drawRectangle(Canvas canvas, int x, int y, int w, int h, int color,
876            Paint paint) {
877        paint.setStyle(Paint.Style.STROKE);
878        paint.setStrokeWidth(1.0f);
879        paint.setColor(color);
880        canvas.translate(x, y);
881        canvas.drawRect(0, 0, w, h, paint);
882        canvas.translate(-x, -y);
883    }
884
885    public void setForeground(boolean foreground) {
886        mInForeground = foreground;
887    }
888
889    // TODO: clean up this method.
890    private void dismissKeyPreview() {
891        for (PointerTracker tracker : mPointerTrackers)
892            tracker.releaseKey();
893        showPreview(KeyDetector.NOT_A_KEY, null);
894    }
895
896    @Override
897    public void showPreview(int keyIndex, PointerTracker tracker) {
898        int oldKeyIndex = mOldPreviewKeyIndex;
899        mOldPreviewKeyIndex = keyIndex;
900        // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show
901        // the space key preview and 3) pointer moves off the space key to other letter key, we
902        // should hide the preview of the previous key.
903        final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null)
904                || (SubtypeSwitcher.getInstance().useSpacebarLanguageSwitcher()
905                        && SubtypeSwitcher.getInstance().needsToDisplayLanguage()
906                        && (tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex)));
907        // If key changed and preview is on or the key is space (language switch is enabled)
908        if (oldKeyIndex != keyIndex && (mShowPreview || (hidePreviewOrShowSpaceKeyPreview))) {
909            if (keyIndex == KeyDetector.NOT_A_KEY) {
910                mHandler.cancelPopupPreview();
911                mHandler.dismissPreview(mDelayAfterPreview);
912            } else if (tracker != null) {
913                mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker);
914            }
915        }
916    }
917
918    // TODO Must fix popup preview on xlarge layout
919    private void showKey(final int keyIndex, PointerTracker tracker) {
920        Key key = tracker.getKey(keyIndex);
921        // If keyIndex is invalid or IME is already closed, we must not show key preview.
922        // Trying to show preview PopupWindow while root window is closed causes
923        // WindowManager.BadTokenException.
924        if (key == null || !mInForeground)
925            return;
926        final int keyDrawX = key.mX + key.mVisualInsetsLeft;
927        final int keyDrawWidth = key.mWidth - key.mVisualInsetsLeft - key.mVisualInsetsRight;
928        // What we show as preview should match what we show on key top in onBufferDraw().
929        if (key.mLabel != null) {
930            // TODO Should take care of temporaryShiftLabel here.
931            mPreviewText.setCompoundDrawables(null, null, null, null);
932            mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
933            if (key.mLabel.length() > 1) {
934                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyLetterSize);
935                mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
936            } else {
937                mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
938                mPreviewText.setTypeface(mKeyLetterStyle);
939            }
940        } else {
941            final Drawable previewIcon = key.getPreviewIcon();
942            mPreviewText.setCompoundDrawables(null, null, null,
943                   previewIcon != null ? previewIcon : key.getIcon());
944            mPreviewText.setText(null);
945        }
946        mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
947                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
948        int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), keyDrawWidth
949                + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
950        final int popupHeight = mPreviewHeight;
951        LayoutParams lp = mPreviewText.getLayoutParams();
952        if (lp != null) {
953            lp.width = popupWidth;
954            lp.height = popupHeight;
955        }
956
957        int popupPreviewX = keyDrawX - (popupWidth - keyDrawWidth) / 2;
958        int popupPreviewY = key.mY - popupHeight + mPreviewOffset;
959
960        mHandler.cancelDismissPreview();
961        if (mOffsetInWindow == null) {
962            mOffsetInWindow = new int[2];
963            getLocationInWindow(mOffsetInWindow);
964            mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero
965            mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero
966            int[] windowLocation = new int[2];
967            getLocationOnScreen(windowLocation);
968            mWindowY = windowLocation[1];
969        }
970        // Set the preview background state
971        mPreviewText.getBackground().setState(
972                key.mPopupCharacters != null ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
973        popupPreviewX += mOffsetInWindow[0];
974        popupPreviewY += mOffsetInWindow[1];
975
976        // If the popup cannot be shown above the key, put it on the side
977        if (popupPreviewY + mWindowY < 0) {
978            // If the key you're pressing is on the left side of the keyboard, show the popup on
979            // the right, offset by enough to see at least one key to the left/right.
980            if (keyDrawX + keyDrawWidth <= getWidth() / 2) {
981                popupPreviewX += (int) (keyDrawWidth * 2.5);
982            } else {
983                popupPreviewX -= (int) (keyDrawWidth * 2.5);
984            }
985            popupPreviewY += popupHeight;
986        }
987
988        try {
989            if (mPreviewPopup.isShowing()) {
990                mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight);
991            } else {
992                mPreviewPopup.setWidth(popupWidth);
993                mPreviewPopup.setHeight(popupHeight);
994                mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY,
995                        popupPreviewX, popupPreviewY);
996            }
997        } catch (WindowManager.BadTokenException e) {
998            // Swallow the exception which will be happened when IME is already closed.
999            Log.w(TAG, "LatinIME is already closed when tried showing key preview.");
1000        }
1001        // Record popup preview position to display mini-keyboard later at the same positon
1002        mPopupPreviewDisplayedY = popupPreviewY;
1003        mPreviewText.setVisibility(VISIBLE);
1004    }
1005
1006    /**
1007     * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
1008     * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
1009     * draws the cached buffer.
1010     * @see #invalidateKey(Key)
1011     */
1012    public void invalidateAllKeys() {
1013        mDirtyRect.union(0, 0, getWidth(), getHeight());
1014        mDrawPending = true;
1015        invalidate();
1016    }
1017
1018    /**
1019     * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
1020     * one key is changing it's content. Any changes that affect the position or size of the key
1021     * may not be honored.
1022     * @param key key in the attached {@link Keyboard}.
1023     * @see #invalidateAllKeys
1024     */
1025    @Override
1026    public void invalidateKey(Key key) {
1027        if (key == null)
1028            return;
1029        mInvalidatedKey = key;
1030        mInvalidatedKeyRect.set(0, 0, key.mWidth, key.mHeight);
1031        mInvalidatedKeyRect.offset(key.mX + getPaddingLeft(), key.mY + getPaddingTop());
1032        mDirtyRect.union(mInvalidatedKeyRect);
1033        onBufferDraw();
1034        invalidate(mInvalidatedKeyRect);
1035    }
1036
1037    private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) {
1038        // Check if we have a popup layout specified first.
1039        if (mPopupLayout == 0) {
1040            return false;
1041        }
1042
1043        Key popupKey = tracker.getKey(keyIndex);
1044        if (popupKey == null)
1045            return false;
1046        boolean result = onLongPress(popupKey, tracker);
1047        if (result) {
1048            dismissKeyPreview();
1049            mMiniKeyboardTrackerId = tracker.mPointerId;
1050            // Mark this tracker "already processed" and remove it from the pointer queue
1051            tracker.setAlreadyProcessed();
1052            mPointerQueue.remove(tracker);
1053        }
1054        return result;
1055    }
1056
1057    private void onLongPressShiftKey(PointerTracker tracker) {
1058        tracker.setAlreadyProcessed();
1059        mPointerQueue.remove(tracker);
1060        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1061    }
1062
1063    private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker) {
1064        // When shift key is double tapped, the first tap is correctly processed as usual tap. And
1065        // the second tap is treated as this double tap event, so that we need not mark tracker
1066        // calling setAlreadyProcessed() nor remove the tracker from mPointerQueueueue.
1067        mKeyboardActionListener.onCodeInput(Keyboard.CODE_CAPSLOCK, null, 0, 0);
1068    }
1069
1070    private View inflateMiniKeyboardContainer(Key popupKey) {
1071        final View container = LayoutInflater.from(getContext()).inflate(mPopupLayout, null);
1072        if (container == null)
1073            throw new NullPointerException();
1074
1075        final KeyboardView miniKeyboardView =
1076                (KeyboardView)container.findViewById(R.id.KeyboardView);
1077        miniKeyboardView.setOnKeyboardActionListener(new KeyboardActionListener() {
1078            @Override
1079            public void onCodeInput(int primaryCode, int[] keyCodes, int x, int y) {
1080                mKeyboardActionListener.onCodeInput(primaryCode, keyCodes, x, y);
1081                dismissPopupKeyboard();
1082            }
1083
1084            @Override
1085            public void onTextInput(CharSequence text) {
1086                mKeyboardActionListener.onTextInput(text);
1087                dismissPopupKeyboard();
1088            }
1089
1090            @Override
1091            public void onCancelInput() {
1092                mKeyboardActionListener.onCancelInput();
1093                dismissPopupKeyboard();
1094            }
1095
1096            @Override
1097            public void onSwipeDown() {
1098                // Nothing to do.
1099            }
1100            @Override
1101            public void onPress(int primaryCode, boolean withSliding) {
1102                mKeyboardActionListener.onPress(primaryCode, withSliding);
1103            }
1104            @Override
1105            public void onRelease(int primaryCode, boolean withSliding) {
1106                mKeyboardActionListener.onRelease(primaryCode, withSliding);
1107            }
1108        });
1109        // Override default ProximityKeyDetector.
1110        miniKeyboardView.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance);
1111        // Remove gesture detector on mini-keyboard
1112        miniKeyboardView.mGestureDetector = null;
1113
1114        final Keyboard keyboard = new MiniKeyboardBuilder(this, mKeyboard.getPopupKeyboardResId(),
1115                popupKey).build();
1116        miniKeyboardView.setKeyboard(keyboard);
1117        miniKeyboardView.mMiniKeyboardParent = this;
1118
1119        container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
1120                MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
1121
1122        return container;
1123    }
1124
1125    private static boolean isOneRowKeys(List<Key> keys) {
1126        if (keys.size() == 0) return false;
1127        final int edgeFlags = keys.get(0).mEdgeFlags;
1128        // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
1129        // does not have both top and bottom edge flags on at the same time.  On the other hand,
1130        // the first key of mini keyboard that was created with popupCharacters must have both top
1131        // and bottom edge flags on.
1132        // When you want to use one row mini-keyboard from xml file, make sure that the row has
1133        // both top and bottom edge flags set.
1134        return (edgeFlags & Keyboard.EDGE_TOP) != 0
1135                && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
1136    }
1137
1138    /**
1139     * Called when a key is long pressed. By default this will open any popup keyboard associated
1140     * with this key through the attributes popupLayout and popupCharacters.
1141     * @param popupKey the key that was long pressed
1142     * @return true if the long press is handled, false otherwise. Subclasses should call the
1143     * method on the base class if the subclass doesn't wish to handle the call.
1144     */
1145    protected boolean onLongPress(Key popupKey, PointerTracker tracker) {
1146        if (popupKey.mPopupCharacters == null)
1147            return false;
1148
1149        View container = mMiniKeyboardCache.get(popupKey);
1150        if (container == null) {
1151            container = inflateMiniKeyboardContainer(popupKey);
1152            mMiniKeyboardCache.put(popupKey, container);
1153        }
1154        mMiniKeyboardView = (KeyboardView)container.findViewById(R.id.KeyboardView);
1155        final MiniKeyboard miniKeyboard = (MiniKeyboard)mMiniKeyboardView.getKeyboard();
1156
1157        if (mWindowOffset == null) {
1158            mWindowOffset = new int[2];
1159            getLocationInWindow(mWindowOffset);
1160        }
1161        final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
1162                : popupKey.mX + popupKey.mWidth / 2;
1163        final int popupX = pointX - miniKeyboard.getDefaultCoordX()
1164                - container.getPaddingLeft()
1165                + getPaddingLeft() + mWindowOffset[0];
1166        final int popupY = popupKey.mY - mKeyboard.getVerticalGap()
1167                - (container.getMeasuredHeight() - container.getPaddingBottom())
1168                + getPaddingTop() + mWindowOffset[1];
1169        final int x = popupX;
1170        final int y = mShowPreview && isOneRowKeys(miniKeyboard.getKeys())
1171                ? mPopupPreviewDisplayedY : popupY;
1172
1173        mMiniKeyboardOriginX = x + container.getPaddingLeft() - mWindowOffset[0];
1174        mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1];
1175        mMiniKeyboardView.setPopupOffset(x, y);
1176        if (miniKeyboard.setShifted(
1177                mKeyboard == null ? false : mKeyboard.isShiftedOrShiftLocked())) {
1178            mMiniKeyboardView.invalidateAllKeys();
1179        }
1180        // Mini keyboard needs no pop-up key preview displayed.
1181        mMiniKeyboardView.setPreviewEnabled(false);
1182        mMiniKeyboardPopup.setContentView(container);
1183        mMiniKeyboardPopup.setWidth(container.getMeasuredWidth());
1184        mMiniKeyboardPopup.setHeight(container.getMeasuredHeight());
1185        mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
1186
1187        // Inject down event on the key to mini keyboard.
1188        final long eventTime = SystemClock.uptimeMillis();
1189        mMiniKeyboardPopupTime = eventTime;
1190        final MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN,
1191                pointX, popupKey.mY + popupKey.mHeight / 2, eventTime);
1192        mMiniKeyboardView.onTouchEvent(downEvent);
1193        downEvent.recycle();
1194
1195        invalidateAllKeys();
1196        return true;
1197    }
1198
1199    private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) {
1200        return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action,
1201                    x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0);
1202    }
1203
1204    private PointerTracker getPointerTracker(final int id) {
1205        final ArrayList<PointerTracker> pointers = mPointerTrackers;
1206        final Key[] keys = mKeys;
1207        final KeyboardActionListener listener = mKeyboardActionListener;
1208
1209        // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
1210        for (int i = pointers.size(); i <= id; i++) {
1211            final PointerTracker tracker =
1212                new PointerTracker(i, mHandler, mKeyDetector, this, getResources());
1213            if (keys != null)
1214                tracker.setKeyboard(mKeyboard, keys, mKeyHysteresisDistance);
1215            if (listener != null)
1216                tracker.setOnKeyboardActionListener(listener);
1217            pointers.add(tracker);
1218        }
1219
1220        return pointers.get(id);
1221    }
1222
1223    public boolean isInSlidingKeyInput() {
1224        if (mMiniKeyboardView != null) {
1225            return mMiniKeyboardView.isInSlidingKeyInput();
1226        } else {
1227            return mPointerQueue.isInSlidingKeyInput();
1228        }
1229    }
1230
1231    public int getPointerCount() {
1232        return mOldPointerCount;
1233    }
1234
1235    @Override
1236    public boolean onTouchEvent(MotionEvent me) {
1237        final int action = me.getActionMasked();
1238        final int pointerCount = me.getPointerCount();
1239        final int oldPointerCount = mOldPointerCount;
1240        mOldPointerCount = pointerCount;
1241
1242        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1243        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1244        // events except a transition from/to single-touch.
1245        if ((!mHasDistinctMultitouch || mIsAccessibilityEnabled)
1246                && pointerCount > 1 && oldPointerCount > 1) {
1247            return true;
1248        }
1249
1250        // Track the last few movements to look for spurious swipes.
1251        mSwipeTracker.addMovement(me);
1252
1253        // Gesture detector must be enabled only when mini-keyboard is not on the screen and
1254        // accessibility is not enabled.
1255        // TODO: Reconcile gesture detection and accessibility features.
1256        if (mMiniKeyboardView == null && !mIsAccessibilityEnabled
1257                && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
1258            dismissKeyPreview();
1259            mHandler.cancelKeyTimers();
1260            return true;
1261        }
1262
1263        final long eventTime = me.getEventTime();
1264        final int index = me.getActionIndex();
1265        final int id = me.getPointerId(index);
1266        final int x = (int)me.getX(index);
1267        final int y = (int)me.getY(index);
1268
1269        // Needs to be called after the gesture detector gets a turn, as it may have
1270        // displayed the mini keyboard
1271        if (mMiniKeyboardView != null) {
1272            final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId);
1273            if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) {
1274                final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex);
1275                final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex);
1276                MotionEvent translated = generateMiniKeyboardMotionEvent(action,
1277                        miniKeyboardX, miniKeyboardY, eventTime);
1278                mMiniKeyboardView.onTouchEvent(translated);
1279                translated.recycle();
1280            }
1281            return true;
1282        }
1283
1284        if (mHandler.isInKeyRepeat()) {
1285            // It will keep being in the key repeating mode while the key is being pressed.
1286            if (action == MotionEvent.ACTION_MOVE) {
1287                return true;
1288            }
1289            final PointerTracker tracker = getPointerTracker(id);
1290            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1291            // event (UP or DOWN) is non-modifier key.
1292            if (pointerCount > 1 && !tracker.isModifier()) {
1293                mHandler.cancelKeyRepeatTimer();
1294            }
1295            // Up event will pass through.
1296        }
1297
1298        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1299        // Translate mutli-touch event to single-touch events on the device that has no distinct
1300        // multi-touch panel.
1301        if (!mHasDistinctMultitouch || mIsAccessibilityEnabled) {
1302            // Use only main (id=0) pointer tracker.
1303            PointerTracker tracker = getPointerTracker(0);
1304            if (pointerCount == 1 && oldPointerCount == 2) {
1305                // Multi-touch to single touch transition.
1306                // Send a down event for the latest pointer.
1307                tracker.onDownEvent(x, y, eventTime, null);
1308            } else if (pointerCount == 2 && oldPointerCount == 1) {
1309                // Single-touch to multi-touch transition.
1310                // Send an up event for the last pointer.
1311                tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime, null);
1312            } else if (pointerCount == 1 && oldPointerCount == 1) {
1313                tracker.onTouchEvent(action, x, y, eventTime, null);
1314            } else {
1315                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1316                        + " (old " + oldPointerCount + ")");
1317            }
1318            return true;
1319        }
1320
1321        final PointerTrackerQueue queue = mPointerQueue;
1322        if (action == MotionEvent.ACTION_MOVE) {
1323            for (int i = 0; i < pointerCount; i++) {
1324                final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
1325                tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime, queue);
1326            }
1327        } else {
1328            final PointerTracker tracker = getPointerTracker(id);
1329            switch (action) {
1330            case MotionEvent.ACTION_DOWN:
1331            case MotionEvent.ACTION_POINTER_DOWN:
1332                tracker.onDownEvent(x, y, eventTime, queue);
1333                break;
1334            case MotionEvent.ACTION_UP:
1335            case MotionEvent.ACTION_POINTER_UP:
1336                tracker.onUpEvent(x, y, eventTime, queue);
1337                break;
1338            case MotionEvent.ACTION_CANCEL:
1339                tracker.onCancelEvent(x, y, eventTime, queue);
1340                break;
1341            }
1342        }
1343
1344        return true;
1345    }
1346
1347    protected void onSwipeDown() {
1348        mKeyboardActionListener.onSwipeDown();
1349    }
1350
1351    public void closing() {
1352        mPreviewPopup.dismiss();
1353        mHandler.cancelAllMessages();
1354
1355        dismissPopupKeyboard();
1356        mDirtyRect.union(0, 0, getWidth(), getHeight());
1357        mMiniKeyboardCache.clear();
1358        requestLayout();
1359    }
1360
1361    public void purgeKeyboardAndClosing() {
1362        mKeyboard = null;
1363        closing();
1364    }
1365
1366    @Override
1367    public void onDetachedFromWindow() {
1368        super.onDetachedFromWindow();
1369        closing();
1370    }
1371
1372    private void dismissPopupKeyboard() {
1373        if (mMiniKeyboardPopup.isShowing()) {
1374            mMiniKeyboardPopup.dismiss();
1375            mMiniKeyboardView = null;
1376            mMiniKeyboardOriginX = 0;
1377            mMiniKeyboardOriginY = 0;
1378            invalidateAllKeys();
1379        }
1380    }
1381
1382    public boolean handleBack() {
1383        if (mMiniKeyboardPopup.isShowing()) {
1384            dismissPopupKeyboard();
1385            return true;
1386        }
1387        return false;
1388    }
1389}
1390