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