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