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
17 package com.android.internal.inputmethod;
18
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 import android.text.TextUtils;
22 import android.util.Log;
23 import android.util.Printer;
24 import android.util.Slog;
25 import android.view.inputmethod.InputMethodInfo;
26 import android.view.inputmethod.InputMethodSubtype;
27
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
30
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.Comparator;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Objects;
39 import java.util.TreeMap;
40
41 /**
42 * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes.
43 * <p>
44 * This class is designed to be used from and only from {@link InputMethodManagerService} by using
45 * {@link InputMethodManagerService#mMethodMap} as a global lock.
46 * </p>
47 */
48 public class InputMethodSubtypeSwitchingController {
49 private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName();
50 private static final boolean DEBUG = false;
51 private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
52
53 public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
54 public final CharSequence mImeName;
55 public final CharSequence mSubtypeName;
56 public final InputMethodInfo mImi;
57 public final int mSubtypeId;
58 public final boolean mIsSystemLocale;
59 public final boolean mIsSystemLanguage;
60
61 public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
62 InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
63 mImeName = imeName;
64 mSubtypeName = subtypeName;
65 mImi = imi;
66 mSubtypeId = subtypeId;
67 if (TextUtils.isEmpty(subtypeLocale)) {
68 mIsSystemLocale = false;
69 mIsSystemLanguage = false;
70 } else {
71 mIsSystemLocale = subtypeLocale.equals(systemLocale);
72 if (mIsSystemLocale) {
73 mIsSystemLanguage = true;
74 } else {
75 // TODO: Use Locale#getLanguage or Locale#toLanguageTag
76 final String systemLanguage = parseLanguageFromLocaleString(systemLocale);
77 final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
78 mIsSystemLanguage = systemLanguage.length() >= 2 &&
79 systemLanguage.equals(subtypeLanguage);
80 }
81 }
82 }
83
84 /**
85 * Returns the language component of a given locale string.
86 * TODO: Use {@link Locale#getLanguage()} instead.
87 */
88 private static String parseLanguageFromLocaleString(final String locale) {
89 final int idx = locale.indexOf('_');
90 if (idx < 0) {
91 return locale;
92 } else {
93 return locale.substring(0, idx);
94 }
95 }
96
97 @Override
98 public int compareTo(ImeSubtypeListItem other) {
99 if (TextUtils.isEmpty(mImeName)) {
100 return 1;
101 }
102 if (TextUtils.isEmpty(other.mImeName)) {
103 return -1;
104 }
105 if (!TextUtils.equals(mImeName, other.mImeName)) {
106 return mImeName.toString().compareTo(other.mImeName.toString());
107 }
108 if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
109 return 0;
110 }
111 if (mIsSystemLocale) {
112 return -1;
113 }
114 if (other.mIsSystemLocale) {
115 return 1;
116 }
117 if (mIsSystemLanguage) {
118 return -1;
119 }
120 if (other.mIsSystemLanguage) {
121 return 1;
122 }
123 if (TextUtils.isEmpty(mSubtypeName)) {
124 return 1;
125 }
126 if (TextUtils.isEmpty(other.mSubtypeName)) {
127 return -1;
128 }
129 return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
130 }
131
132 @Override
133 public String toString() {
134 return "ImeSubtypeListItem{"
135 + "mImeName=" + mImeName
136 + " mSubtypeName=" + mSubtypeName
137 + " mSubtypeId=" + mSubtypeId
138 + " mIsSystemLocale=" + mIsSystemLocale
139 + " mIsSystemLanguage=" + mIsSystemLanguage
140 + "}";
141 }
142
143 @Override
144 public boolean equals(Object o) {
145 if (o == this) {
146 return true;
147 }
148 if (o instanceof ImeSubtypeListItem) {
149 final ImeSubtypeListItem that = (ImeSubtypeListItem)o;
150 if (!Objects.equals(this.mImi, that.mImi)) {
151 return false;
152 }
153 if (this.mSubtypeId != that.mSubtypeId) {
154 return false;
155 }
156 return true;
157 }
158 return false;
159 }
160 }
161
162 private static class InputMethodAndSubtypeList {
163 private final Context mContext;
164 // Used to load label
165 private final PackageManager mPm;
166 private final String mSystemLocaleStr;
167 private final InputMethodSettings mSettings;
168
169 public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) {
170 mContext = context;
171 mSettings = settings;
172 mPm = context.getPackageManager();
173 final Locale locale = context.getResources().getConfiguration().locale;
174 mSystemLocaleStr = locale != null ? locale.toString() : "";
175 }
176
177 private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis =
178 new TreeMap<InputMethodInfo, List<InputMethodSubtype>>(
179 new Comparator<InputMethodInfo>() {
180 @Override
181 public int compare(InputMethodInfo imi1, InputMethodInfo imi2) {
182 if (imi2 == null)
183 return 0;
184 if (imi1 == null)
185 return 1;
186 if (mPm == null) {
187 return imi1.getId().compareTo(imi2.getId());
188 }
189 CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId();
190 CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId();
191 return imiId1.toString().compareTo(imiId2.toString());
192 }
193 });
194
195 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
196 return getSortedInputMethodAndSubtypeList(true, false, false);
197 }
198
199 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
200 boolean showSubtypes, boolean includeAuxiliarySubtypes, boolean isScreenLocked) {
201 final ArrayList<ImeSubtypeListItem> imList =
202 new ArrayList<ImeSubtypeListItem>();
203 final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
204 mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
205 mContext);
206 if (immis == null || immis.size() == 0) {
207 return Collections.emptyList();
208 }
209 if (isScreenLocked && includeAuxiliarySubtypes) {
210 if (DEBUG) {
211 Slog.w(TAG, "Auxiliary subtypes are not allowed to be shown in lock screen.");
212 }
213 includeAuxiliarySubtypes = false;
214 }
215 mSortedImmis.clear();
216 mSortedImmis.putAll(immis);
217 for (InputMethodInfo imi : mSortedImmis.keySet()) {
218 if (imi == null) {
219 continue;
220 }
221 List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
222 HashSet<String> enabledSubtypeSet = new HashSet<String>();
223 for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
224 enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
225 }
226 final CharSequence imeLabel = imi.loadLabel(mPm);
227 if (showSubtypes && enabledSubtypeSet.size() > 0) {
228 final int subtypeCount = imi.getSubtypeCount();
229 if (DEBUG) {
230 Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
231 }
232 for (int j = 0; j < subtypeCount; ++j) {
233 final InputMethodSubtype subtype = imi.getSubtypeAt(j);
234 final String subtypeHashCode = String.valueOf(subtype.hashCode());
235 // We show all enabled IMEs and subtypes when an IME is shown.
236 if (enabledSubtypeSet.contains(subtypeHashCode)
237 && (includeAuxiliarySubtypes || !subtype.isAuxiliary())) {
238 final CharSequence subtypeLabel =
239 subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
240 .getDisplayName(mContext, imi.getPackageName(),
241 imi.getServiceInfo().applicationInfo);
242 imList.add(new ImeSubtypeListItem(imeLabel,
243 subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
244
245 // Removing this subtype from enabledSubtypeSet because we no
246 // longer need to add an entry of this subtype to imList to avoid
247 // duplicated entries.
248 enabledSubtypeSet.remove(subtypeHashCode);
249 }
250 }
251 } else {
252 imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
253 mSystemLocaleStr));
254 }
255 }
256 Collections.sort(imList);
257 return imList;
258 }
259 }
260
261 private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) {
262 return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
263 subtype.hashCode()) : NOT_A_SUBTYPE_ID;
264 }
265
266 private static class StaticRotationList {
267 private final List<ImeSubtypeListItem> mImeSubtypeList;
268 public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) {
269 mImeSubtypeList = imeSubtypeList;
270 }
271
272 /**
273 * Returns the index of the specified input method and subtype in the given list.
274 * @param imi The {@link InputMethodInfo} to be searched.
275 * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method
276 * does not have a subtype.
277 * @return The index in the given list. -1 if not found.
278 */
279 private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) {
280 final int currentSubtypeId = calculateSubtypeId(imi, subtype);
281 final int N = mImeSubtypeList.size();
282 for (int i = 0; i < N; ++i) {
283 final ImeSubtypeListItem isli = mImeSubtypeList.get(i);
284 // Skip until the current IME/subtype is found.
285 if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) {
286 return i;
287 }
288 }
289 return -1;
290 }
291
292 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
293 InputMethodInfo imi, InputMethodSubtype subtype) {
294 if (imi == null) {
295 return null;
296 }
297 if (mImeSubtypeList.size() <= 1) {
298 return null;
299 }
300 final int currentIndex = getIndex(imi, subtype);
301 if (currentIndex < 0) {
302 return null;
303 }
304 final int N = mImeSubtypeList.size();
305 for (int offset = 1; offset < N; ++offset) {
306 // Start searching the next IME/subtype from the next of the current index.
307 final int candidateIndex = (currentIndex + offset) % N;
308 final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex);
309 // Skip if searching inside the current IME only, but the candidate is not
310 // the current IME.
311 if (onlyCurrentIme && !imi.equals(candidate.mImi)) {
312 continue;
313 }
314 return candidate;
315 }
316 return null;
317 }
318
319 protected void dump(final Printer pw, final String prefix) {
320 final int N = mImeSubtypeList.size();
321 for (int i = 0; i < N; ++i) {
322 final int rank = i;
323 final ImeSubtypeListItem item = mImeSubtypeList.get(i);
324 pw.println(prefix + "rank=" + rank + " item=" + item);
325 }
326 }
327 }
328
329 private static class DynamicRotationList {
330 private static final String TAG = DynamicRotationList.class.getSimpleName();
331 private final List<ImeSubtypeListItem> mImeSubtypeList;
332 private final int[] mUsageHistoryOfSubtypeListItemIndex;
333
334 private DynamicRotationList(final List<ImeSubtypeListItem> imeSubtypeListItems) {
335 mImeSubtypeList = imeSubtypeListItems;
336 mUsageHistoryOfSubtypeListItemIndex = new int[mImeSubtypeList.size()];
337 final int N = mImeSubtypeList.size();
338 for (int i = 0; i < N; i++) {
339 mUsageHistoryOfSubtypeListItemIndex[i] = i;
340 }
341 }
342
343 /**
344 * Returns the index of the specified object in
345 * {@link #mUsageHistoryOfSubtypeListItemIndex}.
346 * <p>We call the index of {@link #mUsageHistoryOfSubtypeListItemIndex} as "Usage Rank"
347 * so as not to be confused with the index in {@link #mImeSubtypeList}.
348 * @return -1 when the specified item doesn't belong to {@link #mImeSubtypeList} actually.
349 */
350 private int getUsageRank(final InputMethodInfo imi, InputMethodSubtype subtype) {
351 final int currentSubtypeId = calculateSubtypeId(imi, subtype);
352 final int N = mUsageHistoryOfSubtypeListItemIndex.length;
353 for (int usageRank = 0; usageRank < N; usageRank++) {
354 final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank];
355 final ImeSubtypeListItem subtypeListItem =
356 mImeSubtypeList.get(subtypeListItemIndex);
357 if (subtypeListItem.mImi.equals(imi) &&
358 subtypeListItem.mSubtypeId == currentSubtypeId) {
359 return usageRank;
360 }
361 }
362 // Not found in the known IME/Subtype list.
363 return -1;
364 }
365
366 public void onUserAction(InputMethodInfo imi, InputMethodSubtype subtype) {
367 final int currentUsageRank = getUsageRank(imi, subtype);
368 // Do nothing if currentUsageRank == -1 (not found), or currentUsageRank == 0
369 if (currentUsageRank <= 0) {
370 return;
371 }
372 final int currentItemIndex = mUsageHistoryOfSubtypeListItemIndex[currentUsageRank];
373 System.arraycopy(mUsageHistoryOfSubtypeListItemIndex, 0,
374 mUsageHistoryOfSubtypeListItemIndex, 1, currentUsageRank);
375 mUsageHistoryOfSubtypeListItemIndex[0] = currentItemIndex;
376 }
377
378 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
379 InputMethodInfo imi, InputMethodSubtype subtype) {
380 int currentUsageRank = getUsageRank(imi, subtype);
381 if (currentUsageRank < 0) {
382 if (DEBUG) {
383 Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype);
384 }
385 return null;
386 }
387 final int N = mUsageHistoryOfSubtypeListItemIndex.length;
388 for (int i = 1; i < N; i++) {
389 final int subtypeListItemRank = (currentUsageRank + i) % N;
390 final int subtypeListItemIndex =
391 mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank];
392 final ImeSubtypeListItem subtypeListItem =
393 mImeSubtypeList.get(subtypeListItemIndex);
394 if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) {
395 continue;
396 }
397 return subtypeListItem;
398 }
399 return null;
400 }
401
402 protected void dump(final Printer pw, final String prefix) {
403 for (int i = 0; i < mUsageHistoryOfSubtypeListItemIndex.length; ++i) {
404 final int rank = mUsageHistoryOfSubtypeListItemIndex[i];
405 final ImeSubtypeListItem item = mImeSubtypeList.get(i);
406 pw.println(prefix + "rank=" + rank + " item=" + item);
407 }
408 }
409 }
410
411 @VisibleForTesting
412 public static class ControllerImpl {
413 private final DynamicRotationList mSwitchingAwareRotationList;
414 private final StaticRotationList mSwitchingUnawareRotationList;
415
416 public static ControllerImpl createFrom(final ControllerImpl currentInstance,
417 final List<ImeSubtypeListItem> sortedEnabledItems) {
418 DynamicRotationList switchingAwareRotationList = null;
419 {
420 final List<ImeSubtypeListItem> switchingAwareImeSubtypes =
421 filterImeSubtypeList(sortedEnabledItems,
422 true /* supportsSwitchingToNextInputMethod */);
423 if (currentInstance != null &&
424 currentInstance.mSwitchingAwareRotationList != null &&
425 Objects.equals(currentInstance.mSwitchingAwareRotationList.mImeSubtypeList,
426 switchingAwareImeSubtypes)) {
427 // Can reuse the current instance.
428 switchingAwareRotationList = currentInstance.mSwitchingAwareRotationList;
429 }
430 if (switchingAwareRotationList == null) {
431 switchingAwareRotationList = new DynamicRotationList(switchingAwareImeSubtypes);
432 }
433 }
434
435 StaticRotationList switchingUnawareRotationList = null;
436 {
437 final List<ImeSubtypeListItem> switchingUnawareImeSubtypes = filterImeSubtypeList(
438 sortedEnabledItems, false /* supportsSwitchingToNextInputMethod */);
439 if (currentInstance != null &&
440 currentInstance.mSwitchingUnawareRotationList != null &&
441 Objects.equals(
442 currentInstance.mSwitchingUnawareRotationList.mImeSubtypeList,
443 switchingUnawareImeSubtypes)) {
444 // Can reuse the current instance.
445 switchingUnawareRotationList = currentInstance.mSwitchingUnawareRotationList;
446 }
447 if (switchingUnawareRotationList == null) {
448 switchingUnawareRotationList =
449 new StaticRotationList(switchingUnawareImeSubtypes);
450 }
451 }
452
453 return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList);
454 }
455
456 private ControllerImpl(final DynamicRotationList switchingAwareRotationList,
457 final StaticRotationList switchingUnawareRotationList) {
458 mSwitchingAwareRotationList = switchingAwareRotationList;
459 mSwitchingUnawareRotationList = switchingUnawareRotationList;
460 }
461
462 public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi,
463 InputMethodSubtype subtype) {
464 if (imi == null) {
465 return null;
466 }
467 if (imi.supportsSwitchingToNextInputMethod()) {
468 return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
469 subtype);
470 } else {
471 return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi,
472 subtype);
473 }
474 }
475
476 public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
477 if (imi == null) {
478 return;
479 }
480 if (imi.supportsSwitchingToNextInputMethod()) {
481 mSwitchingAwareRotationList.onUserAction(imi, subtype);
482 }
483 }
484
485 private static List<ImeSubtypeListItem> filterImeSubtypeList(
486 final List<ImeSubtypeListItem> items,
487 final boolean supportsSwitchingToNextInputMethod) {
488 final ArrayList<ImeSubtypeListItem> result = new ArrayList<>();
489 final int ALL_ITEMS_COUNT = items.size();
490 for (int i = 0; i < ALL_ITEMS_COUNT; i++) {
491 final ImeSubtypeListItem item = items.get(i);
492 if (item.mImi.supportsSwitchingToNextInputMethod() ==
493 supportsSwitchingToNextInputMethod) {
494 result.add(item);
495 }
496 }
497 return result;
498 }
499
500 protected void dump(final Printer pw) {
501 pw.println(" mSwitchingAwareRotationList:");
502 mSwitchingAwareRotationList.dump(pw, " ");
503 pw.println(" mSwitchingUnawareRotationList:");
504 mSwitchingUnawareRotationList.dump(pw, " ");
505 }
506 }
507
508 private final InputMethodSettings mSettings;
509 private InputMethodAndSubtypeList mSubtypeList;
510 private ControllerImpl mController;
511
512 private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) {
513 mSettings = settings;
514 resetCircularListLocked(context);
515 }
516
517 public static InputMethodSubtypeSwitchingController createInstanceLocked(
518 InputMethodSettings settings, Context context) {
519 return new InputMethodSubtypeSwitchingController(settings, context);
520 }
521
522 public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
523 if (mController == null) {
524 if (DEBUG) {
525 Log.e(TAG, "mController shouldn't be null.");
526 }
527 return;
528 }
529 mController.onUserActionLocked(imi, subtype);
530 }
531
532 public void resetCircularListLocked(Context context) {
533 mSubtypeList = new InputMethodAndSubtypeList(context, mSettings);
534 mController = ControllerImpl.createFrom(mController,
535 mSubtypeList.getSortedInputMethodAndSubtypeList());
536 }
537
538 public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi,
539 InputMethodSubtype subtype) {
540 if (mController == null) {
541 if (DEBUG) {
542 Log.e(TAG, "mController shouldn't be null.");
543 }
544 return null;
545 }
546 return mController.getNextInputMethod(onlyCurrentIme, imi, subtype);
547 }
548
549 public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes,
550 boolean includingAuxiliarySubtypes, boolean isScreenLocked) {
551 return mSubtypeList.getSortedInputMethodAndSubtypeList(
552 showSubtypes, includingAuxiliarySubtypes, isScreenLocked);
553 }
554
555 public void dump(final Printer pw) {
556 if (mController != null) {
557 mController.dump(pw);
558 } else {
559 pw.println(" mController=null");
560 }
561 }
562 }