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