InputMethodSubtypeSwitchingController.java revision 1c63079b55cbb161ec6bed731a751943e6ac5736
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.Slog; 23import android.view.inputmethod.InputMethodInfo; 24import android.view.inputmethod.InputMethodSubtype; 25 26import com.android.internal.annotations.VisibleForTesting; 27import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; 28 29import java.util.ArrayList; 30import java.util.Collections; 31import java.util.Comparator; 32import java.util.HashMap; 33import java.util.HashSet; 34import java.util.List; 35import java.util.Locale; 36import java.util.TreeMap; 37 38/** 39 * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes. 40 */ 41public class InputMethodSubtypeSwitchingController { 42 private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName(); 43 private static final boolean DEBUG = false; 44 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; 45 46 public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { 47 public final CharSequence mImeName; 48 public final CharSequence mSubtypeName; 49 public final InputMethodInfo mImi; 50 public final int mSubtypeId; 51 private final boolean mIsSystemLocale; 52 private final boolean mIsSystemLanguage; 53 54 public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, 55 InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { 56 mImeName = imeName; 57 mSubtypeName = subtypeName; 58 mImi = imi; 59 mSubtypeId = subtypeId; 60 if (TextUtils.isEmpty(subtypeLocale)) { 61 mIsSystemLocale = false; 62 mIsSystemLanguage = false; 63 } else { 64 mIsSystemLocale = subtypeLocale.equals(systemLocale); 65 mIsSystemLanguage = mIsSystemLocale 66 || subtypeLocale.startsWith(systemLocale.substring(0, 2)); 67 } 68 } 69 70 @Override 71 public int compareTo(ImeSubtypeListItem other) { 72 if (TextUtils.isEmpty(mImeName)) { 73 return 1; 74 } 75 if (TextUtils.isEmpty(other.mImeName)) { 76 return -1; 77 } 78 if (!TextUtils.equals(mImeName, other.mImeName)) { 79 return mImeName.toString().compareTo(other.mImeName.toString()); 80 } 81 if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { 82 return 0; 83 } 84 if (mIsSystemLocale) { 85 return -1; 86 } 87 if (other.mIsSystemLocale) { 88 return 1; 89 } 90 if (mIsSystemLanguage) { 91 return -1; 92 } 93 if (other.mIsSystemLanguage) { 94 return 1; 95 } 96 if (TextUtils.isEmpty(mSubtypeName)) { 97 return 1; 98 } 99 if (TextUtils.isEmpty(other.mSubtypeName)) { 100 return -1; 101 } 102 return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); 103 } 104 105 @Override 106 public String toString() { 107 return "ImeSubtypeListItem{" 108 + "mImeName=" + mImeName 109 + " mSubtypeName=" + mSubtypeName 110 + " mSubtypeId=" + mSubtypeId 111 + " mIsSystemLocale=" + mIsSystemLocale 112 + " mIsSystemLanguage=" + mIsSystemLanguage 113 + "}"; 114 } 115 } 116 117 private static class InputMethodAndSubtypeList { 118 private final Context mContext; 119 // Used to load label 120 private final PackageManager mPm; 121 private final String mSystemLocaleStr; 122 private final InputMethodSettings mSettings; 123 124 public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) { 125 mContext = context; 126 mSettings = settings; 127 mPm = context.getPackageManager(); 128 final Locale locale = context.getResources().getConfiguration().locale; 129 mSystemLocaleStr = locale != null ? locale.toString() : ""; 130 } 131 132 private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis = 133 new TreeMap<InputMethodInfo, List<InputMethodSubtype>>( 134 new Comparator<InputMethodInfo>() { 135 @Override 136 public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { 137 if (imi2 == null) 138 return 0; 139 if (imi1 == null) 140 return 1; 141 if (mPm == null) { 142 return imi1.getId().compareTo(imi2.getId()); 143 } 144 CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId(); 145 CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId(); 146 return imiId1.toString().compareTo(imiId2.toString()); 147 } 148 }); 149 150 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() { 151 return getSortedInputMethodAndSubtypeList(true, false, false); 152 } 153 154 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( 155 boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { 156 final ArrayList<ImeSubtypeListItem> imList = 157 new ArrayList<ImeSubtypeListItem>(); 158 final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = 159 mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked( 160 mContext); 161 if (immis == null || immis.size() == 0) { 162 return Collections.emptyList(); 163 } 164 mSortedImmis.clear(); 165 mSortedImmis.putAll(immis); 166 for (InputMethodInfo imi : mSortedImmis.keySet()) { 167 if (imi == null) { 168 continue; 169 } 170 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); 171 HashSet<String> enabledSubtypeSet = new HashSet<String>(); 172 for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { 173 enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); 174 } 175 final CharSequence imeLabel = imi.loadLabel(mPm); 176 if (showSubtypes && enabledSubtypeSet.size() > 0) { 177 final int subtypeCount = imi.getSubtypeCount(); 178 if (DEBUG) { 179 Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); 180 } 181 for (int j = 0; j < subtypeCount; ++j) { 182 final InputMethodSubtype subtype = imi.getSubtypeAt(j); 183 final String subtypeHashCode = String.valueOf(subtype.hashCode()); 184 // We show all enabled IMEs and subtypes when an IME is shown. 185 if (enabledSubtypeSet.contains(subtypeHashCode) 186 && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { 187 final CharSequence subtypeLabel = 188 subtype.overridesImplicitlyEnabledSubtype() ? null : subtype 189 .getDisplayName(mContext, imi.getPackageName(), 190 imi.getServiceInfo().applicationInfo); 191 imList.add(new ImeSubtypeListItem(imeLabel, 192 subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); 193 194 // Removing this subtype from enabledSubtypeSet because we no 195 // longer need to add an entry of this subtype to imList to avoid 196 // duplicated entries. 197 enabledSubtypeSet.remove(subtypeHashCode); 198 } 199 } 200 } else { 201 imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, 202 mSystemLocaleStr)); 203 } 204 } 205 Collections.sort(imList); 206 return imList; 207 } 208 } 209 210 private final Object mLock = new Object(); 211 private final InputMethodSettings mSettings; 212 private InputMethodAndSubtypeList mSubtypeList; 213 214 @VisibleForTesting 215 public static ImeSubtypeListItem getNextInputMethodImpl(List<ImeSubtypeListItem> imList, 216 boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { 217 if (imi == null) { 218 return null; 219 } 220 if (imList.size() <= 1) { 221 return null; 222 } 223 // Here we have two rotation groups, depending on the returned boolean value of 224 // {@link InputMethodInfo#supportsSwitchingToNextInputMethod()}. 225 final boolean expectedValueOfSupportsSwitchingToNextInputMethod = 226 imi.supportsSwitchingToNextInputMethod(); 227 final int N = imList.size(); 228 final int currentSubtypeId = 229 subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, 230 subtype.hashCode()) : NOT_A_SUBTYPE_ID; 231 for (int i = 0; i < N; ++i) { 232 final ImeSubtypeListItem isli = imList.get(i); 233 // Skip until the current IME/subtype is found. 234 if (!isli.mImi.equals(imi) || isli.mSubtypeId != currentSubtypeId) { 235 continue; 236 } 237 // Found the current IME/subtype. Start searching the next IME/subtype from here. 238 for (int j = 0; j < N - 1; ++j) { 239 final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N); 240 // Skip if the candidate doesn't belong to the expected rotation group. 241 if (expectedValueOfSupportsSwitchingToNextInputMethod != 242 candidate.mImi.supportsSwitchingToNextInputMethod()) { 243 continue; 244 } 245 // Skip if searching inside the current IME only, but the candidate is not 246 // the current IME. 247 if (onlyCurrentIme && !candidate.mImi.equals(imi)) { 248 continue; 249 } 250 return candidate; 251 } 252 // No appropriate IME/subtype is found in the list. Give up. 253 return null; 254 } 255 // The current IME/subtype is not found in the list. Give up. 256 return null; 257 } 258 259 public InputMethodSubtypeSwitchingController(InputMethodSettings settings) { 260 mSettings = settings; 261 } 262 263 // TODO: write unit tests for this method and the logic that determines the next subtype 264 public void onCommitText(InputMethodInfo imi, InputMethodSubtype subtype) { 265 // TODO: Implement this. 266 } 267 268 public void resetCircularListLocked(Context context) { 269 synchronized(mLock) { 270 mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); 271 } 272 } 273 274 public ImeSubtypeListItem getNextInputMethod( 275 boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { 276 synchronized(mLock) { 277 return getNextInputMethodImpl(mSubtypeList.getSortedInputMethodAndSubtypeList(), 278 onlyCurrentIme, imi, subtype); 279 } 280 } 281 282 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes, 283 boolean inputShown, boolean isScreenLocked) { 284 synchronized(mLock) { 285 return mSubtypeList.getSortedInputMethodAndSubtypeList( 286 showSubtypes, inputShown, isScreenLocked); 287 } 288 } 289} 290