MainKeyboardView.java revision 4daf32b6c0358f0273a99b622a259ecdf6b44fa4
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
17package com.android.inputmethod.keyboard;
18
19import android.animation.AnimatorInflater;
20import android.animation.ObjectAnimator;
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Paint.Align;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.os.Message;
31import android.text.TextUtils;
32import android.util.AttributeSet;
33import android.util.Log;
34import android.view.LayoutInflater;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.ViewConfiguration;
38import android.view.ViewGroup;
39import android.view.inputmethod.InputMethodSubtype;
40import android.widget.PopupWindow;
41
42import com.android.inputmethod.accessibility.AccessibilityUtils;
43import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
44import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
45import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
46import com.android.inputmethod.latin.Constants;
47import com.android.inputmethod.latin.LatinIME;
48import com.android.inputmethod.latin.LatinImeLogger;
49import com.android.inputmethod.latin.R;
50import com.android.inputmethod.latin.ResearchLogger;
51import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
52import com.android.inputmethod.latin.StringUtils;
53import com.android.inputmethod.latin.SubtypeLocale;
54import com.android.inputmethod.latin.Utils;
55import com.android.inputmethod.latin.Utils.UsabilityStudyLogUtils;
56import com.android.inputmethod.latin.define.ProductionFlag;
57
58import java.util.Locale;
59import java.util.WeakHashMap;
60
61/**
62 * A view that is responsible for detecting key presses and touch movements.
63 *
64 * @attr ref R.styleable#KeyboardView_keyHysteresisDistance
65 * @attr ref R.styleable#KeyboardView_verticalCorrection
66 * @attr ref R.styleable#KeyboardView_popupLayout
67 */
68public class LatinKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
69        SuddenJumpingTouchEventHandler.ProcessMotionEvent {
70    private static final String TAG = LatinKeyboardView.class.getSimpleName();
71
72    // TODO: Kill process when the usability study mode was changed.
73    private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
74
75    /** Listener for {@link KeyboardActionListener}. */
76    private KeyboardActionListener mKeyboardActionListener;
77
78    /* Space key and its icons */
79    private Key mSpaceKey;
80    private Drawable mSpaceIcon;
81    // Stuff to draw language name on spacebar.
82    private final int mLanguageOnSpacebarFinalAlpha;
83    private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
84    private boolean mNeedsToDisplayLanguage;
85    private boolean mHasMultipleEnabledIMEsOrSubtypes;
86    private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
87    private final float mSpacebarTextRatio;
88    private float mSpacebarTextSize;
89    private final int mSpacebarTextColor;
90    private final int mSpacebarTextShadowColor;
91    // The minimum x-scale to fit the language name on spacebar.
92    private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
93    // Stuff to draw auto correction LED on spacebar.
94    private boolean mAutoCorrectionSpacebarLedOn;
95    private final boolean mAutoCorrectionSpacebarLedEnabled;
96    private final Drawable mAutoCorrectionSpacebarLedIcon;
97    private static final int SPACE_LED_LENGTH_PERCENT = 80;
98
99    // Stuff to draw altCodeWhileTyping keys.
100    private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
101    private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
102    private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
103
104    // More keys keyboard
105    private PopupWindow mMoreKeysWindow;
106    private MoreKeysPanel mMoreKeysPanel;
107    private int mMoreKeysPanelPointerTrackerId;
108    private final WeakHashMap<Key, MoreKeysPanel> mMoreKeysPanelCache =
109            new WeakHashMap<Key, MoreKeysPanel>();
110    private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
111
112    private final PointerTrackerParams mPointerTrackerParams;
113    private final SuddenJumpingTouchEventHandler mTouchScreenRegulator;
114
115    protected KeyDetector mKeyDetector;
116    private boolean mHasDistinctMultitouch;
117    private int mOldPointerCount = 1;
118    private Key mOldKey;
119
120    private final KeyTimerHandler mKeyTimerHandler;
121
122    private static class KeyTimerHandler extends StaticInnerHandlerWrapper<LatinKeyboardView>
123            implements TimerProxy {
124        private static final int MSG_REPEAT_KEY = 1;
125        private static final int MSG_LONGPRESS_KEY = 2;
126        private static final int MSG_DOUBLE_TAP = 3;
127        private static final int MSG_TYPING_STATE_EXPIRED = 4;
128
129        private final KeyTimerParams mParams;
130
131        public KeyTimerHandler(LatinKeyboardView outerInstance, KeyTimerParams params) {
132            super(outerInstance);
133            mParams = params;
134        }
135
136        @Override
137        public void handleMessage(Message msg) {
138            final LatinKeyboardView keyboardView = getOuterInstance();
139            final PointerTracker tracker = (PointerTracker) msg.obj;
140            switch (msg.what) {
141            case MSG_REPEAT_KEY:
142                final Key currentKey = tracker.getKey();
143                if (currentKey != null && currentKey.mCode == msg.arg1) {
144                    tracker.onRegisterKey(currentKey);
145                    startKeyRepeatTimer(tracker, mParams.mKeyRepeatInterval);
146                }
147                break;
148            case MSG_LONGPRESS_KEY:
149                if (tracker != null) {
150                    keyboardView.openMoreKeysKeyboardIfRequired(tracker.getKey(), tracker);
151                } else {
152                    KeyboardSwitcher.getInstance().onLongPressTimeout(msg.arg1);
153                }
154                break;
155            case MSG_TYPING_STATE_EXPIRED:
156                cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
157                        keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
158                break;
159            }
160        }
161
162        private void startKeyRepeatTimer(PointerTracker tracker, long delay) {
163            final Key key = tracker.getKey();
164            if (key == null) return;
165            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
166        }
167
168        @Override
169        public void startKeyRepeatTimer(PointerTracker tracker) {
170            startKeyRepeatTimer(tracker, mParams.mKeyRepeatStartTimeout);
171        }
172
173        public void cancelKeyRepeatTimer() {
174            removeMessages(MSG_REPEAT_KEY);
175        }
176
177        // TODO: Suppress layout changes in key repeat mode
178        public boolean isInKeyRepeat() {
179            return hasMessages(MSG_REPEAT_KEY);
180        }
181
182        @Override
183        public void startLongPressTimer(int code) {
184            cancelLongPressTimer();
185            final int delay;
186            switch (code) {
187            case Keyboard.CODE_SHIFT:
188                delay = mParams.mLongPressShiftKeyTimeout;
189                break;
190            default:
191                delay = 0;
192                break;
193            }
194            if (delay > 0) {
195                sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, code, 0), delay);
196            }
197        }
198
199        @Override
200        public void startLongPressTimer(PointerTracker tracker) {
201            cancelLongPressTimer();
202            if (tracker == null) {
203                return;
204            }
205            final Key key = tracker.getKey();
206            final int delay;
207            switch (key.mCode) {
208            case Keyboard.CODE_SHIFT:
209                delay = mParams.mLongPressShiftKeyTimeout;
210                break;
211            default:
212                if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
213                    // We use longer timeout for sliding finger input started from the symbols
214                    // mode key.
215                    delay = mParams.mLongPressKeyTimeout * 3;
216                } else {
217                    delay = mParams.mLongPressKeyTimeout;
218                }
219                break;
220            }
221            if (delay > 0) {
222                sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
223            }
224        }
225
226        @Override
227        public void cancelLongPressTimer() {
228            removeMessages(MSG_LONGPRESS_KEY);
229        }
230
231        public static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
232                final ObjectAnimator animatorToStart) {
233            float startFraction = 0.0f;
234            if (animatorToCancel.isStarted()) {
235                animatorToCancel.cancel();
236                startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
237            }
238            final long startTime = (long)(animatorToStart.getDuration() * startFraction);
239            animatorToStart.start();
240            animatorToStart.setCurrentPlayTime(startTime);
241        }
242
243        @Override
244        public void startTypingStateTimer() {
245            final boolean isTyping = isTypingState();
246            removeMessages(MSG_TYPING_STATE_EXPIRED);
247            sendMessageDelayed(
248                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mParams.mIgnoreAltCodeKeyTimeout);
249            if (isTyping) {
250                return;
251            }
252            final LatinKeyboardView keyboardView = getOuterInstance();
253            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
254                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
255        }
256
257        @Override
258        public boolean isTypingState() {
259            return hasMessages(MSG_TYPING_STATE_EXPIRED);
260        }
261
262        @Override
263        public void startDoubleTapTimer() {
264            sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP),
265                    ViewConfiguration.getDoubleTapTimeout());
266        }
267
268        @Override
269        public void cancelDoubleTapTimer() {
270            removeMessages(MSG_DOUBLE_TAP);
271        }
272
273        @Override
274        public boolean isInDoubleTapTimeout() {
275            return hasMessages(MSG_DOUBLE_TAP);
276        }
277
278        @Override
279        public void cancelKeyTimers() {
280            cancelKeyRepeatTimer();
281            cancelLongPressTimer();
282        }
283
284        public void cancelAllMessages() {
285            cancelKeyTimers();
286        }
287    }
288
289    public static class PointerTrackerParams {
290        public final boolean mSlidingKeyInputEnabled;
291        public final int mTouchNoiseThresholdTime;
292        public final float mTouchNoiseThresholdDistance;
293
294        public static final PointerTrackerParams DEFAULT = new PointerTrackerParams();
295
296        private PointerTrackerParams() {
297            mSlidingKeyInputEnabled = false;
298            mTouchNoiseThresholdTime =0;
299            mTouchNoiseThresholdDistance = 0;
300        }
301
302        public PointerTrackerParams(TypedArray latinKeyboardViewAttr) {
303            mSlidingKeyInputEnabled = latinKeyboardViewAttr.getBoolean(
304                    R.styleable.LatinKeyboardView_slidingKeyInputEnable, false);
305            mTouchNoiseThresholdTime = latinKeyboardViewAttr.getInt(
306                    R.styleable.LatinKeyboardView_touchNoiseThresholdTime, 0);
307            mTouchNoiseThresholdDistance = latinKeyboardViewAttr.getDimension(
308                    R.styleable.LatinKeyboardView_touchNoiseThresholdDistance, 0);
309        }
310    }
311
312    static class KeyTimerParams {
313        public final int mKeyRepeatStartTimeout;
314        public final int mKeyRepeatInterval;
315        public final int mLongPressKeyTimeout;
316        public final int mLongPressShiftKeyTimeout;
317        public final int mIgnoreAltCodeKeyTimeout;
318
319        public KeyTimerParams(TypedArray latinKeyboardViewAttr) {
320            mKeyRepeatStartTimeout = latinKeyboardViewAttr.getInt(
321                    R.styleable.LatinKeyboardView_keyRepeatStartTimeout, 0);
322            mKeyRepeatInterval = latinKeyboardViewAttr.getInt(
323                    R.styleable.LatinKeyboardView_keyRepeatInterval, 0);
324            mLongPressKeyTimeout = latinKeyboardViewAttr.getInt(
325                    R.styleable.LatinKeyboardView_longPressKeyTimeout, 0);
326            mLongPressShiftKeyTimeout = latinKeyboardViewAttr.getInt(
327                    R.styleable.LatinKeyboardView_longPressShiftKeyTimeout, 0);
328            mIgnoreAltCodeKeyTimeout = latinKeyboardViewAttr.getInt(
329                    R.styleable.LatinKeyboardView_ignoreAltCodeKeyTimeout, 0);
330        }
331    }
332
333    public LatinKeyboardView(Context context, AttributeSet attrs) {
334        this(context, attrs, R.attr.latinKeyboardViewStyle);
335    }
336
337    public LatinKeyboardView(Context context, AttributeSet attrs, int defStyle) {
338        super(context, attrs, defStyle);
339
340        mTouchScreenRegulator = new SuddenJumpingTouchEventHandler(getContext(), this);
341
342        mHasDistinctMultitouch = context.getPackageManager()
343                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
344        final Resources res = getResources();
345        final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
346                Utils.getDeviceOverrideValue(res,
347                        R.array.phantom_sudden_move_event_device_list, "false"));
348        final boolean gestureInputEnabledByBuildConfig = res.getBoolean(
349                R.bool.config_gesture_input_enabled_by_build_config);
350        PointerTracker.init(mHasDistinctMultitouch, needsPhantomSuddenMoveEventHack,
351                gestureInputEnabledByBuildConfig);
352
353        final TypedArray a = context.obtainStyledAttributes(
354                attrs, R.styleable.LatinKeyboardView, defStyle, R.style.LatinKeyboardView);
355        mAutoCorrectionSpacebarLedEnabled = a.getBoolean(
356                R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedEnabled, false);
357        mAutoCorrectionSpacebarLedIcon = a.getDrawable(
358                R.styleable.LatinKeyboardView_autoCorrectionSpacebarLedIcon);
359        mSpacebarTextRatio = a.getFraction(R.styleable.LatinKeyboardView_spacebarTextRatio,
360                1000, 1000, 1) / 1000.0f;
361        mSpacebarTextColor = a.getColor(R.styleable.LatinKeyboardView_spacebarTextColor, 0);
362        mSpacebarTextShadowColor = a.getColor(
363                R.styleable.LatinKeyboardView_spacebarTextShadowColor, 0);
364        mLanguageOnSpacebarFinalAlpha = a.getInt(
365                R.styleable.LatinKeyboardView_languageOnSpacebarFinalAlpha,
366                Constants.Color.ALPHA_OPAQUE);
367        final int languageOnSpacebarFadeoutAnimatorResId = a.getResourceId(
368                R.styleable.LatinKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
369        final int altCodeKeyWhileTypingFadeoutAnimatorResId = a.getResourceId(
370                R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
371        final int altCodeKeyWhileTypingFadeinAnimatorResId = a.getResourceId(
372                R.styleable.LatinKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
373
374        final KeyTimerParams keyTimerParams = new KeyTimerParams(a);
375        mPointerTrackerParams = new PointerTrackerParams(a);
376
377        final float keyHysteresisDistance = a.getDimension(
378                R.styleable.LatinKeyboardView_keyHysteresisDistance, 0);
379        mKeyDetector = new KeyDetector(keyHysteresisDistance);
380        mKeyTimerHandler = new KeyTimerHandler(this, keyTimerParams);
381        mConfigShowMoreKeysKeyboardAtTouchedPoint = a.getBoolean(
382                R.styleable.LatinKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
383        a.recycle();
384
385        PointerTracker.setParameters(mPointerTrackerParams);
386
387        mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
388                languageOnSpacebarFadeoutAnimatorResId, this);
389        mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
390                altCodeKeyWhileTypingFadeoutAnimatorResId, this);
391        mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
392                altCodeKeyWhileTypingFadeinAnimatorResId, this);
393    }
394
395    private ObjectAnimator loadObjectAnimator(int resId, Object target) {
396        if (resId == 0) return null;
397        final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
398                getContext(), resId);
399        if (animator != null) {
400            animator.setTarget(target);
401        }
402        return animator;
403    }
404
405    // Getter/setter methods for {@link ObjectAnimator}.
406    public int getLanguageOnSpacebarAnimAlpha() {
407        return mLanguageOnSpacebarAnimAlpha;
408    }
409
410    public void setLanguageOnSpacebarAnimAlpha(int alpha) {
411        mLanguageOnSpacebarAnimAlpha = alpha;
412        invalidateKey(mSpaceKey);
413    }
414
415    public int getAltCodeKeyWhileTypingAnimAlpha() {
416        return mAltCodeKeyWhileTypingAnimAlpha;
417    }
418
419    public void setAltCodeKeyWhileTypingAnimAlpha(int alpha) {
420        mAltCodeKeyWhileTypingAnimAlpha = alpha;
421        updateAltCodeKeyWhileTyping();
422    }
423
424    public void setKeyboardActionListener(KeyboardActionListener listener) {
425        mKeyboardActionListener = listener;
426        PointerTracker.setKeyboardActionListener(listener);
427    }
428
429    /**
430     * Returns the {@link KeyboardActionListener} object.
431     * @return the listener attached to this keyboard
432     */
433    @Override
434    public KeyboardActionListener getKeyboardActionListener() {
435        return mKeyboardActionListener;
436    }
437
438    @Override
439    public KeyDetector getKeyDetector() {
440        return mKeyDetector;
441    }
442
443    @Override
444    public DrawingProxy getDrawingProxy() {
445        return this;
446    }
447
448    @Override
449    public TimerProxy getTimerProxy() {
450        return mKeyTimerHandler;
451    }
452
453    /**
454     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
455     * view will re-layout itself to accommodate the keyboard.
456     * @see Keyboard
457     * @see #getKeyboard()
458     * @param keyboard the keyboard to display in this view
459     */
460    @Override
461    public void setKeyboard(Keyboard keyboard) {
462        // Remove any pending messages, except dismissing preview and key repeat.
463        mKeyTimerHandler.cancelLongPressTimer();
464        super.setKeyboard(keyboard);
465        mKeyDetector.setKeyboard(
466                keyboard, -getPaddingLeft(), -getPaddingTop() + mVerticalCorrection);
467        PointerTracker.setKeyDetector(mKeyDetector);
468        mTouchScreenRegulator.setKeyboard(keyboard);
469        mMoreKeysPanelCache.clear();
470
471        mSpaceKey = keyboard.getKey(Keyboard.CODE_SPACE);
472        mSpaceIcon = (mSpaceKey != null)
473                ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
474        final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
475        mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
476        if (ProductionFlag.IS_EXPERIMENTAL) {
477            ResearchLogger.latinKeyboardView_setKeyboard(keyboard);
478        }
479
480        // This always needs to be set since the accessibility state can
481        // potentially change without the keyboard being set again.
482        AccessibleKeyboardViewProxy.getInstance().setKeyboard(keyboard);
483    }
484
485    /**
486     * Returns whether the device has distinct multi-touch panel.
487     * @return true if the device has distinct multi-touch panel.
488     */
489    public boolean hasDistinctMultitouch() {
490        return mHasDistinctMultitouch;
491    }
492
493    public void setDistinctMultitouch(boolean hasDistinctMultitouch) {
494        mHasDistinctMultitouch = hasDistinctMultitouch;
495    }
496
497    /**
498     * When enabled, calls to {@link KeyboardActionListener#onCodeInput} will include key
499     * codes for adjacent keys.  When disabled, only the primary key code will be
500     * reported.
501     * @param enabled whether or not the proximity correction is enabled
502     */
503    public void setProximityCorrectionEnabled(boolean enabled) {
504        mKeyDetector.setProximityCorrectionEnabled(enabled);
505    }
506
507    /**
508     * Returns true if proximity correction is enabled.
509     */
510    public boolean isProximityCorrectionEnabled() {
511        return mKeyDetector.isProximityCorrectionEnabled();
512    }
513
514    @Override
515    public void cancelAllMessages() {
516        mKeyTimerHandler.cancelAllMessages();
517        super.cancelAllMessages();
518    }
519
520    private boolean openMoreKeysKeyboardIfRequired(Key parentKey, PointerTracker tracker) {
521        // Check if we have a popup layout specified first.
522        if (mMoreKeysLayout == 0) {
523            return false;
524        }
525
526        // Check if we are already displaying popup panel.
527        if (mMoreKeysPanel != null)
528            return false;
529        if (parentKey == null)
530            return false;
531        return onLongPress(parentKey, tracker);
532    }
533
534    // This default implementation returns a more keys panel.
535    protected MoreKeysPanel onCreateMoreKeysPanel(Key parentKey) {
536        if (parentKey.mMoreKeys == null)
537            return null;
538
539        final View container = LayoutInflater.from(getContext()).inflate(mMoreKeysLayout, null);
540        if (container == null)
541            throw new NullPointerException();
542
543        final MoreKeysKeyboardView moreKeysKeyboardView =
544                (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
545        final Keyboard moreKeysKeyboard = new MoreKeysKeyboard.Builder(container, parentKey, this)
546                .build();
547        moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
548        container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
549
550        return moreKeysKeyboardView;
551    }
552
553    /**
554     * Called when a key is long pressed. By default this will open more keys keyboard associated
555     * with this key.
556     * @param parentKey the key that was long pressed
557     * @param tracker the pointer tracker which pressed the parent key
558     * @return true if the long press is handled, false otherwise. Subclasses should call the
559     * method on the base class if the subclass doesn't wish to handle the call.
560     */
561    protected boolean onLongPress(Key parentKey, PointerTracker tracker) {
562        if (ProductionFlag.IS_EXPERIMENTAL) {
563            ResearchLogger.latinKeyboardView_onLongPress();
564        }
565        final int primaryCode = parentKey.mCode;
566        if (parentKey.hasEmbeddedMoreKey()) {
567            final int embeddedCode = parentKey.mMoreKeys[0].mCode;
568            tracker.onLongPressed();
569            invokeCodeInput(embeddedCode);
570            invokeReleaseKey(primaryCode);
571            KeyboardSwitcher.getInstance().hapticAndAudioFeedback(primaryCode);
572            return true;
573        }
574        if (primaryCode == Keyboard.CODE_SPACE || primaryCode == Keyboard.CODE_LANGUAGE_SWITCH) {
575            // Long pressing the space key invokes IME switcher dialog.
576            if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
577                tracker.onLongPressed();
578                invokeReleaseKey(primaryCode);
579                return true;
580            }
581        }
582        return openMoreKeysPanel(parentKey, tracker);
583    }
584
585    private boolean invokeCustomRequest(int code) {
586        return mKeyboardActionListener.onCustomRequest(code);
587    }
588
589    private void invokeCodeInput(int primaryCode) {
590        mKeyboardActionListener.onCodeInput(primaryCode,
591                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
592                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
593    }
594
595    private void invokeReleaseKey(int primaryCode) {
596        mKeyboardActionListener.onReleaseKey(primaryCode, false);
597    }
598
599    private boolean openMoreKeysPanel(Key parentKey, PointerTracker tracker) {
600        MoreKeysPanel moreKeysPanel = mMoreKeysPanelCache.get(parentKey);
601        if (moreKeysPanel == null) {
602            moreKeysPanel = onCreateMoreKeysPanel(parentKey);
603            if (moreKeysPanel == null)
604                return false;
605            mMoreKeysPanelCache.put(parentKey, moreKeysPanel);
606        }
607        if (mMoreKeysWindow == null) {
608            mMoreKeysWindow = new PopupWindow(getContext());
609            mMoreKeysWindow.setBackgroundDrawable(null);
610            mMoreKeysWindow.setAnimationStyle(R.style.MoreKeysKeyboardAnimation);
611        }
612        mMoreKeysPanel = moreKeysPanel;
613        mMoreKeysPanelPointerTrackerId = tracker.mPointerId;
614
615        final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !parentKey.noKeyPreview();
616        // The more keys keyboard is usually horizontally aligned with the center of the parent key.
617        // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
618        // keys keyboard is placed at the touch point of the parent key.
619        final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
620                ? tracker.getLastX()
621                : parentKey.mX + parentKey.mWidth / 2;
622        // The more keys keyboard is usually vertically aligned with the top edge of the parent key
623        // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
624        // aligned with the bottom edge of the visible part of the key preview.
625        final int pointY = parentKey.mY + (keyPreviewEnabled
626                ? mKeyPreviewDrawParams.mPreviewVisibleOffset
627                : -parentKey.mVerticalGap);
628        moreKeysPanel.showMoreKeysPanel(
629                this, this, pointX, pointY, mMoreKeysWindow, mKeyboardActionListener);
630        final int translatedX = moreKeysPanel.translateX(tracker.getLastX());
631        final int translatedY = moreKeysPanel.translateY(tracker.getLastY());
632        tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
633        dimEntireKeyboard(true);
634        return true;
635    }
636
637    public boolean isInSlidingKeyInput() {
638        if (mMoreKeysPanel != null) {
639            return true;
640        } else {
641            return PointerTracker.isAnyInSlidingKeyInput();
642        }
643    }
644
645    public int getPointerCount() {
646        return mOldPointerCount;
647    }
648
649    @Override
650    public boolean onTouchEvent(MotionEvent me) {
651        if (getKeyboard() == null) {
652            return false;
653        }
654        return mTouchScreenRegulator.onTouchEvent(me);
655    }
656
657    @Override
658    public boolean processMotionEvent(MotionEvent me) {
659        final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
660        final int action = me.getActionMasked();
661        final int pointerCount = me.getPointerCount();
662        final int oldPointerCount = mOldPointerCount;
663        mOldPointerCount = pointerCount;
664
665        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
666        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
667        // events except a transition from/to single-touch.
668        if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
669            return true;
670        }
671
672        final long eventTime = me.getEventTime();
673        final int index = me.getActionIndex();
674        final int id = me.getPointerId(index);
675        final int x, y;
676        if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
677            x = mMoreKeysPanel.translateX((int)me.getX(index));
678            y = mMoreKeysPanel.translateY((int)me.getY(index));
679        } else {
680            x = (int)me.getX(index);
681            y = (int)me.getY(index);
682        }
683        if (ENABLE_USABILITY_STUDY_LOG) {
684            final String eventTag;
685            switch (action) {
686                case MotionEvent.ACTION_UP:
687                    eventTag = "[Up]";
688                    break;
689                case MotionEvent.ACTION_DOWN:
690                    eventTag = "[Down]";
691                    break;
692                case MotionEvent.ACTION_POINTER_UP:
693                    eventTag = "[PointerUp]";
694                    break;
695                case MotionEvent.ACTION_POINTER_DOWN:
696                    eventTag = "[PointerDown]";
697                    break;
698                case MotionEvent.ACTION_MOVE: // Skip this as being logged below
699                    eventTag = "";
700                    break;
701                default:
702                    eventTag = "[Action" + action + "]";
703                    break;
704            }
705            if (!TextUtils.isEmpty(eventTag)) {
706                final float size = me.getSize(index);
707                final float pressure = me.getPressure(index);
708                UsabilityStudyLogUtils.getInstance().write(
709                        eventTag + eventTime + "," + id + "," + x + "," + y + ","
710                        + size + "," + pressure);
711            }
712        }
713        if (ProductionFlag.IS_EXPERIMENTAL) {
714            ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime, index, id,
715                    x, y);
716        }
717
718        if (mKeyTimerHandler.isInKeyRepeat()) {
719            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
720            // Key repeating timer will be canceled if 2 or more keys are in action, and current
721            // event (UP or DOWN) is non-modifier key.
722            if (pointerCount > 1 && !tracker.isModifier()) {
723                mKeyTimerHandler.cancelKeyRepeatTimer();
724            }
725            // Up event will pass through.
726        }
727
728        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
729        // Translate mutli-touch event to single-touch events on the device that has no distinct
730        // multi-touch panel.
731        if (nonDistinctMultitouch) {
732            // Use only main (id=0) pointer tracker.
733            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
734            if (pointerCount == 1 && oldPointerCount == 2) {
735                // Multi-touch to single touch transition.
736                // Send a down event for the latest pointer if the key is different from the
737                // previous key.
738                final Key newKey = tracker.getKeyOn(x, y);
739                if (mOldKey != newKey) {
740                    tracker.onDownEvent(x, y, eventTime, this);
741                    if (action == MotionEvent.ACTION_UP)
742                        tracker.onUpEvent(x, y, eventTime);
743                }
744            } else if (pointerCount == 2 && oldPointerCount == 1) {
745                // Single-touch to multi-touch transition.
746                // Send an up event for the last pointer.
747                final int lastX = tracker.getLastX();
748                final int lastY = tracker.getLastY();
749                mOldKey = tracker.getKeyOn(lastX, lastY);
750                tracker.onUpEvent(lastX, lastY, eventTime);
751            } else if (pointerCount == 1 && oldPointerCount == 1) {
752                tracker.processMotionEvent(action, x, y, eventTime, this);
753            } else {
754                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
755                        + " (old " + oldPointerCount + ")");
756            }
757            return true;
758        }
759
760        if (action == MotionEvent.ACTION_MOVE) {
761            for (int i = 0; i < pointerCount; i++) {
762                final int pointerId = me.getPointerId(i);
763                final PointerTracker tracker = PointerTracker.getPointerTracker(
764                        pointerId, this);
765                final int px, py;
766                final MotionEvent motionEvent;
767                if (mMoreKeysPanel != null
768                        && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
769                    px = mMoreKeysPanel.translateX((int)me.getX(i));
770                    py = mMoreKeysPanel.translateY((int)me.getY(i));
771                    motionEvent = null;
772                } else {
773                    px = (int)me.getX(i);
774                    py = (int)me.getY(i);
775                    motionEvent = me;
776                }
777                tracker.onMoveEvent(px, py, eventTime, motionEvent);
778                if (ENABLE_USABILITY_STUDY_LOG) {
779                    final float pointerSize = me.getSize(i);
780                    final float pointerPressure = me.getPressure(i);
781                    UsabilityStudyLogUtils.getInstance().write("[Move]"  + eventTime + ","
782                            + pointerId + "," + px + "," + py + ","
783                            + pointerSize + "," + pointerPressure);
784                }
785                if (ProductionFlag.IS_EXPERIMENTAL) {
786                    ResearchLogger.latinKeyboardView_processMotionEvent(me, action, eventTime,
787                            i, pointerId, px, py);
788                }
789            }
790        } else {
791            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
792            tracker.processMotionEvent(action, x, y, eventTime, this);
793        }
794
795        return true;
796    }
797
798    @Override
799    public void closing() {
800        super.closing();
801        dismissMoreKeysPanel();
802        mMoreKeysPanelCache.clear();
803    }
804
805    @Override
806    public boolean dismissMoreKeysPanel() {
807        if (mMoreKeysWindow != null && mMoreKeysWindow.isShowing()) {
808            mMoreKeysWindow.dismiss();
809            mMoreKeysPanel = null;
810            mMoreKeysPanelPointerTrackerId = -1;
811            dimEntireKeyboard(false);
812            return true;
813        }
814        return false;
815    }
816
817    @Override
818    public void draw(Canvas c) {
819        Utils.GCUtils.getInstance().reset();
820        boolean tryGC = true;
821        for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) {
822            try {
823                super.draw(c);
824                tryGC = false;
825            } catch (OutOfMemoryError e) {
826                tryGC = Utils.GCUtils.getInstance().tryGCOrWait(TAG, e);
827            }
828        }
829    }
830
831    /**
832     * Receives hover events from the input framework.
833     *
834     * @param event The motion event to be dispatched.
835     * @return {@code true} if the event was handled by the view, {@code false}
836     *         otherwise
837     */
838    @Override
839    public boolean dispatchHoverEvent(MotionEvent event) {
840        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
841            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
842            return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
843        }
844
845        // Reflection doesn't support calling superclass methods.
846        return false;
847    }
848
849    public void updateShortcutKey(boolean available) {
850        final Keyboard keyboard = getKeyboard();
851        if (keyboard == null) return;
852        final Key shortcutKey = keyboard.getKey(Keyboard.CODE_SHORTCUT);
853        if (shortcutKey == null) return;
854        shortcutKey.setEnabled(available);
855        invalidateKey(shortcutKey);
856    }
857
858    private void updateAltCodeKeyWhileTyping() {
859        final Keyboard keyboard = getKeyboard();
860        if (keyboard == null) return;
861        for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
862            invalidateKey(key);
863        }
864    }
865
866    public void startDisplayLanguageOnSpacebar(boolean subtypeChanged,
867            boolean needsToDisplayLanguage, boolean hasMultipleEnabledIMEsOrSubtypes) {
868        mNeedsToDisplayLanguage = needsToDisplayLanguage;
869        mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
870        final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
871        if (animator == null) {
872            mNeedsToDisplayLanguage = false;
873        } else {
874            if (subtypeChanged && needsToDisplayLanguage) {
875                setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
876                if (animator.isStarted()) {
877                    animator.cancel();
878                }
879                animator.start();
880            } else {
881                if (!animator.isStarted()) {
882                    mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
883                }
884            }
885        }
886        invalidateKey(mSpaceKey);
887    }
888
889    public void updateAutoCorrectionState(boolean isAutoCorrection) {
890        if (!mAutoCorrectionSpacebarLedEnabled) return;
891        mAutoCorrectionSpacebarLedOn = isAutoCorrection;
892        invalidateKey(mSpaceKey);
893    }
894
895    @Override
896    protected void onDrawKeyTopVisuals(Key key, Canvas canvas, Paint paint, KeyDrawParams params) {
897        if (key.altCodeWhileTyping() && key.isEnabled()) {
898            params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
899        }
900        if (key.mCode == Keyboard.CODE_SPACE) {
901            drawSpacebar(key, canvas, paint);
902            // Whether space key needs to show the "..." popup hint for special purposes
903            if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
904                drawKeyPopupHint(key, canvas, paint, params);
905            }
906        } else if (key.mCode == Keyboard.CODE_LANGUAGE_SWITCH) {
907            super.onDrawKeyTopVisuals(key, canvas, paint, params);
908            drawKeyPopupHint(key, canvas, paint, params);
909        } else {
910            super.onDrawKeyTopVisuals(key, canvas, paint, params);
911        }
912    }
913
914    private boolean fitsTextIntoWidth(final int width, String text, Paint paint) {
915        paint.setTextScaleX(1.0f);
916        final float textWidth = getLabelWidth(text, paint);
917        if (textWidth < width) return true;
918
919        final float scaleX = width / textWidth;
920        if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) return false;
921
922        paint.setTextScaleX(scaleX);
923        return getLabelWidth(text, paint) < width;
924    }
925
926    // Layout language name on spacebar.
927    private String layoutLanguageOnSpacebar(Paint paint, InputMethodSubtype subtype,
928            final int width) {
929        // Choose appropriate language name to fit into the width.
930        String text = getFullDisplayName(subtype, getResources());
931        if (fitsTextIntoWidth(width, text, paint)) {
932            return text;
933        }
934
935        text = getMiddleDisplayName(subtype);
936        if (fitsTextIntoWidth(width, text, paint)) {
937            return text;
938        }
939
940        text = getShortDisplayName(subtype);
941        if (fitsTextIntoWidth(width, text, paint)) {
942            return text;
943        }
944
945        return "";
946    }
947
948    private void drawSpacebar(Key key, Canvas canvas, Paint paint) {
949        final int width = key.mWidth;
950        final int height = key.mHeight;
951
952        // If input language are explicitly selected.
953        if (mNeedsToDisplayLanguage) {
954            paint.setTextAlign(Align.CENTER);
955            paint.setTypeface(Typeface.DEFAULT);
956            paint.setTextSize(mSpacebarTextSize);
957            final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
958            final String language = layoutLanguageOnSpacebar(paint, subtype, width);
959            // Draw language text with shadow
960            final float descent = paint.descent();
961            final float textHeight = -paint.ascent() + descent;
962            final float baseline = height / 2 + textHeight / 2;
963            paint.setColor(mSpacebarTextShadowColor);
964            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
965            canvas.drawText(language, width / 2, baseline - descent - 1, paint);
966            paint.setColor(mSpacebarTextColor);
967            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
968            canvas.drawText(language, width / 2, baseline - descent, paint);
969        }
970
971        // Draw the spacebar icon at the bottom
972        if (mAutoCorrectionSpacebarLedOn) {
973            final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
974            final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
975            int x = (width - iconWidth) / 2;
976            int y = height - iconHeight;
977            drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
978        } else if (mSpaceIcon != null) {
979            final int iconWidth = mSpaceIcon.getIntrinsicWidth();
980            final int iconHeight = mSpaceIcon.getIntrinsicHeight();
981            int x = (width - iconWidth) / 2;
982            int y = height - iconHeight;
983            drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
984        }
985    }
986
987    // InputMethodSubtype's display name for spacebar text in its locale.
988    //        isAdditionalSubtype (T=true, F=false)
989    // locale layout | Short  Middle      Full
990    // ------ ------ - ---- --------- ----------------------
991    //  en_US qwerty F  En  English   English (US)           exception
992    //  en_GB qwerty F  En  English   English (UK)           exception
993    //  fr    azerty F  Fr  Français  Français
994    //  fr_CA qwerty F  Fr  Français  Français (Canada)
995    //  de    qwertz F  De  Deutsch   Deutsch
996    //  zz    qwerty F      QWERTY    QWERTY
997    //  fr    qwertz T  Fr  Français  Français (QWERTZ)
998    //  de    qwerty T  De  Deutsch   Deutsch (QWERTY)
999    //  en_US azerty T  En  English   English (US) (AZERTY)
1000    //  zz    azerty T      AZERTY    AZERTY
1001
1002    // Get InputMethodSubtype's full display name in its locale.
1003    static String getFullDisplayName(InputMethodSubtype subtype, Resources res) {
1004        if (SubtypeLocale.isNoLanguage(subtype)) {
1005            return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
1006        }
1007
1008        return SubtypeLocale.getSubtypeDisplayName(subtype, res);
1009    }
1010
1011    // Get InputMethodSubtype's short display name in its locale.
1012    static String getShortDisplayName(InputMethodSubtype subtype) {
1013        if (SubtypeLocale.isNoLanguage(subtype)) {
1014            return "";
1015        }
1016        final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1017        return StringUtils.toTitleCase(locale.getLanguage(), locale);
1018    }
1019
1020    // Get InputMethodSubtype's middle display name in its locale.
1021    static String getMiddleDisplayName(InputMethodSubtype subtype) {
1022        if (SubtypeLocale.isNoLanguage(subtype)) {
1023            return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
1024        }
1025        final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1026        return StringUtils.toTitleCase(locale.getDisplayLanguage(locale), locale);
1027    }
1028}
1029