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