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