Home | History | Annotate | Download | only in keyboard
      1 /*
      2  * Copyright (C) 2011 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.keyboard;
     18 
     19 import android.content.Context;
     20 import android.content.pm.PackageManager;
     21 import android.content.res.Resources;
     22 import android.graphics.Canvas;
     23 import android.os.Message;
     24 import android.os.SystemClock;
     25 import android.util.AttributeSet;
     26 import android.util.Log;
     27 import android.view.GestureDetector;
     28 import android.view.LayoutInflater;
     29 import android.view.MotionEvent;
     30 import android.view.View;
     31 import android.view.ViewConfiguration;
     32 import android.view.ViewGroup;
     33 import android.view.accessibility.AccessibilityEvent;
     34 import android.widget.PopupWindow;
     35 
     36 import com.android.inputmethod.accessibility.AccessibilityUtils;
     37 import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
     38 import com.android.inputmethod.deprecated.VoiceProxy;
     39 import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
     40 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
     41 import com.android.inputmethod.latin.LatinIME;
     42 import com.android.inputmethod.latin.R;
     43 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
     44 import com.android.inputmethod.latin.Utils;
     45 
     46 import java.util.WeakHashMap;
     47 
     48 /**
     49  * A view that is responsible for detecting key presses and touch movements.
     50  *
     51  * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
     52  * @attr ref R.styleable#KeyboardView_verticalCorrection
     53  * @attr ref R.styleable#KeyboardView_popupLayout
     54  */
     55 public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
     56         SuddenJumpingTouchEventHandler.ProcessMotionEvent {
     57     private static final String TAG = LatinKeyboardView.class.getSimpleName();
     58 
     59     private static final boolean ENABLE_CAPSLOCK_BY_DOUBLETAP = true;
     60 
     61     private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
     62 
     63     // Timing constants
     64     private final int mKeyRepeatInterval;
     65 
     66     // Mini keyboard
     67     private PopupWindow mMoreKeysWindow;
     68     private MoreKeysPanel mMoreKeysPanel;
     69     private int mMoreKeysPanelPointerTrackerId;
     70     private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
     71             new WeakHashMap<Key, MoreKeysPanel>();
     72 
     73     /** Listener for {@link KeyboardActionListener}. */
     74     private KeyboardActionListener mKeyboardActionListener;
     75 
     76     private final boolean mHasDistinctMultitouch;
     77     private int mOldPointerCount = 1;
     78     private int mOldKeyIndex;
     79 
     80     private final boolean mConfigShowMiniKeyboardAtTouchedPoint;
     81     protected KeyDetector mKeyDetector;
     82 
     83     // To detect double tap.
     84     protected GestureDetector mGestureDetector;
     85 
     86     private final KeyTimerHandler mKeyTimerHandler = new KeyTimerHandler(this);
     87 
     88     private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
     89             implements TimerProxy {
     90         private static final int MSG_REPEAT_KEY = 1;
     91         private static final int MSG_LONGPRESS_KEY = 2;
     92         private static final int MSG_IGNORE_DOUBLE_TAP = 3;
     93 
     94         private boolean mInKeyRepeat;
     95 
     96         public KeyTimerHandler(LatinKeyboardView outerInstance) {
     97             super(outerInstance);
     98         }
     99 
    100         @Override
    101         public void handleMessage(Message msg) {
    102             final LatinKeyboardView keyboardView = getOuterInstance();
    103             final PointerTracker tracker = (PointerTracker) msg.obj;
    104             switch (msg.what) {
    105             case MSG_REPEAT_KEY:
    106                 tracker.onRepeatKey(msg.arg1);
    107                 startKeyRepeatTimer(keyboardView.mKeyRepeatInterval, msg.arg1, tracker);
    108                 break;
    109             case MSG_LONGPRESS_KEY:
    110                 keyboardView.openMiniKeyboardIfRequired(msg.arg1, tracker);
    111                 break;
    112             }
    113         }
    114 
    115         @Override
    116         public void startKeyRepeatTimer(long delay, int keyIndex, PointerTracker tracker) {
    117             mInKeyRepeat = true;
    118             sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, keyIndex, 0, tracker), delay);
    119         }
    120 
    121         public void cancelKeyRepeatTimer() {
    122             mInKeyRepeat = false;
    123             removeMessages(MSG_REPEAT_KEY);
    124         }
    125 
    126         public boolean isInKeyRepeat() {
    127             return mInKeyRepeat;
    128         }
    129 
    130         @Override
    131         public void startLongPressTimer(long delay, int keyIndex, PointerTracker tracker) {
    132             cancelLongPressTimer();
    133             sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, keyIndex, 0, tracker), delay);
    134         }
    135 
    136         @Override
    137         public void cancelLongPressTimer() {
    138             removeMessages(MSG_LONGPRESS_KEY);
    139         }
    140 
    141         @Override
    142         public void cancelKeyTimers() {
    143             cancelKeyRepeatTimer();
    144             cancelLongPressTimer();
    145             removeMessages(MSG_IGNORE_DOUBLE_TAP);
    146         }
    147 
    148         public void startIgnoringDoubleTap() {
    149             sendMessageDelayed(obtainMessage(MSG_IGNORE_DOUBLE_TAP),
    150                     ViewConfiguration.getDoubleTapTimeout());
    151         }
    152 
    153         public boolean isIgnoringDoubleTap() {
    154             return hasMessages(MSG_IGNORE_DOUBLE_TAP);
    155         }
    156 
    157         public void cancelAllMessages() {
    158             cancelKeyTimers();
    159         }
    160     }
    161 
    162     private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener {
    163         private boolean mProcessingShiftDoubleTapEvent = false;
    164 
    165         @Override
    166         public boolean onDoubleTap(MotionEvent firstDown) {
    167             final Keyboard keyboard = getKeyboard();
    168             if (ENABLE_CAPSLOCK_BY_DOUBLETAP && keyboard instanceof LatinKeyboard
    169                     && ((LatinKeyboard) keyboard).isAlphaKeyboard()) {
    170                 final int pointerIndex = firstDown.getActionIndex();
    171                 final int id = firstDown.getPointerId(pointerIndex);
    172                 final PointerTracker tracker = getPointerTracker(id);
    173                 // If the first down event is on shift key.
    174                 if (tracker.isOnShiftKey((int) firstDown.getX(), (int) firstDown.getY())) {
    175                     mProcessingShiftDoubleTapEvent = true;
    176                     return true;
    177                 }
    178             }
    179             mProcessingShiftDoubleTapEvent = false;
    180             return false;
    181         }
    182 
    183         @Override
    184         public boolean onDoubleTapEvent(MotionEvent secondTap) {
    185             if (mProcessingShiftDoubleTapEvent
    186                     && secondTap.getAction() == MotionEvent.ACTION_DOWN) {
    187                 final MotionEvent secondDown = secondTap;
    188                 final int pointerIndex = secondDown.getActionIndex();
    189                 final int id = secondDown.getPointerId(pointerIndex);
    190                 final PointerTracker tracker = getPointerTracker(id);
    191                 // If the second down event is also on shift key.
    192                 if (tracker.isOnShiftKey((int) secondDown.getX(), (int) secondDown.getY())) {
    193                     // Detected a double tap on shift key. If we are in the ignoring double tap
    194                     // mode, it means we have already turned off caps lock in
    195                     // {@link KeyboardSwitcher#onReleaseShift} .
    196                     onDoubleTapShiftKey(tracker, mKeyTimerHandler.isIgnoringDoubleTap());
    197                     return true;
    198                 }
    199                 // Otherwise these events should not be handled as double tap.
    200                 mProcessingShiftDoubleTapEvent = false;
    201             }
    202             return mProcessingShiftDoubleTapEvent;
    203         }
    204     }
    205 
    206     public LatinKeyboardView(Context context, AttributeSet attrs) {
    207         this(context, attrs, R.attr.keyboardViewStyle);
    208     }
    209 
    210     public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
    211         super(context, attrs, defStyle);
    212 
    213         mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
    214 
    215         final Resources res = getResources();
    216         mConfigShowMiniKeyboardAtTouchedPoint = res.getBoolean(
    217                 R.bool.config_show_mini_keyboard_at_touched_point);
    218         final float keyHysteresisDistance = res.getDimension(R.dimen.key_hysteresis_distance);
    219         mKeyDetector = new KeyDetector(keyHysteresisDistance);
    220 
    221         final boolean ignoreMultitouch = true;
    222         mGestureDetector = new GestureDetector(
    223                 getContext(), new DoubleTapListener(), null, ignoreMultitouch);
    224         mGestureDetector.setIsLongpressEnabled(false);
    225 
    226         mHasDistinctMultitouch = context.getPackageManager()
    227                 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
    228         mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
    229 
    230         PointerTracker.init(mHasDistinctMultitouch, getContext());
    231     }
    232 
    233     public void startIgnoringDoubleTap() {
    234         if (ENABLE_CAPSLOCK_BY_DOUBLETAP)
    235             mKeyTimerHandler.startIgnoringDoubleTap();
    236     }
    237 
    238     public void setKeyboardActionListener(KeyboardActionListener listener) {
    239         mKeyboardActionListener = listener;
    240         PointerTracker.setKeyboardActionListener(listener);
    241     }
    242 
    243     /**
    244      * Returns the {@link KeyboardActionListener} object.
    245      * @return the listener attached to this keyboard
    246      */
    247     @Override
    248     public KeyboardActionListener getKeyboardActionListener() {
    249         return mKeyboardActionListener;
    250     }
    251 
    252     @Override
    253     public KeyDetector getKeyDetector() {
    254         return mKeyDetector;
    255     }
    256 
    257     @Override
    258     public DrawingProxy getDrawingProxy() {
    259         return this;
    260     }
    261 
    262     @Override
    263     public TimerProxy getTimerProxy() {
    264         return mKeyTimerHandler;
    265     }
    266 
    267     @Override
    268     public void setKeyPreviewPopupEnabled(boolean previewEnabled, int delay) {
    269         final Keyboard keyboard = getKeyboard();
    270         if (keyboard instanceof LatinKeyboard) {
    271             final LatinKeyboard latinKeyboard = (LatinKeyboard)keyboard;
    272             if (latinKeyboard.isPhoneKeyboard() || latinKeyboard.isNumberKeyboard()) {
    273                 // Phone and number keyboard never shows popup preview.
    274                 super.setKeyPreviewPopupEnabled(false, delay);
    275                 return;
    276             }
    277         }
    278         super.setKeyPreviewPopupEnabled(previewEnabled, delay);
    279     }
    280 
    281     /**
    282      * Attaches a keyboard to this view. The keyboard can be switched at any time and the
    283      * view will re-layout itself to accommodate the keyboard.
    284      * @see Keyboard
    285      * @see #getKeyboard()
    286      * @param keyboard the keyboard to display in this view
    287      */
    288     @Override
    289     public void setKeyboard(Keyboard keyboard) {
    290         // Remove any pending messages, except dismissing preview
    291         mKeyTimerHandler.cancelKeyTimers();
    292         super.setKeyboard(keyboard);
    293         mKeyDetector.setKeyboard(
    294                 keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
    295         mKeyDetector.setProximityThreshold(keyboard.mMostCommonKeyWidth);
    296         PointerTracker.setKeyDetector(mKeyDetector);
    297         mTouchScreenRegulator.setKeyboard(keyboard);
    298         mMoreKeysPanelCache.clear();
    299     }
    300 
    301     /**
    302      * Returns whether the device has distinct multi-touch panel.
    303      * @return true if the device has distinct multi-touch panel.
    304      */
    305     public boolean hasDistinctMultitouch() {
    306         return mHasDistinctMultitouch;
    307     }
    308 
    309     /**
    310      * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
    311      * codes for adjacent keys.  When disabled, only the primary key code will be
    312      * reported.
    313      * @param enabled whether or not the proximity correction is enabled
    314      */
    315     public void setProximityCorrectionEnabled(boolean enabled) {
    316         mKeyDetector.setProximityCorrectionEnabled(enabled);
    317     }
    318 
    319     /**
    320      * Returns true if proximity correction is enabled.
    321      */
    322     public boolean isProximityCorrectionEnabled() {
    323         return mKeyDetector.isProximityCorrectionEnabled();
    324     }
    325 
    326     @Override
    327     public void cancelAllMessages() {
    328         mKeyTimerHandler.cancelAllMessages();
    329         super.cancelAllMessages();
    330     }
    331 
    332     private boolean openMiniKeyboardIfRequired(int keyIndex, PointerTracker tracker) {
    333         // Check if we have a popup layout specified first.
    334         if (mMoreKeysLayout == 0) {
    335             return false;
    336         }
    337 
    338         // Check if we are already displaying popup panel.
    339         if (mMoreKeysPanel != null)
    340             return false;
    341         final Key parentKey = tracker.getKey(keyIndex);
    342         if (parentKey == null)
    343             return false;
    344         return onLongPress(parentKey, tracker);
    345     }
    346 
    347     private void onDoubleTapShiftKey(@SuppressWarnings("unused") PointerTracker tracker,
    348             final boolean ignore) {
    349         // When shift key is double tapped, the first tap is correctly processed as usual tap. And
    350         // the second tap is treated as this double tap event, so that we need not mark tracker
    351         // calling setAlreadyProcessed() nor remove the tracker from mPointerQueue.
    352         final int primaryCode = ignore ? Keyboard.CODE_HAPTIC_AND_AUDIO_FEEDBACK_ONLY
    353                 : Keyboard.CODE_CAPSLOCK;
    354         invokeCodeInput(primaryCode);
    355     }
    356 
    357     // This default implementation returns a more keys panel.
    358     protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
    359         if (parentKey.mMoreKeys == null)
    360             return null;
    361 
    362         final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
    363         if (container == null)
    364             throw new NullPointerException();
    365 
    366         final MiniKeyboardView miniKeyboardView =
    367                 (MiniKeyboardView)container.findViewById(R.id.mini_keyboard_view);
    368         final Keyboard parentKeyboard = getKeyboard();
    369         final Keyboard miniKeyboard = new MiniKeyboard.Builder(
    370                 this, parentKeyboard.mMoreKeysTemplate, parentKey, parentKeyboard).build();
    371         miniKeyboardView.setKeyboard(miniKeyboard);
    372         container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
    373 
    374         return miniKeyboardView;
    375     }
    376 
    377     public void setSpacebarTextFadeFactor(float fadeFactor, LatinKeyboard oldKeyboard) {
    378         final Keyboard keyboard = getKeyboard();
    379         // We should not set text fade factor to the keyboard which does not display the language on
    380         // its spacebar.
    381         if (keyboard instanceof LatinKeyboard && keyboard == oldKeyboard) {
    382             ((LatinKeyboard)keyboard).setSpacebarTextFadeFactor(fadeFactor, this);
    383         }
    384     }
    385 
    386     /**
    387      * Called when a key is long pressed. By default this will open mini keyboard associated
    388      * with this key.
    389      * @param parentKey the key that was long pressed
    390      * @param tracker the pointer tracker which pressed the parent key
    391      * @return true if the long press is handled, false otherwise. Subclasses should call the
    392      * method on the base class if the subclass doesn't wish to handle the call.
    393      */
    394     protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
    395         final int primaryCode = parentKey.mCode;
    396         final Keyboard keyboard = getKeyboard();
    397         if (keyboard instanceof LatinKeyboard) {
    398             final LatinKeyboard latinKeyboard = (LatinKeyboard) keyboard;
    399             if (primaryCode == Keyboard.CODE_DIGIT0 && latinKeyboard.isPhoneKeyboard()) {
    400                 tracker.onLongPressed();
    401                 // Long pressing on 0 in phone number keypad gives you a '+'.
    402                 invokeCodeInput(Keyboard.CODE_PLUS);
    403                 invokeReleaseKey(primaryCode);
    404                 return true;
    405             }
    406             if (primaryCode == Keyboard.CODE_SHIFT && latinKeyboard.isAlphaKeyboard()) {
    407                 tracker.onLongPressed();
    408                 invokeCodeInput(Keyboard.CODE_CAPSLOCK);
    409                 invokeReleaseKey(primaryCode);
    410                 return true;
    411             }
    412         }
    413         if (primaryCode == Keyboard.CODE_SETTINGS || primaryCode == Keyboard.CODE_SPACE) {
    414             // Both long pressing settings key and space key invoke IME switcher dialog.
    415             if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
    416                 tracker.onLongPressed();
    417                 invokeReleaseKey(primaryCode);
    418                 return true;
    419             } else {
    420                 return openMoreKeysPanel(parentKey, tracker);
    421             }
    422         } else {
    423             return openMoreKeysPanel(parentKey, tracker);
    424         }
    425     }
    426 
    427     private boolean invokeCustomRequest(int code) {
    428         return getKeyboardActionListener().onCustomRequest(code);
    429     }
    430 
    431     private void invokeCodeInput(int primaryCode) {
    432         getKeyboardActionListener().onCodeInput(primaryCode, null,
    433                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
    434                 KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
    435     }
    436 
    437     private void invokeReleaseKey(int primaryCode) {
    438         getKeyboardActionListener().onRelease(primaryCode, false);
    439     }
    440 
    441     private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
    442         MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
    443         if (moreKeysPanel == null) {
    444             moreKeysPanel = onCreateMoreKeysPanel(parentKey);
    445             if (moreKeysPanel == null)
    446                 return false;
    447             mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
    448         }
    449         if (mMoreKeysWindow == null) {
    450             mMoreKeysWindow = new PopupWindow(getContext());
    451             mMoreKeysWindow.setBackgroundDrawable(null);
    452             mMoreKeysWindow.setAnimationStyle(R.style.MiniKeyboardAnimation);
    453         }
    454         mMoreKeysPanel = moreKeysPanel;
    455         mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
    456 
    457         final Keyboard keyboard = getKeyboard();
    458         moreKeysPanel.setShifted(keyboard.isShiftedOrShiftLocked());
    459         final int pointX = (mConfigShowMiniKeyboardAtTouchedPoint) ? tracker.getLastX()
    460                 : parentKey.mX + parentKey.mWidth / 2;
    461         final int pointY = parentKey.mY - keyboard.mVerticalGap;
    462         moreKeysPanel.showMoreKeysPanel(
    463                 this, this, pointX, pointY, mMoreKeysWindow, getKeyboardActionListener());
    464         final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
    465         final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
    466         tracker.onShowMoreKeysPanel(
    467                 translatedX, translatedY, SystemClock.uptimeMillis(), moreKeysPanel);
    468         dimEntireKeyboard(true);
    469         return true;
    470     }
    471 
    472     private PointerTracker getPointerTracker(final int id) {
    473         return PointerTracker.getPointerTracker(id, this);
    474     }
    475 
    476     public boolean isInSlidingKeyInput() {
    477         if (mMoreKeysPanel != null) {
    478             return true;
    479         } else {
    480             return PointerTracker.isAnyInSlidingKeyInput();
    481         }
    482     }
    483 
    484     public int getPointerCount() {
    485         return mOldPointerCount;
    486     }
    487 
    488     @Override
    489     public boolean onTouchEvent(MotionEvent me) {
    490         if (getKeyboard() == null) {
    491             return false;
    492         }
    493         return mTouchScreenRegulator.onTouchEvent(me);
    494     }
    495 
    496     @Override
    497     public boolean processMotionEvent(MotionEvent me) {
    498         final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
    499         final int action = me.getActionMasked();
    500         final int pointerCount = me.getPointerCount();
    501         final int oldPointerCount = mOldPointerCount;
    502         mOldPointerCount = pointerCount;
    503 
    504         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
    505         // If the device does not have distinct multi-touch support panel, ignore all multi-touch
    506         // events except a transition from/to single-touch.
    507         if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
    508             return true;
    509         }
    510 
    511         // Gesture detector must be enabled only when mini-keyboard is not on the screen.
    512         if (mMoreKeysPanel == null && mGestureDetector != null
    513                 && mGestureDetector.onTouchEvent(me)) {
    514             PointerTracker.dismissAllKeyPreviews();
    515             mKeyTimerHandler.cancelKeyTimers();
    516             return true;
    517         }
    518 
    519         final long eventTime = me.getEventTime();
    520         final int index = me.getActionIndex();
    521         final int id = me.getPointerId(index);
    522         final int x, y;
    523         if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
    524             x = mMoreKeysPanel.translateX((int)me.getX(index));
    525             y = mMoreKeysPanel.translateY((int)me.getY(index));
    526         } else {
    527             x = (int)me.getX(index);
    528             y = (int)me.getY(index);
    529         }
    530 
    531         if (mKeyTimerHandler.isInKeyRepeat()) {
    532             final PointerTracker tracker = getPointerTracker(id);
    533             // Key repeating timer will be canceled if 2 or more keys are in action, and current
    534             // event (UP or DOWN) is non-modifier key.
    535             if (pointerCount > 1 && !tracker.isModifier()) {
    536                 mKeyTimerHandler.cancelKeyRepeatTimer();
    537             }
    538             // Up event will pass through.
    539         }
    540 
    541         // TODO: cleanup this code into a multi-touch to single-touch event converter class?
    542         // Translate mutli-touch event to single-touch events on the device that has no distinct
    543         // multi-touch panel.
    544         if (nonDistinctMultitouch) {
    545             // Use only main (id=0) pointer tracker.
    546             PointerTracker tracker = getPointerTracker(0);
    547             if (pointerCount == 1 && oldPointerCount == 2) {
    548                 // Multi-touch to single touch transition.
    549                 // Send a down event for the latest pointer if the key is different from the
    550                 // previous key.
    551                 final int newKeyIndex = tracker.getKeyIndexOn(x, y);
    552                 if (mOldKeyIndex != newKeyIndex) {
    553                     tracker.onDownEvent(x, y, eventTime, this);
    554                     if (action == MotionEvent.ACTION_UP)
    555                         tracker.onUpEvent(x, y, eventTime);
    556                 }
    557             } else if (pointerCount == 2 && oldPointerCount == 1) {
    558                 // Single-touch to multi-touch transition.
    559                 // Send an up event for the last pointer.
    560                 final int lastX = tracker.getLastX();
    561                 final int lastY = tracker.getLastY();
    562                 mOldKeyIndex = tracker.getKeyIndexOn(lastX, lastY);
    563                 tracker.onUpEvent(lastX, lastY, eventTime);
    564             } else if (pointerCount == 1 && oldPointerCount == 1) {
    565                 tracker.processMotionEvent(action, x, y, eventTime, this);
    566             } else {
    567                 Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
    568                         + " (old " + oldPointerCount + ")");
    569             }
    570             return true;
    571         }
    572 
    573         if (action == MotionEvent.ACTION_MOVE) {
    574             for (int i = 0; i < pointerCount; i++) {
    575                 final PointerTracker tracker = getPointerTracker(me.getPointerId(i));
    576                 final int px, py;
    577                 if (mMoreKeysPanel != null
    578                         && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
    579                     px = mMoreKeysPanel.translateX((int)me.getX(i));
    580                     py = mMoreKeysPanel.translateY((int)me.getY(i));
    581                 } else {
    582                     px = (int)me.getX(i);
    583                     py = (int)me.getY(i);
    584                 }
    585                 tracker.onMoveEvent(px, py, eventTime);
    586             }
    587         } else {
    588             getPointerTracker(id).processMotionEvent(action, x, y, eventTime, this);
    589         }
    590 
    591         return true;
    592     }
    593 
    594     @Override
    595     public void closing() {
    596         super.closing();
    597         dismissMoreKeysPanel();
    598         mMoreKeysPanelCache.clear();
    599     }
    600 
    601     @Override
    602     public boolean dismissMoreKeysPanel() {
    603         if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
    604             mMoreKeysWindow.dismiss();
    605             mMoreKeysPanel = null;
    606             mMoreKeysPanelPointerTrackerId = -1;
    607             dimEntireKeyboard(false);
    608             return true;
    609         }
    610         return false;
    611     }
    612 
    613     public boolean handleBack() {
    614         return dismissMoreKeysPanel();
    615     }
    616 
    617     @Override
    618     public void draw(Canvas c) {
    619         Utils.GCUtils.getInstance().reset();
    620         boolean tryGC = true;
    621         for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
    622             try {
    623                 super.draw(c);
    624                 tryGC = false;
    625             } catch (OutOfMemoryError e) {
    626                 tryGC = Utils.GCUtils.getInstance().tryGCOrWait("LatinKeyboardView", e);
    627             }
    628         }
    629     }
    630 
    631     @Override
    632     protected void onAttachedToWindow() {
    633         // Token is available from here.
    634         VoiceProxy.getInstance().onAttachedToWindow();
    635     }
    636 
    637     @Override
    638     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    639         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
    640             final PointerTracker tracker = getPointerTracker(0);
    641             return AccessibleKeyboardViewProxy.getInstance().dispatchPopulateAccessibilityEvent(
    642                     event, tracker) || super.dispatchPopulateAccessibilityEvent(event);
    643         }
    644 
    645         return super.dispatchPopulateAccessibilityEvent(event);
    646     }
    647 
    648     /**
    649      * Receives hover events from the input framework. This method overrides
    650      * View.dispatchHoverEvent(MotionEvent) on SDK version ICS or higher. On
    651      * lower SDK versions, this method is never called.
    652      *
    653      * @param event The motion event to be dispatched.
    654      * @return {@code true} if the event was handled by the view, {@code false}
    655      *         otherwise
    656      */
    657     public boolean dispatchHoverEvent(MotionEvent event) {
    658         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
    659             final PointerTracker tracker = getPointerTracker(0);
    660             return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
    661         }
    662 
    663         // Reflection doesn't support calling superclass methods.
    664         return false;
    665     }
    666 }
    667