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