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