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 32 import java.util.Collections; 33 import java.util.List; 34 35 /** 36 * Enrichment class for InputMethodManager to simplify interaction and add functionality. 37 */ 38 public final class RichInputMethodManager { 39 private static final String TAG = RichInputMethodManager.class.getSimpleName(); 40 41 private RichInputMethodManager() { 42 // This utility class is not publicly instantiable. 43 } 44 45 private static final RichInputMethodManager sInstance = new RichInputMethodManager(); 46 47 private InputMethodManagerCompatWrapper mImmWrapper; 48 private InputMethodInfo mInputMethodInfoOfThisIme; 49 50 private static final int INDEX_NOT_FOUND = -1; 51 52 public static RichInputMethodManager getInstance() { 53 sInstance.checkInitialized(); 54 return sInstance; 55 } 56 57 // Caveat: This may cause IPC 58 public static boolean isInputMethodManagerValidForUserOfThisProcess(final Context context) { 59 // Basically called to check whether this IME has been triggered by the current user or not 60 return !((InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE)). 61 getInputMethodList().isEmpty(); 62 } 63 64 public static void init(final Context context) { 65 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 66 sInstance.initInternal(context, prefs); 67 } 68 69 private boolean isInitialized() { 70 return mImmWrapper != null; 71 } 72 73 private void checkInitialized() { 74 if (!isInitialized()) { 75 throw new RuntimeException(TAG + " is used before initialization"); 76 } 77 } 78 79 private void initInternal(final Context context, final SharedPreferences prefs) { 80 if (isInitialized()) { 81 return; 82 } 83 mImmWrapper = new InputMethodManagerCompatWrapper(context); 84 mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context); 85 86 // Initialize additional subtypes. 87 SubtypeLocale.init(context); 88 final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes( 89 prefs, context.getResources()); 90 final InputMethodSubtype[] additionalSubtypes = 91 AdditionalSubtype.createAdditionalSubtypesArray(prefAdditionalSubtypes); 92 setAdditionalInputMethodSubtypes(additionalSubtypes); 93 } 94 95 public InputMethodManager getInputMethodManager() { 96 checkInitialized(); 97 return mImmWrapper.mImm; 98 } 99 100 private InputMethodInfo getInputMethodInfoOfThisIme(final Context context) { 101 final String packageName = context.getPackageName(); 102 for (final InputMethodInfo imi : mImmWrapper.mImm.getInputMethodList()) { 103 if (imi.getPackageName().equals(packageName)) { 104 return imi; 105 } 106 } 107 throw new RuntimeException("Input method id for " + packageName + " not found."); 108 } 109 110 public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList( 111 boolean allowsImplicitlySelectedSubtypes) { 112 return mImmWrapper.mImm.getEnabledInputMethodSubtypeList( 113 mInputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes); 114 } 115 116 public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { 117 if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) { 118 return true; 119 } 120 // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)} 121 // because the current device is running ICS or previous and lacks the API. 122 if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) { 123 return true; 124 } 125 return switchToNextInputMethodAndSubtype(token); 126 } 127 128 private boolean switchToNextInputSubtypeInThisIme(final IBinder token, 129 final boolean onlyCurrentIme) { 130 final InputMethodManager imm = mImmWrapper.mImm; 131 final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype(); 132 final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList( 133 true /* allowsImplicitlySelectedSubtypes */); 134 final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes); 135 if (currentIndex == INDEX_NOT_FOUND) { 136 Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype=" 137 + SubtypeLocale.getSubtypeDisplayName(currentSubtype)); 138 return false; 139 } 140 final int nextIndex = (currentIndex + 1) % enabledSubtypes.size(); 141 if (nextIndex <= currentIndex && !onlyCurrentIme) { 142 // The current subtype is the last or only enabled one and it needs to switch to 143 // next IME. 144 return false; 145 } 146 final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex); 147 setInputMethodAndSubtype(token, nextSubtype); 148 return true; 149 } 150 151 private boolean switchToNextInputMethodAndSubtype(final IBinder token) { 152 final InputMethodManager imm = mImmWrapper.mImm; 153 final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList(); 154 final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis); 155 if (currentIndex == INDEX_NOT_FOUND) { 156 Log.w(TAG, "Can't find current IME in enabled IMEs: IME package=" 157 + mInputMethodInfoOfThisIme.getPackageName()); 158 return false; 159 } 160 final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis); 161 final List<InputMethodSubtype> enabledSubtypes = imm.getEnabledInputMethodSubtypeList( 162 nextImi, true /* allowsImplicitlySelectedSubtypes */); 163 if (enabledSubtypes.isEmpty()) { 164 // The next IME has no subtype. 165 imm.setInputMethod(token, nextImi.getId()); 166 return true; 167 } 168 final InputMethodSubtype firstSubtype = enabledSubtypes.get(0); 169 imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype); 170 return true; 171 } 172 173 private static int getImiIndexInList(final InputMethodInfo inputMethodInfo, 174 final List<InputMethodInfo> imiList) { 175 final int count = imiList.size(); 176 for (int index = 0; index < count; index++) { 177 final InputMethodInfo imi = imiList.get(index); 178 if (imi.equals(inputMethodInfo)) { 179 return index; 180 } 181 } 182 return INDEX_NOT_FOUND; 183 } 184 185 // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}. 186 private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex, 187 final List<InputMethodInfo> imiList) { 188 final int count = imiList.size(); 189 for (int i = 1; i < count; i++) { 190 final int nextIndex = (currentIndex + i) % count; 191 final InputMethodInfo nextImi = imiList.get(nextIndex); 192 if (!isAuxiliaryIme(nextImi)) { 193 return nextImi; 194 } 195 } 196 return imiList.get(currentIndex); 197 } 198 199 // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined. 200 private static boolean isAuxiliaryIme(final InputMethodInfo imi) { 201 final int count = imi.getSubtypeCount(); 202 if (count == 0) { 203 return false; 204 } 205 for (int index = 0; index < count; index++) { 206 final InputMethodSubtype subtype = imi.getSubtypeAt(index); 207 if (!subtype.isAuxiliary()) { 208 return false; 209 } 210 } 211 return true; 212 } 213 214 public InputMethodInfo getInputMethodInfoOfThisIme() { 215 return mInputMethodInfoOfThisIme; 216 } 217 218 public String getInputMethodIdOfThisIme() { 219 return mInputMethodInfoOfThisIme.getId(); 220 } 221 222 public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) { 223 return checkIfSubtypeBelongsToImeAndEnabled(mInputMethodInfoOfThisIme, subtype); 224 } 225 226 public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled( 227 final InputMethodSubtype subtype) { 228 final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype); 229 final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList( 230 subtype, getMyEnabledInputMethodSubtypeList( 231 false /* allowsImplicitlySelectedSubtypes */)); 232 return subtypeEnabled && !subtypeExplicitlyEnabled; 233 } 234 235 public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi, 236 final InputMethodSubtype subtype) { 237 return checkIfSubtypeBelongsToList( 238 subtype, mImmWrapper.mImm.getEnabledInputMethodSubtypeList( 239 imi, true /* allowsImplicitlySelectedSubtypes */)); 240 } 241 242 private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype, 243 final List<InputMethodSubtype> subtypes) { 244 return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND; 245 } 246 247 private static int getSubtypeIndexInList(final InputMethodSubtype subtype, 248 final List<InputMethodSubtype> subtypes) { 249 final int count = subtypes.size(); 250 for (int index = 0; index < count; index++) { 251 final InputMethodSubtype ims = subtypes.get(index); 252 if (ims.equals(subtype)) { 253 return index; 254 } 255 } 256 return INDEX_NOT_FOUND; 257 } 258 259 public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) { 260 return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND; 261 } 262 263 private static int getSubtypeIndexInIme(final InputMethodSubtype subtype, 264 final InputMethodInfo imi) { 265 final int count = imi.getSubtypeCount(); 266 for (int index = 0; index < count; index++) { 267 final InputMethodSubtype ims = imi.getSubtypeAt(index); 268 if (ims.equals(subtype)) { 269 return index; 270 } 271 } 272 return INDEX_NOT_FOUND; 273 } 274 275 public InputMethodSubtype getCurrentInputMethodSubtype( 276 final InputMethodSubtype defaultSubtype) { 277 final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype(); 278 return (currentSubtype != null) ? currentSubtype : defaultSubtype; 279 } 280 281 public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) { 282 final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList(); 283 return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis); 284 } 285 286 public boolean hasMultipleEnabledSubtypesInThisIme( 287 final boolean shouldIncludeAuxiliarySubtypes) { 288 final List<InputMethodInfo> imiList = Collections.singletonList(mInputMethodInfoOfThisIme); 289 return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList); 290 } 291 292 private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes, 293 final List<InputMethodInfo> imiList) { 294 // Number of the filtered IMEs 295 int filteredImisCount = 0; 296 297 for (InputMethodInfo imi : imiList) { 298 // We can return true immediately after we find two or more filtered IMEs. 299 if (filteredImisCount > 1) return true; 300 final List<InputMethodSubtype> subtypes = 301 mImmWrapper.mImm.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 = SubtypeLocale.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 } 365 } 366