InputMethodSubtypeSwitchingController.java revision f03ba0cacea9f05d60e7b36058efa291b616fc8c
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 com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
20
21import android.content.Context;
22import android.content.pm.PackageManager;
23import android.text.TextUtils;
24import android.util.Slog;
25import android.view.inputmethod.InputMethodInfo;
26import android.view.inputmethod.InputMethodSubtype;
27
28import java.util.ArrayDeque;
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    // TODO: Turn on this flag and add CTS when the platform starts expecting that all IMEs return
45    // true for supportsSwitchingToNextInputMethod().
46    private static final boolean REQUIRE_SWITCHING_SUPPORT = false;
47    private static final int MAX_HISTORY_SIZE = 4;
48    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
49
50    private static class SubtypeParams {
51        public final InputMethodInfo mImi;
52        public final InputMethodSubtype mSubtype;
53        public final long mTime;
54
55        public SubtypeParams(InputMethodInfo imi, InputMethodSubtype subtype) {
56            mImi = imi;
57            mSubtype = subtype;
58            mTime = System.currentTimeMillis();
59        }
60    }
61
62    public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
63        public final CharSequence mImeName;
64        public final CharSequence mSubtypeName;
65        public final InputMethodInfo mImi;
66        public final int mSubtypeId;
67        private final boolean mIsSystemLocale;
68        private final boolean mIsSystemLanguage;
69
70        public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
71                InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
72            mImeName = imeName;
73            mSubtypeName = subtypeName;
74            mImi = imi;
75            mSubtypeId = subtypeId;
76            if (TextUtils.isEmpty(subtypeLocale)) {
77                mIsSystemLocale = false;
78                mIsSystemLanguage = false;
79            } else {
80                mIsSystemLocale = subtypeLocale.equals(systemLocale);
81                mIsSystemLanguage = mIsSystemLocale
82                        || subtypeLocale.startsWith(systemLocale.substring(0, 2));
83            }
84        }
85
86        @Override
87        public int compareTo(ImeSubtypeListItem other) {
88            if (TextUtils.isEmpty(mImeName)) {
89                return 1;
90            }
91            if (TextUtils.isEmpty(other.mImeName)) {
92                return -1;
93            }
94            if (!TextUtils.equals(mImeName, other.mImeName)) {
95                return mImeName.toString().compareTo(other.mImeName.toString());
96            }
97            if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
98                return 0;
99            }
100            if (mIsSystemLocale) {
101                return -1;
102            }
103            if (other.mIsSystemLocale) {
104                return 1;
105            }
106            if (mIsSystemLanguage) {
107                return -1;
108            }
109            if (other.mIsSystemLanguage) {
110                return 1;
111            }
112            if (TextUtils.isEmpty(mSubtypeName)) {
113                return 1;
114            }
115            if (TextUtils.isEmpty(other.mSubtypeName)) {
116                return -1;
117            }
118            return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
119        }
120    }
121
122    private static class InputMethodAndSubtypeCircularList {
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 InputMethodAndSubtypeCircularList(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 ImeSubtypeListItem getNextInputMethod(
156                boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
157            if (imi == null) {
158                return null;
159            }
160            final List<ImeSubtypeListItem> imList =
161                    getSortedInputMethodAndSubtypeList();
162            if (imList.size() <= 1) {
163                return null;
164            }
165            final int N = imList.size();
166            final int currentSubtypeId =
167                    subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
168                            subtype.hashCode()) : NOT_A_SUBTYPE_ID;
169            for (int i = 0; i < N; ++i) {
170                final ImeSubtypeListItem isli = imList.get(i);
171                if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) {
172                    if (!onlyCurrentIme) {
173                        return imList.get((i + 1) % N);
174                    }
175                    for (int j = 0; j < N - 1; ++j) {
176                        final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N);
177                        if (candidate.mImi.equals(imi)) {
178                            return candidate;
179                        }
180                    }
181                    return null;
182                }
183            }
184            return null;
185        }
186
187        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
188            return getSortedInputMethodAndSubtypeList(true, false, false);
189        }
190
191        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
192                boolean showSubtypes, boolean inputShown, boolean isScreenLocked) {
193            final ArrayList<ImeSubtypeListItem> imList =
194                    new ArrayList<ImeSubtypeListItem>();
195            final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
196                    mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
197                            mContext);
198            if (immis == null || immis.size() == 0) {
199                return Collections.emptyList();
200            }
201            mSortedImmis.clear();
202            mSortedImmis.putAll(immis);
203            for (InputMethodInfo imi : mSortedImmis.keySet()) {
204                if (imi == null) {
205                    continue;
206                }
207                List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
208                HashSet<String> enabledSubtypeSet = new HashSet<String>();
209                for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
210                    enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
211                }
212                final CharSequence imeLabel = imi.loadLabel(mPm);
213                if (showSubtypes && enabledSubtypeSet.size() > 0) {
214                    final int subtypeCount = imi.getSubtypeCount();
215                    if (DEBUG) {
216                        Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
217                    }
218                    for (int j = 0; j < subtypeCount; ++j) {
219                        final InputMethodSubtype subtype = imi.getSubtypeAt(j);
220                        final String subtypeHashCode = String.valueOf(subtype.hashCode());
221                        // We show all enabled IMEs and subtypes when an IME is shown.
222                        if (enabledSubtypeSet.contains(subtypeHashCode)
223                                && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) {
224                            final CharSequence subtypeLabel =
225                                    subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
226                                            .getDisplayName(mContext, imi.getPackageName(),
227                                                    imi.getServiceInfo().applicationInfo);
228                            imList.add(new ImeSubtypeListItem(imeLabel,
229                                    subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
230
231                            // Removing this subtype from enabledSubtypeSet because we no
232                            // longer need to add an entry of this subtype to imList to avoid
233                            // duplicated entries.
234                            enabledSubtypeSet.remove(subtypeHashCode);
235                        }
236                    }
237                } else {
238                    imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
239                            mSystemLocaleStr));
240                }
241            }
242            Collections.sort(imList);
243            return imList;
244        }
245    }
246
247    private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>();
248    private final Object mLock = new Object();
249    private final InputMethodSettings mSettings;
250    private InputMethodAndSubtypeCircularList mSubtypeList;
251
252    public InputMethodSubtypeSwitchingController(InputMethodSettings settings) {
253        mSettings = settings;
254    }
255
256    // TODO: write unit tests for this method and the logic that determines the next subtype
257    public void onCommitText(InputMethodInfo imi, InputMethodSubtype subtype) {
258        synchronized (mTypedSubtypeHistory) {
259            if (subtype == null) {
260                Slog.w(TAG, "Invalid InputMethodSubtype: " + imi.getId() + ", " + subtype);
261                return;
262            }
263            if (DEBUG) {
264                Slog.d(TAG, "onCommitText: " + imi.getId() + ", " + subtype);
265            }
266            if (REQUIRE_SWITCHING_SUPPORT) {
267                if (!imi.supportsSwitchingToNextInputMethod()) {
268                    Slog.w(TAG, imi.getId() + " doesn't support switching to next input method.");
269                    return;
270                }
271            }
272            if (mTypedSubtypeHistory.size() >= MAX_HISTORY_SIZE) {
273                mTypedSubtypeHistory.poll();
274            }
275            mTypedSubtypeHistory.addFirst(new SubtypeParams(imi, subtype));
276        }
277    }
278
279    public void resetCircularListLocked(Context context) {
280        synchronized(mLock) {
281            mSubtypeList = new InputMethodAndSubtypeCircularList(context, mSettings);
282        }
283    }
284
285    public ImeSubtypeListItem getNextInputMethod(
286            boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
287        synchronized(mLock) {
288            return mSubtypeList.getNextInputMethod(onlyCurrentIme, imi, subtype);
289        }
290    }
291
292    public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes,
293            boolean inputShown, boolean isScreenLocked) {
294        synchronized(mLock) {
295            return mSubtypeList.getSortedInputMethodAndSubtypeList(
296                    showSubtypes, inputShown, isScreenLocked);
297        }
298    }
299}
300