KeyboardSwitcher.java revision 34f18203960d34dca01c80355bae3549e09aaf88
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.util.DisplayMetrics; 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.KeyboardState; 33import com.android.inputmethod.latin.InputView; 34import com.android.inputmethod.latin.LatinIME; 35import com.android.inputmethod.latin.LatinImeLogger; 36import com.android.inputmethod.latin.LocaleUtils; 37import com.android.inputmethod.latin.R; 38import com.android.inputmethod.latin.Settings; 39import com.android.inputmethod.latin.SettingsValues; 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 KeyboardState.SwitchActions, 48 SharedPreferences.OnSharedPreferenceChangeListener { 49 private static final String TAG = KeyboardSwitcher.class.getSimpleName(); 50 private static final boolean DEBUG_CACHE = LatinImeLogger.sDBG; 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 73 private KeyboardId mMainKeyboardId; 74 private KeyboardId mSymbolsKeyboardId; 75 private KeyboardId mSymbolsShiftedKeyboardId; 76 77 private final HashMap<KeyboardId, SoftReference<LatinKeyboard>> mKeyboardCache = 78 new HashMap<KeyboardId, SoftReference<LatinKeyboard>>(); 79 80 /** mIsAutoCorrectionActive indicates that auto corrected word will be input instead of 81 * what user actually typed. */ 82 private boolean mIsAutoCorrectionActive; 83 84 private int mThemeIndex = -1; 85 private Context mThemeContext; 86 87 private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); 88 89 public static KeyboardSwitcher getInstance() { 90 return sInstance; 91 } 92 93 private KeyboardSwitcher() { 94 // Intentional empty constructor for singleton. 95 } 96 97 public static void init(LatinIME ims, SharedPreferences prefs) { 98 sInstance.initInternal(ims, prefs); 99 } 100 101 private void initInternal(LatinIME ims, SharedPreferences prefs) { 102 mInputMethodService = ims; 103 mPackageName = ims.getPackageName(); 104 mResources = ims.getResources(); 105 mPrefs = prefs; 106 mSubtypeSwitcher = SubtypeSwitcher.getInstance(); 107 mState = new KeyboardState(this); 108 setContextThemeWrapper(ims, getKeyboardThemeIndex(ims, prefs)); 109 prefs.registerOnSharedPreferenceChangeListener(this); 110 } 111 112 private static int getKeyboardThemeIndex(Context context, SharedPreferences prefs) { 113 final String defaultThemeId = context.getString(R.string.config_default_keyboard_theme_id); 114 final String themeId = prefs.getString(PREF_KEYBOARD_LAYOUT, defaultThemeId); 115 try { 116 final int themeIndex = Integer.valueOf(themeId); 117 if (themeIndex >= 0 && themeIndex < KEYBOARD_THEMES.length) 118 return themeIndex; 119 } catch (NumberFormatException e) { 120 // Format error, keyboard theme is default to 0. 121 } 122 Log.w(TAG, "Illegal keyboard theme in preference: " + themeId + ", default to 0"); 123 return 0; 124 } 125 126 private void setContextThemeWrapper(Context context, int themeIndex) { 127 if (mThemeIndex != themeIndex) { 128 mThemeIndex = themeIndex; 129 mThemeContext = new ContextThemeWrapper(context, KEYBOARD_THEMES[themeIndex]); 130 mKeyboardCache.clear(); 131 } 132 } 133 134 public void loadKeyboard(EditorInfo editorInfo, SettingsValues settingsValues) { 135 try { 136 mMainKeyboardId = getKeyboardId(editorInfo, false, false, settingsValues); 137 mSymbolsKeyboardId = getKeyboardId(editorInfo, true, false, settingsValues); 138 mSymbolsShiftedKeyboardId = getKeyboardId(editorInfo, true, true, settingsValues); 139 mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols), 140 hasDistinctMultitouch()); 141 // TODO: Should get rid of this special case handling for Phone Number layouts once we 142 // have separate layouts with unique KeyboardIds for alphabet and alphabet-shifted 143 // respectively. 144 if (mMainKeyboardId.isPhoneKeyboard()) { 145 mState.onToggleAlphabetAndSymbols(); 146 } 147 } catch (RuntimeException e) { 148 Log.w(TAG, "loading keyboard failed: " + mMainKeyboardId, e); 149 LatinImeLogger.logOnException(mMainKeyboardId.toString(), e); 150 } 151 } 152 153 public void saveKeyboardState() { 154 if (isKeyboardAvailable()) { 155 mState.onSaveKeyboardState(); 156 } 157 } 158 159 public void onFinishInputView() { 160 mIsAutoCorrectionActive = false; 161 } 162 163 public void onHideWindow() { 164 mIsAutoCorrectionActive = false; 165 } 166 167 private void setKeyboard(final Keyboard keyboard) { 168 final Keyboard oldKeyboard = mKeyboardView.getKeyboard(); 169 mKeyboardView.setKeyboard(keyboard); 170 mCurrentInputView.setKeyboardGeometry(keyboard.mTopPadding); 171 mKeyboardView.setKeyPreviewPopupEnabled( 172 SettingsValues.isKeyPreviewPopupEnabled(mPrefs, mResources), 173 SettingsValues.getKeyPreviewPopupDismissDelay(mPrefs, mResources)); 174 final boolean localeChanged = (oldKeyboard == null) 175 || !keyboard.mId.mLocale.equals(oldKeyboard.mId.mLocale); 176 mInputMethodService.mHandler.startDisplayLanguageOnSpacebar(localeChanged); 177 updateShiftState(); 178 } 179 180 private LatinKeyboard getKeyboard(KeyboardId id) { 181 final SoftReference<LatinKeyboard> ref = mKeyboardCache.get(id); 182 LatinKeyboard keyboard = (ref == null) ? null : ref.get(); 183 if (keyboard == null) { 184 final Locale savedLocale = LocaleUtils.setSystemLocale(mResources, id.mLocale); 185 try { 186 final LatinKeyboard.Builder builder = new LatinKeyboard.Builder(mThemeContext); 187 builder.load(id); 188 builder.setTouchPositionCorrectionEnabled( 189 mSubtypeSwitcher.currentSubtypeContainsExtraValueKey( 190 LatinIME.SUBTYPE_EXTRA_VALUE_SUPPORT_TOUCH_POSITION_CORRECTION)); 191 keyboard = builder.build(); 192 } finally { 193 LocaleUtils.setSystemLocale(mResources, savedLocale); 194 } 195 mKeyboardCache.put(id, new SoftReference<LatinKeyboard>(keyboard)); 196 197 if (DEBUG_CACHE) { 198 Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": " 199 + ((ref == null) ? "LOAD" : "GCed") + " id=" + id 200 + " theme=" + Keyboard.themeName(keyboard.mThemeId)); 201 } 202 } else if (DEBUG_CACHE) { 203 Log.d(TAG, "keyboard cache size=" + mKeyboardCache.size() + ": HIT id=" + id 204 + " theme=" + Keyboard.themeName(keyboard.mThemeId)); 205 } 206 207 keyboard.onAutoCorrectionStateChanged(mIsAutoCorrectionActive); 208 keyboard.setShiftLocked(false); 209 keyboard.setShifted(false); 210 // If the cached keyboard had been switched to another keyboard while the language was 211 // displayed on its spacebar, it might have had arbitrary text fade factor. In such case, 212 // we should reset the text fade factor. It is also applicable to shortcut key. 213 keyboard.setSpacebarTextFadeFactor(0.0f, null); 214 keyboard.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady(), null); 215 return keyboard; 216 } 217 218 private KeyboardId getKeyboardId(EditorInfo editorInfo, final boolean isSymbols, 219 final boolean isShift, SettingsValues settingsValues) { 220 final int mode = Utils.getKeyboardMode(editorInfo); 221 final int xmlId; 222 switch (mode) { 223 case KeyboardId.MODE_PHONE: 224 xmlId = (isSymbols && isShift) ? R.xml.kbd_phone_shift : R.xml.kbd_phone; 225 break; 226 case KeyboardId.MODE_NUMBER: 227 xmlId = R.xml.kbd_number; 228 break; 229 default: 230 if (isSymbols) { 231 xmlId = isShift ? R.xml.kbd_symbols_shift : R.xml.kbd_symbols; 232 } else { 233 xmlId = R.xml.kbd_qwerty; 234 } 235 break; 236 } 237 238 final boolean settingsKeyEnabled = settingsValues.isSettingsKeyEnabled(); 239 @SuppressWarnings("deprecation") 240 final boolean noMicrophone = Utils.inPrivateImeOptions( 241 mPackageName, LatinIME.IME_OPTION_NO_MICROPHONE, editorInfo) 242 || Utils.inPrivateImeOptions( 243 null, LatinIME.IME_OPTION_NO_MICROPHONE_COMPAT, editorInfo); 244 final boolean voiceKeyEnabled = settingsValues.isVoiceKeyEnabled(editorInfo) 245 && !noMicrophone; 246 final boolean voiceKeyOnMain = settingsValues.isVoiceKeyOnMain(); 247 final boolean noSettingsKey = Utils.inPrivateImeOptions( 248 mPackageName, LatinIME.IME_OPTION_NO_SETTINGS_KEY, editorInfo); 249 final boolean hasSettingsKey = settingsKeyEnabled && !noSettingsKey; 250 final int f2KeyMode = getF2KeyMode(settingsKeyEnabled, noSettingsKey); 251 final boolean hasShortcutKey = voiceKeyEnabled && (isSymbols != voiceKeyOnMain); 252 final boolean forceAscii = Utils.inPrivateImeOptions( 253 mPackageName, LatinIME.IME_OPTION_FORCE_ASCII, editorInfo); 254 final boolean asciiCapable = mSubtypeSwitcher.currentSubtypeContainsExtraValueKey( 255 LatinIME.SUBTYPE_EXTRA_VALUE_ASCII_CAPABLE); 256 final Locale locale = (forceAscii && !asciiCapable) 257 ? Locale.US : mSubtypeSwitcher.getInputLocale(); 258 final Configuration conf = mResources.getConfiguration(); 259 final DisplayMetrics dm = mResources.getDisplayMetrics(); 260 261 return new KeyboardId( 262 mResources.getResourceEntryName(xmlId), xmlId, locale, conf.orientation, 263 dm.widthPixels, mode, editorInfo, hasSettingsKey, f2KeyMode, noSettingsKey, 264 voiceKeyEnabled, hasShortcutKey); 265 } 266 267 public boolean isAlphabetMode() { 268 final Keyboard keyboard = getLatinKeyboard(); 269 return keyboard != null && keyboard.mId.isAlphabetKeyboard(); 270 } 271 272 public boolean isInputViewShown() { 273 return mCurrentInputView != null && mCurrentInputView.isShown(); 274 } 275 276 public boolean isShiftedOrShiftLocked() { 277 final Keyboard keyboard = getLatinKeyboard(); 278 return keyboard != null && keyboard.isShiftedOrShiftLocked(); 279 } 280 281 public boolean isManualTemporaryUpperCase() { 282 final Keyboard keyboard = getLatinKeyboard(); 283 return keyboard != null && keyboard.isManualTemporaryUpperCase(); 284 } 285 286 public boolean isKeyboardAvailable() { 287 if (mKeyboardView != null) 288 return mKeyboardView.getKeyboard() != null; 289 return false; 290 } 291 292 public LatinKeyboard getLatinKeyboard() { 293 if (mKeyboardView != null) { 294 final Keyboard keyboard = mKeyboardView.getKeyboard(); 295 if (keyboard instanceof LatinKeyboard) 296 return (LatinKeyboard)keyboard; 297 } 298 return null; 299 } 300 301 // Implements {@link KeyboardState.SwitchActions}. 302 @Override 303 public void setShifted(int shiftMode) { 304 mInputMethodService.mHandler.cancelUpdateShiftState(); 305 LatinKeyboard latinKeyboard = getLatinKeyboard(); 306 if (latinKeyboard == null) 307 return; 308 if (shiftMode == AUTOMATIC_SHIFT) { 309 latinKeyboard.setAutomaticTemporaryUpperCase(); 310 } else { 311 final boolean shifted = (shiftMode == MANUAL_SHIFT); 312 // On non-distinct multi touch panel device, we should also turn off the shift locked 313 // state when shift key is pressed to go to normal mode. 314 // On the other hand, on distinct multi touch panel device, turning off the shift 315 // locked state with shift key pressing is handled by onReleaseShift(). 316 if (!hasDistinctMultitouch() && !shifted && mState.isShiftLocked()) { 317 latinKeyboard.setShiftLocked(false); 318 } 319 latinKeyboard.setShifted(shifted); 320 } 321 mKeyboardView.invalidateAllKeys(); 322 } 323 324 // Implements {@link KeyboardState.SwitchActions}. 325 @Override 326 public void setShiftLocked(boolean shiftLocked) { 327 mInputMethodService.mHandler.cancelUpdateShiftState(); 328 LatinKeyboard latinKeyboard = getLatinKeyboard(); 329 if (latinKeyboard == null) 330 return; 331 latinKeyboard.setShiftLocked(shiftLocked); 332 mKeyboardView.invalidateAllKeys(); 333 if (!shiftLocked) { 334 // To be able to turn off caps lock by "double tap" on shift key, we should ignore 335 // the second tap of the "double tap" from now for a while because we just have 336 // already turned off caps lock above. 337 mKeyboardView.startIgnoringDoubleTap(); 338 } 339 } 340 341 /** 342 * Toggle keyboard shift state triggered by user touch event. 343 */ 344 public void toggleShift() { 345 mState.onToggleShift(); 346 } 347 348 /** 349 * Toggle caps lock state triggered by user touch event. 350 */ 351 public void toggleCapsLock() { 352 mState.onToggleCapsLock(); 353 } 354 355 /** 356 * Toggle between alphabet and symbols modes triggered by user touch event. 357 */ 358 public void toggleAlphabetAndSymbols() { 359 mState.onToggleAlphabetAndSymbols(); 360 } 361 362 /** 363 * Update keyboard shift state triggered by connected EditText status change. 364 */ 365 public void updateShiftState() { 366 mState.onUpdateShiftState(mInputMethodService.getCurrentAutoCapsState()); 367 } 368 369 public void onPressShift(boolean withSliding) { 370 mState.onPressShift(withSliding); 371 } 372 373 public void onReleaseShift(boolean withSliding) { 374 mState.onReleaseShift(withSliding); 375 } 376 377 public void onPressSymbol() { 378 mState.onPressSymbol(); 379 } 380 381 public void onReleaseSymbol() { 382 mState.onReleaseSymbol(); 383 } 384 385 public void onOtherKeyPressed() { 386 mState.onOtherKeyPressed(); 387 } 388 389 public void onCancelInput() { 390 mState.onCancelInput(isSinglePointer()); 391 } 392 393 // Implements {@link KeyboardState.SwitchActions}. 394 @Override 395 public void setSymbolsKeyboard() { 396 setKeyboard(getKeyboard(mSymbolsKeyboardId)); 397 } 398 399 // Implements {@link KeyboardState.SwitchActions}. 400 @Override 401 public void setAlphabetKeyboard() { 402 setKeyboard(getKeyboard(mMainKeyboardId)); 403 } 404 405 // Implements {@link KeyboardState.SwitchActions}. 406 @Override 407 public void setSymbolsShiftedKeyboard() { 408 final Keyboard keyboard = getKeyboard(mSymbolsShiftedKeyboardId); 409 setKeyboard(keyboard); 410 // TODO: Remove this logic once we introduce initial keyboard shift state attribute. 411 // Symbol shift keyboard may have a shift key that has a caps lock style indicator (a.k.a. 412 // sticky shift key). To show or dismiss the indicator, we need to call setShiftLocked() 413 // that takes care of the current keyboard having such shift key or not. 414 keyboard.setShiftLocked(keyboard.hasShiftLockKey()); 415 } 416 417 public boolean isInMomentarySwitchState() { 418 return mState.isInMomentarySwitchState(); 419 } 420 421 public boolean isVibrateAndSoundFeedbackRequired() { 422 return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput(); 423 } 424 425 private boolean isSinglePointer() { 426 return mKeyboardView != null && mKeyboardView.getPointerCount() == 1; 427 } 428 429 public boolean hasDistinctMultitouch() { 430 return mKeyboardView != null && mKeyboardView.hasDistinctMultitouch(); 431 } 432 433 /** 434 * Updates state machine to figure out when to automatically snap back to the previous mode. 435 */ 436 public void onCodeInput(int code) { 437 mState.onCodeInput(code, isSinglePointer()); 438 } 439 440 public LatinKeyboardView getKeyboardView() { 441 return mKeyboardView; 442 } 443 444 public View onCreateInputView() { 445 return createInputView(mThemeIndex, true); 446 } 447 448 private View createInputView(final int newThemeIndex, final boolean forceRecreate) { 449 if (mCurrentInputView != null && mThemeIndex == newThemeIndex && !forceRecreate) 450 return mCurrentInputView; 451 452 if (mKeyboardView != null) { 453 mKeyboardView.closing(); 454 } 455 456 final int oldThemeIndex = mThemeIndex; 457 Utils.GCUtils.getInstance().reset(); 458 boolean tryGC = true; 459 for (int i = 0; i < Utils.GCUtils.GC_TRY_LOOP_MAX && tryGC; ++i) { 460 try { 461 setContextThemeWrapper(mInputMethodService, newThemeIndex); 462 mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( 463 R.layout.input_view, null); 464 tryGC = false; 465 } catch (OutOfMemoryError e) { 466 Log.w(TAG, "load keyboard failed: " + e); 467 tryGC = Utils.GCUtils.getInstance().tryGCOrWait( 468 oldThemeIndex + "," + newThemeIndex, e); 469 } catch (InflateException e) { 470 Log.w(TAG, "load keyboard failed: " + e); 471 tryGC = Utils.GCUtils.getInstance().tryGCOrWait( 472 oldThemeIndex + "," + newThemeIndex, e); 473 } 474 } 475 476 mKeyboardView = (LatinKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); 477 mKeyboardView.setKeyboardActionListener(mInputMethodService); 478 479 // This always needs to be set since the accessibility state can 480 // potentially change without the input view being re-created. 481 AccessibleKeyboardViewProxy.setView(mKeyboardView); 482 483 return mCurrentInputView; 484 } 485 486 private void postSetInputView(final View newInputView) { 487 final LatinIME latinIme = mInputMethodService; 488 latinIme.mHandler.post(new Runnable() { 489 @Override 490 public void run() { 491 if (newInputView != null) { 492 latinIme.setInputView(newInputView); 493 } 494 latinIme.updateInputViewShown(); 495 } 496 }); 497 } 498 499 @Override 500 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 501 if (PREF_KEYBOARD_LAYOUT.equals(key)) { 502 final int themeIndex = getKeyboardThemeIndex(mInputMethodService, sharedPreferences); 503 postSetInputView(createInputView(themeIndex, false)); 504 } else if (Settings.PREF_SHOW_SETTINGS_KEY.equals(key)) { 505 postSetInputView(createInputView(mThemeIndex, true)); 506 } 507 } 508 509 public void onAutoCorrectionStateChanged(boolean isAutoCorrection) { 510 if (mIsAutoCorrectionActive != isAutoCorrection) { 511 mIsAutoCorrectionActive = isAutoCorrection; 512 final LatinKeyboard keyboard = getLatinKeyboard(); 513 if (keyboard != null && keyboard.needsAutoCorrectionSpacebarLed()) { 514 final Key invalidatedKey = keyboard.onAutoCorrectionStateChanged(isAutoCorrection); 515 final LatinKeyboardView keyboardView = getKeyboardView(); 516 if (keyboardView != null) 517 keyboardView.invalidateKey(invalidatedKey); 518 } 519 } 520 } 521 522 private static int getF2KeyMode(boolean settingsKeyEnabled, boolean noSettingsKey) { 523 if (noSettingsKey) { 524 // Never shows the Settings key 525 return KeyboardId.F2KEY_MODE_SHORTCUT_IME; 526 } 527 528 if (settingsKeyEnabled) { 529 return KeyboardId.F2KEY_MODE_SETTINGS; 530 } else { 531 // It should be alright to fall back to the Settings key on 7-inch layouts 532 // even when the Settings key is not explicitly enabled. 533 return KeyboardId.F2KEY_MODE_SHORTCUT_IME_OR_SETTINGS; 534 } 535 } 536} 537