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