1 /* 2 * Copyright (C) 2012 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 static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE; 20 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.os.Build; 24 import android.os.IBinder; 25 import android.preference.PreferenceManager; 26 import android.util.Log; 27 import android.view.inputmethod.InputMethodInfo; 28 import android.view.inputmethod.InputMethodManager; 29 import android.view.inputmethod.InputMethodSubtype; 30 31 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; 32 import com.android.inputmethod.latin.settings.Settings; 33 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; 34 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 35 36 import java.util.Collections; 37 import java.util.HashMap; 38 import java.util.List; 39 40 /** 41 * Enrichment class for InputMethodManager to simplify interaction and add functionality. 42 */ 43 public final class RichInputMethodManager { 44 private static final String TAG = RichInputMethodManager.class.getSimpleName(); 45 RichInputMethodManager()46 private RichInputMethodManager() { 47 // This utility class is not publicly instantiable. 48 } 49 50 private static final RichInputMethodManager sInstance = new RichInputMethodManager(); 51 52 private InputMethodManagerCompatWrapper mImmWrapper; 53 private InputMethodInfoCache mInputMethodInfoCache; 54 final HashMap<InputMethodInfo, List<InputMethodSubtype>> 55 mSubtypeListCacheWithImplicitlySelectedSubtypes = new HashMap<>(); 56 final HashMap<InputMethodInfo, List<InputMethodSubtype>> 57 mSubtypeListCacheWithoutImplicitlySelectedSubtypes = new HashMap<>(); 58 59 private static final int INDEX_NOT_FOUND = -1; 60 getInstance()61 public static RichInputMethodManager getInstance() { 62 sInstance.checkInitialized(); 63 return sInstance; 64 } 65 init(final Context context)66 public static void init(final Context context) { 67 sInstance.initInternal(context); 68 } 69 isInitialized()70 private boolean isInitialized() { 71 return mImmWrapper != null; 72 } 73 checkInitialized()74 private void checkInitialized() { 75 if (!isInitialized()) { 76 throw new RuntimeException(TAG + " is used before initialization"); 77 } 78 } 79 initInternal(final Context context)80 private void initInternal(final Context context) { 81 if (isInitialized()) { 82 return; 83 } 84 mImmWrapper = new InputMethodManagerCompatWrapper(context); 85 mInputMethodInfoCache = new InputMethodInfoCache( 86 mImmWrapper.mImm, context.getPackageName()); 87 88 // Initialize additional subtypes. 89 SubtypeLocaleUtils.init(context); 90 final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(context); 91 setAdditionalInputMethodSubtypes(additionalSubtypes); 92 } 93 getAdditionalSubtypes(final Context context)94 public InputMethodSubtype[] getAdditionalSubtypes(final Context context) { 95 SubtypeLocaleUtils.init(context); 96 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 97 final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes( 98 prefs, context.getResources()); 99 return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes); 100 } 101 getInputMethodManager()102 public InputMethodManager getInputMethodManager() { 103 checkInitialized(); 104 return mImmWrapper.mImm; 105 } 106 getMyEnabledInputMethodSubtypeList( boolean allowsImplicitlySelectedSubtypes)107 public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList( 108 boolean allowsImplicitlySelectedSubtypes) { 109 return getEnabledInputMethodSubtypeList( 110 getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes); 111 } 112 switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme)113 public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { 114 if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) { 115 return true; 116 } 117 // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)} 118 // because the current device is running ICS or previous and lacks the API. 119 if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) { 120 return true; 121 } 122 return switchToNextInputMethodAndSubtype(token); 123 } 124 switchToNextInputSubtypeInThisIme(final IBinder token, final boolean onlyCurrentIme)125 private boolean switchToNextInputSubtypeInThisIme(final IBinder token, 126 final boolean onlyCurrentIme) { 127 final InputMethodManager imm = mImmWrapper.mImm; 128 final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype(); 129 final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList( 130 true /* allowsImplicitlySelectedSubtypes */); 131 final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes); 132 if (currentIndex == INDEX_NOT_FOUND) { 133 Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" 134 + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype)); 135 return false; 136 } 137 final int nextIndex = (currentIndex + 1) % enabledSubtypes.size(); 138 if (nextIndex <= currentIndex && !onlyCurrentIme) { 139 // The current subtype is the last or only enabled one and it needs to switch to 140 // next IME. 141 return false; 142 } 143 final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex); 144 setInputMethodAndSubtype(token, nextSubtype); 145 return true; 146 } 147 switchToNextInputMethodAndSubtype(final IBinder token)148 private boolean switchToNextInputMethodAndSubtype(final IBinder token) { 149 final InputMethodManager imm = mImmWrapper.mImm; 150 final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); 151 final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis); 152 if (currentIndex == INDEX_NOT_FOUND) { 153 Log.w(TAG, "Can't find current IME in enabled IMEs: IME package=" 154 + getInputMethodInfoOfThisIme().getPackageName()); 155 return false; 156 } 157 final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis); 158 final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi, 159 true /* allowsImplicitlySelectedSubtypes */); 160 if (enabledSubtypes.isEmpty()) { 161 // The next IME has no subtype. 162 imm.setInputMethod(token, nextImi.getId()); 163 return true; 164 } 165 final InputMethodSubtype firstSubtype = enabledSubtypes.get(0); 166 imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype); 167 return true; 168 } 169 getImiIndexInList(final InputMethodInfo inputMethodInfo, final List<InputMethodInfo> imiList)170 private static int getImiIndexInList(final InputMethodInfo inputMethodInfo, 171 final List<InputMethodInfo> imiList) { 172 final int count = imiList.size(); 173 for (int index = 0; index < count; index++) { 174 final InputMethodInfo imi = imiList.get(index); 175 if (imi.equals(inputMethodInfo)) { 176 return index; 177 } 178 } 179 return INDEX_NOT_FOUND; 180 } 181 182 // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}. getNextNonAuxiliaryIme(final int currentIndex, final List<InputMethodInfo> imiList)183 private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex, 184 final List<InputMethodInfo> imiList) { 185 final int count = imiList.size(); 186 for (int i = 1; i < count; i++) { 187 final int nextIndex = (currentIndex + i) % count; 188 final InputMethodInfo nextImi = imiList.get(nextIndex); 189 if (!isAuxiliaryIme(nextImi)) { 190 return nextImi; 191 } 192 } 193 return imiList.get(currentIndex); 194 } 195 196 // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined. isAuxiliaryIme(final InputMethodInfo imi)197 private static boolean isAuxiliaryIme(final InputMethodInfo imi) { 198 final int count = imi.getSubtypeCount(); 199 if (count == 0) { 200 return false; 201 } 202 for (int index = 0; index < count; index++) { 203 final InputMethodSubtype subtype = imi.getSubtypeAt(index); 204 if (!subtype.isAuxiliary()) { 205 return false; 206 } 207 } 208 return true; 209 } 210 211 private static class InputMethodInfoCache { 212 private final InputMethodManager mImm; 213 private final String mImePackageName; 214 215 private InputMethodInfo mCachedValue; 216 InputMethodInfoCache(final InputMethodManager imm, final String imePackageName)217 public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) { 218 mImm = imm; 219 mImePackageName = imePackageName; 220 } 221 get()222 public synchronized InputMethodInfo get() { 223 if (mCachedValue != null) { 224 return mCachedValue; 225 } 226 for (final InputMethodInfo imi : mImm.getInputMethodList()) { 227 if (imi.getPackageName().equals(mImePackageName)) { 228 mCachedValue = imi; 229 return imi; 230 } 231 } 232 throw new RuntimeException("Input method id for " + mImePackageName + " not found."); 233 } 234 clear()235 public synchronized void clear() { 236 mCachedValue = null; 237 } 238 } 239 getInputMethodInfoOfThisIme()240 public InputMethodInfo getInputMethodInfoOfThisIme() { 241 return mInputMethodInfoCache.get(); 242 } 243 getInputMethodIdOfThisIme()244 public String getInputMethodIdOfThisIme() { 245 return getInputMethodInfoOfThisIme().getId(); 246 } 247 checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype)248 public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { 249 return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype); 250 } 251 checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( final InputMethodSubtype subtype)252 public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( 253 final InputMethodSubtype subtype) { 254 final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype); 255 final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList( 256 subtype, getMyEnabledInputMethodSubtypeList( 257 false /* allowsImplicitlySelectedSubtypes */)); 258 return subtypeEnabled && !subtypeExplicitlyEnabled; 259 } 260 checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi, final InputMethodSubtype subtype)261 public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi, 262 final InputMethodSubtype subtype) { 263 return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi, 264 true /* allowsImplicitlySelectedSubtypes */)); 265 } 266 checkIfSubtypeBelongsToList(final InputMethodSubtype subtype, final List<InputMethodSubtype> subtypes)267 private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype, 268 final List<InputMethodSubtype> subtypes) { 269 return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND; 270 } 271 getSubtypeIndexInList(final InputMethodSubtype subtype, final List<InputMethodSubtype> subtypes)272 private static int getSubtypeIndexInList(final InputMethodSubtype subtype, 273 final List<InputMethodSubtype> subtypes) { 274 final int count = subtypes.size(); 275 for (int index = 0; index < count; index++) { 276 final InputMethodSubtype ims = subtypes.get(index); 277 if (ims.equals(subtype)) { 278 return index; 279 } 280 } 281 return INDEX_NOT_FOUND; 282 } 283 checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype)284 public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { 285 return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND; 286 } 287 getSubtypeIndexInIme(final InputMethodSubtype subtype, final InputMethodInfo imi)288 private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, 289 final InputMethodInfo imi) { 290 final int count = imi.getSubtypeCount(); 291 for (int index = 0; index < count; index++) { 292 final InputMethodSubtype ims = imi.getSubtypeAt(index); 293 if (ims.equals(subtype)) { 294 return index; 295 } 296 } 297 return INDEX_NOT_FOUND; 298 } 299 getCurrentInputMethodSubtype( final InputMethodSubtype defaultSubtype)300 public InputMethodSubtype getCurrentInputMethodSubtype( 301 final InputMethodSubtype defaultSubtype) { 302 final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype(); 303 return (currentSubtype != null) ? currentSubtype : defaultSubtype; 304 } 305 hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes)306 public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { 307 final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList(); 308 return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis); 309 } 310 hasMultipleEnabledSubtypesInThisIme( final boolean shouldIncludeAuxiliarySubtypes)311 public boolean hasMultipleEnabledSubtypesInThisIme( 312 final boolean shouldIncludeAuxiliarySubtypes) { 313 final List<InputMethodInfo> imiList = Collections.singletonList( 314 getInputMethodInfoOfThisIme()); 315 return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); 316 } 317 hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes, final List<InputMethodInfo> imiList)318 private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes, 319 final List<InputMethodInfo> imiList) { 320 // Number of the filtered IMEs 321 int filteredImisCount = 0; 322 323 for (InputMethodInfo imi : imiList) { 324 // We can return true immediately after we find two or more filtered IMEs. 325 if (filteredImisCount > 1) return true; 326 final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true); 327 // IMEs that have no subtypes should be counted. 328 if (subtypes.isEmpty()) { 329 ++filteredImisCount; 330 continue; 331 } 332 333 int auxCount = 0; 334 for (InputMethodSubtype subtype : subtypes) { 335 if (subtype.isAuxiliary()) { 336 ++auxCount; 337 } 338 } 339 final int nonAuxCount = subtypes.size() - auxCount; 340 341 // IMEs that have one or more non-auxiliary subtypes should be counted. 342 // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary 343 // subtypes should be counted as well. 344 if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) { 345 ++filteredImisCount; 346 continue; 347 } 348 } 349 350 if (filteredImisCount > 1) { 351 return true; 352 } 353 final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true); 354 int keyboardCount = 0; 355 // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's 356 // both explicitly and implicitly enabled input method subtype. 357 // (The current IME should be LatinIME.) 358 for (InputMethodSubtype subtype : subtypes) { 359 if (KEYBOARD_MODE.equals(subtype.getMode())) { 360 ++keyboardCount; 361 } 362 } 363 return keyboardCount > 1; 364 } 365 findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString, final String keyboardLayoutSetName)366 public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString, 367 final String keyboardLayoutSetName) { 368 final InputMethodInfo myImi = getInputMethodInfoOfThisIme(); 369 final int count = myImi.getSubtypeCount(); 370 for (int i = 0; i < count; i++) { 371 final InputMethodSubtype subtype = myImi.getSubtypeAt(i); 372 final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); 373 if (localeString.equals(subtype.getLocale()) 374 && keyboardLayoutSetName.equals(layoutName)) { 375 return subtype; 376 } 377 } 378 return null; 379 } 380 setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype)381 public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) { 382 mImmWrapper.mImm.setInputMethodAndSubtype( 383 token, getInputMethodIdOfThisIme(), subtype); 384 } 385 setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes)386 public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) { 387 mImmWrapper.mImm.setAdditionalInputMethodSubtypes( 388 getInputMethodIdOfThisIme(), subtypes); 389 // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of 390 // subtypes again next time. 391 clearSubtypeCaches(); 392 } 393 getEnabledInputMethodSubtypeList(final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes)394 private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi, 395 final boolean allowsImplicitlySelectedSubtypes) { 396 final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache = 397 allowsImplicitlySelectedSubtypes 398 ? mSubtypeListCacheWithImplicitlySelectedSubtypes 399 : mSubtypeListCacheWithoutImplicitlySelectedSubtypes; 400 final List<InputMethodSubtype> cachedList = cache.get(imi); 401 if (null != cachedList) return cachedList; 402 final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList( 403 imi, allowsImplicitlySelectedSubtypes); 404 cache.put(imi, result); 405 return result; 406 } 407 clearSubtypeCaches()408 public void clearSubtypeCaches() { 409 mSubtypeListCacheWithImplicitlySelectedSubtypes.clear(); 410 mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear(); 411 mInputMethodInfoCache.clear(); 412 } 413 shouldOfferSwitchingToNextInputMethod(final IBinder binder, boolean defaultValue)414 public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder, 415 boolean defaultValue) { 416 // Use the default value instead on Jelly Bean MR2 and previous where 417 // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} isn't yet available 418 // and on KitKat where the API is still just a stub to return true always. 419 if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { 420 return defaultValue; 421 } 422 return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder); 423 } 424 } 425