InputMethodSubtypeSwitchingController.java revision 1c63079b55cbb161ec6bed731a751943e6ac5736
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.Slog;
23import android.view.inputmethod.InputMethodInfo;
24import android.view.inputmethod.InputMethodSubtype;
25
26import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
28
29import 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;
37
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;
44    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
45
46    public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
47        public final CharSequence mImeName;
48        public final CharSequence mSubtypeName;
49        public final InputMethodInfo mImi;
50        public final int mSubtypeId;
51        private final boolean mIsSystemLocale;
52        private final boolean mIsSystemLanguage;
53
54        public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
55                InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
56            mImeName = imeName;
57            mSubtypeName = subtypeName;
58            mImi = imi;
59            mSubtypeId = subtypeId;
60            if (TextUtils.isEmpty(subtypeLocale)) {
61                mIsSystemLocale = false;
62                mIsSystemLanguage = false;
63            } else {
64                mIsSystemLocale = subtypeLocale.equals(systemLocale);
65                mIsSystemLanguage = mIsSystemLocale
66                        || subtypeLocale.startsWith(systemLocale.substring(0, 2));
67            }
68        }
69
70        @Override
71        public int compareTo(ImeSubtypeListItem other) {
72            if (TextUtils.isEmpty(mImeName)) {
73                return 1;
74            }
75            if (TextUtils.isEmpty(other.mImeName)) {
76                return -1;
77            }
78            if (!TextUtils.equals(mImeName, other.mImeName)) {
79                return mImeName.toString().compareTo(other.mImeName.toString());
80            }
81            if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
82                return 0;
83            }
84            if (mIsSystemLocale) {
85                return -1;
86            }
87            if (other.mIsSystemLocale) {
88                return 1;
89            }
90            if (mIsSystemLanguage) {
91                return -1;
92            }
93            if (other.mIsSystemLanguage) {
94                return 1;
95            }
96            if (TextUtils.isEmpty(mSubtypeName)) {
97                return 1;
98            }
99            if (TextUtils.isEmpty(other.mSubtypeName)) {
100                return -1;
101            }
102            return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
103        }
104
105        @Override
106        public String toString() {
107            return "ImeSubtypeListItem{"
108                    + "mImeName=" + mImeName
109                    + " mSubtypeName=" + mSubtypeName
110                    + " mSubtypeId=" + mSubtypeId
111                    + " mIsSystemLocale=" + mIsSystemLocale
112                    + " mIsSystemLanguage=" + mIsSystemLanguage
113                    + "}";
114        }
115    }
116
117    private static class InputMethodAndSubtypeList {
118        private final Context mContext;
119        // Used to load label
120        private final PackageManager mPm;
121        private final String mSystemLocaleStr;
122        private final InputMethodSettings mSettings;
123
124        public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) {
125            mContext = context;
126            mSettings = settings;
127            mPm = context.getPackageManager();
128            final Locale locale = context.getResources().getConfiguration().locale;
129            mSystemLocaleStr = locale != null ? locale.toString() : "";
130        }
131
132        private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis =
133                new TreeMap<InputMethodInfo, List<InputMethodSubtype>>(
134                        new Comparator<InputMethodInfo>() {
135                            @Override
136                            public int compare(InputMethodInfo imi1, InputMethodInfo imi2) {
137                                if (imi2 == null)
138                                    return 0;
139                                if (imi1 == null)
140                                    return 1;
141                                if (mPm == null) {
142                                    return imi1.getId().compareTo(imi2.getId());
143                                }
144                                CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId();
145                                CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId();
146                                return imiId1.toString().compareTo(imiId2.toString());
147                            }
148                        });
149
150        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
151            return getSortedInputMethodAndSubtypeList(true, false, false);
152        }
153
154        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
155                boolean showSubtypes, boolean inputShown, boolean isScreenLocked) {
156            final ArrayList<ImeSubtypeListItem> imList =
157                    new ArrayList<ImeSubtypeListItem>();
158            final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
159                    mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
160                            mContext);
161            if (immis == null || immis.size() == 0) {
162                return Collections.emptyList();
163            }
164            mSortedImmis.clear();
165            mSortedImmis.putAll(immis);
166            for (InputMethodInfo imi : mSortedImmis.keySet()) {
167                if (imi == null) {
168                    continue;
169                }
170                List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
171                HashSet<String> enabledSubtypeSet = new HashSet<String>();
172                for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
173                    enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
174                }
175                final CharSequence imeLabel = imi.loadLabel(mPm);
176                if (showSubtypes && enabledSubtypeSet.size() > 0) {
177                    final int subtypeCount = imi.getSubtypeCount();
178                    if (DEBUG) {
179                        Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
180                    }
181                    for (int j = 0; j < subtypeCount; ++j) {
182                        final InputMethodSubtype subtype = imi.getSubtypeAt(j);
183                        final String subtypeHashCode = String.valueOf(subtype.hashCode());
184                        // We show all enabled IMEs and subtypes when an IME is shown.
185                        if (enabledSubtypeSet.contains(subtypeHashCode)
186                                && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) {
187                            final CharSequence subtypeLabel =
188                                    subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
189                                            .getDisplayName(mContext, imi.getPackageName(),
190                                                    imi.getServiceInfo().applicationInfo);
191                            imList.add(new ImeSubtypeListItem(imeLabel,
192                                    subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
193
194                            // Removing this subtype from enabledSubtypeSet because we no
195                            // longer need to add an entry of this subtype to imList to avoid
196                            // duplicated entries.
197                            enabledSubtypeSet.remove(subtypeHashCode);
198                        }
199                    }
200                } else {
201                    imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
202                            mSystemLocaleStr));
203                }
204            }
205            Collections.sort(imList);
206            return imList;
207        }
208    }
209
210    private final Object mLock = new Object();
211    private final InputMethodSettings mSettings;
212    private InputMethodAndSubtypeList mSubtypeList;
213
214    @VisibleForTesting
215    public static ImeSubtypeListItem getNextInputMethodImpl(List<ImeSubtypeListItem> imList,
216            boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
217        if (imi == null) {
218            return null;
219        }
220        if (imList.size() <= 1) {
221            return null;
222        }
223        // Here we have two rotation groups, depending on the returned boolean value of
224        // {@link InputMethodInfo#supportsSwitchingToNextInputMethod()}.
225        final boolean expectedValueOfSupportsSwitchingToNextInputMethod =
226                imi.supportsSwitchingToNextInputMethod();
227        final int N = imList.size();
228        final int currentSubtypeId =
229                subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
230                        subtype.hashCode()) : NOT_A_SUBTYPE_ID;
231        for (int i = 0; i < N; ++i) {
232            final ImeSubtypeListItem isli = imList.get(i);
233            // Skip until the current IME/subtype is found.
234            if (!isli.mImi.equals(imi) || isli.mSubtypeId != currentSubtypeId) {
235                continue;
236            }
237            // Found the current IME/subtype. Start searching the next IME/subtype from here.
238            for (int j = 0; j < N - 1; ++j) {
239                final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N);
240                // Skip if the candidate doesn't belong to the expected rotation group.
241                if (expectedValueOfSupportsSwitchingToNextInputMethod !=
242                        candidate.mImi.supportsSwitchingToNextInputMethod()) {
243                    continue;
244                }
245                // Skip if searching inside the current IME only, but the candidate is not
246                // the current IME.
247                if (onlyCurrentIme && !candidate.mImi.equals(imi)) {
248                    continue;
249                }
250                return candidate;
251            }
252            // No appropriate IME/subtype is found in the list. Give up.
253            return null;
254        }
255        // The current IME/subtype is not found in the list. Give up.
256        return null;
257    }
258
259    public InputMethodSubtypeSwitchingController(InputMethodSettings settings) {
260        mSettings = settings;
261    }
262
263    // TODO: write unit tests for this method and the logic that determines the next subtype
264    public void onCommitText(InputMethodInfo imi, InputMethodSubtype subtype) {
265        // TODO: Implement this.
266    }
267
268    public void resetCircularListLocked(Context context) {
269        synchronized(mLock) {
270            mSubtypeList = new InputMethodAndSubtypeList(context, mSettings);
271        }
272    }
273
274    public ImeSubtypeListItem getNextInputMethod(
275            boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
276        synchronized(mLock) {
277            return getNextInputMethodImpl(mSubtypeList.getSortedInputMethodAndSubtypeList(),
278                    onlyCurrentIme, imi, subtype);
279        }
280    }
281
282    public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes,
283            boolean inputShown, boolean isScreenLocked) {
284        synchronized(mLock) {
285            return mSubtypeList.getSortedInputMethodAndSubtypeList(
286                    showSubtypes, inputShown, isScreenLocked);
287        }
288    }
289}
290