1 /* 2 * Copyright (C) 2013 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.internal.inputmethod; 18 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.text.TextUtils; 22 import android.util.Log; 23 import android.util.Slog; 24 import android.view.inputmethod.InputMethodInfo; 25 import android.view.inputmethod.InputMethodSubtype; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; 29 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.Comparator; 33 import java.util.HashMap; 34 import java.util.HashSet; 35 import java.util.List; 36 import java.util.Locale; 37 import java.util.Objects; 38 import java.util.TreeMap; 39 40 /** 41 * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes. 42 * <p> 43 * This class is designed to be used from and only from {@link InputMethodManagerService} by using 44 * {@link InputMethodManagerService#mMethodMap} as a global lock. 45 * </p> 46 */ 47 public class InputMethodSubtypeSwitchingController { 48 private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName(); 49 private static final boolean DEBUG = false; 50 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; 51 52 public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { 53 public final CharSequence mImeName; 54 public final CharSequence mSubtypeName; 55 public final InputMethodInfo mImi; 56 public final int mSubtypeId; 57 public final boolean mIsSystemLocale; 58 public final boolean mIsSystemLanguage; 59 60 public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, 61 InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { 62 mImeName = imeName; 63 mSubtypeName = subtypeName; 64 mImi = imi; 65 mSubtypeId = subtypeId; 66 if (TextUtils.isEmpty(subtypeLocale)) { 67 mIsSystemLocale = false; 68 mIsSystemLanguage = false; 69 } else { 70 mIsSystemLocale = subtypeLocale.equals(systemLocale); 71 if (mIsSystemLocale) { 72 mIsSystemLanguage = true; 73 } else { 74 // TODO: Use Locale#getLanguage or Locale#toLanguageTag 75 final String systemLanguage = parseLanguageFromLocaleString(systemLocale); 76 final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); 77 mIsSystemLanguage = systemLanguage.length() >= 2 && 78 systemLanguage.equals(subtypeLanguage); 79 } 80 } 81 } 82 83 /** 84 * Returns the language component of a given locale string. 85 * TODO: Use {@link Locale#getLanguage()} instead. 86 */ 87 private static String parseLanguageFromLocaleString(final String locale) { 88 final int idx = locale.indexOf('_'); 89 if (idx < 0) { 90 return locale; 91 } else { 92 return locale.substring(0, idx); 93 } 94 } 95 96 @Override 97 public int compareTo(ImeSubtypeListItem other) { 98 if (TextUtils.isEmpty(mImeName)) { 99 return 1; 100 } 101 if (TextUtils.isEmpty(other.mImeName)) { 102 return -1; 103 } 104 if (!TextUtils.equals(mImeName, other.mImeName)) { 105 return mImeName.toString().compareTo(other.mImeName.toString()); 106 } 107 if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { 108 return 0; 109 } 110 if (mIsSystemLocale) { 111 return -1; 112 } 113 if (other.mIsSystemLocale) { 114 return 1; 115 } 116 if (mIsSystemLanguage) { 117 return -1; 118 } 119 if (other.mIsSystemLanguage) { 120 return 1; 121 } 122 if (TextUtils.isEmpty(mSubtypeName)) { 123 return 1; 124 } 125 if (TextUtils.isEmpty(other.mSubtypeName)) { 126 return -1; 127 } 128 return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); 129 } 130 131 @Override 132 public String toString() { 133 return "ImeSubtypeListItem{" 134 + "mImeName=" + mImeName 135 + " mSubtypeName=" + mSubtypeName 136 + " mSubtypeId=" + mSubtypeId 137 + " mIsSystemLocale=" + mIsSystemLocale 138 + " mIsSystemLanguage=" + mIsSystemLanguage 139 + "}"; 140 } 141 142 @Override 143 public boolean equals(Object o) { 144 if (o == this) { 145 return true; 146 } 147 if (o instanceof ImeSubtypeListItem) { 148 final ImeSubtypeListItem that = (ImeSubtypeListItem)o; 149 if (!Objects.equals(this.mImi, that.mImi)) { 150 return false; 151 } 152 if (this.mSubtypeId != that.mSubtypeId) { 153 return false; 154 } 155 return true; 156 } 157 return false; 158 } 159 } 160 161 private static class InputMethodAndSubtypeList { 162 private final Context mContext; 163 // Used to load label 164 private final PackageManager mPm; 165 private final String mSystemLocaleStr; 166 private final InputMethodSettings mSettings; 167 168 public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) { 169 mContext = context; 170 mSettings = settings; 171 mPm = context.getPackageManager(); 172 final Locale locale = context.getResources().getConfiguration().locale; 173 mSystemLocaleStr = locale != null ? locale.toString() : ""; 174 } 175 176 private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis = 177 new TreeMap<InputMethodInfo, List<InputMethodSubtype>>( 178 new Comparator<InputMethodInfo>() { 179 @Override 180 public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { 181 if (imi2 == null) 182 return 0; 183 if (imi1 == null) 184 return 1; 185 if (mPm == null) { 186 return imi1.getId().compareTo(imi2.getId()); 187 } 188 CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId(); 189 CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId(); 190 return imiId1.toString().compareTo(imiId2.toString()); 191 } 192 }); 193 194 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() { 195 return getSortedInputMethodAndSubtypeList(true, false, false); 196 } 197 198 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( 199 boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { 200 final ArrayList<ImeSubtypeListItem> imList = 201 new ArrayList<ImeSubtypeListItem>(); 202 final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = 203 mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked( 204 mContext); 205 if (immis == null || immis.size() == 0) { 206 return Collections.emptyList(); 207 } 208 mSortedImmis.clear(); 209 mSortedImmis.putAll(immis); 210 for (InputMethodInfo imi : mSortedImmis.keySet()) { 211 if (imi == null) { 212 continue; 213 } 214 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); 215 HashSet<String> enabledSubtypeSet = new HashSet<String>(); 216 for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { 217 enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); 218 } 219 final CharSequence imeLabel = imi.loadLabel(mPm); 220 if (showSubtypes && enabledSubtypeSet.size() > 0) { 221 final int subtypeCount = imi.getSubtypeCount(); 222 if (DEBUG) { 223 Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); 224 } 225 for (int j = 0; j < subtypeCount; ++j) { 226 final InputMethodSubtype subtype = imi.getSubtypeAt(j); 227 final String subtypeHashCode = String.valueOf(subtype.hashCode()); 228 // We show all enabled IMEs and subtypes when an IME is shown. 229 if (enabledSubtypeSet.contains(subtypeHashCode) 230 && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { 231 final CharSequence subtypeLabel = 232 subtype.overridesImplicitlyEnabledSubtype() ? null : subtype 233 .getDisplayName(mContext, imi.getPackageName(), 234 imi.getServiceInfo().applicationInfo); 235 imList.add(new ImeSubtypeListItem(imeLabel, 236 subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); 237 238 // Removing this subtype from enabledSubtypeSet because we no 239 // longer need to add an entry of this subtype to imList to avoid 240 // duplicated entries. 241 enabledSubtypeSet.remove(subtypeHashCode); 242 } 243 } 244 } else { 245 imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, 246 mSystemLocaleStr)); 247 } 248 } 249 Collections.sort(imList); 250 return imList; 251 } 252 } 253 254 private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) { 255 return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, 256 subtype.hashCode()) : NOT_A_SUBTYPE_ID; 257 } 258 259 private static class StaticRotationList { 260 private final List<ImeSubtypeListItem> mImeSubtypeList; 261 public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) { 262 mImeSubtypeList = imeSubtypeList; 263 } 264 265 /** 266 * Returns the index of the specified input method and subtype in the given list. 267 * @param imi The {@link InputMethodInfo} to be searched. 268 * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method 269 * does not have a subtype. 270 * @return The index in the given list. -1 if not found. 271 */ 272 private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) { 273 final int currentSubtypeId = calculateSubtypeId(imi, subtype); 274 final int N = mImeSubtypeList.size(); 275 for (int i = 0; i < N; ++i) { 276 final ImeSubtypeListItem isli = mImeSubtypeList.get(i); 277 // Skip until the current IME/subtype is found. 278 if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) { 279 return i; 280 } 281 } 282 return -1; 283 } 284 285 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, 286 InputMethodInfo imi, InputMethodSubtype subtype) { 287 if (imi == null) { 288 return null; 289 } 290 if (mImeSubtypeList.size() <= 1) { 291 return null; 292 } 293 final int currentIndex = getIndex(imi, subtype); 294 if (currentIndex < 0) { 295 return null; 296 } 297 final int N = mImeSubtypeList.size(); 298 for (int offset = 1; offset < N; ++offset) { 299 // Start searching the next IME/subtype from the next of the current index. 300 final int candidateIndex = (currentIndex + offset) % N; 301 final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex); 302 // Skip if searching inside the current IME only, but the candidate is not 303 // the current IME. 304 if (onlyCurrentIme && !imi.equals(candidate.mImi)) { 305 continue; 306 } 307 return candidate; 308 } 309 return null; 310 } 311 } 312 313 private static class DynamicRotationList { 314 private static final String TAG = DynamicRotationList.class.getSimpleName(); 315 private final List<ImeSubtypeListItem> mImeSubtypeList; 316 private final int[] mUsageHistoryOfSubtypeListItemIndex; 317 318 private DynamicRotationList(final List<ImeSubtypeListItem> imeSubtypeListItems) { 319 mImeSubtypeList = imeSubtypeListItems; 320 mUsageHistoryOfSubtypeListItemIndex = new int[mImeSubtypeList.size()]; 321 final int N = mImeSubtypeList.size(); 322 for (int i = 0; i < N; i++) { 323 mUsageHistoryOfSubtypeListItemIndex[i] = i; 324 } 325 } 326 327 /** 328 * Returns the index of the specified object in 329 * {@link #mUsageHistoryOfSubtypeListItemIndex}. 330 * <p>We call the index of {@link #mUsageHistoryOfSubtypeListItemIndex} as "Usage Rank" 331 * so as not to be confused with the index in {@link #mImeSubtypeList}. 332 * @return -1 when the specified item doesn't belong to {@link #mImeSubtypeList} actually. 333 */ 334 private int getUsageRank(final InputMethodInfo imi, InputMethodSubtype subtype) { 335 final int currentSubtypeId = calculateSubtypeId(imi, subtype); 336 final int N = mUsageHistoryOfSubtypeListItemIndex.length; 337 for (int usageRank = 0; usageRank < N; usageRank++) { 338 final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank]; 339 final ImeSubtypeListItem subtypeListItem = 340 mImeSubtypeList.get(subtypeListItemIndex); 341 if (subtypeListItem.mImi.equals(imi) && 342 subtypeListItem.mSubtypeId == currentSubtypeId) { 343 return usageRank; 344 } 345 } 346 // Not found in the known IME/Subtype list. 347 return -1; 348 } 349 350 public void onUserAction(InputMethodInfo imi, InputMethodSubtype subtype) { 351 final int currentUsageRank = getUsageRank(imi, subtype); 352 // Do nothing if currentUsageRank == -1 (not found), or currentUsageRank == 0 353 if (currentUsageRank <= 0) { 354 return; 355 } 356 final int currentItemIndex = mUsageHistoryOfSubtypeListItemIndex[currentUsageRank]; 357 System.arraycopy(mUsageHistoryOfSubtypeListItemIndex, 0, 358 mUsageHistoryOfSubtypeListItemIndex, 1, currentUsageRank); 359 mUsageHistoryOfSubtypeListItemIndex[0] = currentItemIndex; 360 } 361 362 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, 363 InputMethodInfo imi, InputMethodSubtype subtype) { 364 int currentUsageRank = getUsageRank(imi, subtype); 365 if (currentUsageRank < 0) { 366 if (DEBUG) { 367 Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype); 368 } 369 return null; 370 } 371 final int N = mUsageHistoryOfSubtypeListItemIndex.length; 372 for (int i = 1; i < N; i++) { 373 final int subtypeListItemRank = (currentUsageRank + i) % N; 374 final int subtypeListItemIndex = 375 mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank]; 376 final ImeSubtypeListItem subtypeListItem = 377 mImeSubtypeList.get(subtypeListItemIndex); 378 if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) { 379 continue; 380 } 381 return subtypeListItem; 382 } 383 return null; 384 } 385 } 386 387 @VisibleForTesting 388 public static class ControllerImpl { 389 private final DynamicRotationList mSwitchingAwareRotationList; 390 private final StaticRotationList mSwitchingUnawareRotationList; 391 392 public static ControllerImpl createFrom(final ControllerImpl currentInstance, 393 final List<ImeSubtypeListItem> sortedEnabledItems) { 394 DynamicRotationList switchingAwareRotationList = null; 395 { 396 final List<ImeSubtypeListItem> switchingAwareImeSubtypes = 397 filterImeSubtypeList(sortedEnabledItems, 398 true /* supportsSwitchingToNextInputMethod */); 399 if (currentInstance != null && 400 currentInstance.mSwitchingAwareRotationList != null && 401 Objects.equals(currentInstance.mSwitchingAwareRotationList.mImeSubtypeList, 402 switchingAwareImeSubtypes)) { 403 // Can reuse the current instance. 404 switchingAwareRotationList = currentInstance.mSwitchingAwareRotationList; 405 } 406 if (switchingAwareRotationList == null) { 407 switchingAwareRotationList = new DynamicRotationList(switchingAwareImeSubtypes); 408 } 409 } 410 411 StaticRotationList switchingUnawareRotationList = null; 412 { 413 final List<ImeSubtypeListItem> switchingUnawareImeSubtypes = filterImeSubtypeList( 414 sortedEnabledItems, false /* supportsSwitchingToNextInputMethod */); 415 if (currentInstance != null && 416 currentInstance.mSwitchingUnawareRotationList != null && 417 Objects.equals( 418 currentInstance.mSwitchingUnawareRotationList.mImeSubtypeList, 419 switchingUnawareImeSubtypes)) { 420 // Can reuse the current instance. 421 switchingUnawareRotationList = currentInstance.mSwitchingUnawareRotationList; 422 } 423 if (switchingUnawareRotationList == null) { 424 switchingUnawareRotationList = 425 new StaticRotationList(switchingUnawareImeSubtypes); 426 } 427 } 428 429 return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList); 430 } 431 432 private ControllerImpl(final DynamicRotationList switchingAwareRotationList, 433 final StaticRotationList switchingUnawareRotationList) { 434 mSwitchingAwareRotationList = switchingAwareRotationList; 435 mSwitchingUnawareRotationList = switchingUnawareRotationList; 436 } 437 438 public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi, 439 InputMethodSubtype subtype) { 440 if (imi == null) { 441 return null; 442 } 443 if (imi.supportsSwitchingToNextInputMethod()) { 444 return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, 445 subtype); 446 } else { 447 return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, 448 subtype); 449 } 450 } 451 452 public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) { 453 if (imi == null) { 454 return; 455 } 456 if (imi.supportsSwitchingToNextInputMethod()) { 457 mSwitchingAwareRotationList.onUserAction(imi, subtype); 458 } 459 } 460 461 private static List<ImeSubtypeListItem> filterImeSubtypeList( 462 final List<ImeSubtypeListItem> items, 463 final boolean supportsSwitchingToNextInputMethod) { 464 final ArrayList<ImeSubtypeListItem> result = new ArrayList<>(); 465 final int ALL_ITEMS_COUNT = items.size(); 466 for (int i = 0; i < ALL_ITEMS_COUNT; i++) { 467 final ImeSubtypeListItem item = items.get(i); 468 if (item.mImi.supportsSwitchingToNextInputMethod() == 469 supportsSwitchingToNextInputMethod) { 470 result.add(item); 471 } 472 } 473 return result; 474 } 475 } 476 477 private final InputMethodSettings mSettings; 478 private InputMethodAndSubtypeList mSubtypeList; 479 private ControllerImpl mController; 480 481 private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) { 482 mSettings = settings; 483 resetCircularListLocked(context); 484 } 485 486 public static InputMethodSubtypeSwitchingController createInstanceLocked( 487 InputMethodSettings settings, Context context) { 488 return new InputMethodSubtypeSwitchingController(settings, context); 489 } 490 491 public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) { 492 if (mController == null) { 493 if (DEBUG) { 494 Log.e(TAG, "mController shouldn't be null."); 495 } 496 return; 497 } 498 mController.onUserActionLocked(imi, subtype); 499 } 500 501 public void resetCircularListLocked(Context context) { 502 mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); 503 mController = ControllerImpl.createFrom(mController, 504 mSubtypeList.getSortedInputMethodAndSubtypeList()); 505 } 506 507 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi, 508 InputMethodSubtype subtype) { 509 if (mController == null) { 510 if (DEBUG) { 511 Log.e(TAG, "mController shouldn't be null."); 512 } 513 return null; 514 } 515 return mController.getNextInputMethod(onlyCurrentIme, imi, subtype); 516 } 517 518 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes, 519 boolean inputShown, boolean isScreenLocked) { 520 return mSubtypeList.getSortedInputMethodAndSubtypeList( 521 showSubtypes, inputShown, isScreenLocked); 522 } 523 } 524