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