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