MainKeyboardView.java revision e28eba5074664d5716b8e58b8d0a235746b261eb
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.Color;
28import android.graphics.Paint;
29import android.graphics.Paint.Align;
30import android.graphics.Typeface;
31import android.graphics.drawable.Drawable;
32import android.os.Message;
33import android.os.SystemClock;
34import android.preference.PreferenceManager;
35import android.util.AttributeSet;
36import android.util.DisplayMetrics;
37import android.util.Log;
38import android.util.SparseArray;
39import android.util.TypedValue;
40import android.view.LayoutInflater;
41import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewConfiguration;
44import android.view.ViewGroup;
45import android.view.inputmethod.InputMethodSubtype;
46import android.widget.TextView;
47
48import com.android.inputmethod.accessibility.AccessibilityUtils;
49import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy;
50import com.android.inputmethod.annotations.ExternallyReferenced;
51import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
52import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
53import com.android.inputmethod.keyboard.internal.GestureFloatingPreviewText;
54import com.android.inputmethod.keyboard.internal.GestureTrailsPreview;
55import com.android.inputmethod.keyboard.internal.KeyDrawParams;
56import com.android.inputmethod.keyboard.internal.KeyPreviewDrawParams;
57import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
58import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview;
59import com.android.inputmethod.keyboard.internal.TouchScreenRegulator;
60import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
61import com.android.inputmethod.latin.Constants;
62import com.android.inputmethod.latin.DebugSettings;
63import com.android.inputmethod.latin.LatinIME;
64import com.android.inputmethod.latin.LatinImeLogger;
65import com.android.inputmethod.latin.R;
66import com.android.inputmethod.latin.Settings;
67import com.android.inputmethod.latin.SubtypeLocale;
68import com.android.inputmethod.latin.SuggestedWords;
69import com.android.inputmethod.latin.define.ProductionFlag;
70import com.android.inputmethod.latin.utils.CollectionUtils;
71import com.android.inputmethod.latin.utils.CoordinateUtils;
72import com.android.inputmethod.latin.utils.ResourceUtils;
73import com.android.inputmethod.latin.utils.StaticInnerHandlerWrapper;
74import com.android.inputmethod.latin.utils.StringUtils;
75import com.android.inputmethod.latin.utils.Utils.UsabilityStudyLogUtils;
76import com.android.inputmethod.research.ResearchLogger;
77
78import java.util.Locale;
79import java.util.WeakHashMap;
80
81/**
82 * A view that is responsible for detecting key presses and touch movements.
83 *
84 * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedEnabled
85 * @attr ref R.styleable#MainKeyboardView_autoCorrectionSpacebarLedIcon
86 * @attr ref R.styleable#MainKeyboardView_spacebarTextRatio
87 * @attr ref R.styleable#MainKeyboardView_spacebarTextColor
88 * @attr ref R.styleable#MainKeyboardView_spacebarTextShadowColor
89 * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFinalAlpha
90 * @attr ref R.styleable#MainKeyboardView_languageOnSpacebarFadeoutAnimator
91 * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator
92 * @attr ref R.styleable#MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator
93 * @attr ref R.styleable#MainKeyboardView_keyHysteresisDistance
94 * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdTime
95 * @attr ref R.styleable#MainKeyboardView_touchNoiseThresholdDistance
96 * @attr ref R.styleable#MainKeyboardView_slidingKeyInputEnable
97 * @attr ref R.styleable#MainKeyboardView_keyRepeatStartTimeout
98 * @attr ref R.styleable#MainKeyboardView_keyRepeatInterval
99 * @attr ref R.styleable#MainKeyboardView_longPressKeyTimeout
100 * @attr ref R.styleable#MainKeyboardView_longPressShiftKeyTimeout
101 * @attr ref R.styleable#MainKeyboardView_ignoreAltCodeKeyTimeout
102 * @attr ref R.styleable#MainKeyboardView_keyPreviewLayout
103 * @attr ref R.styleable#MainKeyboardView_keyPreviewOffset
104 * @attr ref R.styleable#MainKeyboardView_keyPreviewHeight
105 * @attr ref R.styleable#MainKeyboardView_keyPreviewLingerTimeout
106 * @attr ref R.styleable#MainKeyboardView_moreKeysKeyboardLayout
107 * @attr ref R.styleable#MainKeyboardView_backgroundDimAlpha
108 * @attr ref R.styleable#MainKeyboardView_showMoreKeysKeyboardAtTouchPoint
109 * @attr ref R.styleable#MainKeyboardView_gestureFloatingPreviewTextLingerTimeout
110 * @attr ref R.styleable#MainKeyboardView_gestureStaticTimeThresholdAfterFastTyping
111 * @attr ref R.styleable#MainKeyboardView_gestureDetectFastMoveSpeedThreshold
112 * @attr ref R.styleable#MainKeyboardView_gestureDynamicThresholdDecayDuration
113 * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdFrom
114 * @attr ref R.styleable#MainKeyboardView_gestureDynamicTimeThresholdTo
115 * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdFrom
116 * @attr ref R.styleable#MainKeyboardView_gestureDynamicDistanceThresholdTo
117 * @attr ref R.styleable#MainKeyboardView_gestureSamplingMinimumDistance
118 * @attr ref R.styleable#MainKeyboardView_gestureRecognitionMinimumTime
119 * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold
120 * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
121 */
122public final class MainKeyboardView extends KeyboardView implements PointerTracker.KeyEventHandler,
123        PointerTracker.DrawingProxy, MoreKeysPanel.Controller,
124        TouchScreenRegulator.ProcessMotionEvent {
125    private static final String TAG = MainKeyboardView.class.getSimpleName();
126
127    // TODO: Kill process when the usability study mode was changed.
128    private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
129
130    /** Listener for {@link KeyboardActionListener}. */
131    private KeyboardActionListener mKeyboardActionListener;
132
133    /* Space key and its icons */
134    private Key mSpaceKey;
135    private Drawable mSpaceIcon;
136    // Stuff to draw language name on spacebar.
137    private final int mLanguageOnSpacebarFinalAlpha;
138    private ObjectAnimator mLanguageOnSpacebarFadeoutAnimator;
139    private boolean mNeedsToDisplayLanguage;
140    private boolean mHasMultipleEnabledIMEsOrSubtypes;
141    private int mLanguageOnSpacebarAnimAlpha = Constants.Color.ALPHA_OPAQUE;
142    private final float mSpacebarTextRatio;
143    private float mSpacebarTextSize;
144    private final int mSpacebarTextColor;
145    private final int mSpacebarTextShadowColor;
146    // The minimum x-scale to fit the language name on spacebar.
147    private static final float MINIMUM_XSCALE_OF_LANGUAGE_NAME = 0.8f;
148    // Stuff to draw auto correction LED on spacebar.
149    private boolean mAutoCorrectionSpacebarLedOn;
150    private final boolean mAutoCorrectionSpacebarLedEnabled;
151    private final Drawable mAutoCorrectionSpacebarLedIcon;
152    private static final int SPACE_LED_LENGTH_PERCENT = 80;
153
154    // Stuff to draw altCodeWhileTyping keys.
155    private ObjectAnimator mAltCodeKeyWhileTypingFadeoutAnimator;
156    private ObjectAnimator mAltCodeKeyWhileTypingFadeinAnimator;
157    private int mAltCodeKeyWhileTypingAnimAlpha = Constants.Color.ALPHA_OPAQUE;
158
159    // Preview placer view
160    private final PreviewPlacerView mPreviewPlacerView;
161    private final int[] mOriginCoords = CoordinateUtils.newInstance();
162    private final GestureFloatingPreviewText mGestureFloatingPreviewText;
163    private final GestureTrailsPreview mGestureTrailsPreview;
164    private final SlidingKeyInputPreview mSlidingKeyInputPreview;
165
166    // Key preview
167    private static final int PREVIEW_ALPHA = 240;
168    private final int mKeyPreviewLayoutId;
169    private final int mKeyPreviewOffset;
170    private final int mKeyPreviewHeight;
171    private final SparseArray<TextView> mKeyPreviewTexts = CollectionUtils.newSparseArray();
172    private final KeyPreviewDrawParams mKeyPreviewDrawParams = new KeyPreviewDrawParams();
173    private boolean mShowKeyPreviewPopup = true;
174    private int mKeyPreviewLingerTimeout;
175
176    // More keys keyboard
177    private final Paint mBackgroundDimAlphaPaint = new Paint();
178    private boolean mNeedsToDimEntireKeyboard;
179    private final View mMoreKeysKeyboardContainer;
180    private final WeakHashMap<Key, Keyboard> mMoreKeysKeyboardCache =
181            CollectionUtils.newWeakHashMap();
182    private final boolean mConfigShowMoreKeysKeyboardAtTouchedPoint;
183    // More keys panel (used by both more keys keyboard and more suggestions view)
184    // TODO: Consider extending to support multiple more keys panels
185    private MoreKeysPanel mMoreKeysPanel;
186
187    // Gesture floating preview text
188    // TODO: Make this parameter customizable by user via settings.
189    private int mGestureFloatingPreviewTextLingerTimeout;
190
191    private final TouchScreenRegulator mTouchScreenRegulator;
192
193    private KeyDetector mKeyDetector;
194    private final boolean mHasDistinctMultitouch;
195    private int mOldPointerCount = 1;
196    private Key mOldKey;
197
198    private final KeyTimerHandler mKeyTimerHandler;
199
200    private static final class KeyTimerHandler extends StaticInnerHandlerWrapper<MainKeyboardView>
201            implements TimerProxy {
202        private static final int MSG_TYPING_STATE_EXPIRED = 0;
203        private static final int MSG_REPEAT_KEY = 1;
204        private static final int MSG_LONGPRESS_KEY = 2;
205        private static final int MSG_DOUBLE_TAP_SHIFT_KEY = 3;
206        private static final int MSG_UPDATE_BATCH_INPUT = 4;
207
208        private final int mKeyRepeatStartTimeout;
209        private final int mKeyRepeatInterval;
210        private final int mLongPressShiftLockTimeout;
211        private final int mIgnoreAltCodeKeyTimeout;
212        private final int mGestureRecognitionUpdateTime;
213
214        public KeyTimerHandler(final MainKeyboardView outerInstance,
215                final TypedArray mainKeyboardViewAttr) {
216            super(outerInstance);
217
218            mKeyRepeatStartTimeout = mainKeyboardViewAttr.getInt(
219                    R.styleable.MainKeyboardView_keyRepeatStartTimeout, 0);
220            mKeyRepeatInterval = mainKeyboardViewAttr.getInt(
221                    R.styleable.MainKeyboardView_keyRepeatInterval, 0);
222            mLongPressShiftLockTimeout = mainKeyboardViewAttr.getInt(
223                    R.styleable.MainKeyboardView_longPressShiftLockTimeout, 0);
224            mIgnoreAltCodeKeyTimeout = mainKeyboardViewAttr.getInt(
225                    R.styleable.MainKeyboardView_ignoreAltCodeKeyTimeout, 0);
226            mGestureRecognitionUpdateTime = mainKeyboardViewAttr.getInt(
227                    R.styleable.MainKeyboardView_gestureRecognitionUpdateTime, 0);
228        }
229
230        @Override
231        public void handleMessage(final Message msg) {
232            final MainKeyboardView keyboardView = getOuterInstance();
233            if (keyboardView == null) {
234                return;
235            }
236            final PointerTracker tracker = (PointerTracker) msg.obj;
237            switch (msg.what) {
238            case MSG_TYPING_STATE_EXPIRED:
239                startWhileTypingFadeinAnimation(keyboardView);
240                break;
241            case MSG_REPEAT_KEY:
242                final Key currentKey = tracker.getKey();
243                if (currentKey != null && currentKey.mCode == msg.arg1) {
244                    tracker.onRepeatKey(currentKey);
245                    AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
246                            currentKey.mCode, keyboardView);
247                    startKeyRepeatTimer(tracker, mKeyRepeatInterval);
248                }
249                break;
250            case MSG_LONGPRESS_KEY:
251                keyboardView.onLongPress(tracker);
252                break;
253            case MSG_UPDATE_BATCH_INPUT:
254                tracker.updateBatchInputByTimer(SystemClock.uptimeMillis());
255                startUpdateBatchInputTimer(tracker);
256                break;
257            }
258        }
259
260        private void startKeyRepeatTimer(final PointerTracker tracker, final long delay) {
261            final Key key = tracker.getKey();
262            if (key == null) {
263                return;
264            }
265            sendMessageDelayed(obtainMessage(MSG_REPEAT_KEY, key.mCode, 0, tracker), delay);
266        }
267
268        @Override
269        public void startKeyRepeatTimer(final PointerTracker tracker) {
270            startKeyRepeatTimer(tracker, mKeyRepeatStartTimeout);
271        }
272
273        public void cancelKeyRepeatTimer() {
274            removeMessages(MSG_REPEAT_KEY);
275        }
276
277        // TODO: Suppress layout changes in key repeat mode
278        public boolean isInKeyRepeat() {
279            return hasMessages(MSG_REPEAT_KEY);
280        }
281
282        @Override
283        public void startLongPressTimer(final PointerTracker tracker) {
284            cancelLongPressTimer();
285            if (tracker == null) {
286                return;
287            }
288            final Key key = tracker.getKey();
289            final int delay;
290            switch (key.mCode) {
291            case Constants.CODE_SHIFT:
292                delay = mLongPressShiftLockTimeout;
293                break;
294            default:
295                final int longpressTimeout =
296                        Settings.getInstance().getCurrent().mKeyLongpressTimeout;
297                if (tracker.isInSlidingKeyInputFromModifier()) {
298                    // We use longer timeout for sliding finger input started from the modifier key.
299                    delay = longpressTimeout * 3;
300                } else {
301                    delay = longpressTimeout;
302                }
303                break;
304            }
305            if (delay > 0) {
306                sendMessageDelayed(obtainMessage(MSG_LONGPRESS_KEY, tracker), delay);
307            }
308        }
309
310        @Override
311        public void cancelLongPressTimer() {
312            removeMessages(MSG_LONGPRESS_KEY);
313        }
314
315        private static void cancelAndStartAnimators(final ObjectAnimator animatorToCancel,
316                final ObjectAnimator animatorToStart) {
317            if (animatorToCancel == null || animatorToStart == null) {
318                // TODO: Stop using null as a no-operation animator.
319                return;
320            }
321            float startFraction = 0.0f;
322            if (animatorToCancel.isStarted()) {
323                animatorToCancel.cancel();
324                startFraction = 1.0f - animatorToCancel.getAnimatedFraction();
325            }
326            final long startTime = (long)(animatorToStart.getDuration() * startFraction);
327            animatorToStart.start();
328            animatorToStart.setCurrentPlayTime(startTime);
329        }
330
331        private static void startWhileTypingFadeinAnimation(final MainKeyboardView keyboardView) {
332            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator,
333                    keyboardView.mAltCodeKeyWhileTypingFadeinAnimator);
334        }
335
336        private static void startWhileTypingFadeoutAnimation(final MainKeyboardView keyboardView) {
337            cancelAndStartAnimators(keyboardView.mAltCodeKeyWhileTypingFadeinAnimator,
338                    keyboardView.mAltCodeKeyWhileTypingFadeoutAnimator);
339        }
340
341        @Override
342        public void startTypingStateTimer(final Key typedKey) {
343            if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
344                return;
345            }
346
347            final boolean isTyping = isTypingState();
348            removeMessages(MSG_TYPING_STATE_EXPIRED);
349            final MainKeyboardView keyboardView = getOuterInstance();
350
351            // When user hits the space or the enter key, just cancel the while-typing timer.
352            final int typedCode = typedKey.mCode;
353            if (typedCode == Constants.CODE_SPACE || typedCode == Constants.CODE_ENTER) {
354                if (isTyping) {
355                    startWhileTypingFadeinAnimation(keyboardView);
356                }
357                return;
358            }
359
360            sendMessageDelayed(
361                    obtainMessage(MSG_TYPING_STATE_EXPIRED), mIgnoreAltCodeKeyTimeout);
362            if (isTyping) {
363                return;
364            }
365            startWhileTypingFadeoutAnimation(keyboardView);
366        }
367
368        @Override
369        public boolean isTypingState() {
370            return hasMessages(MSG_TYPING_STATE_EXPIRED);
371        }
372
373        @Override
374        public void startDoubleTapShiftKeyTimer() {
375            sendMessageDelayed(obtainMessage(MSG_DOUBLE_TAP_SHIFT_KEY),
376                    ViewConfiguration.getDoubleTapTimeout());
377        }
378
379        @Override
380        public void cancelDoubleTapShiftKeyTimer() {
381            removeMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
382        }
383
384        @Override
385        public boolean isInDoubleTapShiftKeyTimeout() {
386            return hasMessages(MSG_DOUBLE_TAP_SHIFT_KEY);
387        }
388
389        @Override
390        public void cancelKeyTimers() {
391            cancelKeyRepeatTimer();
392            cancelLongPressTimer();
393        }
394
395        @Override
396        public void startUpdateBatchInputTimer(final PointerTracker tracker) {
397            if (mGestureRecognitionUpdateTime <= 0) {
398                return;
399            }
400            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
401            sendMessageDelayed(obtainMessage(MSG_UPDATE_BATCH_INPUT, tracker),
402                    mGestureRecognitionUpdateTime);
403        }
404
405        @Override
406        public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
407            removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
408        }
409
410        @Override
411        public void cancelAllUpdateBatchInputTimers() {
412            removeMessages(MSG_UPDATE_BATCH_INPUT);
413        }
414
415        public void cancelAllMessages() {
416            cancelKeyTimers();
417            cancelAllUpdateBatchInputTimers();
418        }
419    }
420
421    private final DrawingHandler mDrawingHandler = new DrawingHandler(this);
422
423    public static class DrawingHandler extends StaticInnerHandlerWrapper<MainKeyboardView> {
424        private static final int MSG_DISMISS_KEY_PREVIEW = 0;
425        private static final int MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
426
427        public DrawingHandler(final MainKeyboardView outerInstance) {
428            super(outerInstance);
429        }
430
431        @Override
432        public void handleMessage(final Message msg) {
433            final MainKeyboardView mainKeyboardView = getOuterInstance();
434            if (mainKeyboardView == null) return;
435            final PointerTracker tracker = (PointerTracker) msg.obj;
436            switch (msg.what) {
437            case MSG_DISMISS_KEY_PREVIEW:
438                final TextView previewText = mainKeyboardView.mKeyPreviewTexts.get(
439                        tracker.mPointerId);
440                if (previewText != null) {
441                    previewText.setVisibility(INVISIBLE);
442                }
443                break;
444            case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
445                mainKeyboardView.showGestureFloatingPreviewText(SuggestedWords.EMPTY);
446                break;
447            }
448        }
449
450        public void dismissKeyPreview(final long delay, final PointerTracker tracker) {
451            sendMessageDelayed(obtainMessage(MSG_DISMISS_KEY_PREVIEW, tracker), delay);
452        }
453
454        public void cancelDismissKeyPreview(final PointerTracker tracker) {
455            removeMessages(MSG_DISMISS_KEY_PREVIEW, tracker);
456        }
457
458        private void cancelAllDismissKeyPreviews() {
459            removeMessages(MSG_DISMISS_KEY_PREVIEW);
460        }
461
462        public void dismissGestureFloatingPreviewText(final long delay) {
463            sendMessageDelayed(obtainMessage(MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT), delay);
464        }
465
466        public void cancelAllMessages() {
467            cancelAllDismissKeyPreviews();
468        }
469    }
470
471    public MainKeyboardView(final Context context, final AttributeSet attrs) {
472        this(context, attrs, R.attr.mainKeyboardViewStyle);
473    }
474
475    public MainKeyboardView(final Context context, final AttributeSet attrs, final int defStyle) {
476        super(context, attrs, defStyle);
477
478        mTouchScreenRegulator = new TouchScreenRegulator(context, this);
479
480        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
481        final boolean forceNonDistinctMultitouch = prefs.getBoolean(
482                DebugSettings.PREF_FORCE_NON_DISTINCT_MULTITOUCH, false);
483        final boolean hasDistinctMultitouch = context.getPackageManager()
484                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
485        mHasDistinctMultitouch = hasDistinctMultitouch && !forceNonDistinctMultitouch;
486        final Resources res = getResources();
487        final boolean needsPhantomSuddenMoveEventHack = Boolean.parseBoolean(
488                ResourceUtils.getDeviceOverrideValue(
489                        res, R.array.phantom_sudden_move_event_device_list));
490        PointerTracker.init(needsPhantomSuddenMoveEventHack);
491        mPreviewPlacerView = new PreviewPlacerView(context, attrs);
492
493        final TypedArray mainKeyboardViewAttr = context.obtainStyledAttributes(
494                attrs, R.styleable.MainKeyboardView, defStyle, R.style.MainKeyboardView);
495        final int backgroundDimAlpha = mainKeyboardViewAttr.getInt(
496                R.styleable.MainKeyboardView_backgroundDimAlpha, 0);
497        mBackgroundDimAlphaPaint.setColor(Color.BLACK);
498        mBackgroundDimAlphaPaint.setAlpha(backgroundDimAlpha);
499        mAutoCorrectionSpacebarLedEnabled = mainKeyboardViewAttr.getBoolean(
500                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedEnabled, false);
501        mAutoCorrectionSpacebarLedIcon = mainKeyboardViewAttr.getDrawable(
502                R.styleable.MainKeyboardView_autoCorrectionSpacebarLedIcon);
503        mSpacebarTextRatio = mainKeyboardViewAttr.getFraction(
504                R.styleable.MainKeyboardView_spacebarTextRatio, 1, 1, 1.0f);
505        mSpacebarTextColor = mainKeyboardViewAttr.getColor(
506                R.styleable.MainKeyboardView_spacebarTextColor, 0);
507        mSpacebarTextShadowColor = mainKeyboardViewAttr.getColor(
508                R.styleable.MainKeyboardView_spacebarTextShadowColor, 0);
509        mLanguageOnSpacebarFinalAlpha = mainKeyboardViewAttr.getInt(
510                R.styleable.MainKeyboardView_languageOnSpacebarFinalAlpha,
511                Constants.Color.ALPHA_OPAQUE);
512        final int languageOnSpacebarFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
513                R.styleable.MainKeyboardView_languageOnSpacebarFadeoutAnimator, 0);
514        final int altCodeKeyWhileTypingFadeoutAnimatorResId = mainKeyboardViewAttr.getResourceId(
515                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeoutAnimator, 0);
516        final int altCodeKeyWhileTypingFadeinAnimatorResId = mainKeyboardViewAttr.getResourceId(
517                R.styleable.MainKeyboardView_altCodeKeyWhileTypingFadeinAnimator, 0);
518
519        final float keyHysteresisDistance = mainKeyboardViewAttr.getDimension(
520                R.styleable.MainKeyboardView_keyHysteresisDistance, 0.0f);
521        final float keyHysteresisDistanceForSlidingModifier = mainKeyboardViewAttr.getDimension(
522                R.styleable.MainKeyboardView_keyHysteresisDistanceForSlidingModifier, 0.0f);
523        mKeyDetector = new KeyDetector(
524                keyHysteresisDistance, keyHysteresisDistanceForSlidingModifier);
525        mKeyTimerHandler = new KeyTimerHandler(this, mainKeyboardViewAttr);
526        mKeyPreviewOffset = mainKeyboardViewAttr.getDimensionPixelOffset(
527                R.styleable.MainKeyboardView_keyPreviewOffset, 0);
528        mKeyPreviewHeight = mainKeyboardViewAttr.getDimensionPixelSize(
529                R.styleable.MainKeyboardView_keyPreviewHeight, 0);
530        mKeyPreviewLingerTimeout = mainKeyboardViewAttr.getInt(
531                R.styleable.MainKeyboardView_keyPreviewLingerTimeout, 0);
532        mKeyPreviewLayoutId = mainKeyboardViewAttr.getResourceId(
533                R.styleable.MainKeyboardView_keyPreviewLayout, 0);
534        if (mKeyPreviewLayoutId == 0) {
535            mShowKeyPreviewPopup = false;
536        }
537        final int moreKeysKeyboardLayoutId = mainKeyboardViewAttr.getResourceId(
538                R.styleable.MainKeyboardView_moreKeysKeyboardLayout, 0);
539        mConfigShowMoreKeysKeyboardAtTouchedPoint = mainKeyboardViewAttr.getBoolean(
540                R.styleable.MainKeyboardView_showMoreKeysKeyboardAtTouchedPoint, false);
541
542        mGestureFloatingPreviewTextLingerTimeout = mainKeyboardViewAttr.getInt(
543                R.styleable.MainKeyboardView_gestureFloatingPreviewTextLingerTimeout, 0);
544        PointerTracker.setParameters(mainKeyboardViewAttr);
545
546        mGestureFloatingPreviewText = new GestureFloatingPreviewText(
547                mPreviewPlacerView, mainKeyboardViewAttr);
548        mPreviewPlacerView.addPreview(mGestureFloatingPreviewText);
549
550        mGestureTrailsPreview = new GestureTrailsPreview(
551                mPreviewPlacerView, mainKeyboardViewAttr);
552        mPreviewPlacerView.addPreview(mGestureTrailsPreview);
553
554        mSlidingKeyInputPreview = new SlidingKeyInputPreview(
555                mPreviewPlacerView, mainKeyboardViewAttr);
556        mPreviewPlacerView.addPreview(mSlidingKeyInputPreview);
557        mainKeyboardViewAttr.recycle();
558
559        mMoreKeysKeyboardContainer = LayoutInflater.from(getContext())
560                .inflate(moreKeysKeyboardLayoutId, null);
561        mLanguageOnSpacebarFadeoutAnimator = loadObjectAnimator(
562                languageOnSpacebarFadeoutAnimatorResId, this);
563        mAltCodeKeyWhileTypingFadeoutAnimator = loadObjectAnimator(
564                altCodeKeyWhileTypingFadeoutAnimatorResId, this);
565        mAltCodeKeyWhileTypingFadeinAnimator = loadObjectAnimator(
566                altCodeKeyWhileTypingFadeinAnimatorResId, this);
567    }
568
569    private ObjectAnimator loadObjectAnimator(final int resId, final Object target) {
570        if (resId == 0) {
571            // TODO: Stop returning null.
572            return null;
573        }
574        final ObjectAnimator animator = (ObjectAnimator)AnimatorInflater.loadAnimator(
575                getContext(), resId);
576        if (animator != null) {
577            animator.setTarget(target);
578        }
579        return animator;
580    }
581
582    @ExternallyReferenced
583    public int getLanguageOnSpacebarAnimAlpha() {
584        return mLanguageOnSpacebarAnimAlpha;
585    }
586
587    @ExternallyReferenced
588    public void setLanguageOnSpacebarAnimAlpha(final int alpha) {
589        mLanguageOnSpacebarAnimAlpha = alpha;
590        invalidateKey(mSpaceKey);
591    }
592
593    @ExternallyReferenced
594    public int getAltCodeKeyWhileTypingAnimAlpha() {
595        return mAltCodeKeyWhileTypingAnimAlpha;
596    }
597
598    @ExternallyReferenced
599    public void setAltCodeKeyWhileTypingAnimAlpha(final int alpha) {
600        if (mAltCodeKeyWhileTypingAnimAlpha == alpha) {
601            return;
602        }
603        // Update the visual of alt-code-key-while-typing.
604        mAltCodeKeyWhileTypingAnimAlpha = alpha;
605        final Keyboard keyboard = getKeyboard();
606        if (keyboard == null) {
607            return;
608        }
609        for (final Key key : keyboard.mAltCodeKeysWhileTyping) {
610            invalidateKey(key);
611        }
612    }
613
614    public void setKeyboardActionListener(final KeyboardActionListener listener) {
615        mKeyboardActionListener = listener;
616        PointerTracker.setKeyboardActionListener(listener);
617    }
618
619    /**
620     * Returns the {@link KeyboardActionListener} object.
621     * @return the listener attached to this keyboard
622     */
623    @Override
624    public KeyboardActionListener getKeyboardActionListener() {
625        return mKeyboardActionListener;
626    }
627
628    @Override
629    public KeyDetector getKeyDetector() {
630        return mKeyDetector;
631    }
632
633    @Override
634    public DrawingProxy getDrawingProxy() {
635        return this;
636    }
637
638    @Override
639    public TimerProxy getTimerProxy() {
640        return mKeyTimerHandler;
641    }
642
643    /**
644     * Attaches a keyboard to this view. The keyboard can be switched at any time and the
645     * view will re-layout itself to accommodate the keyboard.
646     * @see Keyboard
647     * @see #getKeyboard()
648     * @param keyboard the keyboard to display in this view
649     */
650    @Override
651    public void setKeyboard(final Keyboard keyboard) {
652        // Remove any pending messages, except dismissing preview and key repeat.
653        mKeyTimerHandler.cancelLongPressTimer();
654        super.setKeyboard(keyboard);
655        mKeyDetector.setKeyboard(
656                keyboard, -getPaddingLeft(), -getPaddingTop() + getVerticalCorrection());
657        PointerTracker.setKeyDetector(mKeyDetector);
658        mTouchScreenRegulator.setKeyboardGeometry(keyboard.mOccupiedWidth);
659        mMoreKeysKeyboardCache.clear();
660
661        mSpaceKey = keyboard.getKey(Constants.CODE_SPACE);
662        mSpaceIcon = (mSpaceKey != null)
663                ? mSpaceKey.getIcon(keyboard.mIconsSet, Constants.Color.ALPHA_OPAQUE) : null;
664        final int keyHeight = keyboard.mMostCommonKeyHeight - keyboard.mVerticalGap;
665        mSpacebarTextSize = keyHeight * mSpacebarTextRatio;
666        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
667            ResearchLogger.mainKeyboardView_setKeyboard(keyboard);
668        }
669
670        // This always needs to be set since the accessibility state can
671        // potentially change without the keyboard being set again.
672        AccessibleKeyboardViewProxy.getInstance().setKeyboard();
673    }
674
675    /**
676     * Enables or disables the key feedback popup. This is a popup that shows a magnified
677     * version of the depressed key. By default the preview is enabled.
678     * @param previewEnabled whether or not to enable the key feedback preview
679     * @param delay the delay after which the preview is dismissed
680     * @see #isKeyPreviewPopupEnabled()
681     */
682    public void setKeyPreviewPopupEnabled(final boolean previewEnabled, final int delay) {
683        mShowKeyPreviewPopup = previewEnabled;
684        mKeyPreviewLingerTimeout = delay;
685    }
686
687
688    private void locatePreviewPlacerView() {
689        if (mPreviewPlacerView.getParent() != null) {
690            return;
691        }
692        final int width = getWidth();
693        final int height = getHeight();
694        if (width == 0 || height == 0) {
695            // In transient state.
696            return;
697        }
698        getLocationInWindow(mOriginCoords);
699        final DisplayMetrics dm = getResources().getDisplayMetrics();
700        if (CoordinateUtils.y(mOriginCoords) < dm.heightPixels / 4) {
701            // In transient state.
702            return;
703        }
704        final View rootView = getRootView();
705        if (rootView == null) {
706            Log.w(TAG, "Cannot find root view");
707            return;
708        }
709        final ViewGroup windowContentView = (ViewGroup)rootView.findViewById(android.R.id.content);
710        // Note: It'd be very weird if we get null by android.R.id.content.
711        if (windowContentView == null) {
712            Log.w(TAG, "Cannot find android.R.id.content view to add PreviewPlacerView");
713        } else {
714            windowContentView.addView(mPreviewPlacerView);
715            mPreviewPlacerView.setKeyboardViewGeometry(mOriginCoords, width, height);
716        }
717    }
718
719    /**
720     * Returns the enabled state of the key feedback preview
721     * @return whether or not the key feedback preview is enabled
722     * @see #setKeyPreviewPopupEnabled(boolean, int)
723     */
724    public boolean isKeyPreviewPopupEnabled() {
725        return mShowKeyPreviewPopup;
726    }
727
728    private void addKeyPreview(final TextView keyPreview) {
729        locatePreviewPlacerView();
730        mPreviewPlacerView.addView(
731                keyPreview, ViewLayoutUtils.newLayoutParam(mPreviewPlacerView, 0, 0));
732    }
733
734    private TextView getKeyPreviewText(final int pointerId) {
735        TextView previewText = mKeyPreviewTexts.get(pointerId);
736        if (previewText != null) {
737            return previewText;
738        }
739        final Context context = getContext();
740        if (mKeyPreviewLayoutId != 0) {
741            previewText = (TextView)LayoutInflater.from(context).inflate(mKeyPreviewLayoutId, null);
742        } else {
743            previewText = new TextView(context);
744        }
745        mKeyPreviewTexts.put(pointerId, previewText);
746        return previewText;
747    }
748
749    private void dismissAllKeyPreviews() {
750        final int pointerCount = mKeyPreviewTexts.size();
751        for (int id = 0; id < pointerCount; id++) {
752            final TextView previewText = mKeyPreviewTexts.get(id);
753            if (previewText != null) {
754                previewText.setVisibility(INVISIBLE);
755            }
756        }
757        PointerTracker.setReleasedKeyGraphicsToAllKeys();
758    }
759
760    // Background state set
761    private static final int[][][] KEY_PREVIEW_BACKGROUND_STATE_TABLE = {
762        { // STATE_MIDDLE
763            EMPTY_STATE_SET,
764            { R.attr.state_has_morekeys }
765        },
766        { // STATE_LEFT
767            { R.attr.state_left_edge },
768            { R.attr.state_left_edge, R.attr.state_has_morekeys }
769        },
770        { // STATE_RIGHT
771            { R.attr.state_right_edge },
772            { R.attr.state_right_edge, R.attr.state_has_morekeys }
773        }
774    };
775    private static final int STATE_MIDDLE = 0;
776    private static final int STATE_LEFT = 1;
777    private static final int STATE_RIGHT = 2;
778    private static final int STATE_NORMAL = 0;
779    private static final int STATE_HAS_MOREKEYS = 1;
780    private static final int[] KEY_PREVIEW_BACKGROUND_DEFAULT_STATE =
781            KEY_PREVIEW_BACKGROUND_STATE_TABLE[STATE_MIDDLE][STATE_NORMAL];
782
783    @Override
784    public void showKeyPreview(final PointerTracker tracker) {
785        final KeyPreviewDrawParams previewParams = mKeyPreviewDrawParams;
786        final Keyboard keyboard = getKeyboard();
787        if (!mShowKeyPreviewPopup) {
788            previewParams.mPreviewVisibleOffset = -keyboard.mVerticalGap;
789            return;
790        }
791
792        final TextView previewText = getKeyPreviewText(tracker.mPointerId);
793        // If the key preview has no parent view yet, add it to the ViewGroup which can place
794        // key preview absolutely in SoftInputWindow.
795        if (previewText.getParent() == null) {
796            addKeyPreview(previewText);
797        }
798
799        mDrawingHandler.cancelDismissKeyPreview(tracker);
800        final Key key = tracker.getKey();
801        // If key is invalid or IME is already closed, we must not show key preview.
802        // Trying to show key preview while root window is closed causes
803        // WindowManager.BadTokenException.
804        if (key == null) {
805            return;
806        }
807
808        final KeyDrawParams drawParams = mKeyDrawParams;
809        previewText.setTextColor(drawParams.mPreviewTextColor);
810        final Drawable background = previewText.getBackground();
811        final String label = key.getPreviewLabel();
812        // What we show as preview should match what we show on a key top in onDraw().
813        if (label != null) {
814            // TODO Should take care of temporaryShiftLabel here.
815            previewText.setCompoundDrawables(null, null, null, null);
816            previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX,
817                    key.selectPreviewTextSize(drawParams));
818            previewText.setTypeface(key.selectPreviewTypeface(drawParams));
819            previewText.setText(label);
820        } else {
821            previewText.setCompoundDrawables(null, null, null,
822                    key.getPreviewIcon(keyboard.mIconsSet));
823            previewText.setText(null);
824        }
825
826        previewText.measure(
827                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
828        final int keyDrawWidth = key.getDrawWidth();
829        final int previewWidth = previewText.getMeasuredWidth();
830        final int previewHeight = mKeyPreviewHeight;
831        // The width and height of visible part of the key preview background. The content marker
832        // of the background 9-patch have to cover the visible part of the background.
833        previewParams.mPreviewVisibleWidth = previewWidth - previewText.getPaddingLeft()
834                - previewText.getPaddingRight();
835        previewParams.mPreviewVisibleHeight = previewHeight - previewText.getPaddingTop()
836                - previewText.getPaddingBottom();
837        // The distance between the top edge of the parent key and the bottom of the visible part
838        // of the key preview background.
839        previewParams.mPreviewVisibleOffset = mKeyPreviewOffset - previewText.getPaddingBottom();
840        getLocationInWindow(mOriginCoords);
841        // The key preview is horizontally aligned with the center of the visible part of the
842        // parent key. If it doesn't fit in this {@link KeyboardView}, it is moved inward to fit and
843        // the left/right background is used if such background is specified.
844        final int statePosition;
845        int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2
846                + CoordinateUtils.x(mOriginCoords);
847        if (previewX < 0) {
848            previewX = 0;
849            statePosition = STATE_LEFT;
850        } else if (previewX > getWidth() - previewWidth) {
851            previewX = getWidth() - previewWidth;
852            statePosition = STATE_RIGHT;
853        } else {
854            statePosition = STATE_MIDDLE;
855        }
856        // The key preview is placed vertically above the top edge of the parent key with an
857        // arbitrary offset.
858        final int previewY = key.mY - previewHeight + mKeyPreviewOffset
859                + CoordinateUtils.y(mOriginCoords);
860
861        if (background != null) {
862            final int hasMoreKeys = (key.mMoreKeys != null) ? STATE_HAS_MOREKEYS : STATE_NORMAL;
863            background.setState(KEY_PREVIEW_BACKGROUND_STATE_TABLE[statePosition][hasMoreKeys]);
864            background.setAlpha(PREVIEW_ALPHA);
865        }
866        ViewLayoutUtils.placeViewAt(
867                previewText, previewX, previewY, previewWidth, previewHeight);
868        previewText.setVisibility(VISIBLE);
869    }
870
871    @Override
872    public void dismissKeyPreview(final PointerTracker tracker) {
873        mDrawingHandler.dismissKeyPreview(mKeyPreviewLingerTimeout, tracker);
874    }
875
876    public void setSlidingKeyInputPreviewEnabled(final boolean enabled) {
877        mSlidingKeyInputPreview.setPreviewEnabled(enabled);
878    }
879
880    @Override
881    public void showSlidingKeyInputPreview(final PointerTracker tracker) {
882        locatePreviewPlacerView();
883        mSlidingKeyInputPreview.setPreviewPosition(tracker);
884    }
885
886    @Override
887    public void dismissSlidingKeyInputPreview() {
888        mSlidingKeyInputPreview.dismissSlidingKeyInputPreview();
889    }
890
891    public void setGesturePreviewMode(final boolean drawsGestureTrail,
892            final boolean drawsGestureFloatingPreviewText) {
893        mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
894        mGestureTrailsPreview.setPreviewEnabled(drawsGestureTrail);
895    }
896
897    public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
898        locatePreviewPlacerView();
899        mGestureFloatingPreviewText.setSuggetedWords(suggestedWords);
900    }
901
902    public void dismissGestureFloatingPreviewText() {
903        locatePreviewPlacerView();
904        mDrawingHandler.dismissGestureFloatingPreviewText(mGestureFloatingPreviewTextLingerTimeout);
905    }
906
907    @Override
908    public void showGestureTrail(final PointerTracker tracker) {
909        locatePreviewPlacerView();
910        mGestureFloatingPreviewText.setPreviewPosition(tracker);
911        mGestureTrailsPreview.setPreviewPosition(tracker);
912    }
913
914    // Note that this method is called from a non-UI thread.
915    public void setMainDictionaryAvailability(final boolean mainDictionaryAvailable) {
916        PointerTracker.setMainDictionaryAvailability(mainDictionaryAvailable);
917    }
918
919    public void setGestureHandlingEnabledByUser(final boolean gestureHandlingEnabledByUser) {
920        PointerTracker.setGestureHandlingEnabledByUser(gestureHandlingEnabledByUser);
921    }
922
923    @Override
924    protected void onAttachedToWindow() {
925        super.onAttachedToWindow();
926        // Notify the ResearchLogger (development only diagnostics) that the keyboard view has
927        // been attached.  This is needed to properly show the splash screen, which requires that
928        // the window token of the KeyboardView be non-null.
929        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
930            ResearchLogger.getInstance().mainKeyboardView_onAttachedToWindow(this);
931        }
932    }
933
934    @Override
935    protected void onDetachedFromWindow() {
936        super.onDetachedFromWindow();
937        mPreviewPlacerView.removeAllViews();
938        // Notify the ResearchLogger (development only diagnostics) that the keyboard view has
939        // been detached.  This is needed to invalidate the reference of {@link MainKeyboardView}
940        // to null.
941        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
942            ResearchLogger.getInstance().mainKeyboardView_onDetachedFromWindow();
943        }
944    }
945
946    private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) {
947        if (key.mMoreKeys == null) {
948            return null;
949        }
950        Keyboard moreKeysKeyboard = mMoreKeysKeyboardCache.get(key);
951        if (moreKeysKeyboard == null) {
952            moreKeysKeyboard = new MoreKeysKeyboard.Builder(
953                    context, key, this, mKeyPreviewDrawParams).build();
954            mMoreKeysKeyboardCache.put(key, moreKeysKeyboard);
955        }
956
957        final View container = mMoreKeysKeyboardContainer;
958        final MoreKeysKeyboardView moreKeysKeyboardView =
959                (MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
960        moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
961        container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
962        return moreKeysKeyboardView;
963    }
964
965    /**
966     * Called when a key is long pressed.
967     * @param tracker the pointer tracker which pressed the parent key
968     */
969    private void onLongPress(final PointerTracker tracker) {
970        if (isShowingMoreKeysPanel()) {
971            return;
972        }
973        final Key key = tracker.getKey();
974        if (key == null) {
975            return;
976        }
977        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
978            ResearchLogger.mainKeyboardView_onLongPress();
979        }
980        final int code = key.mCode;
981        if (key.hasNoPanelAutoMoreKey()) {
982            final int embeddedCode = key.mMoreKeys[0].mCode;
983            tracker.onLongPressed();
984            invokeCodeInput(embeddedCode);
985            invokeReleaseKey(code);
986            AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, this);
987            return;
988        }
989        if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
990            // Long pressing the space key invokes IME switcher dialog.
991            if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
992                tracker.onLongPressed();
993                invokeReleaseKey(code);
994                return;
995            }
996        }
997        openMoreKeysPanel(key, tracker);
998    }
999
1000    private boolean invokeCustomRequest(final int requestCode) {
1001        return mKeyboardActionListener.onCustomRequest(requestCode);
1002    }
1003
1004    private void invokeCodeInput(final int code) {
1005        mKeyboardActionListener.onCodeInput(
1006                code, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
1007    }
1008
1009    private void invokeReleaseKey(final int code) {
1010        mKeyboardActionListener.onReleaseKey(code, false);
1011    }
1012
1013    private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
1014        final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
1015        if (moreKeysPanel == null) {
1016            return;
1017        }
1018
1019        final int[] lastCoords = CoordinateUtils.newInstance();
1020        tracker.getLastCoordinates(lastCoords);
1021        final boolean keyPreviewEnabled = isKeyPreviewPopupEnabled() && !key.noKeyPreview();
1022        // The more keys keyboard is usually horizontally aligned with the center of the parent key.
1023        // If showMoreKeysKeyboardAtTouchedPoint is true and the key preview is disabled, the more
1024        // keys keyboard is placed at the touch point of the parent key.
1025        final int pointX = (mConfigShowMoreKeysKeyboardAtTouchedPoint && !keyPreviewEnabled)
1026                ? CoordinateUtils.x(lastCoords)
1027                : key.mX + key.mWidth / 2;
1028        // The more keys keyboard is usually vertically aligned with the top edge of the parent key
1029        // (plus vertical gap). If the key preview is enabled, the more keys keyboard is vertically
1030        // aligned with the bottom edge of the visible part of the key preview.
1031        // {@code mPreviewVisibleOffset} has been set appropriately in
1032        // {@link KeyboardView#showKeyPreview(PointerTracker)}.
1033        final int pointY = key.mY + mKeyPreviewDrawParams.mPreviewVisibleOffset;
1034        moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
1035        final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords));
1036        final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords));
1037        tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
1038    }
1039
1040    public boolean isInSlidingKeyInput() {
1041        if (isShowingMoreKeysPanel()) {
1042            return true;
1043        }
1044        return PointerTracker.isAnyInSlidingKeyInput();
1045    }
1046
1047    @Override
1048    public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
1049        locatePreviewPlacerView();
1050        if (isShowingMoreKeysPanel()) {
1051            onDismissMoreKeysPanel();
1052        }
1053        mPreviewPlacerView.addView(panel.getContainerView());
1054        mMoreKeysPanel = panel;
1055        dimEntireKeyboard(true /* dimmed */);
1056    }
1057
1058    public boolean isShowingMoreKeysPanel() {
1059        return mMoreKeysPanel != null && mMoreKeysPanel.isShowingInParent();
1060    }
1061
1062    @Override
1063    public void onCancelMoreKeysPanel() {
1064        PointerTracker.dismissAllMoreKeysPanels();
1065    }
1066
1067    @Override
1068    public boolean onDismissMoreKeysPanel() {
1069        dimEntireKeyboard(false /* dimmed */);
1070        if (isShowingMoreKeysPanel()) {
1071            mPreviewPlacerView.removeView(mMoreKeysPanel.getContainerView());
1072            mMoreKeysPanel = null;
1073            return true;
1074        }
1075        return false;
1076    }
1077
1078    @Override
1079    public boolean dispatchTouchEvent(MotionEvent event) {
1080        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
1081            return AccessibleKeyboardViewProxy.getInstance().dispatchTouchEvent(event);
1082        }
1083        return super.dispatchTouchEvent(event);
1084    }
1085
1086    @Override
1087    public boolean onTouchEvent(final MotionEvent me) {
1088        if (getKeyboard() == null) {
1089            return false;
1090        }
1091        return mTouchScreenRegulator.onTouchEvent(me);
1092    }
1093
1094    @Override
1095    public boolean processMotionEvent(final MotionEvent me) {
1096        final boolean nonDistinctMultitouch = !mHasDistinctMultitouch;
1097        final int action = me.getActionMasked();
1098        final int pointerCount = me.getPointerCount();
1099        final int oldPointerCount = mOldPointerCount;
1100        mOldPointerCount = pointerCount;
1101
1102        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1103        // If the device does not have distinct multi-touch support panel, ignore all multi-touch
1104        // events except a transition from/to single-touch.
1105        if (nonDistinctMultitouch && pointerCount > 1 && oldPointerCount > 1) {
1106            return true;
1107        }
1108
1109        final long eventTime = me.getEventTime();
1110        final int index = me.getActionIndex();
1111        final int id = me.getPointerId(index);
1112        final int x = (int)me.getX(index);
1113        final int y = (int)me.getY(index);
1114
1115        // TODO: This might be moved to the tracker.processMotionEvent() call below.
1116        if (ENABLE_USABILITY_STUDY_LOG && action != MotionEvent.ACTION_MOVE) {
1117            writeUsabilityStudyLog(me, action, eventTime, index, id, x, y);
1118        }
1119        // TODO: This should be moved to the tracker.processMotionEvent() call below.
1120        // Currently the same "move" event is being logged twice.
1121        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1122            ResearchLogger.mainKeyboardView_processMotionEvent(
1123                    me, action, eventTime, index, id, x, y);
1124        }
1125
1126        if (mKeyTimerHandler.isInKeyRepeat()) {
1127            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
1128            // Key repeating timer will be canceled if 2 or more keys are in action, and current
1129            // event (UP or DOWN) is non-modifier key.
1130            if (pointerCount > 1 && !tracker.isModifier()) {
1131                mKeyTimerHandler.cancelKeyRepeatTimer();
1132            }
1133            // Up event will pass through.
1134        }
1135
1136        // TODO: cleanup this code into a multi-touch to single-touch event converter class?
1137        // Translate mutli-touch event to single-touch events on the device that has no distinct
1138        // multi-touch panel.
1139        if (nonDistinctMultitouch) {
1140            // Use only main (id=0) pointer tracker.
1141            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
1142            if (pointerCount == 1 && oldPointerCount == 2) {
1143                // Multi-touch to single touch transition.
1144                // Send a down event for the latest pointer if the key is different from the
1145                // previous key.
1146                final Key newKey = tracker.getKeyOn(x, y);
1147                if (mOldKey != newKey) {
1148                    tracker.onDownEvent(x, y, eventTime, this);
1149                    if (action == MotionEvent.ACTION_UP) {
1150                        tracker.onUpEvent(x, y, eventTime);
1151                    }
1152                }
1153            } else if (pointerCount == 2 && oldPointerCount == 1) {
1154                // Single-touch to multi-touch transition.
1155                // Send an up event for the last pointer.
1156                final int[] lastCoords = CoordinateUtils.newInstance();
1157                mOldKey = tracker.getKeyOn(
1158                        CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords));
1159                tracker.onUpEvent(
1160                        CoordinateUtils.x(lastCoords), CoordinateUtils.y(lastCoords), eventTime);
1161            } else if (pointerCount == 1 && oldPointerCount == 1) {
1162                tracker.processMotionEvent(action, x, y, eventTime, this);
1163            } else {
1164                Log.w(TAG, "Unknown touch panel behavior: pointer count is " + pointerCount
1165                        + " (old " + oldPointerCount + ")");
1166            }
1167            return true;
1168        }
1169
1170        if (action == MotionEvent.ACTION_MOVE) {
1171            for (int i = 0; i < pointerCount; i++) {
1172                final int pointerId = me.getPointerId(i);
1173                final PointerTracker tracker = PointerTracker.getPointerTracker(
1174                        pointerId, this);
1175                final int px = (int)me.getX(i);
1176                final int py = (int)me.getY(i);
1177                tracker.onMoveEvent(px, py, eventTime, me);
1178                if (ENABLE_USABILITY_STUDY_LOG) {
1179                    writeUsabilityStudyLog(me, action, eventTime, i, pointerId, px, py);
1180                }
1181                // TODO: This seems to be no longer necessary, and confusing because it leads to
1182                // duplicate MotionEvents being recorded.
1183                // if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
1184                //     ResearchLogger.mainKeyboardView_processMotionEvent(
1185                //             me, action, eventTime, i, pointerId, px, py);
1186                // }
1187            }
1188        } else {
1189            final PointerTracker tracker = PointerTracker.getPointerTracker(id, this);
1190            tracker.processMotionEvent(action, x, y, eventTime, this);
1191        }
1192
1193        return true;
1194    }
1195
1196    private static void writeUsabilityStudyLog(final MotionEvent me, final int action,
1197            final long eventTime, final int index, final int id, final int x, final int y) {
1198        final String eventTag;
1199        switch (action) {
1200        case MotionEvent.ACTION_UP:
1201            eventTag = "[Up]";
1202            break;
1203        case MotionEvent.ACTION_DOWN:
1204            eventTag = "[Down]";
1205            break;
1206        case MotionEvent.ACTION_POINTER_UP:
1207            eventTag = "[PointerUp]";
1208            break;
1209        case MotionEvent.ACTION_POINTER_DOWN:
1210            eventTag = "[PointerDown]";
1211            break;
1212        case MotionEvent.ACTION_MOVE:
1213            eventTag = "[Move]";
1214            break;
1215        default:
1216            eventTag = "[Action" + action + "]";
1217            break;
1218        }
1219        final float size = me.getSize(index);
1220        final float pressure = me.getPressure(index);
1221        UsabilityStudyLogUtils.getInstance().write(
1222                eventTag + eventTime + "," + id + "," + x + "," + y + "," + size + "," + pressure);
1223    }
1224
1225    public void cancelAllMessages() {
1226        mKeyTimerHandler.cancelAllMessages();
1227        mDrawingHandler.cancelAllMessages();
1228    }
1229
1230    public void closing() {
1231        dismissAllKeyPreviews();
1232        cancelAllMessages();
1233        onDismissMoreKeysPanel();
1234        mMoreKeysKeyboardCache.clear();
1235    }
1236
1237    /**
1238     * Receives hover events from the input framework.
1239     *
1240     * @param event The motion event to be dispatched.
1241     * @return {@code true} if the event was handled by the view, {@code false}
1242     *         otherwise
1243     */
1244    @Override
1245    public boolean dispatchHoverEvent(final MotionEvent event) {
1246        if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
1247            final PointerTracker tracker = PointerTracker.getPointerTracker(0, this);
1248            return AccessibleKeyboardViewProxy.getInstance().dispatchHoverEvent(event, tracker);
1249        }
1250
1251        // Reflection doesn't support calling superclass methods.
1252        return false;
1253    }
1254
1255    public void updateShortcutKey(final boolean available) {
1256        final Keyboard keyboard = getKeyboard();
1257        if (keyboard == null) {
1258            return;
1259        }
1260        final Key shortcutKey = keyboard.getKey(Constants.CODE_SHORTCUT);
1261        if (shortcutKey == null) {
1262            return;
1263        }
1264        shortcutKey.setEnabled(available);
1265        invalidateKey(shortcutKey);
1266    }
1267
1268    public void startDisplayLanguageOnSpacebar(final boolean subtypeChanged,
1269            final boolean needsToDisplayLanguage, final boolean hasMultipleEnabledIMEsOrSubtypes) {
1270        mNeedsToDisplayLanguage = needsToDisplayLanguage;
1271        mHasMultipleEnabledIMEsOrSubtypes = hasMultipleEnabledIMEsOrSubtypes;
1272        final ObjectAnimator animator = mLanguageOnSpacebarFadeoutAnimator;
1273        if (animator == null) {
1274            mNeedsToDisplayLanguage = false;
1275        } else {
1276            if (subtypeChanged && needsToDisplayLanguage) {
1277                setLanguageOnSpacebarAnimAlpha(Constants.Color.ALPHA_OPAQUE);
1278                if (animator.isStarted()) {
1279                    animator.cancel();
1280                }
1281                animator.start();
1282            } else {
1283                if (!animator.isStarted()) {
1284                    mLanguageOnSpacebarAnimAlpha = mLanguageOnSpacebarFinalAlpha;
1285                }
1286            }
1287        }
1288        invalidateKey(mSpaceKey);
1289    }
1290
1291    public void updateAutoCorrectionState(final boolean isAutoCorrection) {
1292        if (!mAutoCorrectionSpacebarLedEnabled) {
1293            return;
1294        }
1295        mAutoCorrectionSpacebarLedOn = isAutoCorrection;
1296        invalidateKey(mSpaceKey);
1297    }
1298
1299    private void dimEntireKeyboard(final boolean dimmed) {
1300        final boolean needsRedrawing = mNeedsToDimEntireKeyboard != dimmed;
1301        mNeedsToDimEntireKeyboard = dimmed;
1302        if (needsRedrawing) {
1303            invalidateAllKeys();
1304        }
1305    }
1306
1307    @Override
1308    protected void onDraw(final Canvas canvas) {
1309        super.onDraw(canvas);
1310
1311        // Overlay a dark rectangle to dim.
1312        if (mNeedsToDimEntireKeyboard) {
1313            canvas.drawRect(0.0f, 0.0f, getWidth(), getHeight(), mBackgroundDimAlphaPaint);
1314        }
1315    }
1316
1317    @Override
1318    protected void onDrawKeyTopVisuals(final Key key, final Canvas canvas, final Paint paint,
1319            final KeyDrawParams params) {
1320        if (key.altCodeWhileTyping() && key.isEnabled()) {
1321            params.mAnimAlpha = mAltCodeKeyWhileTypingAnimAlpha;
1322        }
1323        if (key.mCode == Constants.CODE_SPACE) {
1324            drawSpacebar(key, canvas, paint);
1325            // Whether space key needs to show the "..." popup hint for special purposes
1326            if (key.isLongPressEnabled() && mHasMultipleEnabledIMEsOrSubtypes) {
1327                drawKeyPopupHint(key, canvas, paint, params);
1328            }
1329        } else if (key.mCode == Constants.CODE_LANGUAGE_SWITCH) {
1330            super.onDrawKeyTopVisuals(key, canvas, paint, params);
1331            drawKeyPopupHint(key, canvas, paint, params);
1332        } else {
1333            super.onDrawKeyTopVisuals(key, canvas, paint, params);
1334        }
1335    }
1336
1337    private static boolean fitsTextIntoWidth(final int width, final String text,
1338            final Paint paint) {
1339        paint.setTextScaleX(1.0f);
1340        final float textWidth = TypefaceUtils.getLabelWidth(text, paint);
1341        if (textWidth < width) {
1342            return true;
1343        }
1344
1345        final float scaleX = width / textWidth;
1346        if (scaleX < MINIMUM_XSCALE_OF_LANGUAGE_NAME) {
1347            return false;
1348        }
1349
1350        paint.setTextScaleX(scaleX);
1351        return TypefaceUtils.getLabelWidth(text, paint) < width;
1352    }
1353
1354    // Layout language name on spacebar.
1355    private static String layoutLanguageOnSpacebar(final Paint paint,
1356            final InputMethodSubtype subtype, final int width) {
1357        // Choose appropriate language name to fit into the width.
1358        final String fullText = getFullDisplayName(subtype);
1359        if (fitsTextIntoWidth(width, fullText, paint)) {
1360            return fullText;
1361        }
1362
1363        final String middleText = getMiddleDisplayName(subtype);
1364        if (fitsTextIntoWidth(width, middleText, paint)) {
1365            return middleText;
1366        }
1367
1368        final String shortText = getShortDisplayName(subtype);
1369        if (fitsTextIntoWidth(width, shortText, paint)) {
1370            return shortText;
1371        }
1372
1373        return "";
1374    }
1375
1376    private void drawSpacebar(final Key key, final Canvas canvas, final Paint paint) {
1377        final int width = key.mWidth;
1378        final int height = key.mHeight;
1379
1380        // If input language are explicitly selected.
1381        if (mNeedsToDisplayLanguage) {
1382            paint.setTextAlign(Align.CENTER);
1383            paint.setTypeface(Typeface.DEFAULT);
1384            paint.setTextSize(mSpacebarTextSize);
1385            final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
1386            final String language = layoutLanguageOnSpacebar(paint, subtype, width);
1387            // Draw language text with shadow
1388            final float descent = paint.descent();
1389            final float textHeight = -paint.ascent() + descent;
1390            final float baseline = height / 2 + textHeight / 2;
1391            paint.setColor(mSpacebarTextShadowColor);
1392            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
1393            canvas.drawText(language, width / 2, baseline - descent - 1, paint);
1394            paint.setColor(mSpacebarTextColor);
1395            paint.setAlpha(mLanguageOnSpacebarAnimAlpha);
1396            canvas.drawText(language, width / 2, baseline - descent, paint);
1397        }
1398
1399        // Draw the spacebar icon at the bottom
1400        if (mAutoCorrectionSpacebarLedOn) {
1401            final int iconWidth = width * SPACE_LED_LENGTH_PERCENT / 100;
1402            final int iconHeight = mAutoCorrectionSpacebarLedIcon.getIntrinsicHeight();
1403            int x = (width - iconWidth) / 2;
1404            int y = height - iconHeight;
1405            drawIcon(canvas, mAutoCorrectionSpacebarLedIcon, x, y, iconWidth, iconHeight);
1406        } else if (mSpaceIcon != null) {
1407            final int iconWidth = mSpaceIcon.getIntrinsicWidth();
1408            final int iconHeight = mSpaceIcon.getIntrinsicHeight();
1409            int x = (width - iconWidth) / 2;
1410            int y = height - iconHeight;
1411            drawIcon(canvas, mSpaceIcon, x, y, iconWidth, iconHeight);
1412        }
1413    }
1414
1415    // InputMethodSubtype's display name for spacebar text in its locale.
1416    //        isAdditionalSubtype (T=true, F=false)
1417    // locale layout  | Short  Middle      Full
1418    // ------ ------- - ---- --------- ----------------------
1419    //  en_US qwerty  F  En  English   English (US)           exception
1420    //  en_GB qwerty  F  En  English   English (UK)           exception
1421    //  es_US spanish F  Es  Español   Español (EE.UU.)       exception
1422    //  fr    azerty  F  Fr  Français  Français
1423    //  fr_CA qwerty  F  Fr  Français  Français (Canada)
1424    //  de    qwertz  F  De  Deutsch   Deutsch
1425    //  zz    qwerty  F      QWERTY    QWERTY
1426    //  fr    qwertz  T  Fr  Français  Français
1427    //  de    qwerty  T  De  Deutsch   Deutsch
1428    //  en_US azerty  T  En  English   English (US)
1429    //  zz    azerty  T      AZERTY    AZERTY
1430
1431    // Get InputMethodSubtype's full display name in its locale.
1432    static String getFullDisplayName(final InputMethodSubtype subtype) {
1433        if (SubtypeLocale.isNoLanguage(subtype)) {
1434            return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
1435        }
1436        return SubtypeLocale.getSubtypeLocaleDisplayName(subtype.getLocale());
1437    }
1438
1439    // Get InputMethodSubtype's short display name in its locale.
1440    static String getShortDisplayName(final InputMethodSubtype subtype) {
1441        if (SubtypeLocale.isNoLanguage(subtype)) {
1442            return "";
1443        }
1444        final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1445        return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale);
1446    }
1447
1448    // Get InputMethodSubtype's middle display name in its locale.
1449    static String getMiddleDisplayName(final InputMethodSubtype subtype) {
1450        if (SubtypeLocale.isNoLanguage(subtype)) {
1451            return SubtypeLocale.getKeyboardLayoutSetDisplayName(subtype);
1452        }
1453        final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
1454        return SubtypeLocale.getSubtypeLocaleDisplayName(locale.getLanguage());
1455    }
1456}
1457