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