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