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