InputMethodSubtypeSwitchingController.java revision f03ba0cacea9f05d60e7b36058efa291b616fc8c
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.inputmethod.InputMethodUtils.InputMethodSettings; 20 21import android.content.Context; 22import android.content.pm.PackageManager; 23import android.text.TextUtils; 24import android.util.Slog; 25import android.view.inputmethod.InputMethodInfo; 26import android.view.inputmethod.InputMethodSubtype; 27 28import java.util.ArrayDeque; 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 // TODO: Turn on this flag and add CTS when the platform starts expecting that all IMEs return 45 // true for supportsSwitchingToNextInputMethod(). 46 private static final boolean REQUIRE_SWITCHING_SUPPORT = false; 47 private static final int MAX_HISTORY_SIZE = 4; 48 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; 49 50 private static class SubtypeParams { 51 public final InputMethodInfo mImi; 52 public final InputMethodSubtype mSubtype; 53 public final long mTime; 54 55 public SubtypeParams(InputMethodInfo imi, InputMethodSubtype subtype) { 56 mImi = imi; 57 mSubtype = subtype; 58 mTime = System.currentTimeMillis(); 59 } 60 } 61 62 public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> { 63 public final CharSequence mImeName; 64 public final CharSequence mSubtypeName; 65 public final InputMethodInfo mImi; 66 public final int mSubtypeId; 67 private final boolean mIsSystemLocale; 68 private final boolean mIsSystemLanguage; 69 70 public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName, 71 InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) { 72 mImeName = imeName; 73 mSubtypeName = subtypeName; 74 mImi = imi; 75 mSubtypeId = subtypeId; 76 if (TextUtils.isEmpty(subtypeLocale)) { 77 mIsSystemLocale = false; 78 mIsSystemLanguage = false; 79 } else { 80 mIsSystemLocale = subtypeLocale.equals(systemLocale); 81 mIsSystemLanguage = mIsSystemLocale 82 || subtypeLocale.startsWith(systemLocale.substring(0, 2)); 83 } 84 } 85 86 @Override 87 public int compareTo(ImeSubtypeListItem other) { 88 if (TextUtils.isEmpty(mImeName)) { 89 return 1; 90 } 91 if (TextUtils.isEmpty(other.mImeName)) { 92 return -1; 93 } 94 if (!TextUtils.equals(mImeName, other.mImeName)) { 95 return mImeName.toString().compareTo(other.mImeName.toString()); 96 } 97 if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) { 98 return 0; 99 } 100 if (mIsSystemLocale) { 101 return -1; 102 } 103 if (other.mIsSystemLocale) { 104 return 1; 105 } 106 if (mIsSystemLanguage) { 107 return -1; 108 } 109 if (other.mIsSystemLanguage) { 110 return 1; 111 } 112 if (TextUtils.isEmpty(mSubtypeName)) { 113 return 1; 114 } 115 if (TextUtils.isEmpty(other.mSubtypeName)) { 116 return -1; 117 } 118 return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); 119 } 120 } 121 122 private static class InputMethodAndSubtypeCircularList { 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 InputMethodAndSubtypeCircularList(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 ImeSubtypeListItem getNextInputMethod( 156 boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { 157 if (imi == null) { 158 return null; 159 } 160 final List<ImeSubtypeListItem> imList = 161 getSortedInputMethodAndSubtypeList(); 162 if (imList.size() <= 1) { 163 return null; 164 } 165 final int N = imList.size(); 166 final int currentSubtypeId = 167 subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, 168 subtype.hashCode()) : NOT_A_SUBTYPE_ID; 169 for (int i = 0; i < N; ++i) { 170 final ImeSubtypeListItem isli = imList.get(i); 171 if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) { 172 if (!onlyCurrentIme) { 173 return imList.get((i + 1) % N); 174 } 175 for (int j = 0; j < N - 1; ++j) { 176 final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N); 177 if (candidate.mImi.equals(imi)) { 178 return candidate; 179 } 180 } 181 return null; 182 } 183 } 184 return null; 185 } 186 187 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() { 188 return getSortedInputMethodAndSubtypeList(true, false, false); 189 } 190 191 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( 192 boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { 193 final ArrayList<ImeSubtypeListItem> imList = 194 new ArrayList<ImeSubtypeListItem>(); 195 final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = 196 mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked( 197 mContext); 198 if (immis == null || immis.size() == 0) { 199 return Collections.emptyList(); 200 } 201 mSortedImmis.clear(); 202 mSortedImmis.putAll(immis); 203 for (InputMethodInfo imi : mSortedImmis.keySet()) { 204 if (imi == null) { 205 continue; 206 } 207 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi); 208 HashSet<String> enabledSubtypeSet = new HashSet<String>(); 209 for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { 210 enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); 211 } 212 final CharSequence imeLabel = imi.loadLabel(mPm); 213 if (showSubtypes && enabledSubtypeSet.size() > 0) { 214 final int subtypeCount = imi.getSubtypeCount(); 215 if (DEBUG) { 216 Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId()); 217 } 218 for (int j = 0; j < subtypeCount; ++j) { 219 final InputMethodSubtype subtype = imi.getSubtypeAt(j); 220 final String subtypeHashCode = String.valueOf(subtype.hashCode()); 221 // We show all enabled IMEs and subtypes when an IME is shown. 222 if (enabledSubtypeSet.contains(subtypeHashCode) 223 && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { 224 final CharSequence subtypeLabel = 225 subtype.overridesImplicitlyEnabledSubtype() ? null : subtype 226 .getDisplayName(mContext, imi.getPackageName(), 227 imi.getServiceInfo().applicationInfo); 228 imList.add(new ImeSubtypeListItem(imeLabel, 229 subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr)); 230 231 // Removing this subtype from enabledSubtypeSet because we no 232 // longer need to add an entry of this subtype to imList to avoid 233 // duplicated entries. 234 enabledSubtypeSet.remove(subtypeHashCode); 235 } 236 } 237 } else { 238 imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null, 239 mSystemLocaleStr)); 240 } 241 } 242 Collections.sort(imList); 243 return imList; 244 } 245 } 246 247 private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>(); 248 private final Object mLock = new Object(); 249 private final InputMethodSettings mSettings; 250 private InputMethodAndSubtypeCircularList mSubtypeList; 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 InputMethodAndSubtypeCircularList(context, mSettings); 282 } 283 } 284 285 public ImeSubtypeListItem getNextInputMethod( 286 boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { 287 synchronized(mLock) { 288 return mSubtypeList.getNextInputMethod(onlyCurrentIme, imi, subtype); 289 } 290 } 291 292 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes, 293 boolean inputShown, boolean isScreenLocked) { 294 synchronized(mLock) { 295 return mSubtypeList.getSortedInputMethodAndSubtypeList( 296 showSubtypes, inputShown, isScreenLocked); 297 } 298 } 299} 300