blob: dcc0a4c715816af17ca6bf8f3d4a4bfe7e09c67b [file] [log] [blame]
Satoshi Kataokad7443c82013-10-15 17:45:43 +09001/*
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
Satoshi Kataokad787f692013-10-26 04:44:21 +090019import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
20
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.text.TextUtils;
Satoshi Kataokad7443c82013-10-15 17:45:43 +090024import android.util.Slog;
25import android.view.inputmethod.InputMethodInfo;
26import android.view.inputmethod.InputMethodSubtype;
27
28import java.util.ArrayDeque;
Satoshi Kataokad787f692013-10-26 04:44:21 +090029import 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;
Satoshi Kataokad7443c82013-10-15 17:45:43 +090037
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;
Satoshi Kataokaf03ba0c2013-10-28 16:55:38 -070044 // 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;
Satoshi Kataokad7443c82013-10-15 17:45:43 +090047 private static final int MAX_HISTORY_SIZE = 4;
Satoshi Kataokad787f692013-10-26 04:44:21 +090048 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
49
Satoshi Kataokad7443c82013-10-15 17:45:43 +090050 private static class SubtypeParams {
51 public final InputMethodInfo mImi;
52 public final InputMethodSubtype mSubtype;
53 public final long mTime;
Satoshi Kataokad787f692013-10-26 04:44:21 +090054
Satoshi Kataokad7443c82013-10-15 17:45:43 +090055 public SubtypeParams(InputMethodInfo imi, InputMethodSubtype subtype) {
56 mImi = imi;
57 mSubtype = subtype;
58 mTime = System.currentTimeMillis();
59 }
60 }
61
Satoshi Kataokad787f692013-10-26 04:44:21 +090062 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
Satoshi Kataokad7443c82013-10-15 17:45:43 +0900247 private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>();
Satoshi Kataokad787f692013-10-26 04:44:21 +0900248 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 }
Satoshi Kataokad7443c82013-10-15 17:45:43 +0900255
256 // TODO: write unit tests for this method and the logic that determines the next subtype
257 public void onCommitText(InputMethodInfo imi, InputMethodSubtype subtype) {
Satoshi Kataokad787f692013-10-26 04:44:21 +0900258 synchronized (mTypedSubtypeHistory) {
Satoshi Kataokad7443c82013-10-15 17:45:43 +0900259 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 }
Satoshi Kataokaf03ba0c2013-10-28 16:55:38 -0700266 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 }
Satoshi Kataokad7443c82013-10-15 17:45:43 +0900271 }
272 if (mTypedSubtypeHistory.size() >= MAX_HISTORY_SIZE) {
273 mTypedSubtypeHistory.poll();
274 }
275 mTypedSubtypeHistory.addFirst(new SubtypeParams(imi, subtype));
276 }
277 }
Satoshi Kataokad787f692013-10-26 04:44:21 +0900278
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 }
Satoshi Kataokad7443c82013-10-15 17:45:43 +0900299}