KeyboardSwitcher.java revision ec52fb6dbb0794d9872bb3e83ea166c6996acf4e
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.inputmethod.keyboard; 18 19import android.content.Context; 20import android.content.SharedPreferences; 21import android.content.res.Configuration; 22import android.content.res.Resources; 23import android.text.TextUtils; 24import android.util.DisplayMetrics; 25import android.util.Log; 26import android.view.ContextThemeWrapper; 27import android.view.InflateException; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.inputmethod.EditorInfo; 31 32import com.android.inputmethod.accessibility.AccessibleKeyboardViewProxy; 33import com.android.inputmethod.keyboard.internal.KeyboardState; 34import com.android.inputmethod.latin.InputView; 35import com.android.inputmethod.latin.LatinIME; 36import com.android.inputmethod.latin.LatinImeLogger; 37import com.android.inputmethod.latin.LocaleUtils; 38import com.android.inputmethod.latin.R; 39import com.android.inputmethod.latin.Settings; 40import com.android.inputmethod.latin.SubtypeSwitcher; 41import com.android.inputmethod.latin.Utils; 42 43import java.lang.ref.SoftReference; 44import java.util.HashMap; 45import java.util.Locale; 46 47public class KeyboardSwitcher implements SharedPreferences.OnSharedPreferenceChangeListener { 48 private static final String TAG = KeyboardSwitcher.class.getSimpleName(); 49 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 50 public static final boolean DEBUG_STATE = false; 51 52 public static final String PREF_KEYBOARD_LAYOUT = "pref_keyboard_layout_20110916"; 53 private static final int[] KEYBOARD_THEMES = { 54 R.style.KeyboardTheme, 55 R.style.KeyboardTheme_HighContrast, 56 R.style.KeyboardTheme_Stone, 57 R.style.KeyboardTheme_Stone_Bold, 58 R.style.KeyboardTheme_Gingerbread, 59 R.style.KeyboardTheme_IceCreamSandwich, 60 }; 61 62 private SubtypeSwitcher mSubtypeSwitcher; 63 private SharedPreferences mPrefs; 64 65 private InputView mCurrentInputView; 66 private LatinKeyboardView mKeyboardView; 67 private LatinIME mInputMethodService; 68 private String mPackageName; 69 private Resources mResources; 70 71 private KeyboardState mState; 72 private static final int UNSHIFT = 0; 73 private static final int MANUAL_SHIFT = 1; 74 private static final int AUTOMATIC_SHIFT = 2; 75 76 private KeyboardId mMainKeyboardId; 77 private KeyboardId mSymbolsKeyboardId; 78 private KeyboardId mSymbolsShiftedKeyboardId; 79 80 private KeyboardId mCurrentId; 81 private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache = 82 new HashMap<KeyboardId, SoftReference<LatinKeyboard>>(); 83 84 private KeyboardLayoutState mSavedKeyboardState = new KeyboardLayoutState(); 85 86 /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of 87 * what user actually typed. */ 88 private boolean mIsAutoCorrectionActive; 89 90 // TODO: Encapsulate these state handling to separate class and combine with ShiftKeyState 91 // and ModifierKeyState into KeyboardState. 92 private static final int SWITCH_STATE_ALPHA = 0; 93 private static final int SWITCH_STATE_SYMBOL_BEGIN = 1; 94 private static final int SWITCH_STATE_SYMBOL = 2; 95 // The following states are used only on the distinct multi-touch panel devices. 96 private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3; 97 private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4; 98 private static final int SWITCH_STATE_CHORDING_ALPHA = 5; 99 private static final int SWITCH_STATE_CHORDING_SYMBOL = 6; 100 private int mSwitchState = SWITCH_STATE_ALPHA; 101 102 private static String mLayoutSwitchBackSymbols; 103 104 private int mThemeIndex = -1; 105 private Context mThemeContext; 106 107 private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); 108 109 private class KeyboardLayoutState { 110 private boolean mIsValid; 111 private boolean mIsAlphabetMode; 112 private boolean mIsShiftLocked; 113 private boolean mIsShifted; 114 115 public void save() { 116 if (mCurrentId == null) { 117 return; 118 } 119 mIsAlphabetMode = isAlphabetMode(); 120 if (mIsAlphabetMode) { 121 mIsShiftLocked = isShiftLocked(); 122 mIsShifted = !mIsShiftLocked && isShiftedOrShiftLocked(); 123 } else { 124 mIsShiftLocked = false; 125 mIsShifted = mCurrentId.equals(mSymbolsShiftedKeyboardId); 126 } 127 mIsValid = true; 128 } 129 130 public KeyboardId getKeyboardId() { 131 if (!mIsValid) return mMainKeyboardId; 132 133 if (mIsAlphabetMode) { 134 return mMainKeyboardId; 135 } else { 136 return mIsShifted ? mSymbolsShiftedKeyboardId : mSymbolsKeyboardId; 137 } 138 } 139 140 public void restore() { 141 if (!mIsValid) return; 142 mIsValid = false; 143 144 if (mIsAlphabetMode) { 145 final boolean isAlphabetMode = isAlphabetMode(); 146 final boolean isShiftLocked = isAlphabetMode && isShiftLocked(); 147 final boolean isShifted = !isShiftLocked && isShiftedOrShiftLocked(); 148 if (mIsShiftLocked != isShiftLocked) { 149 toggleCapsLock(); 150 } else if (mIsShifted != isShifted) { 151 onPressShift(false); 152 onReleaseShift(false); 153 } 154 } 155 } 156 } 157 158 public static KeyboardSwitcher getInstance() { 159 return sInstance; 160 } 161 162 private KeyboardSwitcher() { 163 // Intentional empty constructor for singleton. 164 } 165 166 public static void init(LatinIME ims, SharedPreferences prefs) { 167 sInstance.initInternal(ims, prefs); 168 } 169 170 private void initInternal(LatinIME ims, SharedPreferences prefs) { 171 mInputMethodService = ims; 172 mPackageName = ims.getPackageName(); 173 mResources = ims.getResources(); 174 mPrefs = prefs; 175 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 176 mState = new KeyboardState(); 177 setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs)); 178 prefs.registerOnSharedPreferenceChangeListener(this); 179 } 180 181 private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) { 182 final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id); 183 final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId); 184 try { 185 final int themeIndex = Integer.valueOf(themeId); 186 if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length) 187 return themeIndex; 188 } catch (NumberFormatException e) { 189 // Format error, keyboard theme is default to 0. 190 } 191 Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0"); 192 return 0; 193 } 194 195 private void setContextThemeWrapper(Context context, int themeIndex) { 196 if (mThemeIndex != themeIndex) { 197 mThemeIndex = themeIndex; 198 mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]); 199 mKeyboardCache.clear(); 200 } 201 } 202 203 public void loadKeyboard(EditorInfo editorInfo, Settings.Values settingsValues) { 204 try { 205 mMainKeyboardId = getKeyboardId(editorInfo, false, false, settingsValues); 206 mSymbolsKeyboardId = getKeyboardId(editorInfo, true, false, settingsValues); 207 mSymbolsShiftedKeyboardId = getKeyboardId(editorInfo, true, true, settingsValues); 208 mLayoutSwitchBackSymbols = mResources.getString(R.string.layout_switch_back_symbols); 209 setKeyboard(getKeyboard(mSavedKeyboardState.getKeyboardId())); 210 mSavedKeyboardState.restore(); 211 } catch (RuntimeException e) { 212 Log.w(TAG, "loading keyboard failed: " + mMainKeyboardId, e); 213 LatinImeLogger.logOnException(mMainKeyboardId.toString(), e); 214 } 215 } 216 217 public void saveKeyboardState() { 218 mSavedKeyboardState.save(); 219 } 220 221 public void onFinishInputView() { 222 mIsAutoCorrectionActive = false; 223 } 224 225 public void onHideWindow() { 226 mIsAutoCorrectionActive = false; 227 } 228 229 private void setKeyboard(final Keyboard keyboard) { 230 final Keyboard oldKeyboard = mKeyboardView.getKeyboard(); 231 mKeyboardView.setKeyboard(keyboard); 232 mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding); 233 mCurrentId = keyboard.mId; 234 mSwitchState = getSwitchState(mCurrentId); 235 updateShiftLockState(keyboard); 236 mKeyboardView.setKeyPreviewPopupEnabled( 237 Settings.Values.isKeyPreviewPopupEnabled(mPrefs, mResources), 238 Settings.Values.getKeyPreviewPopupDismissDelay(mPrefs, mResources)); 239 final boolean localeChanged = (oldKeyboard == null) 240 || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); 241 mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged); 242 updateShiftState(); 243 } 244 245 private int getSwitchState(KeyboardId id) { 246 return id.equals(mMainKeyboardId) ? SWITCH_STATE_ALPHA : SWITCH_STATE_SYMBOL_BEGIN; 247 } 248 249 private void updateShiftLockState(Keyboard keyboard) { 250 if (mCurrentId.equals(mSymbolsShiftedKeyboardId)) { 251 // Symbol keyboard may have an ALT key that has a caps lock style indicator (a.k.a. 252 // sticky shift key). To show or dismiss the indicator, we need to call setShiftLocked() 253 // that takes care of the current keyboard having such ALT key or not. 254 keyboard.setShiftLocked(keyboard.hasShiftLockKey()); 255 } else if (mCurrentId.equals(mSymbolsKeyboardId)) { 256 // Symbol keyboard has an ALT key that has a caps lock style indicator. To disable the 257 // indicator, we need to call setShiftLocked(false). 258 keyboard.setShiftLocked(false); 259 } 260 } 261 262 private LatinKeyboard getKeyboard(KeyboardId id) { 263 final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id); 264 LatinKeyboard keyboard = (ref == null) ? null : ref.get(); 265 if (keyboard == null) { 266 final Locale savedLocale = LocaleUtils.setSystemLocale(mResources, id.mLocale); 267 try { 268 final LatinKeyboard.Builder builder = new LatinKeyboard.Builder(mThemeContext); 269 builder.load(id); 270 builder.setTouchPositionCorrectionEnabled( 271 mSubtypeSwitcher.currentSubtypeContainsExtraValueKey( 272 LatinIME.SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION)); 273 keyboard = builder.build(); 274 } finally { 275 LocaleUtils.setSystemLocale(mResources, savedLocale); 276 } 277 mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard)); 278 279 if (DEBUG_CACHE) { 280 Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": " 281 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id 282 + " theme=" + Keyboard.themeName(keyboard.mThemeId)); 283 } 284 } else if (DEBUG_CACHE) { 285 Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT id=" + id 286 + " theme=" + Keyboard.themeName(keyboard.mThemeId)); 287 } 288 289 keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive); 290 keyboard.setShiftLocked(false); 291 keyboard.setShifted(false); 292 // If the cached keyboard had been switched to another keyboard while the language was 293 // displayed on its spacebar, it might have had arbitrary text fade factor. In such case, 294 // we should reset the text fade factor. It is also applicable to shortcut key. 295 keyboard.setSpacebarTextFadeFactor(0.0f, null); 296 keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null); 297 return keyboard; 298 } 299 300 private KeyboardId getKeyboardId(EditorInfo editorInfo, final boolean isSymbols, 301 final boolean isShift, Settings.Values settingsValues) { 302 final int mode = Utils.getKeyboardMode(editorInfo); 303 final int xmlId; 304 switch (mode) { 305 case KeyboardId.MODE_PHONE: 306 xmlId = (isSymbols && isShift) ? R.xml.kbd_phone_shift : R.xml.kbd_phone; 307 break; 308 case KeyboardId.MODE_NUMBER: 309 xmlId = R.xml.kbd_number; 310 break; 311 default: 312 if (isSymbols) { 313 xmlId = isShift ? R.xml.kbd_symbols_shift : R.xml.kbd_symbols; 314 } else { 315 xmlId = R.xml.kbd_qwerty; 316 } 317 break; 318 } 319 320 final boolean settingsKeyEnabled = settingsValues.isSettingsKeyEnabled(); 321 @SuppressWarnings("deprecation") 322 final boolean noMicrophone = Utils.inPrivateImeOptions( 323 mPackageName, LatinIME.IME_OPTION_NO_MICROPHONE, editorInfo) 324 || Utils.inPrivateImeOptions( 325 null, LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo); 326 final boolean voiceKeyEnabled = settingsValues.isVoiceKeyEnabled(editorInfo) 327 && !noMicrophone; 328 final boolean voiceKeyOnMain = settingsValues.isVoiceKeyOnMain(); 329 final boolean noSettingsKey = Utils.inPrivateImeOptions( 330 mPackageName, LatinIME.IME_OPTION_NO_SETTINGS_KEY, editorInfo); 331 final boolean hasSettingsKey = settingsKeyEnabled && !noSettingsKey; 332 final int f2KeyMode = getF2KeyMode(settingsKeyEnabled, noSettingsKey); 333 final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != voiceKeyOnMain); 334 final boolean forceAscii = Utils.inPrivateImeOptions( 335 mPackageName, LatinIME.IME_OPTION_FORCE_ASCII, editorInfo); 336 final boolean asciiCapable = mSubtypeSwitcher.currentSubtypeContainsExtraValueKey( 337 LatinIME.SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE); 338 final Locale locale = (forceAscii && !asciiCapable) 339 ? Locale.US : mSubtypeSwitcher.getInputLocale(); 340 final Configuration conf = mResources.getConfiguration(); 341 final DisplayMetrics dm = mResources.getDisplayMetrics(); 342 343 return new KeyboardId( 344 mResources.getResourceEntryName(xmlId), xmlId, locale, conf.orientation, 345 dm.widthPixels, mode, editorInfo, hasSettingsKey, f2KeyMode, noSettingsKey, 346 voiceKeyEnabled, hasShortcutKey); 347 } 348 349 public int getKeyboardMode() { 350 return mCurrentId != null ? mCurrentId.mMode : KeyboardId.MODE_TEXT; 351 } 352 353 public boolean isAlphabetMode() { 354 return mCurrentId != null && mCurrentId.isAlphabetKeyboard(); 355 } 356 357 public boolean isInputViewShown() { 358 return mCurrentInputView != null && mCurrentInputView.isShown(); 359 } 360 361 public boolean isKeyboardAvailable() { 362 if (mKeyboardView != null) 363 return mKeyboardView.getKeyboard() != null; 364 return false; 365 } 366 367 public LatinKeyboard getLatinKeyboard() { 368 if (mKeyboardView != null) { 369 final Keyboard keyboard = mKeyboardView.getKeyboard(); 370 if (keyboard instanceof LatinKeyboard) 371 return (LatinKeyboard)keyboard; 372 } 373 return null; 374 } 375 376 public boolean isShiftedOrShiftLocked() { 377 LatinKeyboard latinKeyboard = getLatinKeyboard(); 378 if (latinKeyboard != null) 379 return latinKeyboard.isShiftedOrShiftLocked(); 380 return false; 381 } 382 383 public boolean isShiftLocked() { 384 LatinKeyboard latinKeyboard = getLatinKeyboard(); 385 if (latinKeyboard != null) 386 return latinKeyboard.isShiftLocked(); 387 return false; 388 } 389 390 private boolean isShiftLockShifted() { 391 LatinKeyboard latinKeyboard = getLatinKeyboard(); 392 if (latinKeyboard != null) 393 return latinKeyboard.isShiftLockShifted(); 394 return false; 395 } 396 397 private boolean isAutomaticTemporaryUpperCase() { 398 LatinKeyboard latinKeyboard = getLatinKeyboard(); 399 if (latinKeyboard != null) 400 return latinKeyboard.isAutomaticTemporaryUpperCase(); 401 return false; 402 } 403 404 public boolean isManualTemporaryUpperCase() { 405 LatinKeyboard latinKeyboard = getLatinKeyboard(); 406 if (latinKeyboard != null) 407 return latinKeyboard.isManualTemporaryUpperCase(); 408 return false; 409 } 410 411 private boolean isManualTemporaryUpperCaseFromAuto() { 412 LatinKeyboard latinKeyboard = getLatinKeyboard(); 413 if (latinKeyboard != null) 414 return latinKeyboard.isManualTemporaryUpperCaseFromAuto(); 415 return false; 416 } 417 418 private void setShift(int shiftMode) { 419 LatinKeyboard latinKeyboard = getLatinKeyboard(); 420 if (latinKeyboard == null) 421 return; 422 if (shiftMode == AUTOMATIC_SHIFT) { 423 latinKeyboard.setAutomaticTemporaryUpperCase(); 424 mKeyboardView.invalidateAllKeys(); 425 } else { 426 final boolean shifted = (shiftMode == MANUAL_SHIFT); 427 // On non-distinct multi touch panel device, we should also turn off the shift locked 428 // state when shift key is pressed to go to normal mode. 429 // On the other hand, on distinct multi touch panel device, turning off the shift 430 // locked state with shift key pressing is handled by onReleaseShift(). 431 if (!hasDistinctMultitouch() && !shifted && latinKeyboard.isShiftLocked()) { 432 latinKeyboard.setShiftLocked(false); 433 } 434 if (latinKeyboard.setShifted(shifted)) { 435 mKeyboardView.invalidateAllKeys(); 436 } 437 } 438 } 439 440 private void setShiftLocked(boolean shiftLocked) { 441 LatinKeyboard latinKeyboard = getLatinKeyboard(); 442 if (latinKeyboard != null && latinKeyboard.setShiftLocked(shiftLocked)) { 443 mKeyboardView.invalidateAllKeys(); 444 } 445 } 446 447 /** 448 * Toggle keyboard shift state triggered by user touch event. 449 */ 450 public void toggleShift() { 451 mInputMethodService.mHandler.cancelUpdateShiftState(); 452 if (DEBUG_STATE) { 453 Log.d(TAG, "toggleShift:" 454 + " keyboard=" + getLatinKeyboard().mShiftState 455 + " state=" + mState); 456 } 457 if (isAlphabetMode()) { 458 setShift(isShiftedOrShiftLocked() ? UNSHIFT : MANUAL_SHIFT); 459 } else { 460 toggleShiftInSymbol(); 461 } 462 } 463 464 public void toggleCapsLock() { 465 mInputMethodService.mHandler.cancelUpdateShiftState(); 466 if (DEBUG_STATE) { 467 Log.d(TAG, "toggleCapsLock:" 468 + " keyboard=" + getLatinKeyboard().mShiftState 469 + " state=" + mState); 470 } 471 if (isAlphabetMode()) { 472 if (isShiftLocked()) { 473 // Shift key is long pressed while caps lock state, we will toggle back to normal 474 // state. And mark as if shift key is released. 475 setShiftLocked(false); 476 mState.onToggleCapsLock(); 477 } else { 478 setShiftLocked(true); 479 } 480 } 481 } 482 483 public void changeKeyboardMode() { 484 if (DEBUG_STATE) { 485 Log.d(TAG, "changeKeyboardMode:" 486 + " keyboard=" + getLatinKeyboard().mShiftState 487 + " state=" + mState); 488 } 489 toggleKeyboardMode(); 490 if (isShiftLocked() && isAlphabetMode()) { 491 setShiftLocked(true); 492 } 493 updateShiftState(); 494 } 495 496 private void startIgnoringDoubleTap() { 497 if (mKeyboardView != null) { 498 mKeyboardView.startIgnoringDoubleTap(); 499 } 500 } 501 502 /** 503 * Update keyboard shift state triggered by connected EditText status change. 504 */ 505 public void updateShiftState() { 506 final boolean isAlphabetMode = isAlphabetMode(); 507 final boolean isShiftLocked = isShiftLocked(); 508 if (DEBUG_STATE) { 509 Log.d(TAG, "updateShiftState:" 510 + " autoCaps=" + mInputMethodService.getCurrentAutoCapsState() 511 + " keyboard=" + getLatinKeyboard().mShiftState 512 + " isAlphabetMode=" + isAlphabetMode 513 + " isShiftLocked=" + isShiftLocked 514 + " state=" + mState); 515 } 516 if (isAlphabetMode) { 517 if (!isShiftLocked && !mState.isShiftKeyIgnoring()) { 518 if (mState.isShiftKeyReleasing() && mInputMethodService.getCurrentAutoCapsState()) { 519 // Only when shift key is releasing, automatic temporary upper case will be set. 520 setShift(AUTOMATIC_SHIFT); 521 } else { 522 setShift(mState.isShiftKeyMomentary() ? MANUAL_SHIFT : UNSHIFT); 523 } 524 } 525 } 526 mState.onUpdateShiftState(isAlphabetMode); 527 } 528 529 public void onPressShift(boolean withSliding) { 530 if (!isKeyboardAvailable()) 531 return; 532 if (DEBUG_STATE) { 533 Log.d(TAG, "onPressShift:" 534 + " keyboard=" + getLatinKeyboard().mShiftState 535 + " state=" + mState + " sliding=" + withSliding); 536 } 537 final boolean isAlphabetMode = isAlphabetMode(); 538 final boolean isShiftLocked = isShiftLocked(); 539 final boolean isAutomaticTemporaryUpperCase = isAutomaticTemporaryUpperCase(); 540 final boolean isShiftedOrShiftLocked = isShiftedOrShiftLocked(); 541 if (isAlphabetMode) { 542 if (isShiftLocked) { 543 // Shift key is pressed while caps lock state, we will treat this state as shifted 544 // caps lock state and mark as if shift key pressed while normal state. 545 setShift(MANUAL_SHIFT); 546 } else if (isAutomaticTemporaryUpperCase) { 547 // Shift key is pressed while automatic temporary upper case, we have to move to 548 // manual temporary upper case. 549 setShift(MANUAL_SHIFT); 550 } else if (isShiftedOrShiftLocked) { 551 // In manual upper case state, we just record shift key has been pressing while 552 // shifted state. 553 } else { 554 // In base layout, chording or manual temporary upper case mode is started. 555 toggleShift(); 556 } 557 } else { 558 // In symbol mode, just toggle symbol and symbol more keyboard. 559 toggleShift(); 560 mSwitchState = SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE; 561 } 562 mState.onPressShift(isAlphabetMode, isShiftLocked, isAutomaticTemporaryUpperCase, 563 isShiftedOrShiftLocked); 564 } 565 566 public void onReleaseShift(boolean withSliding) { 567 if (!isKeyboardAvailable()) 568 return; 569 if (DEBUG_STATE) { 570 Log.d(TAG, "onReleaseShift:" 571 + " keyboard=" + getLatinKeyboard().mShiftState 572 + " state=" + mState + " sliding=" + withSliding); 573 } 574 final boolean isAlphabetMode = isAlphabetMode(); 575 final boolean isShiftLocked = isShiftLocked(); 576 final boolean isShiftLockShifted = isShiftLockShifted(); 577 final boolean isShiftedOrShiftLocked = isShiftedOrShiftLocked(); 578 final boolean isManualTemporaryUpperCaseFromAuto = isManualTemporaryUpperCaseFromAuto(); 579 if (isAlphabetMode) { 580 if (mState.isShiftKeyMomentary()) { 581 // After chording input while normal state. 582 toggleShift(); 583 } else if (isShiftLocked && !isShiftLockShifted && (mState.isShiftKeyPressing() 584 || mState.isShiftKeyPressingOnShifted()) && !withSliding) { 585 // Shift has been long pressed, ignore this release. 586 } else if (isShiftLocked && !mState.isShiftKeyIgnoring() && !withSliding) { 587 // Shift has been pressed without chording while caps lock state. 588 toggleCapsLock(); 589 // To be able to turn off caps lock by "double tap" on shift key, we should ignore 590 // the second tap of the "double tap" from now for a while because we just have 591 // already turned off caps lock above. 592 startIgnoringDoubleTap(); 593 } else if (isShiftedOrShiftLocked && mState.isShiftKeyPressingOnShifted() 594 && !withSliding) { 595 // Shift has been pressed without chording while shifted state. 596 toggleShift(); 597 } else if (isManualTemporaryUpperCaseFromAuto && mState.isShiftKeyPressing() 598 && !withSliding) { 599 // Shift has been pressed without chording while manual temporary upper case 600 // transited from automatic temporary upper case. 601 toggleShift(); 602 } 603 } else { 604 // In symbol mode, snap back to the previous keyboard mode if the user chords the shift 605 // key and another key, then releases the shift key. 606 if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) { 607 toggleShift(); 608 } 609 } 610 mState.onReleaseShift(); 611 } 612 613 public void onPressSymbol() { 614 if (DEBUG_STATE) { 615 Log.d(TAG, "onPressSymbol:" 616 + " keyboard=" + getLatinKeyboard().mShiftState 617 + " state=" + mState); 618 } 619 changeKeyboardMode(); 620 mState.onPressSymbol(); 621 mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL; 622 } 623 624 public void onReleaseSymbol() { 625 if (DEBUG_STATE) { 626 Log.d(TAG, "onReleaseSymbol:" 627 + " keyboard=" + getLatinKeyboard().mShiftState 628 + " state=" + mState); 629 } 630 // Snap back to the previous keyboard mode if the user chords the mode change key and 631 // another key, then releases the mode change key. 632 if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) { 633 changeKeyboardMode(); 634 } 635 mState.onReleaseSymbol(); 636 } 637 638 public void onOtherKeyPressed() { 639 if (DEBUG_STATE) { 640 Log.d(TAG, "onOtherKeyPressed:" 641 + " keyboard=" + getLatinKeyboard().mShiftState 642 + " state=" + mState); 643 } 644 mState.onOtherKeyPressed(); 645 } 646 647 public void onCancelInput() { 648 // Snap back to the previous keyboard mode if the user cancels sliding input. 649 if (isSinglePointer()) { 650 if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) { 651 changeKeyboardMode(); 652 } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) { 653 toggleShift(); 654 } 655 } 656 } 657 658 private boolean mPrevMainKeyboardWasShiftLocked; 659 660 private void toggleKeyboardMode() { 661 if (mCurrentId.equals(mMainKeyboardId)) { 662 mPrevMainKeyboardWasShiftLocked = isShiftLocked(); 663 setKeyboard(getKeyboard(mSymbolsKeyboardId)); 664 } else { 665 setKeyboard(getKeyboard(mMainKeyboardId)); 666 setShiftLocked(mPrevMainKeyboardWasShiftLocked); 667 mPrevMainKeyboardWasShiftLocked = false; 668 } 669 } 670 671 private void toggleShiftInSymbol() { 672 if (isAlphabetMode()) 673 return; 674 final LatinKeyboard keyboard; 675 if (mCurrentId.equals(mSymbolsKeyboardId) 676 || !mCurrentId.equals(mSymbolsShiftedKeyboardId)) { 677 keyboard = getKeyboard(mSymbolsShiftedKeyboardId); 678 } else { 679 keyboard = getKeyboard(mSymbolsKeyboardId); 680 } 681 setKeyboard(keyboard); 682 } 683 684 public boolean isInMomentarySwitchState() { 685 return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL 686 || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE; 687 } 688 689 public boolean isVibrateAndSoundFeedbackRequired() { 690 return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput(); 691 } 692 693 private boolean isSinglePointer() { 694 return mKeyboardView != null && mKeyboardView.getPointerCount() == 1; 695 } 696 697 public boolean hasDistinctMultitouch() { 698 return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch(); 699 } 700 701 private static boolean isSpaceCharacter(int c) { 702 return c == Keyboard.CODE_SPACE || c == Keyboard.CODE_ENTER; 703 } 704 705 private static boolean isLayoutSwitchBackCharacter(int c) { 706 if (TextUtils.isEmpty(mLayoutSwitchBackSymbols)) return false; 707 if (mLayoutSwitchBackSymbols.indexOf(c) >= 0) return true; 708 return false; 709 } 710 711 /** 712 * Updates state machine to figure out when to automatically snap back to the previous mode. 713 */ 714 public void onKey(int code) { 715 if (DEBUG_STATE) { 716 Log.d(TAG, "onKey: code=" + code + " switchState=" + mSwitchState 717 + " isSinglePointer=" + isSinglePointer()); 718 } 719 switch (mSwitchState) { 720 case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: 721 // Only distinct multi touch devices can be in this state. 722 // On non-distinct multi touch devices, mode change key is handled by 723 // {@link LatinIME#onCodeInput}, not by {@link LatinIME#onPress} and 724 // {@link LatinIME#onRelease}. So, on such devices, {@link #mSwitchState} starts 725 // from {@link #SWITCH_STATE_SYMBOL_BEGIN}, or {@link #SWITCH_STATE_ALPHA}, not from 726 // {@link #SWITCH_STATE_MOMENTARY}. 727 if (code == Keyboard.CODE_SWITCH_ALPHA_SYMBOL) { 728 // Detected only the mode change key has been pressed, and then released. 729 if (mCurrentId.equals(mMainKeyboardId)) { 730 mSwitchState = SWITCH_STATE_ALPHA; 731 } else { 732 mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; 733 } 734 } else if (isSinglePointer()) { 735 // Snap back to the previous keyboard mode if the user pressed the mode change key 736 // and slid to other key, then released the finger. 737 // If the user cancels the sliding input, snapping back to the previous keyboard 738 // mode is handled by {@link #onCancelInput}. 739 changeKeyboardMode(); 740 } else { 741 // Chording input is being started. The keyboard mode will be snapped back to the 742 // previous mode in {@link onReleaseSymbol} when the mode change key is released. 743 mSwitchState = SWITCH_STATE_CHORDING_ALPHA; 744 } 745 break; 746 case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: 747 if (code == Keyboard.CODE_SHIFT) { 748 // Detected only the shift key has been pressed on symbol layout, and then released. 749 mSwitchState = SWITCH_STATE_SYMBOL_BEGIN; 750 } else if (isSinglePointer()) { 751 // Snap back to the previous keyboard mode if the user pressed the shift key on 752 // symbol mode and slid to other key, then released the finger. 753 toggleShift(); 754 mSwitchState = SWITCH_STATE_SYMBOL; 755 } else { 756 // Chording input is being started. The keyboard mode will be snapped back to the 757 // previous mode in {@link onReleaseShift} when the shift key is released. 758 mSwitchState = SWITCH_STATE_CHORDING_SYMBOL; 759 } 760 break; 761 case SWITCH_STATE_SYMBOL_BEGIN: 762 if (!isSpaceCharacter(code) && code >= 0) { 763 mSwitchState = SWITCH_STATE_SYMBOL; 764 } 765 // Snap back to alpha keyboard mode immediately if user types a quote character. 766 if (isLayoutSwitchBackCharacter(code)) { 767 changeKeyboardMode(); 768 } 769 break; 770 case SWITCH_STATE_SYMBOL: 771 case SWITCH_STATE_CHORDING_SYMBOL: 772 // Snap back to alpha keyboard mode if user types one or more non-space/enter 773 // characters followed by a space/enter or a quote character. 774 if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) { 775 changeKeyboardMode(); 776 } 777 break; 778 } 779 } 780 781 public LatinKeyboardView getKeyboardView() { 782 return mKeyboardView; 783 } 784 785 public View onCreateInputView() { 786 return createInputView(mThemeIndex, true); 787 } 788 789 private View createInputView(final int newThemeIndex, final boolean forceRecreate) { 790 if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate) 791 return mCurrentInputView; 792 793 if (mKeyboardView != null) { 794 mKeyboardView.closing(); 795 } 796 797 final int oldThemeIndex = mThemeIndex; 798 Utils.GCUtils.getInstance().reset(); 799 boolean tryGC = true; 800 for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 801 try { 802 setContextThemeWrapper(mInputMethodService, newThemeIndex); 803 mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( 804 R.layout.input_view, null); 805 tryGC = false; 806 } catch (OutOfMemoryError e) { 807 Log.w(TAG, "load keyboard failed: " + e); 808 tryGC = Utils.GCUtils.getInstance().tryGCOrWait( 809 oldThemeIndex + "," + newThemeIndex, e); 810 } catch (InflateException e) { 811 Log.w(TAG, "load keyboard failed: " + e); 812 tryGC = Utils.GCUtils.getInstance().tryGCOrWait( 813 oldThemeIndex + "," + newThemeIndex, e); 814 } 815 } 816 817 mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); 818 mKeyboardView.setKeyboardActionListener(mInputMethodService); 819 820 // This always needs to be set since the accessibility state can 821 // potentially change without the input view being re-created. 822 AccessibleKeyboardViewProxy.setView(mKeyboardView); 823 824 return mCurrentInputView; 825 } 826 827 private void postSetInputView(final View newInputView) { 828 mInputMethodService.mHandler.post(new Runnable() { 829 @Override 830 public void run() { 831 if (newInputView != null) { 832 mInputMethodService.setInputView(newInputView); 833 } 834 mInputMethodService.updateInputViewShown(); 835 } 836 }); 837 } 838 839 @Override 840 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 841 if (PREF_KEYBOARD_LAYOUT.equals(key)) { 842 final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences); 843 postSetInputView(createInputView(themeIndex, false)); 844 } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) { 845 postSetInputView(createInputView(mThemeIndex, true)); 846 } 847 } 848 849 public void onAutoCorrectionStateChanged(boolean isAutoCorrection) { 850 if (mIsAutoCorrectionActive != isAutoCorrection) { 851 mIsAutoCorrectionActive = isAutoCorrection; 852 final LatinKeyboard keyboard = getLatinKeyboard(); 853 if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) { 854 final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection); 855 final LatinKeyboardView keyboardView = getKeyboardView(); 856 if (keyboardView != null) 857 keyboardView.invalidateKey(invalidatedKey); 858 } 859 } 860 } 861 862 private static int getF2KeyMode(boolean settingsKeyEnabled, boolean noSettingsKey) { 863 if (noSettingsKey) { 864 // Never shows the Settings key 865 return KeyboardId.F2KEY_MODE_SHORTCUT_IME; 866 } 867 868 if (settingsKeyEnabled) { 869 return KeyboardId.F2KEY_MODE_SETTINGS; 870 } else { 871 // It should be alright to fall back to the Settings key on 7-inch layouts 872 // even when the Settings key is not explicitly enabled. 873 return KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS; 874 } 875 } 876} 877