InputMethodSubtypeSwitchingController.java revision d1da11529813f6d3f51518b5fe028e0b8084f5cc
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.annotations.VisibleForTesting;
20import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
21
22import android.content.Context;
23import android.content.pm.PackageManager;
24import android.text.TextUtils;
25import android.util.Slog;
26import android.view.inputmethod.InputMethodInfo;
27import android.view.inputmethod.InputMethodSubtype;
28
29import java.util.ArrayDeque;
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 */
42public class InputMethodSubtypeSwitchingController {
43    private static final String TAG = InputMethodSubtypeSwitchingController.class.getSimpleName();
44    private static final boolean DEBUG = false;
45    // TODO: Turn on this flag and add CTS when the platform starts expecting that all IMEs return
46    // true for supportsSwitchingToNextInputMethod().
47    private static final boolean REQUIRE_SWITCHING_SUPPORT = false;
48    private static final int MAX_HISTORY_SIZE = 4;
49    private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
50
51    private static class SubtypeParams {
52        public final InputMethodInfo mImi;
53        public final InputMethodSubtype mSubtype;
54        public final long mTime;
55
56        public SubtypeParams(InputMethodInfo imi, InputMethodSubtype subtype) {
57            mImi = imi;
58            mSubtype = subtype;
59            mTime = System.currentTimeMillis();
60        }
61    }
62
63    public static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
64        public final CharSequence mImeName;
65        public final CharSequence mSubtypeName;
66        public final InputMethodInfo mImi;
67        public final int mSubtypeId;
68        private final boolean mIsSystemLocale;
69        private final boolean mIsSystemLanguage;
70
71        public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
72                InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
73            mImeName = imeName;
74            mSubtypeName = subtypeName;
75            mImi = imi;
76            mSubtypeId = subtypeId;
77            if (TextUtils.isEmpty(subtypeLocale)) {
78                mIsSystemLocale = false;
79                mIsSystemLanguage = false;
80            } else {
81                mIsSystemLocale = subtypeLocale.equals(systemLocale);
82                mIsSystemLanguage = mIsSystemLocale
83                        || subtypeLocale.startsWith(systemLocale.substring(0, 2));
84            }
85        }
86
87        @Override
88        public int compareTo(ImeSubtypeListItem other) {
89            if (TextUtils.isEmpty(mImeName)) {
90                return 1;
91            }
92            if (TextUtils.isEmpty(other.mImeName)) {
93                return -1;
94            }
95            if (!TextUtils.equals(mImeName, other.mImeName)) {
96                return mImeName.toString().compareTo(other.mImeName.toString());
97            }
98            if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
99                return 0;
100            }
101            if (mIsSystemLocale) {
102                return -1;
103            }
104            if (other.mIsSystemLocale) {
105                return 1;
106            }
107            if (mIsSystemLanguage) {
108                return -1;
109            }
110            if (other.mIsSystemLanguage) {
111                return 1;
112            }
113            if (TextUtils.isEmpty(mSubtypeName)) {
114                return 1;
115            }
116            if (TextUtils.isEmpty(other.mSubtypeName)) {
117                return -1;
118            }
119            return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
120        }
121    }
122
123    private static class InputMethodAndSubtypeList {
124        private final Context mContext;
125        // Used to load label
126        private final PackageManager mPm;
127        private final String mSystemLocaleStr;
128        private final InputMethodSettings mSettings;
129
130        public InputMethodAndSubtypeList(Context context, InputMethodSettings settings) {
131            mContext = context;
132            mSettings = settings;
133            mPm = context.getPackageManager();
134            final Locale locale = context.getResources().getConfiguration().locale;
135            mSystemLocaleStr = locale != null ? locale.toString() : "";
136        }
137
138        private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis =
139                new TreeMap<InputMethodInfo, List<InputMethodSubtype>>(
140                        new Comparator<InputMethodInfo>() {
141                            @Override
142                            public int compare(InputMethodInfo imi1, InputMethodInfo imi2) {
143                                if (imi2 == null)
144                                    return 0;
145                                if (imi1 == null)
146                                    return 1;
147                                if (mPm == null) {
148                                    return imi1.getId().compareTo(imi2.getId());
149                                }
150                                CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId();
151                                CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId();
152                                return imiId1.toString().compareTo(imiId2.toString());
153                            }
154                        });
155
156        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
157            return getSortedInputMethodAndSubtypeList(true, false, false);
158        }
159
160        public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
161                boolean showSubtypes, boolean inputShown, boolean isScreenLocked) {
162            final ArrayList<ImeSubtypeListItem> imList =
163                    new ArrayList<ImeSubtypeListItem>();
164            final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
165                    mSettings.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(
166                            mContext);
167            if (immis == null || immis.size() == 0) {
168                return Collections.emptyList();
169            }
170            mSortedImmis.clear();
171            mSortedImmis.putAll(immis);
172            for (InputMethodInfo imi : mSortedImmis.keySet()) {
173                if (imi == null) {
174                    continue;
175                }
176                List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
177                HashSet<String> enabledSubtypeSet = new HashSet<String>();
178                for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
179                    enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
180                }
181                final CharSequence imeLabel = imi.loadLabel(mPm);
182                if (showSubtypes && enabledSubtypeSet.size() > 0) {
183                    final int subtypeCount = imi.getSubtypeCount();
184                    if (DEBUG) {
185                        Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
186                    }
187                    for (int j = 0; j < subtypeCount; ++j) {
188                        final InputMethodSubtype subtype = imi.getSubtypeAt(j);
189                        final String subtypeHashCode = String.valueOf(subtype.hashCode());
190                        // We show all enabled IMEs and subtypes when an IME is shown.
191                        if (enabledSubtypeSet.contains(subtypeHashCode)
192                                && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) {
193                            final CharSequence subtypeLabel =
194                                    subtype.overridesImplicitlyEnabledSubtype() ? null : subtype
195                                            .getDisplayName(mContext, imi.getPackageName(),
196                                                    imi.getServiceInfo().applicationInfo);
197                            imList.add(new ImeSubtypeListItem(imeLabel,
198                                    subtypeLabel, imi, j, subtype.getLocale(), mSystemLocaleStr));
199
200                            // Removing this subtype from enabledSubtypeSet because we no
201                            // longer need to add an entry of this subtype to imList to avoid
202                            // duplicated entries.
203                            enabledSubtypeSet.remove(subtypeHashCode);
204                        }
205                    }
206                } else {
207                    imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID, null,
208                            mSystemLocaleStr));
209                }
210            }
211            Collections.sort(imList);
212            return imList;
213        }
214    }
215
216    private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>();
217    private final Object mLock = new Object();
218    private final InputMethodSettings mSettings;
219    private InputMethodAndSubtypeList mSubtypeList;
220
221    @VisibleForTesting
222    public static ImeSubtypeListItem getNextInputMethodImpl(List<ImeSubtypeListItem> imList,
223            boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
224        if (imi == null) {
225            return null;
226        }
227        if (imList.size() <= 1) {
228            return null;
229        }
230        final int N = imList.size();
231        final int currentSubtypeId =
232                subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi,
233                        subtype.hashCode()) : NOT_A_SUBTYPE_ID;
234        for (int i = 0; i < N; ++i) {
235            final ImeSubtypeListItem isli = imList.get(i);
236            if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) {
237                if (!onlyCurrentIme) {
238                    return imList.get((i + 1) % N);
239                }
240                for (int j = 0; j < N - 1; ++j) {
241                    final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N);
242                    if (candidate.mImi.equals(imi)) {
243                        return candidate;
244                    }
245                }
246                return null;
247            }
248        }
249        return null;
250    }
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 InputMethodAndSubtypeList(context, mSettings);
282        }
283    }
284
285    public ImeSubtypeListItem getNextInputMethod(
286            boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
287        synchronized(mLock) {
288            return getNextInputMethodImpl(mSubtypeList.getSortedInputMethodAndSubtypeList(),
289                    onlyCurrentIme, imi, subtype);
290        }
291    }
292
293    public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes,
294            boolean inputShown, boolean isScreenLocked) {
295        synchronized(mLock) {
296            return mSubtypeList.getSortedInputMethodAndSubtypeList(
297                    showSubtypes, inputShown, isScreenLocked);
298        }
299    }
300}
301