InputMethodSubtypeSwitchingController.java revision 9b29d04565e1faf0a49054f538ed1881cb24fe12
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.TreeMap;
38
39/**
40 * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes.
41 * <p>
42 * This class is designed to be used from and only from {@link InputMethodManagerService} by using
43 * {@link InputMethodManagerService#mMethodMap} as a global lock.
44 * </p>
45 */
46public class InputMethodSubtypeSwitchingController {
47    private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName();
48    private static final boolean DEBUG = false;
49    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
50
51    public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
52        public final CharSequence mImeName;
53        public final CharSequence mSubtypeName;
54        public final InputMethodInfo mImi;
55        public final int mSubtypeId;
56        private final boolean mIsSystemLocale;
57        private final boolean mIsSystemLanguage;
58
59        public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
60                InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
61            mImeName = imeName;
62            mSubtypeName = subtypeName;
63            mImi = imi;
64            mSubtypeId = subtypeId;
65            if (TextUtils.isEmpty(subtypeLocale)) {
66                mIsSystemLocale = false;
67                mIsSystemLanguage = false;
68            } else {
69                mIsSystemLocale = subtypeLocale.equals(systemLocale);
70                mIsSystemLanguage = mIsSystemLocale
71                        || subtypeLocale.startsWith(systemLocale.substring(0, 2));
72            }
73        }
74
75        @Override
76        public int compareTo(ImeSubtypeListItem other) {
77            if (TextUtils.isEmpty(mImeName)) {
78                return 1;
79            }
80            if (TextUtils.isEmpty(other.mImeName)) {
81                return -1;
82            }
83            if (!TextUtils.equals(mImeName, other.mImeName)) {
84                return mImeName.toString().compareTo(other.mImeName.toString());
85            }
86            if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
87                return 0;
88            }
89            if (mIsSystemLocale) {
90                return -1;
91            }
92            if (other.mIsSystemLocale) {
93                return 1;
94            }
95            if (mIsSystemLanguage) {
96                return -1;
97            }
98            if (other.mIsSystemLanguage) {
99                return 1;
100            }
101            if (TextUtils.isEmpty(mSubtypeName)) {
102                return 1;
103            }
104            if (TextUtils.isEmpty(other.mSubtypeName)) {
105                return -1;
106            }
107            return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
108        }
109
110        @Override
111        public String toString() {
112            return "ImeSubtypeListItem{"
113                    + "mImeName=" + mImeName
114                    + " mSubtypeName=" + mSubtypeName
115                    + " mSubtypeId=" + mSubtypeId
116                    + " mIsSystemLocale=" + mIsSystemLocale
117                    + " mIsSystemLanguage=" + mIsSystemLanguage
118                    + "}";
119        }
120    }
121
122    private static class InputMethodAndSubtypeList {
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 InputMethodAndSubtypeList(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 List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
156            return getSortedInputMethodAndSubtypeList(true, false, false);
157        }
158
159        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
160                boolean showSubtypes, boolean inputShown, boolean isScreenLocked) {
161            final ArrayList<ImeSubtypeListItem> imList =
162                    new ArrayList<ImeSubtypeListItem>();
163            final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
164                    mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
165                            mContext);
166            if (immis == null || immis.size() == 0) {
167                return Collections.emptyList();
168            }
169            mSortedImmis.clear();
170            mSortedImmis.putAll(immis);
171            for (InputMethodInfo imi : mSortedImmis.keySet()) {
172                if (imi == null) {
173                    continue;
174                }
175                List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
176                HashSet<String> enabledSubtypeSet = new HashSet<String>();
177                for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
178                    enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
179                }
180                final CharSequence imeLabel = imi.loadLabel(mPm);
181                if (showSubtypes && enabledSubtypeSet.size() > 0) {
182                    final int subtypeCount = imi.getSubtypeCount();
183                    if (DEBUG) {
184                        Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
185                    }
186                    for (int j = 0; j < subtypeCount; ++j) {
187                        final InputMethodSubtype subtype = imi.getSubtypeAt(j);
188                        final String subtypeHashCode = String.valueOf(subtype.hashCode());
189                        // We show all enabled IMEs and subtypes when an IME is shown.
190                        if (enabledSubtypeSet.contains(subtypeHashCode)
191                                && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) {
192                            final CharSequence subtypeLabel =
193                                    subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
194                                            .getDisplayName(mContext, imi.getPackageName(),
195                                                    imi.getServiceInfo().applicationInfo);
196                            imList.add(new ImeSubtypeListItem(imeLabel,
197                                    subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
198
199                            // Removing this subtype from enabledSubtypeSet because we no
200                            // longer need to add an entry of this subtype to imList to avoid
201                            // duplicated entries.
202                            enabledSubtypeSet.remove(subtypeHashCode);
203                        }
204                    }
205                } else {
206                    imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
207                            mSystemLocaleStr));
208                }
209            }
210            Collections.sort(imList);
211            return imList;
212        }
213    }
214
215    private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) {
216        return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
217                subtype.hashCode()) : NOT_A_SUBTYPE_ID;
218    }
219
220    private static class StaticRotationList {
221        private final List<ImeSubtypeListItem> mImeSubtypeList;
222        public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) {
223            mImeSubtypeList = imeSubtypeList;
224        }
225
226        /**
227         * Returns the index of the specified input method and subtype in the given list.
228         * @param imi The {@link InputMethodInfo} to be searched.
229         * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method
230         * does not have a subtype.
231         * @return The index in the given list. -1 if not found.
232         */
233        private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) {
234            final int currentSubtypeId = calculateSubtypeId(imi, subtype);
235            final int N = mImeSubtypeList.size();
236            for (int i = 0; i < N; ++i) {
237                final ImeSubtypeListItem isli = mImeSubtypeList.get(i);
238                // Skip until the current IME/subtype is found.
239                if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) {
240                    return i;
241                }
242            }
243            return -1;
244        }
245
246        public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme,
247                InputMethodInfo imi, InputMethodSubtype subtype) {
248            if (imi == null) {
249                return null;
250            }
251            if (mImeSubtypeList.size() <= 1) {
252                return null;
253            }
254            final int currentIndex = getIndex(imi, subtype);
255            if (currentIndex < 0) {
256                return null;
257            }
258            final int N = mImeSubtypeList.size();
259            for (int offset = 1; offset < N; ++offset) {
260                // Start searching the next IME/subtype from the next of the current index.
261                final int candidateIndex = (currentIndex + offset) % N;
262                final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex);
263                // Skip if searching inside the current IME only, but the candidate is not
264                // the current IME.
265                if (onlyCurrentIme && !imi.equals(candidate.mImi)) {
266                    continue;
267                }
268                return candidate;
269            }
270            return null;
271        }
272    }
273
274    @VisibleForTesting
275    public static class ControllerImpl {
276        private final StaticRotationList mSwitchingAwareSubtypeList;
277        private final StaticRotationList mSwitchingUnawareSubtypeList;
278
279        public ControllerImpl(final List<ImeSubtypeListItem> sortedItems) {
280            mSwitchingAwareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems,
281                    true /* supportsSwitchingToNextInputMethod */));
282            mSwitchingUnawareSubtypeList = new StaticRotationList(filterImeSubtypeList(sortedItems,
283                    false /* supportsSwitchingToNextInputMethod */));
284        }
285
286        public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi,
287                InputMethodSubtype subtype) {
288            if (imi == null) {
289                return null;
290            }
291            if (imi.supportsSwitchingToNextInputMethod()) {
292                return mSwitchingAwareSubtypeList.getNextInputMethodLocked(onlyCurrentIme, imi,
293                        subtype);
294            } else {
295                return mSwitchingUnawareSubtypeList.getNextInputMethodLocked(onlyCurrentIme, imi,
296                        subtype);
297            }
298        }
299
300        private static List<ImeSubtypeListItem> filterImeSubtypeList(
301                final List<ImeSubtypeListItem> items,
302                final boolean supportsSwitchingToNextInputMethod) {
303            final ArrayList<ImeSubtypeListItem> result = new ArrayList<>();
304            final int ALL_ITEMS_COUNT = items.size();
305            for (int i = 0; i < ALL_ITEMS_COUNT; i++) {
306                final ImeSubtypeListItem item = items.get(i);
307                if (item.mImi.supportsSwitchingToNextInputMethod() ==
308                        supportsSwitchingToNextInputMethod) {
309                    result.add(item);
310                }
311            }
312            return result;
313        }
314    }
315
316    private final InputMethodSettings mSettings;
317    private InputMethodAndSubtypeList mSubtypeList;
318    private ControllerImpl mController;
319
320    private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) {
321        mSettings = settings;
322        resetCircularListLocked(context);
323    }
324
325    public static InputMethodSubtypeSwitchingController createInstanceLocked(
326            InputMethodSettings settings, Context context) {
327        return new InputMethodSubtypeSwitchingController(settings, context);
328    }
329
330    // TODO: write unit tests for this method and the logic that determines the next subtype
331    public void onCommitTextLocked(InputMethodInfo imi, InputMethodSubtype subtype) {
332        // TODO: Implement this.
333    }
334
335    public void resetCircularListLocked(Context context) {
336        mSubtypeList = new InputMethodAndSubtypeList(context, mSettings);
337        mController = new ControllerImpl(mSubtypeList.getSortedInputMethodAndSubtypeList());
338    }
339
340    public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi,
341            InputMethodSubtype subtype) {
342        if (mController == null) {
343            if (DEBUG) {
344                Log.e(TAG, "mController shouldn't be null.");
345            }
346            return null;
347        }
348        return mController.getNextInputMethod(onlyCurrentIme, imi, subtype);
349    }
350
351    public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes,
352            boolean inputShown, boolean isScreenLocked) {
353        return mSubtypeList.getSortedInputMethodAndSubtypeList(
354                showSubtypes, inputShown, isScreenLocked);
355    }
356}
357