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 17 package com.android.inputmethod.latin; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.content.pm.PackageManager; 22 import android.content.pm.PackageManager.NameNotFoundException; 23 import android.content.res.Configuration; 24 import android.content.res.Resources; 25 import android.graphics.drawable.Drawable; 26 import android.net.ConnectivityManager; 27 import android.net.NetworkInfo; 28 import android.os.AsyncTask; 29 import android.os.IBinder; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; 34 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; 35 import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; 36 import com.android.inputmethod.deprecated.VoiceProxy; 37 import com.android.inputmethod.keyboard.KeyboardSwitcher; 38 import com.android.inputmethod.keyboard.LatinKeyboard; 39 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 46 public class SubtypeSwitcher { 47 private static boolean DBG = LatinImeLogger.sDBG; 48 private static final String TAG = SubtypeSwitcher.class.getSimpleName(); 49 50 private static final char LOCALE_SEPARATER = '_'; 51 private static final String KEYBOARD_MODE = "keyboard"; 52 private static final String VOICE_MODE = "voice"; 53 private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY = 54 "requireNetworkConnectivity"; 55 56 private final TextUtils.SimpleStringSplitter mLocaleSplitter = 57 new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); 58 59 private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); 60 private /* final */ LatinIME mService; 61 private /* final */ InputMethodManagerCompatWrapper mImm; 62 private /* final */ Resources mResources; 63 private /* final */ ConnectivityManager mConnectivityManager; 64 private final ArrayList<InputMethodSubtypeCompatWrapper> 65 mEnabledKeyboardSubtypesOfCurrentInputMethod = 66 new ArrayList<InputMethodSubtypeCompatWrapper>(); 67 private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); 68 69 /*-----------------------------------------------------------*/ 70 // Variants which should be changed only by reload functions. 71 private boolean mNeedsToDisplayLanguage; 72 private boolean mIsSystemLanguageSameAsInputLanguage; 73 private InputMethodInfoCompatWrapper mShortcutInputMethodInfo; 74 private InputMethodSubtypeCompatWrapper mShortcutSubtype; 75 private List<InputMethodSubtypeCompatWrapper> mAllEnabledSubtypesOfCurrentInputMethod; 76 private InputMethodSubtypeCompatWrapper mCurrentSubtype; 77 private Locale mSystemLocale; 78 private Locale mInputLocale; 79 private String mInputLocaleStr; 80 private VoiceProxy.VoiceInputWrapper mVoiceInputWrapper; 81 /*-----------------------------------------------------------*/ 82 83 private boolean mIsNetworkConnected; 84 85 public static SubtypeSwitcher getInstance() { 86 return sInstance; 87 } 88 89 public static void init(LatinIME service) { 90 SubtypeLocale.init(service); 91 sInstance.initialize(service); 92 sInstance.updateAllParameters(); 93 } 94 95 private SubtypeSwitcher() { 96 // Intentional empty constructor for singleton. 97 } 98 99 private void initialize(LatinIME service) { 100 mService = service; 101 mResources = service.getResources(); 102 mImm = InputMethodManagerCompatWrapper.getInstance(); 103 mConnectivityManager = (ConnectivityManager) service.getSystemService( 104 Context.CONNECTIVITY_SERVICE); 105 mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); 106 mEnabledLanguagesOfCurrentInputMethod.clear(); 107 mSystemLocale = null; 108 mInputLocale = null; 109 mInputLocaleStr = null; 110 mCurrentSubtype = null; 111 mAllEnabledSubtypesOfCurrentInputMethod = null; 112 mVoiceInputWrapper = null; 113 114 final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); 115 mIsNetworkConnected = (info != null && info.isConnected()); 116 } 117 118 // Update all parameters stored in SubtypeSwitcher. 119 // Only configuration changed event is allowed to call this because this is heavy. 120 private void updateAllParameters() { 121 mSystemLocale = mResources.getConfiguration().locale; 122 updateSubtype(mImm.getCurrentInputMethodSubtype()); 123 updateParametersOnStartInputView(); 124 } 125 126 // Update parameters which are changed outside LatinIME. This parameters affect UI so they 127 // should be updated every time onStartInputview. 128 public void updateParametersOnStartInputView() { 129 updateEnabledSubtypes(); 130 updateShortcutIME(); 131 } 132 133 // Reload enabledSubtypes from the framework. 134 private void updateEnabledSubtypes() { 135 final String currentMode = getCurrentSubtypeMode(); 136 boolean foundCurrentSubtypeBecameDisabled = true; 137 mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList( 138 null, true); 139 mEnabledLanguagesOfCurrentInputMethod.clear(); 140 mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); 141 for (InputMethodSubtypeCompatWrapper ims : mAllEnabledSubtypesOfCurrentInputMethod) { 142 final String locale = getSubtypeLocale(ims); 143 final String mode = ims.getMode(); 144 mLocaleSplitter.setString(locale); 145 if (mLocaleSplitter.hasNext()) { 146 mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); 147 } 148 if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) { 149 foundCurrentSubtypeBecameDisabled = false; 150 } 151 if (KEYBOARD_MODE.equals(ims.getMode())) { 152 mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims); 153 } 154 } 155 mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 156 && mIsSystemLanguageSameAsInputLanguage); 157 if (foundCurrentSubtypeBecameDisabled) { 158 if (DBG) { 159 Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode); 160 Log.w(TAG, "Last subtype was disabled. Update to the current one."); 161 } 162 updateSubtype(mImm.getCurrentInputMethodSubtype()); 163 } 164 } 165 166 private void updateShortcutIME() { 167 if (DBG) { 168 Log.d(TAG, "Update shortcut IME from : " 169 + (mShortcutInputMethodInfo == null 170 ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " 171 + (mShortcutSubtype == null ? "<null>" : (getSubtypeLocale(mShortcutSubtype) 172 + ", " + mShortcutSubtype.getMode()))); 173 } 174 // TODO: Update an icon for shortcut IME 175 final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts = 176 mImm.getShortcutInputMethodsAndSubtypes(); 177 mShortcutInputMethodInfo = null; 178 mShortcutSubtype = null; 179 for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) { 180 List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi); 181 // TODO: Returns the first found IMI for now. Should handle all shortcuts as 182 // appropriate. 183 mShortcutInputMethodInfo = imi; 184 // TODO: Pick up the first found subtype for now. Should handle all subtypes 185 // as appropriate. 186 mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; 187 break; 188 } 189 if (DBG) { 190 Log.d(TAG, "Update shortcut IME to : " 191 + (mShortcutInputMethodInfo == null 192 ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " 193 + (mShortcutSubtype == null ? "<null>" : (getSubtypeLocale(mShortcutSubtype) 194 + ", " + mShortcutSubtype.getMode()))); 195 } 196 } 197 198 private static String getSubtypeLocale(InputMethodSubtypeCompatWrapper subtype) { 199 final String keyboardLocale = subtype.getExtraValueOf( 200 LatinIME.SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE); 201 return keyboardLocale != null ? keyboardLocale : subtype.getLocale(); 202 } 203 204 // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. 205 public void updateSubtype(InputMethodSubtypeCompatWrapper newSubtype) { 206 final String newLocale; 207 final String newMode; 208 final String oldMode = getCurrentSubtypeMode(); 209 if (newSubtype == null) { 210 // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, 211 // fallback to the default locale. 212 Log.w(TAG, "Couldn't get the current subtype."); 213 newLocale = "en_US"; 214 newMode = KEYBOARD_MODE; 215 } else { 216 newLocale = getSubtypeLocale(newSubtype); 217 newMode = newSubtype.getMode(); 218 } 219 if (DBG) { 220 Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode 221 + ", from: " + mInputLocaleStr + ", " + oldMode); 222 } 223 boolean languageChanged = false; 224 if (!newLocale.equals(mInputLocaleStr)) { 225 if (mInputLocaleStr != null) { 226 languageChanged = true; 227 } 228 updateInputLocale(newLocale); 229 } 230 boolean modeChanged = false; 231 if (!newMode.equals(oldMode)) { 232 if (oldMode != null) { 233 modeChanged = true; 234 } 235 } 236 mCurrentSubtype = newSubtype; 237 238 // If the old mode is voice input, we need to reset or cancel its status. 239 // We cancel its status when we change mode, while we reset otherwise. 240 if (isKeyboardMode()) { 241 if (modeChanged) { 242 if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) { 243 mVoiceInputWrapper.cancel(); 244 } 245 } 246 if (modeChanged || languageChanged) { 247 updateShortcutIME(); 248 mService.onRefreshKeyboard(); 249 } 250 } else if (isVoiceMode() && mVoiceInputWrapper != null) { 251 if (VOICE_MODE.equals(oldMode)) { 252 mVoiceInputWrapper.reset(); 253 } 254 // If needsToShowWarningDialog is true, voice input need to show warning before 255 // show recognition view. 256 if (languageChanged || modeChanged 257 || VoiceProxy.getInstance().needsToShowWarningDialog()) { 258 triggerVoiceIME(); 259 } 260 } else { 261 if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) { 262 // We need to reset the voice input to release the resources and to reset its status 263 // as it is not the current input mode. 264 mVoiceInputWrapper.reset(); 265 } 266 final String packageName = mService.getPackageName(); 267 int version = -1; 268 try { 269 version = mService.getPackageManager().getPackageInfo( 270 packageName, 0).versionCode; 271 } catch (NameNotFoundException e) { 272 } 273 Log.w(TAG, "Unknown subtype mode: " + newMode + "," + version + ", " + packageName 274 + ", " + mVoiceInputWrapper + ". IME is already changed to other IME."); 275 if (newSubtype != null) { 276 Log.w(TAG, "Subtype mode:" + newSubtype.getMode()); 277 Log.w(TAG, "Subtype locale:" + newSubtype.getLocale()); 278 Log.w(TAG, "Subtype extra value:" + newSubtype.getExtraValue()); 279 Log.w(TAG, "Subtype is auxiliary:" + newSubtype.isAuxiliary()); 280 } 281 } 282 } 283 284 // Update the current input locale from Locale string. 285 private void updateInputLocale(String inputLocaleStr) { 286 // example: inputLocaleStr = "en_US" "en" "" 287 // "en_US" --> language: en & country: US 288 // "en" --> language: en 289 // "" --> the system locale 290 if (!TextUtils.isEmpty(inputLocaleStr)) { 291 mInputLocale = LocaleUtils.constructLocaleFromString(inputLocaleStr); 292 mInputLocaleStr = inputLocaleStr; 293 } else { 294 mInputLocale = mSystemLocale; 295 String country = mSystemLocale.getCountry(); 296 mInputLocaleStr = mSystemLocale.getLanguage() 297 + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage()); 298 } 299 mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase( 300 getInputLocale().getLanguage()); 301 mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 302 && mIsSystemLanguageSameAsInputLanguage); 303 } 304 305 //////////////////////////// 306 // Shortcut IME functions // 307 //////////////////////////// 308 309 public void switchToShortcutIME() { 310 if (mShortcutInputMethodInfo == null) { 311 return; 312 } 313 314 final String imiId = mShortcutInputMethodInfo.getId(); 315 final InputMethodSubtypeCompatWrapper subtype = mShortcutSubtype; 316 switchToTargetIME(imiId, subtype); 317 } 318 319 private void switchToTargetIME( 320 final String imiId, final InputMethodSubtypeCompatWrapper subtype) { 321 final IBinder token = mService.getWindow().getWindow().getAttributes().token; 322 if (token == null) { 323 return; 324 } 325 new AsyncTask<Void, Void, Void>() { 326 @Override 327 protected Void doInBackground(Void... params) { 328 mImm.setInputMethodAndSubtype(token, imiId, subtype); 329 return null; 330 } 331 332 @Override 333 protected void onPostExecute(Void result) { 334 // Calls in this method need to be done in the same thread as the thread which 335 // called switchToShortcutIME(). 336 337 // Notify an event that the current subtype was changed. This event will be 338 // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented 339 // when the API level is 10 or previous. 340 mService.notifyOnCurrentInputMethodSubtypeChanged(subtype); 341 } 342 }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 343 } 344 345 public Drawable getShortcutIcon() { 346 return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype); 347 } 348 349 private Drawable getSubtypeIcon( 350 InputMethodInfoCompatWrapper imi, InputMethodSubtypeCompatWrapper subtype) { 351 final PackageManager pm = mService.getPackageManager(); 352 if (imi != null) { 353 final String imiPackageName = imi.getPackageName(); 354 if (DBG) { 355 Log.d(TAG, "Update icons of IME: " + imiPackageName + "," 356 + getSubtypeLocale(subtype) + "," + subtype.getMode()); 357 } 358 if (subtype != null) { 359 return pm.getDrawable(imiPackageName, subtype.getIconResId(), 360 imi.getServiceInfo().applicationInfo); 361 } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) { 362 return pm.getDrawable(imiPackageName, 363 imi.getSubtypeAt(0).getIconResId(), 364 imi.getServiceInfo().applicationInfo); 365 } else { 366 try { 367 return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm); 368 } catch (PackageManager.NameNotFoundException e) { 369 Log.w(TAG, "IME can't be found: " + imiPackageName); 370 } 371 } 372 } 373 return null; 374 } 375 376 private static boolean contains(String[] hay, String needle) { 377 for (String element : hay) { 378 if (element.equals(needle)) 379 return true; 380 } 381 return false; 382 } 383 384 public boolean isShortcutImeEnabled() { 385 if (mShortcutInputMethodInfo == null) { 386 return false; 387 } 388 if (mShortcutSubtype == null) { 389 return true; 390 } 391 // For compatibility, if the shortcut subtype is dummy, we assume the shortcut IME 392 // (built-in voice dummy subtype) is available. 393 if (!mShortcutSubtype.hasOriginalObject()) { 394 return true; 395 } 396 final boolean allowsImplicitlySelectedSubtypes = true; 397 for (final InputMethodSubtypeCompatWrapper enabledSubtype : 398 mImm.getEnabledInputMethodSubtypeList( 399 mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) { 400 if (enabledSubtype.equals(mShortcutSubtype)) { 401 return true; 402 } 403 } 404 return false; 405 } 406 407 public boolean isShortcutImeReady() { 408 if (mShortcutInputMethodInfo == null) 409 return false; 410 if (mShortcutSubtype == null) 411 return true; 412 if (contains(mShortcutSubtype.getExtraValue().split(","), 413 SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) { 414 return mIsNetworkConnected; 415 } 416 return true; 417 } 418 419 public void onNetworkStateChanged(Intent intent) { 420 final boolean noConnection = intent.getBooleanExtra( 421 ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); 422 mIsNetworkConnected = !noConnection; 423 424 final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); 425 final LatinKeyboard keyboard = switcher.getLatinKeyboard(); 426 if (keyboard != null) { 427 keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView()); 428 } 429 } 430 431 ////////////////////////////////// 432 // Language Switching functions // 433 ////////////////////////////////// 434 435 public int getEnabledKeyboardLocaleCount() { 436 return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); 437 } 438 439 public boolean needsToDisplayLanguage(Locale keyboardLocale) { 440 if (!keyboardLocale.equals(mInputLocale)) { 441 return false; 442 } 443 return mNeedsToDisplayLanguage; 444 } 445 446 public Locale getInputLocale() { 447 return mInputLocale; 448 } 449 450 public String getInputLocaleStr() { 451 return mInputLocaleStr; 452 } 453 454 public String[] getEnabledLanguages() { 455 int enabledLanguageCount = mEnabledLanguagesOfCurrentInputMethod.size(); 456 // Workaround for explicitly specifying the voice language 457 if (enabledLanguageCount == 1) { 458 mEnabledLanguagesOfCurrentInputMethod.add(mEnabledLanguagesOfCurrentInputMethod 459 .get(0)); 460 ++enabledLanguageCount; 461 } 462 return mEnabledLanguagesOfCurrentInputMethod.toArray(new String[enabledLanguageCount]); 463 } 464 465 public Locale getSystemLocale() { 466 return mSystemLocale; 467 } 468 469 public boolean isSystemLanguageSameAsInputLanguage() { 470 return mIsSystemLanguageSameAsInputLanguage; 471 } 472 473 public void onConfigurationChanged(Configuration conf) { 474 final Locale systemLocale = conf.locale; 475 // If system configuration was changed, update all parameters. 476 if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { 477 updateAllParameters(); 478 } 479 } 480 481 public boolean isKeyboardMode() { 482 return KEYBOARD_MODE.equals(getCurrentSubtypeMode()); 483 } 484 485 486 /////////////////////////// 487 // Voice Input functions // 488 /////////////////////////// 489 490 public boolean setVoiceInputWrapper(VoiceProxy.VoiceInputWrapper vi) { 491 if (mVoiceInputWrapper == null && vi != null) { 492 mVoiceInputWrapper = vi; 493 if (isVoiceMode()) { 494 if (DBG) { 495 Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr()); 496 } 497 triggerVoiceIME(); 498 return true; 499 } 500 } 501 return false; 502 } 503 504 public boolean isVoiceMode() { 505 return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode()); 506 } 507 508 public boolean isDummyVoiceMode() { 509 return mCurrentSubtype != null && mCurrentSubtype.getOriginalObject() == null 510 && VOICE_MODE.equals(getCurrentSubtypeMode()); 511 } 512 513 private void triggerVoiceIME() { 514 if (!mService.isInputViewShown()) return; 515 VoiceProxy.getInstance().startListening(false, 516 KeyboardSwitcher.getInstance().getKeyboardView().getWindowToken()); 517 } 518 519 public String getInputLanguageName() { 520 return Utils.getDisplayLanguage(getInputLocale()); 521 } 522 523 ///////////////////////////// 524 // Other utility functions // 525 ///////////////////////////// 526 527 public String getCurrentSubtypeExtraValue() { 528 // If null, return what an empty ExtraValue would return : the empty string. 529 return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : ""; 530 } 531 532 public boolean currentSubtypeContainsExtraValueKey(String key) { 533 // If null, return what an empty ExtraValue would return : false. 534 return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false; 535 } 536 537 public String getCurrentSubtypeExtraValueOf(String key) { 538 // If null, return what an empty ExtraValue would return : null. 539 return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null; 540 } 541 542 public String getCurrentSubtypeMode() { 543 return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE; 544 } 545 546 547 public static boolean isVoiceSupported(Context context, String locale) { 548 // Get the current list of supported locales and check the current locale against that 549 // list. We cache this value so as not to check it every time the user starts a voice 550 // input. Because this method is called by onStartInputView, this should mean that as 551 // long as the locale doesn't change while the user is keeping the IME open, the 552 // value should never be stale. 553 String supportedLocalesString = VoiceProxy.getSupportedLocalesString( 554 context.getContentResolver()); 555 List<String> voiceInputSupportedLocales = Arrays.asList( 556 supportedLocalesString.split("\\s+")); 557 return voiceInputSupportedLocales.contains(locale); 558 } 559 } 560