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.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