SubtypeSwitcher.java revision 9313bef894cef4be2f5821be1d812b30f1451894
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import com.android.inputmethod.keyboard.KeyboardSwitcher; 20import com.android.inputmethod.voice.SettingsUtil; 21import com.android.inputmethod.voice.VoiceIMEConnector; 22import com.android.inputmethod.voice.VoiceInput; 23 24import android.content.Context; 25import android.content.SharedPreferences; 26import android.content.pm.PackageManager; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.graphics.drawable.Drawable; 30import android.os.IBinder; 31import android.text.TextUtils; 32import android.util.Log; 33import android.view.inputmethod.InputMethodInfo; 34import android.view.inputmethod.InputMethodManager; 35import android.view.inputmethod.InputMethodSubtype; 36 37import java.util.ArrayList; 38import java.util.Arrays; 39import java.util.List; 40import java.util.Locale; 41import java.util.Map; 42 43public class SubtypeSwitcher { 44 private static boolean DBG = LatinImeLogger.sDBG; 45 private static final String TAG = "SubtypeSwitcher"; 46 47 private static final char LOCALE_SEPARATER = '_'; 48 private static final String KEYBOARD_MODE = "keyboard"; 49 private static final String VOICE_MODE = "voice"; 50 private final TextUtils.SimpleStringSplitter mLocaleSplitter = 51 new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); 52 53 private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); 54 private /* final */ LatinIME mService; 55 private /* final */ SharedPreferences mPrefs; 56 private /* final */ InputMethodManager mImm; 57 private /* final */ Resources mResources; 58 private final ArrayList<InputMethodSubtype> mEnabledKeyboardSubtypesOfCurrentInputMethod = 59 new ArrayList<InputMethodSubtype>(); 60 private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); 61 62 private boolean mConfigUseSpacebarLanguageSwitcher; 63 64 /*-----------------------------------------------------------*/ 65 // Variants which should be changed only by reload functions. 66 private boolean mNeedsToDisplayLanguage; 67 private boolean mIsSystemLanguageSameAsInputLanguage; 68 private InputMethodInfo mShortcutInfo; 69 private InputMethodSubtype mShortcutSubtype; 70 private List<InputMethodSubtype> mAllEnabledSubtypesOfCurrentInputMethod; 71 private Locale mSystemLocale; 72 private Locale mInputLocale; 73 private String mInputLocaleStr; 74 private String mMode; 75 private VoiceInput mVoiceInput; 76 /*-----------------------------------------------------------*/ 77 78 public static SubtypeSwitcher getInstance() { 79 return sInstance; 80 } 81 82 public static void init(LatinIME service, SharedPreferences prefs) { 83 sInstance.mPrefs = prefs; 84 sInstance.resetParams(service); 85 sInstance.updateAllParameters(); 86 87 SubtypeLocale.init(service); 88 } 89 90 private SubtypeSwitcher() { 91 // Intentional empty constructor for singleton. 92 } 93 94 private void resetParams(LatinIME service) { 95 mService = service; 96 mResources = service.getResources(); 97 mImm = (InputMethodManager) service.getSystemService(Context.INPUT_METHOD_SERVICE); 98 mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); 99 mEnabledLanguagesOfCurrentInputMethod.clear(); 100 mSystemLocale = null; 101 mInputLocale = null; 102 mInputLocaleStr = null; 103 // Mode is initialized to KEYBOARD_MODE, in case that LatinIME can't obtain currentSubtype 104 mMode = KEYBOARD_MODE; 105 mAllEnabledSubtypesOfCurrentInputMethod = null; 106 // TODO: Voice input should be created here 107 mVoiceInput = null; 108 mConfigUseSpacebarLanguageSwitcher = mResources.getBoolean( 109 R.bool.config_use_spacebar_language_switcher); 110 if (mConfigUseSpacebarLanguageSwitcher) 111 initLanguageSwitcher(service); 112 } 113 114 // Update all parameters stored in SubtypeSwitcher. 115 // Only configuration changed event is allowed to call this because this is heavy. 116 private void updateAllParameters() { 117 mSystemLocale = mResources.getConfiguration().locale; 118 updateSubtype(mImm.getCurrentInputMethodSubtype()); 119 updateParametersOnStartInputView(); 120 } 121 122 // Update parameters which are changed outside LatinIME. This parameters affect UI so they 123 // should be updated every time onStartInputview. 124 public void updateParametersOnStartInputView() { 125 if (mConfigUseSpacebarLanguageSwitcher) { 126 updateForSpacebarLanguageSwitch(); 127 } else { 128 updateEnabledSubtypes(); 129 } 130 updateShortcutIME(); 131 } 132 133 // Reload enabledSubtypes from the framework. 134 private void updateEnabledSubtypes() { 135 boolean foundCurrentSubtypeBecameDisabled = true; 136 mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList( 137 null, true); 138 mEnabledLanguagesOfCurrentInputMethod.clear(); 139 mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); 140 for (InputMethodSubtype ims: mAllEnabledSubtypesOfCurrentInputMethod) { 141 final String locale = ims.getLocale(); 142 final String mode = ims.getMode(); 143 mLocaleSplitter.setString(locale); 144 if (mLocaleSplitter.hasNext()) { 145 mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); 146 } 147 if (locale.equals(mInputLocaleStr) && mode.equals(mMode)) { 148 foundCurrentSubtypeBecameDisabled = false; 149 } 150 if (KEYBOARD_MODE.equals(ims.getMode())) { 151 mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims); 152 } 153 } 154 mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 155 && mIsSystemLanguageSameAsInputLanguage); 156 if (foundCurrentSubtypeBecameDisabled) { 157 if (DBG) { 158 Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + mMode); 159 Log.w(TAG, "Last subtype was disabled. Update to the current one."); 160 } 161 updateSubtype(mImm.getCurrentInputMethodSubtype()); 162 } 163 } 164 165 private void updateShortcutIME() { 166 if (DBG) { 167 Log.d(TAG, "Update shortcut IME from : " 168 + (mShortcutInfo == null ? "<null>" : mShortcutInfo.getId()) + ", " 169 + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale() 170 + ", " + mShortcutSubtype.getMode()))); 171 } 172 // TODO: Update an icon for shortcut IME 173 Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts = 174 mImm.getShortcutInputMethodsAndSubtypes(); 175 for (InputMethodInfo imi: shortcuts.keySet()) { 176 List<InputMethodSubtype> subtypes = shortcuts.get(imi); 177 // TODO: Returns the first found IMI for now. Should handle all shortcuts as 178 // appropriate. 179 mShortcutInfo = imi; 180 // TODO: Pick up the first found subtype for now. Should handle all subtypes 181 // as appropriate. 182 mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; 183 break; 184 } 185 if (DBG) { 186 Log.d(TAG, "Update shortcut IME to : " 187 + (mShortcutInfo == null ? "<null>" : mShortcutInfo.getId()) + ", " 188 + (mShortcutSubtype == null ? "<null>" : (mShortcutSubtype.getLocale() 189 + ", " + mShortcutSubtype.getMode()))); 190 } 191 } 192 193 // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. 194 public void updateSubtype(InputMethodSubtype newSubtype) { 195 final String newLocale; 196 final String newMode; 197 if (newSubtype == null) { 198 // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, 199 // fallback to the default locale and mode. 200 Log.w(TAG, "Couldn't get the current subtype."); 201 newLocale = "en_US"; 202 newMode = KEYBOARD_MODE; 203 } else { 204 newLocale = newSubtype.getLocale(); 205 newMode = newSubtype.getMode(); 206 } 207 if (DBG) { 208 Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode 209 + ", from: " + mInputLocaleStr + ", " + mMode); 210 } 211 boolean languageChanged = false; 212 if (!newLocale.equals(mInputLocaleStr)) { 213 if (mInputLocaleStr != null) { 214 languageChanged = true; 215 } 216 updateInputLocale(newLocale); 217 } 218 boolean modeChanged = false; 219 String oldMode = mMode; 220 if (!newMode.equals(mMode)) { 221 if (mMode != null) { 222 modeChanged = true; 223 } 224 mMode = newMode; 225 } 226 227 // If the old mode is voice input, we need to reset or cancel its status. 228 // We cancel its status when we change mode, while we reset otherwise. 229 if (isKeyboardMode()) { 230 if (modeChanged) { 231 if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { 232 mVoiceInput.cancel(); 233 } 234 } 235 if (modeChanged || languageChanged) { 236 updateShortcutIME(); 237 mService.onRefreshKeyboard(); 238 } 239 } else if (isVoiceMode() && mVoiceInput != null) { 240 if (VOICE_MODE.equals(oldMode)) { 241 mVoiceInput.reset(); 242 } 243 // If needsToShowWarningDialog is true, voice input need to show warning before 244 // show recognition view. 245 if (languageChanged || modeChanged 246 || VoiceIMEConnector.getInstance().needsToShowWarningDialog()) { 247 triggerVoiceIME(); 248 } 249 } else { 250 Log.w(TAG, "Unknown subtype mode: " + mMode); 251 if (VOICE_MODE.equals(oldMode) && mVoiceInput != null) { 252 // We need to reset the voice input to release the resources and to reset its status 253 // as it is not the current input mode. 254 mVoiceInput.reset(); 255 } 256 } 257 } 258 259 // Update the current input locale from Locale string. 260 private void updateInputLocale(String inputLocaleStr) { 261 // example: inputLocaleStr = "en_US" "en" "" 262 // "en_US" --> language: en & country: US 263 // "en" --> language: en 264 // "" --> the system locale 265 mLocaleSplitter.setString(inputLocaleStr); 266 if (mLocaleSplitter.hasNext()) { 267 String language = mLocaleSplitter.next(); 268 if (mLocaleSplitter.hasNext()) { 269 mInputLocale = new Locale(language, mLocaleSplitter.next()); 270 } else { 271 mInputLocale = new Locale(language); 272 } 273 mInputLocaleStr = inputLocaleStr; 274 } else { 275 mInputLocale = mSystemLocale; 276 String country = mSystemLocale.getCountry(); 277 mInputLocaleStr = mSystemLocale.getLanguage() 278 + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage()); 279 } 280 mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase( 281 getInputLocale().getLanguage()); 282 mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 283 && mIsSystemLanguageSameAsInputLanguage); 284 } 285 286 //////////////////////////// 287 // Shortcut IME functions // 288 //////////////////////////// 289 290 public void switchToShortcutIME() { 291 final IBinder token = mService.getWindow().getWindow().getAttributes().token; 292 if (token == null || mShortcutInfo == null) { 293 return; 294 } 295 final String imiId = mShortcutInfo.getId(); 296 final InputMethodSubtype subtype = mShortcutSubtype; 297 new Thread("SwitchToShortcutIME") { 298 @Override 299 public void run() { 300 mImm.setInputMethodAndSubtype(token, imiId, subtype); 301 } 302 }.start(); 303 } 304 305 public Drawable getShortcutIcon() { 306 return getSubtypeIcon(mShortcutInfo, mShortcutSubtype); 307 } 308 309 private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) { 310 final PackageManager pm = mService.getPackageManager(); 311 if (imi != null) { 312 final String imiPackageName = imi.getPackageName(); 313 if (DBG) { 314 Log.d(TAG, "Update icons of IME: " + imiPackageName + "," 315 + subtype.getLocale() + "," + subtype.getMode()); 316 } 317 if (subtype != null) { 318 return pm.getDrawable(imiPackageName, subtype.getIconResId(), 319 imi.getServiceInfo().applicationInfo); 320 } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) { 321 return pm.getDrawable(imiPackageName, 322 imi.getSubtypeAt(0).getIconResId(), 323 imi.getServiceInfo().applicationInfo); 324 } else { 325 try { 326 return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm); 327 } catch (PackageManager.NameNotFoundException e) { 328 Log.w(TAG, "IME can't be found: " + imiPackageName); 329 } 330 } 331 } 332 return null; 333 } 334 335 ////////////////////////////////// 336 // Language Switching functions // 337 ////////////////////////////////// 338 339 public int getEnabledKeyboardLocaleCount() { 340 if (mConfigUseSpacebarLanguageSwitcher) { 341 return mLanguageSwitcher.getLocaleCount(); 342 } else { 343 return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); 344 } 345 } 346 347 public boolean useSpacebarLanguageSwitcher() { 348 return mConfigUseSpacebarLanguageSwitcher; 349 } 350 351 public boolean needsToDisplayLanguage() { 352 return mNeedsToDisplayLanguage; 353 } 354 355 public Locale getInputLocale() { 356 if (mConfigUseSpacebarLanguageSwitcher) { 357 return mLanguageSwitcher.getInputLocale(); 358 } else { 359 return mInputLocale; 360 } 361 } 362 363 public String getInputLocaleStr() { 364 if (mConfigUseSpacebarLanguageSwitcher) { 365 String inputLanguage = null; 366 inputLanguage = mLanguageSwitcher.getInputLanguage(); 367 // Should return system locale if there is no Language available. 368 if (inputLanguage == null) { 369 inputLanguage = getSystemLocale().getLanguage(); 370 } 371 return inputLanguage; 372 } else { 373 return mInputLocaleStr; 374 } 375 } 376 377 public String[] getEnabledLanguages() { 378 if (mConfigUseSpacebarLanguageSwitcher) { 379 return mLanguageSwitcher.getEnabledLanguages(); 380 } else { 381 return mEnabledLanguagesOfCurrentInputMethod.toArray( 382 new String[mEnabledLanguagesOfCurrentInputMethod.size()]); 383 } 384 } 385 386 public Locale getSystemLocale() { 387 if (mConfigUseSpacebarLanguageSwitcher) { 388 return mLanguageSwitcher.getSystemLocale(); 389 } else { 390 return mSystemLocale; 391 } 392 } 393 394 public boolean isSystemLanguageSameAsInputLanguage() { 395 if (mConfigUseSpacebarLanguageSwitcher) { 396 return getSystemLocale().getLanguage().equalsIgnoreCase( 397 getInputLocaleStr().substring(0, 2)); 398 } else { 399 return mIsSystemLanguageSameAsInputLanguage; 400 } 401 } 402 403 public void onConfigurationChanged(Configuration conf) { 404 final Locale systemLocale = conf.locale; 405 // If system configuration was changed, update all parameters. 406 if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { 407 if (mConfigUseSpacebarLanguageSwitcher) { 408 // If the system locale changes and is different from the saved 409 // locale (mSystemLocale), then reload the input locale list from the 410 // latin ime settings (shared prefs) and reset the input locale 411 // to the first one. 412 mLanguageSwitcher.loadLocales(mPrefs); 413 mLanguageSwitcher.setSystemLocale(systemLocale); 414 } else { 415 updateAllParameters(); 416 } 417 } 418 } 419 420 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 421 if (mConfigUseSpacebarLanguageSwitcher) { 422 if (Settings.PREF_SELECTED_LANGUAGES.equals(key)) { 423 mLanguageSwitcher.loadLocales(sharedPreferences); 424 } 425 } 426 } 427 428 /** 429 * Change system locale for this application 430 * @param newLocale 431 * @return oldLocale 432 */ 433 public Locale changeSystemLocale(Locale newLocale) { 434 Configuration conf = mResources.getConfiguration(); 435 Locale oldLocale = conf.locale; 436 conf.locale = newLocale; 437 mResources.updateConfiguration(conf, mResources.getDisplayMetrics()); 438 return oldLocale; 439 } 440 441 public boolean isKeyboardMode() { 442 return KEYBOARD_MODE.equals(mMode); 443 } 444 445 446 /////////////////////////// 447 // Voice Input functions // 448 /////////////////////////// 449 450 public boolean setVoiceInput(VoiceInput vi) { 451 if (mVoiceInput == null && vi != null) { 452 mVoiceInput = vi; 453 if (isVoiceMode()) { 454 if (DBG) { 455 Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr()); 456 } 457 triggerVoiceIME(); 458 return true; 459 } 460 } 461 return false; 462 } 463 464 public boolean isVoiceMode() { 465 return VOICE_MODE.equals(mMode); 466 } 467 468 private void triggerVoiceIME() { 469 if (!mService.isInputViewShown()) return; 470 VoiceIMEConnector.getInstance().startListening(false, 471 KeyboardSwitcher.getInstance().getInputView().getWindowToken()); 472 } 473 474 ////////////////////////////////////// 475 // Spacebar Language Switch support // 476 ////////////////////////////////////// 477 478 private LanguageSwitcher mLanguageSwitcher; 479 480 public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) { 481 if (returnsNameInThisLocale) { 482 return toTitleCase(SubtypeLocale.getFullDisplayName(locale)); 483 } else { 484 return toTitleCase(locale.getDisplayName()); 485 } 486 } 487 488 public static String getDisplayLanguage(Locale locale) { 489 return toTitleCase(locale.getDisplayLanguage(locale)); 490 } 491 492 public static String getShortDisplayLanguage(Locale locale) { 493 return toTitleCase(locale.getLanguage()); 494 } 495 496 private static String toTitleCase(String s) { 497 if (s.length() == 0) { 498 return s; 499 } 500 return Character.toUpperCase(s.charAt(0)) + s.substring(1); 501 } 502 503 private void updateForSpacebarLanguageSwitch() { 504 // We need to update mNeedsToDisplayLanguage in onStartInputView because 505 // getEnabledKeyboardLocaleCount could have been changed. 506 mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 507 && getSystemLocale().getLanguage().equalsIgnoreCase( 508 getInputLocale().getLanguage())); 509 } 510 511 public String getInputLanguageName() { 512 return getDisplayLanguage(getInputLocale()); 513 } 514 515 public String getNextInputLanguageName() { 516 if (mConfigUseSpacebarLanguageSwitcher) { 517 return getDisplayLanguage(mLanguageSwitcher.getNextInputLocale()); 518 } else { 519 return ""; 520 } 521 } 522 523 public String getPreviousInputLanguageName() { 524 if (mConfigUseSpacebarLanguageSwitcher) { 525 return getDisplayLanguage(mLanguageSwitcher.getPrevInputLocale()); 526 } else { 527 return ""; 528 } 529 } 530 531 // A list of locales which are supported by default for voice input, unless we get a 532 // different list from Gservices. 533 private static final String DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES = 534 "en " + 535 "en_US " + 536 "en_GB " + 537 "en_AU " + 538 "en_CA " + 539 "en_IE " + 540 "en_IN " + 541 "en_NZ " + 542 "en_SG " + 543 "en_ZA "; 544 545 public boolean isVoiceSupported(String locale) { 546 // Get the current list of supported locales and check the current locale against that 547 // list. We cache this value so as not to check it every time the user starts a voice 548 // input. Because this method is called by onStartInputView, this should mean that as 549 // long as the locale doesn't change while the user is keeping the IME open, the 550 // value should never be stale. 551 String supportedLocalesString = SettingsUtil.getSettingsString( 552 mService.getContentResolver(), 553 SettingsUtil.LATIN_IME_VOICE_INPUT_SUPPORTED_LOCALES, 554 DEFAULT_VOICE_INPUT_SUPPORTED_LOCALES); 555 List<String> voiceInputSupportedLocales = Arrays.asList( 556 supportedLocalesString.split("\\s+")); 557 return voiceInputSupportedLocales.contains(locale); 558 } 559 560 public void loadSettings() { 561 if (mConfigUseSpacebarLanguageSwitcher) { 562 mLanguageSwitcher.loadLocales(mPrefs); 563 } 564 } 565 566 public void toggleLanguage(boolean reset, boolean next) { 567 if (mConfigUseSpacebarLanguageSwitcher) { 568 if (reset) { 569 mLanguageSwitcher.reset(); 570 } else { 571 if (next) { 572 mLanguageSwitcher.next(); 573 } else { 574 mLanguageSwitcher.prev(); 575 } 576 } 577 mLanguageSwitcher.persist(mPrefs); 578 } 579 } 580 581 private void initLanguageSwitcher(LatinIME service) { 582 final Configuration conf = service.getResources().getConfiguration(); 583 mLanguageSwitcher = new LanguageSwitcher(service); 584 mLanguageSwitcher.loadLocales(mPrefs); 585 mLanguageSwitcher.setSystemLocale(conf.locale); 586 } 587} 588