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