InputMethodSubtypeSwitchingController.java revision 9b29d04565e1faf0a49054f538ed1881cb24fe12
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.TreeMap; 38 39/** 40 * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes. 41 * <p> 42 * This class is designed to be used from and only from {@link InputMethodManagerService} by using 43 * {@link InputMethodManagerService#mMethodMap} as a global lock. 44 * </p> 45 */ 46public class InputMethodSubtypeSwitchingController { 47 private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName(); 48 private static final boolean DEBUG = false; 49 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; 50 51 public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { 52 public final CharSequence mImeName; 53 public final CharSequence mSubtypeName; 54 public final InputMethodInfo mImi; 55 public final int mSubtypeId; 56 private final boolean mIsSystemLocale; 57 private final boolean mIsSystemLanguage; 58 59 public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, 60 InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { 61 mImeName = imeName; 62 mSubtypeName = subtypeName; 63 mImi = imi; 64 mSubtypeId = subtypeId; 65 if (TextUtils.isEmpty(subtypeLocale)) { 66 mIsSystemLocale = false; 67 mIsSystemLanguage = false; 68 } else { 69 mIsSystemLocale = subtypeLocale.equals(systemLocale); 70 mIsSystemLanguage = mIsSystemLocale 71 || subtypeLocale.startsWith(systemLocale.substring(0, 2)); 72 } 73 } 74 75 @Override 76 public int compareTo(ImeSubtypeListItem other) { 77 if (TextUtils.isEmpty(mImeName)) { 78 return 1; 79 } 80 if (TextUtils.isEmpty(other.mImeName)) { 81 return -1; 82 } 83 if (!TextUtils.equals(mImeName, other.mImeName)) { 84 return mImeName.toString().compareTo(other.mImeName.toString()); 85 } 86 if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { 87 return 0; 88 } 89 if (mIsSystemLocale) { 90 return -1; 91 } 92 if (other.mIsSystemLocale) { 93 return 1; 94 } 95 if (mIsSystemLanguage) { 96 return -1; 97 } 98 if (other.mIsSystemLanguage) { 99 return 1; 100 } 101 if (TextUtils.isEmpty(mSubtypeName)) { 102 return 1; 103 } 104 if (TextUtils.isEmpty(other.mSubtypeName)) { 105 return -1; 106 } 107 return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); 108 } 109 110 @Override 111 public String toString() { 112 return "ImeSubtypeListItem{" 113 + "mImeName=" + mImeName 114 + " mSubtypeName=" + mSubtypeName 115 + " mSubtypeId=" + mSubtypeId 116 + " mIsSystemLocale=" + mIsSystemLocale 117 + " mIsSystemLanguage=" + mIsSystemLanguage 118 + "}"; 119 } 120 } 121 122 private static class InputMethodAndSubtypeList { 123 private final Context mContext; 124 // Used to load label 125 private final PackageManager mPm; 126 private final String mSystemLocaleStr; 127 private final InputMethodSettings mSettings; 128 129 public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) { 130 mContext = context; 131 mSettings = settings; 132 mPm = context.getPackageManager(); 133 final Locale locale = context.getResources().getConfiguration().locale; 134 mSystemLocaleStr = locale != null ? locale.toString() : ""; 135 } 136 137 private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis = 138 new TreeMap<InputMethodInfo, List<InputMethodSubtype>>( 139 new Comparator<InputMethodInfo>() { 140 @Override 141 public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { 142 if (imi2 == null) 143 return 0; 144 if (imi1 == null) 145 return 1; 146 if (mPm == null) { 147 return imi1.getId().compareTo(imi2.getId()); 148 } 149 CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId(); 150 CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId(); 151 return imiId1.toString().compareTo(imiId2.toString()); 152 } 153 }); 154 155 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() { 156 return getSortedInputMethodAndSubtypeList(true, false, false); 157 } 158 159 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( 160 boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { 161 final ArrayList<ImeSubtypeListItem> imList = 162 new ArrayList<ImeSubtypeListItem>(); 163 final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = 164 mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked( 165 mContext); 166 if (immis == null || immis.size() == 0) { 167 return Collections.emptyList(); 168 } 169 mSortedImmis.clear(); 170 mSortedImmis.putAll(immis); 171 for (InputMethodInfo imi : mSortedImmis.keySet()) { 172 if (imi == null) { 173 continue; 174 } 175 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); 176 HashSet<String> enabledSubtypeSet = new HashSet<String>(); 177 for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { 178 enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); 179 } 180 final CharSequence imeLabel = imi.loadLabel(mPm); 181 if (showSubtypes && enabledSubtypeSet.size() > 0) { 182 final int subtypeCount = imi.getSubtypeCount(); 183 if (DEBUG) { 184 Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); 185 } 186 for (int j = 0; j < subtypeCount; ++j) { 187 final InputMethodSubtype subtype = imi.getSubtypeAt(j); 188 final String subtypeHashCode = String.valueOf(subtype.hashCode()); 189 // We show all enabled IMEs and subtypes when an IME is shown. 190 if (enabledSubtypeSet.contains(subtypeHashCode) 191 && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { 192 final CharSequence subtypeLabel = 193 subtype.overridesImplicitlyEnabledSubtype() ? null : subtype 194 .getDisplayName(mContext, imi.getPackageName(), 195 imi.getServiceInfo().applicationInfo); 196 imList.add(new ImeSubtypeListItem(imeLabel, 197 subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); 198 199 // Removing this subtype from enabledSubtypeSet because we no 200 // longer need to add an entry of this subtype to imList to avoid 201 // duplicated entries. 202 enabledSubtypeSet.remove(subtypeHashCode); 203 } 204 } 205 } else { 206 imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, 207 mSystemLocaleStr)); 208 } 209 } 210 Collections.sort(imList); 211 return imList; 212 } 213 } 214 215 private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) { 216 return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, 217 subtype.hashCode()) : NOT_A_SUBTYPE_ID; 218 } 219 220 private static class StaticRotationList { 221 private final List<ImeSubtypeListItem> mImeSubtypeList; 222 public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) { 223 mImeSubtypeList = imeSubtypeList; 224 } 225 226 /** 227 * Returns the index of the specified input method and subtype in the given list. 228 * @param imi The {@link InputMethodInfo} to be searched. 229 * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method 230 * does not have a subtype. 231 * @return The index in the given list. -1 if not found. 232 */ 233 private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) { 234 final int currentSubtypeId = calculateSubtypeId(imi, subtype); 235 final int N = mImeSubtypeList.size(); 236 for (int i = 0; i < N; ++i) { 237 final ImeSubtypeListItem isli = mImeSubtypeList.get(i); 238 // Skip until the current IME/subtype is found. 239 if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) { 240 return i; 241 } 242 } 243 return -1; 244 } 245 246 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, 247 InputMethodInfo imi, InputMethodSubtype subtype) { 248 if (imi == null) { 249 return null; 250 } 251 if (mImeSubtypeList.size() <= 1) { 252 return null; 253 } 254 final int currentIndex = getIndex(imi, subtype); 255 if (currentIndex < 0) { 256 return null; 257 } 258 final int N = mImeSubtypeList.size(); 259 for (int offset = 1; offset < N; ++offset) { 260 // Start searching the next IME/subtype from the next of the current index. 261 final int candidateIndex = (currentIndex + offset) % N; 262 final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex); 263 // Skip if searching inside the current IME only, but the candidate is not 264 // the current IME. 265 if (onlyCurrentIme && !imi.equals(candidate.mImi)) { 266 continue; 267 } 268 return candidate; 269 } 270 return null; 271 } 272 } 273 274 @VisibleForTesting 275 public static class ControllerImpl { 276 private final StaticRotationList mSwitchingAwareSubtypeList; 277 private final StaticRotationList mSwitchingUnawareSubtypeList; 278 279 public ControllerImpl(final List<ImeSubtypeListItem> sortedItems) { 280 mSwitchingAwareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems, 281 true /* supportsSwitchingToNextInputMethod */)); 282 mSwitchingUnawareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems, 283 false /* supportsSwitchingToNextInputMethod */)); 284 } 285 286 public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi, 287 InputMethodSubtype subtype) { 288 if (imi == null) { 289 return null; 290 } 291 if (imi.supportsSwitchingToNextInputMethod()) { 292 return mSwitchingAwareSubtypeList.getNextInputMethodLocked(onlyCurrentIme, imi, 293 subtype); 294 } else { 295 return mSwitchingUnawareSubtypeList.getNextInputMethodLocked(onlyCurrentIme, imi, 296 subtype); 297 } 298 } 299 300 private static List<ImeSubtypeListItem> filterImeSubtypeList( 301 final List<ImeSubtypeListItem> items, 302 final boolean supportsSwitchingToNextInputMethod) { 303 final ArrayList<ImeSubtypeListItem> result = new ArrayList<>(); 304 final int ALL_ITEMS_COUNT = items.size(); 305 for (int i = 0; i < ALL_ITEMS_COUNT; i++) { 306 final ImeSubtypeListItem item = items.get(i); 307 if (item.mImi.supportsSwitchingToNextInputMethod() == 308 supportsSwitchingToNextInputMethod) { 309 result.add(item); 310 } 311 } 312 return result; 313 } 314 } 315 316 private final InputMethodSettings mSettings; 317 private InputMethodAndSubtypeList mSubtypeList; 318 private ControllerImpl mController; 319 320 private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) { 321 mSettings = settings; 322 resetCircularListLocked(context); 323 } 324 325 public static InputMethodSubtypeSwitchingController createInstanceLocked( 326 InputMethodSettings settings, Context context) { 327 return new InputMethodSubtypeSwitchingController(settings, context); 328 } 329 330 // TODO: write unit tests for this method and the logic that determines the next subtype 331 public void onCommitTextLocked(InputMethodInfo imi, InputMethodSubtype subtype) { 332 // TODO: Implement this. 333 } 334 335 public void resetCircularListLocked(Context context) { 336 mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); 337 mController = new ControllerImpl(mSubtypeList.getSortedInputMethodAndSubtypeList()); 338 } 339 340 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi, 341 InputMethodSubtype subtype) { 342 if (mController == null) { 343 if (DEBUG) { 344 Log.e(TAG, "mController shouldn't be null."); 345 } 346 return null; 347 } 348 return mController.getNextInputMethod(onlyCurrentIme, imi, subtype); 349 } 350 351 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes, 352 boolean inputShown, boolean isScreenLocked) { 353 return mSubtypeList.getSortedInputMethodAndSubtypeList( 354 showSubtypes, inputShown, isScreenLocked); 355 } 356} 357