Home | History | Annotate | Download | only in latin
      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 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageManager;
     21 import android.content.res.Resources;
     22 import android.content.res.TypedArray;
     23 import android.graphics.Bitmap;
     24 import android.graphics.Canvas;
     25 import android.graphics.Paint;
     26 import android.graphics.Paint.Align;
     27 import android.graphics.PorterDuff;
     28 import android.graphics.Rect;
     29 import android.graphics.Region.Op;
     30 import android.graphics.Typeface;
     31 import android.graphics.drawable.Drawable;
     32 import android.inputmethodservice.Keyboard;
     33 import android.inputmethodservice.Keyboard.Key;
     34 import android.os.Handler;
     35 import android.os.Message;
     36 import android.os.SystemClock;
     37 import android.util.AttributeSet;
     38 import android.util.Log;
     39 import android.util.TypedValue;
     40 import android.view.GestureDetector;
     41 import android.view.Gravity;
     42 import android.view.LayoutInflater;
     43 import android.view.MotionEvent;
     44 import android.view.View;
     45 import android.view.ViewGroup.LayoutParams;
     46 import android.widget.PopupWindow;
     47 import android.widget.TextView;
     48 
     49 import java.util.ArrayList;
     50 import java.util.HashMap;
     51 import java.util.LinkedList;
     52 import java.util.List;
     53 import java.util.Locale;
     54 import java.util.WeakHashMap;
     55 
     56 /**
     57  * A view that renders a virtual {@link LatinKeyboard}. It handles rendering of keys and
     58  * detecting key presses and touch movements.
     59  *
     60  * TODO: References to LatinKeyboard in this class should be replaced with ones to its base class.
     61  *
     62  * @attr ref R.styleable#LatinKeyboardBaseView_keyBackground
     63  * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewLayout
     64  * @attr ref R.styleable#LatinKeyboardBaseView_keyPreviewOffset
     65  * @attr ref R.styleable#LatinKeyboardBaseView_labelTextSize
     66  * @attr ref R.styleable#LatinKeyboardBaseView_keyTextSize
     67  * @attr ref R.styleable#LatinKeyboardBaseView_keyTextColor
     68  * @attr ref R.styleable#LatinKeyboardBaseView_verticalCorrection
     69  * @attr ref R.styleable#LatinKeyboardBaseView_popupLayout
     70  */
     71 public class LatinKeyboardBaseView extends View implements PointerTracker.UIProxy {
     72     private static final String TAG = "LatinKeyboardBaseView";
     73     private static final boolean DEBUG = false;
     74 
     75     public static final int NOT_A_TOUCH_COORDINATE = -1;
     76 
     77     public interface OnKeyboardActionListener {
     78 
     79         /**
     80          * Called when the user presses a key. This is sent before the
     81          * {@link #onKey} is called. For keys that repeat, this is only
     82          * called once.
     83          *
     84          * @param primaryCode
     85          *            the unicode of the key being pressed. If the touch is
     86          *            not on a valid key, the value will be zero.
     87          */
     88         void onPress(int primaryCode);
     89 
     90         /**
     91          * Called when the user releases a key. This is sent after the
     92          * {@link #onKey} is called. For keys that repeat, this is only
     93          * called once.
     94          *
     95          * @param primaryCode
     96          *            the code of the key that was released
     97          */
     98         void onRelease(int primaryCode);
     99 
    100         /**
    101          * Send a key press to the listener.
    102          *
    103          * @param primaryCode
    104          *            this is the key that was pressed
    105          * @param keyCodes
    106          *            the codes for all the possible alternative keys with
    107          *            the primary code being the first. If the primary key
    108          *            code is a single character such as an alphabet or
    109          *            number or symbol, the alternatives will include other
    110          *            characters that may be on the same key or adjacent
    111          *            keys. These codes are useful to correct for
    112          *            accidental presses of a key adjacent to the intended
    113          *            key.
    114          * @param x
    115          *            x-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
    116          *            the value should be NOT_A_TOUCH_COORDINATE.
    117          * @param y
    118          *            y-coordinate pixel of touched event. If onKey is not called by onTouchEvent,
    119          *            the value should be NOT_A_TOUCH_COORDINATE.
    120          */
    121         void onKey(int primaryCode, int[] keyCodes, int x, int y);
    122 
    123         /**
    124          * Sends a sequence of characters to the listener.
    125          *
    126          * @param text
    127          *            the sequence of characters to be displayed.
    128          */
    129         void onText(CharSequence text);
    130 
    131         /**
    132          * Called when user released a finger outside any key.
    133          */
    134         void onCancel();
    135 
    136         /**
    137          * Called when the user quickly moves the finger from right to
    138          * left.
    139          */
    140         void swipeLeft();
    141 
    142         /**
    143          * Called when the user quickly moves the finger from left to
    144          * right.
    145          */
    146         void swipeRight();
    147 
    148         /**
    149          * Called when the user quickly moves the finger from up to down.
    150          */
    151         void swipeDown();
    152 
    153         /**
    154          * Called when the user quickly moves the finger from down to up.
    155          */
    156         void swipeUp();
    157     }
    158 
    159     // Timing constants
    160     private final int mKeyRepeatInterval;
    161 
    162     // Miscellaneous constants
    163     /* package */ static final int NOT_A_KEY = -1;
    164     private static final int[] LONG_PRESSABLE_STATE_SET = { android.R.attr.state_long_pressable };
    165     private static final int NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL = -1;
    166 
    167     // XML attribute
    168     private int mKeyTextSize;
    169     private int mKeyTextColor;
    170     private Typeface mKeyTextStyle = Typeface.DEFAULT;
    171     private int mLabelTextSize;
    172     private int mSymbolColorScheme = 0;
    173     private int mShadowColor;
    174     private float mShadowRadius;
    175     private Drawable mKeyBackground;
    176     private float mBackgroundDimAmount;
    177     private float mKeyHysteresisDistance;
    178     private float mVerticalCorrection;
    179     private int mPreviewOffset;
    180     private int mPreviewHeight;
    181     private int mPopupLayout;
    182 
    183     // Main keyboard
    184     private Keyboard mKeyboard;
    185     private Key[] mKeys;
    186     // TODO this attribute should be gotten from Keyboard.
    187     private int mKeyboardVerticalGap;
    188 
    189     // Key preview popup
    190     private TextView mPreviewText;
    191     private PopupWindow mPreviewPopup;
    192     private int mPreviewTextSizeLarge;
    193     private int[] mOffsetInWindow;
    194     private int mOldPreviewKeyIndex = NOT_A_KEY;
    195     private boolean mShowPreview = true;
    196     private boolean mShowTouchPoints = true;
    197     private int mPopupPreviewOffsetX;
    198     private int mPopupPreviewOffsetY;
    199     private int mWindowY;
    200     private int mPopupPreviewDisplayedY;
    201     private final int mDelayBeforePreview;
    202     private final int mDelayAfterPreview;
    203 
    204     // Popup mini keyboard
    205     private PopupWindow mMiniKeyboardPopup;
    206     private LatinKeyboardBaseView mMiniKeyboard;
    207     private View mMiniKeyboardParent;
    208     private final WeakHashMap<Key, View> mMiniKeyboardCache = new WeakHashMap<Key, View>();
    209     private int mMiniKeyboardOriginX;
    210     private int mMiniKeyboardOriginY;
    211     private long mMiniKeyboardPopupTime;
    212     private int[] mWindowOffset;
    213     private final float mMiniKeyboardSlideAllowance;
    214     private int mMiniKeyboardTrackerId;
    215 
    216     /** Listener for {@link OnKeyboardActionListener}. */
    217     private OnKeyboardActionListener mKeyboardActionListener;
    218 
    219     private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
    220 
    221     // TODO: Let the PointerTracker class manage this pointer queue
    222     private final PointerQueue mPointerQueue = new PointerQueue();
    223 
    224     private final boolean mHasDistinctMultitouch;
    225     private int mOldPointerCount = 1;
    226 
    227     protected KeyDetector mKeyDetector = new ProximityKeyDetector();
    228 
    229     // Swipe gesture detector
    230     private GestureDetector mGestureDetector;
    231     private final SwipeTracker mSwipeTracker = new SwipeTracker();
    232     private final int mSwipeThreshold;
    233     private final boolean mDisambiguateSwipe;
    234 
    235     // Drawing
    236     /** Whether the keyboard bitmap needs to be redrawn before it's blitted. **/
    237     private boolean mDrawPending;
    238     /** The dirty region in the keyboard bitmap */
    239     private final Rect mDirtyRect = new Rect();
    240     /** The keyboard bitmap for faster updates */
    241     private Bitmap mBuffer;
    242     /** Notes if the keyboard just changed, so that we could possibly reallocate the mBuffer. */
    243     private boolean mKeyboardChanged;
    244     private Key mInvalidatedKey;
    245     /** The canvas for the above mutable keyboard bitmap */
    246     private Canvas mCanvas;
    247     private final Paint mPaint;
    248     private final Rect mPadding;
    249     private final Rect mClipRegion = new Rect(0, 0, 0, 0);
    250     // This map caches key label text height in pixel as value and key label text size as map key.
    251     private final HashMap<Integer, Integer> mTextHeightCache = new HashMap<Integer, Integer>();
    252     // Distance from horizontal center of the key, proportional to key label text height.
    253     private final float KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR = 0.55f;
    254     private final String KEY_LABEL_HEIGHT_REFERENCE_CHAR = "H";
    255 
    256     private final UIHandler mHandler = new UIHandler();
    257 
    258     class UIHandler extends Handler {
    259         private static final int MSG_POPUP_PREVIEW = 1;
    260         private static final int MSG_DISMISS_PREVIEW = 2;
    261         private static final int MSG_REPEAT_KEY = 3;
    262         private static final int MSG_LONGPRESS_KEY = 4;
    263 
    264         private boolean mInKeyRepeat;
    265 
    266         @Override
    267         public void handleMessage(Message msg) {
    268             switch (msg.what) {
    269                 case MSG_POPUP_PREVIEW:
    270                     showKey(msg.arg1, (PointerTracker)msg.obj);
    271                     break;
    272                 case MSG_DISMISS_PREVIEW:
    273                     mPreviewPopup.dismiss();
    274                     break;
    275                 case MSG_REPEAT_KEY: {
    276                     final PointerTracker tracker = (PointerTracker)msg.obj;
    277                     tracker.repeatKey(msg.arg1);
    278                     startKeyRepeatTimer(mKeyRepeatInterval, msg.arg1, tracker);
    279                     break;
    280                 }
    281                 case MSG_LONGPRESS_KEY: {
    282                     final PointerTracker tracker = (PointerTracker)msg.obj;
    283                     openPopupIfRequired(msg.arg1, tracker);
    284                     break;
    285                 }
    286             }
    287         }
    288 
    289         public void popupPreview(long delay, int keyIndex, PointerTracker tracker) {
    290             removeMessages(MSG_POPUP_PREVIEW);
    291             if (mPreviewPopup.isShowing() && mPreviewText.getVisibility() == VISIBLE) {
    292                 // Show right away, if it's already visible and finger is moving around
    293                 showKey(keyIndex, tracker);
    294             } else {
    295                 sendMessageDelayed(obtainMessage(MSG_POPUP_PREVIEW, keyIndex, 0, tracker),
    296                         delay);
    297             }
    298         }
    299 
    300         public void cancelPopupPreview() {
    301             removeMessages(MSG_POPUP_PREVIEW);
    302         }
    303 
    304         public void dismissPreview(long delay) {
    305             if (mPreviewPopup.isShowing()) {
    306                 sendMessageDelayed(obtainMessage(MSG_DISMISS_PREVIEW), delay);
    307             }
    308         }
    309 
    310         public void cancelDismissPreview() {
    311             removeMessages(MSG_DISMISS_PREVIEW);
    312         }
    313 
    314         public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
    315             mInKeyRepeat = true;
    316             sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
    317         }
    318 
    319         public void cancelKeyRepeatTimer() {
    320             mInKeyRepeat = false;
    321             removeMessages(MSG_REPEAT_KEY);
    322         }
    323 
    324         public boolean isInKeyRepeat() {
    325             return mInKeyRepeat;
    326         }
    327 
    328         public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
    329             removeMessages(MSG_LONGPRESS_KEY);
    330             sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
    331         }
    332 
    333         public void cancelLongPressTimer() {
    334             removeMessages(MSG_LONGPRESS_KEY);
    335         }
    336 
    337         public void cancelKeyTimers() {
    338             cancelKeyRepeatTimer();
    339             cancelLongPressTimer();
    340         }
    341 
    342         public void cancelAllMessages() {
    343             cancelKeyTimers();
    344             cancelPopupPreview();
    345             cancelDismissPreview();
    346         }
    347     }
    348 
    349     static class PointerQueue {
    350         private LinkedList<PointerTracker> mQueue = new LinkedList<PointerTracker>();
    351 
    352         public void add(PointerTracker tracker) {
    353             mQueue.add(tracker);
    354         }
    355 
    356         public int lastIndexOf(PointerTracker tracker) {
    357             LinkedList<PointerTracker> queue = mQueue;
    358             for (int index = queue.size() - 1; index >= 0; index--) {
    359                 PointerTracker t = queue.get(index);
    360                 if (t == tracker)
    361                     return index;
    362             }
    363             return -1;
    364         }
    365 
    366         public void releaseAllPointersOlderThan(PointerTracker tracker, long eventTime) {
    367             LinkedList<PointerTracker> queue = mQueue;
    368             int oldestPos = 0;
    369             for (PointerTracker t = queue.get(oldestPos); t != tracker; t = queue.get(oldestPos)) {
    370                 if (t.isModifier()) {
    371                     oldestPos++;
    372                 } else {
    373                     t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
    374                     t.setAlreadyProcessed();
    375                     queue.remove(oldestPos);
    376                 }
    377             }
    378         }
    379 
    380         public void releaseAllPointersExcept(PointerTracker tracker, long eventTime) {
    381             for (PointerTracker t : mQueue) {
    382                 if (t == tracker)
    383                     continue;
    384                 t.onUpEvent(t.getLastX(), t.getLastY(), eventTime);
    385                 t.setAlreadyProcessed();
    386             }
    387             mQueue.clear();
    388             if (tracker != null)
    389                 mQueue.add(tracker);
    390         }
    391 
    392         public void remove(PointerTracker tracker) {
    393             mQueue.remove(tracker);
    394         }
    395 
    396         public boolean isInSlidingKeyInput() {
    397             for (final PointerTracker tracker : mQueue) {
    398                 if (tracker.isInSlidingKeyInput())
    399                     return true;
    400             }
    401             return false;
    402         }
    403     }
    404 
    405     public LatinKeyboardBaseView(Context context, AttributeSet attrs) {
    406         this(context, attrs, R.attr.keyboardViewStyle);
    407     }
    408 
    409     public LatinKeyboardBaseView(Context context, AttributeSet attrs, int defStyle) {
    410         super(context, attrs, defStyle);
    411 
    412         TypedArray a = context.obtainStyledAttributes(
    413                 attrs, R.styleable.LatinKeyboardBaseView, defStyle, R.style.LatinKeyboardBaseView);
    414         LayoutInflater inflate =
    415                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    416         int previewLayout = 0;
    417         int keyTextSize = 0;
    418 
    419         int n = a.getIndexCount();
    420 
    421         for (int i = 0; i < n; i++) {
    422             int attr = a.getIndex(i);
    423 
    424             switch (attr) {
    425             case R.styleable.LatinKeyboardBaseView_keyBackground:
    426                 mKeyBackground = a.getDrawable(attr);
    427                 break;
    428             case R.styleable.LatinKeyboardBaseView_keyHysteresisDistance:
    429                 mKeyHysteresisDistance = a.getDimensionPixelOffset(attr, 0);
    430                 break;
    431             case R.styleable.LatinKeyboardBaseView_verticalCorrection:
    432                 mVerticalCorrection = a.getDimensionPixelOffset(attr, 0);
    433                 break;
    434             case R.styleable.LatinKeyboardBaseView_keyPreviewLayout:
    435                 previewLayout = a.getResourceId(attr, 0);
    436                 break;
    437             case R.styleable.LatinKeyboardBaseView_keyPreviewOffset:
    438                 mPreviewOffset = a.getDimensionPixelOffset(attr, 0);
    439                 break;
    440             case R.styleable.LatinKeyboardBaseView_keyPreviewHeight:
    441                 mPreviewHeight = a.getDimensionPixelSize(attr, 80);
    442                 break;
    443             case R.styleable.LatinKeyboardBaseView_keyTextSize:
    444                 mKeyTextSize = a.getDimensionPixelSize(attr, 18);
    445                 break;
    446             case R.styleable.LatinKeyboardBaseView_keyTextColor:
    447                 mKeyTextColor = a.getColor(attr, 0xFF000000);
    448                 break;
    449             case R.styleable.LatinKeyboardBaseView_labelTextSize:
    450                 mLabelTextSize = a.getDimensionPixelSize(attr, 14);
    451                 break;
    452             case R.styleable.LatinKeyboardBaseView_popupLayout:
    453                 mPopupLayout = a.getResourceId(attr, 0);
    454                 break;
    455             case R.styleable.LatinKeyboardBaseView_shadowColor:
    456                 mShadowColor = a.getColor(attr, 0);
    457                 break;
    458             case R.styleable.LatinKeyboardBaseView_shadowRadius:
    459                 mShadowRadius = a.getFloat(attr, 0f);
    460                 break;
    461             // TODO: Use Theme (android.R.styleable.Theme_backgroundDimAmount)
    462             case R.styleable.LatinKeyboardBaseView_backgroundDimAmount:
    463                 mBackgroundDimAmount = a.getFloat(attr, 0.5f);
    464                 break;
    465             //case android.R.styleable.
    466             case R.styleable.LatinKeyboardBaseView_keyTextStyle:
    467                 int textStyle = a.getInt(attr, 0);
    468                 switch (textStyle) {
    469                     case 0:
    470                         mKeyTextStyle = Typeface.DEFAULT;
    471                         break;
    472                     case 1:
    473                         mKeyTextStyle = Typeface.DEFAULT_BOLD;
    474                         break;
    475                     default:
    476                         mKeyTextStyle = Typeface.defaultFromStyle(textStyle);
    477                         break;
    478                 }
    479                 break;
    480             case R.styleable.LatinKeyboardBaseView_symbolColorScheme:
    481                 mSymbolColorScheme = a.getInt(attr, 0);
    482                 break;
    483             }
    484         }
    485 
    486         final Resources res = getResources();
    487 
    488         mPreviewPopup = new PopupWindow(context);
    489         if (previewLayout != 0) {
    490             mPreviewText = (TextView) inflate.inflate(previewLayout, null);
    491             mPreviewTextSizeLarge = (int) res.getDimension(R.dimen.key_preview_text_size_large);
    492             mPreviewPopup.setContentView(mPreviewText);
    493             mPreviewPopup.setBackgroundDrawable(null);
    494         } else {
    495             mShowPreview = false;
    496         }
    497         mPreviewPopup.setTouchable(false);
    498         mPreviewPopup.setAnimationStyle(R.style.KeyPreviewAnimation);
    499         mDelayBeforePreview = res.getInteger(R.integer.config_delay_before_preview);
    500         mDelayAfterPreview = res.getInteger(R.integer.config_delay_after_preview);
    501 
    502         mMiniKeyboardParent = this;
    503         mMiniKeyboardPopup = new PopupWindow(context);
    504         mMiniKeyboardPopup.setBackgroundDrawable(null);
    505         mMiniKeyboardPopup.setAnimationStyle(R.style.MiniKeyboardAnimation);
    506 
    507         mPaint = new Paint();
    508         mPaint.setAntiAlias(true);
    509         mPaint.setTextSize(keyTextSize);
    510         mPaint.setTextAlign(Align.CENTER);
    511         mPaint.setAlpha(255);
    512 
    513         mPadding = new Rect(0, 0, 0, 0);
    514         mKeyBackground.getPadding(mPadding);
    515 
    516         mSwipeThreshold = (int) (500 * res.getDisplayMetrics().density);
    517         // TODO: Refer frameworks/base/core/res/res/values/config.xml
    518         mDisambiguateSwipe = res.getBoolean(R.bool.config_swipeDisambiguation);
    519         mMiniKeyboardSlideAllowance = res.getDimension(R.dimen.mini_keyboard_slide_allowance);
    520 
    521         GestureDetector.SimpleOnGestureListener listener =
    522                 new GestureDetector.SimpleOnGestureListener() {
    523             @Override
    524             public boolean onFling(MotionEvent me1, MotionEvent me2, float velocityX,
    525                     float velocityY) {
    526                 final float absX = Math.abs(velocityX);
    527                 final float absY = Math.abs(velocityY);
    528                 float deltaX = me2.getX() - me1.getX();
    529                 float deltaY = me2.getY() - me1.getY();
    530                 int travelX = getWidth() / 2; // Half the keyboard width
    531                 int travelY = getHeight() / 2; // Half the keyboard height
    532                 mSwipeTracker.computeCurrentVelocity(1000);
    533                 final float endingVelocityX = mSwipeTracker.getXVelocity();
    534                 final float endingVelocityY = mSwipeTracker.getYVelocity();
    535                 if (velocityX > mSwipeThreshold && absY < absX && deltaX > travelX) {
    536                     if (mDisambiguateSwipe && endingVelocityX >= velocityX / 4) {
    537                         swipeRight();
    538                         return true;
    539                     }
    540                 } else if (velocityX < -mSwipeThreshold && absY < absX && deltaX < -travelX) {
    541                     if (mDisambiguateSwipe && endingVelocityX <= velocityX / 4) {
    542                         swipeLeft();
    543                         return true;
    544                     }
    545                 } else if (velocityY < -mSwipeThreshold && absX < absY && deltaY < -travelY) {
    546                     if (mDisambiguateSwipe && endingVelocityY <= velocityY / 4) {
    547                         swipeUp();
    548                         return true;
    549                     }
    550                 } else if (velocityY > mSwipeThreshold && absX < absY / 2 && deltaY > travelY) {
    551                     if (mDisambiguateSwipe && endingVelocityY >= velocityY / 4) {
    552                         swipeDown();
    553                         return true;
    554                     }
    555                 }
    556                 return false;
    557             }
    558         };
    559 
    560         final boolean ignoreMultitouch = true;
    561         mGestureDetector = new GestureDetector(getContext(), listener, null, ignoreMultitouch);
    562         mGestureDetector.setIsLongpressEnabled(false);
    563 
    564         mHasDistinctMultitouch = context.getPackageManager()
    565                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
    566         mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
    567     }
    568 
    569     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
    570         mKeyboardActionListener = listener;
    571         for (PointerTracker tracker : mPointerTrackers) {
    572             tracker.setOnKeyboardActionListener(listener);
    573         }
    574     }
    575 
    576     /**
    577      * Returns the {@link OnKeyboardActionListener} object.
    578      * @return the listener attached to this keyboard
    579      */
    580     protected OnKeyboardActionListener getOnKeyboardActionListener() {
    581         return mKeyboardActionListener;
    582     }
    583 
    584     /**
    585      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
    586      * view will re-layout itself to accommodate the keyboard.
    587      * @see Keyboard
    588      * @see #getKeyboard()
    589      * @param keyboard the keyboard to display in this view
    590      */
    591     public void setKeyboard(Keyboard keyboard) {
    592         if (mKeyboard != null) {
    593             dismissKeyPreview();
    594         }
    595         // Remove any pending messages, except dismissing preview
    596         mHandler.cancelKeyTimers();
    597         mHandler.cancelPopupPreview();
    598         mKeyboard = keyboard;
    599         LatinImeLogger.onSetKeyboard(keyboard);
    600         mKeys = mKeyDetector.setKeyboard(keyboard, -getPaddingLeft(),
    601                 -getPaddingTop() + mVerticalCorrection);
    602         mKeyboardVerticalGap = (int)getResources().getDimension(R.dimen.key_bottom_gap);
    603         for (PointerTracker tracker : mPointerTrackers) {
    604             tracker.setKeyboard(mKeys, mKeyHysteresisDistance);
    605         }
    606         requestLayout();
    607         // Hint to reallocate the buffer if the size changed
    608         mKeyboardChanged = true;
    609         invalidateAllKeys();
    610         computeProximityThreshold(keyboard);
    611         mMiniKeyboardCache.clear();
    612     }
    613 
    614     /**
    615      * Returns the current keyboard being displayed by this view.
    616      * @return the currently attached keyboard
    617      * @see #setKeyboard(Keyboard)
    618      */
    619     public Keyboard getKeyboard() {
    620         return mKeyboard;
    621     }
    622 
    623     /**
    624      * Return whether the device has distinct multi-touch panel.
    625      * @return true if the device has distinct multi-touch panel.
    626      */
    627     public boolean hasDistinctMultitouch() {
    628         return mHasDistinctMultitouch;
    629     }
    630 
    631     /**
    632      * Sets the state of the shift key of the keyboard, if any.
    633      * @param shifted whether or not to enable the state of the shift key
    634      * @return true if the shift key state changed, false if there was no change
    635      */
    636     public boolean setShifted(boolean shifted) {
    637         if (mKeyboard != null) {
    638             if (mKeyboard.setShifted(shifted)) {
    639                 // The whole keyboard probably needs to be redrawn
    640                 invalidateAllKeys();
    641                 return true;
    642             }
    643         }
    644         return false;
    645     }
    646 
    647     /**
    648      * Returns the state of the shift key of the keyboard, if any.
    649      * @return true if the shift is in a pressed state, false otherwise. If there is
    650      * no shift key on the keyboard or there is no keyboard attached, it returns false.
    651      */
    652     public boolean isShifted() {
    653         if (mKeyboard != null) {
    654             return mKeyboard.isShifted();
    655         }
    656         return false;
    657     }
    658 
    659     /**
    660      * Enables or disables the key feedback popup. This is a popup that shows a magnified
    661      * version of the depressed key. By default the preview is enabled.
    662      * @param previewEnabled whether or not to enable the key feedback popup
    663      * @see #isPreviewEnabled()
    664      */
    665     public void setPreviewEnabled(boolean previewEnabled) {
    666         mShowPreview = previewEnabled;
    667     }
    668 
    669     /**
    670      * Returns the enabled state of the key feedback popup.
    671      * @return whether or not the key feedback popup is enabled
    672      * @see #setPreviewEnabled(boolean)
    673      */
    674     public boolean isPreviewEnabled() {
    675         return mShowPreview;
    676     }
    677 
    678     public int getSymbolColorScheme() {
    679         return mSymbolColorScheme;
    680     }
    681 
    682     public void setPopupParent(View v) {
    683         mMiniKeyboardParent = v;
    684     }
    685 
    686     public void setPopupOffset(int x, int y) {
    687         mPopupPreviewOffsetX = x;
    688         mPopupPreviewOffsetY = y;
    689         mPreviewPopup.dismiss();
    690     }
    691 
    692     /**
    693      * When enabled, calls to {@link OnKeyboardActionListener#onKey} will include key
    694      * codes for adjacent keys.  When disabled, only the primary key code will be
    695      * reported.
    696      * @param enabled whether or not the proximity correction is enabled
    697      */
    698     public void setProximityCorrectionEnabled(boolean enabled) {
    699         mKeyDetector.setProximityCorrectionEnabled(enabled);
    700     }
    701 
    702     /**
    703      * Returns true if proximity correction is enabled.
    704      */
    705     public boolean isProximityCorrectionEnabled() {
    706         return mKeyDetector.isProximityCorrectionEnabled();
    707     }
    708 
    709     protected Locale getKeyboardLocale() {
    710         if (mKeyboard instanceof LatinKeyboard) {
    711             return ((LatinKeyboard)mKeyboard).getInputLocale();
    712         } else {
    713             return getContext().getResources().getConfiguration().locale;
    714         }
    715     }
    716 
    717     protected CharSequence adjustCase(CharSequence label) {
    718         if (mKeyboard.isShifted() && label != null && label.length() < 3
    719                 && Character.isLowerCase(label.charAt(0))) {
    720             return label.toString().toUpperCase(getKeyboardLocale());
    721         }
    722         return label;
    723     }
    724 
    725     @Override
    726     public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    727         // Round up a little
    728         if (mKeyboard == null) {
    729             setMeasuredDimension(
    730                     getPaddingLeft() + getPaddingRight(), getPaddingTop() + getPaddingBottom());
    731         } else {
    732             int width = mKeyboard.getMinWidth() + getPaddingLeft() + getPaddingRight();
    733             if (MeasureSpec.getSize(widthMeasureSpec) < width + 10) {
    734                 width = MeasureSpec.getSize(widthMeasureSpec);
    735             }
    736             setMeasuredDimension(
    737                     width, mKeyboard.getHeight() + getPaddingTop() + getPaddingBottom());
    738         }
    739     }
    740 
    741     /**
    742      * Compute the average distance between adjacent keys (horizontally and vertically)
    743      * and square it to get the proximity threshold. We use a square here and in computing
    744      * the touch distance from a key's center to avoid taking a square root.
    745      * @param keyboard
    746      */
    747     private void computeProximityThreshold(Keyboard keyboard) {
    748         if (keyboard == null) return;
    749         final Key[] keys = mKeys;
    750         if (keys == null) return;
    751         int length = keys.length;
    752         int dimensionSum = 0;
    753         for (int i = 0; i < length; i++) {
    754             Key key = keys[i];
    755             dimensionSum += Math.min(key.width, key.height + mKeyboardVerticalGap) + key.gap;
    756         }
    757         if (dimensionSum < 0 || length == 0) return;
    758         mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length));
    759     }
    760 
    761     @Override
    762     public void onSizeChanged(int w, int h, int oldw, int oldh) {
    763         super.onSizeChanged(w, h, oldw, oldh);
    764         // Release the buffer, if any and it will be reallocated on the next draw
    765         mBuffer = null;
    766     }
    767 
    768     @Override
    769     public void onDraw(Canvas canvas) {
    770         super.onDraw(canvas);
    771         if (mDrawPending || mBuffer == null || mKeyboardChanged) {
    772             onBufferDraw();
    773         }
    774         canvas.drawBitmap(mBuffer, 0, 0, null);
    775     }
    776 
    777     private void onBufferDraw() {
    778         if (mBuffer == null || mKeyboardChanged) {
    779             if (mBuffer == null || mKeyboardChanged &&
    780                     (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) {
    781                 // Make sure our bitmap is at least 1x1
    782                 final int width = Math.max(1, getWidth());
    783                 final int height = Math.max(1, getHeight());
    784                 mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    785                 mCanvas = new Canvas(mBuffer);
    786             }
    787             invalidateAllKeys();
    788             mKeyboardChanged = false;
    789         }
    790         final Canvas canvas = mCanvas;
    791         canvas.clipRect(mDirtyRect, Op.REPLACE);
    792 
    793         if (mKeyboard == null) return;
    794 
    795         final Paint paint = mPaint;
    796         final Drawable keyBackground = mKeyBackground;
    797         final Rect clipRegion = mClipRegion;
    798         final Rect padding = mPadding;
    799         final int kbdPaddingLeft = getPaddingLeft();
    800         final int kbdPaddingTop = getPaddingTop();
    801         final Key[] keys = mKeys;
    802         final Key invalidKey = mInvalidatedKey;
    803 
    804         paint.setColor(mKeyTextColor);
    805         boolean drawSingleKey = false;
    806         if (invalidKey != null && canvas.getClipBounds(clipRegion)) {
    807             // TODO we should use Rect.inset and Rect.contains here.
    808             // Is clipRegion completely contained within the invalidated key?
    809             if (invalidKey.x + kbdPaddingLeft - 1 <= clipRegion.left &&
    810                     invalidKey.y + kbdPaddingTop - 1 <= clipRegion.top &&
    811                     invalidKey.x + invalidKey.width + kbdPaddingLeft + 1 >= clipRegion.right &&
    812                     invalidKey.y + invalidKey.height + kbdPaddingTop + 1 >= clipRegion.bottom) {
    813                 drawSingleKey = true;
    814             }
    815         }
    816         canvas.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
    817         final int keyCount = keys.length;
    818         for (int i = 0; i < keyCount; i++) {
    819             final Key key = keys[i];
    820             if (drawSingleKey && invalidKey != key) {
    821                 continue;
    822             }
    823             int[] drawableState = key.getCurrentDrawableState();
    824             keyBackground.setState(drawableState);
    825 
    826             // Switch the character to uppercase if shift is pressed
    827             String label = key.label == null? null : adjustCase(key.label).toString();
    828 
    829             final Rect bounds = keyBackground.getBounds();
    830             if (key.width != bounds.right || key.height != bounds.bottom) {
    831                 keyBackground.setBounds(0, 0, key.width, key.height);
    832             }
    833             canvas.translate(key.x + kbdPaddingLeft, key.y + kbdPaddingTop);
    834             keyBackground.draw(canvas);
    835 
    836             boolean shouldDrawIcon = true;
    837             if (label != null) {
    838                 // For characters, use large font. For labels like "Done", use small font.
    839                 final int labelSize;
    840                 if (label.length() > 1 && key.codes.length < 2) {
    841                     labelSize = mLabelTextSize;
    842                     paint.setTypeface(Typeface.DEFAULT_BOLD);
    843                 } else {
    844                     labelSize = mKeyTextSize;
    845                     paint.setTypeface(mKeyTextStyle);
    846                 }
    847                 paint.setTextSize(labelSize);
    848 
    849                 Integer labelHeightValue = mTextHeightCache.get(labelSize);
    850                 final int labelHeight;
    851                 if (labelHeightValue != null) {
    852                     labelHeight = labelHeightValue;
    853                 } else {
    854                     Rect textBounds = new Rect();
    855                     paint.getTextBounds(KEY_LABEL_HEIGHT_REFERENCE_CHAR, 0, 1, textBounds);
    856                     labelHeight = textBounds.height();
    857                     mTextHeightCache.put(labelSize, labelHeight);
    858                 }
    859 
    860                 // Draw a drop shadow for the text
    861                 paint.setShadowLayer(mShadowRadius, 0, 0, mShadowColor);
    862                 final int centerX = (key.width + padding.left - padding.right) / 2;
    863                 final int centerY = (key.height + padding.top - padding.bottom) / 2;
    864                 final float baseline = centerY
    865                         + labelHeight * KEY_LABEL_VERTICAL_ADJUSTMENT_FACTOR;
    866                 canvas.drawText(label, centerX, baseline, paint);
    867                 // Turn off drop shadow
    868                 paint.setShadowLayer(0, 0, 0, 0);
    869 
    870                 // Usually don't draw icon if label is not null, but we draw icon for the number
    871                 // hint and popup hint.
    872                 shouldDrawIcon = shouldDrawLabelAndIcon(key);
    873             }
    874             if (key.icon != null && shouldDrawIcon) {
    875                 // Special handing for the upper-right number hint icons
    876                 final int drawableWidth;
    877                 final int drawableHeight;
    878                 final int drawableX;
    879                 final int drawableY;
    880                 if (shouldDrawIconFully(key)) {
    881                     drawableWidth = key.width;
    882                     drawableHeight = key.height;
    883                     drawableX = 0;
    884                     drawableY = NUMBER_HINT_VERTICAL_ADJUSTMENT_PIXEL;
    885                 } else {
    886                     drawableWidth = key.icon.getIntrinsicWidth();
    887                     drawableHeight = key.icon.getIntrinsicHeight();
    888                     drawableX = (key.width + padding.left - padding.right - drawableWidth) / 2;
    889                     drawableY = (key.height + padding.top - padding.bottom - drawableHeight) / 2;
    890                 }
    891                 canvas.translate(drawableX, drawableY);
    892                 key.icon.setBounds(0, 0, drawableWidth, drawableHeight);
    893                 key.icon.draw(canvas);
    894                 canvas.translate(-drawableX, -drawableY);
    895             }
    896             canvas.translate(-key.x - kbdPaddingLeft, -key.y - kbdPaddingTop);
    897         }
    898         mInvalidatedKey = null;
    899         // Overlay a dark rectangle to dim the keyboard
    900         if (mMiniKeyboard != null) {
    901             paint.setColor((int) (mBackgroundDimAmount * 0xFF) << 24);
    902             canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
    903         }
    904 
    905         if (DEBUG) {
    906             if (mShowTouchPoints) {
    907                 for (PointerTracker tracker : mPointerTrackers) {
    908                     int startX = tracker.getStartX();
    909                     int startY = tracker.getStartY();
    910                     int lastX = tracker.getLastX();
    911                     int lastY = tracker.getLastY();
    912                     paint.setAlpha(128);
    913                     paint.setColor(0xFFFF0000);
    914                     canvas.drawCircle(startX, startY, 3, paint);
    915                     canvas.drawLine(startX, startY, lastX, lastY, paint);
    916                     paint.setColor(0xFF0000FF);
    917                     canvas.drawCircle(lastX, lastY, 3, paint);
    918                     paint.setColor(0xFF00FF00);
    919                     canvas.drawCircle((startX + lastX) / 2, (startY + lastY) / 2, 2, paint);
    920                 }
    921             }
    922         }
    923 
    924         mDrawPending = false;
    925         mDirtyRect.setEmpty();
    926     }
    927 
    928     // TODO: clean up this method.
    929     private void dismissKeyPreview() {
    930         for (PointerTracker tracker : mPointerTrackers)
    931             tracker.updateKey(NOT_A_KEY);
    932         showPreview(NOT_A_KEY, null);
    933     }
    934 
    935     public void showPreview(int keyIndex, PointerTracker tracker) {
    936         int oldKeyIndex = mOldPreviewKeyIndex;
    937         mOldPreviewKeyIndex = keyIndex;
    938         final boolean isLanguageSwitchEnabled = (mKeyboard instanceof LatinKeyboard)
    939                 && ((LatinKeyboard)mKeyboard).isLanguageSwitchEnabled();
    940         // We should re-draw popup preview when 1) we need to hide the preview, 2) we will show
    941         // the space key preview and 3) pointer moves off the space key to other letter key, we
    942         // should hide the preview of the previous key.
    943         final boolean hidePreviewOrShowSpaceKeyPreview = (tracker == null)
    944                 || tracker.isSpaceKey(keyIndex) || tracker.isSpaceKey(oldKeyIndex);
    945         // If key changed and preview is on or the key is space (language switch is enabled)
    946         if (oldKeyIndex != keyIndex
    947                 && (mShowPreview
    948                         || (hidePreviewOrShowSpaceKeyPreview && isLanguageSwitchEnabled))) {
    949             if (keyIndex == NOT_A_KEY) {
    950                 mHandler.cancelPopupPreview();
    951                 mHandler.dismissPreview(mDelayAfterPreview);
    952             } else if (tracker != null) {
    953                 mHandler.popupPreview(mDelayBeforePreview, keyIndex, tracker);
    954             }
    955         }
    956     }
    957 
    958     private void showKey(final int keyIndex, PointerTracker tracker) {
    959         Key key = tracker.getKey(keyIndex);
    960         if (key == null)
    961             return;
    962         // Should not draw hint icon in key preview
    963         if (key.icon != null && !shouldDrawLabelAndIcon(key)) {
    964             mPreviewText.setCompoundDrawables(null, null, null,
    965                     key.iconPreview != null ? key.iconPreview : key.icon);
    966             mPreviewText.setText(null);
    967         } else {
    968             mPreviewText.setCompoundDrawables(null, null, null, null);
    969             mPreviewText.setText(adjustCase(tracker.getPreviewText(key)));
    970             if (key.label.length() > 1 && key.codes.length < 2) {
    971                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize);
    972                 mPreviewText.setTypeface(Typeface.DEFAULT_BOLD);
    973             } else {
    974                 mPreviewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge);
    975                 mPreviewText.setTypeface(mKeyTextStyle);
    976             }
    977         }
    978         mPreviewText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
    979                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
    980         int popupWidth = Math.max(mPreviewText.getMeasuredWidth(), key.width
    981                 + mPreviewText.getPaddingLeft() + mPreviewText.getPaddingRight());
    982         final int popupHeight = mPreviewHeight;
    983         LayoutParams lp = mPreviewText.getLayoutParams();
    984         if (lp != null) {
    985             lp.width = popupWidth;
    986             lp.height = popupHeight;
    987         }
    988 
    989         int popupPreviewX = key.x - (popupWidth - key.width) / 2;
    990         int popupPreviewY = key.y - popupHeight + mPreviewOffset;
    991 
    992         mHandler.cancelDismissPreview();
    993         if (mOffsetInWindow == null) {
    994             mOffsetInWindow = new int[2];
    995             getLocationInWindow(mOffsetInWindow);
    996             mOffsetInWindow[0] += mPopupPreviewOffsetX; // Offset may be zero
    997             mOffsetInWindow[1] += mPopupPreviewOffsetY; // Offset may be zero
    998             int[] windowLocation = new int[2];
    999             getLocationOnScreen(windowLocation);
   1000             mWindowY = windowLocation[1];
   1001         }
   1002         // Set the preview background state
   1003         mPreviewText.getBackground().setState(
   1004                 key.popupResId != 0 ? LONG_PRESSABLE_STATE_SET : EMPTY_STATE_SET);
   1005         popupPreviewX += mOffsetInWindow[0];
   1006         popupPreviewY += mOffsetInWindow[1];
   1007 
   1008         // If the popup cannot be shown above the key, put it on the side
   1009         if (popupPreviewY + mWindowY < 0) {
   1010             // If the key you're pressing is on the left side of the keyboard, show the popup on
   1011             // the right, offset by enough to see at least one key to the left/right.
   1012             if (key.x + key.width <= getWidth() / 2) {
   1013                 popupPreviewX += (int) (key.width * 2.5);
   1014             } else {
   1015                 popupPreviewX -= (int) (key.width * 2.5);
   1016             }
   1017             popupPreviewY += popupHeight;
   1018         }
   1019 
   1020         if (mPreviewPopup.isShowing()) {
   1021             mPreviewPopup.update(popupPreviewX, popupPreviewY, popupWidth, popupHeight);
   1022         } else {
   1023             mPreviewPopup.setWidth(popupWidth);
   1024             mPreviewPopup.setHeight(popupHeight);
   1025             mPreviewPopup.showAtLocation(mMiniKeyboardParent, Gravity.NO_GRAVITY,
   1026                     popupPreviewX, popupPreviewY);
   1027         }
   1028         // Record popup preview position to display mini-keyboard later at the same positon
   1029         mPopupPreviewDisplayedY = popupPreviewY;
   1030         mPreviewText.setVisibility(VISIBLE);
   1031     }
   1032 
   1033     /**
   1034      * Requests a redraw of the entire keyboard. Calling {@link #invalidate} is not sufficient
   1035      * because the keyboard renders the keys to an off-screen buffer and an invalidate() only
   1036      * draws the cached buffer.
   1037      * @see #invalidateKey(Key)
   1038      */
   1039     public void invalidateAllKeys() {
   1040         mDirtyRect.union(0, 0, getWidth(), getHeight());
   1041         mDrawPending = true;
   1042         invalidate();
   1043     }
   1044 
   1045     /**
   1046      * Invalidates a key so that it will be redrawn on the next repaint. Use this method if only
   1047      * one key is changing it's content. Any changes that affect the position or size of the key
   1048      * may not be honored.
   1049      * @param key key in the attached {@link Keyboard}.
   1050      * @see #invalidateAllKeys
   1051      */
   1052     public void invalidateKey(Key key) {
   1053         if (key == null)
   1054             return;
   1055         mInvalidatedKey = key;
   1056         // TODO we should clean up this and record key's region to use in onBufferDraw.
   1057         mDirtyRect.union(key.x + getPaddingLeft(), key.y + getPaddingTop(),
   1058                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
   1059         onBufferDraw();
   1060         invalidate(key.x + getPaddingLeft(), key.y + getPaddingTop(),
   1061                 key.x + key.width + getPaddingLeft(), key.y + key.height + getPaddingTop());
   1062     }
   1063 
   1064     private boolean openPopupIfRequired(int keyIndex, PointerTracker tracker) {
   1065         // Check if we have a popup layout specified first.
   1066         if (mPopupLayout == 0) {
   1067             return false;
   1068         }
   1069 
   1070         Key popupKey = tracker.getKey(keyIndex);
   1071         if (popupKey == null)
   1072             return false;
   1073         boolean result = onLongPress(popupKey);
   1074         if (result) {
   1075             dismissKeyPreview();
   1076             mMiniKeyboardTrackerId = tracker.mPointerId;
   1077             // Mark this tracker "already processed" and remove it from the pointer queue
   1078             tracker.setAlreadyProcessed();
   1079             mPointerQueue.remove(tracker);
   1080         }
   1081         return result;
   1082     }
   1083 
   1084     private View inflateMiniKeyboardContainer(Key popupKey) {
   1085         int popupKeyboardId = popupKey.popupResId;
   1086         LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(
   1087                 Context.LAYOUT_INFLATER_SERVICE);
   1088         View container = inflater.inflate(mPopupLayout, null);
   1089         if (container == null)
   1090             throw new NullPointerException();
   1091 
   1092         LatinKeyboardBaseView miniKeyboard =
   1093                 (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
   1094         miniKeyboard.setOnKeyboardActionListener(new OnKeyboardActionListener() {
   1095             public void onKey(int primaryCode, int[] keyCodes, int x, int y) {
   1096                 mKeyboardActionListener.onKey(primaryCode, keyCodes, x, y);
   1097                 dismissPopupKeyboard();
   1098             }
   1099 
   1100             public void onText(CharSequence text) {
   1101                 mKeyboardActionListener.onText(text);
   1102                 dismissPopupKeyboard();
   1103             }
   1104 
   1105             public void onCancel() {
   1106                 mKeyboardActionListener.onCancel();
   1107                 dismissPopupKeyboard();
   1108             }
   1109 
   1110             public void swipeLeft() {
   1111             }
   1112             public void swipeRight() {
   1113             }
   1114             public void swipeUp() {
   1115             }
   1116             public void swipeDown() {
   1117             }
   1118             public void onPress(int primaryCode) {
   1119                 mKeyboardActionListener.onPress(primaryCode);
   1120             }
   1121             public void onRelease(int primaryCode) {
   1122                 mKeyboardActionListener.onRelease(primaryCode);
   1123             }
   1124         });
   1125         // Override default ProximityKeyDetector.
   1126         miniKeyboard.mKeyDetector = new MiniKeyboardKeyDetector(mMiniKeyboardSlideAllowance);
   1127         // Remove gesture detector on mini-keyboard
   1128         miniKeyboard.mGestureDetector = null;
   1129 
   1130         Keyboard keyboard;
   1131         if (popupKey.popupCharacters != null) {
   1132             keyboard = new Keyboard(getContext(), popupKeyboardId, popupKey.popupCharacters,
   1133                     -1, getPaddingLeft() + getPaddingRight());
   1134         } else {
   1135             keyboard = new Keyboard(getContext(), popupKeyboardId);
   1136         }
   1137         miniKeyboard.setKeyboard(keyboard);
   1138         miniKeyboard.setPopupParent(this);
   1139 
   1140         container.measure(MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST),
   1141                 MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.AT_MOST));
   1142 
   1143         return container;
   1144     }
   1145 
   1146     private static boolean isOneRowKeys(List<Key> keys) {
   1147         if (keys.size() == 0) return false;
   1148         final int edgeFlags = keys.get(0).edgeFlags;
   1149         // HACK: The first key of mini keyboard which was inflated from xml and has multiple rows,
   1150         // does not have both top and bottom edge flags on at the same time.  On the other hand,
   1151         // the first key of mini keyboard that was created with popupCharacters must have both top
   1152         // and bottom edge flags on.
   1153         // When you want to use one row mini-keyboard from xml file, make sure that the row has
   1154         // both top and bottom edge flags set.
   1155         return (edgeFlags & Keyboard.EDGE_TOP) != 0 && (edgeFlags & Keyboard.EDGE_BOTTOM) != 0;
   1156     }
   1157 
   1158     /**
   1159      * Called when a key is long pressed. By default this will open any popup keyboard associated
   1160      * with this key through the attributes popupLayout and popupCharacters.
   1161      * @param popupKey the key that was long pressed
   1162      * @return true if the long press is handled, false otherwise. Subclasses should call the
   1163      * method on the base class if the subclass doesn't wish to handle the call.
   1164      */
   1165     protected boolean onLongPress(Key popupKey) {
   1166         // TODO if popupKey.popupCharacters has only one letter, send it as key without opening
   1167         // mini keyboard.
   1168 
   1169         if (popupKey.popupResId == 0)
   1170             return false;
   1171 
   1172         View container = mMiniKeyboardCache.get(popupKey);
   1173         if (container == null) {
   1174             container = inflateMiniKeyboardContainer(popupKey);
   1175             mMiniKeyboardCache.put(popupKey, container);
   1176         }
   1177         mMiniKeyboard = (LatinKeyboardBaseView)container.findViewById(R.id.LatinKeyboardBaseView);
   1178         if (mWindowOffset == null) {
   1179             mWindowOffset = new int[2];
   1180             getLocationInWindow(mWindowOffset);
   1181         }
   1182 
   1183         // Get width of a key in the mini popup keyboard = "miniKeyWidth".
   1184         // On the other hand, "popupKey.width" is width of the pressed key on the main keyboard.
   1185         // We adjust the position of mini popup keyboard with the edge key in it:
   1186         //  a) When we have the leftmost key in popup keyboard directly above the pressed key
   1187         //     Right edges of both keys should be aligned for consistent default selection
   1188         //  b) When we have the rightmost key in popup keyboard directly above the pressed key
   1189         //     Left edges of both keys should be aligned for consistent default selection
   1190         final List<Key> miniKeys = mMiniKeyboard.getKeyboard().getKeys();
   1191         final int miniKeyWidth = miniKeys.size() > 0 ? miniKeys.get(0).width : 0;
   1192 
   1193         // HACK: Have the leftmost number in the popup characters right above the key
   1194         boolean isNumberAtLeftmost =
   1195                 hasMultiplePopupChars(popupKey) && isNumberAtLeftmostPopupChar(popupKey);
   1196         int popupX = popupKey.x + mWindowOffset[0];
   1197         popupX += getPaddingLeft();
   1198         if (isNumberAtLeftmost) {
   1199             popupX += popupKey.width - miniKeyWidth;  // adjustment for a) described above
   1200             popupX -= container.getPaddingLeft();
   1201         } else {
   1202             popupX += miniKeyWidth;  // adjustment for b) described above
   1203             popupX -= container.getMeasuredWidth();
   1204             popupX += container.getPaddingRight();
   1205         }
   1206         int popupY = popupKey.y + mWindowOffset[1];
   1207         popupY += getPaddingTop();
   1208         popupY -= container.getMeasuredHeight();
   1209         popupY += container.getPaddingBottom();
   1210         final int x = popupX;
   1211         final int y = mShowPreview && isOneRowKeys(miniKeys) ? mPopupPreviewDisplayedY : popupY;
   1212 
   1213         int adjustedX = x;
   1214         if (x < 0) {
   1215             adjustedX = 0;
   1216         } else if (x > (getMeasuredWidth() - container.getMeasuredWidth())) {
   1217             adjustedX = getMeasuredWidth() - container.getMeasuredWidth();
   1218         }
   1219         mMiniKeyboardOriginX = adjustedX + container.getPaddingLeft() - mWindowOffset[0];
   1220         mMiniKeyboardOriginY = y + container.getPaddingTop() - mWindowOffset[1];
   1221         mMiniKeyboard.setPopupOffset(adjustedX, y);
   1222         mMiniKeyboard.setShifted(isShifted());
   1223         // Mini keyboard needs no pop-up key preview displayed.
   1224         mMiniKeyboard.setPreviewEnabled(false);
   1225         mMiniKeyboardPopup.setContentView(container);
   1226         mMiniKeyboardPopup.setWidth(container.getMeasuredWidth());
   1227         mMiniKeyboardPopup.setHeight(container.getMeasuredHeight());
   1228         mMiniKeyboardPopup.showAtLocation(this, Gravity.NO_GRAVITY, x, y);
   1229 
   1230         // Inject down event on the key to mini keyboard.
   1231         long eventTime = SystemClock.uptimeMillis();
   1232         mMiniKeyboardPopupTime = eventTime;
   1233         MotionEvent downEvent = generateMiniKeyboardMotionEvent(MotionEvent.ACTION_DOWN, popupKey.x
   1234                 + popupKey.width / 2, popupKey.y + popupKey.height / 2, eventTime);
   1235         mMiniKeyboard.onTouchEvent(downEvent);
   1236         downEvent.recycle();
   1237 
   1238         invalidateAllKeys();
   1239         return true;
   1240     }
   1241 
   1242     private static boolean hasMultiplePopupChars(Key key) {
   1243         if (key.popupCharacters != null && key.popupCharacters.length() > 1) {
   1244             return true;
   1245         }
   1246         return false;
   1247     }
   1248 
   1249     private boolean shouldDrawIconFully(Key key) {
   1250         return isNumberAtEdgeOfPopupChars(key) || isLatinF1Key(key)
   1251                 || LatinKeyboard.hasPuncOrSmileysPopup(key);
   1252     }
   1253 
   1254     private boolean shouldDrawLabelAndIcon(Key key) {
   1255         return isNumberAtEdgeOfPopupChars(key) || isNonMicLatinF1Key(key)
   1256                 || LatinKeyboard.hasPuncOrSmileysPopup(key);
   1257     }
   1258 
   1259     private boolean isLatinF1Key(Key key) {
   1260         return (mKeyboard instanceof LatinKeyboard) && ((LatinKeyboard)mKeyboard).isF1Key(key);
   1261     }
   1262 
   1263     private boolean isNonMicLatinF1Key(Key key) {
   1264         return isLatinF1Key(key) && key.label != null;
   1265     }
   1266 
   1267     private static boolean isNumberAtEdgeOfPopupChars(Key key) {
   1268         return isNumberAtLeftmostPopupChar(key) || isNumberAtRightmostPopupChar(key);
   1269     }
   1270 
   1271     /* package */ static boolean isNumberAtLeftmostPopupChar(Key key) {
   1272         if (key.popupCharacters != null && key.popupCharacters.length() > 0
   1273                 && isAsciiDigit(key.popupCharacters.charAt(0))) {
   1274             return true;
   1275         }
   1276         return false;
   1277     }
   1278 
   1279     /* package */ static boolean isNumberAtRightmostPopupChar(Key key) {
   1280         if (key.popupCharacters != null && key.popupCharacters.length() > 0
   1281                 && isAsciiDigit(key.popupCharacters.charAt(key.popupCharacters.length() - 1))) {
   1282             return true;
   1283         }
   1284         return false;
   1285     }
   1286 
   1287     private static boolean isAsciiDigit(char c) {
   1288         return (c < 0x80) && Character.isDigit(c);
   1289     }
   1290 
   1291     private MotionEvent generateMiniKeyboardMotionEvent(int action, int x, int y, long eventTime) {
   1292         return MotionEvent.obtain(mMiniKeyboardPopupTime, eventTime, action,
   1293                     x - mMiniKeyboardOriginX, y - mMiniKeyboardOriginY, 0);
   1294     }
   1295 
   1296     private PointerTracker getPointerTracker(final int id) {
   1297         final ArrayList<PointerTracker> pointers = mPointerTrackers;
   1298         final Key[] keys = mKeys;
   1299         final OnKeyboardActionListener listener = mKeyboardActionListener;
   1300 
   1301         // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
   1302         for (int i = pointers.size(); i <= id; i++) {
   1303             final PointerTracker tracker =
   1304                 new PointerTracker(i, mHandler, mKeyDetector, this, getResources());
   1305             if (keys != null)
   1306                 tracker.setKeyboard(keys, mKeyHysteresisDistance);
   1307             if (listener != null)
   1308                 tracker.setOnKeyboardActionListener(listener);
   1309             pointers.add(tracker);
   1310         }
   1311 
   1312         return pointers.get(id);
   1313     }
   1314 
   1315     public boolean isInSlidingKeyInput() {
   1316         if (mMiniKeyboard != null) {
   1317             return mMiniKeyboard.isInSlidingKeyInput();
   1318         } else {
   1319             return mPointerQueue.isInSlidingKeyInput();
   1320         }
   1321     }
   1322 
   1323     public int getPointerCount() {
   1324         return mOldPointerCount;
   1325     }
   1326 
   1327     @Override
   1328     public boolean onTouchEvent(MotionEvent me) {
   1329         final int action = me.getActionMasked();
   1330         final int pointerCount = me.getPointerCount();
   1331         final int oldPointerCount = mOldPointerCount;
   1332         mOldPointerCount = pointerCount;
   1333 
   1334         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
   1335         // If the device does not have distinct multi-touch support panel, ignore all multi-touch
   1336         // events except a transition from/to single-touch.
   1337         if (!mHasDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
   1338             return true;
   1339         }
   1340 
   1341         // Track the last few movements to look for spurious swipes.
   1342         mSwipeTracker.addMovement(me);
   1343 
   1344         // Gesture detector must be enabled only when mini-keyboard is not on the screen.
   1345         if (mMiniKeyboard == null
   1346                 && mGestureDetector != null && mGestureDetector.onTouchEvent(me)) {
   1347             dismissKeyPreview();
   1348             mHandler.cancelKeyTimers();
   1349             return true;
   1350         }
   1351 
   1352         final long eventTime = me.getEventTime();
   1353         final int index = me.getActionIndex();
   1354         final int id = me.getPointerId(index);
   1355         final int x = (int)me.getX(index);
   1356         final int y = (int)me.getY(index);
   1357 
   1358         // Needs to be called after the gesture detector gets a turn, as it may have
   1359         // displayed the mini keyboard
   1360         if (mMiniKeyboard != null) {
   1361             final int miniKeyboardPointerIndex = me.findPointerIndex(mMiniKeyboardTrackerId);
   1362             if (miniKeyboardPointerIndex >= 0 && miniKeyboardPointerIndex < pointerCount) {
   1363                 final int miniKeyboardX = (int)me.getX(miniKeyboardPointerIndex);
   1364                 final int miniKeyboardY = (int)me.getY(miniKeyboardPointerIndex);
   1365                 MotionEvent translated = generateMiniKeyboardMotionEvent(action,
   1366                         miniKeyboardX, miniKeyboardY, eventTime);
   1367                 mMiniKeyboard.onTouchEvent(translated);
   1368                 translated.recycle();
   1369             }
   1370             return true;
   1371         }
   1372 
   1373         if (mHandler.isInKeyRepeat()) {
   1374             // It will keep being in the key repeating mode while the key is being pressed.
   1375             if (action == MotionEvent.ACTION_MOVE) {
   1376                 return true;
   1377             }
   1378             final PointerTracker tracker = getPointerTracker(id);
   1379             // Key repeating timer will be canceled if 2 or more keys are in action, and current
   1380             // event (UP or DOWN) is non-modifier key.
   1381             if (pointerCount > 1 && !tracker.isModifier()) {
   1382                 mHandler.cancelKeyRepeatTimer();
   1383             }
   1384             // Up event will pass through.
   1385         }
   1386 
   1387         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
   1388         // Translate mutli-touch event to single-touch events on the device that has no distinct
   1389         // multi-touch panel.
   1390         if (!mHasDistinctMultitouch) {
   1391             // Use only main (id=0) pointer tracker.
   1392             PointerTracker tracker = getPointerTracker(0);
   1393             if (pointerCount == 1 && oldPointerCount == 2) {
   1394                 // Multi-touch to single touch transition.
   1395                 // Send a down event for the latest pointer.
   1396                 tracker.onDownEvent(x, y, eventTime);
   1397             } else if (pointerCount == 2 && oldPointerCount == 1) {
   1398                 // Single-touch to multi-touch transition.
   1399                 // Send an up event for the last pointer.
   1400                 tracker.onUpEvent(tracker.getLastX(), tracker.getLastY(), eventTime);
   1401             } else if (pointerCount == 1 && oldPointerCount == 1) {
   1402                 tracker.onTouchEvent(action, x, y, eventTime);
   1403             } else {
   1404                 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
   1405                         + " (old " + oldPointerCount + ")");
   1406             }
   1407             return true;
   1408         }
   1409 
   1410         if (action == MotionEvent.ACTION_MOVE) {
   1411             for (int i = 0; i < pointerCount; i++) {
   1412                 PointerTracker tracker = getPointerTracker(me.getPointerId(i));
   1413                 tracker.onMoveEvent((int)me.getX(i), (int)me.getY(i), eventTime);
   1414             }
   1415         } else {
   1416             PointerTracker tracker = getPointerTracker(id);
   1417             switch (action) {
   1418             case MotionEvent.ACTION_DOWN:
   1419             case MotionEvent.ACTION_POINTER_DOWN:
   1420                 onDownEvent(tracker, x, y, eventTime);
   1421                 break;
   1422             case MotionEvent.ACTION_UP:
   1423             case MotionEvent.ACTION_POINTER_UP:
   1424                 onUpEvent(tracker, x, y, eventTime);
   1425                 break;
   1426             case MotionEvent.ACTION_CANCEL:
   1427                 onCancelEvent(tracker, x, y, eventTime);
   1428                 break;
   1429             }
   1430         }
   1431 
   1432         return true;
   1433     }
   1434 
   1435     private void onDownEvent(PointerTracker tracker, int x, int y, long eventTime) {
   1436         if (tracker.isOnModifierKey(x, y)) {
   1437             // Before processing a down event of modifier key, all pointers already being tracked
   1438             // should be released.
   1439             mPointerQueue.releaseAllPointersExcept(null, eventTime);
   1440         }
   1441         tracker.onDownEvent(x, y, eventTime);
   1442         mPointerQueue.add(tracker);
   1443     }
   1444 
   1445     private void onUpEvent(PointerTracker tracker, int x, int y, long eventTime) {
   1446         if (tracker.isModifier()) {
   1447             // Before processing an up event of modifier key, all pointers already being tracked
   1448             // should be released.
   1449             mPointerQueue.releaseAllPointersExcept(tracker, eventTime);
   1450         } else {
   1451             int index = mPointerQueue.lastIndexOf(tracker);
   1452             if (index >= 0) {
   1453                 mPointerQueue.releaseAllPointersOlderThan(tracker, eventTime);
   1454             } else {
   1455                 Log.w(TAG, "onUpEvent: corresponding down event not found for pointer "
   1456                         + tracker.mPointerId);
   1457             }
   1458         }
   1459         tracker.onUpEvent(x, y, eventTime);
   1460         mPointerQueue.remove(tracker);
   1461     }
   1462 
   1463     private void onCancelEvent(PointerTracker tracker, int x, int y, long eventTime) {
   1464         tracker.onCancelEvent(x, y, eventTime);
   1465         mPointerQueue.remove(tracker);
   1466     }
   1467 
   1468     protected void swipeRight() {
   1469         mKeyboardActionListener.swipeRight();
   1470     }
   1471 
   1472     protected void swipeLeft() {
   1473         mKeyboardActionListener.swipeLeft();
   1474     }
   1475 
   1476     protected void swipeUp() {
   1477         mKeyboardActionListener.swipeUp();
   1478     }
   1479 
   1480     protected void swipeDown() {
   1481         mKeyboardActionListener.swipeDown();
   1482     }
   1483 
   1484     public void closing() {
   1485         mPreviewPopup.dismiss();
   1486         mHandler.cancelAllMessages();
   1487 
   1488         dismissPopupKeyboard();
   1489         mBuffer = null;
   1490         mCanvas = null;
   1491         mMiniKeyboardCache.clear();
   1492     }
   1493 
   1494     @Override
   1495     public void onDetachedFromWindow() {
   1496         super.onDetachedFromWindow();
   1497         closing();
   1498     }
   1499 
   1500     private void dismissPopupKeyboard() {
   1501         if (mMiniKeyboardPopup.isShowing()) {
   1502             mMiniKeyboardPopup.dismiss();
   1503             mMiniKeyboard = null;
   1504             mMiniKeyboardOriginX = 0;
   1505             mMiniKeyboardOriginY = 0;
   1506             invalidateAllKeys();
   1507         }
   1508     }
   1509 
   1510     public boolean handleBack() {
   1511         if (mMiniKeyboardPopup.isShowing()) {
   1512             dismissPopupKeyboard();
   1513             return true;
   1514         }
   1515         return false;
   1516     }
   1517 }
   1518