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