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