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