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